diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx b/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx
index 4e549ed872f..b99d6d7ecad 100644
--- a/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx
+++ b/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx
@@ -1,11 +1,37 @@
import ValueState from '@ui5/webcomponents-base/dist/types/ValueState.js';
+import paperPlaneIcon from '@ui5/webcomponents-icons/paper-plane.js';
import { ThemingParameters } from '@ui5/webcomponents-react-base';
import { useCallback, useEffect, useMemo, useRef, useState, version as reactVersion } from 'react';
import type {
+ AnalyticalTableCellInstance,
AnalyticalTableColumnDefinition,
AnalyticalTableDomRef,
AnalyticalTablePropTypes,
+ InputDomRef,
PopoverDomRef,
+ ButtonDomRef,
+ CheckBoxDomRef,
+ ComboBoxDomRef,
+ DatePickerDomRef,
+ DateRangePickerDomRef,
+ DateTimePickerDomRef,
+ DynamicDateRangeDomRef,
+ FileUploaderDomRef,
+ MessageViewButtonDomRef,
+ MultiComboBoxDomRef,
+ MultiInputDomRef,
+ RadioButtonDomRef,
+ RatingIndicatorDomRef,
+ SearchDomRef,
+ SegmentedButtonDomRef,
+ SelectDomRef,
+ SliderDomRef,
+ SplitButtonDomRef,
+ StepInputDomRef,
+ SwitchDomRef,
+ TextAreaDomRef,
+ TimePickerDomRef,
+ ToggleButtonDomRef,
} from '../..';
import {
AnalyticalTable,
@@ -17,11 +43,36 @@ import {
AnalyticalTableSubComponentsBehavior,
AnalyticalTableVisibleRowCountMode,
Button,
+ CheckBox,
+ ComboBox,
+ DatePicker,
+ DateRangePicker,
+ DateTimePicker,
+ DynamicDateRange,
+ FileUploader,
IndicationColor,
Input,
+ MessageViewButton,
+ MultiComboBox,
+ MultiInput,
Popover,
+ RadioButton,
+ RatingIndicator,
+ Search,
+ SegmentedButton,
+ SegmentedButtonItem,
+ Select,
+ Slider,
+ SplitButton,
+ StepInput,
+ Switch,
+ Tag,
Text,
+ TextArea,
+ TimePicker,
+ ToggleButton,
} from '../..';
+import { useF2CellEdit } from './pluginHooks/useF2CellEdit.js';
import { useManualRowSelect } from './pluginHooks/useManualRowSelect';
import { useRowDisableSelection } from './pluginHooks/useRowDisableSelection';
import { cssVarToRgb, cypressPassThroughTestsFactory } from '@/cypress/support/utils';
@@ -3933,6 +3984,141 @@ describe('AnalyticalTable', () => {
});
});
+ it('plugin hook: useF2CellEdit - navigation', () => {
+ const tableHooks = [useF2CellEdit];
+ cy.mount(
+ <>
+
+ {' '}
+
+ >,
+ );
+
+ cy.findByText('Before').click();
+
+ cy.realPress('Tab');
+ cy.log('Cell 0-0');
+ 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.text', 'After');
+
+ cy.realPress(['Shift', 'Tab']);
+ cy.log('Cell 0-0');
+ cy.focused().should('have.attr', 'data-row-index', '0');
+ cy.focused().should('have.attr', 'data-column-index', '0');
+
+ 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('Tab');
+ cy.focused().should('have.text', 'After');
+
+ cy.realPress(['Shift', 'Tab']);
+ 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('F2');
+ cy.log('Input 1-0');
+ cy.focused().should('have.attr', 'type', 'text');
+
+ cy.realPress('Tab');
+ cy.log('Input 1-1');
+ cy.focused().should('have.attr', 'type', 'text');
+
+ cy.realPress('Tab');
+ cy.log('Button 1-1');
+ cy.focused().should('have.attr', 'type', 'button');
+
+ cy.realPress('Tab');
+ cy.log('Button 1-3');
+ cy.focused().should('have.attr', 'type', 'button');
+
+ cy.realPress('Tab');
+ cy.log('Switch 1-5');
+ cy.focused().should('have.attr', 'role', 'switch');
+
+ for (let i = 0; i < 5; i++) {
+ cy.realPress('Tab');
+ }
+ cy.log('CheckBox 2-5');
+ cy.focused().should('have.attr', 'role', 'checkbox');
+
+ cy.realPress('F2');
+ cy.log('Cell 2-5');
+ cy.focused().should('have.attr', 'data-row-index', '2');
+ cy.focused().should('have.attr', 'data-column-index', '5');
+
+ cy.realPress('Tab');
+ cy.focused().should('have.text', 'After');
+ cy.realPress(['Shift', 'Tab']);
+ cy.focused().should('have.attr', 'data-row-index', '2');
+ cy.focused().should('have.attr', 'data-column-index', '5');
+
+ cy.realPress('PageDown');
+ cy.log('Cell 7-5');
+ cy.focused().should('have.attr', 'data-row-index', '7');
+ cy.focused().should('have.attr', 'data-column-index', '5');
+
+ cy.realPress('F2');
+ cy.get('[data-component-name="AnalyticalTableBody"]').then(($el) => {
+ const scrollTop = $el[0].scrollTop;
+ cy.realPress('PageUp');
+ cy.wrap($el).should(($elAfter) => {
+ expect($elAfter[0].scrollTop).to.eq(scrollTop);
+ });
+ });
+ });
+
+ it('plugin hook: useF2CellEdit - all ui5wc inputs', () => {
+ const tableHooks = [useF2CellEdit];
+ const dummyData = new Array(1).fill({});
+ cy.mount(
+ <>
+
+
+
+ >,
+ );
+
+ cy.findByText('Before').click();
+ cy.realPress('Tab');
+ cy.log('Cell 0-0');
+ 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.text', 'After');
+ cy.realPress(['Shift', 'Tab']);
+ cy.realPress('ArrowDown');
+
+ cy.realPress('F2');
+ allRelevantInputCompontentsForF2.forEach((_) => {
+ cy.realPress('Tab');
+ });
+ // SegmentedButton has two tab stops
+ cy.realPress('Tab');
+ cy.focused().should('have.text', 'After');
+
+ cy.realPress('F2');
+ allRelevantInputCompontentsForF2.forEach((_) => {
+ cy.realPress(['Shift', 'Tab']);
+ });
+ // SegmentedButton has two tab stops
+ cy.realPress(['Shift', 'Tab']);
+ cy.focused().should('have.text', 'Before');
+ });
+
cypressPassThroughTestsFactory(AnalyticalTable, { data, columns });
});
@@ -4019,6 +4205,69 @@ const mockNames = [
'Zara',
];
+const inputCols: AnalyticalTableColumnDefinition[] = [
+ {
+ Header: 'Input',
+ id: 'input',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'Input',
+ },
+ {
+ Header: 'Input & Button',
+ id: 'input_btn',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return (
+ <>
+
+
+ >
+ );
+ },
+ interactiveElementName: 'Input and Button',
+ },
+ {
+ Header: 'Text',
+ accessor: 'name',
+ },
+ {
+ Header: 'Button',
+ id: 'btn',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: () => 'Button',
+ },
+ {
+ Header: 'Non-interactive custom content',
+ accessor: 'friend.name',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ return {props.value};
+ },
+ },
+ {
+ Header: 'Switch or CheckBox',
+ id: 'switch_checkbox',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ if (props.row.index % 2) {
+ return ;
+ }
+ return ;
+ },
+ interactiveElementName: (props: AnalyticalTableCellInstance) => {
+ if (props.row.index % 2) {
+ return 'CheckBox';
+ }
+ return 'Switch';
+ },
+ },
+];
+
const columnsWithPopIn = [
{
Header: 'Name',
@@ -5665,3 +5914,311 @@ const dataTree = [
],
},
];
+
+const allRelevantInputCompontentsForF2 = [
+ {
+ Header: 'Button',
+ id: 'button',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'Button',
+ tagName: 'ui5-button',
+ },
+ // {
+ // Header: 'Calendar',
+ // id: 'calendar',
+ // Cell: (props: AnalyticalTableCellInstance) => {
+ // const callbackRef = useF2CellEdit.useCallbackRef(props);
+ // return ;
+ // },
+ // interactiveElementName: 'Calendar',
+ // tagName: 'ui5-calendar',
+ // },
+ {
+ Header: 'CheckBox',
+ id: 'check-box',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'CheckBox',
+ tagName: 'ui5-checkbox',
+ },
+ // {
+ // Header: 'ColorPicker',
+ // id: 'color-picker',
+ // Cell: (props: AnalyticalTableCellInstance) => {
+ // const callbackRef = useF2CellEdit.useCallbackRef(props);
+ // return ;
+ // },
+ // interactiveElementName: 'ColorPicker',
+ // tagName: 'ui5-color-picker',
+ // },
+ {
+ Header: 'ComboBox',
+ id: 'combo-box',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'ComboBox',
+ tagName: 'ui5-combobox',
+ },
+ {
+ Header: 'DatePicker',
+ id: 'date-picker',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'DatePicker',
+ tagName: 'ui5-date-picker',
+ },
+ {
+ Header: 'DateRangePicker',
+ id: 'date-range-picker',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'DateRangePicker',
+ tagName: 'ui5-daterange-picker',
+ },
+ {
+ Header: 'DateTimePicker',
+ id: 'date-time-picker',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'DateTimePicker',
+ tagName: 'ui5-datetime-picker',
+ },
+ {
+ Header: 'DynamicDateRange',
+ id: 'dynamic-date-range',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'DynamicDateRange',
+ tagName: 'ui5-dynamic-date-range',
+ },
+ {
+ Header: 'FileUploader',
+ id: 'file-uploader',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'FileUploader',
+ tagName: 'ui5-file-uploader',
+ },
+ {
+ Header: 'Input',
+ id: 'input',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'Input',
+ tagName: 'ui5-input',
+ },
+ {
+ Header: 'MessageViewButton',
+ id: 'message-view-button',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'MessageViewButton',
+ tagName: 'ui5-message-view-button',
+ },
+ {
+ Header: 'MultiComboBox',
+ id: 'multi-combo-box',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'MultiComboBox',
+ tagName: 'ui5-multi-combobox',
+ },
+ {
+ Header: 'MultiInput',
+ id: 'multi-input',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'MultiInput',
+ tagName: 'ui5-multi-input',
+ },
+ {
+ Header: 'RadioButton',
+ id: 'radio-button',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'RadioButton',
+ tagName: 'ui5-radio-button',
+ },
+ // {
+ // Header: 'RangeSlider',
+ // id: 'range-slider',
+ // Cell: (props: AnalyticalTableCellInstance) => {
+ // const callbackRef = useF2CellEdit.useCallbackRef(props);
+ // return ;
+ // },
+ // interactiveElementName: 'RangeSlider',
+ // tagName: 'ui5-range-slider',
+ // },
+ {
+ Header: 'RatingIndicator',
+ id: 'rating-indicator',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'RatingIndicator',
+ tagName: 'ui5-rating-indicator',
+ },
+ {
+ Header: 'Search',
+ id: 'search',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'Search',
+ tagName: 'ui5-search-field',
+ },
+ {
+ Header: 'SegmentedButton',
+ id: 'segmented-button',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return (
+
+ Btn1
+ Btn2
+
+ );
+ },
+ interactiveElementName: 'SegmentedButton',
+ tagName: 'ui5-segmented-button',
+ },
+ {
+ Header: 'Select',
+ id: 'select',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'Select',
+ tagName: 'ui5-select',
+ },
+ {
+ Header: 'Slider',
+ id: 'slider',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'Slider',
+ tagName: 'ui5-slider',
+ },
+ {
+ Header: 'SplitButton',
+ id: 'split-button',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'SplitButton',
+ tagName: 'ui5-split-button',
+ },
+ {
+ Header: 'StepInput',
+ id: 'step-input',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'StepInput',
+ tagName: 'ui5-step-input',
+ },
+ {
+ Header: 'Switch',
+ id: 'switch',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'Switch',
+ tagName: 'ui5-switch',
+ },
+ {
+ Header: 'TextArea',
+ id: 'text-area',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'TextArea',
+ tagName: 'ui5-textarea',
+ },
+ {
+ Header: 'TimePicker',
+ id: 'time-picker',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'TimePicker',
+ tagName: 'ui5-time-picker',
+ },
+ {
+ Header: 'ToggleButton',
+ id: 'toggle-button',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'ToggleButton',
+ tagName: 'ui5-toggle-button',
+ },
+ // {
+ // Header: 'Tokenizer',
+ // id: 'tokenizer',
+ // Cell: (props: AnalyticalTableCellInstance) => {
+ // const callbackRef = useF2CellEdit.useCallbackRef(props);
+ // return ;
+ // },
+ // interactiveElementName: 'Tokenizer',
+ // tagName: 'ui5-tokenizer',
+ // },
+ // {
+ // Header: 'UploadCollection',
+ // id: 'upload-collection',
+ // Cell: (props: AnalyticalTableCellInstance) => {
+ // const callbackRef = useF2CellEdit.useCallbackRef(props);
+ // return ;
+ // },
+ // interactiveElementName: 'UploadCollection',
+ // tagName: 'ui5-upload-collection',
+ // },
+ // {
+ // Header: 'VariantManagement',
+ // id: 'variant-management',
+ // Cell: (props: AnalyticalTableCellInstance) => {
+ // const callbackRef = useF2CellEdit.useCallbackRef(props);
+ // return ;
+ // },
+ // interactiveElementName: 'VariantManagement',
+ // tagName: 'ui5-variant-management',
+ // },
+];
diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTableHooks.stories.tsx b/packages/main/src/components/AnalyticalTable/AnalyticalTableHooks.stories.tsx
index 9f1c4202f96..2e5a1027bb2 100644
--- a/packages/main/src/components/AnalyticalTable/AnalyticalTableHooks.stories.tsx
+++ b/packages/main/src/components/AnalyticalTable/AnalyticalTableHooks.stories.tsx
@@ -4,13 +4,24 @@ import dataManualSelect from '@sb/mockData/FriendsManualSelect25.json';
import dataTree from '@sb/mockData/FriendsTree.json';
import type { Meta, StoryObj } from '@storybook/react-vite';
import InputType from '@ui5/webcomponents/dist/types/InputType.js';
+import paperPlaneIcon from '@ui5/webcomponents-icons/dist/paper-plane';
import { useCallback, useMemo, useReducer, useState } from 'react';
import { AnalyticalTableSelectionMode, FlexBoxAlignItems, FlexBoxDirection } from '../../enums';
-import { Button, CheckBox, Input, Label, ToggleButton, Text } from '../../webComponents';
+import { Button } from '../../webComponents/Button/index.js';
+import { CheckBox } from '../../webComponents/CheckBox/index.js';
+import type { InputDomRef } from '../../webComponents/Input/index.js';
+import { Input } from '../../webComponents/Input/index.js';
+import { Label } from '../../webComponents/Label/index.js';
+import { Switch } from '../../webComponents/Switch/index.js';
+import { Tag } from '../../webComponents/Tag/index.js';
+import { Text } from '../../webComponents/Text/index.js';
+import { ToggleButton } from '../../webComponents/ToggleButton/index.js';
import { FlexBox } from '../FlexBox';
import meta from './AnalyticalTable.stories';
import * as AnalyticalTableHooks from './pluginHooks/AnalyticalTableHooks';
+import { useF2CellEdit } from './pluginHooks/AnalyticalTableHooks';
import { AnalyticalTable } from './index';
+import type { AnalyticalTableCellInstance, AnalyticalTableColumnDefinition } from './index';
const pluginsMeta = {
...meta,
@@ -293,3 +304,76 @@ export const PluginOrderedMultiSort = {
);
},
};
+
+const inputCols: AnalyticalTableColumnDefinition[] = [
+ {
+ Header: 'Input',
+ id: 'input',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'Input',
+ },
+ {
+ Header: 'Input & Button',
+ id: 'input_btn',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return (
+ <>
+
+
+ >
+ );
+ },
+ interactiveElementName: 'Input and Button',
+ },
+ {
+ Header: 'Text',
+ accessor: 'name',
+ },
+ {
+ Header: 'Button',
+ id: 'btn',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: () => 'Button',
+ },
+ {
+ Header: 'Non-interactive custom content',
+ accessor: 'friend.name',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ return {props.value};
+ },
+ },
+ {
+ Header: 'Switch or CheckBox',
+ id: 'switch_checkbox',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ if (props.row.index % 2) {
+ return ;
+ }
+ return ;
+ },
+ interactiveElementName: (props: AnalyticalTableCellInstance) => {
+ if (props.row.index % 2) {
+ return 'CheckBox';
+ }
+ return 'Switch';
+ },
+ },
+];
+
+const tableHooks = [useF2CellEdit];
+
+export const F2CellEdit: Story = {
+ render(args) {
+ return (
+
+ );
+ },
+};
diff --git a/packages/main/src/components/AnalyticalTable/PluginF2CellEdit.mdx b/packages/main/src/components/AnalyticalTable/PluginF2CellEdit.mdx
new file mode 100644
index 00000000000..798fd102510
--- /dev/null
+++ b/packages/main/src/components/AnalyticalTable/PluginF2CellEdit.mdx
@@ -0,0 +1,115 @@
+import { ImportStatement } from '@sb/components/Import';
+import { Canvas, Meta } from '@storybook/addon-docs/blocks';
+import { DocsHeader, Footer } from '@sb/components';
+import * as ComponentStories from './AnalyticalTableHooks.stories';
+
+
+
+# AnalyticalTable Plugin: useF2CellEdit
+
+
+
+**Since: v2.14.0**
+
+A plugin hook that enables F2-based cell editing for interactive elements inside a cell.
+
+To **ensure the hook works correctly**, make sure that:
+
+- Each column containing interactive elements has the `interactiveElementName` property set. **Note:** This property is also used to describe the cell's content for screen readers.
+- The callback Ref returned by `useF2CellEdit.useCallbackRef` is attached to every interactive element within the cell.
+
+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.
+- Updates the cell's `aria-label` with the interactive element's name for accessibility.
+- Prevents standard navigation keys from interfering when editing a cell.
+
+## Example
+
+
+
+### Code
+
+```tsx
+import type {
+ AnalyticalTableCellInstance,
+ AnalyticalTableColumnDefinition,
+ InputDomRef,
+ AnalyticalTablePropTypes,
+} from '@ui5/webcomponents-react';
+import { AnalyticalTableHooks, AnalyticalTable, Button, CheckBox, Input, Switch, Tag } from '@ui5/webcomponents-react';
+import paperPlaneIcon from '@ui5/webcomponents-icons/dist/paper-plane';
+
+const { useF2CellEdit } = AnalyticalTableHooks;
+
+const columns: AnalyticalTableColumnDefinition[] = [
+ {
+ Header: 'Input',
+ id: 'input',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: 'Input',
+ },
+ {
+ Header: 'Input & Button',
+ id: 'input_btn',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return (
+ <>
+
+
+ >
+ );
+ },
+ interactiveElementName: 'Input and Button',
+ },
+ {
+ Header: 'Text',
+ accessor: 'name',
+ },
+ {
+ Header: 'Button',
+ id: 'btn',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ return ;
+ },
+ interactiveElementName: () => 'Button',
+ },
+ {
+ Header: 'Non-interactive custom content',
+ accessor: 'friend.name',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ return {props.value};
+ },
+ },
+ {
+ Header: 'Switch or CheckBox',
+ id: 'switch_checkbox',
+ Cell: (props: AnalyticalTableCellInstance) => {
+ const callbackRef = useF2CellEdit.useCallbackRef(props);
+ if (props.row.index % 2) {
+ return ;
+ }
+ return ;
+ },
+ interactiveElementName: (props: AnalyticalTableCellInstance) => {
+ if (props.row.index % 2) {
+ return 'CheckBox';
+ }
+ return 'Switch';
+ },
+ },
+];
+
+const tableHooks: AnalyticalTablePropTypes['tableHooks'] = [useF2CellEdit];
+
+function TableWithInputs({ data }) {
+ return ;
+}
+```
+
+
diff --git a/packages/main/src/components/AnalyticalTable/defaults/Column/Cell.tsx b/packages/main/src/components/AnalyticalTable/defaults/Column/Cell.tsx
index 55edb484581..2e3d5206c39 100644
--- a/packages/main/src/components/AnalyticalTable/defaults/Column/Cell.tsx
+++ b/packages/main/src/components/AnalyticalTable/defaults/Column/Cell.tsx
@@ -1,4 +1,12 @@
-export const Cell = ({ cell: { value = '', isGrouped }, column, row, webComponentsReactProperties }) => {
+import type { CellInstance } from '../../types/index.js';
+
+export const Cell = (props: CellInstance) => {
+ const {
+ cell: { value = '', isGrouped },
+ column,
+ row,
+ webComponentsReactProperties,
+ } = props;
let cellContent = `${value ?? ''}`;
if (isGrouped) {
cellContent += ` (${row.subRows.length})`;
diff --git a/packages/main/src/components/AnalyticalTable/hooks/useDynamicColumnWidths.ts b/packages/main/src/components/AnalyticalTable/hooks/useDynamicColumnWidths.ts
index 0714db194a8..ee6baccf6a0 100644
--- a/packages/main/src/components/AnalyticalTable/hooks/useDynamicColumnWidths.ts
+++ b/packages/main/src/components/AnalyticalTable/hooks/useDynamicColumnWidths.ts
@@ -318,13 +318,13 @@ const calculateSmartColumns = (columns: AnalyticalTableColumnDefinition[], insta
});
};
-const columnsDeps = (
+const useColumnsDeps = (
deps,
{ instance: { state, webComponentsReactProperties, visibleColumns, data, rows, columns } },
) => {
const isLoadingPlaceholder = !data?.length && webComponentsReactProperties.loading;
const hasRows = rows?.length > 0;
- // eslint-disable-next-line react-hooks/rules-of-hooks
+
const colsEqual = useMemo(() => {
return visibleColumns
?.filter(
@@ -497,5 +497,5 @@ const columns = (columns: TableInstance['columns'], { instance }: { instance: Ta
export const useDynamicColumnWidths = (hooks: ReactTableHooks) => {
hooks.columns.push(columns);
- hooks.columnsDeps.push(columnsDeps);
+ hooks.columnsDeps.push(useColumnsDeps);
};
diff --git a/packages/main/src/components/AnalyticalTable/hooks/useKeyboardNavigation.ts b/packages/main/src/components/AnalyticalTable/hooks/useKeyboardNavigation.ts
index 412d3dc40ed..84622d34f74 100644
--- a/packages/main/src/components/AnalyticalTable/hooks/useKeyboardNavigation.ts
+++ b/packages/main/src/components/AnalyticalTable/hooks/useKeyboardNavigation.ts
@@ -1,7 +1,8 @@
+import type { FocusEventHandler, KeyboardEvent, KeyboardEventHandler, MutableRefObject } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { actions } from 'react-table';
import type { ColumnType, ReactTableHooks, TableInstance } from '../types/index.js';
-import { getLeafHeaders } from '../util/index.js';
+import { getLeafHeaders, NAVIGATION_KEYS } from '../util/index.js';
const CELL_DATA_ATTRIBUTES = ['visibleColumnIndex', 'columnIndex', 'rowIndex', 'visibleRowIndex'];
@@ -27,7 +28,7 @@ const getFirstVisibleCell = (target, currentlyFocusedCell, noData) => {
}
};
-function recursiveSubComponentElementSearch(element) {
+function recursiveSubComponentElementSearch(element: HTMLElement): HTMLElement | null {
if (!element.parentElement) {
return null;
}
@@ -37,7 +38,7 @@ function recursiveSubComponentElementSearch(element) {
return recursiveSubComponentElementSearch(element.parentElement);
}
-const findParentCell = (target) => {
+const findParentCell = (target: HTMLElement | undefined | null): HTMLElement | null | undefined => {
if (target === undefined || target === null) return;
if (
(target.dataset.rowIndex !== undefined && target.dataset.columnIndex !== undefined) ||
@@ -49,7 +50,7 @@ const findParentCell = (target) => {
}
};
-const setFocus = (currentlyFocusedCell, nextElement) => {
+const setFocus = (currentlyFocusedCell: MutableRefObject, nextElement: HTMLElement | null) => {
currentlyFocusedCell.current.tabIndex = -1;
if (nextElement) {
nextElement.tabIndex = 0;
@@ -58,8 +59,8 @@ const setFocus = (currentlyFocusedCell, nextElement) => {
}
};
-const navigateFromActiveSubCompItem = (currentlyFocusedCell, e) => {
- setFocus(currentlyFocusedCell, recursiveSubComponentElementSearch(e.target));
+const navigateFromActiveSubCompItem = (currentlyFocusedCell: MutableRefObject, e: KeyboardEvent) => {
+ setFocus(currentlyFocusedCell, recursiveSubComponentElementSearch(e.target as HTMLElement));
};
const useGetTableProps = (
@@ -67,7 +68,7 @@ const useGetTableProps = (
{ instance: { webComponentsReactProperties, data, columns, state } }: { instance: TableInstance },
) => {
const { showOverlay, tableRef } = webComponentsReactProperties;
- const currentlyFocusedCell = useRef(null);
+ const currentlyFocusedCell = useRef(null);
const noData = data.length === 0;
useEffect(() => {
@@ -77,7 +78,7 @@ const useGetTableProps = (
}
}, [showOverlay]);
- const onTableBlur = (e) => {
+ const onTableBlur: FocusEventHandler = (e) => {
if (e.target.tagName === 'UI5-LI' || e.target.tagName === 'UI5-LI-CUSTOM') {
currentlyFocusedCell.current = null;
}
@@ -175,9 +176,13 @@ const useGetTableProps = (
currentlyFocusedCell.current.dataset.rowIndex ?? currentlyFocusedCell.current.dataset.subcomponentRowIndex,
10,
);
+
+ if (NAVIGATION_KEYS.has(e.key)) {
+ e.preventDefault();
+ }
+
switch (e.key) {
case 'End': {
- e.preventDefault();
const visibleColumns = tableRef.current.querySelector(
`div[data-component-name="AnalyticalTableHeaderRow"]`,
).children;
@@ -193,31 +198,29 @@ const useGetTableProps = (
return 0;
}, 0);
- const newElement = tableRef.current.querySelector(
+ const newElement: HTMLElement | null = tableRef.current.querySelector(
`div[data-visible-column-index="${lastVisibleColumn}"][data-row-index="${rowIndex}"]`,
);
setFocus(currentlyFocusedCell, newElement);
break;
}
case 'Home': {
- e.preventDefault();
- const newElement = tableRef.current.querySelector(
+ const newElement: HTMLElement | null = tableRef.current.querySelector(
`div[data-visible-column-index="0"][data-row-index="${rowIndex}"]`,
);
setFocus(currentlyFocusedCell, newElement);
break;
}
case 'PageDown': {
- e.preventDefault();
if (currentlyFocusedCell.current.dataset.rowIndex === '0') {
- const newElement = tableRef.current.querySelector(
+ const newElement: HTMLElement | null = tableRef.current.querySelector(
`div[data-column-index="${columnIndex}"][data-row-index="${rowIndex + 1}"]`,
);
setFocus(currentlyFocusedCell, newElement);
} else {
const lastVisibleRow = tableRef.current.querySelector(`div[data-component-name="AnalyticalTableBody"]`)
?.children?.[0].children.length;
- const newElement = tableRef.current.querySelector(
+ const newElement: HTMLElement | null = tableRef.current.querySelector(
`div[data-column-index="${columnIndex}"][data-visible-row-index="${lastVisibleRow}"]`,
);
setFocus(currentlyFocusedCell, newElement);
@@ -225,14 +228,13 @@ const useGetTableProps = (
break;
}
case 'PageUp': {
- e.preventDefault();
if (currentlyFocusedCell.current.dataset.rowIndex <= '1') {
- const newElement = tableRef.current.querySelector(
+ const newElement: HTMLElement | null = tableRef.current.querySelector(
`div[data-column-index="${columnIndex}"][data-row-index="0"]`,
);
setFocus(currentlyFocusedCell, newElement);
} else {
- const newElement = tableRef.current.querySelector(
+ const newElement: HTMLElement | null = tableRef.current.querySelector(
`div[data-column-index="${columnIndex}"][data-visible-row-index="1"]`,
);
setFocus(currentlyFocusedCell, newElement);
@@ -240,12 +242,11 @@ const useGetTableProps = (
break;
}
case 'ArrowRight': {
- e.preventDefault();
if (isActiveItemInSubComponent) {
navigateFromActiveSubCompItem(currentlyFocusedCell, e);
return;
}
- const newElement = tableRef.current.querySelector(
+ const newElement: HTMLElement | null = tableRef.current.querySelector(
`div[data-column-index="${columnIndex + (isRtl ? -1 : 1)}"][data-row-index="${rowIndex}"]`,
);
if (newElement) {
@@ -256,12 +257,11 @@ const useGetTableProps = (
break;
}
case 'ArrowLeft': {
- e.preventDefault();
if (isActiveItemInSubComponent) {
navigateFromActiveSubCompItem(currentlyFocusedCell, e);
return;
}
- const newElement = tableRef.current.querySelector(
+ const newElement: HTMLElement | null = tableRef.current.querySelector(
`div[data-column-index="${columnIndex - (isRtl ? -1 : 1)}"][data-row-index="${rowIndex}"]`,
);
if (newElement) {
@@ -272,7 +272,6 @@ const useGetTableProps = (
break;
}
case 'ArrowDown': {
- e.preventDefault();
if (isActiveItemInSubComponent) {
navigateFromActiveSubCompItem(currentlyFocusedCell, e);
return;
@@ -280,7 +279,7 @@ const useGetTableProps = (
const parent = currentlyFocusedCell.current.parentElement as HTMLDivElement;
const firstChildOfParent = parent?.children?.[0] as HTMLDivElement;
const hasSubcomponent = firstChildOfParent?.dataset?.subcomponent;
- const newElement = tableRef.current.querySelector(
+ const newElement: HTMLElement | null = tableRef.current.querySelector(
`div[data-column-index="${columnIndex}"][data-row-index="${rowIndex + 1}"]`,
);
if (hasSubcomponent && !currentlyFocusedCell.current?.dataset?.subcomponent) {
@@ -296,7 +295,6 @@ const useGetTableProps = (
break;
}
case 'ArrowUp': {
- e.preventDefault();
if (isActiveItemInSubComponent) {
navigateFromActiveSubCompItem(currentlyFocusedCell, e);
return;
@@ -306,7 +304,7 @@ const useGetTableProps = (
if (isSubComponent) {
prevRowIndex++;
}
- const previousRowCell = tableRef.current.querySelector(
+ const previousRowCell: HTMLElement | null = tableRef.current.querySelector(
`div[data-column-index="${columnIndex}"][data-row-index="${prevRowIndex}"]`,
);
const firstChildPrevRow = previousRowCell?.parentElement.children[0] as HTMLDivElement;
@@ -332,20 +330,29 @@ const useGetTableProps = (
if (showOverlay) {
return tableProps;
}
+
+ // keyboard nav is only enabled if the table is not in edit mode
+ const handleEditModeKeyDown: KeyboardEventHandler = (e) => {
+ if (typeof tableProps.onKeyDown === 'function') {
+ tableProps.onKeyDown(e);
+ }
+ };
+
return [
tableProps,
{
onFocus: onTableFocus,
- onKeyDown: onKeyboardNavigation,
+ onKeyDown: state.cellContentTabIndex === 0 ? handleEditModeKeyDown : onKeyboardNavigation,
onBlur: onTableBlur,
},
];
};
-function getPayload(e, column) {
+function getPayload(e: KeyboardEvent, column: ColumnType) {
e.preventDefault();
e.stopPropagation();
- const clientX = e.target.getBoundingClientRect().x + e.target.getBoundingClientRect().width;
+ const target = e.target as HTMLElement;
+ const clientX = target.getBoundingClientRect().x + target.getBoundingClientRect().width;
const columnId = column.id;
const columnWidth = column.totalWidth;
const headersToResize = getLeafHeaders(column);
@@ -358,7 +365,7 @@ const setHeaderProps = (
{ instance: { dispatch }, column }: { instance: TableInstance; column: ColumnType },
) => {
// resize col with keyboard
- const handleKeyDown = (e) => {
+ const handleKeyDown: KeyboardEventHandler = (e) => {
if (typeof headerProps.onKeyDown === 'function') {
headerProps.onKeyDown(e);
}
diff --git a/packages/main/src/components/AnalyticalTable/index.tsx b/packages/main/src/components/AnalyticalTable/index.tsx
index 4017f6ae470..844aae43482 100644
--- a/packages/main/src/components/AnalyticalTable/index.tsx
+++ b/packages/main/src/components/AnalyticalTable/index.tsx
@@ -89,6 +89,7 @@ import type {
AnalyticalTableState,
DivWithCustomScrollProp,
TableInstance,
+ CellInstance,
} from './types/index.js';
import { getRowHeight, getSubRowsByString, tagNamesWhichShouldNotSelectARow } from './util/index.js';
import { VerticalResizer } from './VerticalResizer.js';
@@ -932,4 +933,6 @@ export type {
AnalyticalTableDomRef,
AnalyticalTablePropTypes,
DivWithCustomScrollProp,
+ TableInstance as AnalyticalTableInstance,
+ CellInstance as AnalyticalTableCellInstance,
};
diff --git a/packages/main/src/components/AnalyticalTable/pluginHooks/AnalyticalTableHooks.ts b/packages/main/src/components/AnalyticalTable/pluginHooks/AnalyticalTableHooks.ts
index 6d7ab88c65a..14dfe7d5d79 100644
--- a/packages/main/src/components/AnalyticalTable/pluginHooks/AnalyticalTableHooks.ts
+++ b/packages/main/src/components/AnalyticalTable/pluginHooks/AnalyticalTableHooks.ts
@@ -1,4 +1,5 @@
import { useAnnounceEmptyCells } from './useAnnounceEmptyCells.js';
+import { useF2CellEdit } from './useF2CellEdit.js';
import { useIndeterminateRowSelection } from './useIndeterminateRowSelection.js';
import { useManualRowSelect } from './useManualRowSelect.js';
import { useOnColumnResize } from './useOnColumnResize.js';
@@ -7,6 +8,7 @@ import { useRowDisableSelection } from './useRowDisableSelection.js';
export {
useAnnounceEmptyCells,
+ useF2CellEdit,
useIndeterminateRowSelection,
useManualRowSelect,
useOnColumnResize,
diff --git a/packages/main/src/components/AnalyticalTable/pluginHooks/useF2CellEdit.ts b/packages/main/src/components/AnalyticalTable/pluginHooks/useF2CellEdit.ts
new file mode 100644
index 00000000000..7a2c7545de2
--- /dev/null
+++ b/packages/main/src/components/AnalyticalTable/pluginHooks/useF2CellEdit.ts
@@ -0,0 +1,220 @@
+import type { Ui5DomRef } from '@ui5/webcomponents-react-base';
+import { useI18nBundle } from '@ui5/webcomponents-react-base';
+import type { FocusEventHandler, KeyboardEventHandler } from 'react';
+import { useCallback, useEffect } from 'react';
+//todo: once available - add translation
+// import { INCLUDES_X } from '../../../i18n/i18n-defaults.js';
+import type { CellInstance, CellType, ReactTableHooks, TableInstance } from '../types/index.js';
+import { NAVIGATION_KEYS } from '../util/index.js';
+
+const NON_STANDARD_INTERACTIVE_ELEMENTS = [
+ '[ui5-checkbox]',
+ '[ui5-switch]',
+ '[ui5-radio-button]',
+ '[ui5-rating-indicator]',
+ '[ui5-segmented-button]',
+ '[ui5-select]',
+ '[ui5-slider]',
+];
+
+/**
+ * A plugin hook that enables F2-based cell editing for interactive elements inside a cell.
+ *
+ * To __ensure the hook works correctly__, make sure that:
+ *
+ * - Each column containing interactive elements has the `interactiveElementName` property set. __Note:__ This property is also used to describe the cell's content for screen readers.
+ * - The callback Ref returned by `useF2CellEdit.useCallbackRef` is attached to every interactive element within the cell.
+ *
+ * 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.
+ * - Updates the cell's `aria-label` with the interactive element's name for accessibility.
+ * - Prevents standard navigation keys from interfering when editing a cell.
+ *
+ * @example
+ * ```tsx
+ * import type {
+ * AnalyticalTableCellInstance,
+ * AnalyticalTableColumnDefinition,
+ * InputDomRef,
+ * AnalyticalTablePropTypes,
+ * } from '@ui5/webcomponents-react';
+ * import { AnalyticalTableHooks, AnalyticalTable, Input } from '@ui5/webcomponents-react';
+ *
+ * const columns: AnalyticalTableColumnDefinition[] = [
+ * {
+ * Header: 'Input',
+ * id: 'input',
+ * Cell: (props: AnalyticalTableCellInstance) => {
+ * const callbackRef = AnalyticalTableHooks.useF2CellEdit.useCallbackRef(props);
+ * return ;
+ * },
+ * interactiveElementName: 'Input',
+ * },
+ * ];
+ *
+ * const tableHooks: AnalyticalTablePropTypes['tableHooks'] = [AnalyticalTableHooks.useF2CellEdit];
+ *
+ * function TableWithInput() {
+ * return ;
+ * }
+ * ```
+ *
+ * @since 2.14.0
+ */
+export const useF2CellEdit = (hooks: ReactTableHooks) => {
+ const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
+
+ const setCellProps = useCallback(
+ (props, { cell, instance }: { cell: CellType; instance: TableInstance }) => {
+ const { dispatch, state } = instance;
+ const { interactiveElementName } = cell.column;
+ const inputName =
+ typeof interactiveElementName === 'function' ? interactiveElementName(cell) : interactiveElementName;
+ //todo: once available - add translation
+ // const ariaLabel =
+ // (interactiveElementName ? i18nBundle.getText(INCLUDES_X, inputName) : '') + ' ' + props['aria-label'];
+ const ariaLabel = (interactiveElementName ? `Includes ${inputName}` : '') + ' ' + props['aria-label'];
+
+ const handleKeyDown: KeyboardEventHandler = (e) => {
+ if (state.cellContentTabIndex === 0 && NAVIGATION_KEYS.has(e.key) && !e.key.includes('Arrow')) {
+ e.preventDefault();
+ }
+
+ if (e.key === 'F2') {
+ if (e.currentTarget === e.target && interactiveElementName) {
+ const interactiveElement = findFirstFocusableInside(e.target as HTMLElement);
+ if (interactiveElement) {
+ dispatch({ type: 'CELL_CONTENT_TAB_INDEX', payload: 0 });
+ e.currentTarget.tabIndex = -1;
+ requestAnimationFrame(() => {
+ interactiveElement.focus();
+ });
+ }
+ }
+ if (e.currentTarget !== e.target) {
+ dispatch({ type: 'CELL_CONTENT_TAB_INDEX', payload: -1 });
+ e.currentTarget.tabIndex = 0;
+ e.currentTarget.focus();
+ }
+ }
+ };
+
+ const handleFocus: FocusEventHandler = (e) => {
+ if (typeof props.onFocus === 'function') {
+ props.onFocus(e);
+ }
+
+ if (e.currentTarget !== e.target) {
+ dispatch({ type: 'CELL_CONTENT_TAB_INDEX', payload: 0 });
+ } else {
+ dispatch({ type: 'CELL_CONTENT_TAB_INDEX', payload: -1 });
+ }
+ };
+
+ return [props, { onKeyDown: handleKeyDown, onFocus: handleFocus, 'aria-label': ariaLabel }];
+ },
+ [i18nBundle],
+ );
+
+ hooks.getCellProps.push(setCellProps);
+ hooks.stateReducers.push(stateReducer);
+ hooks.useInstanceBeforeDimensions.push(useInstanceBeforeDimensions);
+};
+useF2CellEdit.pluginName = 'useF2CellEdit';
+
+/**
+ * Returns a callback ref for a cell's interactive element, setting its `tabindex` based on the cell state.
+ *
+ * **Must be attached to every interactive element inside the cell!**
+ *
+ * @param props - The table cell props containing state.
+ *
+ * @example
+ * ```tsx
+ * Cell: (props: AnalyticalTableCellInstance) => {
+ * const callbackRef = useF2CellEdit.useCallbackRef(props);
+ * return ;
+ * },
+ * ```
+ */
+useF2CellEdit.useCallbackRef = (props: CellInstance) => {
+ const cellContentTabIndex = props.state.cellContentTabIndex === -1 ? '-1' : '0';
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ return useCallback(
+ (node: T | null) => {
+ if (node) {
+ const setTabIndex = (el: Element | Ui5DomRef) => {
+ if (typeof (el as Ui5DomRef).getFocusDomRefAsync === 'function') {
+ void (el as Ui5DomRef)
+ .getFocusDomRefAsync()
+ .then((resolved) => setTabIndex(resolved))
+ .catch(() => {
+ // fail silently
+ });
+ } else {
+ el.setAttribute('tabindex', cellContentTabIndex);
+ }
+ };
+
+ setTabIndex(node);
+ }
+ },
+ [cellContentTabIndex],
+ );
+};
+
+const stateReducer: TableInstance['stateReducer'] = (state, action, _prevState) => {
+ const { payload, type } = action;
+
+ if (type === 'CELL_CONTENT_TAB_INDEX') {
+ return { ...state, cellContentTabIndex: payload };
+ }
+ return state;
+};
+
+function findFirstFocusableInside(element: HTMLElement) {
+ if (!element) return null;
+
+ function recursiveFindInteractiveElement(el) {
+ for (const child of el.children) {
+ const style = getComputedStyle(child);
+ if (child.disabled || style.display === 'none' || style.visibility === 'hidden') {
+ continue;
+ }
+
+ const focusableSelectors = [
+ 'a[href]',
+ 'button',
+ 'input',
+ 'textarea',
+ 'select',
+ '[tabindex]:not([tabindex="-1"])',
+ ...NON_STANDARD_INTERACTIVE_ELEMENTS,
+ ];
+
+ if (child.matches(focusableSelectors.join(','))) {
+ return child;
+ }
+
+ if (child.shadowRoot) {
+ const shadowFocusable = recursiveFindInteractiveElement(child.shadowRoot);
+ if (shadowFocusable) return shadowFocusable;
+ }
+
+ const nestedFocusable = recursiveFindInteractiveElement(child);
+ if (nestedFocusable) return nestedFocusable;
+ }
+ return null;
+ }
+
+ return recursiveFindInteractiveElement(element);
+}
+
+/**
+ * Init `cellContentTabIndex` if the plugin hook is used.
+ */
+function useInstanceBeforeDimensions(instance: TableInstance) {
+ useEffect(() => {
+ instance.dispatch({ type: 'CELL_CONTENT_TAB_INDEX', payload: -1 });
+ }, [instance.dispatch]);
+}
diff --git a/packages/main/src/components/AnalyticalTable/types/index.ts b/packages/main/src/components/AnalyticalTable/types/index.ts
index 28ba4e3b3c6..42acb6d33f7 100644
--- a/packages/main/src/components/AnalyticalTable/types/index.ts
+++ b/packages/main/src/components/AnalyticalTable/types/index.ts
@@ -91,6 +91,14 @@ export interface ColumnType extends Omit
[key: string]: any;
}
+export interface CellType {
+ column: ColumnType;
+ row: RowType;
+ value: string | undefined;
+ getCellProps: (props?: any) => any;
+ [key: string]: any;
+}
+
export interface TableInstance {
allColumns?: ColumnType[];
allColumnsHidden?: boolean;
@@ -103,7 +111,7 @@ export interface TableInstance {
disableSortBy?: boolean;
dispatch?: (action: {
type: string;
- payload?: Record | AnalyticalTableState['popInColumns'] | boolean | string;
+ payload?: Record | AnalyticalTableState['popInColumns'] | boolean | string | number;
clientX?: number;
}) => void;
expandedDepth?: number;
@@ -249,6 +257,8 @@ export interface WCRPropertiesType {
onFilter: AnalyticalTablePropTypes['onFilter'];
}
+export type CellInstance = TableInstance & { cell: CellType } & Omit;
+
export interface RowType {
allCells: Record[];
canExpand: boolean;
@@ -319,6 +329,7 @@ export interface AnalyticalTableState {
interactiveRowsHavePopIn?: boolean;
tableColResized?: true;
triggerScroll?: TriggerScrollState;
+ cellContentTabIndex?: number;
}
interface Filter {
@@ -328,7 +339,7 @@ interface Filter {
interface CellLabelParam {
instance: Record;
- cell: Record;
+ cell: CellInstance;
}
interface ScaleWidthModeOptions {
@@ -419,7 +430,7 @@ export interface AnalyticalTableColumnDefinition {
/**
* Custom cell renderer. If set, the table will call that component for every cell and pass all required information as props, e.g. the cell value as `props.cell.value`
*/
- Cell?: string | ComponentType | ((props?: any) => ReactNode);
+ Cell?: string | ComponentType | ((props?: CellInstance) => ReactNode);
/**
* Defines a function that receives an object as a parameter, including the cell and table instance, and should return the `aria-label` of the current cell.
*
@@ -605,6 +616,17 @@ export interface AnalyticalTableColumnDefinition {
*/
enableMultiSort?: boolean;
+ // useF2CellEdit
+ /**
+ * __Required when using the `useF2CellEdit` plugin hook.__ Without the hook, this property has no effect.
+ *
+ * Defines the name of interactive element(s) inside the cell.
+ * This property is used both to describe the cell's content for screen readers and to manage focus and keyboard navigation.
+ *
+ * @since 2.14.0
+ */
+ interactiveElementName?: string | ((cell: CellInstance) => string);
+
// all other custom properties of [React Table v7](https://github.com/TanStack/table/blob/v7/docs/src/pages/docs/api/overview.md) column options
[key: string]: any;
}
diff --git a/packages/main/src/components/AnalyticalTable/util/index.ts b/packages/main/src/components/AnalyticalTable/util/index.ts
index 69fb05a178c..0550cc1c64d 100644
--- a/packages/main/src/components/AnalyticalTable/util/index.ts
+++ b/packages/main/src/components/AnalyticalTable/util/index.ts
@@ -1,10 +1,61 @@
import type { CSSProperties, RefObject } from 'react';
import { TextAlign, VerticalAlign } from '../../../enums/index.js';
+// ╔════════════════════════ Constants ═══════════════════════╗
+
+export const NAVIGATION_KEYS = new Set([
+ 'End',
+ 'Home',
+ 'PageDown',
+ 'PageUp',
+ 'ArrowRight',
+ 'ArrowLeft',
+ 'ArrowDown',
+ 'ArrowUp',
+]);
+
+export const tagNamesWhichShouldNotSelectARow = new Set([
+ 'UI5-AI-BUTTON',
+ 'UI5-AI-PROMPT-INPUT',
+ 'UI5-AVATAR',
+ 'UI5-BUTTON',
+ 'UI5-CALENDAR',
+ 'UI5-CHECKBOX',
+ 'UI5-COLOR-PICKER',
+ 'UI5-COMBOBOX',
+ 'UI5-DATE-PICKER',
+ 'UI5-DATERANGE-PICKER',
+ 'UI5-DATETIME-PICKER',
+ 'UI5-DURATION-PICKER',
+ 'UI5-DYNAMIC-DATE-RANGE',
+ 'UI5-FILE-UPLOADER',
+ 'UI5-ICON',
+ 'UI5-INPUT',
+ 'UI5-LINK',
+ 'UI5-MENU-ITEM',
+ 'UI5-MENU-ITEM-GROUP',
+ 'UI5-MULTI-COMBOBOX',
+ 'UI5-MULTI-INPUT',
+ 'UI5-RADIO-BUTTON',
+ 'UI5-RANGE-SLIDER',
+ 'UI5-RATING-INDICATOR',
+ 'UI5-SEGMENTED-BUTTON',
+ 'UI5-SELECT',
+ 'UI5-SLIDER',
+ 'UI5-SPLIT-BUTTON',
+ 'UI5-STEP-INPUT',
+ 'UI5-SWITCH',
+ 'UI5-TEXT-AREA',
+ 'UI5-TIME-PICKER',
+ 'UI5-TOGGLE-BUTTON',
+ 'UI5-UPLOAD-COLLECTION',
+]);
+
+// ╔════════════════════════ Util Functions ═══════════════════════╗
+
// copied from https://github.com/tannerlinsley/react-table/blob/f97fb98509d0b27cc0bebcf3137872afe4f2809e/src/utils.js#L320-L347 (13. Jan 2021)
const reOpenBracket = /\[/g;
const reCloseBracket = /]/g;
-
function makePathArray(obj) {
return (
flattenDeep(obj)
@@ -19,7 +70,6 @@ function makePathArray(obj) {
.split('.')
);
}
-
function flattenDeep(arr, newArr = []) {
if (!Array.isArray(arr)) {
newArr.push(arr);
@@ -33,7 +83,6 @@ function flattenDeep(arr, newArr = []) {
// copied from https://github.com/tannerlinsley/react-table/blob/master/src/utils.js#L169-L191 (13.Jan 2021)
const pathObjCache = new Map();
-
export function getBy(obj, path, def) {
if (!path) {
return obj;
@@ -59,43 +108,6 @@ export function getBy(obj, path, def) {
return typeof val !== 'undefined' ? val : def;
}
-export const tagNamesWhichShouldNotSelectARow = new Set([
- 'UI5-AI-BUTTON',
- 'UI5-AI-PROMPT-INPUT',
- 'UI5-AVATAR',
- 'UI5-BUTTON',
- 'UI5-CALENDAR',
- 'UI5-CHECKBOX',
- 'UI5-COLOR-PICKER',
- 'UI5-COMBOBOX',
- 'UI5-DATE-PICKER',
- 'UI5-DATERANGE-PICKER',
- 'UI5-DATETIME-PICKER',
- 'UI5-DURATION-PICKER',
- 'UI5-DYNAMIC-DATE-RANGE',
- 'UI5-FILE-UPLOADER',
- 'UI5-ICON',
- 'UI5-INPUT',
- 'UI5-LINK',
- 'UI5-MENU-ITEM',
- 'UI5-MENU-ITEM-GROUP',
- 'UI5-MULTI-COMBOBOX',
- 'UI5-MULTI-INPUT',
- 'UI5-RADIO-BUTTON',
- 'UI5-RANGE-SLIDER',
- 'UI5-RATING-INDICATOR',
- 'UI5-SEGMENTED-BUTTON',
- 'UI5-SELECT',
- 'UI5-SLIDER',
- 'UI5-SPLIT-BUTTON',
- 'UI5-STEP-INPUT',
- 'UI5-SWITCH',
- 'UI5-TEXT-AREA',
- 'UI5-TIME-PICKER',
- 'UI5-TOGGLE-BUTTON',
- 'UI5-UPLOAD-COLLECTION',
-]);
-
export const resolveCellAlignment = (column) => {
const style: CSSProperties = {};