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
Original file line number Diff line number Diff line change
Expand Up @@ -4517,26 +4517,71 @@ describe('AnalyticalTable', () => {
cy.focused().should('have.attr', 'data-column-index', '0');

cy.realPress('Tab');
cy.focused().should('have.text', 'After');
cy.log('Cell 1-0');
cy.focused().should('have.attr', 'data-row-index', '1');
cy.focused().should('have.attr', 'data-column-index', '0');

cy.realPress(['Shift', 'Tab']);
cy.log('Cell 0-0');
cy.log('Header 0-0');
cy.focused().should('have.attr', 'data-row-index', '0');
cy.focused().should('have.attr', 'data-column-index', '0');

cy.realPress('Tab');
cy.log('Cell 1-0');
cy.realPress('ArrowDown');
cy.focused().should('have.attr', 'data-row-index', '1');
cy.focused().should('have.attr', 'data-column-index', '0');

cy.realPress('ArrowDown');
cy.realPress('ArrowDown');
cy.realPress('ArrowRight');
cy.realPress('ArrowRight');
cy.log('Cell 3-2');
cy.focused().should('have.attr', 'data-row-index', '3');
cy.focused().should('have.attr', 'data-column-index', '2');

cy.realPress(['Shift', 'Tab']);
cy.log('Header 0-2');
cy.focused().should('have.attr', 'data-row-index', '0');
cy.focused().should('have.attr', 'data-column-index', '2');

cy.realPress('Tab');
cy.focused().should('have.text', 'After');
cy.log('Cell 3-2');
cy.focused().should('have.attr', 'data-row-index', '3');
cy.focused().should('have.attr', 'data-column-index', '2');

cy.realPress('Home');
cy.log('Cell 3-0');
cy.focused().should('have.attr', 'data-row-index', '3');
cy.focused().should('have.attr', 'data-column-index', '0');

cy.realPress(['Shift', 'Tab']);
cy.log('Cell 1-0');
cy.focused().should('have.attr', 'data-row-index', '1');
cy.log('Header 0-0');
cy.focused().should('have.attr', 'data-row-index', '0');
cy.focused().should('have.attr', 'data-column-index', '0');

cy.realPress('ArrowRight');
cy.realPress('ArrowRight');
cy.realPress('ArrowRight');
cy.log('Header 0-3');
cy.focused().should('have.attr', 'data-row-index', '0');
cy.focused().should('have.attr', 'data-column-index', '3');

cy.realPress('Tab');
cy.log('Cell 3-3');
cy.focused().should('have.attr', 'data-row-index', '3');
cy.focused().should('have.attr', 'data-column-index', '3');

cy.realPress('Tab');
cy.focused().should('have.text', 'After');

cy.realPress(['Shift', 'Tab']);
cy.log('Cell 3-3');
cy.focused().should('have.attr', 'data-row-index', '3');
cy.focused().should('have.attr', 'data-column-index', '3');

cy.realPress('Home');
cy.realPress('PageUp');
cy.log('Cell 1-0');
cy.realPress('F2');
cy.log('Input 1-0');
cy.focused().should('have.attr', 'type', 'text');
Expand Down Expand Up @@ -4613,6 +4658,9 @@ describe('AnalyticalTable', () => {
cy.focused().should('have.attr', 'data-row-index', '0');
cy.focused().should('have.attr', 'data-column-index', '0');
cy.realPress('Tab');
cy.focused().should('have.attr', 'data-row-index', '1');
cy.focused().should('have.attr', 'data-column-index', '0');
cy.realPress('Tab');
cy.focused().should('have.text', 'After');
cy.realPress(['Shift', 'Tab']);
cy.realPress('ArrowDown');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ To **ensure the hook works correctly**, make sure that:
The hook manages focus, keyboard navigation, and `tabindex` for cells with interactive content:

- Pressing `F2` moves focus between the cell container and its first interactive element.
- Pressing `Tab` on a focused header cell moves focus to the body cell in the same column at the last focused body row (or the first row if none was focused).
- Pressing `Shift+Tab` on a focused body cell moves focus back to the header cell of the same column.
- Updates the cell's `aria-label` with the interactive element's name for accessibility.
- Prevents standard navigation keys from interfering when editing a cell.

Expand Down
11 changes: 6 additions & 5 deletions packages/main/src/components/AnalyticalTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,19 +118,20 @@ const measureElement = (el: HTMLElement) => {
* The `AnalyticalTable` provides a set of convenient functions for responsive table design, including virtualization of rows and columns, infinite scrolling and customizable columns that will, unless otherwise defined, distribute the available space equally among themselves.
* It also provides several possibilities for working with the data, including sorting, filtering, grouping and aggregation.
*
* __Note:__ The `AnalyticalTable` has some limitations and includes features that do not have a defined design specification.
* To follow UXC guidelines, please refer to the table below:
* __Note:__ Some features listed below have technical limitations, lack a defined design specification, or should be enabled by default to align with UXC guidelines:
*
*| Function / Feature | Reason |
*|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
*| No sticky columns/rows | Not supported due to technical limitations. |
*| `useF2CellEdit` plugin hook | To mimic the `sap.ui.table` edit-mode and tabbing behavior, use the `useF2CellEdit` plugin hook. |
*| No sticky columns/rows | Not supported due to technical limitations. |
*| Pop-in behavior | The `sap.ui.table` doesn’t support pop-in behavior (unlike `sap.m.Table`); it’s unclear whether this should be part of the design. |
*| `visibleRowCountMode: "Auto"` | `"AutoWithEmptyRows"` is preferred. `"Auto"` mode can lead to inconsistent table heights depending on the container. |
*| `alwaysShowBusyIndicator` | Should generally be `true`, only if loading times are over 1 second, the default skeleton loading indicator is sufficient: [Fiori Skeleton Loading](https://www.sap.com/design-system/fiori-design-ios/ui-elements/patterns/skeleton-loading/?external). |
*| `scaleWidthMode` | Only the default mode is available out of the box for the `sap.m.Table`; similar behavior to the `"Grow"` mode can be achieved in `sap.ui.table` using `autoResizeColumn`. |
*| `renderRowSubComponent` | There is no design/UX concept for this functionality. |
*| `useIndeterminateRowSelection` | There is no design/UX concept for this functionality. |
*| `renderRowSubComponent` | There is no design/UX concept for this functionality. |
*| `useIndeterminateRowSelection` | There is no design/UX concept for this functionality. |
*| `useRowDisableSelection` (deprecated) | Table rows should not be disabled. |
*
*/
const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTypes>((props, ref) => {
const {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useI18nBundle } from '@ui5/webcomponents-react-base';
import type { Ui5DomRef } from '@ui5/webcomponents-react-base';
import type { FocusEventHandler, KeyboardEventHandler } from 'react';
import { useCallback, useEffect } from 'react';
import { INCLUDES_X } from '../../../i18n/i18n-defaults.js';
import type { CellInstance, CellType, ReactTableHooks, TableInstance } from '../types/index.js';
import { useCallback, useEffect, useRef } from 'react';
import { INCLUDES_X, MOVE_TO_CONTENT_F2 } from '../../../i18n/i18n-defaults.js';
import type { CellInstance, CellType, ColumnType, ReactTableHooks, TableInstance } from '../types/index.js';
import { NAVIGATION_KEYS } from '../util/index.js';

const NON_STANDARD_INTERACTIVE_ELEMENTS = [
Expand All @@ -28,6 +28,8 @@ const NON_STANDARD_INTERACTIVE_ELEMENTS = [
*
* It manages focus, keyboard navigation, and `tabindex` for cells with interactive content:
* - Pressing `F2` moves focus between the cell container and its first interactive element.
* - Pressing `Tab` on a focused header cell moves focus to the body cell in the same column at the last focused body row (or the first row if none was focused).
* - Pressing `Shift+Tab` on a focused body cell moves focus back to the header cell of the same column.
* - Updates the cell's `aria-label` with the interactive element's name for accessibility.
* - Prevents standard navigation keys from interfering when editing a cell.
*
Expand Down Expand Up @@ -64,10 +66,12 @@ const NON_STANDARD_INTERACTIVE_ELEMENTS = [
*/
export const useF2CellEdit = (hooks: ReactTableHooks) => {
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
const lastFocusedBodyRowRef = useRef<number | null>(null);

const setCellProps = useCallback(
(props, { cell, instance }: { cell: CellType; instance: TableInstance }) => {
const { dispatch, state } = instance;
const { dispatch, state, webComponentsReactProperties } = instance;
const { tableRef } = webComponentsReactProperties;
const { interactiveElementName } = cell.column;
const inputName =
typeof interactiveElementName === 'function' ? interactiveElementName(cell) : interactiveElementName;
Expand Down Expand Up @@ -96,6 +100,25 @@ export const useF2CellEdit = (hooks: ReactTableHooks) => {
e.currentTarget.focus();
}
}

// Shift+Tab on body cell -> focus same column header cell
if (e.key === 'Tab' && e.shiftKey && e.currentTarget === e.target) {
const rowIndex = parseInt(e.currentTarget.dataset.rowIndex, 10);
const columnIndex = parseInt(e.currentTarget.dataset.columnIndex, 10);

if (rowIndex > 0) {
lastFocusedBodyRowRef.current = rowIndex;
const headerCell: HTMLElement | null = tableRef.current.querySelector(
`div[data-column-index="${columnIndex}"][data-row-index="0"]`,
);
if (headerCell) {
e.preventDefault();
e.currentTarget.tabIndex = -1;
headerCell.tabIndex = 0;
headerCell.focus();
}
}
}
};

const handleFocus: FocusEventHandler<HTMLDivElement> = (e) => {
Expand All @@ -108,14 +131,66 @@ export const useF2CellEdit = (hooks: ReactTableHooks) => {
} else {
dispatch({ type: 'CELL_CONTENT_TAB_INDEX', payload: -1 });
}

const rowIndex = parseInt(e.currentTarget.dataset.rowIndex, 10);
if (rowIndex > 0) {
lastFocusedBodyRowRef.current = rowIndex;
}
};

return [props, { onKeyDown: handleKeyDown, onFocus: handleFocus, 'aria-label': ariaLabel }];
},
[i18nBundle],
);

const setHeaderProps = useCallback((headerProps, { instance }: { instance: TableInstance; column: ColumnType }) => {
const { webComponentsReactProperties } = instance;
const { tableRef } = webComponentsReactProperties;

// Tab on header cell -> focus same column body cell
const handleKeyDown: KeyboardEventHandler<HTMLElement> = (e) => {
if (typeof headerProps.onKeyDown === 'function') {
headerProps.onKeyDown(e);
}

if (e.key === 'Tab' && !e.shiftKey && e.currentTarget === e.target) {
const columnIndex = parseInt(e.currentTarget.dataset.columnIndex, 10);
const targetRowIndex = lastFocusedBodyRowRef.current ?? 1;
let targetCell: HTMLElement | null = tableRef.current.querySelector(
`div[data-column-index="${columnIndex}"][data-row-index="${targetRowIndex}"]`,
);
if (!targetCell) {
targetCell = tableRef.current.querySelector(
`div[data-column-index="${columnIndex}"][data-visible-row-index="1"]`,
);
}
if (targetCell) {
e.preventDefault();
e.currentTarget.tabIndex = -1;
targetCell.tabIndex = 0;
targetCell.focus();
targetCell.scrollIntoView({ block: 'nearest' });
}
}
};

return [headerProps, { onKeyDown: handleKeyDown }];
}, []);

const setTableProps = useCallback(
(tableProps) => {
const f2Description = i18nBundle.getText(MOVE_TO_CONTENT_F2);
const existingDescription = tableProps['aria-description'];
const ariaDescription = existingDescription ? `${existingDescription} ${f2Description}` : f2Description;

return [tableProps, { 'aria-description': ariaDescription }];
},
[i18nBundle],
);

hooks.getTableProps.push(setTableProps);
hooks.getCellProps.push(setCellProps);
hooks.getHeaderProps.push(setHeaderProps);
hooks.stateReducers.push(stateReducer);
hooks.useInstanceBeforeDimensions.push(useInstanceBeforeDimensions);
};
Expand Down
Loading