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;
 			}
 		}