Skip to content
Merged
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
33 changes: 23 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -213,11 +214,23 @@ 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)
## 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:

- **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/<Component>/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

Expand Down
27 changes: 14 additions & 13 deletions skills/maintainer/analytical-table/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

---

Expand Down
Loading
Loading