From 601e5092be6134faa0eec5b47d2cfcb501c49da7 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Thu, 1 Oct 2026 09:14:54 +0200 Subject: [PATCH 1/2] docs: update AI resource files --- CLAUDE.md | 27 ++++++++++++------- skills/maintainer/analytical-table/SKILL.md | 27 ++++++++++--------- .../references/HOOK-REFERENCE.md | 20 +++++++------- 3 files changed, 42 insertions(+), 32 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a00f99264f5..d55c052cbe4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -164,15 +164,16 @@ import { Device } from '@ui5/webcomponents-react-base/Device'; ### Package Structure -| Package | npm Name | Description | -| ------------------ | ------------------------------------- | --------------------------------------------- | -| `main` | `@ui5/webcomponents-react` | React wrappers + custom components | -| `base` | `@ui5/webcomponents-react-base` | Core utilities, hooks, wrapper infrastructure | -| `charts` | `@ui5/webcomponents-react-charts` | Chart components (recharts-based) | -| `compat` | `@ui5/webcomponents-react-compat` | Legacy components | -| `cli` | `@ui5/webcomponents-react-cli` | Wrapper generation, codemods | -| `cypress-commands` | `@ui5/webcomponents-cypress-commands` | Testing utilities | -| `ai` | `@ui5/webcomponents-ai-react` | AI component wrappers | +| Package | npm Name | Description | +| ------------------ | ------------------------------------- | ---------------------------------------------------- | +| `main` | `@ui5/webcomponents-react` | React wrappers + custom components | +| `base` | `@ui5/webcomponents-react-base` | Core utilities, hooks, wrapper infrastructure | +| `charts` | `@ui5/webcomponents-react-charts` | Chart components (recharts-based) | +| `compat` | `@ui5/webcomponents-react-compat` | Legacy components | +| `cli` | `@ui5/webcomponents-react-cli` | Wrapper generation, codemods | +| `cypress-commands` | `@ui5/webcomponents-cypress-commands` | Testing utilities | +| `ai` | `@ui5/webcomponents-ai-react` | AI component wrappers | +| `mcp-server` | `@ui5/webcomponents-react-mcp` | MCP server exposing component APIs/docs to AI agents | ### Main Package Structure @@ -213,12 +214,20 @@ Use **yarn** (not pnpm). For tools, use project binaries via yarn (e.g., `yarn c ```bash yarn start # Storybook (localhost:6006) yarn test # Cypress component tests +yarn test:pw # Playwright component tests yarn lint # ESLint yarn prettier:all # Format all files ``` ## Tests (Cypress Component Tests) +The repo uses **two** component-test runners side by side: + +- **Cypress** — `ComponentName.cy.tsx` co-located next to `index.tsx`. Run with `yarn test` (or `yarn cypress`). Config: `cypress.config.ts`. +- **Playwright** — `ComponentName.spec.tsx` under a `test/` subfolder (`packages/main/src/components//test/*.spec.tsx`). Run with `yarn test:pw` (`yarn test:pw:open` for the UI). Config: `playwright.config.ts`. + +The section below documents the Cypress setup; the same web-component selector/shadow-DOM principles apply to Playwright (its locators pierce shadow roots by default). + ### File Structure Tests are co-located with components: `ComponentName.cy.tsx` next to `index.tsx`. diff --git a/skills/maintainer/analytical-table/SKILL.md b/skills/maintainer/analytical-table/SKILL.md index 2aaf94bb90f..86e7aaf8f8e 100644 --- a/skills/maintainer/analytical-table/SKILL.md +++ b/skills/maintainer/analytical-table/SKILL.md @@ -61,19 +61,20 @@ Sibling row attrs: `data-row-index` (header = 0, body = `virtualRow.index + 1`), Opt-in via `AnalyticalTableHooks.useStickyColumns(options?)` in `tableHooks`, where `options` is `{ onStickyColumnsChange?, onAutoToggleSticky? }`. `@experimental`. Hook file: `pluginHooks/useStickyColumns.ts` (re-exported through the namespace). -- **State is the source of truth.** `state.stickyColumns: string[]` is authoritative — mirrors react-table's `useColumnOrder`. The `sticky: 'start'` column option is **only a seed**: the reducer's `init` case derives it from `instance.columns` (raw defs filtered by `sticky === 'start'`); a consumer-supplied `initialState.stickyColumns` wins (`init` returns early if the slice is already set). There is no wrapper-built `initialState.stickyColumns` in `index.tsx` (`pluginHooks/useStickyColumns.ts:37-47`). -- **Effective sticky set = `state.stickyColumns ∪ state.groupBy`.** Grouped columns auto-pin (react-table already moves grouped cols to front); ungrouping un-pins (`getStickySet`, `useStickyColumns.ts:71-72`). -- **Internal start columns** (`__ui5wcr__internal_selection_column`, `__ui5wcr__internal_highlight_column`) auto-pin **only when at least one user/grouped column is sticky** (`hasUserStickyColumn` helper, `useStickyColumns.ts:78-79`, applied in `visibleColumns` and `useStickyMetadata` at `:83,128`). The navigation column (appended) never pins. -- **`instance.stickyStartIndices` / `instance.totalStickyStartWidth`** are computed in the `useInstance` hook (`useStickyMetadata`). `hasStickyColumns = stickyStartIndices.length > 0` (`index.tsx:440`) flips the scroll model: the `.table` scroll container (`tableRef`) becomes the single scroll container instead of `.tbody` (`parentRef`), the `.stickyColumnsMode` class is applied, the **native** scrollbar is used, and `scrollPaddingInlineStart = totalStickyStartWidth` is set (`index.tsx:719,747,774,878-883`). -- **Horizontal virtualization is bypassed for sticky columns.** A `stickyRangeExtractor` force-includes the sticky indices in the virtual range so pinned columns are always rendered (`index.tsx:388-409`). -- **Auto-disable when too narrow.** In `useStickyMetadata`, sticky rendering is dropped (`stickyStartIndices = []`, `autoDisabled = true`) when `tableClientWidth - reservedScrollable <= totalStickyStartWidth`, where `reservedScrollable = minNonStickyColWidth + (wasActive ? 0 : scrollbarWidth)` and `minNonStickyColWidth` is `DEFAULT_COLUMN_WIDTH` (60px) desktop / 88px mobile. The scrollbar is reserved **only when sticky is currently inactive** (`wasActive` = prior `instance.stickyStartIndices.length > 0`): `tableClientWidth` already excludes the vertical scrollbar while sticky is active (the outer table is the scroll container then), so reserving it again would double-count — and keying the reservation on the prior state makes the enable/disable thresholds coincide, preventing width oscillation at the boundary on classic-scrollbar platforms (no effect on overlay scrollbars, width 0). Skipped on first render (`tableClientWidth === 0`); re-enables when the container grows (`useStickyColumns.ts:135-160`). **The frozen-set config (`state.stickyColumns`) is NOT cleared on auto-disable** — pins persist across resize; only explicit unfreeze clears them. Consequence: while auto-disabled, a non-first frozen column stays hoisted to the start as an ordinary (unfrozen) column (revert its order by toggling it off). `useStickyMetadata` has **no early returns** (computes into locals, single `Object.assign`) so its `useRef`/`useEffect` stay unconditional. -- **`onAutoToggleSticky(detail)` fires on width-driven enable/disable transitions** (not on explicit freeze/unfreeze, and not when no user column is sticky). `detail = { enabled, stickyColumns }` (`AnalyticalTableStickyAutoToggleDetail`). Fired from a transition-detecting `useEffect` in `useStickyMetadata` (`useStickyColumns.ts:165-172`); a mount into an already-too-narrow container fires `{ enabled: false }` once on first measure. -- **`onStickyColumnsChange(detail)` fires ONLY on the popover freeze/unfreeze**, never on programmatic toggles — the app already controls those calls. `detail = { column, sticky, stickyColumns }` (`AnalyticalTableStickyColumnsChangeDetail`, `useStickyColumns.ts:225-237`). -- **`disableSticky` column option is a UI-affordance gate ONLY.** It skips assigning `column.toggleSticky`, which removes the popover freeze entry — but does NOT gate the state APIs: `sticky: 'start'` seed and programmatic toggles still pin it (`useStickyColumns.ts:117-123`). +- **State is the source of truth.** `state.stickyColumns: string[]` is authoritative — mirrors react-table's `useColumnOrder`. The `sticky: 'start'` column option is **only a seed**: the reducer's `init` case derives it from `instance.columns` (raw defs filtered by `sticky === 'start'`); a consumer-supplied `initialState.stickyColumns` wins (`init` returns early if the slice is already set). There is no wrapper-built `initialState.stickyColumns` in `index.tsx` (`pluginHooks/useStickyColumns.ts:39-49`). +- **Effective sticky set = `state.stickyColumns ∪ state.groupBy`.** Grouped columns auto-pin (react-table already moves grouped cols to front); ungrouping un-pins (`getStickySet`, `useStickyColumns.ts:69-70`). +- **Internal start columns** (`__ui5wcr__internal_selection_column`, `__ui5wcr__internal_highlight_column`) auto-pin **only when at least one user/grouped column is sticky** (`hasUserStickyColumn` helper, `useStickyColumns.ts:76-77`, applied in `visibleColumns` and `useStickyMetadata` at `:81,126`). The navigation column (appended) never pins. +- **`instance.stickyStartIndices` / `instance.totalStickyStartWidth`** are computed in the `useInstance` hook (`useStickyMetadata`). `stickyStartIndices` is returned as a **content-keyed stable reference** (`useMemo` on the `join(',')` of the indices, `useStickyColumns.ts:161-166`) so downstream `useMemo`/`useCallback` deps don't rebuild every render. `hasStickyColumns = stickyStartIndices.length > 0` (`index.tsx:457`) flips the scroll model: the `.table` scroll container (`tableRef`) becomes the single scroll container instead of `.tbody` (`parentRef`), the `.stickyColumnsMode` class is applied, the **native** scrollbar is used, and `scrollPaddingInlineStart = totalStickyStartWidth` is set (`index.tsx:737,756-759,767,904`). +- **Horizontal virtualization is bypassed for sticky columns.** A `stickyRangeExtractor` force-includes the sticky indices in the virtual range so pinned columns are always rendered (`index.tsx:403-412`). +- **Auto-disable when too narrow.** In `useStickyMetadata`, sticky rendering is dropped (`stickyStartIndices = []`, `autoDisabled = true`) when the columns no longer fit: `fits = tableClientWidth - reservedScrollable > totalStickyStartWidth`, where `reservedScrollable = minNonStickyColWidth + (wasActive ? 0 : scrollbarWidth + STICKY_FIT_HYSTERESIS_PX)`, `minNonStickyColWidth` is `DEFAULT_COLUMN_WIDTH` (60px) desktop / 88px mobile, and `STICKY_FIT_HYSTERESIS_PX = 4`. The scrollbar + hysteresis is reserved **only when sticky is currently inactive** (`wasActive` = prior `instance.stickyStartIndices.length > 0`): `tableClientWidth` already excludes the vertical scrollbar while sticky is active (the outer table is the scroll container then), so reserving it again would double-count. Keying the reservation on the prior state, plus the explicit 4px hysteresis margin, keeps the enable and disable thresholds from coinciding and prevents width oscillation at the boundary on classic-scrollbar platforms (no effect on overlay scrollbars, width 0). The fit check is gated on `measured` = `tableClientWidth > 0 && fontsReady` (skipped on first render / before fonts load, since unmeasured widths sit at the 150px default and would skew the check); re-enables when the container grows (`useStickyColumns.ts:145-157`). **The frozen-set config (`state.stickyColumns`) is NOT cleared on auto-disable** — pins persist across resize; only explicit unfreeze clears them. Consequence: while auto-disabled, a non-first frozen column stays hoisted to the start as an ordinary (unfrozen) column (revert its order by toggling it off). `useStickyMetadata` has **no early returns** (computes into locals, single `Object.assign`) so its `useRef`/`useEffect`/`useMemo` stay unconditional. +- **`onAutoToggleSticky(detail)` fires on width-driven enable/disable transitions** (not on explicit freeze/unfreeze, and not when no user column is sticky). `detail = { enabled, stickyColumns }` (`AnalyticalTableStickyAutoToggleDetail`). Fired from a transition-detecting `useEffect` in `useStickyMetadata` (`useStickyColumns.ts:172-182`); a mount into an already-too-narrow container fires `{ enabled: false }` once on first measure. +- **`onStickyColumnsChange(detail)` fires ONLY on the popover freeze/unfreeze**, never on programmatic toggles — the app already controls those calls. `detail = { column, sticky, stickyColumns }` (`AnalyticalTableStickyColumnsChangeDetail`; interface `useStickyColumns.ts:187-194`, fired from the modal item's `run` at `:246`). +- **`disableSticky` column option is a UI-affordance gate ONLY.** It skips assigning `column.toggleSticky`, which removes the popover freeze entry — but does NOT gate the state APIs: `sticky: 'start'` seed and programmatic toggles still pin it (`useStickyColumns.ts:116-122`). - **Consumer API** is on the `tableInstance` prop-ref: `tableInstance.current.setStickyColumns(ids)` / `toggleStickyColumn(id, value?)` (`types/index.ts:225,233`, `@experimental`). These are **not** on the DOM ref. The popover freeze item calls `column.toggleSticky` internally. -- **The freeze/unfreeze popover item** is contributed via the generic `columnHeaderModalItems` hook (see [REACT-TABLE-PIPELINE.md](references/REACT-TABLE-PIPELINE.md)) — a non-empty result opens the popover even for a column with no sort/filter/group. Skipped for internal, grouped, and `disableSticky` columns (`useStickyColumns.ts:173-207`). -- **DOM attributes:** `data-sticky-start` on sticky header `_thContainer`s and body cells; `data-sticky-start-last` on the **last sticky header only** (draws the freeze line via `::after`). `.stickyColumnsNoData` (container class when `rows.length === 0`) clamps the freeze line to the header so it doesn't dangle over an empty body (`ColumnHeader/index.tsx:210-211`, `TableBody/VirtualTableBody.tsx:154,237`, `index.tsx:749`). -- **Drag subtlety:** both the drag **source** (`isDraggable={... && !isStickyStart}`, `ColumnHeaderContainer.tsx:62`) and the drop **target** (`isStickyTarget`, `useDragAndDrop.ts`) read the live, state-derived `stickyStartIndices` (the target resolves the column's index in `visibleColumns` and checks membership). They stay in lockstep across runtime `toggleStickyColumn` and width auto-disable — a column frozen only at runtime is both non-draggable and a non-drop-target, and when sticky auto-disables everything becomes draggable/droppable again. +- **The freeze/unfreeze popover item** is contributed via the generic `columnHeaderModalItems` hook (see [REACT-TABLE-PIPELINE.md](references/REACT-TABLE-PIPELINE.md)) — a non-empty result opens the popover even for a column with no sort/filter/group. Skipped for internal, navigation, grouped, and `disableSticky` columns (`getColumnHeaderModalItems`, `useStickyColumns.ts:217-250`). Item label/icon toggle on `isFrozen` via translatable `freezeColumnText`/`unfreezeColumnText`. +- **Active sticky headers get a `fixedColumnText` aria-label.** `setHeaderProps` (registered on `getHeaderProps`, `useStickyColumns.ts:252-267`) appends the translatable `fixedColumnText` ("fixed column") to the `aria-label` of every header in the effective sticky set — but only while sticky is actually active (`stickyStartIndices.length > 0`) and only if `fixedColumnText` is set. No-op while auto-disabled. +- **DOM attributes:** `data-sticky-start` on sticky header `_thContainer`s and body cells; `data-sticky-start-last` on the **last sticky header only** (`isLastStickyStart = isStickyStart && !stickyStartSet.has(index + 1)`, `ColumnHeaderContainer.tsx:93`; draws the freeze line via `::after`). `.stickyColumnsNoData` (container class when `rows.length === 0`) clamps the freeze line to the header so it doesn't dangle over an empty body (`ColumnHeader/index.tsx:208-209`, `TableBody/VirtualTableBody.tsx:152,235`, `index.tsx:769`). +- **Drag subtlety:** both the drag **source** (`isDraggable={... && !isStickyStart}`, `ColumnHeaderContainer.tsx:87`) and the drop **target** (`isStickyTarget`, `useDragAndDrop.ts:27`) read the live, state-derived `stickyStartIndices` (the target resolves the column's index in `visibleColumns` and checks membership). They stay in lockstep across runtime `toggleStickyColumn` and width auto-disable — a column frozen only at runtime is both non-draggable and a non-drop-target, and when sticky auto-disables everything becomes draggable/droppable again. - **Limitations:** not combinable with `renderRowSubComponent` or `responsivePopIn`; only `sticky: 'start'` (no `'end'`); sticky mode always shows the native scrollbar, not the custom styled one. ### `AnalyticalTableHooks` namespace @@ -100,7 +101,7 @@ Available members (all set `pluginName`): - **`useRowDisableSelection(accessor: string | (row) => boolean)`** — Disables selection on specific rows. **Deprecated; no replacement.** Don't suggest a swap-in for new code; either keep using it as-is or implement disable logic in `useManualRowSelect` callbacks. - **`useOnColumnResize(callback, { liveUpdate?: boolean; wait?: number = 100 })`** — Fires callback on column resize. Registers on `hooks.useFinalInstance`. With `liveUpdate: true`, also registers on `hooks.getResizerProps` to fire continuously during drag (debounced by `wait`). - **`useAnnounceEmptyCells`** — Appends `cellEmptyDescId` to **`aria-labelledby`** (not `aria-label`) on cells whose value is `''`/`null`/`undefined`/`false`. `0` is **not** treated as empty. Falsy JSX returned from a custom `Cell` is announced as empty (`pluginHooks/useAnnounceEmptyCells.ts:22-23`). -- **`useStickyColumns(options?: { onStickyColumnsChange?, onAutoToggleSticky? })`** — `@experimental`. Pins columns to the inline-start (frozen columns). Registers on `stateReducers`, `visibleColumnsDeps`, `visibleColumns`, `useInstance`, `getHeaderProps`, `columnHeaderModalItems`. Hook file: `pluginHooks/useStickyColumns.ts`. See **Sticky (frozen-start) columns** under Key Behaviors for the full model. +- **`useStickyColumns(options?: { onStickyColumnsChange?, onAutoToggleSticky? })`** — `@experimental`. Pins columns to the inline-start (frozen columns). Registers on `stateReducers`, `visibleColumnsDeps`, `visibleColumns`, `useInstance` (`useStickyMetadata`), `getHeaderProps` (adds the `fixedColumnText` aria-label to active sticky headers), `columnHeaderModalItems` (the freeze/unfreeze item). Hook file: `pluginHooks/useStickyColumns.ts`. See **Sticky (frozen-start) columns** under Key Behaviors for the full model. --- diff --git a/skills/maintainer/analytical-table/references/HOOK-REFERENCE.md b/skills/maintainer/analytical-table/references/HOOK-REFERENCE.md index 1d24852ad16..591195868b2 100644 --- a/skills/maintainer/analytical-table/references/HOOK-REFERENCE.md +++ b/skills/maintainer/analytical-table/references/HOOK-REFERENCE.md @@ -99,16 +99,16 @@ useTable( User-facing plugins passed via `tableHooks`. Imported as `AnalyticalTableHooks.` from `@ui5/webcomponents-react`. All eight live in `pluginHooks/` and are re-exported through the namespace. -| Plugin | Factory | Registration | Notes (beyond MCP) | -| -------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **useManualRowSelect** | `useManualRowSelect(key = 'isSelected')` | `hooks.useInstanceAfterData` | Sets `autoResetSelectedRows = false` **only if not already defined** on instance. Skips `toggleRowSelected` when state would not change. | -| **useOrderedMultiSort** | `useOrderedMultiSort(orderedIds)` | `hooks.stateReducers` | Reorders `sortBy` array by priority on `toggleSortBy`. | -| **useIndeterminateRowSelection** | `useIndeterminateRowSelection(callback)` | `hooks.getToggleRowSelectedProps`, `hooks.stateReducers`, `hooks.useInstanceAfterData` | No-op unless `isTreeTable && Multiple && selectionBehavior !== RowOnly`. Auto-selects parents when all siblings selected. Callback signature: `(rowsById, instance) => void`. | -| **useF2CellEdit** | Direct plugin (not a factory) | `hooks.getTableProps`, `hooks.getCellProps`, `hooks.getHeaderProps`, `hooks.stateReducers`, `hooks.useInstanceBeforeDimensions` | Adds `aria-description` "Press F2 to move to content" on the table. Tab from header → body cell same column at `lastFocusedBodyRowRef`/row 1; Shift+Tab body → header. Static method `useF2CellEdit.useCallbackRef(props)` is mandatory for interactive cell content. | -| **useRowDisableSelection** | `useRowDisableSelection(accessor)` | `hooks.getHeaderProps`, `hooks.getRowProps`, `hooks.columns`, `hooks.columnsDeps`, `hooks.getCellProps`, `hooks.getToggleRowSelectedProps` | `accessor: string \| (row) => boolean`. **Deprecated.** | -| **useOnColumnResize** | `useOnColumnResize(callback, opts?)` | `hooks.useFinalInstance`, conditionally `hooks.getResizerProps` | Options: `{ liveUpdate?: boolean; wait?: number = 100 }`. `getResizerProps` registration is gated on `liveUpdate: true`. | -| **useAnnounceEmptyCells** | Direct plugin | `hooks.getCellProps` | Appends `cellEmptyDescId` to **`aria-labelledby`** (not `aria-label`). Treats `0` as non-empty; `''`/`null`/`undefined`/`false` as empty. | -| **useStickyColumns** | `useStickyColumns(options?: { onStickyColumnsChange?, onAutoToggleSticky? })` | `hooks.stateReducers`, `hooks.visibleColumnsDeps`, `hooks.visibleColumns`, `hooks.useInstance`, `hooks.getHeaderProps`, `hooks.columnHeaderModalItems` | **Experimental** frozen-start columns (`pluginHooks/useStickyColumns.ts`). State-driven (`state.stickyColumns`); reorders sticky columns to front, sets `instance.stickyStartIndices`/`totalStickyStartWidth`, contributes the popover Freeze/Unfreeze item. `onStickyColumnsChange` fires on popover toggles only; `onAutoToggleSticky` fires on width-driven auto-disable/re-enable (`{ enabled, stickyColumns }`; config persists across auto-disable). See the Sticky-columns section in SKILL.md. | +| Plugin | Factory | Registration | Notes (beyond MCP) | +| -------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **useManualRowSelect** | `useManualRowSelect(key = 'isSelected')` | `hooks.useInstanceAfterData` | Sets `autoResetSelectedRows = false` **only if not already defined** on instance. Skips `toggleRowSelected` when state would not change. | +| **useOrderedMultiSort** | `useOrderedMultiSort(orderedIds)` | `hooks.stateReducers` | Reorders `sortBy` array by priority on `toggleSortBy`. | +| **useIndeterminateRowSelection** | `useIndeterminateRowSelection(callback)` | `hooks.getToggleRowSelectedProps`, `hooks.stateReducers`, `hooks.useInstanceAfterData` | No-op unless `isTreeTable && Multiple && selectionBehavior !== RowOnly`. Auto-selects parents when all siblings selected. Callback signature: `(rowsById, instance) => void`. | +| **useF2CellEdit** | Direct plugin (not a factory) | `hooks.getTableProps`, `hooks.getCellProps`, `hooks.getHeaderProps`, `hooks.stateReducers`, `hooks.useInstanceBeforeDimensions` | Adds `aria-description` "Press F2 to move to content" on the table. Tab from header → body cell same column at `lastFocusedBodyRowRef`/row 1; Shift+Tab body → header. Static method `useF2CellEdit.useCallbackRef(props)` is mandatory for interactive cell content. | +| **useRowDisableSelection** | `useRowDisableSelection(accessor)` | `hooks.getHeaderProps`, `hooks.getRowProps`, `hooks.columns`, `hooks.columnsDeps`, `hooks.getCellProps`, `hooks.getToggleRowSelectedProps` | `accessor: string \| (row) => boolean`. **Deprecated.** | +| **useOnColumnResize** | `useOnColumnResize(callback, opts?)` | `hooks.useFinalInstance`, conditionally `hooks.getResizerProps` | Options: `{ liveUpdate?: boolean; wait?: number = 100 }`. `getResizerProps` registration is gated on `liveUpdate: true`. | +| **useAnnounceEmptyCells** | Direct plugin | `hooks.getCellProps` | Appends `cellEmptyDescId` to **`aria-labelledby`** (not `aria-label`). Treats `0` as non-empty; `''`/`null`/`undefined`/`false` as empty. | +| **useStickyColumns** | `useStickyColumns(options?: { onStickyColumnsChange?, onAutoToggleSticky? })` | `hooks.stateReducers`, `hooks.visibleColumnsDeps`, `hooks.visibleColumns`, `hooks.useInstance`, `hooks.getHeaderProps`, `hooks.columnHeaderModalItems` | **Experimental** frozen-start columns (`pluginHooks/useStickyColumns.ts`). State-driven (`state.stickyColumns`); reorders sticky columns to front, sets `instance.stickyStartIndices` (content-keyed stable ref) / `totalStickyStartWidth` in `useInstance`, contributes the popover Freeze/Unfreeze item, and via `getHeaderProps` appends the translatable `fixedColumnText` aria-label to active sticky headers. Auto-disable fit check reserves `minNonStickyColWidth + (wasActive ? 0 : scrollbarWidth + 4px hysteresis)`. `onStickyColumnsChange` fires on popover toggles only; `onAutoToggleSticky` fires on width-driven auto-disable/re-enable (`{ enabled, stickyColumns }`; config persists across auto-disable). See the Sticky-columns section in SKILL.md. | ## Factory Pattern for Plugin Hooks From ca8bc5b2c08c49dbccfba89587ee3961d8218cdd Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Thu, 1 Oct 2026 09:28:18 +0200 Subject: [PATCH 2/2] Update CLAUDE.md --- CLAUDE.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index d55c052cbe4..01eef6cec9e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -219,7 +219,11 @@ yarn lint # ESLint yarn prettier:all # Format all files ``` -## Tests (Cypress Component Tests) +## Tests + +See also [Testing guide](docs/knowledge-base/Testing.mdx) for more details. + +**Always write new tests in Playwright.** For large changes to existing Cypress tests, migrate the test to Playwright instead of updating the Cypress version. The repo uses **two** component-test runners side by side: