Conversation
docs/ holds notes written for whoever is building this: a phase plan, a write-up of a popover bug in the dependency, and a feature inventory read off the source. None of it is documentation for someone installing the package, and the repo is public, so leaving it there published all of it to strangers by default. Untracked rather than deleted: the files stay on disk and .gitignore keeps them from coming back. Nothing referenced them, and `files` in package.json ships dist alone, so the tarball never carried them either.
A class names one of a fixed set of looks. It cannot name a value computed
per cell, which is what a colour scale, a data bar or a per-user cursor tint
is — so a feature that wanted one had to reach outside the extension point,
which is the same as saying the extension point was not enough.
`CellDecoration` takes a `style` record now, keyed by CSS property, custom
properties included: `{ '--dg-bar': '42%' }` is how a feature reaches a
pseudo-element without an element of its own. Several features decorating the
same cell merge per property with the later one winning, matching how their
classes already concatenated.
The cell keeps its own layout either way. The grid writes the grid column, the
pinned offsets and the editor's padding as style *directives*, which outrank
the attribute a decoration lands in, so a decoration can paint a cell but
cannot move it out of its column, unpin it or escape the row — there is a test
holding that line. A value is cut at the first `;` as well, so one entry stays
one declaration and a colour read out of row data cannot open a second.
The playground grows a heatmap on the metrics grid, which is the whole feature
in nine lines.
`openFilter` carried a single ellipsis glyph while `search`, three lines above it in the same file, carried three dots. Both are the same pause, and one of them is typeable, so both are three dots now, across all twelve packs. The chip for a numeric range used an en dash between the ends. A hyphen reads the same and can be typed. Left alone, because there the character is the content rather than the typography: the em dash a blank cell draws, and the >= symbols that are the filter operators themselves.
The same three places sv5ui puts them, so the two repos read alike: a FUNDING.yml for the Sponsor button GitHub draws on the repo page, a badge beside the other three in the README, and a `funding` field in package.json, which is what makes npm print the line on install. Indentation follows this repo's prettier rather than sv5ui's, which is the only thing that differs.
`cellDecoration` paints a cell; it cannot change what the cell is. A feature that had to hold a value back could therefore grey it out and watch it leave through the clipboard, the CSV, and the text a quick filter searches. `GridFeature.cellValue` is asked per column and purpose and returns a reader for the cells it substitutes. Six exits go through it: the cell and its tooltip, export, the clipboard, the quick filter's text, the list a set filter offers, and the draft an editor opens with. `grid.readerFor(columnId, purpose)` is how a pass that reads a whole column asks once and then loops; `grid.getValue` takes the purpose and defaults to what the cell draws. Identity is the contract: hand the value back unchanged for a cell left alone. The grid reads that to settle editing as well, since a cell whose value was substituted is one an editor would commit the substitute over, so it does not open. A batch drops such a cell rather than writing it, which is what a paste across a masked column now does. Sorting and filter predicates keep reading the raw value. A gate in the sort would undo the single-pass comparators of 1.2.0, and a predicate deciding which rows survive cannot decide on a substitute, so a masked column is still orderable and still narrowable: take `sortable` and `filter` off it. The two caches that hold results per column, the search text and the set filter's value list, are now keyed by the reader as well, so swapping one does not answer out of the list built for the other. A guard test keeps `getCellValue` behind the door so a seventh exit cannot grow back unnoticed. The playground route /value-gate shows all six exits changing at once.
`applyEdits` drops a cell the column will not take; `commitRow` wrote every draft it held. Through the UI the two agreed, since a row edit only opens fields `editableAt` allows and only those fields write drafts. Through `setRowDraft` they did not, so a draft set by hand on a column a value gate is holding back, or on one that is simply not editable, reached the data. Also pins what a built-in renderer does with a substitute it cannot draw: a string on a currency column parses as no number and the cell draws empty, `null` draws the column's empty text. Nothing leaks either way, but the mark only appears where the renderer can show one, so a gate on a typed column has to answer in that type. Said in the type doc and in the README. Adds the coverage the first pass left out: the cell in the DOM and the markup behind it, the tooltip and `cellClass` callbacks, a column the grid does not show but an export names, a gate that hides one row of a column and not the next, a formatted copy, and the row-edit door above.
The gate was proven on a plain client grid, which is the one arrangement where the rows on screen are also the rows an export writes. Server mode: the filter stage returns the page whole there, so the search path a gate closes on the client is not even walked. What is left is the page in hand, and that is what a user copies. Covered: the page it holds and the page it turns to, the copy and the export of the loaded rows, the set filter's list, the cell that will not open, and the quick filter staying a pass-through whose query is the string the user typed rather than anything read off a cell. Client paging: `allRows` reaches past the window, so twenty rows come out gated while five are on screen, and a search crossing pages finds the substitute rather than the value. Virtualization: rows drawn for the first time after a scroll carry the substitute too, with the value absent from the markup on both windows.
…yped A segmented date field reports every keystroke, so a year on its way to 2026 arrives as 2, then 20, then 202. Three layers each dropped the leading zeros, and between them a half-typed year became a string nothing here can read. `fromDateValue` wrote `2-01-05`. `toDateValue` built the same shape before handing it to `parseDate`, which refuses it, so a field controlled by what it had just reported was handed nothing back and cleared itself. And `toDate` read a year under a hundred as nineteen-hundred-and-it, the rule `new Date(y, m, d)` has carried since 1995, so `0002-01-05` came back as 1902 and a field being typed into jumped there. What it cost: a date typed into the filter panel ended with no filter at all, and a cell editor left behind on blur wrote `2-07-04` onto the row, into the snapshot and out to the server. All three now pad to four digits, and `toDate` sets the year explicitly rather than letting the constructor guess at it. Tested at each layer: the helpers round-trip every year from 2 to 2026, the parser keeps 0002 and 0099 out of the 1900s, and a cell editor left mid-year commits `0002-07-04` rather than a string no parser reads. Wrong, as any half-typed value is, but a date.
The column filters were a panel behind an icon. Everything else in the free
tier is on the surface; the one thing a person reaches for first was two
clicks away, and every grid it gets compared against puts it in a row.
`filtering({ floatingRow: true })` draws that row, and `<DataGrid
floatingFilters />` is the same for a grid it builds itself. Each field
filters in the operator the column already uses, so an operator chosen in the
panel survives the next thing typed in the row, as does a Match case it turned
on, and a percent column is written in the unit it draws.
What one field cannot hold stays with the panel rather than being flattened to
fit: a set of discrete values, two conditions joined, a `between` range, and
`blank` or `notBlank`, which an empty field would report as no filter at all.
The row shows what those contain in the words the chips use, next to a button
that opens the panel on them. This is also where the tier line runs, and it
runs in the same place as before: one condition per column here, everything
nested in the panel.
It is a row of the grid rather than a strip above it, which is the part that
reached into the kernel. `FILTER_ROW` is a second navigable line: the linear
walk the focus model does across header, pinned top, body and pinned bottom
now starts with one or two header lines instead of always one. Arrow down from
the header lands in the field, arrow down again is the first body row, and the
cell passes focus on to the field rather than swallowing it, because a row you
can reach and not type into is not a filter row. Left and right stay with the
caret. `aria-rowcount` counts the row and the rows below it are numbered under
it, both through one `headerRowsOf` rather than three copies of the same sum.
The row follows the same column window, pinned offsets and group dividers as
the header above it, so it stays with its columns when the grid scrolls
sideways. `Grid.FilterRow` is the part for a hand-assembled grid, and the flag
is live: turning it off mid-session takes the row out of the keyboard grid and
out of the numbering with it.
Twenty-five tests: what each filter shape offers a cell, the focus model with
and without the line, and the row itself in a browser down to axe. The data
ops demo opens with it and can turn it off.
The filter row drew its own `GridFilterPanel` for a column it hands back to the panel, and the header already has one. A panel reads whether it is open from `filterFor`, which names a column and not an instance, so both answered and a set column opened two panels stacked over each other. The row asks instead of drawing: its button sets `filterFor` and the column's one panel opens on its header. The header controls now also stay visible while that panel is open, since the panel is portalled out of the header and takes focus with it, so `focus-within` cannot hold the trigger it is anchored to on screen. That was already true of a panel opened from the column menu, which hung under an invisible button until the pointer came back.
`anyValue` is the choice that filters nothing on a column whose filter is a choice. It had been borrowing `clear`, which reads as an action rather than as a state, and left the control saying "Clear" while filtering nothing at all. `filterRowValue` names the field in the filter row. It had been borrowing `filterColumn`, which is the panel trigger's name, so one column carried two controls answering to "Filter Active" and a screen reader had no way to tell the choice from the button that opens the panel. Twelve packs, as ever, and `locales.test.ts` holds them to it.
…browser `Input type="date"` drew whatever the browser draws, in a grid whose own date cell editor has always mounted the sv5ui `DatePicker`. The condition editor mounts it too now, for the value and for the upper bound of a range, and takes its accessible name through an `id` a label points at, which is the way the picker asks to be named. The calendar opens into a portal, and the panel's click-outside handler already knows a portal is still itself. Nothing stood on that before; a test does now, and types a date segment by segment as well, which is the path that was ending with no filter at all until the layer below it was fixed.
The row shipped with a text box for everything, which was wrong in five ways and hid a sixth. A boolean column drew an empty control. Its "no filter" choice carried the value `''`, which a select reads as nothing selected, so the trigger rendered blank with no way back to it. It has a value of its own now, and the row's fields are named apart from the panel's trigger, which they had been colliding with on every column. A number types into `InputNumber` with the steppers out of the way, a date into `DatePicker`, and a set of values into a searchable multi-select rather than a button that sent the reader to the panel for what is still one condition. The list reads the column's values only when it is first opened: a pass over every row is not something to spend on drawing a row nobody has touched, and a test counts the reads to keep it that way. Its placeholder is also darkened, sv5ui's own being too faint to pass a contrast check. All of them wait the same 200ms before the model hears them. The date field had been writing on every keystroke, so typing 01/05/2026 filtered on the years 2 and 20 first, and reading the model back while typing fought the segments until the filter cleared itself. Two more the review turned up: switching the row off while the caret stood in it left the grid with no tab stop at all, and arriving on a date cell by keyboard landed on the calendar button rather than on the first segment. `GridFilterCell` moves to `cells/`, beside the other things drawn inside a cell, and the demo, the README, the feature notes and the slot list say what the row now does.
A group's children say which side of the fold they stand on with `columnGroupShow`, and the group says how it folds with `collapseMode`. Folding to a summary swaps the detail columns for the one that stands for them, and the group's header stays where it was. Folding to a rail takes the group away entirely, header and cells alike, and leaves a drawer the height of the grid with the name down its length, which is what a group with no summary column needs and what gives a wide grid its width back. Every level carries its own toggle, so a nested group answers for itself and keeps the state it was left in while the group above it folds and unfolds. The fold is reachable from the column menu and from the keyboard, it is announced, it rides in the snapshot, and it is on the api as `toggleGroup` and `setGroupCollapsed` with a `columnGroupToggled` event behind them. The drawer is one band from the top of the header to the last row. The header draws the head of it, over its own cells and over the rules between them; the strip draws over the row lines below; it follows a pin the way the cells do, travelling no further than they travel; and it draws both of its own edges except where the grid's own border is already standing on one. Closes #40
# Conflicts: # CHANGELOG.md # README.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md # README.md # src/lib/components/grid/GridBody.svelte # src/lib/components/internal/window.ts # src/lib/core/interaction/focus-model.svelte.ts # src/lib/core/interaction/focus-model.test.ts
`openFilter` lost its ellipsis character to the keyboard-characters pass while the filter row was being built on another branch, so the two met here with the test still asking for the old label.
Three release merges that were never brought back. They carry no content dev does not already have; the release preflight needs the history joined before it will cut the next one.
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.
Cut for 1.3.0. The release is taken from
dev, so this pull request carries the work and theversion bump together; the tag is pushed from
mainonce this merges, and that tag is whatpublishes.
What ships
Added
columnGroupShowswaps the detail columns for the summary one, andcollapseMode: 'rail'takes the whole group away behind a drawer the height of the grid.Every level has its own toggle, and the fold is on the keyboard, in the column menu, in the
events and in the snapshot.
filtering({ floatingRow: true }): one control per columntype, sharing the column's single filter panel rather than opening a second one.
GridFeature.cellValue, which stands between a cell's value and every way it leaves thegrid: render, export, clipboard, quick filter, set filter and the editor's draft.
CellDecoration.style, so a feature can write CSS declarations computed per cell, customproperties included, instead of picking from a fixed set of classes.
DataGridLabels.anyValueandDataGridLabels.filterRowValue, in all twelve packs.Changed
the browser's control.
Fixed
is that year rather than nineteen-hundred-and-it.
grid with no tab stop at all.
Every entry has its paragraph and its reasoning in
CHANGELOG.md.Also in here, from the tail of 1.2.0
This branch has been open since the docs change below, which ships with the rest of it.