@rozie-ui/data-table-solid 0.1.0 → 0.2.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/src/DataTable.tsx CHANGED
@@ -2,7 +2,7 @@ import type { JSX } from 'solid-js';
2
2
  import { Show, createEffect, createSignal, mergeProps, on, onCleanup, onMount, splitProps, untrack } from 'solid-js';
3
3
  import { Key } from '@solid-primitives/keyed';
4
4
  import { __rozieInjectStyle, createControllableSignal, parseInlineStyle, rozieAttr, rozieClass, rozieContext, rozieDisplay } from '@rozie/runtime-solid';
5
- import Popover from './Popover';
5
+ import Popover from '@rozie-ui/popover-solid';
6
6
  import { createTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel, getExpandedRowModel, getGroupedRowModel,
7
7
  // Faceted filtering (phase 50 reqs 8-9, D-03). All three are supplied UNCONDITIONALLY
8
8
  // (mirrors the expand/group models) — inert until a consumer READS a column facet via the
@@ -355,7 +355,7 @@ interface SelectCellSlotCtx { row: any; checked: any; toggle: any; }
355
355
 
356
356
  interface CellSlotCtx { columnId: any; column: any; row: any; value: any; }
357
357
 
358
- interface EditorSlotCtx { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; }
358
+ interface EditorSlotCtx { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; autofocus: any; }
359
359
 
360
360
  interface DetailSlotCtx { row: any; }
361
361
 
@@ -565,7 +565,7 @@ export interface DataTableHandle {
565
565
  }
566
566
 
567
567
  export default function DataTable(_props: DataTableProps): JSX.Element {
568
- const _merged = mergeProps({ columns: (() => [])(), selectionMode: 'none', manual: false, rowCount: null, pageCount: null, expandable: false, getSubRows: null, groupable: false, stickyHeader: false, interactionMode: 'table', singleClickEdit: false, undoable: false, undoLimit: 100, virtual: false, estimateRowHeight: 40, maxHeight: '' }, _props);
568
+ const _merged = mergeProps({ columns: (() => [])() as any[], selectionMode: 'none', manual: false, rowCount: null, pageCount: null, expandable: false, getSubRows: null, groupable: false, stickyHeader: false, interactionMode: 'table', singleClickEdit: false, undoable: false, undoLimit: 100, virtual: false, estimateRowHeight: 40, maxHeight: '' }, _props);
569
569
  const [local, attrs] = splitProps(_merged, ['data', 'columns', 'selectionMode', 'sorting', 'globalFilter', 'columnFilters', 'pagination', 'manual', 'rowCount', 'pageCount', 'expandable', 'expanded', 'getSubRows', 'groupable', 'grouping', 'rowSelection', 'columnVisibility', 'columnSizing', 'columnOrder', 'columnPinning', 'stickyHeader', 'interactionMode', 'singleClickEdit', 'undoable', 'undoLimit', 'virtual', 'estimateRowHeight', 'maxHeight', 'children', 'ref']);
570
570
  const resolved = () => local.children;
571
571
  onMount(() => { local.ref?.({ sortColumn, clearSorting, toggleRowExpanded, expandAll, collapseAll, getExpandedRows, applyGrouping, clearGrouping, getFacetedUniqueValues, getFacetedMinMaxValues, getColumnDefs, toggleAllRows, clearSelection, getSelectedRows, setPage, setRowsPerPage, toggleColumnVisibility, applyColumnOrder, resetColumnSizing, pinColumn, focusCell, getActiveCell, clearActiveCell, getRowIndexRelativeToPage, editCell, commitEditing, editRow, getSelectedRange, cut, undo, redo, canUndo, canRedo, clearHistory }); });
@@ -579,8 +579,8 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
579
579
  pageIndex: 0,
580
580
  pageSize: 10
581
581
  }))());
582
- const [expanded, setExpanded] = createControllableSignal<Record<string, any> | boolean>(_props as unknown as Record<string, unknown>, 'expanded', null);
583
- const [grouping, setGrouping] = createControllableSignal<any[]>(_props as unknown as Record<string, unknown>, 'grouping', null);
582
+ const [expanded, setExpanded] = createControllableSignal<Record<string, any> | boolean | null>(_props as unknown as Record<string, unknown>, 'expanded', null);
583
+ const [grouping, setGrouping] = createControllableSignal<any[] | null>(_props as unknown as Record<string, unknown>, 'grouping', null);
584
584
  const [rowSelection, setRowSelection] = createControllableSignal<Record<string, any>>(_props as unknown as Record<string, unknown>, 'rowSelection', (() => ({}))());
585
585
  const [columnVisibility, setColumnVisibility] = createControllableSignal<Record<string, any>>(_props as unknown as Record<string, unknown>, 'columnVisibility', (() => ({}))());
586
586
  const [columnSizing, setColumnSizing] = createControllableSignal<Record<string, any>>(_props as unknown as Record<string, unknown>, 'columnSizing', (() => ({}))());
@@ -630,6 +630,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
630
630
  const [draftValue, setDraftValue] = createSignal<any>(null);
631
631
  const [invalidMsg, setInvalidMsg] = createSignal('');
632
632
  const [editVer, setEditVer] = createSignal(0);
633
+ const [editFocusColId, setEditFocusColId] = createSignal<any>(null);
633
634
  const [editingRowIndex, setEditingRowIndex] = createSignal<any>(null);
634
635
  const [rowDraft, setRowDraft] = createSignal<Record<string, any>>({});
635
636
  const [rangeAnchor, setRangeAnchor] = createSignal<any>(null);
@@ -2605,15 +2606,9 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2605
2606
  function bodyCellStyle(row: any, colId: any) {
2606
2607
  const base = pinStyle(colId);
2607
2608
  if (isExpanderColumn(colId) && row && row.depth) {
2609
+ // Only the expander column indents (the tree affordance lives here).
2608
2610
  const pad = 'padding-left:' + (0.5 + row.depth * 1.25) + 'rem';
2609
- // pinStyle() already ends with ';', so append `pad` directly — inserting a
2610
- // second ';' produces a `;;` empty declaration. Browsers tolerate that in a
2611
- // raw style attribute (React/Vue/Svelte/Lit) and Solid's cssText path does
2612
- // too, but Angular's [style] binding parses the string via ɵɵstyleMap and
2613
- // DROPS the declaration following the empty one — so the depth padding-left
2614
- // silently vanished on Angular only. Emit a single, well-formed separator.
2615
- const sep = base.endsWith(';') ? '' : ';';
2616
- return base ? base + sep + pad : pad;
2611
+ return base ? base + pad : pad;
2617
2612
  }
2618
2613
  return base;
2619
2614
  }
@@ -5169,8 +5164,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5169
5164
  // draft (selecting the seeded char makes the next keystroke replace it: Zeta → eta).
5170
5165
  // beginEdit threads `seed == null` so a seeded entry skips the select and the caret sits
5171
5166
  // AFTER the seeded char; every other caller keeps the default select-all.
5167
+ // Editor-owns-focus contract (quick 260711-i5m): REVERTS the g52 shadow-piercing helper
5168
+ // (commit 5fa30045) that recursed into descendant shadow roots. Built-in editors are
5169
+ // host-DOM — the plain direct query resolves them on all 6 targets (no shadow to cross). A
5170
+ // #editor DROP-IN now owns its OWN focus via the reactive `autofocus` prop (EditorText's
5171
+ // $onMount + lazy $watch), so the host never needs to reach across a Lit drop-in's nested
5172
+ // shadow root at all — see the !hasEditorSlot gate below, which skips the host focus call
5173
+ // entirely for a drop-in target.
5172
5174
  function focusEditorWhenReady(selectAll = true) {
5173
5175
  if (!gridRoot) return;
5176
+ // Editor-owns-focus contract: when the CURRENT focus target is a #editor drop-in, the host
5177
+ // does NOT reach into its DOM — the drop-in self-focuses via its own autofocus prop.
5178
+ if (editFocusColId() != null && hasEditorSlot(editFocusColId())) return;
5174
5179
  let attempts = 0;
5175
5180
  const tryFocus = () => {
5176
5181
  const el = gridRoot ? gridRoot.querySelector('[data-editing-cell]') : null;
@@ -5246,6 +5251,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5246
5251
  setDraftValue(seed != null ? seed : cellValueAt(rowIndex, colIndex));
5247
5252
  setActiveInControl(true);
5248
5253
  setEditVer(editVer() + 1);
5254
+ // Editor-owns-focus contract (quick 260711-i5m): THIS cell's column is the current
5255
+ // focus target — editorAutofocusFor derives the reactive `autofocus` #editor scope prop
5256
+ // from it. Cleared on endEdit.
5257
+ setEditFocusColId(colId);
5249
5258
  // B2: a seeded (type-to-edit) entry must NOT select-all — keep the caret after the
5250
5259
  // seeded char so subsequent typing appends instead of replacing it.
5251
5260
  focusEditorWhenReady(seed == null);
@@ -5300,6 +5309,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5300
5309
  setInvalidMsg('');
5301
5310
  setActiveInControl(false);
5302
5311
  setEditVer(editVer() + 1);
5312
+ setEditFocusColId(null);
5303
5313
  }
5304
5314
 
5305
5315
  // endRowEdit: tear down full-row edit (shared by commitRow/cancelRow). Clears the row
@@ -5311,6 +5321,23 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5311
5321
  setInvalidMsg('');
5312
5322
  setActiveInControl(false);
5313
5323
  setEditVer(editVer() + 1);
5324
+ setEditFocusColId(null);
5325
+ }
5326
+
5327
+ // editorAutofocusFor (quick 260711-i5m, editor-owns-focus contract): the reactive `autofocus`
5328
+ // #editor scope prop for a given (colId, rowIndex) — true for EXACTLY the current focus-
5329
+ // target cell, re-deriving on every editVer bump (mirrors isEditing's reactive gate so
5330
+ // Svelte/Solid re-run this per-cell on a foreign-slot-callback state mutation). Works for
5331
+ // BOTH single-cell ($data.editingRow) and row mode ($data.editingRowIndex) since
5332
+ // $data.editFocusColId is set by both beginEdit and beginRowEdit/commitRow/rowEditTab.
5333
+ function editorAutofocusFor(colId: any, rowIndex: any) {
5334
+ if (editVer() < 0) return false;
5335
+ if (editingRowIndex() != null) {
5336
+ if (editingRowIndex() !== rowIndex) return false;
5337
+ } else {
5338
+ if (editingRow() !== rowIndex) return false;
5339
+ }
5340
+ return editFocusColId() != null && editFocusColId() === colId;
5314
5341
  }
5315
5342
 
5316
5343
  // B3: coerce the committed value by the column's built-in editor type at the single
@@ -5528,8 +5555,15 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5528
5555
  // focuses its [data-editing-cell] control. Bounded rAF-poll (mirrors focusEditorWhenReady)
5529
5556
  // so a React re-render that recreates the input across the focus call still lands it. select-
5530
5557
  // all on text/number editors (a no-op try/catch on select/checkbox).
5558
+ // Editor-owns-focus contract (quick 260711-i5m): when the TARGET column is a #editor
5559
+ // drop-in, the host does NOT reach into its DOM (early return, before starting the rAF poll
5560
+ // at all) — the drop-in self-focuses via its own reactive `autofocus` prop, which the caller
5561
+ // (commitRow's B22 reject path / rowEditTab) already flips via $data.editFocusColId. Built-in
5562
+ // columns are unaffected (hasEditorSlot is false for them) — unchanged host direct-focus.
5531
5563
  function focusRowEditorAt(rowIndex: any, colIndex: any) {
5532
5564
  if (!gridRoot) return;
5565
+ const colId = columnIdAt(rowIndex, colIndex);
5566
+ if (colId != null && hasEditorSlot(colId)) return;
5533
5567
  let attempts = 0;
5534
5568
  const tryFocus = () => {
5535
5569
  const cellEl = resolveCellEl(String(rowIndex), colIndex);
@@ -5581,6 +5615,11 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5581
5615
  setEditingRowIndex(rowIndex);
5582
5616
  setActiveInControl(true);
5583
5617
  setEditVer(editVer() + 1);
5618
+ // Editor-owns-focus contract (quick 260711-i5m): the row's FIRST editable column is the
5619
+ // initial focus target — editorAutofocusFor derives the reactive `autofocus` #editor scope
5620
+ // prop from it (a built-in column is also host-focused below via focusEditorWhenReady; a
5621
+ // drop-in column self-focuses via its own $onMount, gated off the host reach-in in Task 3).
5622
+ setEditFocusColId(editable[0].colId);
5584
5623
  focusEditorWhenReady();
5585
5624
  }
5586
5625
 
@@ -5613,9 +5652,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5613
5652
  const err = runValidator(ec.colId, coerceCellValue(ec.colId, draft[ec.colId]), rowOriginal);
5614
5653
  if (err !== true) {
5615
5654
  setInvalid(err);
5655
+ // Editor-owns-focus contract (quick 260711-i5m): the OFFENDING column becomes the new
5656
+ // reactive focus target BEFORE the host-focus call below — a #editor drop-in already
5657
+ // mounted (full-row edit opens every editable cell at once) picks this up via its own
5658
+ // lazy $watch on the `autofocus` scope prop flipping false→true. Bump editVer so the
5659
+ // coarse-render targets (React/Vue/Angular/Svelte) re-derive the slot binding (Solid's
5660
+ // fine-grained accessor re-runs without the bump, but the bump keeps all 6 in lockstep).
5661
+ setEditFocusColId(ec.colId);
5662
+ setEditVer(editVer() + 1);
5616
5663
  // B22: focus the OFFENDING column's editor (the one whose validator rejected), NOT
5617
5664
  // unconditionally the first editor (focusEditorWhenReady resolves the first
5618
5665
  // [data-editing-cell] in DOM order). ec.colIndex is the offending cell's visible col.
5666
+ // Gated (Task 3) so a #editor drop-in self-focuses instead of a host DOM reach-in.
5619
5667
  focusRowEditorAt(rowIndex, ec.colIndex);
5620
5668
  return false;
5621
5669
  }
@@ -5894,6 +5942,11 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5894
5942
  if (pos < 0) pos = 0;
5895
5943
  const len = cols.length;
5896
5944
  const nextPos = backward ? (pos - 1 + len) % len : (pos + 1) % len;
5945
+ // Editor-owns-focus contract (quick 260711-i5m): the Tab target becomes the new reactive
5946
+ // focus target BEFORE the host-focus call below, so Tab onto an already-mounted #editor
5947
+ // drop-in (row mode) also refocuses it via its own lazy $watch.
5948
+ setEditFocusColId(editable[nextPos].colId);
5949
+ setEditVer(editVer() + 1);
5897
5950
  focusRowEditorAt(rowIndex, cols[nextPos]);
5898
5951
  }
5899
5952
 
@@ -6426,7 +6479,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6426
6479
  {<Show when={hasEditorSlot(cell().column.id)} fallback={<Show when={editorTypeOf(cell().column.id) === 'number'} fallback={<Show when={editorTypeOf(cell().column.id) === 'select'} fallback={<Show when={editorTypeOf(cell().column.id) === 'checkbox'} fallback={<input type="text" data-editing-cell="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}><input type="checkbox" data-editing-cell="" class={"rdt-cell-editor"} checked={editorCheckedFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorCheckbox(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><select data-editing-cell="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLSelectElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="">
6427
6480
  <Key each={editorOptionsOf(cell().column.id) as readonly any[]} by={(opt) => opt.value}>{(opt) => <option value={rozieAttr(opt().value)} data-rozie-s-d5dcab4c="">{rozieDisplay(opt().label)}</option>}</Key>
6428
6481
  </select></Show>}><input type="number" data-editing-cell="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
6429
- {(_props.editorSlot ?? _props.slots?.['editor'])?.({ columnId: cell().column.id, column: cell().column, row: row().original, value: editorValueFor(cell().column.id), commit: editorCommitFor(cell().column.id), cancel: editorCancelFor() })}
6482
+ {(_props.editorSlot ?? _props.slots?.['editor'])?.({ columnId: cell().column.id, column: cell().column, row: row().original, value: editorValueFor(cell().column.id), commit: editorCommitFor(cell().column.id), cancel: editorCancelFor(), autofocus: editorAutofocusFor(cell().column.id, rowIndexOf(row())) })}
6430
6483
  </span></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
6431
6484
  <button type="button" data-expander="" aria-expanded={!!rowIsExpanded(row())} aria-label={rozieAttr(rowIsExpanded(row()) ? 'Collapse group' : 'Expand group')} class={"rdt-expander rdt-group-toggle"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onToggleExpand(row(), $event); }} data-rozie-s-d5dcab4c="">{rozieDisplay(rowIsExpanded(row()) ? '▾' : '▸')}</button>
6432
6485
  <span class={"rdt-group-value"} data-rozie-s-d5dcab4c="">
@@ -6499,7 +6552,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6499
6552
  {<Show when={hasEditorSlot(cell().column.id)} fallback={<Show when={editorTypeOf(cell().column.id) === 'number'} fallback={<Show when={editorTypeOf(cell().column.id) === 'select'} fallback={<Show when={editorTypeOf(cell().column.id) === 'checkbox'} fallback={<input type="text" data-editing-cell="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}><input type="checkbox" data-editing-cell="" class={"rdt-cell-editor"} checked={editorCheckedFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorCheckbox(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><select data-editing-cell="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLSelectElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="">
6500
6553
  <Key each={editorOptionsOf(cell().column.id) as readonly any[]} by={(opt) => opt.value}>{(opt) => <option value={rozieAttr(opt().value)} data-rozie-s-d5dcab4c="">{rozieDisplay(opt().label)}</option>}</Key>
6501
6554
  </select></Show>}><input type="number" data-editing-cell="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
6502
- {(_props.editorSlot ?? _props.slots?.['editor'])?.({ columnId: cell().column.id, column: cell().column, row: wr().row.original, value: editorValueFor(cell().column.id), commit: editorCommitFor(cell().column.id), cancel: editorCancelFor() })}
6555
+ {(_props.editorSlot ?? _props.slots?.['editor'])?.({ columnId: cell().column.id, column: cell().column, row: wr().row.original, value: editorValueFor(cell().column.id), commit: editorCommitFor(cell().column.id), cancel: editorCancelFor(), autofocus: editorAutofocusFor(cell().column.id, wr().vi.index) })}
6503
6556
  </span></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
6504
6557
  <button type="button" data-expander="" aria-expanded={!!rowIsExpanded(wr().row)} aria-label={rozieAttr(rowIsExpanded(wr().row) ? 'Collapse group' : 'Expand group')} class={"rdt-expander rdt-group-toggle"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onToggleExpand(wr().row, $event); }} data-rozie-s-d5dcab4c="">{rozieDisplay(rowIsExpanded(wr().row) ? '▾' : '▸')}</button>
6505
6558
  <span class={"rdt-group-value"} data-rozie-s-d5dcab4c="">
@@ -35,7 +35,7 @@ interface EditorSelectProps {
35
35
  }
36
36
 
37
37
  export default function EditorSelect(_props: EditorSelectProps): JSX.Element {
38
- const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null, options: (() => [])() }, _props);
38
+ const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null, options: (() => [])() as any[] }, _props);
39
39
  const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel', 'options']);
40
40
 
41
41
  const [draft, setDraft] = createSignal('');
@@ -1,5 +1,5 @@
1
1
  import type { JSX } from 'solid-js';
2
- import { createSignal, mergeProps, splitProps } from 'solid-js';
2
+ import { createEffect, createSignal, mergeProps, on, onMount, splitProps, untrack } from 'solid-js';
3
3
 
4
4
  interface EditorTextProps {
5
5
  /**
@@ -26,13 +26,24 @@ interface EditorTextProps {
26
26
  * `() => void` — revert the edit and close the editor (from the `#editor` slot scope). Null-guarded at call sites.
27
27
  */
28
28
  cancel?: ((...args: unknown[]) => unknown) | null;
29
+ /**
30
+ * Focus this editor's primary input when true — the host sets it for the one editor that should hold focus; reactive.
31
+ */
32
+ autofocus?: boolean;
29
33
  }
30
34
 
31
35
  export default function EditorText(_props: EditorTextProps): JSX.Element {
32
- const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null }, _props);
33
- const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel']);
36
+ const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null, autofocus: false }, _props);
37
+ const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel', 'autofocus']);
34
38
 
35
39
  const [draft, setDraft] = createSignal('');
40
+ onMount(() => {
41
+ if (local.autofocus) inputElRef?.focus();
42
+ });
43
+ createEffect(on(() => (() => local.autofocus)(), (v) => untrack(() => ((v: any) => {
44
+ if (v) inputElRef?.focus();
45
+ })(v)), { defer: true }));
46
+ let inputElRef: HTMLElement | null = null;
36
47
 
37
48
  // Seed the draft once at setup from the incoming value (setup-once, NOT in the
38
49
  // template). Normalize null/undefined to '' so the input value binds to a string.
@@ -64,9 +75,15 @@ export default function EditorText(_props: EditorTextProps): JSX.Element {
64
75
  doCommit();
65
76
  }
66
77
 
78
+ // Editor-owns-focus contract: focus OUR OWN input when the host says we should hold focus.
79
+ // $onMount covers the initial open (autofocus already true on first render); the LAZY $watch
80
+ // (NOT { immediate: true } — an immediate watch fires PRE-mount, null ref on Lit/Solid) covers
81
+ // a REACTIVE refocus while already mounted (e.g. a row-mode validation failure that flips
82
+ // autofocus back onto this already-open drop-in).
83
+
67
84
  return (
68
85
  <>
69
- <input type="text" data-editing-cell="" aria-label={local.columnId} class={"rdt-cell-editor"} value={draft()} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onInput($event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onKeydown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onBlur(); }} data-rozie-s-0d17f43a="" />
86
+ <input type="text" data-editing-cell="" aria-label={local.columnId} ref={(el) => { inputElRef = el as HTMLElement; }} class={"rdt-cell-editor"} value={draft()} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onInput($event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onKeydown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onBlur(); }} data-rozie-s-0d17f43a="" />
70
87
  </>
71
88
  );
72
89
  }
@@ -27,7 +27,7 @@ interface FilterSelectProps {
27
27
  }
28
28
 
29
29
  export default function FilterSelect(_props: FilterSelectProps): JSX.Element {
30
- const _merged = mergeProps({ columnId: '', column: null, value: null, setFilter: null, uniqueValues: (() => [])() }, _props);
30
+ const _merged = mergeProps({ columnId: '', column: null, value: null, setFilter: null, uniqueValues: (() => [])() as any[] }, _props);
31
31
  const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'value', 'setFilter', 'uniqueValues']);
32
32
 
33
33
  // The <select> value binding coerced to a string. $props.value is typed `unknown`
package/src/GroupBar.tsx CHANGED
@@ -81,7 +81,7 @@ interface GroupBarProps {
81
81
  }
82
82
 
83
83
  export default function GroupBar(_props: GroupBarProps): JSX.Element {
84
- const _merged = mergeProps({ grouping: (() => [])(), groupableColumns: (() => [])(), applyGrouping: null, clearGrouping: null }, _props);
84
+ const _merged = mergeProps({ grouping: (() => [])() as any[], groupableColumns: (() => [])() as any[], applyGrouping: null, clearGrouping: null }, _props);
85
85
  const [local, attrs] = splitProps(_merged, ['grouping', 'groupableColumns', 'applyGrouping', 'clearGrouping']);
86
86
 
87
87
  const [draggingId, setDraggingId] = createSignal('');