Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3159,6 +3159,35 @@ describe('AnalyticalTable', () => {
);
});

it('a11y: accessibleName and accessibleNameRef', () => {
// no aria-labelledby
cy.mount(<AnalyticalTable columns={columns} data={data} />);
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(<AnalyticalTable columns={columns} data={data} header="Items Table" />);
cy.get('[data-component-name="AnalyticalTableContainer"]')
.should('have.attr', 'aria-labelledby')
.then((labelledby) => {
cy.get(`[id="${labelledby}"]`).should('exist');
});

// accessibleName: aria-label on the grid and removes the header connection
cy.mount(<AnalyticalTable columns={columns} data={data} header="Items Table" accessibleName="Financing Details" />);
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(
<>
<span id="custom-label">Custom Table Label</span>
<AnalyticalTable columns={columns} data={data} header="Items Table" accessibleNameRef="custom-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 }>({});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,22 @@ function ContextMenuExample() {

</details>

## Accessibility

This example demonstrates possible 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.

**Note:**

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

<Canvas of={ComponentStories.Accessibility}/>

## 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -680,6 +681,53 @@ export const ContextMenu: Story = {
},
};

export const Accessibility: Story = {
render() {
const tableHooks = useMemo(() => [useAnnounceEmptyCells], []);
const columns = useMemo<AnalyticalTableColumnDefinition[]>(
() => [
{
Header: 'Name',
accessor: 'name',
},
{
Header: 'Age',
accessor: 'age',
hAlign: 'End',
},
{
Header: '',
headerLabel: 'Actions',
id: 'actions',
disableFilters: true,
disableSortBy: true,
disableGroupBy: true,
disableDragAndDrop: true,
Cell: () => (
<FlexBox>
<Button icon={editIcon} accessibleName="Edit" tooltip="Edit" />
<Button icon={deleteIcon} accessibleName="Delete" tooltip="Delete" />
</FlexBox>
),
cellLabel: () => 'Actions: Edit, Delete',
},
],
[],
);
const data = useMemo(() => [{ name: undefined, age: 80 }, ...dataLarge.slice(0, 4)], []);
return (
<AnalyticalTable
columns={columns}
data={data}
header="Friends"
accessibleName="Friends Table"
selectionMode={AnalyticalTableSelectionMode.Multiple}
tableHooks={tableHooks}
/>
);
},
};

export const KitchenSink: Story = {
args: kitchenSinkArgs,
};
Expand Down
7 changes: 5 additions & 2 deletions packages/main/src/components/AnalyticalTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ const measureElement = (el: HTMLElement) => {
*/
const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTypes>((props, ref) => {
const {
accessibleName,
accessibleNameRef,
adjustTableHeightOnPopIn,
alternateRowColor,
alwaysShowBusyIndicator,
Expand Down Expand Up @@ -780,15 +782,16 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
</span>
<div
tabIndex={0}
aria-labelledby={`${titleBarId} ${invalidTableTextId}`}
aria-labelledby={`${accessibleNameRef ?? titleBarId} ${invalidTableTextId}`}
role="region"
data-component-name="AnalyticalTableOverlay"
className={classNames.overlay}
/>
</>
)}
<div
aria-labelledby={titleBarId}
aria-label={accessibleName}
aria-labelledby={accessibleNameRef ?? (header && !accessibleName ? titleBarId : undefined)}
{...getTableProps()}
tabIndex={loading || showOverlay ? -1 : 0}
role={isTreeTable ? 'treegrid' : 'grid'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function mergeProps(...propList: Record<string, any>[]): Record<string, any> {
};

if (style) {
props.style = props.style ? { ...(props.style || {}), ...(style || {}) } : style;
props.style = props.style ? { ...props.style, ...style } : style;
}

if (className) {
Expand Down
14 changes: 13 additions & 1 deletion packages/main/src/components/AnalyticalTable/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -766,9 +766,21 @@ export interface AnalyticalTablePropTypes extends Omit<CommonProps, 'title'> {
/**
* Component or text rendered in the header section of the `AnalyticalTable`.
*
* __Note:__ If not set, it will be hidden.
* __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.
*/
header?: ReactNode;
/**
* Defines the accessible name of the table.
*
* __Note:__ If set, the `aria-labelledby` derived from the `header` prop will not be applied to the table grid.
*/
accessibleName?: string;
/**
* Defines the IDs of the elements that label the table.
*
* __Note:__ If set, the `aria-labelledby` derived from the `header` prop will not be applied to the table grid.
*/
accessibleNameRef?: string;
/**
* Extension section of the Table. If not set, no extension area will be rendered
*/
Expand Down
Loading