Skip to content
Draft
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 @@ -35,4 +35,25 @@ const tableHooks = [useAnnounceEmptyCells]; // this array should be memoized
<AnalyticalTable {...otherProps} tableHooks={tableHooks} />
```

## Disabling a Plugin Hook

Every plugin hook accepts a `disabled` option (default `false`). When `disabled` is `true`, the hook keeps its slot in the `tableHooks` array but does nothing.

React's [rules of hooks](https://react.dev/reference/rules/rules-of-hooks) forbid adding or removing `tableHooks` entries at runtime, so use `disabled` to switch a hook off instead. To toggle it while the table is mounted, re-create the hook **at the same position** in the (memoized) `tableHooks` array with the new `disabled` value — never add, remove, or reorder entries, and keep `disabled` in the array's dependencies.

The two hooks that are usually passed by reference (`useAnnounceEmptyCells`, `useF2CellEdit`) also accept the option when called as a factory. The call form depends on the hook's other parameters:

```jsx
const tableHooks = useMemo(
() => [
useAnnounceEmptyCells({ disabled }), // factory form of a by-reference hook
useStickyColumns({ disabled }), // options object
useManualRowSelect('isSelected', { disabled }), // positional arg + options
],
[disabled],
);

<AnalyticalTable {...otherProps} tableHooks={tableHooks} />;
```

<Footer/>
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ The `useAnnounceEmptyCells` plugin hook adds screen reader announcements for emp

**Note:** Some screen readers (depending on their configuration) automatically detect empty cells, potentially resulting in duplicate announcements of empty cells.

**Disabling:** Call it as a factory with `disabled: true` (`useAnnounceEmptyCells({ disabled: true })`) to turn the hook off while keeping it in `tableHooks`. See [Disabling a Plugin Hook](?path=/docs/data-display-analyticaltable-plugin-hooks--docs#disabling-a-plugin-hook).

## Example

<Canvas sourceState="none" of={ComponentStories.PluginAnnounceEmptyCells} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ With the `useRowDisableSelection` it's possible to disable the selection of spec

**Note:** Using this hook in combination with the `useIndeterminateRowSelection` hook is not supported!

**Disabling:** Pass `disabled: true` as the second argument (`useRowDisableSelection(disableRowFunc, { disabled: true })`) to turn the hook off while keeping it in `tableHooks`. See [Disabling a Plugin Hook](?path=/docs/data-display-analyticaltable-plugin-hooks--docs#disabling-a-plugin-hook).

<Canvas sourceState="none" of={ComponentStories.PluginDisableRowSelection} />

## Example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ The hook manages focus, keyboard navigation, and `tabindex` for cells with inter
- Updates the cell's `aria-label` with the interactive element's name for accessibility.
- Prevents standard navigation keys from interfering when editing a cell.

**Disabling:** Call it as a factory with `disabled: true` (`useF2CellEdit({ disabled: true })`) to turn the hook off while keeping it in `tableHooks`. See [Disabling a Plugin Hook](?path=/docs/data-display-analyticaltable-plugin-hooks--docs#disabling-a-plugin-hook).

## Example

<Canvas of={ComponentStories.F2CellEdit} sourceState="none" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,6 @@ The plugin hook allows passing a callback as parameter with the following struct
The callback is fired, every time the internal `indeterminateRows` state is changed.
The event parameter is an object, with all indeterminate rows by id (e.g. `{"0.1":true}`) and the table instance.

**Disabling:** Pass `disabled: true` as the second argument (`useIndeterminateRowSelection(onIndeterminateChange, { disabled: true })`) to turn the hook off while keeping it in `tableHooks`. See [Disabling a Plugin Hook](?path=/docs/data-display-analyticaltable-plugin-hooks--docs#disabling-a-plugin-hook).

<Footer />
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ If this key is found on the original data row, and it is `true`, this row will b

**Note:** This hook sets `autoResetSelectedRows` to `false` by default unless explicitly defined via `reactTableOptions`. [Learn more](?path=/docs/data-display-analyticaltable-faq--docs#how-to-stop-the-table-state-from-automatically-resetting-when-the-data-changes)

**Disabling:** Pass `disabled: true` as the second argument (`useManualRowSelect('isSelected', { disabled: true })`) to turn the hook off while keeping it in `tableHooks`. See [Disabling a Plugin Hook](?path=/docs/data-display-analyticaltable-plugin-hooks--docs#disabling-a-plugin-hook).

## Example

<Canvas sourceState="none" of={ComponentStories.PluginManualRowSelect} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The first parameter is the callback itself (`e: { columnWidth: number; header: R

- `liveUpdate`: If `liveUpdate` is `true`, the resize function will fire every time the width has changed depending on the `options.wait` delay.
- `wait`: The number of milliseconds for which the calls are to be delayed. **Defaults to `100`**. (Only has an effect if `liveUpdate` is `true`)
- `disabled`: If `true`, the hook does nothing while keeping it in `tableHooks`. See [Disabling a Plugin Hook](?path=/docs/data-display-analyticaltable-plugin-hooks--docs#disabling-a-plugin-hook).

## Example

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ It receives one parameter that defines the sorting priority by the column id.

**Note:** Column IDs that are not found in the array use the default priority, so the first sorted column has a higher priority than the next sorted column.

**Disabling:** Pass `disabled: true` as the second argument (`useOrderedMultiSort(orderedIds, { disabled: true })`) to turn the hook off while keeping it in `tableHooks`. See [Disabling a Plugin Hook](?path=/docs/data-display-analyticaltable-plugin-hooks--docs#disabling-a-plugin-hook).

## Example

<Canvas sourceState="none" of={ComponentStories.PluginOrderedMultiSort} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ The hook accepts an options object with two optional callbacks:
- `enabled`: `true` if sticky rendering is now active (columns fit), `false` if auto-disabled.
- `stickyColumns`: the current user-frozen column ids (unchanged by the auto-toggle).

**Disabling:** Pass `disabled: true` (`useStickyColumns({ disabled: true })`) to turn the hook off while keeping it in `tableHooks`. See [Disabling a Plugin Hook](?path=/docs/data-display-analyticaltable-plugin-hooks--docs#disabling-a-plugin-hook).

## Example

<Canvas sourceState="none" of={ComponentStories.StickyColumns} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { useCallback } from 'react';
import type { CellType, ReactTableHooks, TableInstance } from '../types/index.js';
import type {
AnalyticalTablePluginHookOptions,
CellType,
PluginHook,
ReactTableHooks,
TableInstance,
} from '../types/index.js';

/**
* The `useAnnounceEmptyCells` plugin hook adds screen reader announcements for empty cells.
*
* **Note:** Some screen readers (depending on their configuration) automatically detect empty cells, potentially resulting in duplicate announcements of empty cells.
*/
export const useAnnounceEmptyCells = (hooks: ReactTableHooks) => {
// Distinguishes legacy direct usage (`[useAnnounceEmptyCells]`, react-table passes the hooks registry)
// from factory usage (`[useAnnounceEmptyCells({ disabled })]`). The registry always exposes array props.
const isHooksRegistry = (arg: unknown): arg is ReactTableHooks =>
!!arg && Array.isArray((arg as ReactTableHooks).getCellProps);

const useAnnounceEmptyCellsPlugin = (hooks: ReactTableHooks, disabled: boolean) => {
const setCellProps = useCallback(
(
cellProps,
Expand All @@ -27,6 +33,42 @@ export const useAnnounceEmptyCells = (hooks: ReactTableHooks) => {
[],
);

if (disabled) {
return;
}
hooks.getCellProps.push(setCellProps);
};

export interface UseAnnounceEmptyCellsHook {
/** Legacy direct usage: `tableHooks={[useAnnounceEmptyCells]}`. */
(hooks: ReactTableHooks): void;
/** Factory usage: `tableHooks={[useAnnounceEmptyCells({ disabled })]}`. */
(options?: AnalyticalTablePluginHookOptions): PluginHook;
pluginName: string;
}

/**
* The `useAnnounceEmptyCells` plugin hook adds screen reader announcements for empty cells.
*
* **Note:** Some screen readers (depending on their configuration) automatically detect empty cells, potentially resulting in duplicate announcements of empty cells.
*
* Can be used either directly (`tableHooks={[useAnnounceEmptyCells]}`) or as a factory to pass options
* (`tableHooks={[useAnnounceEmptyCells({ disabled })]}`).
*
* @param {object} [options] - Additional options.
* @param {boolean} [options.disabled=false] - If `true`, the hook does nothing. To toggle at runtime, re-create the hook at the same position in the (memoized) `tableHooks` array — never add or remove array entries.
*/
export const useAnnounceEmptyCells = ((hooksOrOptions?: ReactTableHooks | AnalyticalTablePluginHookOptions) => {
if (isHooksRegistry(hooksOrOptions)) {
useAnnounceEmptyCellsPlugin(hooksOrOptions, false);
return;
}
const { disabled = false } = hooksOrOptions ?? {};
// The returned `plugin` is pushed into `tableHooks` and invoked by react-table as a hook; threading
// `disabled` through this arrow is not a conditional hook call.
// eslint-disable-next-line react-hooks/rules-of-hooks
const plugin = (hooks: ReactTableHooks) => useAnnounceEmptyCellsPlugin(hooks, disabled);
plugin.pluginName = 'useAnnounceEmptyCells';
return plugin;
}) as UseAnnounceEmptyCellsHook;
useAnnounceEmptyCells.pluginName = 'useAnnounceEmptyCells';
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import type { Ui5DomRef } from '@ui5/webcomponents-react-base';
import type { FocusEventHandler, KeyboardEventHandler } from 'react';
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 type {
AnalyticalTablePluginHookOptions,
CellInstance,
CellType,
ColumnType,
PluginHook,
ReactTableHooks,
TableInstance,
} from '../types/index.js';
import { NAVIGATION_KEYS } from '../util/index.js';

const NON_STANDARD_INTERACTIVE_ELEMENTS = [
Expand All @@ -21,53 +29,7 @@ const NON_STANDARD_INTERACTIVE_ELEMENTS = [
// Frames to wait for a component's focus DOM ref to become resolvable (nested shadow roots may render late).
const MAX_FOCUS_REF_RETRIES = 3;

/**
* 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.
* - 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.
*
* @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<InputDomRef>(props);
* return <Input ref={callbackRef} />;
* },
* interactiveElementName: 'Input',
* },
* ];
*
* const tableHooks: AnalyticalTablePropTypes['tableHooks'] = [AnalyticalTableHooks.useF2CellEdit];
*
* function TableWithInput() {
* return <AnalyticalTable data={data} columns={columns} tableHooks={tableHooks} />;
* }
* ```
*
* @since 2.14.0
*/
export const useF2CellEdit = (hooks: ReactTableHooks) => {
const useF2CellEditPlugin = (hooks: ReactTableHooks, disabled: boolean) => {
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
const lastFocusedBodyRowRef = useRef<number | null>(null);

Expand Down Expand Up @@ -191,12 +153,97 @@ export const useF2CellEdit = (hooks: ReactTableHooks) => {
[i18nBundle],
);

// `useInstanceBeforeDimensions` is called as a React hook by react-table, so it must always be
// registered (stable hook count); it no-ops internally when disabled.
// eslint-disable-next-line react-hooks/rules-of-hooks
hooks.useInstanceBeforeDimensions.push((instance: TableInstance) => useInstanceBeforeDimensions(instance, disabled));
if (disabled) {
return;
}
hooks.getTableProps.push(setTableProps);
hooks.getCellProps.push(setCellProps);
hooks.getHeaderProps.push(setHeaderProps);
hooks.stateReducers.push(stateReducer);
hooks.useInstanceBeforeDimensions.push(useInstanceBeforeDimensions);
};

export interface UseF2CellEditHook {
/** Legacy direct usage: `tableHooks={[useF2CellEdit]}`. */
(hooks: ReactTableHooks): void;
/** Factory usage: `tableHooks={[useF2CellEdit({ disabled })]}`. */
(options?: AnalyticalTablePluginHookOptions): PluginHook;
pluginName: string;
useCallbackRef: <T extends HTMLElement = HTMLElement>(props: CellInstance) => (node: T | null) => void;
}

// Distinguishes legacy direct usage (`[useF2CellEdit]`, react-table passes the hooks registry) from
// factory usage (`[useF2CellEdit({ disabled })]`). The registry always exposes array props.
const isHooksRegistry = (arg: unknown): arg is ReactTableHooks =>
!!arg && Array.isArray((arg as ReactTableHooks).getCellProps);

/**
* 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.
* - 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.
*
* @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<InputDomRef>(props);
* return <Input ref={callbackRef} />;
* },
* interactiveElementName: 'Input',
* },
* ];
*
* const tableHooks: AnalyticalTablePropTypes['tableHooks'] = [AnalyticalTableHooks.useF2CellEdit];
*
* function TableWithInput() {
* return <AnalyticalTable data={data} columns={columns} tableHooks={tableHooks} />;
* }
* ```
*
* @param {AnalyticalTablePluginHookOptions=} [options] Optional configuration. Omit for legacy direct usage
* (`tableHooks={[useF2CellEdit]}`), or pass to use the factory form (`tableHooks={[useF2CellEdit({ disabled })]}`).
* @param {boolean=} options.disabled If `true`, the hook does nothing. To toggle at runtime, re-create the hook
* at the same position in the (memoized) `tableHooks` array — never add or remove array entries. Defaults to `false`.
*
* @since 2.14.0
*/
export const useF2CellEdit = ((hooksOrOptions?: ReactTableHooks | AnalyticalTablePluginHookOptions) => {
if (isHooksRegistry(hooksOrOptions)) {
useF2CellEditPlugin(hooksOrOptions, false);
return;
}
const { disabled = false } = hooksOrOptions ?? {};
// The returned `plugin` is pushed into `tableHooks` and invoked by react-table as a hook; threading
// `disabled` through this arrow is not a conditional hook call.
// eslint-disable-next-line react-hooks/rules-of-hooks
const plugin = (hooks: ReactTableHooks) => useF2CellEditPlugin(hooks, disabled);
plugin.pluginName = 'useF2CellEdit';
return plugin;
}) as UseF2CellEditHook;
useF2CellEdit.pluginName = 'useF2CellEdit';

/**
Expand Down Expand Up @@ -304,9 +351,12 @@ function findFirstFocusableInside(element: HTMLElement) {
/**
* Init `cellContentTabIndex` if the plugin hook is used.
*/
function useInstanceBeforeDimensions(instance: TableInstance) {
function useInstanceBeforeDimensions(instance: TableInstance, disabled: boolean) {
const { dispatch } = instance;
useEffect(() => {
if (disabled) {
return;
}
dispatch({ type: 'CELL_CONTENT_TAB_INDEX', payload: -1 });
}, [dispatch]);
}, [dispatch, disabled]);
}
Loading
Loading