diff --git a/.changeset/components-sp2-select.md b/.changeset/components-sp2-select.md new file mode 100644 index 000000000..4763362ab --- /dev/null +++ b/.changeset/components-sp2-select.md @@ -0,0 +1,44 @@ +--- +"@pretable/react": minor +"@pretable/ui": patch +--- + +The kit's picker, and the four `` elements.** The filter dialog's +operator, the tool panel's filter column and filter operator, and the grouping +aggregate picker are now `button[role="combobox"]` triggers carrying +`data-pretable-value`, which open a `[data-pretable-listbox]` of +`[data-pretable-option][data-value]` in `document.body`. A test keyed on +`select`, on `.value`, on Playwright's `selectOption()` or on `toHaveValue()` +must read the attribute and click the option instead. Every site keeps the +`data-pretable-*` attribute it already had, and the aggregate picker gains +`data-pretable-aggregate`. + +**Keyboard.** Arrows, Enter and Space open a closed picker; arrows wrap and skip +disabled options; Home and End jump to the ends; typing a prefix jumps to the +first match (the buffer clears after 500ms); Enter or Space commits; Escape +closes back to the trigger; Tab closes and moves on. `onChange` fires only when +the value actually changes. The trigger is an explicit tab stop in every +browser, which the native control was not in WebKit. + +A disabled option is shown, skipped and inert rather than hidden — that is the +aggregate picker's `Custom` entry, a consumer-written aggregator the grid +displays but never writes back. + +**Styling.** The enum cell editor's list is now the kit list, so +`data-pretable-enum-listbox` and `data-pretable-enum-option` are gone, replaced +by `data-pretable-listbox` and `data-pretable-option` — plus `[data-active]` for +the roving highlight and `[aria-selected="true"]` for the current value. Tokens +are unchanged. + +**Layout.** The tool panel's filter rows wrap less: a picker now sizes to its +own label, with an ellipsis, instead of to its longest option. diff --git a/apps/website/app/fixtures/components/page.tsx b/apps/website/app/fixtures/components/page.tsx index a6bf4729f..370107882 100644 --- a/apps/website/app/fixtures/components/page.tsx +++ b/apps/website/app/fixtures/components/page.tsx @@ -5,6 +5,7 @@ import { type PretableButtonComponent, type PretableColumn, type PretableIconButtonComponent, + type PretableSelectComponent, } from "@pretable/react"; import { forwardRef } from "react"; @@ -68,13 +69,51 @@ const FixtureIconButton: PretableIconButtonComponent = forwardRef( }, ); +/** + * A picker replacement that is a plain button, not a combobox — deliberately + * nothing like the kit's own. It records what the grid handed it (`site`, the + * committed value, how many options) and, on click, commits the first other + * enabled option, so a test can prove the grid's `onChange` reaches the model + * through a replacement that shares none of the kit's internals. + */ +const FixtureSelect: PretableSelectComponent = forwardRef( + function FixtureSelect( + { site, options, value, onChange, "aria-label": label, ...props }, + ref, + ) { + return ( + + ); + }, +); + export default function ComponentsFixturePage() { return (
row.id} rows={ROWS} toolPanel={{ defaultActiveSection: "columns" }} diff --git a/apps/website/content/docs/grid/components.mdx b/apps/website/content/docs/grid/components.mdx index 82b3ada01..13d5b1e68 100644 --- a/apps/website/content/docs/grid/components.mdx +++ b/apps/website/content/docs/grid/components.mdx @@ -1,22 +1,24 @@ --- title: Components -description: "The kit components the grid renders its own chrome from — Button and IconButton — how to style them, and how to replace either with your own." +description: "The kit components the grid renders its own chrome from — Button, IconButton and Select — how to style them, and how to replace any of them with your own." nav: Grid --- `@pretable/react` renders its own controls from a small component kit, and ships those components for you to use and to replace. This page covers the -first two: `PretableButton` and `PretableIconButton`. Here is a grid whose -every labelled button is the app's own: +three that ship today: `PretableButton`, `PretableIconButton` and +`PretableSelect`. Here is a grid whose every labelled button is the app's +own: ## The components -Both render ` + {open ? ( + close({ restoreFocus: false })} + /> + ) : null} + + ); + }, +); +``` + +Note: `onKeyDown` is in `buttonProps` (not destructured) — the component reads `buttonProps.onKeyDown` then spreads it too; to avoid attaching it twice, destructure `onKeyDown` alongside `onClick`. Do that. + +- [ ] **Step 5: Run the tests to see them pass** + +```bash +cd packages/react && pnpm exec vitest run --environment jsdom src/__tests__/components-select.test.tsx; cd ../.. +pnpm typecheck; echo exit:$? +``` +Expected: `Tests 9 passed (9)`, typecheck 0. If `toHaveFocus()` fails after a click-commit in jsdom, `focus({ preventScroll })` is fine in jsdom; check that `close` runs after `onChange` (it does) and that the trigger is still mounted. + +- [ ] **Step 6: Commit** + +```bash +npx prettier --write packages/react/src/components/select.tsx packages/react/src/components/button.tsx packages/react/src/__tests__/components-select.test.tsx packages/react/src/__tests__/select-helpers.ts +git add packages/react/src/components/select.tsx packages/react/src/components/button.tsx packages/react/src/__tests__/components-select.test.tsx packages/react/src/__tests__/select-helpers.ts +git commit -m "feat(react): PretableSelect, a select-only combobox on the kit Listbox + +A button[role=combobox] trigger carrying data-pretable-select / -site / +-value and the kit list beneath it; opens on click and on the navigation +keys, commits on option click or Enter/Space, closes on Escape (focus +returned), Tab and outside press. A value absent from the options renders +as its own label — the pruned-operator case. jsdom helpers replace the +native-select drivers every later test used. + +Co-Authored-By: Claude Fable 5.1 " +``` + +--- + +### Task 5: The type contract + +**Files:** +- Create: `type-tests/react/components-select.types.tsx` (imports from the SOURCE path until Task 9 switches it, with the same `TEMPORARY` marker SP1 used) + +- [ ] **Step 1: Write it** + +```tsx +// type-tests/react/components-select.types.tsx +// TEMPORARY: imported from source because `@pretable/react` does not export +// these yet. Task 9 of the SP2 plan switches this to the package. +import { + PretableSelect, + type PretableSelectOption, + type PretableSelectProps, +} from "../../packages/react/src/components/select"; +import type { PretableButtonSite } from "../../packages/react/src/components/button"; +import type { Equal, Expect } from "../shared/assert"; + +const options: readonly PretableSelectOption[] = [{ value: "a", label: "A" }, { value: "b", label: "B", disabled: true }]; + +// The name is required. +// @ts-expect-error — aria-label is required + {}} />; + {}} />; + +// The value is a string, and onChange hands one back. +// @ts-expect-error — value is a string + {}} />; + { const s: string = v; void s; }} />; + +// No `type`, no native `onChange`/`value` shapes leak through. +// @ts-expect-error — the element is always type="button" + {}} type="submit" />; + +// The four select sites are in the kit's vocabulary, and it stays open. + {}} site="filter-operator" />; + {}} site="my-app-picker" />; +export type SelectSitesAreBuiltIn = Expect< + Equal, "aggregate" | "filter-row-column"> +>; + +// Refs and native attributes flow through. +const ref = { current: null as HTMLButtonElement | null }; + {}} disabled className="x" />; + +export type NameIsRequired = Expect>; +``` + +- [ ] **Step 2: Run, then mutation-check one directive** + +```bash +pnpm exec tsc -p type-tests/tsconfig.react.json --noEmit; echo exit:$? +``` +Expected exit 0. Comment out the `value={1}` directive → tsc must fail (`TS2322`); restore. + +- [ ] **Step 3: Commit** + +```bash +npx prettier --write type-tests/react/components-select.types.tsx +git add type-tests/react/components-select.types.tsx +git commit -m "test(react): pin the PretableSelect type contract + +Co-Authored-By: Claude Fable 5.1 " +``` + +--- + +### Task 6: The `Select` slot + +**Files:** +- Modify: `packages/react/src/components/context.ts` +- Test: append to `packages/react/src/__tests__/components-button.test.tsx`'s `components context` block + +- [ ] **Step 1: Write the failing tests** (append inside `describe("components context")`) + +```tsx + test("the Select slot resolves like the other two, and its own change is its own", () => { + const MySelect = forwardRef>( + (props, ref) =>