From b9751fb958523254f5c6bdfe682f059ee3eb5cbd Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Wed, 13 May 2026 10:55:52 +0200 Subject: [PATCH 1/3] feat(AnalyticalTable): add `accessibleName` & `accessibleNameRef` props --- .../AnalyticalTable/AnalyticalTable.cy.tsx | 29 +++++++++++ .../AnalyticalTable/docs/AnalyticalTable.mdx | 13 +++++ .../docs/AnalyticalTable.stories.tsx | 48 +++++++++++++++++++ .../src/components/AnalyticalTable/index.tsx | 7 ++- .../react-table/publicUtils.tsx | 2 +- .../components/AnalyticalTable/types/index.ts | 12 +++++ 6 files changed, 108 insertions(+), 3 deletions(-) diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx b/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx index cf311dfbf55..4963f194da0 100644 --- a/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx +++ b/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx @@ -3159,6 +3159,35 @@ describe('AnalyticalTable', () => { ); }); + it('a11y: accessibleName and accessibleNameRef', () => { + // no aria-labelledby + cy.mount(); + cy.get('[data-component-name="AnalyticalTableContainer"]').should('not.have.attr', 'aria-labelledby'); + cy.get('[data-component-name="AnalyticalTableContainer"]').should('not.have.attr', 'aria-label'); + + // with header: aria-labelledby points to the title bar + cy.mount(); + cy.get('[data-component-name="AnalyticalTableContainer"]') + .should('have.attr', 'aria-labelledby') + .then((labelledby) => { + cy.get(`#${labelledby}`).should('exist'); + }); + + // accessibleName: aria-label on the grid and removes the header connection + cy.mount(); + cy.get('[data-component-name="AnalyticalTableContainer"]').should('have.attr', 'aria-label', 'Financing Details'); + cy.get('[data-component-name="AnalyticalTableContainer"]').should('not.have.attr', 'aria-labelledby'); + + // accessibleNameRef: overrides the header connection + cy.mount( + <> + Custom Table Label + + , + ); + cy.get('[data-component-name="AnalyticalTableContainer"]').should('have.attr', 'aria-labelledby', 'custom-label'); + }); + it("Expandable: don't scroll when expanded/collapsed", () => { const TestComp = () => { const tableInstanceRef = useRef<{ toggleRowExpanded?: (e: string) => void }>({}); diff --git a/packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.mdx b/packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.mdx index d35fb3be91a..e3740f588f9 100644 --- a/packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.mdx +++ b/packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.mdx @@ -549,6 +549,19 @@ function ContextMenuExample() { +## Accessibility + +This example demonstrates the recommended accessibility configuration for the `AnalyticalTable`: + +- **`accessibleName`**: Sets a concise `aria-label` on the table grid, giving screen readers a meaningful table name. +- **`accessibleNameRef`**: References the ID of an external labelling element via `aria-labelledby`. When either `accessibleName` or `accessibleNameRef` is set, the automatic connection to the `header` prop is removed. +- **`headerLabel`** (column option): Provides a screen-reader-accessible label for column headers that have no textual content. +- **`cellLabel`** (column option): Returns a descriptive `aria-label` for cells whose visual content is not self-explanatory. + +The example also includes the [useAnnounceEmptyCells](?path=/docs/data-display-analyticaltable-plugin-hooks--docs#announce-empty-cells) plugin hook, which adds explicit empty-cell announcements for screen readers that do not detect them on their own. As this could lead to duplicate screen reader announcement, use with caution. + + + ## Kitchen Sink A comprehensive example combining many AnalyticalTable features: sorting, filtering, grouping, custom cells, row and navigation highlighting, infinite scrolling, column reordering, vertical alignment, `scaleWidthModeOptions` for custom renderers, `retainColumnWidth`, `sortDescFirst`, and more. diff --git a/packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.stories.tsx b/packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.stories.tsx index c81a5b48860..a482057c57a 100644 --- a/packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.stories.tsx +++ b/packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.stories.tsx @@ -38,6 +38,7 @@ import { FlexBox } from '../../FlexBox/index.js'; import { ObjectStatus } from '../../ObjectStatus/index.js'; import type { AnalyticalTableColumnDefinition, AnalyticalTablePropTypes } from '../index.js'; import { AnalyticalTable } from '../index.js'; +import { useAnnounceEmptyCells } from '../pluginHooks/AnalyticalTableHooks.js'; const kitchenSinkArgs: AnalyticalTablePropTypes = { data: dataLarge, @@ -680,6 +681,53 @@ export const ContextMenu: Story = { }, }; +export const Accessibility: Story = { + render() { + const tableHooks = useMemo(() => [useAnnounceEmptyCells], []); + const columns = useMemo( + () => [ + { + Header: 'Name', + accessor: 'name', + }, + { + Header: 'Age', + accessor: 'age', + hAlign: 'End', + }, + { + Header: '', + headerLabel: 'Actions', + id: 'actions', + disableFilters: true, + disableSortBy: true, + disableGroupBy: true, + disableDragAndDrop: true, + Cell: () => ( + +