Skip to content

Commit 2fe532c

Browse files
authored
feat(AnalyticalTable): add accessibleName & accessibleNameRef props (#8592)
Closes #8590
1 parent ca7dc87 commit 2fe532c

6 files changed

Lines changed: 112 additions & 4 deletions

File tree

packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3253,6 +3253,35 @@ describe('AnalyticalTable', () => {
32533253
);
32543254
});
32553255

3256+
it('a11y: accessibleName and accessibleNameRef', () => {
3257+
// no aria-labelledby
3258+
cy.mount(<AnalyticalTable columns={columns} data={data} />);
3259+
cy.get('[data-component-name="AnalyticalTableContainer"]').should('not.have.attr', 'aria-labelledby');
3260+
cy.get('[data-component-name="AnalyticalTableContainer"]').should('not.have.attr', 'aria-label');
3261+
3262+
// with header: aria-labelledby points to the title bar
3263+
cy.mount(<AnalyticalTable columns={columns} data={data} header="Items Table" />);
3264+
cy.get('[data-component-name="AnalyticalTableContainer"]')
3265+
.should('have.attr', 'aria-labelledby')
3266+
.then((labelledby) => {
3267+
cy.get(`[id="${labelledby}"]`).should('exist');
3268+
});
3269+
3270+
// accessibleName: aria-label on the grid and removes the header connection
3271+
cy.mount(<AnalyticalTable columns={columns} data={data} header="Items Table" accessibleName="Financing Details" />);
3272+
cy.get('[data-component-name="AnalyticalTableContainer"]').should('have.attr', 'aria-label', 'Financing Details');
3273+
cy.get('[data-component-name="AnalyticalTableContainer"]').should('not.have.attr', 'aria-labelledby');
3274+
3275+
// accessibleNameRef: overrides the header connection
3276+
cy.mount(
3277+
<>
3278+
<span id="custom-label">Custom Table Label</span>
3279+
<AnalyticalTable columns={columns} data={data} header="Items Table" accessibleNameRef="custom-label" />
3280+
</>,
3281+
);
3282+
cy.get('[data-component-name="AnalyticalTableContainer"]').should('have.attr', 'aria-labelledby', 'custom-label');
3283+
});
3284+
32563285
it("Expandable: don't scroll when expanded/collapsed", () => {
32573286
const TestComp = () => {
32583287
const tableInstanceRef = useRef<{ toggleRowExpanded?: (e: string) => void }>({});

packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.mdx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,22 @@ function ContextMenuExample() {
549549

550550
</details>
551551

552+
## Accessibility
553+
554+
This example demonstrates possible accessibility configuration for the `AnalyticalTable`:
555+
556+
- **`accessibleName`**: Sets a concise `aria-label` on the table grid, giving screen readers a meaningful table name.
557+
- **`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.
558+
- **`headerLabel`** (column option): Provides a screen-reader-accessible label for column headers that have no textual content.
559+
- **`cellLabel`** (column option): Returns a descriptive `aria-label` for cells whose visual content is not self-explanatory.
560+
561+
**Note:**
562+
563+
- 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.
564+
- When the `header` prop is set, its text is automatically used as the table's accessible name. To provide a different accessible name, use `accessibleName` or `accessibleNameRef` instead.
565+
566+
<Canvas of={ComponentStories.Accessibility}/>
567+
552568
## Kitchen Sink
553569

554570
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.

packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.stories.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { FlexBox } from '../../FlexBox/index.js';
3838
import { ObjectStatus } from '../../ObjectStatus/index.js';
3939
import type { AnalyticalTableColumnDefinition, AnalyticalTablePropTypes } from '../index.js';
4040
import { AnalyticalTable } from '../index.js';
41+
import { useAnnounceEmptyCells } from '../pluginHooks/AnalyticalTableHooks.js';
4142

4243
const kitchenSinkArgs: AnalyticalTablePropTypes = {
4344
data: dataLarge,
@@ -680,6 +681,53 @@ export const ContextMenu: Story = {
680681
},
681682
};
682683

684+
export const Accessibility: Story = {
685+
render() {
686+
const tableHooks = useMemo(() => [useAnnounceEmptyCells], []);
687+
const columns = useMemo<AnalyticalTableColumnDefinition[]>(
688+
() => [
689+
{
690+
Header: 'Name',
691+
accessor: 'name',
692+
},
693+
{
694+
Header: 'Age',
695+
accessor: 'age',
696+
hAlign: 'End',
697+
},
698+
{
699+
Header: '',
700+
headerLabel: 'Actions',
701+
id: 'actions',
702+
disableFilters: true,
703+
disableSortBy: true,
704+
disableGroupBy: true,
705+
disableDragAndDrop: true,
706+
Cell: () => (
707+
<FlexBox>
708+
<Button icon={editIcon} accessibleName="Edit" tooltip="Edit" />
709+
<Button icon={deleteIcon} accessibleName="Delete" tooltip="Delete" />
710+
</FlexBox>
711+
),
712+
cellLabel: () => 'Actions: Edit, Delete',
713+
},
714+
],
715+
[],
716+
);
717+
const data = useMemo(() => [{ name: undefined, age: 80 }, ...dataLarge.slice(0, 4)], []);
718+
return (
719+
<AnalyticalTable
720+
columns={columns}
721+
data={data}
722+
header="Friends"
723+
accessibleName="Friends Table"
724+
selectionMode={AnalyticalTableSelectionMode.Multiple}
725+
tableHooks={tableHooks}
726+
/>
727+
);
728+
},
729+
};
730+
683731
export const KitchenSink: Story = {
684732
args: kitchenSinkArgs,
685733
};

packages/main/src/components/AnalyticalTable/index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ const measureElement = (el: HTMLElement) => {
134134
*/
135135
const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTypes>((props, ref) => {
136136
const {
137+
accessibleName,
138+
accessibleNameRef,
137139
adjustTableHeightOnPopIn,
138140
alternateRowColor,
139141
alwaysShowBusyIndicator,
@@ -794,15 +796,16 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
794796
</span>
795797
<div
796798
tabIndex={0}
797-
aria-labelledby={`${titleBarId} ${invalidTableTextId}`}
799+
aria-labelledby={`${accessibleNameRef ?? titleBarId} ${invalidTableTextId}`}
798800
role="region"
799801
data-component-name="AnalyticalTableOverlay"
800802
className={classNames.overlay}
801803
/>
802804
</>
803805
)}
804806
<div
805-
aria-labelledby={titleBarId}
807+
aria-label={accessibleName}
808+
aria-labelledby={accessibleNameRef ?? (header && !accessibleName ? titleBarId : undefined)}
806809
{...getTableProps()}
807810
tabIndex={loading || showOverlay ? -1 : 0}
808811
role={isTreeTable ? 'treegrid' : 'grid'}

packages/main/src/components/AnalyticalTable/react-table/publicUtils.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function mergeProps(...propList: Record<string, any>[]): Record<string, any> {
2727
};
2828

2929
if (style) {
30-
props.style = props.style ? { ...(props.style || {}), ...(style || {}) } : style;
30+
props.style = props.style ? { ...props.style, ...style } : style;
3131
}
3232

3333
if (className) {

packages/main/src/components/AnalyticalTable/types/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -766,9 +766,21 @@ export interface AnalyticalTablePropTypes extends Omit<CommonProps, 'title'> {
766766
/**
767767
* Component or text rendered in the header section of the `AnalyticalTable`.
768768
*
769-
* __Note:__ If not set, it will be hidden.
769+
* __Note:__ If not set, the header section is not rendered. When set, its text is automatically used as the table's accessible name. To provide a different accessible name, use `accessibleName` or `accessibleNameRef` instead.
770770
*/
771771
header?: ReactNode;
772+
/**
773+
* Defines the accessible name of the table.
774+
*
775+
* __Note:__ If set, the `aria-labelledby` derived from the `header` prop will not be applied to the table grid.
776+
*/
777+
accessibleName?: string;
778+
/**
779+
* Defines the IDs of the elements that label the table.
780+
*
781+
* __Note:__ If set, the `aria-labelledby` derived from the `header` prop will not be applied to the table grid.
782+
*/
783+
accessibleNameRef?: string;
772784
/**
773785
* Extension section of the Table. If not set, no extension area will be rendered
774786
*/

0 commit comments

Comments
 (0)