@rozie-ui/data-table-solid 0.8.1 → 0.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @rozie-ui/data-table-solid
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cb54e7e: Range fill and border fixes.
8
+ - **Range fill:** with a block of cells selected, `Ctrl`/`Cmd`+`Enter` in a cell editor writes its value to every selected cell in the active cell's column. It works with built-in, drop-in and custom slot editors. Editor slots get `rangeSize` and `commit(value, { toRange: true })`. The fill follows the paste rules: invalid cells are skipped, it's one undo step with one `cell-edit-commit` per changed cell, and it's announced as "N of M cells updated". The value is written as is, not converted to text. The range stays selected while the editor is open and after the fill.
9
+ - **Checkbox ranges:** `Space` (or `Enter`/`F2`) on a checkbox range sets every selected cell to the opposite of the active cell's value.
10
+ - **Border gaps:** the 1px lines of scrolled content beside pinned columns and above the sticky header are gone. The table now uses separate borders, and each cell draws its own lines. Cells also reset their borders, so a host's global `table td { border }` rule no longer draws inside the table.
11
+ - **Pinned cells:** pinned body and filter cells are opaque, and the group-row and filter-row bands still show on them. The new `--rozie-data-table-pinned-bg` token sets their background. It defaults to `#fff` in the base theme, and to the design system's surface colour in the bootstrap, material and shadcn bridges. On a dark or tinted page, set it to the colour behind the table.
12
+ - **Column lines:** the new `columnLines` prop draws column lines plus the outer lines at the start of each row and above the header; they follow the writing direction. Their colour follows the row-line colour unless `--rozie-data-table-column-border-color` is set.
13
+
3
14
  ## 0.8.1
4
15
 
5
16
  ### Patch Changes
package/README.md CHANGED
@@ -485,11 +485,12 @@ import '@rozie-ui/data-table-solid/themes/shadcn.css'; // or material.css, bo
485
485
  | `columnOrder` | `Array` | `[]` | ✓ | | `ColumnOrderState` — `string[]`. A fresh order array on reorder (never an in-place splice). Two-way: fires `reorder-change`. |
486
486
  | `columnPinning` | `Object` | `{…}` | ✓ | | `ColumnPinningState` — `{ left: string[], right: string[] }`. Pinned columns get `position: sticky` + computed offsets. Defaults to `{ left: [], right: [] }`. Two-way: fires `pin-change`. |
487
487
  | `stickyHeader` | `Boolean` | `false` | | | Pure-CSS sticky header: the `<thead>` sticks to the top of the scroll container. |
488
+ | `columnLines` | `Boolean` | `false` | | | Draw a line between columns, plus an outer line at the start of each row and above the header row. The colour follows `--rozie-data-table-border-color` unless `--rozie-data-table-column-border-color` is set. |
488
489
  | `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. |
489
490
  | `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. |
490
491
  | `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. |
491
492
  | `undoLimit` | `Number` | `100` | | | The maximum number of undo steps retained (oldest evicted past this depth). Only consulted when `undoable` is `true`. |
492
- | `holdView` | `Boolean` | `false` | | | When `true`, the table holds its current view through its own writes (edit, paste, fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`. |
493
+ | `holdView` | `Boolean` | `false` | | | When `true`, the table holds its current view through its own writes (edit, paste, fill, range fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`. |
493
494
  | `virtual` | `any` | `false` | | | Opt-in windowing grammar: `false` (default, off — byte-identical to a non-virtual table) \| `true` or `'rows'` (vertical row windowing; `true` is byte-behavior-identical to every existing consumer, zero churn) \| `'columns'` (horizontal column windowing) \| `'both'` (both axes windowed). Row windowing renders only the visible slice of rows inside a bounded `rdt-scroll` container (with leading/trailing spacer rows preserving total scroll height), windowing over the full filtered + sorted (pre-pagination) model and suppressing the client pagination chrome. Column windowing renders only the visible slice of leaf columns inside the same `rdt-scroll` container. An unrecognised string behaves as `false`. |
494
495
  | `estimateRowHeight` | `Number` | `40` | | | Estimated row height (px) — the first-paint seed for the windowing engine before any row has been measured. Only consulted when rows are windowed. When `autoMeasure` is `true`, later renders progressively refine the estimate from measured content; when `autoMeasure` is `false` this remains the explicit override for every render. |
495
496
  | `autoMeasure` | `Boolean` | `false` | | | Opt-in content-driven row-size estimation. When `true`, the windowing engine feeds `estimateSize()` a running mean of measured row heights instead of the fixed `estimateRowHeight` seed, so `getTotalSize()` converges to the true content total on a large table with variable-height rows. Falls back to `estimateRowHeight` before any row has been measured. Default `false` keeps the estimate fixed at `estimateRowHeight` for every render (today's behavior). |
@@ -597,9 +598,9 @@ A misspelled family key does not fail to compile — the emitted type carries a
597
598
  | placeholder | index, columnId |
598
599
  | selectCell | row, checked, toggle |
599
600
  | cell-<id> | columnId, column, row, value |
600
- | editor-<id> | columnId, column, row, value, commit, cancel, columnLabel, autofocus |
601
+ | editor-<id> | columnId, column, row, value, commit, cancel, columnLabel, autofocus, rangeSize |
601
602
  | detail | row |
602
603
  | colHeader | columnId, column, label |
603
604
  | filter | columnId, value, uniqueValues, minMax, columnLabel, setFilter |
604
605
  | cell | columnId, column, row, value |
605
- | editor | columnId, column, row, value, commit, cancel, columnLabel, autofocus |
606
+ | editor | columnId, column, row, value, commit, cancel, columnLabel, autofocus, rangeSize |
package/dist/index.cjs CHANGED
@@ -317,6 +317,28 @@ const tileIndex = (i, lo, hi) => {
317
317
  return lo + k;
318
318
  };
319
319
  //#endregion
320
+ //#region src/helpers/rangeFill.ts
321
+ const rangeFillTargets = (range, activeRow, activeCol) => {
322
+ const single = {
323
+ top: activeRow,
324
+ bottom: activeRow,
325
+ col: activeCol,
326
+ size: 1
327
+ };
328
+ if (!range) return single;
329
+ const top = Math.min(range.r0, range.r1);
330
+ const bottom = Math.max(range.r0, range.r1);
331
+ const left = Math.min(range.c0, range.c1);
332
+ const right = Math.max(range.c0, range.c1);
333
+ if (activeCol < left || activeCol > right || activeRow < top || activeRow > bottom) return single;
334
+ return {
335
+ top,
336
+ bottom,
337
+ col: activeCol,
338
+ size: bottom - top + 1
339
+ };
340
+ };
341
+ //#endregion
320
342
  //#region src/helpers/rowValueUtils.ts
321
343
  const replaceRowValue = (rows, rowIndex, field, value) => {
322
344
  const src = rows || [];
@@ -502,6 +524,8 @@ var _tmpl$$11 = /* @__PURE__ */ (0, solid_js_web.template)(`<div class=rozie-dat
502
524
  --rdt-border: 1px solid var(--rozie-data-table-border-color, var(--rdt-ds-border-color));
503
525
  --rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
504
526
  --rdt-filter-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
527
+ --rdt-column-border: 1px solid var(--rozie-data-table-column-border-color, var(--rozie-data-table-border-color, var(--rdt-ds-column-border-color)));
528
+ --rdt-pinned-bg: var(--rozie-data-table-pinned-bg, var(--rdt-ds-pinned-bg));
505
529
  --rdt-font: var(--rozie-data-table-font, var(--rdt-ds-font));
506
530
  --rdt-cell-padding: var(--rozie-data-table-cell-padding, var(--rdt-ds-cell-padding));
507
531
  --rdt-header-bg: var(--rozie-data-table-header-bg, var(--rdt-ds-header-bg));
@@ -523,7 +547,12 @@ var _tmpl$$11 = /* @__PURE__ */ (0, solid_js_web.template)(`<div class=rozie-dat
523
547
  --rdt-page-size-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
524
548
  }
525
549
  .rozie-data-table[data-rozie-s-d5dcab4c] {
526
- border-collapse: collapse;
550
+ /* SEPARATE, not collapse. With collapsed borders the browser keeps a sticky cell's shared
551
+ border in the scrolling grid, so a 1px line of scrolled content shows beside pinned
552
+ columns and above the sticky header. Separate borders travel with their cell; zero
553
+ spacing keeps lines 1px because each cell draws only its own sides (see the cell rule). */
554
+ border-collapse: separate;
555
+ border-spacing: 0;
527
556
  width: 100%;
528
557
  font: var(--rdt-font, 14px system-ui, sans-serif);
529
558
  color: var(--rdt-color, inherit);
@@ -582,8 +611,28 @@ var _tmpl$$11 = /* @__PURE__ */ (0, solid_js_web.template)(`<div class=rozie-dat
582
611
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-td[data-rozie-s-d5dcab4c] {
583
612
  padding: var(--rdt-cell-padding, 0.5rem 0.75rem);
584
613
  text-align: left;
614
+ /* Reset every side, then draw only the row line. Under separate borders each cell owns its
615
+ lines, and the reset keeps a host's plain \`table td { border }\` rule from drawing a second
616
+ set inside the table (this class-scoped rule outranks it). */
617
+ border: 0;
585
618
  border-bottom: var(--rdt-border, 1px solid rgba(0, 0, 0, 0.08));
586
619
  }
620
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-detail-cell[data-rozie-s-d5dcab4c] {
621
+ border: 0;
622
+ }
623
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-th[data-rozie-s-d5dcab4c],
624
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-td[data-rozie-s-d5dcab4c],
625
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-filter-cell[data-rozie-s-d5dcab4c],
626
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-detail-cell[data-rozie-s-d5dcab4c] {
627
+ border-inline-end: var(--rdt-column-border, 1px solid rgba(0, 0, 0, 0.08));
628
+ }
629
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] tr[data-rozie-s-d5dcab4c] > [data-rozie-s-d5dcab4c]:first-child:not(.rdt-col-spacer[data-rozie-s-d5dcab4c]),
630
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] tr[data-rozie-s-d5dcab4c] > .rdt-col-spacer[data-rozie-s-d5dcab4c]:first-child + *[data-rozie-s-d5dcab4c] {
631
+ border-inline-start: var(--rdt-column-border, 1px solid rgba(0, 0, 0, 0.08));
632
+ }
633
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-thead[data-rozie-s-d5dcab4c] > tr[data-rozie-s-d5dcab4c]:first-child > .rdt-th[data-rozie-s-d5dcab4c] {
634
+ border-top: var(--rdt-column-border, 1px solid rgba(0, 0, 0, 0.08));
635
+ }
587
636
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-thead[data-rozie-s-d5dcab4c] .rdt-th[data-rozie-s-d5dcab4c] {
588
637
  font-weight: var(--rdt-header-weight, 600);
589
638
  /* OPAQUE default (was rgba(0,0,0,0.03)): a translucent header lets the scrolling body
@@ -686,8 +735,21 @@ var _tmpl$$11 = /* @__PURE__ */ (0, solid_js_web.template)(`<div class=rozie-dat
686
735
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-pinned[data-rozie-s-d5dcab4c] {
687
736
  box-sizing: border-box;
688
737
  }
738
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-td.rdt-pinned[data-rozie-s-d5dcab4c] {
739
+ background-color: var(--rdt-pinned-bg, #fff);
740
+ }
741
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-group-header[data-rozie-s-d5dcab4c] > .rdt-td.rdt-pinned[data-rozie-s-d5dcab4c] {
742
+ background: linear-gradient(var(--rdt-group-header-bg, rgba(0, 0, 0, 0.025)), var(--rdt-group-header-bg, rgba(0, 0, 0, 0.025))), var(--rdt-pinned-bg, #fff);
743
+ }
744
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-cell.rdt-pinned[data-rozie-s-d5dcab4c] {
745
+ background: linear-gradient(var(--rdt-filter-row-bg, rgba(0, 0, 0, 0.015)), var(--rdt-filter-row-bg, rgba(0, 0, 0, 0.015))), var(--rdt-pinned-bg, #fff);
746
+ }
747
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-td.rdt-pinned.rdt-in-range[data-rozie-s-d5dcab4c] {
748
+ background: linear-gradient(var(--rdt-range-bg, rgba(37, 99, 235, 0.12)), var(--rdt-range-bg, rgba(37, 99, 235, 0.12))), var(--rdt-pinned-bg, #fff);
749
+ }
689
750
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-cell[data-rozie-s-d5dcab4c] {
690
751
  padding: var(--rdt-filter-cell-padding, 0.35rem 0.75rem);
752
+ border: 0;
691
753
  border-bottom: var(--rdt-border, 1px solid rgba(0, 0, 0, 0.08));
692
754
  }
693
755
  .rozie-data-table-wrap[data-rozie-s-d5dcab4c] .rdt-filter-row[data-rozie-s-d5dcab4c] .rdt-col-filter {
@@ -908,6 +970,7 @@ function DataTable(_props) {
908
970
  getSubRows: null,
909
971
  groupable: false,
910
972
  stickyHeader: false,
973
+ columnLines: false,
911
974
  interactionMode: "table",
912
975
  singleClickEdit: false,
913
976
  undoable: false,
@@ -940,6 +1003,7 @@ function DataTable(_props) {
940
1003
  "columnOrder",
941
1004
  "columnPinning",
942
1005
  "stickyHeader",
1006
+ "columnLines",
943
1007
  "interactionMode",
944
1008
  "singleClickEdit",
945
1009
  "undoable",
@@ -1251,6 +1315,7 @@ function DataTable(_props) {
1251
1315
  };
1252
1316
  document.addEventListener("pointerdown", docPointerDown, true);
1253
1317
  }
1318
+ if (gridRoot) gridRoot.addEventListener("keydown", onEditorDropinKeyDownCapture, true);
1254
1319
  if (isWindowed()) gridScrollEl = __rozieRootRef ? __rozieRootRef.querySelector(".rdt-scroll") : null;
1255
1320
  if (rowsWindowed()) {
1256
1321
  virtualizer = new _tanstack_virtual_core.Virtualizer(virtualizerOptions());
@@ -1287,6 +1352,7 @@ function DataTable(_props) {
1287
1352
  document.removeEventListener("pointerdown", docPointerDown, true);
1288
1353
  docPointerDown = null;
1289
1354
  }
1355
+ if (gridRoot) gridRoot.removeEventListener("keydown", onEditorDropinKeyDownCapture, true);
1290
1356
  if (virtualizerCleanup) virtualizerCleanup();
1291
1357
  if (colVirtualizerCleanup) colVirtualizerCleanup();
1292
1358
  teardownColRtlWatch();
@@ -3377,7 +3443,7 @@ function DataTable(_props) {
3377
3443
  setActiveColIndex(col);
3378
3444
  if (isHeader !== prevIsHeader || col !== prevCol || (isHeader ? movedLevel !== prevLevel : movedRow !== prevRow)) focusIntentEpoch = focusIntentEpoch + 1;
3379
3445
  if (rangeTransition) rangeTransition = false;
3380
- else if (rangeClickPending && rangeClickPending.isHeader === isHeader && rangeClickPending.r === movedRow && rangeClickPending.c === col) rangeClickPending = null;
3446
+ else if (!isHeader && tgt !== cellEl && editingRow() === movedRow && editingCol() === col) {} else if (rangeClickPending && rangeClickPending.isHeader === isHeader && rangeClickPending.r === movedRow && rangeClickPending.c === col) rangeClickPending = null;
3381
3447
  else {
3382
3448
  rangeClickPending = null;
3383
3449
  clearRange();
@@ -4125,6 +4191,18 @@ function DataTable(_props) {
4125
4191
  }
4126
4192
  applyGridToRange(grid, box.r0, box.c0, "filled");
4127
4193
  }
4194
+ function fillTargets() {
4195
+ return rangeFillTargets(normalizedRange(), activeRow(), activeColIndex());
4196
+ }
4197
+ function rangeSize() {
4198
+ return activeIsHeader() ? 1 : fillTargets().size;
4199
+ }
4200
+ function fillActiveColumn(value) {
4201
+ const t = fillTargets();
4202
+ const grid = [];
4203
+ for (let i = 0; i < t.size; i++) grid.push([value]);
4204
+ return applyGridToRange(grid, t.top, t.col, "updated");
4205
+ }
4128
4206
  let fillDragging = false;
4129
4207
  let fillDragMove = null;
4130
4208
  let fillDragUp = null;
@@ -4547,6 +4625,44 @@ function DataTable(_props) {
4547
4625
  } else if (skipFocusReturn !== true) focusCellWhenReady(focusRow, focusCol);
4548
4626
  return true;
4549
4627
  }
4628
+ function commitEditToRange(overrideValue = void 0) {
4629
+ if (editingRow() < 0) return false;
4630
+ if (committedThisSession) return false;
4631
+ const colId = editingColumnId();
4632
+ if (colId == null) {
4633
+ endEdit();
4634
+ return false;
4635
+ }
4636
+ const rawValue = overrideValue !== void 0 ? overrideValue : draftValue();
4637
+ const err = runValidator(colId, coerceCellValue(colId, rawValue), editingRowOriginal());
4638
+ if (err !== true) {
4639
+ setInvalid(err);
4640
+ focusEditorWhenReady(editingRow(), editingCol());
4641
+ return false;
4642
+ }
4643
+ setInvalid("");
4644
+ const focusRow = editingRow();
4645
+ const focusCol = editingCol();
4646
+ const rowOriginal = editingRowOriginal();
4647
+ const rowId = editingRowId();
4648
+ editTransition = true;
4649
+ committedThisSession = true;
4650
+ const result = fillActiveColumn(rawValue);
4651
+ endEdit();
4652
+ editTransition = false;
4653
+ rangeClickPending = {
4654
+ isHeader: false,
4655
+ r: focusRow,
4656
+ c: focusCol
4657
+ };
4658
+ if (result && result.changed > 0) pendingEditFollow = {
4659
+ rowOriginal,
4660
+ rowId,
4661
+ col: focusCol
4662
+ };
4663
+ else focusCellWhenReady(focusRow, focusCol);
4664
+ return true;
4665
+ }
4550
4666
  function toggleActiveBooleanCell() {
4551
4667
  if (rowIndexIsGrouped(activeRow())) return;
4552
4668
  const row = (rows() || [])[activeRow()];
@@ -4558,6 +4674,11 @@ function DataTable(_props) {
4558
4674
  const rowId = row.id;
4559
4675
  const oldValue = cellValueAt(activeRow(), activeColIndex());
4560
4676
  const newValue = !oldValue;
4677
+ if (rangeSize() > 1) {
4678
+ setInvalid("");
4679
+ fillActiveColumn(newValue);
4680
+ return;
4681
+ }
4561
4682
  const err = runValidator(colId, newValue, rowOriginal);
4562
4683
  if (err !== true) {
4563
4684
  setInvalid(err);
@@ -4779,6 +4900,7 @@ function DataTable(_props) {
4779
4900
  let committedThisSession = false;
4780
4901
  let rowDraftSync = {};
4781
4902
  let dropinTabFlush = null;
4903
+ let rangeCommitIntent = false;
4782
4904
  function inRowEdit() {
4783
4905
  return editingRowIndex() != null;
4784
4906
  }
@@ -4789,11 +4911,17 @@ function DataTable(_props) {
4789
4911
  return !!(inRowEdit() ? rowDraft() ? rowDraft()[colId] : null : draftValue());
4790
4912
  }
4791
4913
  function editorCommitFor(colId) {
4792
- return (value) => {
4914
+ return (value, options) => {
4793
4915
  if (inRowEdit()) {
4794
4916
  setRowDraft$local(colId, value);
4795
4917
  return;
4796
4918
  }
4919
+ const wantsRange = options != null && options.toRange === true || rangeCommitIntent;
4920
+ rangeCommitIntent = false;
4921
+ if (wantsRange && !dropinTabFlush && rangeSize() > 1) {
4922
+ commitEditToRange(value);
4923
+ return;
4924
+ }
4797
4925
  if (dropinTabFlush) {
4798
4926
  dropinTabFlush.called = true;
4799
4927
  if (commitEdit(value, true)) dropinTabFlush.committed = true;
@@ -4868,7 +4996,10 @@ function DataTable(_props) {
4868
4996
  }
4869
4997
  return;
4870
4998
  }
4871
- if (key === "Enter") {
4999
+ if (key === "Enter" && (e.ctrlKey || e.metaKey) && rangeSize() > 1) {
5000
+ e.preventDefault();
5001
+ commitEditToRange(void 0);
5002
+ } else if (key === "Enter") {
4872
5003
  e.preventDefault();
4873
5004
  commitEdit(void 0);
4874
5005
  } else if (key === "Tab") {
@@ -4889,8 +5020,18 @@ function DataTable(_props) {
4889
5020
  cancelEdit();
4890
5021
  }
4891
5022
  }
5023
+ function onEditorDropinKeyDownCapture(e) {
5024
+ if (!e) return;
5025
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
5026
+ rangeCommitIntent = true;
5027
+ setTimeout(() => {
5028
+ rangeCommitIntent = false;
5029
+ }, 0);
5030
+ }
5031
+ }
4892
5032
  function onEditorDropinKeyDown(e) {
4893
5033
  if (!e) return;
5034
+ rangeCommitIntent = false;
4894
5035
  const key = e.key;
4895
5036
  if (inRowEdit()) {
4896
5037
  if (key === "Enter") {
@@ -5752,6 +5893,9 @@ function DataTable(_props) {
5752
5893
  },
5753
5894
  get autofocus() {
5754
5895
  return editorAutofocusFor(cell().column.id, rowIndexOf(row()));
5896
+ },
5897
+ get rangeSize() {
5898
+ return inRowEdit() ? 1 : rangeSize();
5755
5899
  }
5756
5900
  }) ?? (_props.editorSlot ?? _props.slots?.["editor"])?.({
5757
5901
  get columnId() {
@@ -5777,6 +5921,9 @@ function DataTable(_props) {
5777
5921
  },
5778
5922
  get autofocus() {
5779
5923
  return editorAutofocusFor(cell().column.id, rowIndexOf(row()));
5924
+ },
5925
+ get rangeSize() {
5926
+ return inRowEdit() ? 1 : rangeSize();
5780
5927
  }
5781
5928
  }) ?? (() => {
5782
5929
  var _el$68 = _tmpl$29();
@@ -6059,7 +6206,10 @@ function DataTable(_props) {
6059
6206
  }))]
6060
6207
  }));
6061
6208
  (0, solid_js_web.effect)((_p$) => {
6062
- var _v$0 = (0, _rozie_runtime_solid.rozieAttr)(gridAriaRowCount()), _v$1 = (0, _rozie_runtime_solid.rozieAttr)(gridAriaColCount()), _v$10 = "rozie-data-table " + (0, _rozie_runtime_solid.rozieClass)({ "rdt-sticky": local.stickyHeader }), _v$11 = (0, _rozie_runtime_solid.rozieAttr)(tableRole());
6209
+ var _v$0 = (0, _rozie_runtime_solid.rozieAttr)(gridAriaRowCount()), _v$1 = (0, _rozie_runtime_solid.rozieAttr)(gridAriaColCount()), _v$10 = "rozie-data-table " + (0, _rozie_runtime_solid.rozieClass)({
6210
+ "rdt-sticky": local.stickyHeader,
6211
+ "rdt-column-lines": local.columnLines
6212
+ }), _v$11 = (0, _rozie_runtime_solid.rozieAttr)(tableRole());
6063
6213
  _v$0 !== _p$.e && (0, solid_js_web.setAttribute)(_el$23, "aria-rowcount", _p$.e = _v$0);
6064
6214
  _v$1 !== _p$.t && (0, solid_js_web.setAttribute)(_el$23, "aria-colcount", _p$.t = _v$1);
6065
6215
  _v$10 !== _p$.a && (0, solid_js_web.className)(_el$23, _p$.a = _v$10);
@@ -6635,6 +6785,9 @@ function DataTable(_props) {
6635
6785
  },
6636
6786
  get autofocus() {
6637
6787
  return editorAutofocusFor(cell().column.id, wr().vi.index);
6788
+ },
6789
+ get rangeSize() {
6790
+ return inRowEdit() ? 1 : rangeSize();
6638
6791
  }
6639
6792
  }) ?? (_props.editorSlot ?? _props.slots?.["editor"])?.({
6640
6793
  get columnId() {
@@ -6660,6 +6813,9 @@ function DataTable(_props) {
6660
6813
  },
6661
6814
  get autofocus() {
6662
6815
  return editorAutofocusFor(cell().column.id, wr().vi.index);
6816
+ },
6817
+ get rangeSize() {
6818
+ return inRowEdit() ? 1 : rangeSize();
6663
6819
  }
6664
6820
  }) ?? (() => {
6665
6821
  var _el$126 = _tmpl$29();
@@ -6997,7 +7153,8 @@ function DataTable(_props) {
6997
7153
  (0, solid_js_web.effect)((_p$) => {
6998
7154
  var _v$ = (0, _rozie_runtime_solid.parseInlineStyle)(rowsWindowed() && local.maxHeight ? "max-height:" + local.maxHeight + ";overflow:auto;--rozie-data-table-max-height:" + local.maxHeight : "overflow:auto"), _v$2 = (0, _rozie_runtime_solid.rozieAttr)(gridAriaRowCount()), _v$3 = (0, _rozie_runtime_solid.rozieAttr)(gridAriaColCount()), _v$4 = "rozie-data-table " + (0, _rozie_runtime_solid.rozieClass)({
6999
7155
  "rdt-sticky": local.stickyHeader,
7000
- "rdt-col-windowed": colsWindowed()
7156
+ "rdt-col-windowed": colsWindowed(),
7157
+ "rdt-column-lines": local.columnLines
7001
7158
  }), _v$5 = (0, _rozie_runtime_solid.rozieAttr)(tableRole()), _v$6 = (0, _rozie_runtime_solid.rozieAttr)(windowedColSpan()), _v$7 = (0, _rozie_runtime_solid.parseInlineStyle)("height:" + padTop() + "px;padding:0;border:0"), _v$8 = (0, _rozie_runtime_solid.rozieAttr)(windowedColSpan()), _v$9 = (0, _rozie_runtime_solid.parseInlineStyle)("height:" + padBottom() + "px;padding:0;border:0");
7002
7159
  _p$.e = (0, solid_js_web.style)(_el$15, _v$, _p$.e);
7003
7160
  _v$2 !== _p$.t && (0, solid_js_web.setAttribute)(_el$16, "aria-rowcount", _p$.t = _v$2);
package/dist/index.d.cts CHANGED
@@ -123,10 +123,13 @@ interface EditorSlotCtx {
123
123
  column: Column$1<any, unknown>;
124
124
  row: any;
125
125
  value: any;
126
- commit: (value: any) => void;
126
+ commit: (value: any, options?: {
127
+ toRange?: boolean;
128
+ }) => void;
127
129
  cancel: () => void;
128
130
  columnLabel: string;
129
131
  autofocus: boolean;
132
+ rangeSize: number;
130
133
  }
131
134
  interface DataTableProps {
132
135
  /**
@@ -243,6 +246,10 @@ interface DataTableProps {
243
246
  * Pure-CSS sticky header: the `<thead>` sticks to the top of the scroll container.
244
247
  */
245
248
  stickyHeader?: boolean;
249
+ /**
250
+ * Draw a line between columns, plus an outer line at the start of each row and above the header row. The colour follows `--rozie-data-table-border-color` unless `--rozie-data-table-column-border-color` is set.
251
+ */
252
+ columnLines?: boolean;
246
253
  /**
247
254
  * `'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.
248
255
  */
@@ -260,7 +267,7 @@ interface DataTableProps {
260
267
  */
261
268
  undoLimit?: number;
262
269
  /**
263
- * When `true`, the table holds its current view through its own writes (edit, paste, fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`.
270
+ * When `true`, the table holds its current view through its own writes (edit, paste, fill, range fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`.
264
271
  */
265
272
  holdView?: boolean;
266
273
  /**
@@ -332,10 +339,13 @@ interface DataTableProps {
332
339
  column: Column$1<any, unknown>;
333
340
  row: any;
334
341
  value: any;
335
- commit: (value: any) => void;
342
+ commit: (value: any, options?: {
343
+ toRange?: boolean;
344
+ }) => void;
336
345
  cancel: () => void;
337
346
  columnLabel: string;
338
347
  autofocus: boolean;
348
+ rangeSize: number;
339
349
  }) => JSX.Element) | undefined;
340
350
  [key: string]: ((...args: any[]) => JSX.Element) | undefined;
341
351
  };
package/dist/index.d.mts CHANGED
@@ -123,10 +123,13 @@ interface EditorSlotCtx {
123
123
  column: Column$1<any, unknown>;
124
124
  row: any;
125
125
  value: any;
126
- commit: (value: any) => void;
126
+ commit: (value: any, options?: {
127
+ toRange?: boolean;
128
+ }) => void;
127
129
  cancel: () => void;
128
130
  columnLabel: string;
129
131
  autofocus: boolean;
132
+ rangeSize: number;
130
133
  }
131
134
  interface DataTableProps {
132
135
  /**
@@ -243,6 +246,10 @@ interface DataTableProps {
243
246
  * Pure-CSS sticky header: the `<thead>` sticks to the top of the scroll container.
244
247
  */
245
248
  stickyHeader?: boolean;
249
+ /**
250
+ * Draw a line between columns, plus an outer line at the start of each row and above the header row. The colour follows `--rozie-data-table-border-color` unless `--rozie-data-table-column-border-color` is set.
251
+ */
252
+ columnLines?: boolean;
246
253
  /**
247
254
  * `'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.
248
255
  */
@@ -260,7 +267,7 @@ interface DataTableProps {
260
267
  */
261
268
  undoLimit?: number;
262
269
  /**
263
- * When `true`, the table holds its current view through its own writes (edit, paste, fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`.
270
+ * When `true`, the table holds its current view through its own writes (edit, paste, fill, range fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`.
264
271
  */
265
272
  holdView?: boolean;
266
273
  /**
@@ -332,10 +339,13 @@ interface DataTableProps {
332
339
  column: Column$1<any, unknown>;
333
340
  row: any;
334
341
  value: any;
335
- commit: (value: any) => void;
342
+ commit: (value: any, options?: {
343
+ toRange?: boolean;
344
+ }) => void;
336
345
  cancel: () => void;
337
346
  columnLabel: string;
338
347
  autofocus: boolean;
348
+ rangeSize: number;
339
349
  }) => JSX.Element) | undefined;
340
350
  [key: string]: ((...args: any[]) => JSX.Element) | undefined;
341
351
  };