@rozie-ui/data-table-solid 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -52
- package/dist/index.cjs +3198 -837
- package/dist/index.d.cts +71 -8
- package/dist/index.d.mts +71 -8
- package/dist/index.mjs +3122 -761
- package/dist/source/index.jsx +6368 -0
- package/package.json +8 -3
- package/src/Column.tsx +3 -3
- package/src/DataTable.tsx +1401 -133
- package/src/EditorCheckbox.tsx +38 -4
- package/src/EditorDate.tsx +79 -8
- package/src/EditorNumber.tsx +52 -6
- package/src/EditorSelect.tsx +51 -6
- package/src/EditorText.tsx +34 -5
- package/src/FilterNumberRange.tsx +79 -12
- package/src/FilterSelect.tsx +18 -3
- package/src/FilterText.tsx +48 -9
- package/src/GroupBar.tsx +75 -3
- package/src/helpers/columnDefUtils.ts +73 -1
- package/src/helpers/dateValue.ts +55 -0
- package/src/themes/base.css +122 -88
- package/src/themes/bootstrap.css +60 -24
- package/src/themes/material.css +54 -22
- package/src/themes/shadcn.css +59 -23
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @rozie-ui/data-table-solid
|
|
2
2
|
|
|
3
|
-
Idiomatic **solid** `DataTable` — a headless, fully-accessible (WAI-ARIA) data table (sorting, global + per-column filtering, pagination, row selection, column visibility / resize /
|
|
3
|
+
Idiomatic **solid** `DataTable` — a headless, fully-accessible (WAI-ARIA) data table (sorting, global + per-column filtering, pagination, row selection, column visibility / resize / pinning (plus a programmatic `columnOrder` slice, no drag affordance), sticky header) compiled from one [Rozie](https://github.com/One-Learning-Community/rozie.js) source. The state engine is `@tanstack/table-core` — the SAME framework-agnostic core behind TanStack Table, wired to this framework's reactivity with NO per-framework adapter. The table's primary surfaces are themed through 18 `--rozie-data-table-*` custom properties, so it re-skins to any design system. This package is generated; do not edit `src/` by hand.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ Also installed: `@rozie/runtime-solid` — Rozie's small, tree-shaken runtime he
|
|
|
14
14
|
|
|
15
15
|
## Usage
|
|
16
16
|
|
|
17
|
-
Columns may be declared as a
|
|
17
|
+
Columns may be declared as a `columns` config array **or** as `<Column>` children (or both — an id-keyed last-write-wins union). Per-cell rendering is one parent `#cell` / `#colHeader` renderer on `<DataTable>`, dispatched by `columnId`, so it works the same with either column form.
|
|
18
18
|
|
|
19
19
|
### Columns as a config array
|
|
20
20
|
|
|
@@ -95,9 +95,13 @@ export function Demo() {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// TOKEN form — the same height via the CSS custom property (the prop wins when
|
|
98
|
-
// both are set; the token is the fallback).
|
|
99
|
-
//
|
|
100
|
-
//
|
|
98
|
+
// both are set; the token is the fallback). DataTable takes no style/class prop, so
|
|
99
|
+
// set the token in your stylesheet on the table's own wrap element, scoped by a
|
|
100
|
+
// parent class (this also outranks the base.css default). estimateRowHeight tunes the seed:
|
|
101
|
+
// .orders .rozie-data-table-wrap { --rozie-data-table-max-height: 400px; }
|
|
102
|
+
// <div class="orders">
|
|
103
|
+
// <DataTable data={rows} virtual estimateRowHeight={48} />
|
|
104
|
+
// </div>
|
|
101
105
|
```
|
|
102
106
|
|
|
103
107
|
### Editable cells (inline edit + validation)
|
|
@@ -244,12 +248,15 @@ export function Demo() {
|
|
|
244
248
|
<DataTable
|
|
245
249
|
data={rows}
|
|
246
250
|
columnFilters={columnFilters()}
|
|
247
|
-
onFilterChange={(
|
|
251
|
+
onFilterChange={(...args: unknown[]) => {
|
|
252
|
+
const p = args[0] as { columnFilters?: { id: string; value: unknown }[] };
|
|
253
|
+
if (p.columnFilters) setColumnFilters(p.columnFilters);
|
|
254
|
+
}}
|
|
248
255
|
// The #filter scoped slot is a render prop on Solid (the documented edge).
|
|
249
256
|
filterSlot={({ columnId, uniqueValues, minMax }) =>
|
|
250
257
|
columnId === 'category' ? (
|
|
251
258
|
<fieldset>
|
|
252
|
-
{uniqueValues.map((v) => <label><input type="checkbox" /> {String(v)}</label>)}
|
|
259
|
+
{uniqueValues.map((v: unknown) => <label><input type="checkbox" /> {String(v)}</label>)}
|
|
253
260
|
</fieldset>
|
|
254
261
|
) : (
|
|
255
262
|
<input type="range" min={minMax?.[0]} max={minMax?.[1]} />
|
|
@@ -276,8 +283,8 @@ import {
|
|
|
276
283
|
export function Demo() {
|
|
277
284
|
// OPT-IN drop-in editors fill the #editor slot — DataTable stays the headless
|
|
278
285
|
// default; the editors are additive named exports. Spread the slot scope through to
|
|
279
|
-
// each drop-in ({ columnId, column, row, value, commit, cancel }); EditorSelect
|
|
280
|
-
// takes `options`. Use them as-is, or fork one as a template.
|
|
286
|
+
// each drop-in ({ columnId, column, row, value, commit, cancel, autofocus }); EditorSelect
|
|
287
|
+
// also takes `options`. Use them as-is, or fork one as a template.
|
|
281
288
|
const [rows, setRows] = createSignal([
|
|
282
289
|
{ id: 1, name: 'Alpha', qty: 3, status: 'active', active: true, score: 41 },
|
|
283
290
|
{ id: 2, name: 'Beta', qty: 7, status: 'archived', active: false, score: 92 },
|
|
@@ -327,7 +334,7 @@ export function Demo() {
|
|
|
327
334
|
// OPT-IN drop-in filters fill the #filter slot — DataTable stays the headless
|
|
328
335
|
// default; the filters are additive named exports. Mark each column filterable (the
|
|
329
336
|
// #filter slot only renders for filterable columns) and spread the slot scope through
|
|
330
|
-
// to each drop-in ({ columnId,
|
|
337
|
+
// to each drop-in ({ columnId, value, uniqueValues, minMax, setFilter }).
|
|
331
338
|
const rows = [
|
|
332
339
|
{ id: 1, name: 'Alpha', category: 'Hardware', price: 30 },
|
|
333
340
|
{ id: 2, name: 'Beta', category: 'Software', price: 90 },
|
|
@@ -339,7 +346,10 @@ export function Demo() {
|
|
|
339
346
|
<DataTable
|
|
340
347
|
data={rows}
|
|
341
348
|
columnFilters={columnFilters()}
|
|
342
|
-
onFilterChange={(
|
|
349
|
+
onFilterChange={(...args: unknown[]) => {
|
|
350
|
+
const p = args[0] as { columnFilters?: { id: string; value: unknown }[] };
|
|
351
|
+
if (p.columnFilters) setColumnFilters(p.columnFilters);
|
|
352
|
+
}}
|
|
343
353
|
// The #filter scoped slot is a render prop on Solid (the documented edge).
|
|
344
354
|
filterSlot={(scope) => (
|
|
345
355
|
<Switch>
|
|
@@ -427,7 +437,7 @@ export function Demo() {
|
|
|
427
437
|
'filter-category': ({ columnId, uniqueValues, setFilter }) => (
|
|
428
438
|
<select onChange={(e) => setFilter(columnId, e.currentTarget.value || null)}>
|
|
429
439
|
<option value="">All</option>
|
|
430
|
-
{uniqueValues.map((v) => <option value={String(v)}>{String(v)}</option>)}
|
|
440
|
+
{uniqueValues.map((v: unknown) => <option value={String(v)}>{String(v)}</option>)}
|
|
431
441
|
</select>
|
|
432
442
|
),
|
|
433
443
|
}}
|
|
@@ -442,7 +452,7 @@ export function Demo() {
|
|
|
442
452
|
|
|
443
453
|
## Theming
|
|
444
454
|
|
|
445
|
-
|
|
455
|
+
The table's primary surfaces are themed through 18 `--rozie-data-table-*` CSS custom properties — override any of them at any ancestor scope. Some internal details (the grid active-cell ring, range highlight, fill handle, the per-header column ⋯ menu, and most of the pagination bar beyond its control border/radius/disabled-opacity) are not yet part of the public token surface. Ready-made design-system bridges ship in the package (import `base.css` first, then a bridge):
|
|
446
456
|
|
|
447
457
|
```tsx
|
|
448
458
|
import '@rozie-ui/data-table-solid/themes/base.css';
|
|
@@ -453,8 +463,8 @@ import '@rozie-ui/data-table-solid/themes/shadcn.css'; // or material.css, bo
|
|
|
453
463
|
|
|
454
464
|
| Name | Type | Default | Two-way (model) | Required | Description |
|
|
455
465
|
| --- | --- | --- | :---: | :---: | --- |
|
|
456
|
-
| `data` | `Array` | `—` | ✓ | ✓ | The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through `
|
|
457
|
-
| `columns` | `Array` | `[]` | | | Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union). |
|
|
466
|
+
| `data` | `Array` | `—` | ✓ | ✓ | The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through the two-way `data` binding (uncontrolled fallback `dataDefault`). A stable reference per Rozie's setup-once model — fed directly into table-core (never map/cloned in the watcher). |
|
|
467
|
+
| `columns` | `Array` | `[]` | | | Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union). A `pinned` value of `'left'` or `'right'` is applied once as the table's initial `columnPinning` state — identical to `<Column pinned>` — which also REORDERS that column, since table-core orders visible cells `[left-pinned, center, right-pinned]`. A consumer who has already expressed a pin (an initial two-way `columnPinning` model value or an interactive pin) owns the slice and this declaration is ignored. |
|
|
458
468
|
| `selectionMode` | `String` | `"none"` | | | Row-selection mode: `'none'` \| `'single'` \| `'multiple'`. `'multiple'` auto-injects a leading checkbox column with a select-all header. |
|
|
459
469
|
| `sorting` | `Array` | `[]` | ✓ | | `SortingState` — `[{ id, desc }]`. Uncontrolled fallback when unbound. Two-way: writes funnel a fresh value through the `sort-change` event regardless of binding. |
|
|
460
470
|
| `globalFilter` | `String` | `''` | ✓ | | The global search string — narrows all columns. Feeds `getFilteredRowModel()`. Surfaces through `filter-change`. Two-way: fires `filter-change` regardless of binding. |
|
|
@@ -463,18 +473,19 @@ import '@rozie-ui/data-table-solid/themes/shadcn.css'; // or material.css, bo
|
|
|
463
473
|
| `manual` | `Boolean` | `false` | | | Server-side hook: sets `manualPagination` / `manualFiltering` / `manualSorting` so table-core trusts the consumer-supplied rows and only emits the change events (the consumer fetches each page). |
|
|
464
474
|
| `rowCount` | `Number` | `null` | | | Total server-side row count for `manual` pagination; lets the table compute page count when it doesn't hold the full dataset. |
|
|
465
475
|
| `pageCount` | `Number` | `null` | | | Explicit total page count for `manual` pagination; overrides rowCount-derived count. |
|
|
466
|
-
| `expandable` | `Boolean` | `false` | | | Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand).
|
|
476
|
+
| `expandable` | `Boolean` | `false` | | | Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand). Pass `expandable` as a real boolean — a valueless attribute only coerces to `true` on Vue and Lit. |
|
|
467
477
|
| `expanded` | `any` | `null` | ✓ | | `ExpandedState` — `{ [rowId]: true }`, or the `true` literal after `expandAll` (declared `type: [Object, Boolean]`). Multi-expand (multiple rows open at once). Surfaces through `expand-change`; uncontrolled fallback (`$data.expandedDefault`) when unbound — the default is `null` so the uncontrolled fallback AND the grouping auto-expand default are reachable (a non-null default would short-circuit them). When grouping is active and `expanded` is untouched, group subtrees auto-expand. |
|
|
478
|
+
| `getRowId` | `Function` | `null` | | | Row identity `(originalRow, index, parentRow?) => string \| number` — the key `rowSelection`, `expanded` and the per-row caches use. Default null → table-core keys rows by position (`"0"`, `"1"`, … and `"0.1"` for sub-rows), so inserting or removing rows above a selected or expanded row moves that state onto whatever row now sits at its index. Supply it whenever rows can be added or removed while state is held (server push, live lists); the result is coerced to a string. |
|
|
468
479
|
| `getSubRows` | `Function` | `null` | | | Table-level child-row accessor `(originalRow, index) => TData[] \| undefined` that drives nested sub-rows. When supplied (with `expandable`), table-core flattens the hierarchy and the expand seam reveals depth-indented child rows. Null → the `#detail` scoped slot is the expand mode. |
|
|
469
480
|
| `groupable` | `Boolean` | `false` | | | Opt-in gate for the **headless `#groupBar`** host region. Default `false` is byte-identical-off. `getGroupedRowModel` is wired unconditionally (inert when `grouping` is empty), so grouping is driven by the `grouping` model; this flag only gates the consumer-facing group-bar surface (the component ships **no** built-in drag UI). |
|
|
470
|
-
| `grouping` | `Array` | `null` | ✓ | | `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding `
|
|
481
|
+
| `grouping` | `Array` | `null` | ✓ | | `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding the two-way `grouping` model (a non-null `[]` default would short-circuit it). All reads are null-guarded, so table-core still receives an array. |
|
|
471
482
|
| `rowSelection` | `Object` | `{}` | ✓ | | `RowSelectionState` — `{ [rowId]: true }`. Checkbox-only toggle (the row body does not select). Driven by the `selectionMode` chrome. Two-way: fires `selection-change` regardless of binding. |
|
|
472
483
|
| `columnVisibility` | `Object` | `{}` | ✓ | | `VisibilityState` — `{ [colId]: boolean }`. Hidden columns drop automatically from header + body. Two-way: funnels a fresh object through `visibility-change`. |
|
|
473
484
|
| `columnSizing` | `Object` | `{}` | ✓ | | `ColumnSizingState` — `{ [colId]: number }`. Driven live by the pointer-drag resize handle (`columnResizeMode: 'onChange'`). Two-way: fires `resize-change`. |
|
|
474
485
|
| `columnOrder` | `Array` | `[]` | ✓ | | `ColumnOrderState` — `string[]`. A fresh order array on reorder (never an in-place splice). Two-way: fires `reorder-change`. |
|
|
475
486
|
| `columnPinning` | `Object` | `{…}` | ✓ | | `ColumnPinningState` — `{ left: string[], right: string[] }`. Pinned columns get `position: sticky` + computed offsets. Defaults to `{ left: [], right: [] }`. Two-way: fires `pin-change`. |
|
|
476
487
|
| `stickyHeader` | `Boolean` | `false` | | | Pure-CSS sticky header: the `<thead>` sticks to the top of the scroll container. |
|
|
477
|
-
| `interactionMode` | `String` | `"table"` | | | `'table'` (default, row-oriented, byte-behaviorally identical to a plain accessible table) \| `'grid'`
|
|
488
|
+
| `interactionMode` | `String` | `"table"` | | | `'table'` (default, row-oriented, byte-behaviorally identical to a plain accessible table) \| `'grid'` — lights up the full WAI-ARIA **[grid interaction mode](/components/data-table-grid-mode)**: `role="grid"`, a roving single tab-stop, 2-D APG arrow-key cell navigation, range selection, and clipboard support. |
|
|
478
489
|
| `singleClickEdit` | `Boolean` | `false` | | | Grid mode only. When `true`, a plain click on an **editable** cell opens its editor immediately (single-click-to-edit) instead of just activating the cell. Default `false` keeps click-to-activate (double-click opens the editor). Shift+click (range selection) and clicks on non-editable cells are unaffected. |
|
|
479
490
|
| `undoable` | `Boolean` | `false` | | | Grid mode. When `true`, every committed data mutation (cell/row edit, paste, fill, cut, clear) becomes one undo step: Ctrl/Cmd+Z undoes, Ctrl/Cmd+Y or Ctrl/Cmd+Shift+Z redoes. Default `false` records no history and Ctrl+Z/Y are inert. |
|
|
480
491
|
| `undoLimit` | `Number` | `100` | | | The maximum number of undo steps retained (oldest evicted past this depth). Only consulted when `undoable` is `true`. |
|
|
@@ -488,20 +499,22 @@ import '@rozie-ui/data-table-solid/themes/shadcn.css'; // or material.css, bo
|
|
|
488
499
|
| Event | Description |
|
|
489
500
|
| --- | --- |
|
|
490
501
|
| `sort-change` | Fired when the sort state changes (header click / shift-click multi-sort / a `sortColumn`/`clearSorting` call). Payload is the fresh `SortingState` array `[{ id, desc }]`. |
|
|
491
|
-
| `expand-change` | Fired
|
|
492
|
-
| `group-change` | Fired
|
|
502
|
+
| `expand-change` | Fired when the expanded-row set changes (an expander chevron toggle — click / Enter / Space — or a `toggleRowExpanded`/`expandAll`/`collapseAll` call). Fires exactly once per change (the echo-guarded write funnel dedups the React multi-render re-entry) and REGARDLESS of whether the two-way `expanded` model is bound. Payload is the fresh `ExpandedState` — a `{ [rowId]: true }` object, or the `true` literal after `expandAll` (passed through verbatim — never `Object.keys` it without a `=== true` guard). Named `expand-change` (not `expanded-change`): the model:true `expanded` prop owns the `onExpandedChange` callback on the React/Solid Props interface, so the event stems off a distinct name to avoid a duplicate-identifier collision (the house convention every slice follows). |
|
|
503
|
+
| `group-change` | Fired when the grouping state changes (a consumer-built `#groupBar` apply/clear, mutating the `grouping` model, or an `applyGrouping`/`clearGrouping` call). Fires exactly once per change (the echo-guarded write funnel dedups the React multi-render re-entry) and REGARDLESS of whether the two-way `grouping` model is bound. Payload is the fresh `GroupingState` — an ORDERED `string[]` of column ids (e.g. `["region","category"]` for nested groups), or `[]` when cleared. Named `group-change` (not `grouping-change`): the model:true `grouping` prop owns the `onGroupingChange` callback on the React/Solid Props interface, so the event stems off a distinct name to avoid a duplicate-identifier collision (the house convention every slice follows — sorting→sort-change, expanded→expand-change). |
|
|
493
504
|
| `filter-change` | Fired when a filter changes. Payload is `{ globalFilter }` for the global search box or `{ columnFilters }` (the fresh `ColumnFiltersState` `[{ id, value }]`) for a per-column filter — both surface through this one event. |
|
|
494
505
|
| `page-change` | Fired when pagination changes (prev/next, a page-size change, or a `setPage`/`setRowsPerPage` call). Payload is the fresh `{ pageIndex, pageSize }` object. |
|
|
495
506
|
| `selection-change` | Fired when the row selection changes (a row/select-all checkbox toggle or a `toggleAllRows`/`clearSelection` call). Payload is the fresh `RowSelectionState` `{ [rowId]: true }` object. |
|
|
496
507
|
| `visibility-change` | Fired when a column is shown/hidden (the column-toggle menu or a `toggleColumnVisibility` call). Payload is the fresh `VisibilityState` `{ [colId]: boolean }` object. |
|
|
497
508
|
| `resize-change` | Fired live during a column resize drag (`columnResizeMode: "onChange"`). Payload is the fresh `ColumnSizingState` `{ [colId]: number }` object. |
|
|
498
|
-
| `reorder-change` | Fired when the column order changes
|
|
509
|
+
| `reorder-change` | Fired when the column order changes. Column reorder is programmatic only — an `applyColumnOrder` call or a two-way `columnOrder` write; the component ships NO drag or menu affordance for it. Payload is the fresh `ColumnOrderState` `string[]`. |
|
|
499
510
|
| `pin-change` | Fired when a column is pinned/unpinned (the per-header pin buttons or a `pinColumn` call). Payload is the fresh `ColumnPinningState` `{ left: string[], right: string[] }` object. |
|
|
500
|
-
| `history-change` | Fired (
|
|
501
|
-
| `
|
|
502
|
-
| `
|
|
503
|
-
| `
|
|
504
|
-
| `
|
|
511
|
+
| `history-change` | Fired when undo/redo AVAILABILITY changes (one unified grid-wide undo/redo stack, not scoped per-cell or per-row) — EDGE-triggered (only when `canUndo`/`canRedo` actually flip, not on every recorded edit) so a toolbar can enable/disable its buttons without polling. Only meaningful when `undoable` is `true`. Payload is `{ canUndo, canRedo }` booleans. No `history` model prop exists, so there is no model-prop==emit-name duplicate-identifier collision. |
|
|
512
|
+
| `visible-range-change` | Fired when the RENDERED row window changes under row windowing (`virtual`) — the window the table actually renders, overscan included — and once after mount. Payload `{ start, end }` over the full row space (`end` exclusive; with lazy rows that space is `rowCount` long). Deduped: a scroll that does not move the window emits nothing. Pair with `manual` + `rowCount` and sparse `data` to fetch the rows the viewport needs. |
|
|
513
|
+
| `activecell-change` | Fired (grid interaction mode only) whenever the active cell moves — by keyboard navigation or a `focusCell` call. One-way notification; payload is `{ rowIndex, colIndex }` integers over the visible model. Fires on every move including arrow keypresses. |
|
|
514
|
+
| `row-activate` | Fired when the user opens a row (list-style "open this item" — oinbox's email thread). A plain click on a body data row, outside any control in the clicked cell (the selection checkbox, the expander, links, inputs, drop-in widgets and an open editor do NOT activate), in both interaction modes; not a shift/ctrl/meta click (selection gestures), the end of a drag-select, or a `singleClickEdit` click on an editable grid cell (that opens the editor). In grid mode also Enter on a non-editable cell that has no controls of its own (Enter on an editable cell still begins the edit; Enter on a cell with controls still enters them). Group-header and detail rows never activate. Payload is `{ row, index, trigger }` — `row` the original data object, `index` its position in the rendered model (page-relative; the full model when virtual), `trigger` `'click'` or `'keyboard'`. |
|
|
515
|
+
| `range-change` | Fired whenever the rectangular cell-range selection changes — extended by `Shift+Arrow` / `Shift+Click` from the single `extendRange`/`setRangeFocus` call sites (React multi-emit dedup). One-way notification (the range is NOT a `model:true` slice — the model:true count stays at 12, leaving the Angular multi-model-CVA condition untouched); it is a SEPARATE layer from the row-selection slice and the two never corrupt each other. Payload is `getSelectedRange()` → `{ anchor, focus }` where each corner is a `{ rowIndex, colIndex }` index pair over the visible model (integers only — no row data, no DOM node), or `{ anchor: null, focus: null }` when no range is set. |
|
|
516
|
+
| `cell-edit-commit` | Fired when an editable cell commits a NEW, validated value — once per committed cell, from the single `commitEdit` call site (a rejected/invalid commit fires nothing). Pairs with the two-way `data` write (one fresh-array replace per commit). Payload is `{ rowId, columnId, oldValue, newValue }`. `cancel`/`Escape` and a validation failure emit nothing. |
|
|
517
|
+
| `row-edit-commit` | Fired when a FULL-ROW edit commits — once per row save, from the single `commitRow` call site (React multi-emit dedup). A row enters full-row edit via `Shift+F2` or the `editRow` verb (every editable cell edits at once); one save validates every edited cell (a single failure blocks the whole row and emits nothing) then writes the two-way `data` model ONCE (a single fresh-array row-object replace with all changes applied) and fires this event ONCE. `Escape` reverts the whole row as a unit and emits nothing. Payload is `{ rowId, changes }` where `changes` is `[{ columnId, oldValue, newValue }]` for ONLY the columns whose value actually changed. |
|
|
505
518
|
|
|
506
519
|
## Imperative handle
|
|
507
520
|
|
|
@@ -511,15 +524,15 @@ Beyond props, the component exposes imperative methods (declared once in the Roz
|
|
|
511
524
|
| --- | --- |
|
|
512
525
|
| `sortColumn` | Toggle (or set) the sort for a column — `sortColumn(colId, desc?)`. Drives table-core so `sort-change` fires with the fresh `SortingState`. |
|
|
513
526
|
| `clearSorting` | Clear all sorting — `clearSorting()`. Resets to the unsorted core row model and fires `sort-change`. |
|
|
514
|
-
| `toggleRowExpanded` | Toggle ONE row's expanded state
|
|
515
|
-
| `expandAll` | Open every expandable row
|
|
516
|
-
| `collapseAll` | Collapse every row
|
|
517
|
-
| `getExpandedRows` | Return the original row data for the currently-expanded rows
|
|
527
|
+
| `toggleRowExpanded` | Toggle ONE row's expanded state — `toggleRowExpanded(rowId)` where `rowId` is the consumer's row `id` (the data field) OR the table-core row id (both resolve). Scans the core flat-row set so a collapsed parent is still resolvable. Drives table-core so `expand-change` fires with the fresh `ExpandedState`. Multi-expand: it does not collapse other open rows. |
|
|
528
|
+
| `expandAll` | Open every expandable row — `expandAll()`. Drives table-core (`toggleAllRowsExpanded(true)`) so `expand-change` fires; the payload may be the `true` expand-all literal. |
|
|
529
|
+
| `collapseAll` | Collapse every row — `collapseAll()`. Resets the expanded set to a blank state (`resetExpanded(true)` → `{}`) and fires `expand-change` with `{}`. |
|
|
530
|
+
| `getExpandedRows` | Return the original row data for the currently-expanded rows — `getExpandedRows()` → `unknown[]` (empty when nothing is expanded). The read-verb twin of the `expand-change` event. |
|
|
518
531
|
| `applyGrouping` | Set the full grouping — `applyGrouping(cols)` where `cols` is a fresh ORDERED `string[]` of column ids (multi-column → nested groups, e.g. `["region","category"]`). Drives table-core (`table.setGrouping`) so `group-change` fires with the fresh `GroupingState`. (Named `applyGrouping`, not `setGrouping`: a `set<ModelProp>` verb collides with React`s auto-generated `grouping` useState setter and an $expose verb is rename-protected — ROZ524; same `applyColumnOrder` precedent.) |
|
|
519
532
|
| `clearGrouping` | Clear all grouping — `clearGrouping()`. Resets to the ungrouped (flat) row model (`table.setGrouping([])`) and fires `group-change` with `[]`. |
|
|
520
|
-
| `getFacetedUniqueValues` | Return a column's CROSS-FILTERED distinct values
|
|
521
|
-
| `getFacetedMinMaxValues` | Return a numeric column's CROSS-FILTERED `[min, max]` range
|
|
522
|
-
| `getColumnDefs` | Return the resolved `ColumnDef[]` (the id-keyed LWW union of the
|
|
533
|
+
| `getFacetedUniqueValues` | Return a column's CROSS-FILTERED distinct values — `getFacetedUniqueValues(colId)` → `unknown[]` of distinct values (KEYS ONLY — occurrence counts are deliberately NOT exposed). Resolves the column via `table.getColumn(colId)` and reads table-core's faceted unique-value map, returning `Array.from(map.keys())`. Cross-filtered: the values reflect rows passing all OTHER active column filters and update when an upstream filter changes. Empty array when the column/table is missing. Inert (the faceted models stay off-path) until this verb or the `#filter` slot reads a facet. |
|
|
534
|
+
| `getFacetedMinMaxValues` | Return a numeric column's CROSS-FILTERED `[min, max]` range — `getFacetedMinMaxValues(colId)` → `[number, number] | null`. Resolves the column via `table.getColumn(colId)` and reads table-core's faceted min/max. Cross-filtered (reflects rows passing all OTHER active column filters) and updates when an upstream filter changes. `null` when unavailable. The read twin handed to the `#filter` scoped slot so a consumer builds a numeric range slider purely from exposed values. |
|
|
535
|
+
| `getColumnDefs` | Return the resolved `ColumnDef[]` (the id-keyed LWW union of the `columns` config array and the `<Column>` children) — `getColumnDefs()`. |
|
|
523
536
|
| `toggleAllRows` | Select or clear all (filtered) rows — `toggleAllRows(value)`. Drives table-core so `selection-change` fires with the fresh `RowSelectionState`. |
|
|
524
537
|
| `clearSelection` | Clear the row selection — `clearSelection()`. Fires `selection-change` with `{}`. |
|
|
525
538
|
| `getSelectedRows` | Return the original row data for the currently-selected rows — `getSelectedRows()` → `unknown[]` (empty when nothing is selected). |
|
|
@@ -529,20 +542,20 @@ Beyond props, the component exposes imperative methods (declared once in the Roz
|
|
|
529
542
|
| `applyColumnOrder` | Set the full column order — `applyColumnOrder(order)` where `order` is a fresh `string[]`. Fires `reorder-change`. (Named `applyColumnOrder`, not `setColumnOrder`: a `set<ModelProp>` verb collides with React`s auto-generated `columnOrder` setter and an $expose verb is rename-protected — ROZ524.) |
|
|
530
543
|
| `resetColumnSizing` | Reset all column widths to their defaults — `resetColumnSizing()`. Fires `resize-change`. |
|
|
531
544
|
| `pinColumn` | Pin a column to a side or unpin it — `pinColumn(colId, side)` where `side` is `'left'` | `'right'` | `false`. Fires `pin-change` with the fresh `ColumnPinningState`. |
|
|
532
|
-
| `focusCell` | Move + focus the active cell (grid interaction mode) — `focusCell(rowIndex, colIndex)`. `rowIndex` is the ABSOLUTE display-order position in `getPrePaginationRowModel().rows` (filter+sort+expand applied, BEFORE pagination/windowing), in BOTH paginated and virtual modes
|
|
533
|
-
| `getActiveCell` | Return the current active-cell position — `getActiveCell()` → `{ rowIndex, colIndex, isHeader }`. For a body cell `rowIndex` is the ABSOLUTE display-order index (
|
|
534
|
-
| `clearActiveCell` | Reset the roving active-cell position to the entry cell and exit interaction mode — `clearActiveCell()`. The next Tab-in re-enters at the entry cell
|
|
535
|
-
| `getRowIndexRelativeToPage` | Convert an ABSOLUTE display-order row index (the `focusCell` / `getActiveCell` / `activecell-change` space
|
|
536
|
-
| `editCell` | Programmatically open the editor on a cell
|
|
537
|
-
| `commitEditing` | Programmatically commit the open editor
|
|
538
|
-
| `editRow` | Programmatically enter FULL-ROW edit on a body row
|
|
539
|
-
| `getSelectedRange` | Return the current rectangular cell-range selection
|
|
540
|
-
| `cut` | Cut the current cell-range selection
|
|
541
|
-
| `undo` | Undo the most recent data mutation (
|
|
542
|
-
| `redo` | Redo the most recently undone data mutation
|
|
543
|
-
| `canUndo` | Return whether an undo step is available
|
|
544
|
-
| `canRedo` | Return whether a redo step is available
|
|
545
|
-
| `clearHistory` | Discard all undo/redo history
|
|
545
|
+
| `focusCell` | Move + focus the active cell (grid interaction mode) — `focusCell(rowIndex, colIndex)`. `rowIndex` is the ABSOLUTE display-order position in `getPrePaginationRowModel().rows` (filter+sort+expand applied, BEFORE pagination/windowing), in BOTH paginated and virtual modes: a paginated `focusCell(abs)` switches to `Math.floor(abs/pageSize)` then focuses; a virtual `focusCell(abs)` scrolls-then-focuses. Args coerced to integers + clamped to bounds. Fires `activecell-change` with the absolute `rowIndex`. (Named `focusCell`, not `focus`: a bare `focus` verb shadows the inherited `HTMLElement.focus` on Lit — ROZ137.) |
|
|
546
|
+
| `getActiveCell` | Return the current active-cell position — `getActiveCell()` → `{ rowIndex, colIndex, isHeader }`. For a body cell `rowIndex` is the ABSOLUTE display-order index (matching `focusCell` / `activecell-change`) and `isHeader` is `false`; for a header-active cell `rowIndex` is `null` and `isHeader` is `true` (integers only — no row data, no DOM node). |
|
|
547
|
+
| `clearActiveCell` | Reset the roving active-cell position to the entry cell and exit interaction mode — `clearActiveCell()`. The next Tab-in re-enters at the entry cell. (Named `clearActiveCell`, not `clear`: distinct from the listbox `clear` selection verb.) |
|
|
548
|
+
| `getRowIndexRelativeToPage` | Convert an ABSOLUTE display-order row index (the `focusCell` / `getActiveCell` / `activecell-change` space) to the PAGE-RELATIVE index — `getRowIndexRelativeToPage(absRow?)` → `number`. With no argument it converts the current active cell (`abs - pageIndex*pageSize`). In virtual mode (no pagination) it returns the absolute index unchanged. Mirrors MUI `getRowIndexRelativeToVisibleRows`. (Named `getRowIndexRelativeToPage`: collision-clean against the verb/event/prop and Lit ROZ137 reserved sets.) |
|
|
549
|
+
| `editCell` | Programmatically open the editor on a cell — `editCell(rowIndex, colIndex)`, addressed by index over the visible model (args coerced to integers + clamped). No-op on a non-editable cell. (Named `editCell`, not `edit`: collision-clean against the verb/event/prop and Lit ROZ137 reserved sets.) |
|
|
550
|
+
| `commitEditing` | Programmatically commit the open editor — `commitEditing()`. Runs the column validator; on success writes the two-way `data` model and fires one `cell-edit-commit`; on a validation failure keeps the editor open. No-op when no cell is editing. (Named `commitEditing`, not `commit`.) |
|
|
551
|
+
| `editRow` | Programmatically enter FULL-ROW edit on a body row — `editRow(rowIndex)`, addressed by index over the visible model (args coerced to integers + clamped). The API twin of the `Shift+F2` shortcut: every editable cell in the row enters edit at once. A later save commits the whole row in one two-way `data` model write + one `row-edit-commit`; `Escape` reverts the row as a unit. No-op on a row with no editable columns. (Named `editRow`, not `edit`/`editColumn`: collision-clean against the verb/event/prop and Lit ROZ137 reserved sets.) |
|
|
552
|
+
| `getSelectedRange` | Return the current rectangular cell-range selection — `getSelectedRange()` → `{ anchor, focus }` where each corner is a `{ rowIndex, colIndex }` index pair over the visible model (integers only — no row data, no DOM node), or `{ anchor: null, focus: null }` when no range is set. The range is extended by `Shift+Arrow` / `Shift+Click` and is ONE-WAY (this read verb + the `range-change` event), NOT a `model:true` slice. (Named `getSelectedRange`, not `getRange`/`getSelection`: collision-clean against `getSelectedRows`, the verb/event/prop, and the Lit ROZ137 reserved sets.) |
|
|
553
|
+
| `cut` | Cut the current cell-range selection — `cut()`. Copies the range to the clipboard as TSV (the same escaped serialization `Ctrl+C` produces) THEN clears the source cells through the two-way `data` model write-funnel in a single `writeData` (each cell coerced to its column empty — `null` on a numeric column, the empty string on text — under the editable + validator skip rule, firing one `cell-edit-commit` per cleared cell). The API twin of the `Ctrl/Cmd+X` shortcut. A no-op while a header cell is active (the reusable clipboard guard). (Named `cut`, the spreadsheet-standard verb: collision-clean against the verb/event/prop, the React auto-setter set, and the Lit ROZ137 reserved DOM members — `cut` is not on `HTMLElement`.) |
|
|
554
|
+
| `undo` | Undo the most recent data mutation (grid-wide undo/redo) — `undo()`. Only meaningful when `undoable` is `true`. Pops the most recent pre-mutation snapshot, pushes the current data onto the redo stack, and replays the snapshot through the SAME `writeData` funnel every edit/paste/fill/cut/clear commits through — so the two-way `data` model writeback keeps working with zero new code. Fires `history-change` once per call. A no-op when nothing is undoable (`canUndo()` is `false`). The API twin of the `Ctrl/Cmd+Z` shortcut. |
|
|
555
|
+
| `redo` | Redo the most recently undone data mutation — `redo()`. Symmetric to `undo()`: pops the redo stack, pushes the current data back onto the undo stack, replays through the same `writeData` seam, fires `history-change` once. A no-op when nothing is redoable (`canRedo()` is `false`). The API twin of the `Ctrl/Cmd+Y` / `Ctrl/Cmd+Shift+Z` shortcuts. |
|
|
556
|
+
| `canUndo` | Return whether an undo step is available — `canUndo()` → `boolean`. Reflects the undo stack depth; use alongside the `history-change` event to drive a toolbar button`s disabled state without polling. |
|
|
557
|
+
| `canRedo` | Return whether a redo step is available — `canRedo()` → `boolean`. Reflects the redo stack depth; cleared by any new edit after an undo (standard undo semantics). |
|
|
558
|
+
| `clearHistory` | Discard all undo/redo history — `clearHistory()`. Empties both stacks (`canUndo()`/`canRedo()` become `false`). Also called internally on an external data-reference swap (a dataset the component itself did not write), since undoing across a dataset boundary is incoherent. |
|
|
546
559
|
|
|
547
560
|
```tsx
|
|
548
561
|
import { DataTable, type DataTableHandle } from '@rozie-ui/data-table-solid';
|
|
@@ -562,7 +575,7 @@ const cats = handle?.getFacetedUniqueValues('category'); // getFacetedMinMaxValu
|
|
|
562
575
|
|
|
563
576
|
All rendering slots live on the parent `<DataTable>` (a `<Column>` carries metadata only). The `cell` / `colHeader` slots are single renderers dispatched by `columnId` — switch on it to vary the render per column; a column the slot does not render shows the plain accessor value. (On React/Solid these are render-prop props — `renderCell` / `renderColHeader` / `cellSlot` / `colHeaderSlot`; on Lit they are the `.cell` / `.colHeader` properties — the documented cross-framework divergence.)
|
|
564
577
|
|
|
565
|
-
The `detail` (expandable rows), `groupBar` (grouping)
|
|
578
|
+
The `detail` (expandable rows), `groupBar` (grouping), `filter` (faceted filtering) and `placeholder` (lazy-loading skeleton) scoped slots follow the SAME render-prop convention: on React they are `renderDetail` / `renderGroupBar` / `renderFilter` / `renderPlaceholder`; on Solid they are `detailSlot` / `groupBarSlot` / `filterSlot` / `placeholderSlot`; on Lit they are the `.detail` / `.groupBar` / `.filter` / `.placeholder` properties — the documented React render-prop edge (per the cross-framework compatibility bar). On Vue / Svelte / Angular they are ordinary named scoped slots (`#detail` / `#groupBar` / `#filter` / `#placeholder`). The `groupBar` and `filter` slots are HEADLESS — the component ships NO built-in group-bar / facet control, so the consumer builds the UI purely from the exposed slot props. `placeholder` DOES ship a default (a skeleton bar) — fill it only to replace that default with your own loading affordance; it renders for a `virtual` + `manual` + `rowCount` row not yet present in `data`.
|
|
566
579
|
|
|
567
580
|
`cell`, `colHeader`, `filter` and `editor` each also have a **per-column slot family** — a statically-named fill per `columnId` (`cell-price`, `colHeader-price`, `filter-category`, `editor-score`, …) that takes precedence OVER the matching generic slot above it: `<seam>-<id>` → the generic `<seam>` slot → the built-in render. `editor-<id>` (and `editor`) only render on a column declared `editor="custom"`; `filter-<id>` (and `filter`) only render on a column declared `filterable`; `cell-<id>` / `colHeader-<id>` have no gate. The auto-injected row-select / row-expander chrome columns never reach any family — they are excluded structurally. On Solid the family is the additive **`slots`** record prop — `slots={{ 'cell-price': (ctx) => … }}` — keyed by the exact `<seam>-<id>` string.
|
|
568
581
|
|
|
@@ -574,12 +587,13 @@ A misspelled family key does not fail to compile — the emitted type carries a
|
|
|
574
587
|
| groupBar | grouping, groupableColumns, applyGrouping, clearGrouping |
|
|
575
588
|
| selectAll | checked, indeterminate, toggle |
|
|
576
589
|
| colHeader-<id> | columnId, column, label |
|
|
577
|
-
| filter-<id> | columnId, value, uniqueValues, minMax, setFilter |
|
|
590
|
+
| filter-<id> | columnId, value, uniqueValues, minMax, columnLabel, setFilter |
|
|
591
|
+
| placeholder | index, columnId |
|
|
578
592
|
| selectCell | row, checked, toggle |
|
|
579
593
|
| cell-<id> | columnId, column, row, value |
|
|
580
|
-
| editor-<id> | columnId, column, row, value, commit, cancel, autofocus |
|
|
594
|
+
| editor-<id> | columnId, column, row, value, commit, cancel, columnLabel, autofocus |
|
|
581
595
|
| detail | row |
|
|
582
596
|
| colHeader | columnId, column, label |
|
|
583
|
-
| filter | columnId, value, uniqueValues, minMax, setFilter |
|
|
597
|
+
| filter | columnId, value, uniqueValues, minMax, columnLabel, setFilter |
|
|
584
598
|
| cell | columnId, column, row, value |
|
|
585
|
-
| editor | columnId, column, row, value, commit, cancel, autofocus |
|
|
599
|
+
| editor | columnId, column, row, value, commit, cancel, columnLabel, autofocus |
|