Skip to content
Merged
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 @@ -2996,7 +2996,7 @@ describe('AnalyticalTable', () => {
cy.get('[data-column-id="__ui5wcr__internal_selection_column"][role="columnheader"]').should(
'have.attr',
'aria-label',
' Selection Column',
'Selection Column',
);

let selectCalled = 0;
Expand Down Expand Up @@ -3493,12 +3493,18 @@ describe('AnalyticalTable', () => {
cy.get('[data-visible-column-index="0"][data-visible-row-index="0"]')
.as('selAll')
.should('have.attr', 'title', 'Select All')
.and('have.attr', 'aria-label', 'To select all rows, press the spacebar. Selection Column')
.click();
.and('have.attr', 'aria-label', 'Selection Column');
cy.get('@selAll')
.invoke('attr', 'aria-describedby')
.should('match', /^header-select-all-/);
cy.get('@selAll').click();

cy.get('@selAll').should('have.text', 'Select All');
cy.get('@selAll').contains('Select All').should('not.be.visible');
cy.get('@selAll').should('have.attr', 'aria-label', 'To deselect all rows, press the spacebar. Selection Column');
cy.get('@selAll').should('have.attr', 'aria-label', 'Selection Column');
cy.get('@selAll')
.invoke('attr', 'aria-describedby')
.should('match', /^header-deselect-all-/);

cy.get('@selectSpy').should('have.been.calledOnce');
cy.get('@selAll').should('have.attr', 'title', 'Deselect All');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface ColumnHeaderProps {
title?: string;
['aria-sort']?: AriaAttributes['aria-sort'];
['aria-label']?: AriaAttributes['aria-label'];
['aria-describedby']?: AriaAttributes['aria-describedby'];
}

export const ColumnHeader = (props: ColumnHeaderProps) => {
Expand Down Expand Up @@ -82,6 +83,7 @@ export const ColumnHeader = (props: ColumnHeaderProps) => {
title,
'aria-label': ariaLabel,
'aria-sort': ariaSort,
'aria-describedby': ariaDescribedBy,
showVerticalEndBorder,
classNames,
} = props;
Expand Down Expand Up @@ -216,6 +218,7 @@ export const ColumnHeader = (props: ColumnHeaderProps) => {
onKeyUp={handleHeaderCellKeyUp}
aria-label={ariaLabel}
aria-sort={ariaSort}
aria-describedby={ariaDescribedBy}
title={title}
>
<div
Expand Down
22 changes: 14 additions & 8 deletions packages/main/src/components/AnalyticalTable/hooks/useA11y.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,19 @@ const setHeaderProps = (
headerProps,
{ column, instance }: { column: TableInstance['column']; instance: TableInstance },
) => {
const { translatableTexts, selectionMode } = instance.webComponentsReactProperties;
const { translatableTexts, selectionMode, a11yElementIds } = instance.webComponentsReactProperties;

if (!column) {
return headerProps;
}
const isFiltered = column?.filterValue && column?.filterValue.length > 0;

const updatedProps = {};
const updatedProps: {
'aria-label'?: string;
'aria-sort'?: string;
'aria-describedby'?: string;
isFiltered?: boolean;
} = {};
updatedProps['aria-label'] = column.headerLabel ??= '';

if (updatedProps['aria-label']) {
Expand All @@ -132,21 +137,22 @@ const setHeaderProps = (
}

if (selectionMode === AnalyticalTableSelectionMode.Multiple && column.id === '__ui5wcr__internal_selection_column') {
updatedProps['aria-label'] += instance.isAllRowsSelected
? translatableTexts.deselectAllA11yText
: translatableTexts.selectAllA11yText;
// aria-describedby so body cells don't inherit it via aria-labelledby
updatedProps['aria-describedby'] = instance.isAllRowsSelected
? a11yElementIds.headerDeselectAllDescId
: a11yElementIds.headerSelectAllDescId;
}

if (column.id === '__ui5wcr__internal_selection_column') {
updatedProps['aria-label'] += ' ' + translatableTexts.selectionHeaderCellText;
updatedProps['aria-label'] += (updatedProps['aria-label'] ? ' ' : '') + translatableTexts.selectionHeaderCellText;
}

if (column.id === '__ui5wcr__internal_highlight_column') {
updatedProps['aria-label'] += ' ' + translatableTexts.highlightHeaderCellText;
updatedProps['aria-label'] += (updatedProps['aria-label'] ? ' ' : '') + translatableTexts.highlightHeaderCellText;
}

if (column.id === '__ui5wcr__internal_navigation_column') {
updatedProps['aria-label'] += ' ' + translatableTexts.navigationHeaderCellText;
updatedProps['aria-label'] += (updatedProps['aria-label'] ? ' ' : '') + translatableTexts.navigationHeaderCellText;
}

updatedProps['aria-label'] ||= undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
import { enrichEventWithDetails } from '@ui5/webcomponents-react-base/internal/utils';
import announce from '@ui5/webcomponents-base/dist/util/InvisibleMessage.js';
import { useI18nBundle } from '@ui5/webcomponents-react-base/hooks';
import { debounce, enrichEventWithDetails } from '@ui5/webcomponents-react-base/internal/utils';
import { useEffect, useRef } from 'react';
import { AnalyticalTableSelectionMode } from '../../../enums/AnalyticalTableSelectionMode.js';
import {
ALL_ROWS_DESELECTED,
ALL_ROWS_DESELECTED_FILTERED,
ALL_ROWS_SELECTED,
ALL_ROWS_SELECTED_FILTERED,
ROW_DESELECTED,
ROW_DESELECTED_MULTI,
ROW_DESELECTED_MULTI_FILTERED,
ROW_SELECTED,
ROW_SELECTED_MULTI,
ROW_SELECTED_MULTI_FILTERED,
} from '../../../i18n/i18n-defaults.js';
import { ensurePluginOrder } from '../react-table/index.js';
import type { AnalyticalTablePropTypes, ReactTableHooks, TableInstance } from '../types/index.js';

type OnRowSelectEvent = Parameters<NonNullable<AnalyticalTablePropTypes['onRowSelect']>>[0];
type OnRowSelectDetail = OnRowSelectEvent['detail'];

// debounce announce to prevent excessive successive announcements
const debouncedAnnounce = debounce((announcement: string) => {
announce(announcement, 'Polite');
}, 200);

const useInstance = (instance: TableInstance) => {
const { webComponentsReactProperties, rowsById, preFilteredRowsById, state, plugins } = instance;
const { selectedRowIds, filters, globalFilter } = state;
const { onRowSelect, selectionMode } = webComponentsReactProperties;

ensurePluginOrder(plugins, ['useRowSelect'], 'useSelectionChangeCallback');

const i18nBundle = useI18nBundle('@ui5/webcomponents-react');

const prevSelectedRowIdsRef = useRef(selectedRowIds);

// react-table instance is intentionally mutable
Expand Down Expand Up @@ -58,13 +79,46 @@ const useInstance = (instance: TableInstance) => {
selectedRowIds: payload.selectedRowIds,
}) as OnRowSelectEvent,
);
let allRowsMsgKey;
if (instance.isAllRowsSelected) {
allRowsMsgKey = isFiltered ? ALL_ROWS_SELECTED_FILTERED : ALL_ROWS_SELECTED;
} else {
allRowsMsgKey = isFiltered ? ALL_ROWS_DESELECTED_FILTERED : ALL_ROWS_DESELECTED;
}
debouncedAnnounce(i18nBundle.getText(allRowsMsgKey));
} else {
onRowSelect?.(enrichEventWithDetails(e as Event & { detail?: OnRowSelectDetail }, payload) as OnRowSelectEvent);

if (row) {
const isSelected = row.isSelected;
if (selectionMode === AnalyticalTableSelectionMode.Multiple) {
const count = instance.selectedFlatRows.length;
let rowMsgKey;
if (isSelected) {
rowMsgKey = isFiltered ? ROW_SELECTED_MULTI_FILTERED : ROW_SELECTED_MULTI;
} else {
rowMsgKey = isFiltered ? ROW_DESELECTED_MULTI_FILTERED : ROW_DESELECTED_MULTI;
}
debouncedAnnounce(i18nBundle.getText(rowMsgKey, count));
} else if (selectionMode === AnalyticalTableSelectionMode.Single) {
debouncedAnnounce(i18nBundle.getText(isSelected ? ROW_SELECTED : ROW_DESELECTED));
}
}
}
}

prevSelectedRowIdsRef.current = selectedRowIds;
}, [selectedRowIds, rowsById, preFilteredRowsById, filters, globalFilter, selectionMode, instance, onRowSelect]);
}, [
selectedRowIds,
rowsById,
preFilteredRowsById,
filters,
globalFilter,
selectionMode,
instance,
onRowSelect,
i18nBundle,
]);
};

export const useSelectionChangeCallback = (hooks: ReactTableHooks) => {
Expand Down
10 changes: 10 additions & 0 deletions packages/main/src/components/AnalyticalTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
const cellExpandDescId = `cell-expand-${uniqueId}`;
const cellCollapseDescId = `cell-collapse-${uniqueId}`;
const cellEmptyDescId = `cell-empty-${uniqueId}`;
const headerSelectAllDescId = `header-select-all-${uniqueId}`;
const headerDeselectAllDescId = `header-deselect-all-${uniqueId}`;

const tableRef = useRef<DivWithCustomScrollProp>(null);
const parentRef = useRef<DivWithCustomScrollProp>(null);
Expand Down Expand Up @@ -261,6 +263,8 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
cellExpandDescId,
cellCollapseDescId,
cellEmptyDescId,
headerSelectAllDescId,
headerDeselectAllDescId,
},
translatableTexts: {
selectAllText: i18nBundle.getText(SELECT_ALL),
Expand Down Expand Up @@ -945,6 +949,12 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
<span id={cellUnselectDescId} className={classNames.hiddenA11yText}>
{i18nBundle.getText(UNSELECT_PRESS_SPACE)}
</span>
<span id={headerSelectAllDescId} className={classNames.hiddenA11yText}>
{i18nBundle.getText(SELECT_ALL_PRESS_SPACE)}
</span>
<span id={headerDeselectAllDescId} className={classNames.hiddenA11yText}>
{i18nBundle.getText(UNSELECT_ALL_PRESS_SPACE)}
</span>
{/* expand */}
<span id={cellExpandDescId} className={classNames.hiddenA11yText}>
{i18nBundle.getText(EXPAND_PRESS_SPACE)}
Expand Down
2 changes: 2 additions & 0 deletions packages/main/src/components/AnalyticalTable/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ export interface WCRPropertiesType {
cellExpandDescId: string;
cellCollapseDescId: string;
cellEmptyDescId: string;
headerSelectAllDescId: string;
headerDeselectAllDescId: string;
};
translatableTexts: {
selectAllText: string;
Expand Down
30 changes: 30 additions & 0 deletions packages/main/src/i18n/messagebundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,36 @@ ROW_X_EXPANDED=Row {0} expanded
#XACT: ARIA live announcement for collapsed row number {0}
ROW_X_COLLAPSED=Row {0} collapsed

#XACT: ARIA live announcement when a row is selected (single selection mode)
ROW_SELECTED=Row selected

#XACT: ARIA live announcement when a row is deselected (single selection mode)
ROW_DESELECTED=Row deselected

#XACT: ARIA live announcement when a row is selected in multi selection mode. The placeholder is the total number of selected rows.
ROW_SELECTED_MULTI=Row selected, {0} rows selected

#XACT: ARIA live announcement when a row is deselected in multi selection mode. The placeholder is the total number of selected rows.
ROW_DESELECTED_MULTI=Row deselected, {0} rows selected

#XACT: ARIA live announcement when a row is selected in multi selection mode while a filter is active. The placeholder is the number of currently visible (non-filtered-out) selected rows.
ROW_SELECTED_MULTI_FILTERED=Row selected, {0} visible rows selected

#XACT: ARIA live announcement when a row is deselected in multi selection mode while a filter is active. The placeholder is the number of currently visible (non-filtered-out) selected rows.
ROW_DESELECTED_MULTI_FILTERED=Row deselected, {0} visible rows selected

#XACT: ARIA live announcement when the header "Select All" checkbox is toggled and all rows become selected
ALL_ROWS_SELECTED=All rows selected

#XACT: ARIA live announcement when the header "Select All" checkbox is toggled and all rows become deselected
ALL_ROWS_DESELECTED=All rows deselected

#XACT: ARIA live announcement when the header "Select All" checkbox is toggled while a filter is active and all visible (non-filtered-out) rows become selected
ALL_ROWS_SELECTED_FILTERED=All visible rows selected

#XACT: ARIA live announcement when the header "Select All" checkbox is toggled while a filter is active and all visible (non-filtered-out) rows become deselected
ALL_ROWS_DESELECTED_FILTERED=All visible rows deselected

#XACT: Aria label text for selectable table cells in unselected state
SELECT_PRESS_SPACE=To select the row, press the spacebar.

Expand Down
Loading