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',
|