Skip to content
Merged
272 changes: 173 additions & 99 deletions packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const kitchenSinkArgs: AnalyticalTablePropTypes = {
},
{
Header: () => <span>Friend Age</span>,
headerLabel: 'Friend Age',
headerLabel: 'Custom Header Label',
accessor: 'friend.age',
autoResizable: true,
hAlign: TextAlign.End,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ export const ColumnHeader = (props: ColumnHeaderProps) => {
borderInlineStart: dragOver ? `3px solid ${ThemingParameters.sapSelectedColor}` : undefined,
}}
aria-haspopup={hasPopover ? 'menu' : undefined}
aria-expanded={hasPopover ? (popoverOpen ? 'true' : 'false') : undefined}
aria-controls={hasPopover ? `${id}-popover` : undefined}
role={role}
draggable={isDraggable}
onDragEnter={onDragEnter}
Expand Down Expand Up @@ -278,6 +280,7 @@ export const ColumnHeader = (props: ColumnHeaderProps) => {
// render the popover and add the props to the table instance
column.render(RenderColumnTypes.Popover, {
popoverProps: {
id: `${id}-popover`,
openerRef: columnHeaderRef,
setOpen: setPopoverOpen,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
}
prepareRow(row);
const { key, ...rowProps } = row.getRowProps({
'aria-rowindex': virtualRow.index + 1,
'aria-rowindex': virtualRow.index + 2,
'data-virtual-row-index': virtualRow.index,
});
const isNavigatedCell = typeof markNavigatedRow === 'function' ? markNavigatedRow(row) : false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ export const Cell = (props: CellInstance) => {
if (isGrouped) {
cellContent += ` (${row.subRows.length})`;
}

return (
<span
id={`${webComponentsReactProperties.uniqueId}${column.id}${row.id}`}
title={cellContent}
className={webComponentsReactProperties.classes.tableText}
data-column-id-cell-text={column.id}
// VoiceOver announces blank because of `aria-hidden` although `aria-labelledby` is set on the `gridcell` element - this is a known bug and there's no workaround
aria-hidden="true"
>
{cellContent}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import type { TableInstanceWithPopoverProps } from '../../types/index.js';
import { RenderColumnTypes } from '../../types/index.js';

export const ColumnHeaderModal = (instance: TableInstanceWithPopoverProps) => {
const { setOpen, openerRef } = instance.popoverProps;
const { setOpen, openerRef, id } = instance.popoverProps;
const { column, state, webComponentsReactProperties } = instance;
const { isRtl, groupBy } = state;
const { onGroup, onSort, classes: classNames } = webComponentsReactProperties;
Expand Down Expand Up @@ -174,10 +174,11 @@ export const ColumnHeaderModal = (instance: TableInstanceWithPopoverProps) => {
ref.current.open = true;
});
}
}, []);
}, [openerRef]);

return (
<Popover
id={id}
hideArrow
horizontalAlign={horizontalAlign}
placement={PopoverPlacement.Bottom}
Expand Down
69 changes: 50 additions & 19 deletions packages/main/src/components/AnalyticalTable/hooks/useA11y.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,34 @@ import type { ReactTableHooks, TableInstance } from '../types/index.js';

interface UpdatedCellProptypes {
onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
'aria-expanded'?: string | boolean;
'aria-expanded'?: string;
'aria-label'?: string;
'aria-colindex'?: number;
role?: string;
'aria-colindex': number;
'aria-describedby'?: string;
'aria-labelledby': string;
role: string;
}

const setCellProps = (cellProps, { cell, instance }: { cell: TableInstance['cell']; instance: TableInstance }) => {
const { column, row, value } = cell;
const columnIndex = instance.visibleColumns.findIndex(({ id }) => id === column.id);
const { alwaysShowSubComponent, renderRowSubComponent, translatableTexts, selectionMode, selectionBehavior } =
instance.webComponentsReactProperties;
const updatedCellProps: UpdatedCellProptypes = { 'aria-colindex': columnIndex + 1, role: 'gridcell' }; // aria index is 1 based, not 0
const {
alwaysShowSubComponent,
renderRowSubComponent,
selectionMode,
selectionBehavior,
a11yElementIds,
uniqueId,
canUseVoiceOver,
} = instance.webComponentsReactProperties;

const updatedCellProps: UpdatedCellProptypes = {
// aria index is 1 based, not 0
'aria-colindex': columnIndex + 1,
role: 'gridcell',
// header label
'aria-labelledby': `${uniqueId}${column.id}${row.id}` + (canUseVoiceOver ? ` ${uniqueId}${column.id}` : ''),
};

const RowSubComponent = typeof renderRowSubComponent === 'function' ? renderRowSubComponent(row) : undefined;
const rowIsExpandable = row.canExpand || (RowSubComponent && !alwaysShowSubComponent);
Expand All @@ -30,24 +46,16 @@ const setCellProps = (cellProps, { cell, instance }: { cell: TableInstance['cell

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

if ((isFirstUserCol && rowIsExpandable) || (row.isGrouped && row.canExpand)) {
updatedCellProps.onKeyDown = row.getToggleRowExpandedProps?.()?.onKeyDown;
let ariaLabel = '';
if (row.isGrouped) {
ariaLabel += translatableTexts.groupedA11yText + ',';
}
if (row.isExpanded) {
updatedCellProps['aria-expanded'] = 'true';
ariaLabel += ` ${translatableTexts.collapseA11yText}`;
updatedCellProps['aria-describedby'] = ' ' + a11yElementIds.cellCollapseDescId;
} else {
updatedCellProps['aria-expanded'] = 'false';
ariaLabel += ` ${translatableTexts.expandA11yText}`;
updatedCellProps['aria-describedby'] = ' ' + a11yElementIds.cellExpandDescId;
}
updatedCellProps['aria-label'] += ariaLabel;
} else if (
(selectionMode !== AnalyticalTableSelectionMode.None &&
selectionBehavior !== AnalyticalTableSelectionBehavior.RowSelector &&
Expand All @@ -56,17 +64,21 @@ const setCellProps = (cellProps, { cell, instance }: { cell: TableInstance['cell
) {
if (row.isSelected) {
updatedCellProps['aria-selected'] = 'true';
updatedCellProps['aria-label'] += ` ${translatableTexts.unselectA11yText}`;
updatedCellProps['aria-describedby'] = ' ' + a11yElementIds.cellUnselectDescId;
} else {
updatedCellProps['aria-selected'] = 'false';
updatedCellProps['aria-label'] += ` ${translatableTexts.selectA11yText}`;
updatedCellProps['aria-describedby'] = ' ' + a11yElementIds.cellSelectDescId;
}
}
const { cellLabel } = cell.column;
if (typeof cellLabel === 'function') {
cell.cellLabel = updatedCellProps['aria-label'];
const cellHeaderLabel = column.headerLabel || (typeof column.Header === 'string' ? column.Header : '');
const cellValueLabel = value || value === 0 ? `${value} ` : '';
cell.cellLabel = `${cellHeaderLabel} ${cellValueLabel}`;
updatedCellProps['aria-label'] = cellLabel({ cell, instance });
updatedCellProps['aria-labelledby'] = undefined;
}

return [cellProps, updatedCellProps];
};

Expand Down Expand Up @@ -108,11 +120,30 @@ const setHeaderProps = (
: translatableTexts.selectAllA11yText;
}

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

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

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

updatedProps['aria-label'] ||= undefined;

return [headerProps, { isFiltered, ...updatedProps }];
};

const setHeaderGroupProps = (props) => {
return [props, { 'aria-rowindex': 1 }];
};

export const useA11y = (hooks: ReactTableHooks) => {
hooks.getCellProps.push(setCellProps);
hooks.getHeaderProps.push(setHeaderProps);
hooks.getHeaderGroupProps.push(setHeaderGroupProps);
};
useA11y.pluginName = 'useA11y';
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { isIOS, isMac } from '@ui5/webcomponents-react-base/Device';
import { useEffect, useState } from 'react';

/**
* SSR ready check for macOS or iOS (Apple VoiceOver support).
*/
export function useCanUseVoiceOver() {
const [canUseVoiceOver, setCanUseVoiceOver] = useState(false);

useEffect(() => {
setCanUseVoiceOver(isIOS() || isMac());
}, []);

return canUseVoiceOver;
}
Loading
Loading