@rozie-ui/data-table-lit 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -74
- package/dist/index.cjs +69 -2
- package/dist/index.d.cts +10 -7
- package/dist/index.d.mts +10 -7
- package/dist/index.mjs +69 -2
- package/package.json +2 -2
- package/src/Column.ts +2 -2
- package/src/DataTable.ts +222 -11
- package/src/themes/base.css +38 -25
- package/src/themes/bootstrap.css +11 -0
- package/src/themes/material.css +11 -0
- package/src/themes/shadcn.css +11 -0
package/dist/index.d.mts
CHANGED
|
@@ -6,14 +6,14 @@ declare class DataTable extends DataTable_base {
|
|
|
6
6
|
static shadowRootOptions: ShadowRootInit;
|
|
7
7
|
static styles: _$lit.CSSResult;
|
|
8
8
|
/**
|
|
9
|
-
* The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through `
|
|
9
|
+
* The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through the two-way `data` binding (uncontrolled fallback `dataDefault`). A stable reference per Rozie's setup-once model — fed directly into table-core (never map/cloned in the watcher).
|
|
10
10
|
* @example
|
|
11
11
|
* <rozie-data-table .data=${rows} @data-change=${…} .columns=${cols}></rozie-data-table>
|
|
12
12
|
*/
|
|
13
13
|
_data_attr: any[];
|
|
14
14
|
private _dataControllable;
|
|
15
15
|
/**
|
|
16
|
-
* Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union).
|
|
16
|
+
* Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union). A `pinned` value of `'left'` or `'right'` is applied once as the table's initial `columnPinning` state — identical to `<Column pinned>` — which also REORDERS that column, since table-core orders visible cells `[left-pinned, center, right-pinned]`. A consumer who has already expressed a pin (an initial two-way `columnPinning` model value or an interactive pin) owns the slice and this declaration is ignored.
|
|
17
17
|
*/
|
|
18
18
|
columns: any[];
|
|
19
19
|
/**
|
|
@@ -53,7 +53,7 @@ declare class DataTable extends DataTable_base {
|
|
|
53
53
|
*/
|
|
54
54
|
pageCount: number | null;
|
|
55
55
|
/**
|
|
56
|
-
* Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand).
|
|
56
|
+
* Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand). Pass `expandable` as a real boolean — a valueless attribute only coerces to `true` on Vue and Lit.
|
|
57
57
|
*/
|
|
58
58
|
expandable: boolean;
|
|
59
59
|
/**
|
|
@@ -70,7 +70,7 @@ declare class DataTable extends DataTable_base {
|
|
|
70
70
|
*/
|
|
71
71
|
groupable: boolean;
|
|
72
72
|
/**
|
|
73
|
-
* `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding `
|
|
73
|
+
* `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding the two-way `grouping` model (a non-null `[]` default would short-circuit it). All reads are null-guarded, so table-core still receives an array.
|
|
74
74
|
*/
|
|
75
75
|
_grouping_attr: any[] | null;
|
|
76
76
|
private _groupingControllable;
|
|
@@ -104,7 +104,7 @@ declare class DataTable extends DataTable_base {
|
|
|
104
104
|
*/
|
|
105
105
|
stickyHeader: boolean;
|
|
106
106
|
/**
|
|
107
|
-
* `'table'` (default, row-oriented, byte-behaviorally identical to a plain accessible table) | `'grid'`
|
|
107
|
+
* `'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.
|
|
108
108
|
*/
|
|
109
109
|
interactionMode: string;
|
|
110
110
|
/**
|
|
@@ -305,6 +305,8 @@ declare class DataTable extends DataTable_base {
|
|
|
305
305
|
expandedTouched: boolean;
|
|
306
306
|
groupingActiveDefault: () => boolean;
|
|
307
307
|
effectiveColumnPinning: () => any;
|
|
308
|
+
pinSeedApplied: boolean;
|
|
309
|
+
seedColumnPinning: () => void;
|
|
308
310
|
currentState: () => any;
|
|
309
311
|
currentData: () => any;
|
|
310
312
|
buildConfigDef: (c: any) => {
|
|
@@ -385,6 +387,7 @@ declare class DataTable extends DataTable_base {
|
|
|
385
387
|
onColumnSizingChangeCb: (updater: any) => void;
|
|
386
388
|
onColumnOrderChangeCb: (updater: any) => void;
|
|
387
389
|
onColumnPinningChangeCb: (updater: any) => void;
|
|
390
|
+
columnSizingInfoSync: any;
|
|
388
391
|
onColumnSizingInfoChangeCb: (updater: any) => void;
|
|
389
392
|
resolveVirtual: () => "rows" | "columns" | "both" | "off";
|
|
390
393
|
rowsWindowed: () => boolean;
|
|
@@ -779,7 +782,7 @@ declare class Column extends Column_base {
|
|
|
779
782
|
*/
|
|
780
783
|
filterable: boolean;
|
|
781
784
|
/**
|
|
782
|
-
*
|
|
785
|
+
* Initial pin side: `''` (unpinned) | `'left'` | `'right'`. Applied once as the table's starting `columnPinning` state, so `getIsPinned()` reports it and the column joins the matching sticky rail — which also reorders it, since visible cells are ordered `[left-pinned, center, right-pinned]`. Ignored if the consumer has already pinned something; an interactive unpin is never re-applied.
|
|
783
786
|
*/
|
|
784
787
|
pinned: string;
|
|
785
788
|
/**
|
|
@@ -803,7 +806,7 @@ declare class Column extends Column_base {
|
|
|
803
806
|
*/
|
|
804
807
|
editable: boolean;
|
|
805
808
|
/**
|
|
806
|
-
*
|
|
809
|
+
* Editor type for this column: `'text'` | `'number'` | `'select'` | `'checkbox'` (built-in inputs), or `'custom'` to hand rendering to the `#editor` scoped slot (or a per-column `editor-<columnId>` fill). Default `'text'`. **`'custom'` is the gate:** a column left on a built-in type ignores any `#editor` fill and renders the built-in input. Requires `editable: true` — and, for any pointer or keyboard edit entry, `interactionMode="grid"` on the table.
|
|
807
810
|
*/
|
|
808
811
|
editor: string;
|
|
809
812
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -439,6 +439,34 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
|
|
|
439
439
|
left: rail.concat(deduped)
|
|
440
440
|
};
|
|
441
441
|
};
|
|
442
|
+
this.pinSeedApplied = false;
|
|
443
|
+
this.seedColumnPinning = () => {
|
|
444
|
+
if (this.pinSeedApplied) return;
|
|
445
|
+
const live = this.columnPinning != null ? this.columnPinning : this._columnPinningDefault.value;
|
|
446
|
+
const isRealPin = (id) => id !== this.SELECT_COL_ID && id !== this.EXPANDER_COL_ID;
|
|
447
|
+
if ((live && live.left ? live.left : []).filter(isRealPin).length > 0 || (live && live.right ? live.right : []).filter(isRealPin).length > 0) {
|
|
448
|
+
this.pinSeedApplied = true;
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const defs = this.columnDefs();
|
|
452
|
+
if (!defs.length) return;
|
|
453
|
+
const left = [];
|
|
454
|
+
const right = [];
|
|
455
|
+
for (let i = 0; i < defs.length; i++) {
|
|
456
|
+
const def = defs[i];
|
|
457
|
+
if (!def) continue;
|
|
458
|
+
if (def.pinned === "left") left.push(def.id);
|
|
459
|
+
else if (def.pinned === "right") right.push(def.id);
|
|
460
|
+
}
|
|
461
|
+
if (!left.length && !right.length) return;
|
|
462
|
+
this.pinSeedApplied = true;
|
|
463
|
+
const seeded = {
|
|
464
|
+
left,
|
|
465
|
+
right
|
|
466
|
+
};
|
|
467
|
+
this._columnPinningDefault.value = seeded;
|
|
468
|
+
this._columnPinningControllable.write(seeded);
|
|
469
|
+
};
|
|
442
470
|
this.currentState = () => ({
|
|
443
471
|
sorting: this.sorting != null ? this.sorting : this._sortingDefault.value,
|
|
444
472
|
globalFilter: this.globalFilter != null ? this.globalFilter : this._globalFilterDefault.value,
|
|
@@ -835,9 +863,12 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
|
|
|
835
863
|
this.onColumnPinningChangeCb = (updater) => {
|
|
836
864
|
this.writeColumnPinning(applyUpdater(updater, this.currentState().columnPinning));
|
|
837
865
|
};
|
|
866
|
+
this.columnSizingInfoSync = null;
|
|
838
867
|
this.onColumnSizingInfoChangeCb = (updater) => {
|
|
839
|
-
const next = applyUpdater(updater, this._columnSizingInfo.value);
|
|
840
|
-
|
|
868
|
+
const next = applyUpdater(updater, this.columnSizingInfoSync != null ? this.columnSizingInfoSync : this._columnSizingInfo.value);
|
|
869
|
+
if (next == null) return;
|
|
870
|
+
this.columnSizingInfoSync = next;
|
|
871
|
+
this._columnSizingInfo.value = next;
|
|
841
872
|
};
|
|
842
873
|
this.resolveVirtual = () => {
|
|
843
874
|
const v = this.virtual;
|
|
@@ -2976,6 +3007,11 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
|
|
|
2976
3007
|
const tryFocus = () => {
|
|
2977
3008
|
const el = this.resolveCellEl(String(row), col);
|
|
2978
3009
|
if (el) {
|
|
3010
|
+
const ae = this.gridRoot && this.gridRoot.getRootNode ? this.gridRoot.getRootNode().activeElement : null;
|
|
3011
|
+
if (ae && ae !== el && this.gridRoot.contains && this.gridRoot.contains(ae) && ae.closest) {
|
|
3012
|
+
const aeCell = ae.closest("[data-grid-cell]");
|
|
3013
|
+
if (aeCell && aeCell !== el) return;
|
|
3014
|
+
}
|
|
2979
3015
|
el.focus();
|
|
2980
3016
|
return;
|
|
2981
3017
|
}
|
|
@@ -3596,6 +3632,34 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
|
|
|
3596
3632
|
static {
|
|
3597
3633
|
this.styles = css`
|
|
3598
3634
|
:host{display:contents}
|
|
3635
|
+
:host {
|
|
3636
|
+
--rdt-font: var(--rozie-data-table-font);
|
|
3637
|
+
--rdt-color: var(--rozie-data-table-fg);
|
|
3638
|
+
--rdt-cell-padding: var(--rozie-data-table-cell-padding);
|
|
3639
|
+
--rdt-border: 1px solid var(--rozie-data-table-border-color);
|
|
3640
|
+
--rdt-header-bg: var(--rozie-data-table-header-bg);
|
|
3641
|
+
--rdt-header-weight: var(--rozie-data-table-header-weight);
|
|
3642
|
+
--rdt-sort-ind-opacity: var(--rozie-data-table-sort-indicator-opacity);
|
|
3643
|
+
--rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
3644
|
+
--rdt-filter-radius: var(--rozie-data-table-radius);
|
|
3645
|
+
--rdt-page-btn-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
3646
|
+
--rdt-page-btn-radius: var(--rozie-data-table-radius);
|
|
3647
|
+
--rdt-page-btn-disabled-opacity: var(--rozie-data-table-disabled-opacity);
|
|
3648
|
+
--rdt-page-size-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
3649
|
+
--rdt-page-size-radius: var(--rozie-data-table-radius);
|
|
3650
|
+
--rdt-resize-grip-color: var(--rozie-data-table-resize-grip-color);
|
|
3651
|
+
--rdt-resize-grip-active: var(--rozie-data-table-resize-grip-active);
|
|
3652
|
+
--rdt-pin-btn-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
3653
|
+
--rdt-pin-btn-radius: var(--rozie-data-table-radius);
|
|
3654
|
+
--rdt-pin-btn-active-bg: var(--rozie-data-table-pin-active-bg);
|
|
3655
|
+
--rdt-colvis-summary-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
3656
|
+
--rdt-colvis-summary-radius: var(--rozie-data-table-radius);
|
|
3657
|
+
--rdt-colvis-menu-border: 1px solid var(--rozie-data-table-border-color);
|
|
3658
|
+
--rdt-colvis-menu-radius: var(--rozie-data-table-radius);
|
|
3659
|
+
--rdt-colvis-menu-bg: var(--rozie-data-table-menu-bg);
|
|
3660
|
+
--rdt-colvis-menu-shadow: var(--rozie-data-table-menu-shadow);
|
|
3661
|
+
--rdt-select-accent: var(--rozie-data-table-select-accent);
|
|
3662
|
+
}
|
|
3599
3663
|
.rozie-data-table[data-rozie-s-d5dcab4c] {
|
|
3600
3664
|
border-collapse: collapse;
|
|
3601
3665
|
width: 100%;
|
|
@@ -4107,6 +4171,7 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
|
|
|
4107
4171
|
return;
|
|
4108
4172
|
}
|
|
4109
4173
|
(() => {
|
|
4174
|
+
this.seedColumnPinning();
|
|
4110
4175
|
this.reFeed();
|
|
4111
4176
|
this.maybeClearHistoryOnExternalSwap();
|
|
4112
4177
|
})();
|
|
@@ -4146,6 +4211,7 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
|
|
|
4146
4211
|
}))(this));
|
|
4147
4212
|
}));
|
|
4148
4213
|
this._dataDefault.value = this.data || [];
|
|
4214
|
+
this.seedColumnPinning();
|
|
4149
4215
|
this.table = createTable({
|
|
4150
4216
|
data: this.currentData(),
|
|
4151
4217
|
columns: this.tableColumns(),
|
|
@@ -4240,6 +4306,7 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
|
|
|
4240
4306
|
}
|
|
4241
4307
|
if (this.colsWindowed()) {
|
|
4242
4308
|
if (!(this.gridScrollEl ? this.gridScrollEl.clientWidth : 0)) console.warn("[rozie-data-table] virtual is on for columns but the scroll container has no bounded width; set a CSS width on an ancestor so the column window can be measured");
|
|
4309
|
+
this.bumpWindowVer();
|
|
4243
4310
|
}
|
|
4244
4311
|
};
|
|
4245
4312
|
if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(afterFirstFrame));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rozie-ui/data-table-lit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"src"
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@rozie/runtime-lit": "0.7.
|
|
48
|
+
"@rozie/runtime-lit": "^0.7.4"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"lit": "^3.2",
|
package/src/Column.ts
CHANGED
|
@@ -34,7 +34,7 @@ export default class Column extends SignalWatcher(LitElement) {
|
|
|
34
34
|
*/
|
|
35
35
|
@property({ type: Boolean, reflect: true }) filterable: boolean = false;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* Initial pin side: `''` (unpinned) | `'left'` | `'right'`. Applied once as the table's starting `columnPinning` state, so `getIsPinned()` reports it and the column joins the matching sticky rail — which also reorders it, since visible cells are ordered `[left-pinned, center, right-pinned]`. Ignored if the consumer has already pinned something; an interactive unpin is never re-applied.
|
|
38
38
|
*/
|
|
39
39
|
@property({ type: String, reflect: true }) pinned: string = '';
|
|
40
40
|
/**
|
|
@@ -58,7 +58,7 @@ export default class Column extends SignalWatcher(LitElement) {
|
|
|
58
58
|
*/
|
|
59
59
|
@property({ type: Boolean, reflect: true }) editable: boolean = false;
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
61
|
+
* Editor type for this column: `'text'` | `'number'` | `'select'` | `'checkbox'` (built-in inputs), or `'custom'` to hand rendering to the `#editor` scoped slot (or a per-column `editor-<columnId>` fill). Default `'text'`. **`'custom'` is the gate:** a column left on a built-in type ignores any `#editor` fill and renders the built-in input. Requires `editable: true` — and, for any pointer or keyboard edit entry, `interactionMode="grid"` on the table.
|
|
62
62
|
*/
|
|
63
63
|
@property({ type: String, reflect: true }) editor: string = 'text';
|
|
64
64
|
/**
|
package/src/DataTable.ts
CHANGED
|
@@ -130,6 +130,34 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
130
130
|
|
|
131
131
|
static styles = css`
|
|
132
132
|
:host{display:contents}
|
|
133
|
+
:host {
|
|
134
|
+
--rdt-font: var(--rozie-data-table-font);
|
|
135
|
+
--rdt-color: var(--rozie-data-table-fg);
|
|
136
|
+
--rdt-cell-padding: var(--rozie-data-table-cell-padding);
|
|
137
|
+
--rdt-border: 1px solid var(--rozie-data-table-border-color);
|
|
138
|
+
--rdt-header-bg: var(--rozie-data-table-header-bg);
|
|
139
|
+
--rdt-header-weight: var(--rozie-data-table-header-weight);
|
|
140
|
+
--rdt-sort-ind-opacity: var(--rozie-data-table-sort-indicator-opacity);
|
|
141
|
+
--rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
142
|
+
--rdt-filter-radius: var(--rozie-data-table-radius);
|
|
143
|
+
--rdt-page-btn-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
144
|
+
--rdt-page-btn-radius: var(--rozie-data-table-radius);
|
|
145
|
+
--rdt-page-btn-disabled-opacity: var(--rozie-data-table-disabled-opacity);
|
|
146
|
+
--rdt-page-size-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
147
|
+
--rdt-page-size-radius: var(--rozie-data-table-radius);
|
|
148
|
+
--rdt-resize-grip-color: var(--rozie-data-table-resize-grip-color);
|
|
149
|
+
--rdt-resize-grip-active: var(--rozie-data-table-resize-grip-active);
|
|
150
|
+
--rdt-pin-btn-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
151
|
+
--rdt-pin-btn-radius: var(--rozie-data-table-radius);
|
|
152
|
+
--rdt-pin-btn-active-bg: var(--rozie-data-table-pin-active-bg);
|
|
153
|
+
--rdt-colvis-summary-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
154
|
+
--rdt-colvis-summary-radius: var(--rozie-data-table-radius);
|
|
155
|
+
--rdt-colvis-menu-border: 1px solid var(--rozie-data-table-border-color);
|
|
156
|
+
--rdt-colvis-menu-radius: var(--rozie-data-table-radius);
|
|
157
|
+
--rdt-colvis-menu-bg: var(--rozie-data-table-menu-bg);
|
|
158
|
+
--rdt-colvis-menu-shadow: var(--rozie-data-table-menu-shadow);
|
|
159
|
+
--rdt-select-accent: var(--rozie-data-table-select-accent);
|
|
160
|
+
}
|
|
133
161
|
.rozie-data-table[data-rozie-s-d5dcab4c] {
|
|
134
162
|
border-collapse: collapse;
|
|
135
163
|
width: 100%;
|
|
@@ -467,14 +495,14 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
467
495
|
`;
|
|
468
496
|
|
|
469
497
|
/**
|
|
470
|
-
* The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through `
|
|
498
|
+
* The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through the two-way `data` binding (uncontrolled fallback `dataDefault`). A stable reference per Rozie's setup-once model — fed directly into table-core (never map/cloned in the watcher).
|
|
471
499
|
* @example
|
|
472
500
|
* <rozie-data-table .data=${rows} @data-change=${…} .columns=${cols}></rozie-data-table>
|
|
473
501
|
*/
|
|
474
502
|
@property({ type: Array, attribute: 'data' }) _data_attr!: any[];
|
|
475
503
|
private _dataControllable = createLitControllableProperty<any[]>({ host: this, eventName: 'data-change', defaultValue: [], initialControlledValue: undefined });
|
|
476
504
|
/**
|
|
477
|
-
* Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union).
|
|
505
|
+
* Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union). A `pinned` value of `'left'` or `'right'` is applied once as the table's initial `columnPinning` state — identical to `<Column pinned>` — which also REORDERS that column, since table-core orders visible cells `[left-pinned, center, right-pinned]`. A consumer who has already expressed a pin (an initial two-way `columnPinning` model value or an interactive pin) owns the slice and this declaration is ignored.
|
|
478
506
|
*/
|
|
479
507
|
@property({ type: Array }) columns: any[] = [];
|
|
480
508
|
/**
|
|
@@ -520,7 +548,7 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
520
548
|
*/
|
|
521
549
|
@property({ type: Number, reflect: true }) pageCount: number | null = null;
|
|
522
550
|
/**
|
|
523
|
-
* Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand).
|
|
551
|
+
* Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand). Pass `expandable` as a real boolean — a valueless attribute only coerces to `true` on Vue and Lit.
|
|
524
552
|
*/
|
|
525
553
|
@property({ type: Boolean, reflect: true }) expandable: boolean = false;
|
|
526
554
|
/**
|
|
@@ -537,7 +565,7 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
537
565
|
*/
|
|
538
566
|
@property({ type: Boolean, reflect: true }) groupable: boolean = false;
|
|
539
567
|
/**
|
|
540
|
-
* `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding `
|
|
568
|
+
* `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding the two-way `grouping` model (a non-null `[]` default would short-circuit it). All reads are null-guarded, so table-core still receives an array.
|
|
541
569
|
*/
|
|
542
570
|
@property({ type: Array, attribute: 'grouping' }) _grouping_attr: any[] | null = null;
|
|
543
571
|
private _groupingControllable = createLitControllableProperty<any[]>({ host: this, eventName: 'grouping-change', defaultValue: null, initialControlledValue: undefined });
|
|
@@ -577,7 +605,7 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
577
605
|
*/
|
|
578
606
|
@property({ type: Boolean, reflect: true }) stickyHeader: boolean = false;
|
|
579
607
|
/**
|
|
580
|
-
* `'table'` (default, row-oriented, byte-behaviorally identical to a plain accessible table) | `'grid'`
|
|
608
|
+
* `'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.
|
|
581
609
|
*/
|
|
582
610
|
@property({ type: String, reflect: true }) interactionMode: string = 'table';
|
|
583
611
|
/**
|
|
@@ -912,6 +940,12 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
912
940
|
// (Consumers memoize the array as with $props.data/$props.sorting; the uncontrolled
|
|
913
941
|
// <Column>-children path leaves $props.columns undefined — a stable no-op getter.)
|
|
914
942
|
this.columns, this._colReg.value])(); untracked(() => { if (this.__rozieWatchInitial_0) { this.__rozieWatchInitial_0 = false; return; } (() => {
|
|
943
|
+
// Seed BEFORE the re-feed so the pinning this call may write is part of the state object
|
|
944
|
+
// table-core receives on this same tick (rather than landing a frame late and re-ordering
|
|
945
|
+
// getVisibleCells() a render after the columns appear). One-shot and self-latching — see
|
|
946
|
+
// seedColumnPinning() in stateAssembly.rzts. This is the `<Column>`-children entry point:
|
|
947
|
+
// those register during mount, so the $onMount call below can find zero columns.
|
|
948
|
+
this.seedColumnPinning();
|
|
915
949
|
this.reFeed();
|
|
916
950
|
this.maybeClearHistoryOnExternalSwap();
|
|
917
951
|
})(); }); }));
|
|
@@ -945,6 +979,19 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
945
979
|
// a one-way `:data`) has a base array to whole-array-replace. currentData() then sources
|
|
946
980
|
// the bound prop when controlled, this fallback otherwise.
|
|
947
981
|
this._dataDefault.value = this.data || [];
|
|
982
|
+
// Apply any per-column `pinned` declaration as the INITIAL pinning state BEFORE createTable
|
|
983
|
+
// reads currentState() below, so the first row model table-core builds is already correctly
|
|
984
|
+
// ordered (getVisibleCells() returns [left-pinned, center, right-pinned]) rather than
|
|
985
|
+
// re-ordering a render later. Covers the `:columns` config-array form, which is available
|
|
986
|
+
// now; `<Column>` children register during mount and are covered by the re-feed watch's own
|
|
987
|
+
// call. One-shot and self-latching — see seedColumnPinning() in stateAssembly.rzts.
|
|
988
|
+
// Apply any per-column `pinned` declaration as the INITIAL pinning state BEFORE createTable
|
|
989
|
+
// reads currentState() below, so the first row model table-core builds is already correctly
|
|
990
|
+
// ordered (getVisibleCells() returns [left-pinned, center, right-pinned]) rather than
|
|
991
|
+
// re-ordering a render later. Covers the `:columns` config-array form, which is available
|
|
992
|
+
// now; `<Column>` children register during mount and are covered by the re-feed watch's own
|
|
993
|
+
// call. One-shot and self-latching — see seedColumnPinning() in stateAssembly.rzts.
|
|
994
|
+
this.seedColumnPinning();
|
|
948
995
|
// Build the table instance HERE so the closures below capture the live `table`.
|
|
949
996
|
// Build the table instance HERE so the closures below capture the live `table`.
|
|
950
997
|
this.table = createTable({
|
|
@@ -1267,6 +1314,31 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1267
1314
|
if (!w) {
|
|
1268
1315
|
console.warn('[rozie-data-table] virtual is on for columns but the scroll container has no bounded width; set a CSS width on an ancestor so the column window can be measured');
|
|
1269
1316
|
}
|
|
1317
|
+
// COLUMN-AXIS FIRST-WINDOW BUMP (quick 260908-vcy). The mount-kick above fires right
|
|
1318
|
+
// after `new Virtualizer(...)` — BEFORE colVirtualizer has measured, and before the
|
|
1319
|
+
// spacer effects even exist. Any fine-grained subscriber created inside that measure
|
|
1320
|
+
// gap takes colPadLeft()/colPadRight()'s `if (!items.length) return 0` early return,
|
|
1321
|
+
// which reads ONLY windowVer + editVer and never reaches the live path's reads
|
|
1322
|
+
// (forcedColumns()/columnSize()/column state). Its dependency set is therefore those
|
|
1323
|
+
// two counters alone — and without this bump nothing ever touches them again, so the
|
|
1324
|
+
// subscriber stays frozen at 0 for the lifetime of the component. Measured on Svelte:
|
|
1325
|
+
// the FIRST header row's trailing spacer froze at 0px while its siblings (created
|
|
1326
|
+
// after measurement, on the live path) tracked correctly; under table-layout:fixed the
|
|
1327
|
+
// first row alone establishes column widths, so that one cell collapsed the whole
|
|
1328
|
+
// table's scrollWidth from 9000 to 1350. The five other targets re-render wholesale
|
|
1329
|
+
// (Solid re-creates these rows), which masked it.
|
|
1330
|
+
//
|
|
1331
|
+
// The ROW axis has always been safe here only by accident: remeasureWindow() above
|
|
1332
|
+
// already routes through bumpWindowVer() on this same post-first-frame callback. This
|
|
1333
|
+
// is the column axis's missing equivalent — not a Svelte workaround (the emitted
|
|
1334
|
+
// Svelte binding is a correct reactive template_effect; the dependency set is what was
|
|
1335
|
+
// wrong), so it is fixed once, in the shared engine's consumer, for all six targets.
|
|
1336
|
+
//
|
|
1337
|
+
// Routed through bumpWindowVer() rather than a raw `$data.windowVer = $data.windowVer + 1`
|
|
1338
|
+
// to keep 87-10's microtask coalescing AND to avoid manufacturing the ROZ138
|
|
1339
|
+
// false positive that a second in-body counter self-read would trip (see the
|
|
1340
|
+
// mount-kick's own comment above).
|
|
1341
|
+
this.bumpWindowVer();
|
|
1270
1342
|
}
|
|
1271
1343
|
};
|
|
1272
1344
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => requestAnimationFrame(afterFirstFrame));else setTimeout(afterFirstFrame, 0);
|
|
@@ -1287,12 +1359,12 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1287
1359
|
this.maybeClearHistoryOnExternalSwap();
|
|
1288
1360
|
if (!this.table) return;
|
|
1289
1361
|
// Phase 51 req-4: track currentData() (the bound prop OR the uncontrolled
|
|
1290
|
-
// $data.dataDefault) so a committed edit re-feeds on Lit whether or not
|
|
1291
|
-
// bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
|
|
1362
|
+
// $data.dataDefault) so a committed edit re-feeds on Lit whether or not the two-way
|
|
1363
|
+
// `data` model is bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
|
|
1292
1364
|
// identical length) still re-feeds.
|
|
1293
1365
|
// Phase 51 req-4: track currentData() (the bound prop OR the uncontrolled
|
|
1294
|
-
// $data.dataDefault) so a committed edit re-feeds on Lit whether or not
|
|
1295
|
-
// bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
|
|
1366
|
+
// $data.dataDefault) so a committed edit re-feeds on Lit whether or not the two-way
|
|
1367
|
+
// `data` model is bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
|
|
1296
1368
|
// identical length) still re-feeds.
|
|
1297
1369
|
const d = this.currentData() || [];
|
|
1298
1370
|
if (d === this.lastData && d.length === this.lastDataLen) return;
|
|
@@ -1733,6 +1805,96 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1733
1805
|
};
|
|
1734
1806
|
};
|
|
1735
1807
|
|
|
1808
|
+
// seedColumnPinning(): apply the per-column `pinned` declaration ('left' | 'right') — from the
|
|
1809
|
+
// `:columns` config array or a `<Column pinned="...">` child — as the INITIAL columnPinning
|
|
1810
|
+
// state, exactly once.
|
|
1811
|
+
//
|
|
1812
|
+
// Until now that field was carried onto every ColumnDef by columnBuilders.rzts and then read by
|
|
1813
|
+
// nobody: `column.getIsPinned()` returned false for a column declared `pinned="left"`, so the
|
|
1814
|
+
// documented per-column API was inert while `r-model:columnPinning` was the only mechanism that
|
|
1815
|
+
// actually pinned anything.
|
|
1816
|
+
//
|
|
1817
|
+
// Three invariants, each load-bearing:
|
|
1818
|
+
// 1. ONE-SHOT, LATCHED ON THE OUTCOME — never merely on "we ran". The latch flips only when
|
|
1819
|
+
// a seed is actually applied, or when a consumer pin is observed; a call that finds no
|
|
1820
|
+
// declared pins leaves it DOWN and will look again on the next re-feed. Latching on the
|
|
1821
|
+
// first call that saw any column at all was wrong, and Lit proved it: `<Column>` children
|
|
1822
|
+
// register INCREMENTALLY there, one re-feed per child, so the first call sees only
|
|
1823
|
+
// `name` — no pins declared yet — and a run-once latch would disable the seed
|
|
1824
|
+
// permanently before `amount pinned="right"` ever registered. The other five targets
|
|
1825
|
+
// batch all children into a single flush, which hid the race entirely. Once a seed HAS
|
|
1826
|
+
// been applied the latch holds, so a subsequent interactive UNPIN is never re-seeded —
|
|
1827
|
+
// which is the whole reason this is not folded into effectiveColumnPinning().
|
|
1828
|
+
// 2. ONLY WHEN NOTHING IS PINNED YET. Note this is NOT an `$props.columnPinning == null`
|
|
1829
|
+
// check: the prop carries `default: () => ({ left: [], right: [] })`, so it is never null
|
|
1830
|
+
// and a bound-vs-unbound test is impossible here. The meaningful question is whether the
|
|
1831
|
+
// consumer has actually expressed a pin — if they have, they own the slice and the
|
|
1832
|
+
// per-column declaration must not fight it; if the state is empty, `pinned` IS the
|
|
1833
|
+
// initial state and applying it is the whole point.
|
|
1834
|
+
// 3. NO CHROME IDS. The select/expander rail is injected by effectiveColumnPinning() on read
|
|
1835
|
+
// and stripped by writeColumnPinning() on write; the persisted state must never contain
|
|
1836
|
+
// them. Seeding only real column ids keeps that discipline.
|
|
1837
|
+
//
|
|
1838
|
+
// Writes BOTH the uncontrolled holder and `$model.columnPinning` so a consumer with a two-way
|
|
1839
|
+
// binding sees the pinning that is actually in effect rather than a stale empty object — but
|
|
1840
|
+
// does NOT emit `pin-change`: this is initial state derived from the consumer's own markup, not
|
|
1841
|
+
// a user action, and firing a change event for it on mount would be noise.
|
|
1842
|
+
//
|
|
1843
|
+
// Called from $onMount (covers the `:columns` config-array form, known at mount) AND from the
|
|
1844
|
+
// re-feed watch (covers `<Column>` children, which register during mount, and a runtime
|
|
1845
|
+
// `:columns` swap that arrives before any pin interaction). The `!defs.length` bail is what
|
|
1846
|
+
// makes the mount call harmless when columns are not known yet — the latch stays down and the
|
|
1847
|
+
// next re-feed seeds instead.
|
|
1848
|
+
pinSeedApplied = false;
|
|
1849
|
+
|
|
1850
|
+
seedColumnPinning = (): void => {
|
|
1851
|
+
if (this.pinSeedApplied) return;
|
|
1852
|
+
// Invariant 2: bail if the consumer already pinned something (either an initial bound value
|
|
1853
|
+
// or an interaction that beat us here). Read the same source order effectiveColumnPinning()
|
|
1854
|
+
// does, minus the chrome rail it injects on read — the rail is structural, not a consumer pin.
|
|
1855
|
+
const live = this.columnPinning != null ? this.columnPinning : this._columnPinningDefault.value;
|
|
1856
|
+
const isRealPin = (id: string) => id !== this.SELECT_COL_ID && id !== this.EXPANDER_COL_ID;
|
|
1857
|
+
const pinnedAlready = (live && live.left ? live.left : []).filter(isRealPin).length > 0 || (live && live.right ? live.right : []).filter(isRealPin).length > 0;
|
|
1858
|
+
if (pinnedAlready) {
|
|
1859
|
+
this.pinSeedApplied = true;
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
const defs = this.columnDefs();
|
|
1863
|
+
if (!defs.length) return;
|
|
1864
|
+
const left: string[] = [];
|
|
1865
|
+
const right: string[] = [];
|
|
1866
|
+
for (let i = 0; i < defs.length; i++) {
|
|
1867
|
+
const def = defs[i];
|
|
1868
|
+
if (!def) continue;
|
|
1869
|
+
if (def.pinned === 'left') left.push(def.id);else if (def.pinned === 'right') right.push(def.id);
|
|
1870
|
+
}
|
|
1871
|
+
// Invariant 1: no declared pins in the column set we can currently SEE is not evidence there
|
|
1872
|
+
// are none — on Lit the set is still filling in. Leave the latch down and try again on the
|
|
1873
|
+
// next re-feed; the only exits are "seeded" (below) and "consumer already pinned" (above).
|
|
1874
|
+
if (!left.length && !right.length) return;
|
|
1875
|
+
this.pinSeedApplied = true;
|
|
1876
|
+
// Nothing was pinned (invariant 2 above), so this is a plain assignment, not a merge — the
|
|
1877
|
+
// only ids that could be in `live` are the structural chrome rail, which must never be
|
|
1878
|
+
// persisted (invariant 3). Writes the uncontrolled holder AND the two-way model so both
|
|
1879
|
+
// binding modes observe the pinning that is actually in effect; deliberately does NOT route
|
|
1880
|
+
// through writeColumnPinning(), whose job is to emit `pin-change` for real user actions.
|
|
1881
|
+
// Annotated `any` for the same reason currentState() is (see its comment below): the
|
|
1882
|
+
// uncontrolled holder's initial value is the empty literal `{ left: [], right: [] }`, so the
|
|
1883
|
+
// emitted per-target state holder infers `{ left: never[]; right: never[] }` and a genuinely
|
|
1884
|
+
// populated `{ left: string[]; right: string[] }` will not assign to it under the strict
|
|
1885
|
+
// bundled-leaf tsc (react TS2345 on setColumnPinningDefault, solid TS2769, lit TS2322 — one
|
|
1886
|
+
// per target, all the same never[] inference). The runtime shape is correct; this is the
|
|
1887
|
+
// established idiom here rather than widening three per-leaf strict baselines for it.
|
|
1888
|
+
// writeColumnPinning() never hits this because its payload flows through `strip()`, which
|
|
1889
|
+
// returns `any[]`.
|
|
1890
|
+
const seeded: any = {
|
|
1891
|
+
left,
|
|
1892
|
+
right
|
|
1893
|
+
};
|
|
1894
|
+
this._columnPinningDefault.value = seeded;
|
|
1895
|
+
this._columnPinningControllable.write(seeded);
|
|
1896
|
+
};
|
|
1897
|
+
|
|
1736
1898
|
// Assemble the live state object from bound r-model slices (?? uncontrolled fallback).
|
|
1737
1899
|
// All NINE slices are wired (each ?? its own $data.<slice>Default). table-core reads
|
|
1738
1900
|
// this whole object as `state`. Return type annotated `any`: the inferred object-literal
|
|
@@ -2467,9 +2629,43 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2467
2629
|
this.writeColumnPinning(applyUpdater(updater, this.currentState().columnPinning));
|
|
2468
2630
|
};
|
|
2469
2631
|
|
|
2632
|
+
// columnSizingInfo is the ONLY slice table-core drives as a rapid, multi-call gesture: a resize
|
|
2633
|
+
// drag fires this updater once on pointerdown (seeding startOffset/startSize/isResizingColumn)
|
|
2634
|
+
// and then again on EVERY mousemove, each call reading the values the previous one wrote.
|
|
2635
|
+
//
|
|
2636
|
+
// That read-your-own-last-write requirement is what makes a plain `$data` read wrong here on
|
|
2637
|
+
// React. These callbacks are handed to createTable in $onMount, so on React the closure binds a
|
|
2638
|
+
// MOUNT-TIME `$data.columnSizingInfo` and never sees its own writes. Measured on the shipped
|
|
2639
|
+
// leaf (quick 260909, instrumented build): pointerdown correctly wrote
|
|
2640
|
+
// `{ startOffset: 163, isResizingColumn: 'col0' }`, and then every single mousemove still saw
|
|
2641
|
+
// `startOffset: null, isResizingColumn: false` — so it computed deltaOffset from 0 instead of
|
|
2642
|
+
// 163 (179, 195, 211… instead of 16, 32, 48…) AND wrote `isResizingColumn: false` back, wiping
|
|
2643
|
+
// the gesture on the first move. Net effect: column resizing did nothing at all on React while
|
|
2644
|
+
// working on the other five. It was masked because the one VR case that drives a real drag was
|
|
2645
|
+
// `test.fixme`'d on React as a presumed harness limitation (87-05 Task 3) — a real CDP drag
|
|
2646
|
+
// with intermediate steps fails there too, which is what proved it a product bug.
|
|
2647
|
+
//
|
|
2648
|
+
// Why not just read `currentState().columnSizingInfo` like the eleven siblings above: that
|
|
2649
|
+
// idiom relies on the re-feed $watch re-passing fresh callbacks into setOptions, and that watch
|
|
2650
|
+
// fires on data/column changes — NOT on every render, and never mid-gesture. It is sufficient
|
|
2651
|
+
// for one-shot updaters (a sort toggle, a selection click) and insufficient here, where
|
|
2652
|
+
// table-core issues a burst of updaters between any two re-feeds.
|
|
2653
|
+
//
|
|
2654
|
+
// Fix: a synchronous module-scope mirror, written on the same tick as the state. Reads prefer
|
|
2655
|
+
// the mirror, so each updater in a gesture composes on the previous one's result regardless of
|
|
2656
|
+
// when the host framework commits. Behaviourally identical on the five targets whose state read
|
|
2657
|
+
// was already synchronous (mirror and state agree), and the slice is transient gesture state
|
|
2658
|
+
// owned entirely by table-core — nothing else writes it — so the mirror cannot drift.
|
|
2659
|
+
// Same trick as the `committedThisSession` commit latch (editCellLifecycle.rzts), for the same
|
|
2660
|
+
// reason: a sync latch survives React's async window.
|
|
2661
|
+
columnSizingInfoSync: any = null;
|
|
2662
|
+
|
|
2470
2663
|
onColumnSizingInfoChangeCb = (updater: any) => {
|
|
2471
|
-
const
|
|
2472
|
-
|
|
2664
|
+
const base = this.columnSizingInfoSync != null ? this.columnSizingInfoSync : this._columnSizingInfo.value;
|
|
2665
|
+
const next = applyUpdater(updater, base);
|
|
2666
|
+
if (next == null) return;
|
|
2667
|
+
this.columnSizingInfoSync = next;
|
|
2668
|
+
this._columnSizingInfo.value = next;
|
|
2473
2669
|
};
|
|
2474
2670
|
|
|
2475
2671
|
// ══ Vertical row windowing (phase 53, req-1/2/3/6/9/10) — the virtual-core bridge ════════
|
|
@@ -6938,6 +7134,21 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6938
7134
|
const tryFocus = () => {
|
|
6939
7135
|
const el = this.resolveCellEl(String(row), col);
|
|
6940
7136
|
if (el) {
|
|
7137
|
+
// B-05 / F-07: do NOT stomp focus a later interaction has already placed on a DIFFERENT
|
|
7138
|
+
// grid cell. This poll runs up to 30 frames after commit/cancel, so without this guard a
|
|
7139
|
+
// user (or a test) who moves to the next cell in that window gets yanked BACK to the cell
|
|
7140
|
+
// they just left — and because the steal depends on rAF timing it presents as a flake that
|
|
7141
|
+
// rotates across targets rather than as a reproducible bug. Measured 2026-09-12: the
|
|
7142
|
+
// data-table-dropins editor case failed with the active cell pinned to the PREVIOUS column
|
|
7143
|
+
// (want 0/2, active 0/1; want 0/1, active 0/0) with no editor open.
|
|
7144
|
+
// focusEditorWhenReady already carries the equivalent guard against the same class of
|
|
7145
|
+
// theft; this is its missing twin. Compare the OWNING CELL (not node identity) so a
|
|
7146
|
+
// node-replacing re-render on Solid still resolves a genuinely dropped focus as recoverable.
|
|
7147
|
+
const ae = this.gridRoot && this.gridRoot.getRootNode ? this.gridRoot.getRootNode().activeElement : null;
|
|
7148
|
+
if (ae && ae !== el && this.gridRoot.contains && this.gridRoot.contains(ae) && ae.closest) {
|
|
7149
|
+
const aeCell = ae.closest('[data-grid-cell]');
|
|
7150
|
+
if (aeCell && aeCell !== el) return;
|
|
7151
|
+
}
|
|
6941
7152
|
el.focus();
|
|
6942
7153
|
return;
|
|
6943
7154
|
}
|