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
14 changes: 14 additions & 0 deletions .changeset/auto-width-naming-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@pretable/react": minor
"@pretable/core": minor
---

`autosizeColumns()` becomes `setAllColumnsAutoWidth(auto)`, and undeclared columns have one default width.

**The rename.** The grid handle's `autosizeColumns()` never sized anything to content — it put every column into the auto-width set, the mode bit that says "the grid manages this column's width". Nothing in the column-width path measures a cell. It is now `grid.setAllColumnsAutoWidth(auto: boolean)`, symmetric with the per-column `setColumnAutoWidth(columnId, auto)` that shipped alongside the tool panel, and it moves the roster in BOTH directions — `false` freezes every column at the engine's stored width, which the old name could not express. The surface's `autosize?: boolean | AutosizeOptions` prop is likewise `allColumnsAutoWidth?: boolean`, and the `AutosizeOptions` type is gone from both packages: it was a tuning bag (`averageCharWidth`, `cellPaddingPx`, min/max) for a measurement pass that does not exist, so every field was inert.

**One default width.** A column that declares no `widthPx` was drawn by the renderer at 140px but STORED by the engine at 160px, so turning auto width off on a never-resized column jumped its width 140 → 160 for no reason a user could see. Both numbers now come from one shared constant (140, and 220 for a `wrap: "text"` column) and the engine seeds its stored width through the renderer's own resolver, so the freeze lands on the pixel the column was already drawing. 140 won because it is what undeclared columns have always painted at — no grid repaints as a result of this change.

**A double-click that does something.** Double-clicking a column's resize handle was wired to a no-op. It now calls `setColumnAutoWidth(columnId, true)`, the pointer shortcut for handing that column's width back to the grid, which is what the docs had claimed all along. It no longer fires `onColumnWidthsChange`: that callback reports the engine's STORED widths, and handing a column to the grid moves none of them — the notification announced a change that had not happened.

**Fixed: auto width did not survive a controlled `state.columnWidths`.** `setColumnWidth` cleared a column's auto bit unconditionally, and a controlled consumer replays its whole widths map through `setColumnWidth` on every write-back pass. So any re-render of such a consumer silently took every column in the map back out of the auto set — the tool panel's Auto width toggle and the resize handle's double-click both appeared to work and were undone before paint. The bit is now cleared only when the write actually MOVES the stored width, compared across grid-core's own min/max clamping rather than against the requested number. This is a user-visible fix to behavior that shipped with the tool panel's auto-width toggle, not a consequence of the rename.
3 changes: 2 additions & 1 deletion apps/bench/src/bench-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ export function BenchApp({ search, browserVersion }: BenchAppProps) {
}, []);
/**
* Adapter-agnostic autosize entry point. Each adapter calls back with
* a closure over its native autosize API (pretable: grid.autosizeColumns;
* a closure over its native autosize API (pretable:
* grid.setAllColumnsAutoWidth(true) — the grid-managed-width mode bit;
* ag-grid: gridApi.autoSizeColumns; mui: apiRef.autosizeColumns). The
* autosize bench script awaits this callback and times to the next paint.
*/
Expand Down
10 changes: 6 additions & 4 deletions apps/bench/src/pretable-adapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,10 @@ export interface PretableAdapterProps {
initialRows?: readonly ScenarioRow[];
/**
* Called once the adapter has a usable autosize entry point. The
* supplied callback wraps `grid.autosizeColumns()` so the bench
* harness can invoke it on demand for the autosize script.
* supplied callback wraps `grid.setAllColumnsAutoWidth(true)` — pretable's
* "grid-managed width" mode bit, its nearest analog to the other grids'
* autosize APIs — so the bench harness can invoke it on demand for the
* autosize script.
*/
onAutosizeReady?: (autosize: () => Promise<void> | void) => void;
/**
Expand Down Expand Up @@ -416,7 +418,7 @@ export function PretableAdapter({
gridInstanceIdRef.current = String(gridInstanceSeq);
publishGridInstanceId();
onGridReadyRef.current?.(grid);
onAutosizeReadyRef.current?.(grid.autosizeColumns);
onAutosizeReadyRef.current?.(() => grid.setAllColumnsAutoWidth(true));
},
[publishGridInstanceId],
);
Expand Down Expand Up @@ -547,7 +549,7 @@ export function PretableAdapter({

<PretableSurface
ariaLabel="Pretable React adapter"
autosize={autosize}
allColumnsAutoWidth={autosize}
columns={surfaceColumns}
model={rowModelOwner.model}
state={surfaceState}
Expand Down
31 changes: 18 additions & 13 deletions apps/website/content/docs/grid/column-layout.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
title: Column Layout
description: "Resize, reorder, pin, and autosize columns; per-column min/max; controlled state."
description: "Resize, reorder, pin, and auto width; per-column min/max; controlled state."
nav: Grid
---

Pretable supports column resize, reorder, pin, and autosize out of the box. Width, order, and pinning live behind narrow controlled UI-state slices. Query derivations are controlled separately through the exact `query`/`onQueryChange` pair.
Pretable supports column resize, reorder, pin, and auto width out of the box. Width, order, and pinning live behind narrow controlled UI-state slices. Query derivations are controlled separately through the exact `query`/`onQueryChange` pair.

Width, order, and pin are all controlled in the grid below — drag a header to reorder it, drag its right-edge handle to resize, double-click the handle to autosize. (Resizing needs a fine pointer; see [Resizing is a pointer affordance](#resizing-is-a-pointer-affordance).) Symbol is pinned left and Note is pinned right, so dragging a column into either group (or out of one) is visible immediately, and the readout beneath the grid echoes the committed order, pins, and widths after every drag:
Width, order, and pin are all controlled in the grid below — drag a header to reorder it, drag its right-edge handle to resize, double-click the handle to hand the width back to the grid. (Resizing needs a fine pointer; see [Resizing is a pointer affordance](#resizing-is-a-pointer-affordance).) Symbol is pinned left and Note is pinned right, so dragging a column into either group (or out of one) is visible immediately, and the readout beneath the grid echoes the committed order, pins, and widths after every drag:

<Example id="column-layout" />

Expand All @@ -16,7 +16,7 @@ Every header where `column.resizable !== false` exposes a 4px hit-target on its

Per-column `minWidthPx` and `maxWidthPx` clamp the result. Engine-wide defaults are 40px min and 800px max; supply tighter bounds on the column when the data has known constraints.

Double-clicking the resize handle calls `grid.autosizeColumn(columnId)` — the keyboard-free shortcut for "fit this column to its content."
Double-clicking the resize handle calls `grid.setColumnAutoWidth(columnId, true)` — the keyboard-free shortcut for handing this column's width back to the grid. See [Auto width](#auto-width) for what the grid does with it.

The synthetic row-select column has no resize handle. Set `resizable: false` on any other column to opt it out.

Expand All @@ -26,9 +26,9 @@ There is no key that resizes a column, and on a touch device there is no gesture

That is a deliberate removal, not an oversight. The handle is 4px wide, and 4px is not a target a finger can acquire — well under the 24px [WCAG 2.5.8](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html) asks for. Inflating it instead was the other option and it costs more than it buys: the trailing edge it would need is the same edge the filter funnel and column menu spend on their own 24×24 targets, and dragging a column edge inside a phone-width viewport is a poor interaction even with a large target. So the strip is dropped and the 48px it freed goes to the two controls that remain — see [Filtering § The funnel on touch](/docs/grid/filtering#the-funnel-on-touch).

`display: none` rather than a `matchMedia` guard in `@pretable/react` is also deliberate: a media query is evaluated by the engine on both sides of a stream, so a server-rendered grid has no client/server disagreement for hydration to reconcile. Nothing changes on a fine pointer — the strip, the drag, and the double-click-to-autosize are exactly as described above.
`display: none` rather than a `matchMedia` guard in `@pretable/react` is also deliberate: a media query is evaluated by the engine on both sides of a stream, so a server-rendered grid has no client/server disagreement for hydration to reconcile. Nothing changes on a fine pointer — the strip, the drag, and the double-click are exactly as described above.

If your app needs touch resizing, put it behind an explicit control (a menu item calling `grid.setColumnWidth`, or `grid.autosizeColumn`) rather than restoring the strip.
If your app needs touch resizing, put it behind an explicit control (a menu item calling `grid.setColumnWidth`, or `grid.setColumnAutoWidth`) rather than restoring the strip. The [tool panel](/docs/grid/tool-panel)'s column row menu already ships exactly that.

## Reorder

Expand Down Expand Up @@ -64,7 +64,7 @@ Left-pinned columns render first, in a sticky group flush against the viewport's

**Array order is visual order.** The engine's column array is always grouped — leading pinned columns, then the unpinned ones, then trailing pinned columns — so a column's index in that array is the position it renders at. `aria-colindex` is derived from the same index, which means assistive technology reports the position the column is actually drawn at rather than a stale array slot. Both `setColumnPinned` and reorder maintain the grouping: pinning moves the column into its region, and a move that lands in a region takes that region's pin.

You do not have to declare columns in that order. A `columns` array that interleaves pinned and unpinned entries is regrouped on the way in — at mount, on every prop update, and on `resetColumnLayout` — with relative order preserved inside each region. So `[symbol, note (right), name]` becomes `[symbol, name, note]`, and the column you declared second still renders last where its pin puts it.
You do not have to declare columns in that order. A `columns` array that interleaves pinned and unpinned entries is regrouped on the way in — at mount, on every prop update, and on the tool panel's Reset columns — with relative order preserved inside each region. So `[symbol, note (right), name]` becomes `[symbol, name, note]`, and the column you declared second still renders last where its pin puts it.

**Pinned columns are never virtualized away.** Horizontal virtualization only windows the scrollable group, and that window runs from `scrollLeft` to `scrollLeft + viewportWidth - pinnedRightWidth`: it ends before the right-pinned group, which overlays the viewport's trailing edge, but it deliberately does _not_ subtract the left-pinned width. That over-includes the columns currently hidden behind the left-pinned group, which is the conservative direction — a column is rendered when in doubt, never dropped. A pinned column is in the render snapshot at every scroll position, so a pinned "actions" or identity column can't scroll out from under the user.

Expand All @@ -74,17 +74,22 @@ The synthetic row-select column is always at position 0; it is never pinnable, r

[Row grouping](/docs/grid/grouping) changes the drawn column list: grouped columns are dropped from the data area unless `hideGroupedColumns: false`, and a derived group column is prepended. Because the list is then regrouped into its pinned regions, an unpinned group column heads the _scrolling_ run and therefore sits after your left-pinned columns — pass `groupColumn={{ pinned: "left" }}` to seat it ahead of them. Read the drawn order from `grid.getColumns()`, never from the `columns` prop.

## Autosize
## Auto width

- `grid.autosizeColumn(columnId, options?)` — fit one column to its measured content width.
- `grid.autosizeColumns(options?)` — fit every column.
- Double-click on a column's resize handle is the keyboard-free shortcut for the single-column form.
**Auto width is a mode bit, not a content fit.** On, it means "let the grid manage this column's width": the column draws at the renderer's default, or takes a flex share when the column declares `flex` — nothing measures cell content anywhere in the width path. Off means manual, at whatever width the engine currently stores.

`AutosizeOptions` controls the algorithm (sample size, padding, header inclusion). Defaults are sensible; reach for the options when you need to constrain measurement on very long datasets.
- `grid.setColumnAutoWidth(columnId, auto)` — move one column into or out of the auto set.
- `grid.setAllColumnsAutoWidth(auto)` — move every column at once, both directions.
- Double-click on a column's resize handle is the keyboard-free shortcut for the single-column form, turning auto **on**.
- The [tool panel](/docs/grid/tool-panel)'s column row menu exposes the same bit as an **Auto width** toggle, and reflects it.

Columns that declare no `widthPx` start in the auto set; declared ones start manual. Sizing a column yourself takes it out — a resize drag and `grid.setColumnWidth` are both manual gestures, and each writes the width it lands on.

Turning auto **off** freezes the column at the engine's stored width. For a column you never resized, that stored width is the same number the grid was already drawing — the renderer's undeclared-width default and the engine's stored default are one shared constant — so the freeze does not move a pixel.

## Reset

`grid.resetColumnLayout()` restores order, widths, and pinned state to the original `columns` prop snapshot taken at mount. Useful for "Reset layout" toolbar buttons that recover after a user has rearranged the grid.
The [tool panel](/docs/grid/tool-panel)'s columns section carries a **Reset columns** button, and it is the only reset the library ships: it restores the order, pinning, visibility, and auto-width state the grid mounted with. There is no `reset` method on the grid handle — to recover the mount-time layout from your own toolbar button, control the slices you care about (below) and write your saved snapshot back through `state.columnWidths` / `state.columnOrder` / `state.columnPinned`.

## Controlled state

Expand Down
4 changes: 2 additions & 2 deletions apps/website/content/docs/grid/editing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -310,10 +310,10 @@ When `validate` returns a string the edit returns to `editing` with `snapshot.ed

`saving` and `error` are the two phases text on this page can only assert, not show — the field visibly goes read-only and `aria-busy` while a commit is in flight, and a rejection leaves the editor open with an inline message. That's exactly what happens in the grid at the top of this page when you edit **Quantity** to a negative number: the field dims for ~800ms (`saving`), then the commit is rejected with an inline message (`error`).

For most apps the default editor handles all of this and you never touch the phases directly. If you render cells yourself (a custom `render`, or the [headless engine](/docs/headless)), read `grid.getSnapshot().editing` — `{ rowId, columnId, draft, status, error? }` — to drive your own in-cell editor or status affordance:
For most apps the default editor handles all of this and you never touch the phases directly. If you render cells yourself (a custom `render`, or the [headless engine](/docs/headless)), read `grid.getState().editing` — `{ rowId, columnId, draft, status, error? }` — to drive your own in-cell editor or status affordance:

```tsx
const { editing } = grid.getSnapshot();
const { editing } = grid.getState();
if (editing?.status === "saving") {
// show a spinner in the cell at editing.rowId / editing.columnId
}
Expand Down
1 change: 0 additions & 1 deletion apps/website/content/docs/grid/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ The other 47 tokens in [the theming contract](/docs/theming/token-reference) are

The engine has more capabilities than this section covers:

- **Column autosize** — the `autosize` option on `usePretable`; resizes columns to content. Not yet documented as a recipe.
- **Streaming and transactions** — ordinary React updates use the `rows` prop; high-frequency producers explicitly own a row model and connect the [streaming adapter](/docs/streaming).
- **Per-row measured heights** — pass `measuredHeights: Record<string, number>` to `usePretable` for content-aware row sizing. The bench's `pretable-adapter.tsx` shows this pattern.

Expand Down
2 changes: 1 addition & 1 deletion apps/website/content/docs/grid/keyboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ grid.setFocus({ rowId, columnId });
grid.moveFocus("down");
grid.moveFocus("right", { extend: true }); // shift+right equivalent
grid.moveFocus("down", { jumpToEdge: true }); // cmd+down equivalent
grid.selectAll();
grid.selectAllVisibleRows();
grid.clearSelection();
```

Expand Down
2 changes: 1 addition & 1 deletion apps/website/content/docs/grid/tool-panel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The pane lists every data column in **drawn order** — the order the engine act
- **A ⋮ menu** with Pin left, Pin right, Unpin, and an **Auto width** toggle. The menu is also the only way to pin into an _empty_ pinned group: with no rows in a subgroup there is no boundary to drag or arrow across, so the menu is the affordance that creates the first member.
- **Search** filters the list by column label; **Reset columns** restores the order, pinning, visibility, _and_ auto-width state the grid mounted with.

**Auto width is a mode bit, not a content fit.** Checked, it means "let the grid manage this column's width": the column draws at the renderer's default, or takes a flex share when the column declares `flex` — nothing measures cell content. Columns that declare no `widthPx` start in auto mode; sizing a column yourself — dragging its header resize strip, or calling `setColumnWidth` — turns it off, and the toggle reflects that the next time the menu opens. Turning the toggle back on hands the width back to the grid. The same mode bit is scriptable as `grid.setColumnAutoWidth(columnId, auto)` on the handle. One visible consequence worth expecting: turning auto **off** on a column you never resized jumps its width from 140px to 160px — the renderer's default and the engine's stored default are two different numbers, so freezing the column at the engine's width is a real (if small) move, not a no-op. The toggle's label is the `toolPanelAutoWidthLabel` message.
**Auto width is a mode bit, not a content fit.** Checked, it means "let the grid manage this column's width": the column draws at the renderer's default, or takes a flex share when the column declares `flex` — nothing measures cell content. Columns that declare no `widthPx` start in auto mode; sizing a column yourself — dragging its header resize strip, or calling `setColumnWidth` — turns it off, and the toggle reflects that the next time the menu opens. Turning the toggle back on hands the width back to the grid. The same mode bit is scriptable as `grid.setColumnAutoWidth(columnId, auto)` on the handle, and `grid.setAllColumnsAutoWidth(auto)` moves every column at once. Turning auto **off** freezes the column at the engine's stored width — for a column you never resized that is the same number the grid was already drawing (the renderer's default and the engine's stored default are one shared constant), so the freeze is visually a no-op, not a jump. The toggle's label is the `toolPanelAutoWidthLabel` message.

Everything the panel commits writes straight into the engine, so the grid it changes is the same layout header gestures change. That has one consequence worth knowing before you control layout state: a controlled `state.columnOrder` or `state.columnPinned` remains the authority, and it re-imposes the prop's layout over the panel's commits whenever the write-back effect re-runs — any state change reaching the surface is enough. Leave those slices uncontrolled when the panel should own them.

Expand Down
Loading