Skip to content
Open
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
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 Expand Up @@ -130,7 +130,7 @@ const kitchenSinkArgs: AnalyticalTablePropTypes = {
return `${cell.cellLabel} press TAB to focus active elements inside this cell`;
},
},
],
].slice(0, 2),
filterable: true,
alternateRowColor: true,
columnOrder: ['friend.name', 'friend.age', 'name'],
Expand Down Expand Up @@ -195,6 +195,7 @@ const meta = {
visibleRows: 5,
// sb actions has a huge impact on performance here.
onTableScroll: undefined,
header: 'TableTitle',
},
argTypes: {
data: { control: { disable: true } },
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