Commit c17497e02c5 for woocommerce
commit c17497e02c51c32764e9ea96c869841545a35b1d
Author: Mike Jolley <mike.jolley@me.com>
Date: Mon Jun 22 17:36:13 2026 +0100
Fix invalid ARIA on cart line items in responsive grid layouts (#65689)
* Fix invalid ARIA on cart line items in responsive grid layouts
The responsive grid and mini-cart layouts set `display: grid` on each
cart row, which strips the row's implicit table-row role and orphaned the
`rowheader` cell added in #65364. Add an explicit `role="row"` so the
rowheader keeps a valid row context in every layout (a no-op on the
desktop table). Also give the product cell a concise `aria-label` so the
row header announces the product name instead of the whole cell (prices,
metadata, quantity selector and remove button).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Keep cart line item cells aligned with their column headers
Each body row's thumbnail cell was `aria-hidden` when the product image had
no alt text, leaving two accessible cells against three column headers. That
slid the Total cell's header onto the adjacent "Details" header for screen
readers.
Keep all three column headers (with the "Details" header visually hidden via
.screen-reader-text but present in the accessibility tree), and keep the
thumbnail cell in the tree by relying on its enforced alt (the product name
via fallbackAlt) instead of aria-hiding it. The row now always has three
cells that line up with the three headers, so each cell is announced under
the correct column.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Update cart editor test for thumbnail product link
* Fix cart line items table spacing
Remove unnecessary cart table edge padding while preserving the thumbnail-to-details gap and the existing header markup for compatibility.
Co-authored-by: yjailin <yoann.jailin@automattic.com>
* Hide decorative cart image column from screen readers
Addresses a11y review feedback: decode product name entities for screen-reader strings, hide the decorative image column (header + cells) so screen readers see a consistent two-column table instead of an empty "Product" cell, and shorten the changelog entry.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: yjailin <yoann.jailin@automattic.com>
diff --git a/plugins/woocommerce/changelog/wooplug-6831-cart-rowheader-grid-aria b/plugins/woocommerce/changelog/wooplug-6831-cart-rowheader-grid-aria
new file mode 100644
index 00000000000..74187f3c01b
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-6831-cart-rowheader-grid-aria
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Improve cart table accessibility and spacing so screen readers announce line items as rows with the correct column headers.
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 ab415116f3b..29e1ed34f61 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
@@ -3,6 +3,7 @@
*/
import clsx from 'clsx';
import { __, sprintf } from '@wordpress/i18n';
+import { decodeEntities } from '@wordpress/html-entities';
import { speak } from '@wordpress/a11y';
import QuantitySelector from '@woocommerce/base-components/quantity-selector';
import ProductPrice from '@woocommerce/base-components/product-price';
@@ -20,7 +21,7 @@ import {
} from '@woocommerce/blocks-checkout';
import { forwardRef, useMemo } from '@wordpress/element';
import type { CartItem } from '@woocommerce/types';
-import { isBoolean, objectHasProp, Currency } from '@woocommerce/types';
+import { isBoolean, Currency } from '@woocommerce/types';
import { getSetting, getSettingWithCoercion } from '@woocommerce/settings';
import { Icon, trash } from '@wordpress/icons';
import { calculateSaleAmount } from '@woocommerce/base-utils';
@@ -163,6 +164,8 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
extensions,
arg,
} );
+ // `name` is a raw HTML string; decode entities for screen-reader text (aria-label, speak).
+ const decodedName = decodeEntities( name );
const regularAmountSingle = dinero( {
amount: parseInt( prices.raw_prices.regular_price, 10 ),
@@ -234,6 +237,8 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
return (
<tr
+ // Restores the row role that `display: grid` strips in the responsive layout.
+ role="row"
data-cart-item-key={ lineItem.key }
className={ clsx(
'wc-block-cart-items__row',
@@ -245,14 +250,8 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
ref={ ref }
tabIndex={ tabIndex }
>
- { /* If the image has no alt text, this link is unnecessary and can be hidden. */ }
- <td
- className="wc-block-cart-item__image"
- aria-hidden={
- ! objectHasProp( firstImage, 'alt' ) || ! firstImage.alt
- }
- >
- { /* We don't need to make it focusable, because product name has the same link. */ }
+ { /* Decorative image, hidden from screen readers so the row isn't announced as an empty "Product" cell. */ }
+ <td className="wc-block-cart-item__image" aria-hidden="true">
{ isProductHiddenFromCatalog ? (
<ProductImage
image={ firstImage }
@@ -271,7 +270,12 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
</a>
) }
</td>
- <td role="rowheader" className="wc-block-cart-item__product">
+ <td
+ role="rowheader"
+ // Name the rowheader after the product only, not the whole cell's contents.
+ aria-label={ decodedName }
+ className="wc-block-cart-item__product"
+ >
<div className="wc-block-cart-item__wrap">
<ProductName
disabled={
@@ -323,7 +327,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
}
);
} }
- itemName={ name }
+ itemName={ decodedName }
/>
) }
{ showRemoveItemLink && (
@@ -335,7 +339,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
'Remove %s from cart',
'woocommerce'
),
- name
+ decodedName
) }
onClick={ () => {
onRemove();
@@ -354,7 +358,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
'%s has been removed from your cart.',
'woocommerce'
),
- name
+ decodedName
)
);
} }
@@ -400,7 +404,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
'%s has been saved for later and removed from your cart.',
'woocommerce'
),
- name
+ decodedName
)
);
} }
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/index.tsx b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/index.tsx
index 9996f6222b2..2bb389e5795 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/index.tsx
@@ -92,8 +92,10 @@ const CartLineItemsTable = ( {
</caption>
<thead>
<tr className="wc-block-cart-items__header">
+ { /* Decorative image column, hidden from screen readers (see cart-line-item-row.tsx). */ }
<th
scope="col"
+ aria-hidden="true"
className="wc-block-cart-items__header-image"
>
<span>{ __( 'Product', 'woocommerce' ) }</span>
@@ -102,7 +104,9 @@ const CartLineItemsTable = ( {
scope="col"
className="wc-block-cart-items__header-product"
>
- <span>{ __( 'Details', 'woocommerce' ) }</span>
+ <span className="screen-reader-text">
+ { __( 'Details', 'woocommerce' ) }
+ </span>
</th>
<th
scope="col"
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 6bf603630d0..7e96d7037d3 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
@@ -19,9 +19,6 @@ table.wc-block-cart-items {
.wc-block-cart-items__header-image {
width: 80px;
}
- .wc-block-cart-items__header-product {
- visibility: hidden;
- }
.wc-block-cart-items__header-total {
width: 100px;
text-align: right;
@@ -160,12 +157,12 @@ table.wc-block-cart-items {
.wc-block-cart-items__row {
display: grid;
grid-template-columns: 80px 132px;
+ column-gap: $gap;
padding: $gap 0;
.wc-block-cart-item__image {
grid-column-start: 1;
grid-row-start: 1;
- padding-right: $gap;
}
.wc-block-cart-item__product {
grid-column-start: 2;
@@ -216,16 +213,17 @@ table.wc-block-cart-items {
th {
padding: $gap-smaller $gap $gap-small 0;
white-space: nowrap;
+ text-align: left;
}
td {
border-top: 1px solid $universal-border-light;
- padding: $gap * 1.25 0 $gap * 1.25 $gap;
+ padding: $gap * 1.25 0;
vertical-align: top;
}
th:last-child {
- padding-right: $gap;
+ padding-right: 0;
}
- td:last-child {
+ .wc-block-cart-item__image {
padding-right: $gap;
}
}