Skip to content

Commit 46b29cd

Browse files
authored
fix(AnalyticalTable): announce grouped, filtered and sorted columns with screen readers (#4392)
Fixes #4172
1 parent 9be1999 commit 46b29cd

File tree

6 files changed

+109
-11
lines changed

6 files changed

+109
-11
lines changed

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1586,6 +1586,51 @@ describe('AnalyticalTable', () => {
15861586
cy.findByText('Selected: {"0":true,"1":true,"2":true,"3":true}').should('be.visible');
15871587
});
15881588

1589+
it('a11y: grouped, filtered, sorted', () => {
1590+
cy.mount(<AnalyticalTable columns={columns} data={data} groupable filterable sortable />);
1591+
cy.findByText('Name').click();
1592+
cy.findByText('Sort Ascending').shadow().findByRole('listitem').click({ force: true });
1593+
cy.get('[data-column-id="name"]').should('have.attr', 'aria-sort', 'ascending');
1594+
cy.findByText('Name').click();
1595+
cy.findByText('Clear Sorting').shadow().findByRole('listitem').click({ force: true });
1596+
cy.get('[data-column-id="name"]').should('not.have.attr', 'aria-sort');
1597+
cy.findByText('Name').click();
1598+
cy.findByText('Sort Descending').shadow().findByRole('listitem').click({ force: true });
1599+
cy.get('[data-column-id="name"]').should('have.attr', 'aria-sort', 'descending');
1600+
cy.findByText('Name').click();
1601+
cy.findByText('Sort Ascending').shadow().get('[ui5-input]').typeIntoUi5Input('A{enter}');
1602+
cy.get('[data-column-id="name"]')
1603+
.should('have.attr', 'aria-sort', 'descending')
1604+
.and('have.attr', 'aria-label', 'Filtered');
1605+
1606+
cy.findByText('Name').click();
1607+
cy.findByText('Group').shadow().findByRole('listitem').click({ force: true });
1608+
cy.get('[data-column-id="name"]')
1609+
.should('have.attr', 'aria-sort', 'descending')
1610+
.and('have.attr', 'aria-label', 'Filtered Grouped');
1611+
cy.get('[data-visible-row-index="1"][data-visible-column-index="0"]').should(
1612+
'have.attr',
1613+
'aria-label',
1614+
'Grouped, To expand the row, press the spacebar'
1615+
);
1616+
cy.get('[name="navigation-right-arrow"]').click();
1617+
cy.get('[data-visible-row-index="1"][data-visible-column-index="0"]').should(
1618+
'have.attr',
1619+
'aria-label',
1620+
'Grouped, To collapse the row, press the spacebar'
1621+
);
1622+
cy.findByText('Name').click();
1623+
cy.findByText('Ungroup').shadow().findByRole('listitem').click({ force: true });
1624+
cy.get('[data-visible-row-index="1"][data-visible-column-index="0"]').should('not.have.attr', 'aria-label');
1625+
cy.get('[data-column-id="name"]')
1626+
.should('have.attr', 'aria-sort', 'descending')
1627+
.and('have.attr', 'aria-label', 'Filtered');
1628+
1629+
cy.findByText('Name').click();
1630+
cy.findByText('Sort Ascending').shadow().get('[ui5-input]').typeIntoUi5Input('{selectall}{backspace}{enter}');
1631+
cy.get('[data-column-id="name"]').should('have.attr', 'aria-sort', 'descending').and('not.have.attr', 'aria-label');
1632+
});
1633+
15891634
cypressPassThroughTestsFactory(AnalyticalTable, { data, columns });
15901635
});
15911636

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ export const ColumnHeaderModal = (props: ColumnHeaderModalProperties) => {
233233
onKeyDown={handleCustomLiKeyDown}
234234
>
235235
<FlexBox alignItems={FlexBoxAlignItems.Center} className={classes.filter}>
236-
<Icon name={iconFilter} className={classes.filterIcon} />
236+
<Icon name={iconFilter} className={classes.filterIcon} aria-hidden />
237237
<Filter column={column} popoverRef={ref} />
238238
</FlexBox>
239239
</CustomListItem>

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import iconSortDescending from '@ui5/webcomponents-icons/dist/sort-descending.js
66
import { ThemingParameters } from '@ui5/webcomponents-react-base';
77
import { clsx } from 'clsx';
88
import React, {
9+
AriaAttributes,
910
CSSProperties,
1011
DragEventHandler,
1112
FC,
@@ -49,6 +50,9 @@ export interface ColumnHeaderProps {
4950
style: CSSProperties;
5051
column: ColumnType;
5152
role: string;
53+
isFiltered?: boolean;
54+
['aria-sort']?: AriaAttributes['aria-sort'];
55+
['aria-label']?: AriaAttributes['aria-label'];
5256
}
5357

5458
const styles = {
@@ -116,10 +120,12 @@ export const ColumnHeader: FC<ColumnHeaderProps> = (props: ColumnHeaderProps) =>
116120
onClick,
117121
onKeyDown,
118122
portalContainer,
119-
scaleXFactor
123+
scaleXFactor,
124+
isFiltered,
125+
'aria-label': ariaLabel,
126+
'aria-sort': ariaSort
120127
} = props;
121128

122-
const isFiltered = column.filterValue && column.filterValue.length > 0;
123129
const [popoverOpen, setPopoverOpen] = useState(false);
124130
const columnHeaderRef = useRef<HTMLDivElement>(null);
125131

@@ -190,6 +196,7 @@ export const ColumnHeader: FC<ColumnHeaderProps> = (props: ColumnHeaderProps) =>
190196
setPopoverOpen(true);
191197
}
192198
};
199+
193200
if (!column) return null;
194201
return (
195202
<div
@@ -235,6 +242,8 @@ export const ColumnHeader: FC<ColumnHeaderProps> = (props: ColumnHeaderProps) =>
235242
onClick={handleHeaderCellClick}
236243
onKeyDown={handleHeaderCellKeyDown}
237244
onKeyUp={handleHeaderCellKeyUp}
245+
aria-label={ariaLabel}
246+
aria-sort={ariaSort}
238247
>
239248
<div className={classes.header} data-h-align={column.hAlign}>
240249
<Text
@@ -254,9 +263,11 @@ export const ColumnHeader: FC<ColumnHeaderProps> = (props: ColumnHeaderProps) =>
254263
style={iconContainerDirectionStyles}
255264
data-component-name={`AnalyticalTableHeaderIconsContainer-${id}`}
256265
>
257-
{isFiltered && <Icon name={iconFilter} />}
258-
{column.isSorted && <Icon name={column.isSortedDesc ? iconSortDescending : iconSortAscending} />}
259-
{column.isGrouped && <Icon name={iconGroup} />}
266+
{isFiltered && <Icon name={iconFilter} aria-hidden />}
267+
{column.isSorted && (
268+
<Icon name={column.isSortedDesc ? iconSortDescending : iconSortAscending} aria-hidden />
269+
)}
270+
{column.isGrouped && <Icon name={iconGroup} aria-hidden />}
260271
</div>
261272
</div>
262273
{hasPopover && popoverOpen && (

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface UpdatedCellProptypes {
88
'aria-colindex'?: number;
99
}
1010

11-
const getCellProps = (cellProps, { cell: { column, row, value }, instance }) => {
11+
const setCellProps = (cellProps, { cell: { column, row, value }, instance }) => {
1212
const columnIndex = instance.visibleColumns.findIndex(({ id }) => id === column.id);
1313
const { alwaysShowSubComponent, renderRowSubComponent, translatableTexts, selectionMode, selectionBehavior } =
1414
instance.webComponentsReactProperties;
@@ -29,13 +29,18 @@ const getCellProps = (cellProps, { cell: { column, row, value }, instance }) =>
2929

3030
if ((isFirstUserCol && rowIsExpandable) || (row.isGrouped && row.canExpand)) {
3131
updatedCellProps.onKeyDown = row.getToggleRowExpandedProps?.()?.onKeyDown;
32+
let ariaLabel = '';
33+
if (row.isGrouped) {
34+
ariaLabel += translatableTexts.groupedA11yText + ',';
35+
}
3236
if (row.isExpanded) {
3337
updatedCellProps['aria-expanded'] = 'true';
34-
updatedCellProps['aria-label'] = translatableTexts.collapseA11yText;
38+
ariaLabel += ` ${translatableTexts.collapseA11yText}`;
3539
} else {
3640
updatedCellProps['aria-expanded'] = 'false';
37-
updatedCellProps['aria-label'] = translatableTexts.expandA11yText;
41+
ariaLabel += ` ${translatableTexts.expandA11yText}`;
3842
}
43+
updatedCellProps['aria-label'] = ariaLabel;
3944
} else if (
4045
(selectionMode !== AnalyticalTableSelectionMode.None &&
4146
selectionBehavior !== AnalyticalTableSelectionBehavior.RowSelector &&
@@ -54,7 +59,34 @@ const getCellProps = (cellProps, { cell: { column, row, value }, instance }) =>
5459
return [cellProps, updatedCellProps];
5560
};
5661

62+
const setHeaderProps = (headerProps, { column, instance }) => {
63+
const { translatableTexts } = instance.webComponentsReactProperties;
64+
65+
if (!column) {
66+
return headerProps;
67+
}
68+
const isFiltered = column?.filterValue && column?.filterValue.length > 0;
69+
70+
const updatedProps = {};
71+
if (column.isSorted) {
72+
updatedProps['aria-sort'] = column.isSortedDesc ? 'descending' : 'ascending';
73+
}
74+
if (isFiltered) {
75+
updatedProps['aria-label'] = translatableTexts.filteredA11yText;
76+
}
77+
if (column.isGrouped) {
78+
if (updatedProps['aria-label']) {
79+
updatedProps['aria-label'] += ` ${translatableTexts.groupedA11yText}`;
80+
} else {
81+
updatedProps['aria-label'] = translatableTexts.groupedA11yText;
82+
}
83+
}
84+
85+
return [headerProps, { isFiltered, ...updatedProps }];
86+
};
87+
5788
export const useA11y = (hooks) => {
58-
hooks.getCellProps.push(getCellProps);
89+
hooks.getCellProps.push(setCellProps);
90+
hooks.getHeaderProps.push(setHeaderProps);
5991
};
6092
useA11y.pluginName = 'useA11y';

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import {
5555
COLLAPSE_PRESS_SPACE,
5656
EXPAND_NODE,
5757
EXPAND_PRESS_SPACE,
58+
FILTERED,
59+
GROUPED,
5860
INVALID_TABLE,
5961
SELECT_PRESS_SPACE,
6062
UNSELECT_PRESS_SPACE
@@ -671,7 +673,9 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
671673
selectA11yText: i18nBundle.getText(SELECT_PRESS_SPACE),
672674
unselectA11yText: i18nBundle.getText(UNSELECT_PRESS_SPACE),
673675
expandNodeA11yText: i18nBundle.getText(EXPAND_NODE),
674-
collapseNodeA11yText: i18nBundle.getText(COLLAPSE_NODE)
676+
collapseNodeA11yText: i18nBundle.getText(COLLAPSE_NODE),
677+
filteredA11yText: i18nBundle.getText(FILTERED),
678+
groupedA11yText: i18nBundle.getText(GROUPED)
675679
},
676680
tagNamesWhichShouldNotSelectARow,
677681
tableRef,

packages/main/src/i18n/messagebundle.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ SORT_DESCENDING=Sort Descending
4747
#XTXT
4848
GROUP=Group
4949

50+
#XACT: Aria announcement for grouped table rows
51+
GROUPED=Grouped
52+
5053
#XTXT
5154
UNGROUP=Ungroup
5255

@@ -246,6 +249,9 @@ UNSELECT_PRESS_SPACE=To unselect the row, press the spacebar
246249
#XACT: Aria label text for an invalid table with overlay
247250
INVALID_TABLE=Invalid Table
248251

252+
#XACT: Aria announcement for filtered table columns
253+
FILTERED=Filtered
254+
249255
#XACT: Aria Role description for Card Headers
250256
ARIA_DESC_CARD_HEADER=Card Header
251257

0 commit comments

Comments
 (0)