@rozie-ui/data-table-solid 0.1.0 → 0.1.1

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 CHANGED
@@ -529,5 +529,5 @@ The `detail` (expandable rows), `groupBar` (grouping) and `filter` (faceted filt
529
529
  | filter | columnId, value, uniqueValues, minMax, setFilter |
530
530
  | selectCell | row, checked, toggle |
531
531
  | cell | columnId, column, row, value |
532
- | editor | columnId, column, row, value, commit, cancel |
532
+ | editor | columnId, column, row, value, commit, cancel, autofocus |
533
533
  | detail | row |
package/dist/index.cjs CHANGED
@@ -738,6 +738,7 @@ function DataTable(_props) {
738
738
  const [draftValue, setDraftValue] = (0, Solid_js.createSignal)(null);
739
739
  const [invalidMsg, setInvalidMsg] = (0, Solid_js.createSignal)("");
740
740
  const [editVer, setEditVer] = (0, Solid_js.createSignal)(0);
741
+ const [editFocusColId, setEditFocusColId] = (0, Solid_js.createSignal)(null);
741
742
  const [editingRowIndex, setEditingRowIndex] = (0, Solid_js.createSignal)(null);
742
743
  const [rowDraft, setRowDraft] = (0, Solid_js.createSignal)({});
743
744
  const [rangeAnchor, setRangeAnchor] = (0, Solid_js.createSignal)(null);
@@ -3180,6 +3181,7 @@ function DataTable(_props) {
3180
3181
  }
3181
3182
  function focusEditorWhenReady(selectAll = true) {
3182
3183
  if (!gridRoot) return;
3184
+ if (editFocusColId() != null && hasEditorSlot(editFocusColId())) return;
3183
3185
  let attempts = 0;
3184
3186
  const tryFocus = () => {
3185
3187
  const el = gridRoot ? gridRoot.querySelector("[data-editing-cell]") : null;
@@ -3230,6 +3232,7 @@ function DataTable(_props) {
3230
3232
  setDraftValue(seed != null ? seed : cellValueAt(rowIndex, colIndex));
3231
3233
  setActiveInControl(true);
3232
3234
  setEditVer(editVer() + 1);
3235
+ setEditFocusColId(colId);
3233
3236
  focusEditorWhenReady(seed == null);
3234
3237
  }
3235
3238
  function focusCellWhenReady(row, col) {
@@ -3266,6 +3269,7 @@ function DataTable(_props) {
3266
3269
  setInvalidMsg("");
3267
3270
  setActiveInControl(false);
3268
3271
  setEditVer(editVer() + 1);
3272
+ setEditFocusColId(null);
3269
3273
  }
3270
3274
  function endRowEdit() {
3271
3275
  setEditingRowIndex(null);
@@ -3273,6 +3277,14 @@ function DataTable(_props) {
3273
3277
  setInvalidMsg("");
3274
3278
  setActiveInControl(false);
3275
3279
  setEditVer(editVer() + 1);
3280
+ setEditFocusColId(null);
3281
+ }
3282
+ function editorAutofocusFor(colId, rowIndex) {
3283
+ if (editVer() < 0) return false;
3284
+ if (editingRowIndex() != null) {
3285
+ if (editingRowIndex() !== rowIndex) return false;
3286
+ } else if (editingRow() !== rowIndex) return false;
3287
+ return editFocusColId() != null && editFocusColId() === colId;
3276
3288
  }
3277
3289
  function coerceCellValue(colId, raw) {
3278
3290
  if (editorTypeOf(colId) !== "number") return raw;
@@ -3391,6 +3403,8 @@ function DataTable(_props) {
3391
3403
  }
3392
3404
  function focusRowEditorAt(rowIndex, colIndex) {
3393
3405
  if (!gridRoot) return;
3406
+ const colId = columnIdAt(rowIndex, colIndex);
3407
+ if (colId != null && hasEditorSlot(colId)) return;
3394
3408
  let attempts = 0;
3395
3409
  const tryFocus = () => {
3396
3410
  const cellEl = resolveCellEl(String(rowIndex), colIndex);
@@ -3431,6 +3445,7 @@ function DataTable(_props) {
3431
3445
  setEditingRowIndex(rowIndex);
3432
3446
  setActiveInControl(true);
3433
3447
  setEditVer(editVer() + 1);
3448
+ setEditFocusColId(editable[0].colId);
3434
3449
  focusEditorWhenReady();
3435
3450
  }
3436
3451
  function commitRow() {
@@ -3450,6 +3465,8 @@ function DataTable(_props) {
3450
3465
  const err = runValidator(ec.colId, coerceCellValue(ec.colId, draft[ec.colId]), rowOriginal);
3451
3466
  if (err !== true) {
3452
3467
  setInvalid(err);
3468
+ setEditFocusColId(ec.colId);
3469
+ setEditVer(editVer() + 1);
3453
3470
  focusRowEditorAt(rowIndex, ec.colIndex);
3454
3471
  return false;
3455
3472
  }
@@ -3623,7 +3640,10 @@ function DataTable(_props) {
3623
3640
  let pos = cols.indexOf(cur);
3624
3641
  if (pos < 0) pos = 0;
3625
3642
  const len = cols.length;
3626
- focusRowEditorAt(rowIndex, cols[backward ? (pos - 1 + len) % len : (pos + 1) % len]);
3643
+ const nextPos = backward ? (pos - 1 + len) % len : (pos + 1) % len;
3644
+ setEditFocusColId(editable[nextPos].colId);
3645
+ setEditVer(editVer() + 1);
3646
+ focusRowEditorAt(rowIndex, cols[nextPos]);
3627
3647
  }
3628
3648
  function onEditorKeyDown(e) {
3629
3649
  if (!e) return;
@@ -4042,7 +4062,8 @@ function DataTable(_props) {
4042
4062
  row: row().original,
4043
4063
  value: editorValueFor(cell().column.id),
4044
4064
  commit: editorCommitFor(cell().column.id),
4045
- cancel: editorCancelFor()
4065
+ cancel: editorCancelFor(),
4066
+ autofocus: editorAutofocusFor(cell().column.id, rowIndexOf(row()))
4046
4067
  })}
4047
4068
  </span></Solid_js.Show>}</span></Solid_js.Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
4048
4069
  <button type="button" data-expander="" aria-expanded={!!rowIsExpanded(row())} aria-label={(0, _rozie_runtime_solid.rozieAttr)(rowIsExpanded(row()) ? "Collapse group" : "Expand group")} class={"rdt-expander rdt-group-toggle"} onClick={($event) => {
@@ -4231,7 +4252,8 @@ function DataTable(_props) {
4231
4252
  row: wr().row.original,
4232
4253
  value: editorValueFor(cell().column.id),
4233
4254
  commit: editorCommitFor(cell().column.id),
4234
- cancel: editorCancelFor()
4255
+ cancel: editorCancelFor(),
4256
+ autofocus: editorAutofocusFor(cell().column.id, wr().vi.index)
4235
4257
  })}
4236
4258
  </span></Solid_js.Show>}</span></Solid_js.Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
4237
4259
  <button type="button" data-expander="" aria-expanded={!!rowIsExpanded(wr().row)} aria-label={(0, _rozie_runtime_solid.rozieAttr)(rowIsExpanded(wr().row) ? "Collapse group" : "Expand group")} class={"rdt-expander rdt-group-toggle"} onClick={($event) => {
@@ -4406,16 +4428,25 @@ function EditorText(_props) {
4406
4428
  row: null,
4407
4429
  value: null,
4408
4430
  commit: null,
4409
- cancel: null
4431
+ cancel: null,
4432
+ autofocus: false
4410
4433
  }, _props), [
4411
4434
  "columnId",
4412
4435
  "column",
4413
4436
  "row",
4414
4437
  "value",
4415
4438
  "commit",
4416
- "cancel"
4439
+ "cancel",
4440
+ "autofocus"
4417
4441
  ]);
4418
4442
  const [draft, setDraft] = (0, Solid_js.createSignal)("");
4443
+ (0, Solid_js.onMount)(() => {
4444
+ if (local.autofocus) inputElRef?.focus();
4445
+ });
4446
+ (0, Solid_js.createEffect)((0, Solid_js.on)(() => local.autofocus, (v) => (0, Solid_js.untrack)(() => ((v) => {
4447
+ if (v) inputElRef?.focus();
4448
+ })(v)), { defer: true }));
4449
+ let inputElRef = null;
4419
4450
  setDraft(local.value != null ? String(local.value) : "");
4420
4451
  function onInput(e) {
4421
4452
  setDraft(e && e.target ? e.target.value : "");
@@ -4439,7 +4470,9 @@ function EditorText(_props) {
4439
4470
  doCommit();
4440
4471
  }
4441
4472
  return <>
4442
- <input type="text" data-editing-cell="" aria-label={local.columnId} class={"rdt-cell-editor"} value={draft()} onInput={($event) => {
4473
+ <input type="text" data-editing-cell="" aria-label={local.columnId} ref={(el) => {
4474
+ inputElRef = el;
4475
+ }} class={"rdt-cell-editor"} value={draft()} onInput={($event) => {
4443
4476
  onInput($event);
4444
4477
  }} onKeyDown={($event) => {
4445
4478
  onKeydown($event);
package/dist/index.d.cts CHANGED
@@ -42,6 +42,7 @@ interface EditorSlotCtx {
42
42
  value: any;
43
43
  commit: any;
44
44
  cancel: any;
45
+ autofocus: any;
45
46
  }
46
47
  interface DetailSlotCtx {
47
48
  row: any;
@@ -339,6 +340,10 @@ interface EditorTextProps {
339
340
  * `() => void` — revert the edit and close the editor (from the `#editor` slot scope). Null-guarded at call sites.
340
341
  */
341
342
  cancel?: ((...args: unknown[]) => unknown) | null;
343
+ /**
344
+ * Focus this editor's primary input when true — the host sets it for the one editor that should hold focus; reactive.
345
+ */
346
+ autofocus?: boolean;
342
347
  }
343
348
  declare function EditorText(_props: EditorTextProps): JSX.Element;
344
349
  //#endregion
package/dist/index.d.mts CHANGED
@@ -42,6 +42,7 @@ interface EditorSlotCtx {
42
42
  value: any;
43
43
  commit: any;
44
44
  cancel: any;
45
+ autofocus: any;
45
46
  }
46
47
  interface DetailSlotCtx {
47
48
  row: any;
@@ -339,6 +340,10 @@ interface EditorTextProps {
339
340
  * `() => void` — revert the edit and close the editor (from the `#editor` slot scope). Null-guarded at call sites.
340
341
  */
341
342
  cancel?: ((...args: unknown[]) => unknown) | null;
343
+ /**
344
+ * Focus this editor's primary input when true — the host sets it for the one editor that should hold focus; reactive.
345
+ */
346
+ autofocus?: boolean;
342
347
  }
343
348
  declare function EditorText(_props: EditorTextProps): JSX.Element;
344
349
  //#endregion
package/dist/index.mjs CHANGED
@@ -734,6 +734,7 @@ function DataTable(_props) {
734
734
  const [draftValue, setDraftValue] = createSignal(null);
735
735
  const [invalidMsg, setInvalidMsg] = createSignal("");
736
736
  const [editVer, setEditVer] = createSignal(0);
737
+ const [editFocusColId, setEditFocusColId] = createSignal(null);
737
738
  const [editingRowIndex, setEditingRowIndex] = createSignal(null);
738
739
  const [rowDraft, setRowDraft] = createSignal({});
739
740
  const [rangeAnchor, setRangeAnchor] = createSignal(null);
@@ -3176,6 +3177,7 @@ function DataTable(_props) {
3176
3177
  }
3177
3178
  function focusEditorWhenReady(selectAll = true) {
3178
3179
  if (!gridRoot) return;
3180
+ if (editFocusColId() != null && hasEditorSlot(editFocusColId())) return;
3179
3181
  let attempts = 0;
3180
3182
  const tryFocus = () => {
3181
3183
  const el = gridRoot ? gridRoot.querySelector("[data-editing-cell]") : null;
@@ -3226,6 +3228,7 @@ function DataTable(_props) {
3226
3228
  setDraftValue(seed != null ? seed : cellValueAt(rowIndex, colIndex));
3227
3229
  setActiveInControl(true);
3228
3230
  setEditVer(editVer() + 1);
3231
+ setEditFocusColId(colId);
3229
3232
  focusEditorWhenReady(seed == null);
3230
3233
  }
3231
3234
  function focusCellWhenReady(row, col) {
@@ -3262,6 +3265,7 @@ function DataTable(_props) {
3262
3265
  setInvalidMsg("");
3263
3266
  setActiveInControl(false);
3264
3267
  setEditVer(editVer() + 1);
3268
+ setEditFocusColId(null);
3265
3269
  }
3266
3270
  function endRowEdit() {
3267
3271
  setEditingRowIndex(null);
@@ -3269,6 +3273,14 @@ function DataTable(_props) {
3269
3273
  setInvalidMsg("");
3270
3274
  setActiveInControl(false);
3271
3275
  setEditVer(editVer() + 1);
3276
+ setEditFocusColId(null);
3277
+ }
3278
+ function editorAutofocusFor(colId, rowIndex) {
3279
+ if (editVer() < 0) return false;
3280
+ if (editingRowIndex() != null) {
3281
+ if (editingRowIndex() !== rowIndex) return false;
3282
+ } else if (editingRow() !== rowIndex) return false;
3283
+ return editFocusColId() != null && editFocusColId() === colId;
3272
3284
  }
3273
3285
  function coerceCellValue(colId, raw) {
3274
3286
  if (editorTypeOf(colId) !== "number") return raw;
@@ -3387,6 +3399,8 @@ function DataTable(_props) {
3387
3399
  }
3388
3400
  function focusRowEditorAt(rowIndex, colIndex) {
3389
3401
  if (!gridRoot) return;
3402
+ const colId = columnIdAt(rowIndex, colIndex);
3403
+ if (colId != null && hasEditorSlot(colId)) return;
3390
3404
  let attempts = 0;
3391
3405
  const tryFocus = () => {
3392
3406
  const cellEl = resolveCellEl(String(rowIndex), colIndex);
@@ -3427,6 +3441,7 @@ function DataTable(_props) {
3427
3441
  setEditingRowIndex(rowIndex);
3428
3442
  setActiveInControl(true);
3429
3443
  setEditVer(editVer() + 1);
3444
+ setEditFocusColId(editable[0].colId);
3430
3445
  focusEditorWhenReady();
3431
3446
  }
3432
3447
  function commitRow() {
@@ -3446,6 +3461,8 @@ function DataTable(_props) {
3446
3461
  const err = runValidator(ec.colId, coerceCellValue(ec.colId, draft[ec.colId]), rowOriginal);
3447
3462
  if (err !== true) {
3448
3463
  setInvalid(err);
3464
+ setEditFocusColId(ec.colId);
3465
+ setEditVer(editVer() + 1);
3449
3466
  focusRowEditorAt(rowIndex, ec.colIndex);
3450
3467
  return false;
3451
3468
  }
@@ -3619,7 +3636,10 @@ function DataTable(_props) {
3619
3636
  let pos = cols.indexOf(cur);
3620
3637
  if (pos < 0) pos = 0;
3621
3638
  const len = cols.length;
3622
- focusRowEditorAt(rowIndex, cols[backward ? (pos - 1 + len) % len : (pos + 1) % len]);
3639
+ const nextPos = backward ? (pos - 1 + len) % len : (pos + 1) % len;
3640
+ setEditFocusColId(editable[nextPos].colId);
3641
+ setEditVer(editVer() + 1);
3642
+ focusRowEditorAt(rowIndex, cols[nextPos]);
3623
3643
  }
3624
3644
  function onEditorKeyDown(e) {
3625
3645
  if (!e) return;
@@ -4038,7 +4058,8 @@ function DataTable(_props) {
4038
4058
  row: row().original,
4039
4059
  value: editorValueFor(cell().column.id),
4040
4060
  commit: editorCommitFor(cell().column.id),
4041
- cancel: editorCancelFor()
4061
+ cancel: editorCancelFor(),
4062
+ autofocus: editorAutofocusFor(cell().column.id, rowIndexOf(row()))
4042
4063
  })}
4043
4064
  </span></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
4044
4065
  <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) => {
@@ -4227,7 +4248,8 @@ function DataTable(_props) {
4227
4248
  row: wr().row.original,
4228
4249
  value: editorValueFor(cell().column.id),
4229
4250
  commit: editorCommitFor(cell().column.id),
4230
- cancel: editorCancelFor()
4251
+ cancel: editorCancelFor(),
4252
+ autofocus: editorAutofocusFor(cell().column.id, wr().vi.index)
4231
4253
  })}
4232
4254
  </span></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
4233
4255
  <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) => {
@@ -4402,16 +4424,25 @@ function EditorText(_props) {
4402
4424
  row: null,
4403
4425
  value: null,
4404
4426
  commit: null,
4405
- cancel: null
4427
+ cancel: null,
4428
+ autofocus: false
4406
4429
  }, _props), [
4407
4430
  "columnId",
4408
4431
  "column",
4409
4432
  "row",
4410
4433
  "value",
4411
4434
  "commit",
4412
- "cancel"
4435
+ "cancel",
4436
+ "autofocus"
4413
4437
  ]);
4414
4438
  const [draft, setDraft] = createSignal("");
4439
+ onMount(() => {
4440
+ if (local.autofocus) inputElRef?.focus();
4441
+ });
4442
+ createEffect(on(() => local.autofocus, (v) => untrack(() => ((v) => {
4443
+ if (v) inputElRef?.focus();
4444
+ })(v)), { defer: true }));
4445
+ let inputElRef = null;
4415
4446
  setDraft(local.value != null ? String(local.value) : "");
4416
4447
  function onInput(e) {
4417
4448
  setDraft(e && e.target ? e.target.value : "");
@@ -4435,7 +4466,9 @@ function EditorText(_props) {
4435
4466
  doCommit();
4436
4467
  }
4437
4468
  return <>
4438
- <input type="text" data-editing-cell="" aria-label={local.columnId} class={"rdt-cell-editor"} value={draft()} onInput={($event) => {
4469
+ <input type="text" data-editing-cell="" aria-label={local.columnId} ref={(el) => {
4470
+ inputElRef = el;
4471
+ }} class={"rdt-cell-editor"} value={draft()} onInput={($event) => {
4439
4472
  onInput($event);
4440
4473
  }} onKeyDown={($event) => {
4441
4474
  onKeydown($event);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rozie-ui/data-table-solid",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -46,7 +46,7 @@
46
46
  ],
47
47
  "dependencies": {
48
48
  "@solid-primitives/keyed": "^1.5.3",
49
- "@rozie/runtime-solid": "0.1.3"
49
+ "@rozie/runtime-solid": "0.1.4"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "solid-js": "^1.8",
package/src/DataTable.tsx CHANGED
@@ -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
 
@@ -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);
@@ -5169,8 +5170,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5169
5170
  // draft (selecting the seeded char makes the next keystroke replace it: Zeta → eta).
5170
5171
  // beginEdit threads `seed == null` so a seeded entry skips the select and the caret sits
5171
5172
  // AFTER the seeded char; every other caller keeps the default select-all.
5173
+ // Editor-owns-focus contract (quick 260711-i5m): REVERTS the g52 shadow-piercing helper
5174
+ // (commit 5fa30045) that recursed into descendant shadow roots. Built-in editors are
5175
+ // host-DOM — the plain direct query resolves them on all 6 targets (no shadow to cross). A
5176
+ // #editor DROP-IN now owns its OWN focus via the reactive `autofocus` prop (EditorText's
5177
+ // $onMount + lazy $watch), so the host never needs to reach across a Lit drop-in's nested
5178
+ // shadow root at all — see the !hasEditorSlot gate below, which skips the host focus call
5179
+ // entirely for a drop-in target.
5172
5180
  function focusEditorWhenReady(selectAll = true) {
5173
5181
  if (!gridRoot) return;
5182
+ // Editor-owns-focus contract: when the CURRENT focus target is a #editor drop-in, the host
5183
+ // does NOT reach into its DOM — the drop-in self-focuses via its own autofocus prop.
5184
+ if (editFocusColId() != null && hasEditorSlot(editFocusColId())) return;
5174
5185
  let attempts = 0;
5175
5186
  const tryFocus = () => {
5176
5187
  const el = gridRoot ? gridRoot.querySelector('[data-editing-cell]') : null;
@@ -5246,6 +5257,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5246
5257
  setDraftValue(seed != null ? seed : cellValueAt(rowIndex, colIndex));
5247
5258
  setActiveInControl(true);
5248
5259
  setEditVer(editVer() + 1);
5260
+ // Editor-owns-focus contract (quick 260711-i5m): THIS cell's column is the current
5261
+ // focus target — editorAutofocusFor derives the reactive `autofocus` #editor scope prop
5262
+ // from it. Cleared on endEdit.
5263
+ setEditFocusColId(colId);
5249
5264
  // B2: a seeded (type-to-edit) entry must NOT select-all — keep the caret after the
5250
5265
  // seeded char so subsequent typing appends instead of replacing it.
5251
5266
  focusEditorWhenReady(seed == null);
@@ -5300,6 +5315,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5300
5315
  setInvalidMsg('');
5301
5316
  setActiveInControl(false);
5302
5317
  setEditVer(editVer() + 1);
5318
+ setEditFocusColId(null);
5303
5319
  }
5304
5320
 
5305
5321
  // endRowEdit: tear down full-row edit (shared by commitRow/cancelRow). Clears the row
@@ -5311,6 +5327,23 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5311
5327
  setInvalidMsg('');
5312
5328
  setActiveInControl(false);
5313
5329
  setEditVer(editVer() + 1);
5330
+ setEditFocusColId(null);
5331
+ }
5332
+
5333
+ // editorAutofocusFor (quick 260711-i5m, editor-owns-focus contract): the reactive `autofocus`
5334
+ // #editor scope prop for a given (colId, rowIndex) — true for EXACTLY the current focus-
5335
+ // target cell, re-deriving on every editVer bump (mirrors isEditing's reactive gate so
5336
+ // Svelte/Solid re-run this per-cell on a foreign-slot-callback state mutation). Works for
5337
+ // BOTH single-cell ($data.editingRow) and row mode ($data.editingRowIndex) since
5338
+ // $data.editFocusColId is set by both beginEdit and beginRowEdit/commitRow/rowEditTab.
5339
+ function editorAutofocusFor(colId: any, rowIndex: any) {
5340
+ if (editVer() < 0) return false;
5341
+ if (editingRowIndex() != null) {
5342
+ if (editingRowIndex() !== rowIndex) return false;
5343
+ } else {
5344
+ if (editingRow() !== rowIndex) return false;
5345
+ }
5346
+ return editFocusColId() != null && editFocusColId() === colId;
5314
5347
  }
5315
5348
 
5316
5349
  // B3: coerce the committed value by the column's built-in editor type at the single
@@ -5528,8 +5561,15 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5528
5561
  // focuses its [data-editing-cell] control. Bounded rAF-poll (mirrors focusEditorWhenReady)
5529
5562
  // so a React re-render that recreates the input across the focus call still lands it. select-
5530
5563
  // all on text/number editors (a no-op try/catch on select/checkbox).
5564
+ // Editor-owns-focus contract (quick 260711-i5m): when the TARGET column is a #editor
5565
+ // drop-in, the host does NOT reach into its DOM (early return, before starting the rAF poll
5566
+ // at all) — the drop-in self-focuses via its own reactive `autofocus` prop, which the caller
5567
+ // (commitRow's B22 reject path / rowEditTab) already flips via $data.editFocusColId. Built-in
5568
+ // columns are unaffected (hasEditorSlot is false for them) — unchanged host direct-focus.
5531
5569
  function focusRowEditorAt(rowIndex: any, colIndex: any) {
5532
5570
  if (!gridRoot) return;
5571
+ const colId = columnIdAt(rowIndex, colIndex);
5572
+ if (colId != null && hasEditorSlot(colId)) return;
5533
5573
  let attempts = 0;
5534
5574
  const tryFocus = () => {
5535
5575
  const cellEl = resolveCellEl(String(rowIndex), colIndex);
@@ -5581,6 +5621,11 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5581
5621
  setEditingRowIndex(rowIndex);
5582
5622
  setActiveInControl(true);
5583
5623
  setEditVer(editVer() + 1);
5624
+ // Editor-owns-focus contract (quick 260711-i5m): the row's FIRST editable column is the
5625
+ // initial focus target — editorAutofocusFor derives the reactive `autofocus` #editor scope
5626
+ // prop from it (a built-in column is also host-focused below via focusEditorWhenReady; a
5627
+ // drop-in column self-focuses via its own $onMount, gated off the host reach-in in Task 3).
5628
+ setEditFocusColId(editable[0].colId);
5584
5629
  focusEditorWhenReady();
5585
5630
  }
5586
5631
 
@@ -5613,9 +5658,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5613
5658
  const err = runValidator(ec.colId, coerceCellValue(ec.colId, draft[ec.colId]), rowOriginal);
5614
5659
  if (err !== true) {
5615
5660
  setInvalid(err);
5661
+ // Editor-owns-focus contract (quick 260711-i5m): the OFFENDING column becomes the new
5662
+ // reactive focus target BEFORE the host-focus call below — a #editor drop-in already
5663
+ // mounted (full-row edit opens every editable cell at once) picks this up via its own
5664
+ // lazy $watch on the `autofocus` scope prop flipping false→true. Bump editVer so the
5665
+ // coarse-render targets (React/Vue/Angular/Svelte) re-derive the slot binding (Solid's
5666
+ // fine-grained accessor re-runs without the bump, but the bump keeps all 6 in lockstep).
5667
+ setEditFocusColId(ec.colId);
5668
+ setEditVer(editVer() + 1);
5616
5669
  // B22: focus the OFFENDING column's editor (the one whose validator rejected), NOT
5617
5670
  // unconditionally the first editor (focusEditorWhenReady resolves the first
5618
5671
  // [data-editing-cell] in DOM order). ec.colIndex is the offending cell's visible col.
5672
+ // Gated (Task 3) so a #editor drop-in self-focuses instead of a host DOM reach-in.
5619
5673
  focusRowEditorAt(rowIndex, ec.colIndex);
5620
5674
  return false;
5621
5675
  }
@@ -5894,6 +5948,11 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5894
5948
  if (pos < 0) pos = 0;
5895
5949
  const len = cols.length;
5896
5950
  const nextPos = backward ? (pos - 1 + len) % len : (pos + 1) % len;
5951
+ // Editor-owns-focus contract (quick 260711-i5m): the Tab target becomes the new reactive
5952
+ // focus target BEFORE the host-focus call below, so Tab onto an already-mounted #editor
5953
+ // drop-in (row mode) also refocuses it via its own lazy $watch.
5954
+ setEditFocusColId(editable[nextPos].colId);
5955
+ setEditVer(editVer() + 1);
5897
5956
  focusRowEditorAt(rowIndex, cols[nextPos]);
5898
5957
  }
5899
5958
 
@@ -6426,7 +6485,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6426
6485
  {<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
6486
  <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
6487
  </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() })}
6488
+ {(_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
6489
  </span></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
6431
6490
  <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
6491
  <span class={"rdt-group-value"} data-rozie-s-d5dcab4c="">
@@ -6499,7 +6558,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6499
6558
  {<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
6559
  <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
6560
  </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() })}
6561
+ {(_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
6562
  </span></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
6504
6563
  <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
6564
  <span class={"rdt-group-value"} data-rozie-s-d5dcab4c="">
@@ -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
  }