diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx b/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx
index a9c85b92f0e..f67e27bcea6 100644
--- a/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx
+++ b/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx
@@ -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;
@@ -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');
diff --git a/packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx b/packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx
index cd2cb639a05..c4f1d57f57e 100644
--- a/packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx
+++ b/packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx
@@ -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) => {
@@ -82,6 +83,7 @@ export const ColumnHeader = (props: ColumnHeaderProps) => {
title,
'aria-label': ariaLabel,
'aria-sort': ariaSort,
+ 'aria-describedby': ariaDescribedBy,
showVerticalEndBorder,
classNames,
} = props;
@@ -216,6 +218,7 @@ export const ColumnHeader = (props: ColumnHeaderProps) => {
onKeyUp={handleHeaderCellKeyUp}
aria-label={ariaLabel}
aria-sort={ariaSort}
+ aria-describedby={ariaDescribedBy}
title={title}
>
{
- 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']) {
@@ -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;
diff --git a/packages/main/src/components/AnalyticalTable/hooks/useSelectionChangeCallback.ts b/packages/main/src/components/AnalyticalTable/hooks/useSelectionChangeCallback.ts
index 23b8b906c00..f43978de340 100644
--- a/packages/main/src/components/AnalyticalTable/hooks/useSelectionChangeCallback.ts
+++ b/packages/main/src/components/AnalyticalTable/hooks/useSelectionChangeCallback.ts
@@ -1,12 +1,31 @@
-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
>[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;
@@ -14,6 +33,8 @@ const useInstance = (instance: TableInstance) => {
ensurePluginOrder(plugins, ['useRowSelect'], 'useSelectionChangeCallback');
+ const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
+
const prevSelectedRowIdsRef = useRef(selectedRowIds);
// react-table instance is intentionally mutable
@@ -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) => {
diff --git a/packages/main/src/components/AnalyticalTable/index.tsx b/packages/main/src/components/AnalyticalTable/index.tsx
index 6e012b125ab..e8d52a2cb00 100644
--- a/packages/main/src/components/AnalyticalTable/index.tsx
+++ b/packages/main/src/components/AnalyticalTable/index.tsx
@@ -219,6 +219,8 @@ const AnalyticalTable = forwardRef(null);
const parentRef = useRef(null);
@@ -261,6 +263,8 @@ const AnalyticalTable = forwardRef
{i18nBundle.getText(UNSELECT_PRESS_SPACE)}
+
+
{/* expand */}
{i18nBundle.getText(EXPAND_PRESS_SPACE)}
diff --git a/packages/main/src/components/AnalyticalTable/types/index.ts b/packages/main/src/components/AnalyticalTable/types/index.ts
index d9e3cfe8f82..031e1815827 100644
--- a/packages/main/src/components/AnalyticalTable/types/index.ts
+++ b/packages/main/src/components/AnalyticalTable/types/index.ts
@@ -279,6 +279,8 @@ export interface WCRPropertiesType {
cellExpandDescId: string;
cellCollapseDescId: string;
cellEmptyDescId: string;
+ headerSelectAllDescId: string;
+ headerDeselectAllDescId: string;
};
translatableTexts: {
selectAllText: string;
diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties
index 0159fed3cda..04ecee664f3 100644
--- a/packages/main/src/i18n/messagebundle.properties
+++ b/packages/main/src/i18n/messagebundle.properties
@@ -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.