One component with switchable options, virtualization and windowing - #4
One component with switchable options, virtualization and windowing#4yaotzin1 wants to merge 9 commits into
Conversation
…wing
Every capability was becoming a component. A tree was TreeGridwright, row
actions were BubbleMenu composed by hand, editing was editableColumns plus
InlineEditProvider composed by hand. Each worked; none of them worked
together without the consumer reassembling the component's own arrangement,
and the tree was a parallel implementation that could never gain anything
added to Gridwright.
They are now options on one component: tree, virtual, rowActions,
onCellEdit, and icon on a column. They compose, and each piece is still
exported for a layout composed by hand. TreeGridwright survives as an alias
over <Gridwright tree={...} /> with no second implementation behind it.
Scale, which is two problems rather than one:
- virtual renders only the rows on screen, carrying the rest in two spacer
<tr> rows so the element stays a real table with real column alignment and
real grid semantics. aria-rowcount and aria-rowindex carry the truth.
- createWindowedDataSource holds blockSize * maxBlocks rows however large the
result set is. Ten million rows becomes a scrolling problem rather than an
impossible one.
Driving the playground in a browser found two things the suite did not:
- A browser will not render an element taller than about 2^24 pixels, so the
scrollbar could only reach row 419,000 of ten million, silently, while
aria-rowcount claimed all of them. Above that height scroll position is now
a ratio over the result set, with the cost written down.
- Two overlapping window fetches could leave the grid empty and 'ready': a
block being loaded for a request that was then aborted was joined by the
next request, resolved carrying nothing, and the survivor built its window
from an empty cache. A load is now only joined while its own request lives,
and blocks carry the generation of the cache they were fetched for.
Documentation updated everywhere it was wrong: README, docs/tree.md,
docs/data-sources.md, docs/extensibility.md, the docs index, the playground
README, the two agent rules that said this package has no virtualization, and
DEPENDENCY_MAP. New: docs/virtualization.md and specs/unified-options with all
eight artifacts. Version 0.4.0.
npm run verify: 297 unit tests, 12 smoke tests, lint and typecheck green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ndm72nQiMLvEqekHNJuAV
…e pages Using the pages rather than running the suite is what found these. The suite had been green through all of them. - Virtualization over an ordinary paginating source never placed its rows. Only the windowed source publishes where its rows start, and a missing offset was read as zero, so the fetched page was drawn over rows one to a hundred while the rows on screen stayed skeletons for ever. It now falls back to the query and treats rows as unplaceable until they settle, rather than drawing them somewhere wrong. - Switching editing off took the grid down. The engine resolves columns in an effect, so for one render the rows still held editable cells while the prop was already gone, and those cells threw for want of a provider. The tree made it permanent: its column wrapping was keyed on ids alone and so never re-wrapped. Toggling an icon had the same cause. - A column's icon sat beside its editable trigger rather than inside it, so clicking the icon did nothing, which reads as "this cell is not editable". - The row action menu opened a row too low. It is positioned inside a zero-height anchor but was measured against the grid root, so every menu was out by whatever sat above it, usually the toolbar. Each has a regression test, and each was confirmed to fail without its fix. The other two pages now carry the new capabilities too: - The vanilla page gains the windowed source over ten million rows, with no framework involved anywhere on it, plus per-row icons it draws itself. The mock API gained /api/people/range, so those ten million rows are a real network boundary rather than a function pretending to be one. - The React page gains row actions, inline editing and windowing as switches over its paginating REST source, with edits written to the mock table so they survive the next fetch. npm run verify: 303 unit tests, 12 smoke tests, lint and typecheck green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ndm72nQiMLvEqekHNJuAV
A tree grid's rows are placements, so `row.data` is a node there and the consumer's row everywhere else. A menu written for a flat grid therefore broke the moment `tree` was switched on, which makes the switch a lie, and the alternative in the typed example was a double cast. `rowDataOf(row)` answers with the consumer's row either way. Tested on both shapes with a single handler, and used in the type-checked example. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ndm72nQiMLvEqekHNJuAV
|
Two follow-up commits, both driven by using the pages rather than by the suite. Four bugs the playground found, each with a regression test confirmed to fail without its fix:
All three pages now carry the new capabilities. The vanilla page gets the windowed source over ten million rows with no framework involved anywhere on it, talking to a new
🤖 Generated with Claude Code |
…act page Three things, all reported from using the pages. The row menu opened at the table's trailing edge. On a wide table that is a journey away from the row you are pointing at, and it lands on top of the last column when it gets there. It now opens beside the pointer, on the row the pointer is over, clamped to stay inside the grid, and falls back to the row's trailing edge when the row was reached by keyboard, since there is no pointer to be near then. Placed once per row rather than followed continuously: a menu that slides while you approach it is a menu you cannot click. The vanilla page had no tree, which left the impression that hierarchy is a React feature. It is not: the nested set, the controller and the flattening stage are all core. That page now draws one itself with string concatenation, including expansion, search that keeps the folders of a match, nested-set counters, and a row menu the page builds from plain buttons over the same controller. `docs/tree.md` gained the recipe. The React page now has a tree too, as one more switch beside row actions, inline editing and windowing, so the claim that these compose can be checked on the page that shows the plain component. npm run verify: 308 unit tests, 12 smoke tests, lint and typecheck green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ndm72nQiMLvEqekHNJuAV
…inks The tree panel's controls were in the page's `.controls` grid, which spread three buttons across a wide row and pushed the search box under a label. They are a plain flex row now. They also sit outside `.gw-root`, so none of the grid's custom properties reached them and they rendered unstyled; they borrow the page's own tokens instead. The links between the three pages still described the third one as "tree, bubble menu and inline editing", which stopped being true when it became the page where every option is a switch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ndm72nQiMLvEqekHNJuAV
Three questions, and one of them was about a real gap. **Storing a tree.** `onCommit` already carried everything a database needs: `insert` names the parent and the index, `move` names both parents, `remove` names the scope, `update` carries the row and what it replaced. What was missing was saying so. `docs/persistence.md` now writes down the table behind each change, why the nested set is derived rather than stored, what an edge table is for when a row has several parents, and which refusals a server must make on its own. Both playground trees post their changes to the mock API, which keeps an adjacency list: add or rename something and reload the page, and it is still there. **Storing inline edits.** The same answer, one hook earlier: `onCellEdit` gets the row's own id, the column and the value. The vanilla page now sends one, refuses an empty name with a 422, and shows what came back rather than what it hoped for. **Why the vanilla page had none of the adapter's tricks.** Partly because they are DOM work, and partly because the window arithmetic was locked inside a React hook, where nothing without React could reach it. It was never a React concern: four numbers in, a slice and two spacer heights out. It is now `computeVirtualWindow` and `scrollOffsetForIndex` in the core, and `useVirtualRows` is the binding that reads two DOM numbers once per frame. The vanilla page virtualizes ten million rows with it, edits cells in place and opens a row menu, all written by the page. Two bugs found while doing it: - A tree whose rows arrived from a server rendered as a list of roots. `defaultExpandedDepth` was consumed on the first normalise, which for an asynchronous source happens with no rows in hand. - `hidden` did not hide a grid part: every layout rule uses a class selector, which outranks a bare attribute selector, so the pagination footer stayed on screen under a virtualized body. npm run verify: 319 unit tests, 12 smoke tests, lint and typecheck green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ndm72nQiMLvEqekHNJuAV
The core window arithmetic, the tree that rendered as a list of roots, and `hidden` not hiding a part were all in the work and none of them were in the changelog. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ndm72nQiMLvEqekHNJuAV
…nstyled Reported as "none of them work". All three did work; nothing about the page said so. - The switches lived in the data source panel, a screen and a half above the grid they act on, while the first table on the page is the tree, which has no editors. Anyone following "click a name to edit it" clicked a tree row and nothing happened. They now sit directly above their own grid, with a note saying which table they apply to and what to click. - The row menu was appended to `document.body`, outside `.gw-root`, so none of the stylesheet's custom properties reached it: it rendered as bare text on a transparent background, which reads as broken rather than as styled differently. It is appended inside the grid root now. - Ticking "virtual" over a twenty-five row page changed nothing anyone could see, because there was nothing to window. It now also raises the page size, which is what virtualization means for a page: the scrollbar is the navigation and the page is the data window behind it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ndm72nQiMLvEqekHNJuAV
…ens the menu Two asks in one change. **The menu opens on a left click.** `rowActionsTrigger` gained `click` and the default includes it, pinned until you click elsewhere or press Escape. A click landing on a button, a link or a field is left alone, because that click belongs to the control it landed on: clicking an editable cell must open its editor, not a menu over it. The vanilla pages do the same, through one shared helper. **The pages' JavaScript is no longer wrapped in HTML.** Each page was a thousand-line inline module, which meant nothing could be shared between them and nothing checked any of it. Every page is now markup plus one file under `examples/playground/js/`, with the package loader, the row menu, the icons and the two DOM helpers in `js/shared/`, and the framework-free tree in a module of its own. ESLint covers the directory; its first run found a piece of dead state on the React page. That split immediately produced the bug it was supposed to prevent: a relative script path resolved against `/` and the vanilla page silently served an empty grid with no console error. CI now requests every module each page references, which is the check that would have caught it, plus the stored tree endpoint. npm run verify: 322 unit tests, 12 smoke tests, lint and typecheck green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ndm72nQiMLvEqekHNJuAV
Every capability was becoming a component: a tree was
TreeGridwright, row actions wereBubbleMenucomposed by hand, editing waseditableColumnsplusInlineEditProvidercomposed by hand. Each worked. None of them worked together without reassembling the component's own arrangement, and the tree was a parallel implementation that could never gain anything added to<Gridwright />.They are now options on one component, and they compose:
Each piece is still exported for a layout composed by hand, and
TreeGridwrightsurvives as an alias with no second implementation behind it.Scale, which is two problems
virtualrenders only the rows on screen, carrying the rest in two spacer<tr>rows so the element stays a real<table role="grid">with real column alignment.aria-rowcountandaria-rowindexcarry the true numbers.createWindowedDataSourceholdsblockSize * maxBlocksrows however large the result set is.Measured in the browser at ten million rows: 23 rendered rows, 2 blocks cached, 400 rows resident, 2 range requests.
Two bugs the test suite did not find
Driving the playground in Chrome found both.
aria-rowcountclaimed ten million, silently. Above that height scroll position is now a ratio over the result set, with the cost written down rather than hidden.ready. A block being loaded for a request that was then aborted was joined by the next request, resolved carrying nothing, and the survivor built its window from an empty cache. A load is now only joined while its own request is alive, and blocks carry the generation of the cache they were fetched for.Both have regression tests.
Documentation
Updated everywhere it was wrong: README,
docs/tree.md,docs/data-sources.md,docs/extensibility.md, the docs index, the playground README, the two agent rules that claimed this package has no virtualization, andDEPENDENCY_MAP.md. New:docs/virtualization.mdandspecs/unified-options/with all eight artifacts.Semver
Minor. Six optional props, one optional column field, two optional
GridTableprops, and new exports. No signature, removal or default changed, so a 0.3.0 grid renders identically. Version 0.4.0.Verification
npm run verify: 297 unit tests, 12 smoke tests, lint and typecheck green. Plus a manual pass over the playground in Chrome: every switch and combination, ten million rows scrolled to row 10,000,000, and an edit committed over the windowed source with the block cache dropped and refetched.🤖 Generated with Claude Code
https://claude.ai/code/session_011ndm72nQiMLvEqekHNJuAV