Repository navigation
Add group header and group banner - #1
Merged
Merged
Conversation
The stylesheet was nine hand-written rules. The trigger for switching was
always "when the second real UI surface lands", and the group banner, the
column picker and the toolbar are that surface.
Consumers see no change: same `@matthewhsu1/datagrid/datagrid.css` import,
same `dg-` class names for the one rule that stays a class.
Three things are load-bearing in `src/datagrid.src.css`:
- Preflight is deliberately left out. `@import "tailwindcss"` pulls in a
reset, and a package that resets its host's margins and form controls is
a package that vandalises the app installing it. Importing `theme.css`
and `utilities.css` separately takes the utilities without the reset.
- `prefix(dg)` namespaces every utility and every theme variable, so a
host app's own Tailwind and ours cannot collide.
- `@source` is what makes any of this work. Tailwind builds its output by
scanning source, and it never scans `node_modules` — which is why a
package cannot ship unprefixed utilities and hope the consumer emits
them. Scanning at build time and shipping the result closes that hole.
`Input` gains an `invalid` prop rather than taking a `dg-input--invalid`
class from outside. Tailwind orders its output by utility, not by the order
class names appear on an element, so an override layered on a base would be
decided by the generated stylesheet instead of by the component. The two
states are now whole, mutually exclusive strings.
The demo aliases the package stylesheet to the SOURCE and runs Tailwind
itself, so the demo stops styling itself the moment that source stops
compiling.
The same component will render in a hole in the grid canvas and in the bar above it, which is what stops the pinned copy and the in-grid copy drifting apart. Nothing mounts it yet. It takes a RESOLVED css colour rather than a Radix scale. Turning a scale into a colour reads vars off the live Themes root — a document lookup — and keeping that outside leaves this a pure function of its props, renderable in both places without either caring where it is. That lookup moves to `groupHeaderColor.ts`, which `groupHeaderCell` now shares. One source of truth for "what colour is this group's name" while the canvas cell and the DOM row both exist. The collapse control is a real button with a real accessible name, rather than the cell-click the canvas header needs.
`GridStatusBar` rendered nothing whenever there was nothing to report, so the grid body moved under the pointer every time a load settled. The bar is now always there, and the status is one ZONE inside it rather than the whole of it. Three zones: the group the user is currently inside, the status, and the column picker. The group zone renders the same `GroupHeaderRow` the in-grid headers will, minus the host's components — those get a second row here, because the status and the picker have already spent the first one. One deliberate change of behaviour beyond the move: a failed load no longer replaces everything above the grid. It fills the status zone instead, so losing rows never also costs the user the control that decides which columns the grid shows. Every other status precedence is unchanged, and its reasoning moved across with it. `group` is wired to null at both call sites. Nothing computes the current group yet.
Show and hide only, plus a reset. Reordering stays a drag on the column
itself: two ways to do one thing is two sets of edge cases, and a drag here
disagreeing with a drag there about where a column went is the obvious one.
It lives in the bar rather than in a context menu on the header row, because
a hidden column leaves no header to right-click — the only way back would be
a control the user cannot see.
The grouped column is locked. Hiding it would leave the banner naming a
group with no column anywhere to point at.
Two things the picker needed that did not exist:
- `resetColumns`, which restores the descriptor's order and clears widths
and hidden. It COPIES `defaultOrder`; handing out the descriptor's own
array would let the first drag after a reset rewrite the descriptor, and
every later reset would then restore the dragged order.
- `resetColumns` in the persistence effect's watch list. Without it a reset
looked like it worked and then handed the thrown-away layout straight
back on the next load.
Split in two so it can be tested: `ColumnPickerList` holds every decision and
renders with a plain mount, while the popover shell holds none. A popover
needs `ResizeObserver`, which jsdom does not ship — there is now a stub for
it in the test setup, since the tests that mount one assert on what is in it,
never on where it was placed.
The shell was Radix and the rows were browser defaults, so the popover would have rendered half in the app's design system and half not. Radix `Checkbox`, `Text`, `Badge` and `Button` throughout now. A Radix checkbox is a button, and a wrapping `<label>` gives a button no accessible name, so each row names its checkbox with `aria-labelledby`. The lock badge is part of that name rather than decoration beside it: "why can I not hide this column" should reach a screen reader too. The id scope comes from `useId` and not from the field. Field names are unique within a grid but not across two grids on one page, and `aria-labelledby` resolves against the whole document. One test added, for the behaviour the swap could quietly have cost: clicking a row's TEXT, not just its box, still toggles the column. It passed on the first run, so it was checked the only way that means anything — by breaking the label wrapper and watching it fail.
A canvas is a picture, and a picture cannot hold a component. So the canvas
stops drawing the group's name and draws the SPACE for it: full-width, no
text, filled from the theme's header background. `groupHeaderCell` is gone;
`groupHeaderHole` takes its place.
The hole is still filled and still spans every column. The canvas paints
first and React commits after, so an unfilled hole would flash the ordinary
cell fill on any frame the layer has not caught up with, and an unspanned one
would draw column borders straight across the header.
Nothing in the display model changes. A header still occupies exactly one
display row, and every position sum in the engine is untouched.
Three pure pieces the layer needs, each with its own suite:
- `headersInDisplayRange` — the headers a viewport can see. Reads segments
rather than walking rows, so a tall viewport costs one pass over the
groups instead of one lookup per row. A COLLAPSED group is included: its
header is the only way back to its rows.
- `groupAtDisplayRow` — the group a row belongs to, header or data. The
banner asks this about the first visible row, which is what will make it
name the group the user is INSIDE rather than the last header they
scrolled past. The two differ for every group taller than the viewport.
- `rowHeightFor` — a header row gets `headerHeight`, everything else the
grid's row height.
Two `useCellRenderer` tests now assert the opposite of what they did: the
header cell must paint no name and must ask `grouping.label`/`color` for
nothing. A name painted here would sit under the React one and show through.
`GroupHeaderLayer` fills the holes the canvas now reserves. This is what makes host components possible in a group header at all: a canvas is a picture, and a picture cannot hold a button. Two owners, deliberately split. React owns WHICH headers exist — a set that changes only when the viewport crosses a group boundary, a few times per scroll. The DOM owns WHERE they are, through an imperative `place`, called on every visible-region change glide reports. Routing positions through React state instead would re-render the layer on every scroll frame and leave each header trailing the rows it names. This is the same bargain `useRepaintRows` already strikes for changed rows: React for what exists, direct writes for what moves. There is a test that place() costs no render. A header starts hidden and stays hidden until it is placed. React commits a new header before its placement arrives, and one drawn at zero in between would pile up over the grid's first row. The layer takes no pointer events and each header takes them back. A header covers only its own hole, but the layer spans the whole grid, and a layer that swallowed clicks would make every row under it unselectable. Nothing mounts it yet.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.