Commit 1af73caf45 for woocommerce
commit 1af73caf45abe14fb714e907e4304b162aead5c7
Author: Thomas Roberts <5656702+opr@users.noreply.github.com>
Date: Tue Feb 3 16:18:00 2026 +0000
Move product sale badge alongside individual prices in cart and update design (#63012)
* Make badge use theme colour as background, and small font size
* Prevent discount showing twice on mini cart and line prices vertically with button
* Add changelog
* Only show individual save badge if quantity is 1 in cart block
* Remove old badge after merge
* Add state for single item discount badge visibility
* allow badge onto second line on Checkout order summary if small
* Move mini-cart badge back in line after merge conflict
* Add sale badges to checkout order summary
* Make remove button use theme colour
* Align price content centrally
* Don't hide sale badge on mobile
* Align items in checkout order summary
* Extract sale amount calculation to shared util
* Only show quantity on right under item total price
* Remove low stock badge from cart line items
* Remove individual saving and low stock badge in mini cart
* Remove low stock badge and sale badge from checkout order summary
* Reduce spacing under title
* Lint fix
diff --git a/plugins/woocommerce/changelog/wooprd-1450-saved-amount-badges b/plugins/woocommerce/changelog/wooprd-1450-saved-amount-badges
new file mode 100644
index 0000000000..21cc7f4f06
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooprd-1450-saved-amount-badges
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Move the "Save $X" badges in Cart/Mini-cart alongside the individual item prices to save vertical space.
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
index d7acc27190..a15efeb3ac 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
@@ -23,13 +23,13 @@ import type { CartItem } from '@woocommerce/types';
import { objectHasProp, Currency } from '@woocommerce/types';
import { getSetting } from '@woocommerce/settings';
import { Icon, trash } from '@wordpress/icons';
+import { calculateSaleAmount } from '@woocommerce/base-utils';
/**
* Internal dependencies
*/
import ProductBackorderBadge from '../product-backorder-badge';
import ProductImage from '../product-image';
-import ProductLowStockBadge from '../product-low-stock-badge';
import ProductMetadata from '../product-metadata';
import ProductSaleBadge from '../product-sale-badge';
@@ -68,7 +68,6 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
catalog_visibility: catalogVisibility = 'visible',
short_description: shortDescription = '',
description: fullDescription = '',
- low_stock_remaining: lowStockRemaining = null,
show_backorder_badge: showBackorderBadge = false,
quantity_limits: quantityLimits = {
minimum: 1,
@@ -146,9 +145,10 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
amount: parseInt( prices.raw_prices.price, 10 ),
precision: prices.raw_prices.precision,
} );
- const saleAmountSingle =
- regularAmountSingle.subtract( purchaseAmountSingle );
- const saleAmount = saleAmountSingle.multiply( quantity );
+ const saleAmountSingle = calculateSaleAmount(
+ prices,
+ priceCurrency.minorUnit
+ );
const totalsCurrency = getCurrencyFromPriceResponse( totals );
let lineSubtotal = parseInt( totals.line_subtotal, 10 );
if ( getSetting( 'displayCartPricesIncludingTax', false ) ) {
@@ -245,15 +245,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
name={ name }
permalink={ permalink }
/>
- { showBackorderBadge ? (
- <ProductBackorderBadge />
- ) : (
- !! lowStockRemaining && (
- <ProductLowStockBadge
- lowStockRemaining={ lowStockRemaining }
- />
- )
- ) }
+ { showBackorderBadge && <ProductBackorderBadge /> }
<div className="wc-block-cart-item__prices">
<ProductPrice
@@ -270,15 +262,6 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
/>
</div>
- <ProductSaleBadge
- currency={ priceCurrency }
- saleAmount={ getAmountFromRawPrice(
- saleAmountSingle,
- priceCurrency
- ) }
- format={ saleBadgePriceFormat }
- />
-
<ProductMetadata
shortDescription={ shortDescription }
fullDescription={ fullDescription }
@@ -356,16 +339,11 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
price={ subtotalPrice.getAmount() }
/>
- { quantity > 1 && (
- <ProductSaleBadge
- currency={ priceCurrency }
- saleAmount={ getAmountFromRawPrice(
- saleAmount,
- priceCurrency
- ) }
- format={ saleBadgePriceFormat }
- />
- ) }
+ <ProductSaleBadge
+ currency={ priceCurrency }
+ saleAmount={ saleAmountSingle * quantity }
+ format={ saleBadgePriceFormat }
+ />
</div>
</td>
</tr>
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/style.scss b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/style.scss
index d95c773fd9..b759f53b80 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/style.scss
@@ -45,6 +45,24 @@ table.wc-block-cart-items {
}
.wc-block-cart-item__prices {
@include font-small-locked;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ gap: $gap-smaller;
+ align-items: center;
+
+ @include cart-checkout-mobile-container {
+ flex-wrap: wrap;
+ }
+
+ .price {
+ display: flex;
+ align-items: center;
+
+ &[hidden] {
+ display: none;
+ }
+ }
}
.wc-block-cart-item__quantity {
@@ -64,6 +82,10 @@ table.wc-block-cart-items {
&[hidden] {
display: none;
}
+
+ svg {
+ fill: currentColor;
+ }
}
}
.wc-block-components-product-name {
@@ -71,6 +93,8 @@ table.wc-block-cart-items {
display: block;
max-width: max-content;
line-height: 1.4;
+ margin-bottom: 5px;
+
&[hidden] {
display: none;
}
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/order-summary-item.tsx b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/order-summary-item.tsx
index 5908ba9944..367153919c 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/order-summary-item.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/order-summary-item.tsx
@@ -19,14 +19,15 @@ import { getSetting } from '@woocommerce/settings';
import { useMemo } from '@wordpress/element';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { CartItem, isString } from '@woocommerce/types';
+import { calculateSaleAmount } from '@woocommerce/base-utils';
/**
* Internal dependencies
*/
import ProductBackorderBadge from '../product-backorder-badge';
import ProductImage from '../product-image';
-import ProductLowStockBadge from '../product-low-stock-badge';
import ProductMetadata from '../product-metadata';
+import ProductSaleBadge from '../product-sale-badge';
interface OrderSummaryProps {
cartItem: CartItem;
@@ -39,7 +40,6 @@ const OrderSummaryItem = ( {
}: OrderSummaryProps ): JSX.Element => {
const {
images,
- low_stock_remaining: lowStockRemaining,
show_backorder_badge: showBackorderBadge,
name: initialName,
permalink,
@@ -102,6 +102,12 @@ const OrderSummaryItem = ( {
amount: lineSubtotal,
precision: totalsCurrency.minorUnit,
} ).getAmount();
+
+ const saleAmountSingle = calculateSaleAmount(
+ prices,
+ priceCurrency.minorUnit
+ );
+
const subtotalPriceFormat = applyCheckoutFilter( {
filterName: 'subtotalPriceFormat',
defaultValue: '<price/>',
@@ -110,6 +116,14 @@ const OrderSummaryItem = ( {
validation: productPriceValidation,
} );
+ const saleBadgePriceFormat = applyCheckoutFilter( {
+ filterName: 'saleBadgePriceFormat',
+ defaultValue: '<price/>',
+ extensions,
+ arg,
+ validation: productPriceValidation,
+ } );
+
// Allow extensions to filter how the price is displayed. Ie: prepending or appending some values.
const productPriceFormat = applyCheckoutFilter( {
filterName: 'cartItemPrice',
@@ -175,24 +189,18 @@ const OrderSummaryItem = ( {
permalink={ permalink }
disabledTagName="h3"
/>
- <ProductPrice
- currency={ priceCurrency }
- price={ priceSingle }
- regularPrice={ regularPriceSingle }
- className="wc-block-components-order-summary-item__individual-prices"
- priceClassName="wc-block-components-order-summary-item__individual-price"
- regularPriceClassName="wc-block-components-order-summary-item__regular-individual-price"
- format={ subtotalPriceFormat }
- />
- { showBackorderBadge ? (
- <ProductBackorderBadge />
- ) : (
- !! lowStockRemaining && (
- <ProductLowStockBadge
- lowStockRemaining={ lowStockRemaining }
- />
- )
- ) }
+ <div className="wc-block-cart-item__prices">
+ <ProductPrice
+ currency={ priceCurrency }
+ price={ priceSingle }
+ regularPrice={ regularPriceSingle }
+ className="wc-block-components-order-summary-item__individual-prices"
+ priceClassName="wc-block-components-order-summary-item__individual-price"
+ regularPriceClassName="wc-block-components-order-summary-item__regular-individual-price"
+ format={ subtotalPriceFormat }
+ />
+ </div>
+ { showBackorderBadge && <ProductBackorderBadge /> }
<ProductMetadata { ...productMetaProps } />
</div>
<span className="screen-reader-text">
@@ -213,11 +221,18 @@ const OrderSummaryItem = ( {
className="wc-block-components-order-summary-item__total-price"
aria-hidden="true"
>
- <ProductPrice
- currency={ totalsCurrency }
- format={ productPriceFormat }
- price={ subtotalPrice }
- />
+ <div className="wc-block-cart-item__total-price-and-sale-badge-wrapper">
+ <ProductPrice
+ currency={ totalsCurrency }
+ format={ productPriceFormat }
+ price={ subtotalPrice }
+ />
+ <ProductSaleBadge
+ currency={ priceCurrency }
+ saleAmount={ saleAmountSingle * quantity }
+ format={ saleBadgePriceFormat }
+ />
+ </div>
</div>
</div>
);
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/style.scss b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/style.scss
index 4631205628..d3bd823257 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/style.scss
@@ -98,6 +98,35 @@
}
}
+ .wc-block-cart-item__prices {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: $gap-smaller;
+ align-items: center;
+ }
+
+ .wc-block-components-order-summary-item__individual-prices {
+ del {
+ margin-right: 0.5em;
+ }
+
+ ins {
+ display: inline-block;
+ margin-left: 0;
+ }
+ }
+
+ .wc-block-cart-item__total-price-and-sale-badge-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+
+ .wc-block-components-sale-badge {
+ margin-top: $gap-smallest;
+ }
+ }
+
.wc-block-components-order-summary-item__total-price {
margin-left: auto;
text-align: right;
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/product-badge/style.scss b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/product-badge/style.scss
index 1a8021d74f..a49e29e11f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/product-badge/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/product-badge/style.scss
@@ -1,11 +1,8 @@
.wc-block-components-product-badge {
- @include font-size(smaller);
- border-radius: $universal-border-radius;
- border: 1px solid;
+ @include font-x-small-locked;
display: inline-block;
- font-weight: 600;
- padding: 0 0.66em;
- text-transform: uppercase;
+ background: color-mix(in srgb, currentColor 10%, transparent);
+ padding: 2px 4px;
white-space: nowrap;
&[hidden] {
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/utils/calculate-sale-amount.ts b/plugins/woocommerce/client/blocks/assets/js/base/utils/calculate-sale-amount.ts
new file mode 100644
index 0000000000..74ec6dae5e
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/base/utils/calculate-sale-amount.ts
@@ -0,0 +1,37 @@
+/**
+ * External dependencies
+ */
+import Dinero from 'dinero.js';
+import type { CartItem } from '@woocommerce/types';
+
+/**
+ * Calculate the per-unit sale amount from raw prices.
+ *
+ * @param prices Cart item prices containing raw_prices.
+ * @param targetPrecision The target currency minor unit precision.
+ * @return Per-unit sale amount as a number, or 0 if no discount.
+ */
+export function calculateSaleAmount(
+ prices: CartItem[ 'prices' ],
+ targetPrecision: number
+): number {
+ const rawPrecision =
+ typeof prices.raw_prices.precision === 'string'
+ ? parseInt( prices.raw_prices.precision, 10 )
+ : prices.raw_prices.precision;
+
+ const regular = Dinero( {
+ amount: parseInt( prices.raw_prices.regular_price, 10 ),
+ precision: rawPrecision,
+ } );
+
+ const purchase = Dinero( {
+ amount: parseInt( prices.raw_prices.price, 10 ),
+ precision: rawPrecision,
+ } );
+
+ return regular
+ .subtract( purchase )
+ .convertPrecision( targetPrecision )
+ .getAmount();
+}
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/utils/index.js b/plugins/woocommerce/client/blocks/assets/js/base/utils/index.js
index deefaf74e5..d2a6219304 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/utils/index.js
+++ b/plugins/woocommerce/client/blocks/assets/js/base/utils/index.js
@@ -1,5 +1,6 @@
export * from './errors';
export * from './address';
+export * from './calculate-sale-amount';
export * from './shipping-rates';
export * from './legacy-events';
export * from './render-frontend';
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/cart/style.scss
index a9f89521af..65e0522f56 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart/style.scss
@@ -105,12 +105,6 @@
}
@include cart-checkout-below-large-container {
- .wc-block-cart-item__total {
- .wc-block-components-sale-badge {
- display: none;
- }
- }
-
.wc-block-cart {
.wc-block-components-sidebar {
.wc-block-cart__totals-title {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts
index e804695bd3..a6ac7069e0 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts
@@ -45,7 +45,6 @@ const {
increaseQuantityLabel,
quantityDescriptionLabel,
removeFromCartLabel,
- lowInStockLabel,
} = getConfig( 'woocommerce/mini-cart-products-table-block' );
const { itemsInCartTextTemplate } = getConfig(
'woocommerce/mini-cart-title-items-counter-block'
@@ -410,45 +409,6 @@ const { state: cartItemState } = store(
);
},
- get cartItemDiscount(): string {
- const { extensions } = cartItemState.cartItem;
-
- const discountPrice =
- cartItemState.regularAmountSingle -
- cartItemState.purchaseAmountSingle;
-
- const price = formatPriceWithCurrency(
- discountPrice,
- cartItemState.currency
- );
-
- // TODO: Add deprecation notice urging to replace with a
- // `data-wp-text` directive or an alternative solution.
- if (
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- ( window.wc as any )?.blocksCheckout?.applyCheckoutFilter
- ) {
- const priceText =
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- ( window.wc as any ).blocksCheckout.applyCheckoutFilter(
- {
- filterName: 'saleBadgePriceFormat',
- defaultValue: '<price/>',
- extensions,
- arg: {
- context: 'cart',
- cartItem: cartItemState.cartItem,
- cart: woocommerceState.cart,
- },
- }
- );
-
- return priceText.replace( '<price/>', price );
- }
-
- return price;
- },
-
get lineItemDiscount(): string {
const { quantity, extensions } = cartItemState.cartItem;
@@ -717,13 +677,6 @@ const { state: cartItemState } = store(
return price;
},
- get isLineItemTotalDiscountVisible(): boolean {
- return (
- cartItemState.cartItemHasDiscount &&
- cartItemState.cartItem.quantity > 1
- );
- },
-
get isProductHiddenFromCatalog(): boolean {
const context = getContext< { isImageHidden: boolean } >();
const { catalog_visibility: catalogVisibility } =
@@ -735,20 +688,6 @@ const { state: cartItemState } = store(
);
},
- get isLowInStockVisible(): boolean {
- return (
- ! cartItemState.cartItem.show_backorder_badge &&
- !! cartItemState.cartItem.low_stock_remaining
- );
- },
-
- get lowInStockLabel(): string {
- return lowInStockLabel.replace(
- '%d',
- cartItemState.cartItem.low_stock_remaining
- );
- },
-
get itemShowRemoveItemLink(): boolean {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return ( window.wc as any )?.blocksCheckout?.applyCheckoutFilter
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCartProductsTableBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCartProductsTableBlock.php
index 52607b5924..050c626ca4 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCartProductsTableBlock.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCartProductsTableBlock.php
@@ -61,16 +61,11 @@ class MiniCartProductsTableBlock extends AbstractInnerBlock {
/* translators: %s is the discount amount. */
$save_format = __( 'Save %s', 'woocommerce' );
- $cart_item_discount_span = '<span data-wp-text="state.cartItemDiscount" class="wc-block-formatted-money-amount wc-block-components-formatted-money-amount"></span>';
- $cart_item_save_badge = sprintf( $save_format, $cart_item_discount_span );
$line_item_discount_span = '<span data-wp-text="state.lineItemDiscount" class="wc-block-formatted-money-amount wc-block-components-formatted-money-amount"></span>';
$line_item_save_badge = sprintf( $save_format, $line_item_discount_span );
$available_on_backorder_label = __( 'Available on backorder', 'woocommerce' );
- /* translators: %d stock amount (number of items in stock for product) */
- $low_in_stock_label = __( '%d left in stock', 'woocommerce' );
-
wp_interactivity_config(
$this->get_full_block_name(),
array(
@@ -78,7 +73,6 @@ class MiniCartProductsTableBlock extends AbstractInnerBlock {
'increaseQuantityLabel' => $increase_quantity_label,
'quantityDescriptionLabel' => $quantity_description_label,
'removeFromCartLabel' => $remove_from_cart_label,
- 'lowInStockLabel' => $low_in_stock_label,
)
);
@@ -145,12 +139,6 @@ class MiniCartProductsTableBlock extends AbstractInnerBlock {
<div data-wp-bind--hidden="!state.cartItem.show_backorder_badge" class="wc-block-components-product-badge wc-block-components-product-backorder-badge">
<?php echo esc_html( $available_on_backorder_label ); ?>
</div>
- <div
- class="wc-block-components-product-badge wc-block-components-product-low-stock-badge"
- data-wp-bind--hidden="!state.isLowInStockVisible"
- data-wp-text="state.lowInStockLabel"
- >
- </div>
<div class="wc-block-cart-item__prices">
<span data-wp-bind--hidden="!state.cartItemHasDiscount" class="price wc-block-components-product-price">
<span data-wp-text="state.beforeItemPrice"></span>
@@ -171,22 +159,6 @@ class MiniCartProductsTableBlock extends AbstractInnerBlock {
<span data-wp-text="state.afterItemPrice"></span>
</span>
</div>
- <div
- data-wp-bind--hidden="!state.cartItemHasDiscount"
- class="wc-block-components-product-badge wc-block-components-sale-badge"
- >
- <?php
- echo wp_kses(
- $cart_item_save_badge,
- array(
- 'span' => array(
- 'data-wp-text' => true,
- 'class' => true,
- ),
- )
- );
- ?>
- </div>
<div class="wc-block-components-product-metadata">
<div data-wp-watch="callbacks.itemShortDescription" >
<div class="wc-block-components-product-metadata__description"></div>
@@ -247,7 +219,7 @@ class MiniCartProductsTableBlock extends AbstractInnerBlock {
</span>
</span>
<div
- data-wp-bind--hidden="!state.isLineItemTotalDiscountVisible"
+ data-wp-bind--hidden="!state.cartItemHasDiscount"
class="wc-block-components-product-badge wc-block-components-sale-badge"
>
<?php