Skip to content

Commit b9751fb

Browse files
committed
feat(AnalyticalTable): add accessibleName & accessibleNameRef props
1 parent 6b3b513 commit b9751fb

6 files changed

Lines changed: 108 additions & 3 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
@@ -3159,6 +3159,35 @@ describe('AnalyticalTable', () => {
31593159
);
31603160
});
31613161

3162+
it('a11y: accessibleName and accessibleNameRef', () => {
3163+
// no aria-labelledby
3164+
cy.mount(<AnalyticalTable columns={columns} data={data} />);
3165+
cy.get('[data-component-name="AnalyticalTableContainer"]').should('not.have.attr', 'aria-labelledby');
3166+
cy.get('[data-component-name="AnalyticalTableContainer"]').should('not.have.attr', 'aria-label');
3167+
3168+
// with header: aria-labelledby points to the title bar
3169+
cy.mount(<AnalyticalTable columns={columns} data={data} header="Items Table" />);
3170+
cy.get('[data-component-name="AnalyticalTableContainer"]')
3171+
.should('have.attr', 'aria-labelledby')
3172+
.then((labelledby) => {
3173+
cy.get(`#${labelledby}`).should('exist');
3174+
});
3175+
3176+
// accessibleName: aria-label on the grid and removes the header connection
3177+
cy.mount(<AnalyticalTable columns={columns} data={data} header="Items Table" accessibleName="Financing Details" />);
3178+
cy.get('[data-component-name="AnalyticalTableContainer"]').should('have.attr', 'aria-label', 'Financing Details');
3179+
cy.get('[data-component-name="AnalyticalTableContainer"]').should('not.have.attr', 'aria-labelledby');
3180+
3181+
// accessibleNameRef: overrides the header connection
3182+
cy.mount(
3183+
<>
3184+
<span id="custom-label">Custom Table Label</span>
3185+
<AnalyticalTable columns={columns} data={data} header="Items Table" accessibleNameRef="custom-label" />
3186+
</>,
3187+
);
3188+
cy.get('[data-component-name="AnalyticalTableContainer"]').should('have.attr', 'aria-labelledby', 'custom-label');
3189+
});
3190+
31623191
it("Expandable: don't scroll when expanded/collapsed", () => {
31633192
const TestComp = () => {
31643193
const tableInstanceRef = useRef<{ toggleRowExpanded?: (e: string) => void }>({});

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

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

550550
</details>
551551

552+
## Accessibility
553+
554+
This example demonstrates the recommended 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+
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.
562+
563+
<Canvas of={ComponentStories.Accessibility}/>
564+
552565
## Kitchen Sink
553566

554567
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,
@@ -780,15 +782,16 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
780782
</span>
781783
<div
782784
tabIndex={0}
783-
aria-labelledby={`${titleBarId} ${invalidTableTextId}`}
785+
aria-labelledby={`${accessibleNameRef ?? titleBarId} ${invalidTableTextId}`}
784786
role="region"
785787
data-component-name="AnalyticalTableOverlay"
786788
className={classNames.overlay}
787789
/>
788790
</>
789791
)}
790792
<div
791-
aria-labelledby={titleBarId}
793+
aria-label={accessibleName}
794+
aria-labelledby={accessibleNameRef ?? (header && !accessibleName ? titleBarId : undefined)}
792795
{...getTableProps()}
793796
tabIndex={loading || showOverlay ? -1 : 0}
794797
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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,18 @@ export interface AnalyticalTablePropTypes extends Omit<CommonProps, 'title'> {
769769
* __Note:__ If not set, it will be hidden.
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)