Skip to content

Commit 92cc562

Browse files
committed
fix(AnalyticalTable): improve accessibility
1 parent 9af8a06 commit 92cc562

File tree

8 files changed

+265
-192
lines changed

8 files changed

+265
-192
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const kitchenSinkArgs: AnalyticalTablePropTypes = {
6666
},
6767
{
6868
Header: () => <span>Friend Age</span>,
69-
headerLabel: 'Friend Age',
69+
headerLabel: 'Custom Header Label',
7070
accessor: 'friend.age',
7171
autoResizable: true,
7272
hAlign: TextAlign.End,
@@ -130,7 +130,7 @@ const kitchenSinkArgs: AnalyticalTablePropTypes = {
130130
return `${cell.cellLabel} press TAB to focus active elements inside this cell`;
131131
},
132132
},
133-
],
133+
].slice(0, 2),
134134
filterable: true,
135135
alternateRowColor: true,
136136
columnOrder: ['friend.name', 'friend.age', 'name'],
@@ -195,6 +195,7 @@ const meta = {
195195
visibleRows: 5,
196196
// sb actions has a huge impact on performance here.
197197
onTableScroll: undefined,
198+
header: 'TableTitle',
198199
},
199200
argTypes: {
200201
data: { control: { disable: true } },

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ export const ColumnHeader = (props: ColumnHeaderProps) => {
200200
borderInlineStart: dragOver ? `3px solid ${ThemingParameters.sapSelectedColor}` : undefined,
201201
}}
202202
aria-haspopup={hasPopover ? 'menu' : undefined}
203+
aria-expanded={hasPopover ? (popoverOpen ? 'true' : 'false') : undefined}
204+
aria-controls={hasPopover ? `${id}-popover` : undefined}
203205
role={role}
204206
draggable={isDraggable}
205207
onDragEnter={onDragEnter}
@@ -278,6 +280,7 @@ export const ColumnHeader = (props: ColumnHeaderProps) => {
278280
// render the popover and add the props to the table instance
279281
column.render(RenderColumnTypes.Popover, {
280282
popoverProps: {
283+
id: `${id}-popover`,
281284
openerRef: columnHeaderRef,
282285
setOpen: setPopoverOpen,
283286
},

packages/main/src/components/AnalyticalTable/defaults/Column/Cell.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@ export const Cell = (props: CellInstance) => {
1111
if (isGrouped) {
1212
cellContent += ` (${row.subRows.length})`;
1313
}
14+
1415
return (
1516
<span
1617
title={cellContent}
1718
className={webComponentsReactProperties.classes.tableText}
1819
data-column-id-cell-text={column.id}
20+
//todo: VoiceOver announces blank because of `aria-hidden` although `aria-label` is set on the `gridcell` element - this is a known bug and there's no workaround
21+
// options:
22+
// - remove it and accept duplicate announcements
23+
// - keep it and accept blank announcements
24+
// - remove it and don't add the aria-label on the focused cell, which will lead to the `cellContent` being announced last
25+
aria-hidden="true"
1926
>
2027
{cellContent}
2128
</span>

packages/main/src/components/AnalyticalTable/defaults/Column/ColumnHeaderModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import type { TableInstanceWithPopoverProps } from '../../types/index.js';
3434
import { RenderColumnTypes } from '../../types/index.js';
3535

3636
export const ColumnHeaderModal = (instance: TableInstanceWithPopoverProps) => {
37-
const { setOpen, openerRef } = instance.popoverProps;
37+
const { setOpen, openerRef, id } = instance.popoverProps;
3838
const { column, state, webComponentsReactProperties } = instance;
3939
const { isRtl, groupBy } = state;
4040
const { onGroup, onSort, classes: classNames } = webComponentsReactProperties;
@@ -174,10 +174,11 @@ export const ColumnHeaderModal = (instance: TableInstanceWithPopoverProps) => {
174174
ref.current.open = true;
175175
});
176176
}
177-
}, []);
177+
}, [openerRef]);
178178

179179
return (
180180
<Popover
181+
id={id}
181182
hideArrow
182183
horizontalAlign={horizontalAlign}
183184
placement={PopoverPlacement.Bottom}

packages/main/src/components/AnalyticalTable/hooks/useA11y.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,26 @@ import type { ReactTableHooks, TableInstance } from '../types/index.js';
55

66
interface UpdatedCellProptypes {
77
onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
8-
'aria-expanded'?: string | boolean;
8+
'aria-expanded'?: string;
99
'aria-label'?: string;
1010
'aria-colindex'?: number;
11+
'aria-describedby'?: string;
1112
role?: string;
1213
}
1314

1415
const setCellProps = (cellProps, { cell, instance }: { cell: TableInstance['cell']; instance: TableInstance }) => {
1516
const { column, row, value } = cell;
1617
const columnIndex = instance.visibleColumns.findIndex(({ id }) => id === column.id);
17-
const { alwaysShowSubComponent, renderRowSubComponent, translatableTexts, selectionMode, selectionBehavior } =
18+
const { alwaysShowSubComponent, renderRowSubComponent, selectionMode, selectionBehavior, a11yElementIds, uniqueId } =
1819
instance.webComponentsReactProperties;
19-
const updatedCellProps: UpdatedCellProptypes = { 'aria-colindex': columnIndex + 1, role: 'gridcell' }; // aria index is 1 based, not 0
20+
const updatedCellProps: UpdatedCellProptypes = {
21+
// aria index is 1 based, not 0
22+
'aria-colindex': columnIndex + 1,
23+
role: 'gridcell',
24+
// header label
25+
'aria-describedby': `${uniqueId}${column.id}`,
26+
'aria-label': '',
27+
};
2028

2129
const RowSubComponent = typeof renderRowSubComponent === 'function' ? renderRowSubComponent(row) : undefined;
2230
const rowIsExpandable = row.canExpand || (RowSubComponent && !alwaysShowSubComponent);
@@ -30,24 +38,17 @@ const setCellProps = (cellProps, { cell, instance }: { cell: TableInstance['cell
3038

3139
const isFirstUserCol = userCols[0]?.id === column.id || userCols[0]?.accessor === column.accessor;
3240
updatedCellProps['data-is-first-column'] = isFirstUserCol;
33-
updatedCellProps['aria-label'] = column.headerLabel || (typeof column.Header === 'string' ? column.Header : '');
34-
updatedCellProps['aria-label'] &&= `${updatedCellProps['aria-label']} `;
3541
updatedCellProps['aria-label'] += value || value === 0 ? `${value} ` : '';
3642

3743
if ((isFirstUserCol && rowIsExpandable) || (row.isGrouped && row.canExpand)) {
3844
updatedCellProps.onKeyDown = row.getToggleRowExpandedProps?.()?.onKeyDown;
39-
let ariaLabel = '';
40-
if (row.isGrouped) {
41-
ariaLabel += translatableTexts.groupedA11yText + ',';
42-
}
4345
if (row.isExpanded) {
4446
updatedCellProps['aria-expanded'] = 'true';
45-
ariaLabel += ` ${translatableTexts.collapseA11yText}`;
47+
updatedCellProps['aria-describedby'] += ' ' + a11yElementIds.cellCollapseDescId;
4648
} else {
4749
updatedCellProps['aria-expanded'] = 'false';
48-
ariaLabel += ` ${translatableTexts.expandA11yText}`;
50+
updatedCellProps['aria-describedby'] += ' ' + a11yElementIds.cellExpandDescId;
4951
}
50-
updatedCellProps['aria-label'] += ariaLabel;
5152
} else if (
5253
(selectionMode !== AnalyticalTableSelectionMode.None &&
5354
selectionBehavior !== AnalyticalTableSelectionBehavior.RowSelector &&
@@ -56,16 +57,18 @@ const setCellProps = (cellProps, { cell, instance }: { cell: TableInstance['cell
5657
) {
5758
if (row.isSelected) {
5859
updatedCellProps['aria-selected'] = 'true';
59-
updatedCellProps['aria-label'] += ` ${translatableTexts.unselectA11yText}`;
60+
updatedCellProps['aria-describedby'] = ' ' + a11yElementIds.cellUnselectDescId;
6061
} else {
6162
updatedCellProps['aria-selected'] = 'false';
62-
updatedCellProps['aria-label'] += ` ${translatableTexts.selectA11yText}`;
63+
updatedCellProps['aria-describedby'] = ' ' + a11yElementIds.cellSelectDescId;
6364
}
6465
}
6566
const { cellLabel } = cell.column;
6667
if (typeof cellLabel === 'function') {
67-
cell.cellLabel = updatedCellProps['aria-label'];
68+
cell.cellLabel = '';
6869
updatedCellProps['aria-label'] = cellLabel({ cell, instance });
70+
} else {
71+
updatedCellProps['aria-label'] ||= undefined;
6972
}
7073
return [cellProps, updatedCellProps];
7174
};
@@ -108,6 +111,20 @@ const setHeaderProps = (
108111
: translatableTexts.selectAllA11yText;
109112
}
110113

114+
if (column.id === '__ui5wcr__internal_selection_column') {
115+
updatedProps['aria-label'] += translatableTexts.selectionHeaderCellText;
116+
}
117+
118+
if (column.id === '__ui5wcr__internal_highlight_column') {
119+
updatedProps['aria-label'] += translatableTexts.highlightHeaderCellText;
120+
}
121+
122+
if (column.id === '__ui5wcr__internal_navigation_column') {
123+
updatedProps['aria-label'] += translatableTexts.navigationHeaderCellText;
124+
}
125+
126+
updatedProps['aria-label'] ||= undefined;
127+
111128
return [headerProps, { isFiltered, ...updatedProps }];
112129
};
113130

0 commit comments

Comments
 (0)