Skip to content

Commit 8d652b4

Browse files
authored
fix(AnalyticalTable): improve accessibility (#7181)
This PR i.a. improves accessibility for tree tables, but also adds general a11y improvements. __Note:__ To prevent flooding the console with warnings, translations for the "expand/collapse row" announcement will be added once they are available. Fixes #6515 Fixes #7147
1 parent 43fcb35 commit 8d652b4

File tree

11 files changed

+97
-17
lines changed

11 files changed

+97
-17
lines changed

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { VirtualItem, Virtualizer } from '@tanstack/react-virtual';
22
import IconDesign from '@ui5/webcomponents/dist/types/IconDesign.js';
3+
import IconMode from '@ui5/webcomponents/dist/types/IconMode.js';
34
import iconFilter from '@ui5/webcomponents-icons/dist/filter.js';
45
import iconGroup from '@ui5/webcomponents-icons/dist/group-2.js';
56
import iconSortAscending from '@ui5/webcomponents-icons/dist/sort-ascending.js';
@@ -236,18 +237,28 @@ export const ColumnHeader = (props: ColumnHeaderProps) => {
236237
data-component-name={`AnalyticalTableHeaderIconsContainer-${columnId}`}
237238
>
238239
{isFiltered && (
239-
<Icon design={IconDesign.NonInteractive} name={iconFilter} aria-hidden className={classNames.icon} />
240+
<Icon
241+
design={IconDesign.NonInteractive}
242+
name={iconFilter}
243+
className={classNames.icon}
244+
mode={IconMode.Decorative}
245+
/>
240246
)}
241247
{column.isSorted && (
242248
<Icon
243249
design={IconDesign.NonInteractive}
244250
name={column.isSortedDesc ? iconSortDescending : iconSortAscending}
245-
aria-hidden
246251
className={classNames.icon}
252+
mode={IconMode.Decorative}
247253
/>
248254
)}
249255
{column.isGrouped && (
250-
<Icon design={IconDesign.NonInteractive} name={iconGroup} aria-hidden className={classNames.icon} />
256+
<Icon
257+
design={IconDesign.NonInteractive}
258+
name={iconGroup}
259+
className={classNames.icon}
260+
mode={IconMode.Decorative}
261+
/>
251262
)}
252263
</div>
253264
</div>

packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBody.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
143143
key={`${visibleRowIndex}-${emptyRowCellProps.key}`}
144144
data-empty-row-cell="true"
145145
tabIndex={-1}
146-
aria-hidden
146+
aria-hidden="true"
147147
style={{ ...emptyRowCellProps.style, cursor: 'unset', width: item.size }}
148148
/>
149149
);

packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBodyContainer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export const VirtualTableBodyContainer = (props: VirtualTableBodyContainerProps)
123123
}}
124124
data-component-name="AnalyticalTableBody"
125125
tabIndex={-1}
126+
role="rowgroup"
126127
>
127128
{isMounted && children}
128129
</div>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const VerticalResizer = (props: VerticalResizerProps) => {
147147

148148
return (
149149
<div
150+
aria-hidden="true"
150151
className={classNames.verticalResizerContainer}
151152
ref={verticalResizerRef}
152153
onMouseDown={handleResizeStart}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import IconMode from '@ui5/webcomponents/dist/types/IconMode.js';
12
import ListItemType from '@ui5/webcomponents/dist/types/ListItemType.js';
23
import PopoverHorizontalAlign from '@ui5/webcomponents/dist/types/PopoverHorizontalAlign.js';
34
import PopoverPlacement from '@ui5/webcomponents/dist/types/PopoverPlacement.js';
@@ -218,7 +219,7 @@ export const ColumnHeaderModal = (instance: TableInstanceWithPopoverProps) => {
218219
<Icon
219220
name={iconFilter}
220221
className={classNames.filterIcon}
221-
aria-hidden
222+
mode={IconMode.Decorative}
222223
style={{
223224
minWidth: filterStyles.iconDimensions,
224225
minHeight: filterStyles.iconDimensions

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

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import iconNavDownArrow from '@ui5/webcomponents-icons/dist/navigation-down-arro
44
import iconNavRightArrow from '@ui5/webcomponents-icons/dist/navigation-right-arrow.js';
55
import { CssSizeVariables, useCurrentTheme } from '@ui5/webcomponents-react-base';
66
import { clsx } from 'clsx';
7-
import { Button, Icon } from '../../../../webComponents/index.js';
7+
import type { FocusEvent } from 'react';
8+
import type { ButtonDomRef } from '../../../../webComponents/Button/index.js';
9+
import { Button } from '../../../../webComponents/Button/index.js';
10+
import { Icon } from '../../../../webComponents/Icon/index.js';
11+
import type { ColumnType, RowType, WCRPropertiesType } from '../../types/index.js';
812
import { RenderColumnTypes } from '../../types/index.js';
913

1014
const getPadding = (level) => {
@@ -22,7 +26,15 @@ const getPadding = (level) => {
2226
}
2327
};
2428

25-
export const Expandable = (props) => {
29+
interface ExpandableProps {
30+
cell: Record<string, any>;
31+
row: RowType;
32+
column: ColumnType;
33+
visibleColumns: ColumnType[];
34+
webComponentsReactProperties: WCRPropertiesType;
35+
}
36+
37+
export const Expandable = (props: ExpandableProps) => {
2638
const { cell, row, column, visibleColumns: columns, webComponentsReactProperties } = props;
2739
const {
2840
renderRowSubComponent,
@@ -55,24 +67,37 @@ export const Expandable = (props) => {
5567
title={row.isExpanded ? translatableTexts.collapseNodeA11yText : translatableTexts.expandNodeA11yText}
5668
style={{ ...rowProps.style, paddingInlineStart: paddingLeft }}
5769
className={classNames.container}
58-
aria-label={row.isExpanded ? translatableTexts.collapseA11yText : translatableTexts.expandA11yText}
5970
>
6071
{shouldRenderButton ? (
6172
<Button
6273
tabIndex={-1}
6374
icon={row.isExpanded ? iconNavDownArrow : iconNavRightArrow}
6475
design={ButtonDesign.Transparent}
65-
onClick={rowProps.onClick}
6676
className={classNames.button}
77+
onClick={rowProps.onClick}
78+
accessibilityAttributes={{ expanded: row.isExpanded, hasPopup: false, controls: undefined }}
79+
onFocus={(e: FocusEvent<ButtonDomRef>) => {
80+
e.target.accessibleName = row.isExpanded
81+
? translatableTexts.collapseNodeA11yText
82+
: translatableTexts.expandNodeA11yText;
83+
}}
84+
onBlur={(e: FocusEvent<ButtonDomRef>) => {
85+
e.target.accessibleName = '';
86+
}}
6787
/>
6888
) : (
6989
<Icon
90+
aria-hidden="true"
7091
tabIndex={-1}
7192
onClick={rowProps.onClick}
7293
mode={IconMode.Interactive}
7394
name={row.isExpanded ? iconNavDownArrow : iconNavRightArrow}
95+
aria-expanded={`${row.isExpanded}`}
7496
data-component-name="AnalyticalTableExpandIcon"
7597
className={classNames.expandableIcon}
98+
accessibleName={
99+
row.isExpanded ? translatableTexts.collapseNodeA11yText : translatableTexts.expandNodeA11yText
100+
}
76101
/>
77102
)}
78103
</span>
@@ -84,6 +109,14 @@ export const Expandable = (props) => {
84109
classNames.nonExpandableCellSpacer,
85110
shouldRenderButton && classNames.withExpandableButton
86111
)}
112+
onFocus={(e: FocusEvent<ButtonDomRef>) => {
113+
e.target.accessibleName = row.isExpanded
114+
? translatableTexts.collapseNodeA11yText
115+
: translatableTexts.expandNodeA11yText;
116+
}}
117+
onBlur={(e: FocusEvent<ButtonDomRef>) => {
118+
e.target.accessibleName = '';
119+
}}
87120
/>
88121
)}
89122
</>

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import type { MouseEvent } from 'react';
12
import type { ReactTableHooks } from '../types/index.js';
23

34
const useGetResizerProps = (props) => {
45
return {
56
...props,
6-
onMouseDown: (e) => {
7+
'aria-hidden': 'true',
8+
onMouseDown: (e: MouseEvent<HTMLDivElement>) => {
79
e.preventDefault();
810
props.onMouseDown(e);
911
}

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
import { enrichEventWithDetails } from '@ui5/webcomponents-react-base';
1+
import announce from '@ui5/webcomponents-base/dist/util/InvisibleMessage.js';
2+
import { debounce, enrichEventWithDetails } from '@ui5/webcomponents-react-base';
23
import type { ReactTableHooks, RowType, TableInstance } from '../types/index.js';
34

5+
// debounce announce to prevent excessive successive announcements
6+
const debouncedAnnounce = debounce((announcement: string) => {
7+
announce(announcement, 'Polite');
8+
}, 200);
9+
410
const getToggleRowExpandedProps = (
511
rowProps,
612
{ row, instance, userProps }: { row: RowType; instance: TableInstance; userProps: Record<string, any> }
713
) => {
814
const { manualGroupBy } = instance;
9-
const { onRowExpandChange, isTreeTable, renderRowSubComponent, alwaysShowSubComponent } =
15+
const { onRowExpandChange, isTreeTable, renderRowSubComponent, alwaysShowSubComponent, translatableTexts } =
1016
instance.webComponentsReactProperties;
17+
1118
const onClick = (e, noPropagation = true) => {
1219
if (noPropagation) {
1320
e.stopPropagation();
@@ -30,6 +37,11 @@ const getToggleRowExpandedProps = (
3037
);
3138
}
3239
row.toggleRowExpanded();
40+
// cannot use ROW_X_COLLAPSED/ROW_X_EXPANDED here,
41+
// as retrieving the index of the row is not easily possible here and has performance implications
42+
debouncedAnnounce(
43+
!row.isExpanded ? translatableTexts.rowExpandedAnnouncementText : translatableTexts.rowCollapsedAnnouncementText
44+
);
3345
};
3446
const onKeyDown = (e) => {
3547
if (e.code === 'F4') {

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,12 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
209209
filteredA11yText: i18nBundle.getText(FILTERED),
210210
groupedA11yText: i18nBundle.getText(GROUPED),
211211
selectAllA11yText: i18nBundle.getText(SELECT_ALL_PRESS_SPACE),
212-
deselectAllA11yText: i18nBundle.getText(UNSELECT_ALL_PRESS_SPACE)
212+
deselectAllA11yText: i18nBundle.getText(UNSELECT_ALL_PRESS_SPACE),
213+
//todo: use translations once they are available
214+
// rowExpandedAnnouncementText: i18nBundle.getText(ROW_EXPANDED),
215+
// rowCollapsedAnnouncementText: i18nBundle.getText(ROW_COLLAPSED)
216+
rowExpandedAnnouncementText: 'Row expanded',
217+
rowCollapsedAnnouncementText: 'Row collapsed'
213218
},
214219
alternateRowColor,
215220
alwaysShowSubComponent,
@@ -734,7 +739,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
734739
{/*todo: use global CSS once --sapBlockLayer_Opacity is available*/}
735740
{showOverlay && (
736741
<>
737-
<span id={invalidTableTextId} className={classNames.hiddenA11yText} aria-hidden>
742+
<span id={invalidTableTextId} className={classNames.hiddenA11yText} aria-hidden="true">
738743
{invalidTableA11yText}
739744
</span>
740745
<div
@@ -750,7 +755,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
750755
aria-labelledby={titleBarId}
751756
{...getTableProps()}
752757
tabIndex={loading || showOverlay ? -1 : 0}
753-
role="grid"
758+
role={isTreeTable ? 'treegrid' : 'grid'}
754759
aria-rowcount={rows.length}
755760
aria-colcount={visibleColumns.length}
756761
data-per-page={internalVisibleRowCount}
@@ -759,8 +764,8 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
759764
ref={tableRef}
760765
className={tableClasses}
761766
>
762-
<div className={classNames.tableHeaderBackgroundElement} />
763-
<div className={classNames.tableBodyBackgroundElement} />
767+
<div className={classNames.tableHeaderBackgroundElement} aria-hidden="true" />
768+
<div className={classNames.tableBodyBackgroundElement} aria-hidden="true" />
764769
{headerGroups.map((headerGroup) => {
765770
let headerProps: Record<string, unknown> = {};
766771
if (headerGroup.getHeaderGroupProps) {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ export interface WCRPropertiesType {
207207
groupedA11yText: string;
208208
selectAllA11yText: string;
209209
deselectAllA11yText: string;
210+
rowExpandedAnnouncementText: string;
211+
rowCollapsedAnnouncementText: string;
210212
};
211213
tagNamesWhichShouldNotSelectARow: Set<string>;
212214
tableRef: MutableRefObject<DivWithCustomScrollProp>;

packages/main/src/i18n/messagebundle.properties

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,18 @@ EXPAND_PRESS_SPACE=To expand the row, press the spacebar.
246246
#XACT: Aria label text for expandable table cells in expanded state
247247
COLLAPSE_PRESS_SPACE=To collapse the row, press the spacebar.
248248

249+
#XACT: general ARIA live announcement for expanded row
250+
ROW_EXPANDED=Row expanded
251+
252+
#XACT: general ARIA live announcement for collapsed row
253+
ROW_COLLAPSED=Row collapsed
254+
255+
#XACT: ARIA live announcement for expanded row number {0}
256+
ROW_X_EXPANDED=Row {0} expanded
257+
258+
#XACT: ARIA live announcement for collapsed row number {0}
259+
ROW_X_COLLAPSED=Row {0} collapsed
260+
249261
#XACT: Aria label text for selectable table cells in unselected state
250262
SELECT_PRESS_SPACE=To select the row, press the spacebar.
251263

0 commit comments

Comments
 (0)