From 966ce79561686b0df87c24b81b332c144b93b1f0 Mon Sep 17 00:00:00 2001 From: AK <144495202+AKnassa@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:16:12 -0400 Subject: [PATCH] fix(Table): name the row-status column header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit axe empty-table-header (minor): the row-status gutter column rendered a blank , so AT users got a nameless column header. The header now carries VisuallyHidden text from a new i18n key (@astryx.table.rowStatus.columnHeader, default 'Row status') — i18n-key-only override like the selection/rowExpansion plugin strings, no new config prop. Tests pin both the accessible name and that it stays visually hidden. Baseline: -2 entries. Verified: 9/9 plugin tests + i18n catalog guard, eslint clean, axe audit 0 issues on both TableRowStatus stories with --fail-on-new. Part of #4681. --- .changeset/a11y-table-row-status-header.md | 6 ++++++ .github/a11y-baseline.json | 10 ---------- packages/core/locales/en.json | 4 ++++ .../rowStatus/useTableRowStatus.test.tsx | 20 ++++++++++++------- .../plugins/rowStatus/useTableRowStatus.tsx | 15 +++++++++++--- .../core/src/Table/useTableRowStatus.doc.mjs | 2 +- 6 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 .changeset/a11y-table-row-status-header.md diff --git a/.changeset/a11y-table-row-status-header.md b/.changeset/a11y-table-row-status-header.md new file mode 100644 index 000000000000..101cf8eaf400 --- /dev/null +++ b/.changeset/a11y-table-row-status-header.md @@ -0,0 +1,6 @@ +--- +'@astryxdesign/core': patch +--- + +[fix] Table useTableRowStatus: the status gutter's column header now carries a visually hidden localized name ("Row status", key `@astryx.table.rowStatus.columnHeader`) instead of an empty `` (axe empty-table-header, WCAG 1.3.1 best practice). The gutter stays visually blank. +@AKnassa diff --git a/.github/a11y-baseline.json b/.github/a11y-baseline.json index 84e9c710ad67..bfcba98d880d 100644 --- a/.github/a11y-baseline.json +++ b/.github/a11y-baseline.json @@ -1008,16 +1008,6 @@ "impact": "moderate", "helpUrl": "https://dequeuniversity.com/rules/axe/4.12/landmark-unique?application=playwright" }, - { - "key": "TableRowStatus::Default::empty-table-header", - "impact": "minor", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.12/empty-table-header?application=playwright" - }, - { - "key": "TableRowStatus::Raw Colors::empty-table-header", - "impact": "minor", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.12/empty-table-header?application=playwright" - }, { "key": "TableTree::Default::aria-conditional-attr", "impact": "serious", diff --git a/packages/core/locales/en.json b/packages/core/locales/en.json index 9c4b6111001d..3264f8bab02f 100644 --- a/packages/core/locales/en.json +++ b/packages/core/locales/en.json @@ -463,6 +463,10 @@ "defaultMessage": "Apply", "description": "Primary button label inside a table's filter panel/popover; commits pending filter values. Imperative verb; pairs with `Reset`." }, + "@astryx.table.rowStatus.columnHeader": { + "defaultMessage": "Row status", + "description": "Screen-reader-only column header for the narrow status-indicator gutter a Table gains from useTableRowStatus. Sighted users see a blank gutter; assistive tech announces this as the column name." + }, "@astryx.table.selection.selectAllRows": { "defaultMessage": "Select all rows", "description": "Aria label for the \"select all rows\" checkbox in a Table header." diff --git a/packages/core/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx b/packages/core/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx index 7f268b3ab0ea..999e32a36b9b 100644 --- a/packages/core/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +++ b/packages/core/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx @@ -44,12 +44,18 @@ function Harness({ } describe('useTableRowStatus', () => { - it('prepends a narrow status column with an empty header', () => { + it('names the status column header for assistive technology', () => { render(); - const headers = screen.getAllByRole('columnheader'); - // Status column is first; its header is empty. - expect(headers[0]).toHaveAttribute('data-column-key', '__rowStatus'); - expect(headers[0].textContent).toBe(''); + // The gutter looks blank but its th carries a visually hidden name. + const header = screen.getByRole('columnheader', {name: 'Row status'}); + expect(header).toHaveAttribute('data-column-key', '__rowStatus'); + // Status column is first. + expect(screen.getAllByRole('columnheader')[0]).toBe(header); + // The name must come from the clipped VisuallyHidden span — bare th text + // would be a visible header on what should stay a blank gutter. + const hiddenText = within(header).getByText('Row status'); + expect(hiddenText.tagName).toBe('SPAN'); + expect(hiddenText.className).not.toBe(''); }); it('renders a labeled dot for rows with a status', () => { @@ -131,8 +137,8 @@ describe('useTableRowStatus', () => { it('renders the status header with empty data and no indicators', () => { render(); - const headers = screen.getAllByRole('columnheader'); - expect(headers[0]).toHaveAttribute('data-column-key', '__rowStatus'); + const header = screen.getByRole('columnheader', {name: 'Row status'}); + expect(header).toHaveAttribute('data-column-key', '__rowStatus'); expect(screen.queryByRole('img')).not.toBeInTheDocument(); }); }); diff --git a/packages/core/src/Table/plugins/rowStatus/useTableRowStatus.tsx b/packages/core/src/Table/plugins/rowStatus/useTableRowStatus.tsx index 870c0d41916e..d652ad4bc4e9 100644 --- a/packages/core/src/Table/plugins/rowStatus/useTableRowStatus.tsx +++ b/packages/core/src/Table/plugins/rowStatus/useTableRowStatus.tsx @@ -4,7 +4,7 @@ /** * @file useTableRowStatus.tsx - * @input React, StyleX, Icon, Table types + * @input React, StyleX, Icon, i18n, VisuallyHidden, Table types * @output Exports useTableRowStatus hook + config type * @position Row-status plugin; consumed by Table via plugins prop * @@ -16,6 +16,8 @@ import {useMemo} from 'react'; import * as stylex from '@stylexjs/stylex'; import {Icon, type IconColor, type IconName} from '../../../Icon'; import {Tooltip} from '../../../Tooltip'; +import {useTranslator} from '../../../i18n'; +import {VisuallyHidden} from '../../../VisuallyHidden'; import type {TableColumn, TablePlugin} from '../../types'; /** @@ -142,6 +144,7 @@ const styles = stylex.create({ export function useTableRowStatus>( config: UseTableRowStatusConfig, ): TablePlugin { + const t = useTranslator(); const {getStatus} = config; return useMemo( @@ -149,7 +152,13 @@ export function useTableRowStatus>( transformColumns(columns) { const statusColumn: TableColumn = { key: '__rowStatus', - header: '', + // The gutter stays visually blank, but the th needs a discernible + // name for assistive technology (axe: empty-table-header). + header: ( + + {t('@astryx.table.rowStatus.columnHeader')} + + ), width: STATUS_COLUMN_WIDTH, resizable: false, renderCell: (item: T) => { @@ -184,6 +193,6 @@ export function useTableRowStatus>( return [statusColumn, ...columns]; }, }), - [getStatus], + [getStatus, t], ); } diff --git a/packages/core/src/Table/useTableRowStatus.doc.mjs b/packages/core/src/Table/useTableRowStatus.doc.mjs index 49c61da6079d..83f57509d7f7 100644 --- a/packages/core/src/Table/useTableRowStatus.doc.mjs +++ b/packages/core/src/Table/useTableRowStatus.doc.mjs @@ -7,7 +7,7 @@ export const docs = { subComponentOf: 'Table', displayName: 'useTableRowStatus', description: - 'Hook that returns a TablePlugin which prepends a narrow column signaling per-row status: a colored status dot by default, or an icon when provided (shape + color is more accessible than color alone). getStatus maps a row to a semantic color (mapped to a theme token) or raw CSS color, an optional icon, and a required accessible label (shown in a tooltip on hover and announced to assistive technology, so status is never color-only); return null for no indicator. Memoize getStatus with useCallback for a stable plugin identity.', + 'Hook that returns a TablePlugin which prepends a narrow column signaling per-row status: a colored status dot by default, or an icon when provided (shape + color is more accessible than color alone). getStatus maps a row to a semantic color (mapped to a theme token) or raw CSS color, an optional icon, and a required accessible label (shown in a tooltip on hover and announced to assistive technology, so status is never color-only); return null for no indicator. The column header is visually blank but carries a screen-reader-only localized name ("Row status", i18n key @astryx.table.rowStatus.columnHeader). Memoize getStatus with useCallback for a stable plugin identity.', props: [ { name: 'getStatus',