From 5ffe93300fa5946718f46c96e74287b656ee81e5 Mon Sep 17 00:00:00 2001 From: olitreadwell Date: Wed, 5 Aug 2026 23:02:37 +1200 Subject: [PATCH] Add aria-sort to sorted DataTable column headers The DataTable header cells did not expose the current sort state to assistive technology. A screen reader user could not tell which column was sorted, or in which direction. Pass the active sort direction to the MUI TableCell via its `sortDirection` prop. MUI turns this into `aria-sort="ascending"` or `aria-sort="descending"` on the `` of the sorted column only. Other columns get no `aria-sort`, per the ARIA spec. Add unit tests that check `aria-sort` is set on the sorted column, moves when another column is sorted, and flips when the order changes. --- .../src/list/datatable/DataTable.spec.tsx | 30 +++++++++++++++++++ .../src/list/datatable/DataTableHeadCell.tsx | 7 +++++ 2 files changed, 37 insertions(+) diff --git a/packages/ra-ui-materialui/src/list/datatable/DataTable.spec.tsx b/packages/ra-ui-materialui/src/list/datatable/DataTable.spec.tsx index 0277f304871..46688abcff0 100644 --- a/packages/ra-ui-materialui/src/list/datatable/DataTable.spec.tsx +++ b/packages/ra-ui-materialui/src/list/datatable/DataTable.spec.tsx @@ -102,6 +102,36 @@ describe('DataTable', () => { ); }); }); + it('should set aria-sort on the currently sorted column header', async () => { + render(); + const headers = await screen.findAllByRole('columnheader'); + // Basic sorts by id ascending by default + expect(headers[1].getAttribute('aria-sort')).toEqual('ascending'); + // a column that is not the sort column has no aria-sort + expect(headers[2].getAttribute('aria-sort')).toBeNull(); + }); + it('should update aria-sort when the sort order changes', async () => { + render(); + const headers = await screen.findAllByRole('columnheader'); + // clicking the active ascending column switches it to descending + fireEvent.click(headers[1].firstChild as HTMLElement); + await waitFor(() => { + expect(headers[1].getAttribute('aria-sort')).toEqual( + 'descending' + ); + }); + }); + it('should move aria-sort to the newly sorted column', async () => { + render(); + const headers = await screen.findAllByRole('columnheader'); + fireEvent.click(headers[2].firstChild as HTMLElement); + await waitFor(() => { + expect(headers[2].getAttribute('aria-sort')).toEqual( + 'ascending' + ); + }); + expect(headers[1].getAttribute('aria-sort')).toBeNull(); + }); }); describe('Columns', () => { it('should render children as column headers', async () => { diff --git a/packages/ra-ui-materialui/src/list/datatable/DataTableHeadCell.tsx b/packages/ra-ui-materialui/src/list/datatable/DataTableHeadCell.tsx index 943fa049d01..099375f3387 100644 --- a/packages/ra-ui-materialui/src/list/datatable/DataTableHeadCell.tsx +++ b/packages/ra-ui-materialui/src/list/datatable/DataTableHeadCell.tsx @@ -93,6 +93,13 @@ export const DataTableHeadCell = React.memo( `column-${source}` )} variant="head" + sortDirection={ + sort && sort.field === source + ? sort.order === 'ASC' + ? 'asc' + : 'desc' + : false + } {...rest} > {handleSort && sort && !disableSort && source ? (