diff --git a/.changeset/components-sp1-button.md b/.changeset/components-sp1-button.md new file mode 100644 index 000000000..efca4ed92 --- /dev/null +++ b/.changeset/components-sp1-button.md @@ -0,0 +1,46 @@ +--- +"@pretable/react": minor +"@pretable/ui": patch +--- + +The first two components of a kit the grid renders its own chrome from, and a +`components` prop to replace either with your own. + +`PretableButton` (two looks: `ghost`, the 24px action with a hover tint, and +`link`, plain accent text) and `PretableIconButton` (an icon-only button whose +`aria-label` is a required prop — omitting the accessible name is now a +compile error, and an empty one warns in development). Both are always +`type="button"`, pass `className` and `style` straight through, forward their +`ref`, and carry `data-pretable-button` / `data-pretable-icon-button`, +`data-pretable-variant` (labelled buttons) and `data-pretable-site` for styling +through the grid's usual attributes-and-tokens channel. + +`components={{ Button, IconButton }}` on `` and `` +replaces a component everywhere it appears — the tool panel, the header, the +group panel and the portalled filter dialog — and the replacement receives +exactly the props the built-in does, `site` included, so it can branch on where +in the grid it is. It must forward its `ref` (under React 18, `forwardRef`; under +React 19 a plain `ref` prop): the grid anchors menus on and returns focus to +that node. + +The grid's twelve plain push-buttons now render from these. Each keeps its +original attribute (`data-pretable-filter-clear`, `data-pretable-tool-reset`, +…), so selectors and stylesheets keyed on them keep working; the shared look +moved from twelve site rules onto the component rules, and every site's +computed box, colours and focus ring were measured before and after — no pixel +moved. Three things did change on purpose: `Clear` and `Reset columns`, and the +funnel, column-menu and chip-remove icon buttons, now wear the product's focus +ring rather than the browser's; `Clear` and `Reset columns` carry the control +radius (visible only on that ring's corners); and the kit icon-button rule does +not set `position: relative` — the three grid buttons that enlarge their hit +area with a `::after` declare it themselves, so a consumer rendering an +absolutely-positioned child inside `PretableIconButton` should position the +button explicitly. + +Two smaller notes. The set of built-in `site` names (`PretableBuiltInButtonSite`) +grows additively — a new grid button may add one without a major bump. And the +two portalled surfaces that host these controls, the filter dialog and the +column menu, now declare `line-height` alongside the family, size and colour +they already declared: `font: inherit` on a kit button had been pulling the +host page's line-height into `Clear` and every menu item for a consumer with no +CSS reset. diff --git a/apps/website/app/docs/_nav.ts b/apps/website/app/docs/_nav.ts index 0c691f067..305674a38 100644 --- a/apps/website/app/docs/_nav.ts +++ b/apps/website/app/docs/_nav.ts @@ -51,6 +51,7 @@ export const docsNav: DocsNavSection[] = [ title: "Cell presentations", href: "/docs/grid/cell-presentations", }, + { title: "Components", href: "/docs/grid/components" }, { title: "Custom rendering", href: "/docs/grid/custom-rendering", diff --git a/apps/website/app/fixtures/components/page.tsx b/apps/website/app/fixtures/components/page.tsx new file mode 100644 index 000000000..a6bf4729f --- /dev/null +++ b/apps/website/app/fixtures/components/page.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { + PretableSurface, + type PretableButtonComponent, + type PretableColumn, + type PretableIconButtonComponent, +} from "@pretable/react"; +import { forwardRef } from "react"; + +/** + * Test fixture for `apps/website/e2e/components.spec.ts`. + * + * The unit suite (`components-override.test.tsx`) proves the components + * context resolves and reaches every site in jsdom. What only a real + * browser can prove is that a replacement lands inside a popover the grid + * PORTALS into `document.body` — the case the context exists for — and + * that the grid's own behaviour on a replaced icon button (the menu it + * anchors on the node, the focus it returns there) survives through the + * forwarded ref. Both slots below are replaced with components that mark + * themselves and record their `site`. + * + * Deliberately not part of the product surface; `fixtures/layout.tsx` keeps + * the route out of search engines. + */ + +interface Row { + id: string; + name: string; + qty: number; +} + +const ROWS: Row[] = [ + { id: "a", name: "Alpha", qty: 1 }, + { id: "b", name: "Bravo", qty: 2 }, + { id: "c", name: "Charlie", qty: 3 }, +]; + +const COLUMNS: PretableColumn[] = [ + { id: "name", header: "Name", widthPx: 160, type: "text" }, + { id: "qty", header: "Qty", widthPx: 100, type: "number" }, +]; + +const FixtureButton: PretableButtonComponent = forwardRef( + function FixtureButton({ site, variant, ...props }, ref) { + return ( + ` (the one right after `{messages.toolPanelAddFilterLabel()}`) with ``. Do the same for the second: ``. + +Replace the two expansion buttons: + +```tsx + + +``` + +with + +```tsx + + +``` + +- [ ] **Step 3: Run the suites that touch these sections** + +```bash +cd packages/react && pnpm exec vitest run --environment jsdom src/__tests__/filter-builder.test.tsx src/__tests__/tool-panel.test.tsx src/__tests__/grouping-options.test.tsx src/__tests__/components-override.test.tsx; cd ../.. +``` + +Expected: `filter-builder`, `tool-panel`, `grouping-options` pass unchanged (every locator keys on the site attribute, which is still there). `components-override` still has failures for `tool-reset` and the icon sites — expected until Tasks 9–10. + +- [ ] **Step 4: Commit** + +```bash +git add packages/react/src/tool-panel/filters/FiltersSection.tsx packages/react/src/tool-panel/grouping/GroupingSection.tsx +git commit -m "refactor(react): the five ghost actions render the kit Button + ++ filter, + group, Add group, Expand all, Collapse all. Each keeps its site +attribute, so nothing that identified it stops identifying it. + +Co-Authored-By: Claude Fable 5.1 " +``` + +--- + +### Task 9: Migrate the link buttons (2 sites) + +**Files:** + +- Modify: `packages/react/src/filter-menu/FilterMenu.tsx` (~L396) +- Modify: `packages/react/src/tool-panel/ColumnsSection.tsx` (~L586) + +- [ ] **Step 1: FilterMenu — `Clear`** + +Add the import `import { usePretableComponents } from "../components/context";`. In the `FilterMenu` component body, before its `return`, add `const { Button } = usePretableComponents();`. Replace + +```tsx + +``` + +with + +```tsx + +``` + +- [ ] **Step 2: ColumnsSection — `Reset columns`** + +Add the import `import { usePretableComponents } from "../components/context";`. In the component that renders the reset button, before its `return`, add `const { Button } = usePretableComponents();` (this file also hosts the kebab migrated in Task 10 — one `const { Button, IconButton } = usePretableComponents();` serves both if they are in the same component). Replace + +```tsx + +``` + +with + +```tsx + +``` + +- [ ] **Step 3: Run the suites** + +```bash +cd packages/react && pnpm exec vitest run --environment jsdom src/__tests__/filter-menu.test.tsx src/__tests__/filter-menu-surface.test.tsx src/__tests__/tool-panel.test.tsx src/__tests__/components-override.test.tsx; cd ../.. +``` + +Expected: the first three pass unchanged. In `components-override`, the `tool-reset` and portalled `filter-clear` tests now pass; the icon-button tests still fail. + +- [ ] **Step 4: Commit** + +```bash +git add packages/react/src/filter-menu/FilterMenu.tsx packages/react/src/tool-panel/ColumnsSection.tsx +git commit -m "refactor(react): Clear and Reset columns render the kit Button, link variant + +The Clear button is inside the portalled filter dialog, which is the case +the components context exists for. + +Co-Authored-By: Claude Fable 5.1 " +``` + +--- + +### Task 10: Migrate the icon buttons (6 sites) and collapse the CSS + +**Files:** + +- Modify: `packages/react/src/filter-menu/FunnelButton.tsx` +- Modify: `packages/react/src/column-menu/MenuButton.tsx` +- Modify: `packages/react/src/tool-panel/ColumnsSection.tsx` (~L505–L530, the kebab) +- Modify: `packages/react/src/group-panel/GroupPanel.tsx` (~L467–L486) +- Modify: `packages/react/src/tool-panel/filters/FilterRow.tsx` (~L408–L420) +- Modify: `packages/react/src/tool-panel/grouping/GroupingSection.tsx` (~L420–L430) +- Modify: `packages/ui/grid.css` — collapse every site rule the component now owns + +- [ ] **Step 1: FunnelButton** + +Replace the file's body so the button is the kit's: + +```tsx +// packages/react/src/filter-menu/FunnelButton.tsx +import { createElement, type CSSProperties } from "react"; +import { usePretableComponents } from "../components/context"; +import { FunnelIcon } from "../icons"; + +export function FunnelButton({ + columnId, + label, + active, + open, + style, + onToggle, +}: { + columnId: string; + label: string; + active: boolean; + open: boolean; + style?: CSSProperties; + onToggle: (columnId: string, anchor: HTMLElement) => void; +}) { + const { IconButton } = usePretableComponents(); + return ( + e.stopPropagation()} + onClick={(e) => { + e.stopPropagation(); + onToggle(columnId, e.currentTarget); + }} + > + + + ); +} +``` + +Keep the two long comments that were on `tabIndex` and `onPointerDown` — copy them across unchanged; only the element and the two new lines (`import`, `site`) change. + +- [ ] **Step 2: MenuButton** + +Same shape: import `usePretableComponents` from `"../components/context"`, add `const { IconButton } = usePretableComponents();`, and replace `` with ``. The `ref={(node) => onNodeChange?.(columnId, node)}` callback stays exactly as it is — the component forwards it. Keep every comment. + +- [ ] **Step 3: ColumnsSection kebab** + +With `const { Button, IconButton } = usePretableComponents();` already in place from Task 9 (add `IconButton` to that destructuring), replace + +```tsx +