@adia-ai/web-components 0.8.44 → 0.8.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/components/card/card.css +12 -23
- package/components/card/card.yaml +11 -0
- package/components/chart/chart.a2ui.json +16 -1
- package/components/chart/chart.class.js +611 -41
- package/components/chart/chart.css +174 -0
- package/components/chart/chart.d.ts +5 -1
- package/components/chart/chart.yaml +45 -1
- package/components/context-menu/context-menu.a2ui.json +8 -3
- package/components/context-menu/context-menu.class.js +46 -5
- package/components/context-menu/context-menu.d.ts +6 -3
- package/components/context-menu/context-menu.examples.md +2 -2
- package/components/context-menu/context-menu.yaml +22 -5
- package/components/field/field.css +24 -2
- package/components/index.js +1 -0
- package/components/input/input.css +7 -0
- package/components/nav/nav.a2ui.json +2 -2
- package/components/nav/nav.css +1 -1
- package/components/nav/nav.d.ts +1 -1
- package/components/nav/nav.yaml +14 -3
- package/components/nav-group/nav-group.css +37 -3
- package/components/pagination/pagination.class.js +52 -22
- package/components/search/search.class.js +39 -5
- package/components/select/select.a2ui.json +5 -0
- package/components/select/select.class.js +20 -0
- package/components/select/select.css +25 -0
- package/components/select/select.d.ts +2 -0
- package/components/select/select.yaml +12 -0
- package/components/table/cell-types.js +9 -0
- package/components/table/table.a2ui.json +19 -4
- package/components/table/table.class.js +410 -45
- package/components/table/table.css +7 -4
- package/components/table/table.d.ts +9 -3
- package/components/table/table.yaml +115 -9
- package/components/table-footer/table-footer.a2ui.json +150 -0
- package/components/table-footer/table-footer.class.js +391 -0
- package/components/table-footer/table-footer.css +64 -0
- package/components/table-footer/table-footer.d.ts +39 -0
- package/components/table-footer/table-footer.examples.md +46 -0
- package/components/table-footer/table-footer.js +17 -0
- package/components/table-footer/table-footer.yaml +219 -0
- package/components/table-toolbar/table-toolbar.a2ui.json +15 -0
- package/components/table-toolbar/table-toolbar.class.js +61 -17
- package/components/table-toolbar/table-toolbar.css +34 -0
- package/components/table-toolbar/table-toolbar.d.ts +10 -0
- package/components/table-toolbar/table-toolbar.yaml +58 -15
- package/core/data-stream.js +37 -2
- package/core/index.d.ts +1 -0
- package/core/index.js +1 -0
- package/core/provider.d.ts +9 -13
- package/core/provider.js +9 -113
- package/core/store.d.ts +46 -0
- package/core/store.js +89 -0
- package/custom-elements.json +192 -8
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +1 -1
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +96 -96
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/patterns/chart-in-card/chart-in-card.examples.html +36 -9
- package/patterns/new-enrollments/new-enrollments.examples.html +140 -0
- package/patterns/new-enrollments/new-enrollments.html +54 -0
- package/patterns/table-in-card/table-in-card.examples.html +168 -0
- package/patterns/table-in-card/table-in-card.examples.js +139 -0
- package/patterns/table-in-card/table-in-card.html +86 -0
- package/styles/api/sizing.css +46 -0
- package/styles/components.css +1 -0
|
@@ -26,7 +26,21 @@
|
|
|
26
26
|
* table-ui has no local `density` prop (ADR-0054) — it converges onto the
|
|
27
27
|
* ambient global [density="compact"|"spacious"] attribute; --table-py/-px
|
|
28
28
|
* already derive from --a-space-* steps, which --a-density scales.
|
|
29
|
-
* paginate — rows per page (0 = no pagination)
|
|
29
|
+
* paginate — rows per page (0 = no pagination); purely presentational
|
|
30
|
+
* (page-size only) when [range-total] is present — gh#1754/ADR-0082
|
|
31
|
+
* range-total — total row count across ALL server pages (Number, reflected,
|
|
32
|
+
* presence-gated via hasAttribute — the attribute's PRESENCE, not
|
|
33
|
+
* its value, is the server-mode switch, gh#1754/ADR-0082). Absent
|
|
34
|
+
* → client mode, today's behavior exactly. Present + [paginate] > 0
|
|
35
|
+
* → server mode: no local slicing (`.data` IS the current page and
|
|
36
|
+
* renders whole, after local search/sort/filter), page count =
|
|
37
|
+
* max(1, ceil(range-total / paginate)), and the internal page-reset
|
|
38
|
+
* sites (`data` set, setFilter, clearFilters) are suppressed so a
|
|
39
|
+
* fetch write-back never fights the pager back to page 0. Explicit
|
|
40
|
+
* range-total="0" is server-confirmed empty (pager hidden), never
|
|
41
|
+
* conflated with absent. With [paginate] absent/0, [range-total] is
|
|
42
|
+
* inert for this table's own rendering but stays readable by a
|
|
43
|
+
* bound table-footer-ui for its count-only label.
|
|
30
44
|
* loading — show loading overlay
|
|
31
45
|
* search — global search filter string
|
|
32
46
|
*
|
|
@@ -45,13 +59,43 @@
|
|
|
45
59
|
* .sortState → [{key, dir}] (read-only)
|
|
46
60
|
* .exportCSV(filename?)
|
|
47
61
|
*
|
|
48
|
-
* Events:
|
|
62
|
+
* Events (fired):
|
|
49
63
|
* sort — { detail: { key, dir, sortState } }
|
|
50
64
|
* select — { detail: { selected: [...indices] } }
|
|
51
65
|
* page — { detail: { page } }
|
|
52
66
|
* resize — { detail: { key, width } }
|
|
53
67
|
* cell-click — { detail: { key, row, value, dataIndex } }
|
|
54
68
|
* row-click — { detail: { row, dataIndex } } — row-level companion to cell-click
|
|
69
|
+
*
|
|
70
|
+
* Events (listened) — gh#1764/#1780, ADR-0079. table-toolbar-ui dispatches
|
|
71
|
+
* these directly at its [for]-resolved table-ui target instead of writing
|
|
72
|
+
* a property or calling a method; any other consumer may dispatch them too:
|
|
73
|
+
* toolbar-search — { detail: { value } } → this.search =
|
|
74
|
+
* toolbar-filter-set — { detail: { key, value, op } } → this.setFilter(key, value, op)
|
|
75
|
+
* toolbar-filter-clear — { detail: {} } → this.clearFilters()
|
|
76
|
+
* toolbar-columns-set — { detail: { columns } } → this.columns =
|
|
77
|
+
* toolbar-paginate — { detail: { pageSize } } → this.paginate =
|
|
78
|
+
* footer-page — { detail: { page } }, 1-based → gh#1807/ADR-0080,
|
|
79
|
+
* refined 0.2.0/ADR-0082. Dispatched directly at this
|
|
80
|
+
* element (table-footer-ui, or any other consumer).
|
|
81
|
+
* Applies whenever [paginate] > 0 — client mode and
|
|
82
|
+
* table-authoritative server mode ([range-total]
|
|
83
|
+
* present, REQ-D-003) alike, since #pageCount already
|
|
84
|
+
* knows server math; clamped to [1, pageCount]; a
|
|
85
|
+
* non-numeric/NaN detail.page is a documented no-op.
|
|
86
|
+
* At [paginate="0"] the command stays a no-op in
|
|
87
|
+
* either mode — with no page size there is no page
|
|
88
|
+
* range to move within. On a valid page, applies
|
|
89
|
+
* exactly like an internal pager click: updates the
|
|
90
|
+
* page, re-renders, and fires the existing `page`
|
|
91
|
+
* event (the fetch trigger in server mode).
|
|
92
|
+
*
|
|
93
|
+
* [no-pager] — boolean, reflected (gh#1807/ADR-0080). Hides the internal
|
|
94
|
+
* pagination bar while [paginate] slicing, page state, the `page` event, and
|
|
95
|
+
* the `footer-page` listener all stay intact — the anti-doubled-pager
|
|
96
|
+
* mechanism for a table-footer-ui composition, client- or server-mode alike
|
|
97
|
+
* (gh#1754/ADR-0082). Explicit author intent, never an auto-suppression
|
|
98
|
+
* side-effect of binding a footer.
|
|
55
99
|
*/
|
|
56
100
|
|
|
57
101
|
import { UIElement } from '../../core/element.js';
|
|
@@ -94,6 +138,117 @@ function csvEscape(val) {
|
|
|
94
138
|
return str;
|
|
95
139
|
}
|
|
96
140
|
|
|
141
|
+
// ── SSR adopt-in-place (gh#1678) ─────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Idempotent attribute set — skip the DOM write entirely when the value
|
|
145
|
+
* already matches. `setAttribute` mutates (and queues a MutationObserver
|
|
146
|
+
* record) even when the new value is byte-identical to the old one, so a
|
|
147
|
+
* host/row-level attribute this render path sets on EVERY call (`role`,
|
|
148
|
+
* `tabindex`, `data-index`, `aria-selected`) must compare-before-write to
|
|
149
|
+
* let a byte-identical SSR-rendered subtree survive upgrade with zero
|
|
150
|
+
* subtree mutations (AC-004a-equivalent probe) — bare `setAttribute` cannot
|
|
151
|
+
* give that guarantee even when the value never actually changes.
|
|
152
|
+
*/
|
|
153
|
+
function setAttrIfChanged(el, name, value) {
|
|
154
|
+
if (el.getAttribute(name) !== value) el.setAttribute(name, value);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Idempotent attribute removal — same rationale as setAttrIfChanged. */
|
|
158
|
+
function removeAttrIfPresent(el, name) {
|
|
159
|
+
if (el.hasAttribute(name)) el.removeAttribute(name);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Marks a freshly-built candidate cell as renderer-owned — its content came
|
|
164
|
+
* from something free to attach an event listener or otherwise stash
|
|
165
|
+
* runtime state on the node it returns, where `Node.isEqualNode()` (the
|
|
166
|
+
* adopt-or-diff structural check below) has no way to see the difference
|
|
167
|
+
* (it compares tag/attributes/text/descendants only). Two, and only two,
|
|
168
|
+
* sources are marked:
|
|
169
|
+
*
|
|
170
|
+
* 1. `col.render()` — arbitrary consumer code. It can do anything to the
|
|
171
|
+
* node it hands back, so it's always marked; there's no way to inspect
|
|
172
|
+
* an opaque function for safety.
|
|
173
|
+
* 2. A built-in cell-type renderer (`typeDef.render`) that declares
|
|
174
|
+
* `attachesListeners: true` (currently only `cellTypes.actions`, which
|
|
175
|
+
* calls `addEventListener()` directly — see cell-types.js).
|
|
176
|
+
*
|
|
177
|
+
* Deliberately NOT marked: `col.format()`, the plain-text fallback, and
|
|
178
|
+
* every OTHER built-in cell-type renderer (text/number/currency/percent/
|
|
179
|
+
* date/datetime/boolean/badge/avatar/link/markdown/progress) — each of
|
|
180
|
+
* those only sets attributes on already-declarative custom elements
|
|
181
|
+
* (badge-ui, avatar-ui, check-ui, progress-ui, …) or plain nodes with no
|
|
182
|
+
* listeners; their own runtime behavior lives in their OWN
|
|
183
|
+
* `connectedCallback`, which fires independent of whether table.class.js
|
|
184
|
+
* discards or adopts the cell that contains them. Marking every
|
|
185
|
+
* `typeDef.render` cell unconditionally (an earlier draft of this fix) was
|
|
186
|
+
* tried and reverted — it broke the AC-004a zero-mutation acceptance
|
|
187
|
+
* criterion for the overwhelmingly common case (plain text/number/date
|
|
188
|
+
* cells) for no correctness benefit, since none of those renderers ever
|
|
189
|
+
* attach a listener. A cell that discards without ever being marked here
|
|
190
|
+
* (and turns out later to have needed it) is exactly the gap
|
|
191
|
+
* `guard-patterns.md` §4.1 asks a new interactive cell type to close by
|
|
192
|
+
* declaring `attachesListeners: true` on itself.
|
|
193
|
+
*
|
|
194
|
+
* A renderer-owned cell that happens to be structurally identical to the
|
|
195
|
+
* existing DOM would, without this guard, silently keep the OLD node (and
|
|
196
|
+
* its stale/missing listener) instead of the fresh one the renderer just
|
|
197
|
+
* built (gh#1678 CodeRabbit finding, table.class.js `adoptOrDiffChildren`
|
|
198
|
+
* L141-150 as originally shipped).
|
|
199
|
+
*/
|
|
200
|
+
const RENDERER_OWNED = new WeakSet();
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Adopt-or-diff child reconciliation — the core of table-ui's SSR
|
|
204
|
+
* adopt-in-place fix. `freshChildren` are already-built, detached candidate
|
|
205
|
+
* nodes (header cells, row cells, …) computed exactly as this file always
|
|
206
|
+
* computed them. Rather than unconditionally `replaceChild`-ing every
|
|
207
|
+
* position (the old, SSR-hostile behavior — a byte-identical server-
|
|
208
|
+
* rendered subtree got fully rebuilt at upgrade regardless), each position
|
|
209
|
+
* is compared against its EXISTING child with `Node.isEqualNode()` — a
|
|
210
|
+
* standard, structural + attribute + text deep-equality check present in
|
|
211
|
+
* every environment this framework runs under (real browsers, happy-dom,
|
|
212
|
+
* and linkedom under SSR — confirmed directly; unlike the browser-only APIs
|
|
213
|
+
* `ssr-compatibility`'s guard-patterns.md §1 guards, `isEqualNode` is core
|
|
214
|
+
* DOM and never absent). A match discards the freshly-built candidate and
|
|
215
|
+
* leaves the live DOM completely untouched — the "adopt" half. A mismatch
|
|
216
|
+
* replaces the position wholesale exactly as before — the "fall back to
|
|
217
|
+
* full rebuild" half. Never a partial/half-adopt: a position either
|
|
218
|
+
* survives byte-for-byte or gets fully rebuilt, matching the shape
|
|
219
|
+
* pagination-ui's own first-connect adoption fix (gh#1687) established for
|
|
220
|
+
* its flat, keyed item list — generalized here to an arbitrary positional
|
|
221
|
+
* child (header cells, row cells) via a value check instead of a
|
|
222
|
+
* shape/key check, since these cells have no stable identity key of their
|
|
223
|
+
* own the way a pagination item does.
|
|
224
|
+
*
|
|
225
|
+
* **Renderer-owned exception (gh#1678 CodeRabbit follow-up):** structural
|
|
226
|
+
* equality is a necessary but not sufficient adoption test — it says
|
|
227
|
+
* nothing about listeners or other runtime state a renderer attached to the
|
|
228
|
+
* node it returned. A cell in `RENDERER_OWNED` is therefore ALWAYS replaced
|
|
229
|
+
* with its fresh candidate, never adopted via the structural-equality path,
|
|
230
|
+
* even when `isEqualNode()` would have reported a match — correctness over
|
|
231
|
+
* the zero-mutation optimization for exactly the cells where structural
|
|
232
|
+
* equality can't prove the node is safe to keep. See
|
|
233
|
+
* `ssr-compatibility`'s `references/failure-shapes.md` §5.1 and
|
|
234
|
+
* `references/guard-patterns.md` §4.1 for the general shape.
|
|
235
|
+
*/
|
|
236
|
+
function adoptOrDiffChildren(container, freshChildren) {
|
|
237
|
+
while (container.children.length > freshChildren.length) container.lastChild.remove();
|
|
238
|
+
for (let i = 0; i < freshChildren.length; i++) {
|
|
239
|
+
const existing = container.children[i];
|
|
240
|
+
const fresh = freshChildren[i];
|
|
241
|
+
if (!existing) {
|
|
242
|
+
container.appendChild(fresh);
|
|
243
|
+
} else if (RENDERER_OWNED.has(fresh) || !existing.isEqualNode(fresh)) {
|
|
244
|
+
container.replaceChild(fresh, existing);
|
|
245
|
+
}
|
|
246
|
+
// else: existing already matches fresh byte-for-byte AND fresh carries
|
|
247
|
+
// no renderer-owned runtime state — adopt in place, discard the
|
|
248
|
+
// candidate, touch nothing.
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
97
252
|
// ── Component ────────────────────────────────────────────────────────────────
|
|
98
253
|
|
|
99
254
|
export class UITable extends UIElement {
|
|
@@ -123,6 +278,18 @@ export class UITable extends UIElement {
|
|
|
123
278
|
wrap: { type: Boolean, default: false, reflect: true },
|
|
124
279
|
raw: { type: Boolean, default: false, reflect: true },
|
|
125
280
|
paginate: { type: Number, default: 0, reflect: true },
|
|
281
|
+
// gh#1807/ADR-0080 — opt-out for the internal pagination bar. Additive:
|
|
282
|
+
// [paginate] slicing, page state, the `page` event, and the
|
|
283
|
+
// `footer-page` listener are all unaffected; only the internally
|
|
284
|
+
// stamped [data-footer] pagination-ui is suppressed. Explicit intent,
|
|
285
|
+
// for a client-mode table-footer-ui composition to avoid a doubled
|
|
286
|
+
// pager (REQ-W-005).
|
|
287
|
+
noPager: { type: Boolean, default: false, reflect: true, attribute: 'no-pager' },
|
|
288
|
+
// gh#1754/ADR-0082 — the server-total data contract. Presence-gated
|
|
289
|
+
// (#serverMode reads hasAttribute, never this coerced value): total row
|
|
290
|
+
// count across all server pages. See the class-header doc comment above
|
|
291
|
+
// and REQ-D-003..006 for the full server-mode contract.
|
|
292
|
+
rangeTotal: { type: Number, default: 0, reflect: true, attribute: 'range-total' },
|
|
126
293
|
loading: { type: Boolean, default: false, reflect: true },
|
|
127
294
|
search: { type: String, default: '', reflect: true },
|
|
128
295
|
// adiav2 zero-overrides purge (gh#1680) deleted an app-side [data-clickable]
|
|
@@ -151,6 +318,23 @@ export class UITable extends UIElement {
|
|
|
151
318
|
#openFilter = null; // column key of open filter dropdown
|
|
152
319
|
#filterFocusRaf = null;
|
|
153
320
|
|
|
321
|
+
// gh#1754/ADR-0082 — the mode switch is the [range-total] attribute's
|
|
322
|
+
// PRESENCE, never its coerced value (mirrors table-footer-ui's own
|
|
323
|
+
// REQ-S-001/002 hasAttribute discipline). One predicate for both the
|
|
324
|
+
// page-count/no-slice/visibility branches (step 2) and the reset-guard
|
|
325
|
+
// sites (step 3) — deliberately not narrowed by `paginate > 0` (see the
|
|
326
|
+
// reset-suppression call sites for why that widened scope is safe).
|
|
327
|
+
get #serverMode() { return this.hasAttribute('range-total'); }
|
|
328
|
+
|
|
329
|
+
// gh#1754/ADR-0082 REQ-D-003 (CodeRabbit, PR #1828) — a raw
|
|
330
|
+
// `Number(x) || 0` accepts negative and non-finite values verbatim (both
|
|
331
|
+
// are truthy), which can drive #pageCount/the footer's own range text
|
|
332
|
+
// negative or infinite. Clamps to a finite, non-negative integer.
|
|
333
|
+
#safeRangeTotal() {
|
|
334
|
+
const n = Number(this.rangeTotal);
|
|
335
|
+
return Number.isFinite(n) && n > 0 ? Math.trunc(n) : 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
154
338
|
// ── Public API: columns ──
|
|
155
339
|
|
|
156
340
|
set columns(arr) {
|
|
@@ -165,7 +349,11 @@ export class UITable extends UIElement {
|
|
|
165
349
|
set data(arr) {
|
|
166
350
|
this.#data = Array.isArray(arr) ? arr : [];
|
|
167
351
|
this.#selected.clear();
|
|
168
|
-
|
|
352
|
+
// gh#1754/ADR-0082 REQ-D-004 — in server mode a `.data` set is the
|
|
353
|
+
// consumer's fetch write-back for the CURRENT page; resetting to page 0
|
|
354
|
+
// here would fight the pager back to the start on every fetch. Selection
|
|
355
|
+
// still clears unconditionally (rows changed under it either way).
|
|
356
|
+
if (!this.#serverMode) this.#page = 0;
|
|
169
357
|
this.#requestRender();
|
|
170
358
|
}
|
|
171
359
|
|
|
@@ -229,7 +417,10 @@ export class UITable extends UIElement {
|
|
|
229
417
|
} else {
|
|
230
418
|
this.#filters.set(key, { op, value });
|
|
231
419
|
}
|
|
232
|
-
|
|
420
|
+
// gh#1754/ADR-0082 REQ-D-004 — server mode: page state moves only via
|
|
421
|
+
// pager interaction, the `footer-page` command, or setState(); a filter
|
|
422
|
+
// change never resets it (the consumer's fetch loop owns page moves).
|
|
423
|
+
if (!this.#serverMode) this.#page = 0;
|
|
233
424
|
this.#requestRender();
|
|
234
425
|
this.dispatchEvent(new CustomEvent('filter-change', {
|
|
235
426
|
detail: { filters: Object.fromEntries(this.#filters) },
|
|
@@ -240,7 +431,7 @@ export class UITable extends UIElement {
|
|
|
240
431
|
clearFilters() {
|
|
241
432
|
this.#filters.clear();
|
|
242
433
|
this.#openFilter = null;
|
|
243
|
-
this.#page = 0;
|
|
434
|
+
if (!this.#serverMode) this.#page = 0; // gh#1754/ADR-0082 REQ-D-004 — same suppression as setFilter above.
|
|
244
435
|
this.#requestRender();
|
|
245
436
|
this.dispatchEvent(new CustomEvent('filter-change', {
|
|
246
437
|
detail: { filters: {} },
|
|
@@ -250,6 +441,88 @@ export class UITable extends UIElement {
|
|
|
250
441
|
|
|
251
442
|
get filters() { return Object.fromEntries(this.#filters); }
|
|
252
443
|
|
|
444
|
+
// ── table-toolbar-ui command-event listeners (gh#1764/#1780, ADR-0079) ──
|
|
445
|
+
// Each handler applies the exact call table-toolbar.class.js used to make
|
|
446
|
+
// directly on this element before the events-only normalization — no
|
|
447
|
+
// behavior change, only the interaction shape.
|
|
448
|
+
//
|
|
449
|
+
// `e.target !== this` guard (code-checker finding, 2026-08-20): these
|
|
450
|
+
// events are dispatched `bubbles: true` (mirroring the ratified
|
|
451
|
+
// chart-legend-ui↔chart-ui model), so one fired at an INNER table-ui
|
|
452
|
+
// (e.g. inside an expanded row — table-ui's own row-expansion API makes
|
|
453
|
+
// a nested table a real composition) would otherwise also reach and
|
|
454
|
+
// apply to every ANCESTOR table-ui's listener. The old direct-write code
|
|
455
|
+
// had no such hazard — it targeted the resolved element precisely. The
|
|
456
|
+
// ratified model guards this exact case (`chart.class.js`'s
|
|
457
|
+
// `#onLegendToggle` returns unless the bubbled event's own [for] matches
|
|
458
|
+
// `this.id`); here the equivalent guard is simpler since these events are
|
|
459
|
+
// always dispatched directly at their intended table-ui, never through a
|
|
460
|
+
// [for] filter — `e.target !== this` is the correct, minimal check.
|
|
461
|
+
|
|
462
|
+
#onToolbarSearch = (e) => {
|
|
463
|
+
if (e.target !== this) return;
|
|
464
|
+
this.search = e.detail?.value ?? '';
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
#onToolbarFilterSet = (e) => {
|
|
468
|
+
if (e.target !== this) return;
|
|
469
|
+
const { key, value, op } = e.detail || {};
|
|
470
|
+
if (!key) return;
|
|
471
|
+
this.setFilter(key, value, op);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
#onToolbarFilterClear = (e) => {
|
|
475
|
+
if (e.target !== this) return;
|
|
476
|
+
this.clearFilters();
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
#onToolbarColumnsSet = (e) => {
|
|
480
|
+
if (e.target !== this) return;
|
|
481
|
+
const columns = e.detail?.columns;
|
|
482
|
+
if (Array.isArray(columns)) this.columns = columns;
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
#onToolbarPaginate = (e) => {
|
|
486
|
+
if (e.target !== this) return;
|
|
487
|
+
this.paginate = Number(e.detail?.pageSize) || 0;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// gh#1807/ADR-0080, refined 0.2.0/ADR-0082 — `footer-page` is the
|
|
491
|
+
// footer→table page-navigation command (REQ-W-002), sender-prefixed like
|
|
492
|
+
// the five `toolbar-*` commands above but from a second sender
|
|
493
|
+
// (table-footer-ui). Applies whenever [paginate] > 0 — client mode AND
|
|
494
|
+
// table-authoritative server mode ([range-total] present, REQ-D-003)
|
|
495
|
+
// alike, since #pageCount already knows server math (step 2 above): the
|
|
496
|
+
// clamp below reads the right ceiling in either mode with no extra
|
|
497
|
+
// branching needed here. At [paginate="0"] the command stays a documented
|
|
498
|
+
// no-op in either mode — with no page size there is no page range to move
|
|
499
|
+
// within. Clamps to the valid page range and never throws (AC-004/AC-018);
|
|
500
|
+
// a non-numeric/NaN detail.page is silently ignored (no page change, no
|
|
501
|
+
// `page` notification).
|
|
502
|
+
#onFooterPage = (e) => {
|
|
503
|
+
if (e.target !== this) return;
|
|
504
|
+
if (!(this.paginate > 0)) return; // paginate=0: documented no-op, either mode
|
|
505
|
+
const n = Number(e.detail?.page);
|
|
506
|
+
if (!Number.isFinite(n)) return;
|
|
507
|
+
const clamped = Math.max(1, Math.min(Math.trunc(n), this.#pageCount));
|
|
508
|
+
this.#page = clamped - 1; // this.#page is 0-based internally
|
|
509
|
+
this.render();
|
|
510
|
+
this.dispatchEvent(new CustomEvent('page', {
|
|
511
|
+
bubbles: true,
|
|
512
|
+
detail: { page: this.#page },
|
|
513
|
+
}));
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// ── Public API: filteredCount ──
|
|
517
|
+
// gh#1807/ADR-0080, REQ-W-006 — read-only. The row count AFTER search +
|
|
518
|
+
// column filters apply but BEFORE pagination slices it — exactly the
|
|
519
|
+
// number table-footer-ui's client-mode derivation needs for its
|
|
520
|
+
// range-total (a raw `.data.length` read, the toolbar's own `count`
|
|
521
|
+
// fallback precedent, would double-count a filtered-out row). Undocumented
|
|
522
|
+
// in table.yaml by design, matching `.selected`/`.sortState`/`.filters` —
|
|
523
|
+
// a plain read-only JS getter, not a reflected attribute.
|
|
524
|
+
get filteredCount() { return this.#getProcessedIndices().length; }
|
|
525
|
+
|
|
253
526
|
// ── Public API: expansion ──
|
|
254
527
|
|
|
255
528
|
toggleExpand(index) {
|
|
@@ -369,8 +642,12 @@ export class UITable extends UIElement {
|
|
|
369
642
|
} catch (_) { /* malformed JSON — leave empty, render() shows empty state */ }
|
|
370
643
|
}
|
|
371
644
|
|
|
372
|
-
|
|
373
|
-
|
|
645
|
+
// gh#1678 — idempotent: a byte-identical SSR-rendered host already
|
|
646
|
+
// carries both attributes, and an unconditional setAttribute() here
|
|
647
|
+
// would still queue a mutation record even though the value never
|
|
648
|
+
// actually changes.
|
|
649
|
+
setAttrIfChanged(this, 'role', 'grid');
|
|
650
|
+
setAttrIfChanged(this, 'tabindex', '0');
|
|
374
651
|
|
|
375
652
|
// Restore persisted state
|
|
376
653
|
this.#restoreState();
|
|
@@ -386,6 +663,21 @@ export class UITable extends UIElement {
|
|
|
386
663
|
// pen automatically; Playwright's page.mouse.* synthesizes pointer
|
|
387
664
|
// events (not mouse events), so a mousedown-only handler tests dead.
|
|
388
665
|
this.addEventListener('pointerdown', this.#onPointerdown);
|
|
666
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) — the
|
|
667
|
+
// `toolbar-*` command events table-toolbar-ui dispatches AT this
|
|
668
|
+
// element in place of the direct property writes / method calls it
|
|
669
|
+
// used to make. Each handler applies the exact same public-API call
|
|
670
|
+
// the toolbar previously made directly, so observable behavior is
|
|
671
|
+
// unchanged; only the interaction shape moved to events (mirrors
|
|
672
|
+
// chart-legend-ui↔chart-ui's own bubbling-CustomEvent model).
|
|
673
|
+
this.addEventListener('toolbar-search', this.#onToolbarSearch);
|
|
674
|
+
this.addEventListener('toolbar-filter-set', this.#onToolbarFilterSet);
|
|
675
|
+
this.addEventListener('toolbar-filter-clear', this.#onToolbarFilterClear);
|
|
676
|
+
this.addEventListener('toolbar-columns-set', this.#onToolbarColumnsSet);
|
|
677
|
+
this.addEventListener('toolbar-paginate', this.#onToolbarPaginate);
|
|
678
|
+
// gh#1807/ADR-0080 — a second sender-prefixed command, alongside the
|
|
679
|
+
// five toolbar-* listeners above (same symmetric-lifecycle rule).
|
|
680
|
+
this.addEventListener('footer-page', this.#onFooterPage);
|
|
389
681
|
}
|
|
390
682
|
}
|
|
391
683
|
|
|
@@ -393,6 +685,12 @@ export class UITable extends UIElement {
|
|
|
393
685
|
this.removeEventListener('click', this.#onClick);
|
|
394
686
|
this.removeEventListener('keydown', this.#onKeydown);
|
|
395
687
|
this.removeEventListener('pointerdown', this.#onPointerdown);
|
|
688
|
+
this.removeEventListener('toolbar-search', this.#onToolbarSearch);
|
|
689
|
+
this.removeEventListener('toolbar-filter-set', this.#onToolbarFilterSet);
|
|
690
|
+
this.removeEventListener('toolbar-filter-clear', this.#onToolbarFilterClear);
|
|
691
|
+
this.removeEventListener('toolbar-columns-set', this.#onToolbarColumnsSet);
|
|
692
|
+
this.removeEventListener('toolbar-paginate', this.#onToolbarPaginate);
|
|
693
|
+
this.removeEventListener('footer-page', this.#onFooterPage);
|
|
396
694
|
this.#bound = false;
|
|
397
695
|
this.#cleanupResize();
|
|
398
696
|
if (this.#renderRaf) {
|
|
@@ -405,6 +703,21 @@ export class UITable extends UIElement {
|
|
|
405
703
|
}
|
|
406
704
|
}
|
|
407
705
|
|
|
706
|
+
// gh#1754/ADR-0082 — ported from table-footer.class.js's own
|
|
707
|
+
// attributeChangedCallback gotcha (its rangeTotal, same default-0 shape).
|
|
708
|
+
// [range-total]'s default is already 0 (Number), so an absent → "0"
|
|
709
|
+
// transition parses to the SAME coerced value; the base class property
|
|
710
|
+
// setter's Object.is short-circuit (element.js installProps) swallows the
|
|
711
|
+
// signal write, and the reactive render effect never re-runs on its own.
|
|
712
|
+
// This is a presence concern (#serverMode reads hasAttribute, not the
|
|
713
|
+
// coerced value), not a value one. Batched via #requestRender — never
|
|
714
|
+
// render() directly — honoring the R7 render-clock discipline (no edits to
|
|
715
|
+
// #requestRender/#renderRaf themselves).
|
|
716
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
717
|
+
super.attributeChangedCallback(name, oldVal, newVal);
|
|
718
|
+
if (name === 'range-total') this.#requestRender();
|
|
719
|
+
}
|
|
720
|
+
|
|
408
721
|
// ── <col-def> Parsing ──────────────────────────────────────────────────────
|
|
409
722
|
|
|
410
723
|
#parseColDefs() {
|
|
@@ -600,9 +913,15 @@ export class UITable extends UIElement {
|
|
|
600
913
|
}
|
|
601
914
|
|
|
602
915
|
/**
|
|
603
|
-
* Paginate a set of indices.
|
|
916
|
+
* Paginate a set of indices. gh#1754/ADR-0082 REQ-D-003 — server mode:
|
|
917
|
+
* `.data` IS the current page already (the consumer sliced it server-side),
|
|
918
|
+
* so no local slicing runs; [paginate] supplies only the page-size for
|
|
919
|
+
* pager math. `indices` here has already had local search/sort/filter
|
|
920
|
+
* applied by #getProcessedIndices — server mode still renders that whole
|
|
921
|
+
* (unsliced) result.
|
|
604
922
|
*/
|
|
605
923
|
#getPageSlice(indices) {
|
|
924
|
+
if (this.#serverMode) return indices;
|
|
606
925
|
if (!this.paginate || this.paginate <= 0) return indices;
|
|
607
926
|
const start = this.#page * this.paginate;
|
|
608
927
|
return indices.slice(start, start + this.paginate);
|
|
@@ -610,6 +929,11 @@ export class UITable extends UIElement {
|
|
|
610
929
|
|
|
611
930
|
get #pageCount() {
|
|
612
931
|
if (!this.paginate || this.paginate <= 0) return 1;
|
|
932
|
+
// gh#1754/ADR-0082 REQ-D-003 — server mode: page count is server-total-
|
|
933
|
+
// derived, not the loaded page's own length (the exact dual-math drift
|
|
934
|
+
// gh#1754's evidence quoted — this is now the ONE calc site;
|
|
935
|
+
// #renderPagination reads this getter instead of recomputing).
|
|
936
|
+
if (this.#serverMode) return Math.max(1, Math.ceil(this.#safeRangeTotal() / this.paginate));
|
|
613
937
|
// pageCount should reflect filtered data, not raw data
|
|
614
938
|
const filteredCount = this.#getProcessedIndices().length;
|
|
615
939
|
return Math.max(1, Math.ceil(filteredCount / this.paginate));
|
|
@@ -629,8 +953,13 @@ export class UITable extends UIElement {
|
|
|
629
953
|
|
|
630
954
|
const visCols = this.#visibleColumns;
|
|
631
955
|
|
|
632
|
-
// Set grid template
|
|
633
|
-
|
|
956
|
+
// Set grid template — gh#1678: guard the write, same rationale as the
|
|
957
|
+
// host attribute sets in connected(). Assigning the identical string
|
|
958
|
+
// again is still a real style-attribute mutation, not a no-op.
|
|
959
|
+
const gridTemplate = this.#buildGridTemplate();
|
|
960
|
+
if (this.style.gridTemplateColumns !== gridTemplate) {
|
|
961
|
+
this.style.gridTemplateColumns = gridTemplate;
|
|
962
|
+
}
|
|
634
963
|
|
|
635
964
|
// ── Header row ──
|
|
636
965
|
|
|
@@ -669,13 +998,12 @@ export class UITable extends UIElement {
|
|
|
669
998
|
const row = existing || this.#createRow(idx, visCols);
|
|
670
999
|
if (existing) this.#updateRow(existing, idx, visCols);
|
|
671
1000
|
|
|
672
|
-
// Expand toggle in first cell
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
else row.removeAttribute('data-expanded');
|
|
1001
|
+
// Expand toggle in first cell — gh#1678: idempotent (see the
|
|
1002
|
+
// module-level helpers' own rationale).
|
|
1003
|
+
if (this.#isRowExpandable(idx) && this.#expanded.has(idx)) {
|
|
1004
|
+
setAttrIfChanged(row, 'data-expanded', '');
|
|
677
1005
|
} else {
|
|
678
|
-
row
|
|
1006
|
+
removeAttrIfPresent(row, 'data-expanded');
|
|
679
1007
|
}
|
|
680
1008
|
|
|
681
1009
|
bodyChildren.push(row);
|
|
@@ -719,7 +1047,15 @@ export class UITable extends UIElement {
|
|
|
719
1047
|
|
|
720
1048
|
// ── Pagination footer ──
|
|
721
1049
|
|
|
722
|
-
|
|
1050
|
+
// gh#1807/ADR-0080 REQ-W-005 — [no-pager] hides only the internally
|
|
1051
|
+
// stamped bar; slicing/page-state/events are untouched (they don't
|
|
1052
|
+
// route through this flag at all).
|
|
1053
|
+
// gh#1754/ADR-0082 REQ-D-005 — server mode drops the client-mode
|
|
1054
|
+
// `data.length > 0` term: an in-flight fetch (momentarily empty `.data`)
|
|
1055
|
+
// must not flicker the pager away, so visibility gates on `range-total`
|
|
1056
|
+
// instead.
|
|
1057
|
+
const showPagination = this.paginate > 0 && !this.noPager
|
|
1058
|
+
&& (this.#serverMode ? this.#safeRangeTotal() > 0 : this.#data.length > 0);
|
|
723
1059
|
let footer = this.querySelector(':scope > [data-footer]');
|
|
724
1060
|
|
|
725
1061
|
if (showPagination) {
|
|
@@ -728,7 +1064,7 @@ export class UITable extends UIElement {
|
|
|
728
1064
|
footer.setAttribute('data-footer', '');
|
|
729
1065
|
this.appendChild(footer);
|
|
730
1066
|
}
|
|
731
|
-
this.#renderPagination(footer
|
|
1067
|
+
this.#renderPagination(footer);
|
|
732
1068
|
} else if (footer) {
|
|
733
1069
|
footer.remove();
|
|
734
1070
|
}
|
|
@@ -830,12 +1166,10 @@ export class UITable extends UIElement {
|
|
|
830
1166
|
cells.push(cell);
|
|
831
1167
|
}
|
|
832
1168
|
|
|
833
|
-
// Reconcile header cells
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
else header.appendChild(cells[i]);
|
|
838
|
-
}
|
|
1169
|
+
// Reconcile header cells — gh#1678: adopt-or-diff instead of an
|
|
1170
|
+
// unconditional replaceChild per position (see the module-level
|
|
1171
|
+
// adoptOrDiffChildren() doc comment for why this is safe under SSR).
|
|
1172
|
+
adoptOrDiffChildren(header, cells);
|
|
839
1173
|
}
|
|
840
1174
|
|
|
841
1175
|
// ── Row Builders ───────────────────────────────────────────────────────────
|
|
@@ -850,12 +1184,16 @@ export class UITable extends UIElement {
|
|
|
850
1184
|
#updateRow(row, dataIndex, visCols) {
|
|
851
1185
|
const data = this.#data[dataIndex];
|
|
852
1186
|
const isSelected = this.#selected.has(dataIndex);
|
|
853
|
-
|
|
1187
|
+
// gh#1678 — idempotent: `#updateRow` runs on an ADOPTED (SSR-matched)
|
|
1188
|
+
// row exactly as often as on a freshly-created one, and a plain
|
|
1189
|
+
// `row.dataset.index = …` / `setAttribute` always mutates regardless of
|
|
1190
|
+
// whether the value already matches.
|
|
1191
|
+
setAttrIfChanged(row, 'data-index', String(dataIndex));
|
|
854
1192
|
|
|
855
1193
|
if (isSelected) {
|
|
856
|
-
row
|
|
1194
|
+
setAttrIfChanged(row, 'aria-selected', 'true');
|
|
857
1195
|
} else {
|
|
858
|
-
row
|
|
1196
|
+
removeAttrIfPresent(row, 'aria-selected');
|
|
859
1197
|
}
|
|
860
1198
|
|
|
861
1199
|
const cells = [];
|
|
@@ -901,6 +1239,12 @@ export class UITable extends UIElement {
|
|
|
901
1239
|
const value = getCellValue(data, col);
|
|
902
1240
|
|
|
903
1241
|
// Render priority: column.render > column.format > cellType.render > text
|
|
1242
|
+
// gh#1678 CodeRabbit follow-up: `col.render()` and a cell-type renderer
|
|
1243
|
+
// are free to attach listeners / runtime state to the node they hand
|
|
1244
|
+
// back — mark the cell RENDERER_OWNED so adoptOrDiffChildren() never
|
|
1245
|
+
// adopts it on structural equality alone (see that function's own doc
|
|
1246
|
+
// comment). `col.format()` and the plain-text fallback below build
|
|
1247
|
+
// nothing but a static <span>, so they're never marked.
|
|
904
1248
|
if (typeof col.render === 'function') {
|
|
905
1249
|
const result = col.render(value, data, cell, dataIndex);
|
|
906
1250
|
if (isNode(result)) {
|
|
@@ -908,6 +1252,7 @@ export class UITable extends UIElement {
|
|
|
908
1252
|
} else if (typeof result === 'string') {
|
|
909
1253
|
cell.innerHTML = result;
|
|
910
1254
|
}
|
|
1255
|
+
RENDERER_OWNED.add(cell);
|
|
911
1256
|
} else if (typeof col.format === 'function') {
|
|
912
1257
|
const t = col.format(value, data);
|
|
913
1258
|
cell.appendChild(Object.assign(document.createElement('span'), { textContent: t }));
|
|
@@ -916,6 +1261,16 @@ export class UITable extends UIElement {
|
|
|
916
1261
|
const typeDef = cellTypes[col.type || 'text'];
|
|
917
1262
|
if (typeDef?.render) {
|
|
918
1263
|
typeDef.render(value, data, cell, col.meta);
|
|
1264
|
+
// Most built-in cell-type renderers only set attributes on
|
|
1265
|
+
// custom elements (badge-ui, avatar-ui, check-ui, …) — safe to
|
|
1266
|
+
// adopt structurally, since their own runtime behavior lives in
|
|
1267
|
+
// their own connectedCallback, independent of node identity. Only
|
|
1268
|
+
// a type that declares `attachesListeners` (currently: 'actions',
|
|
1269
|
+
// which calls addEventListener() directly) is marked renderer-
|
|
1270
|
+
// owned; marking every typeDef.render() cell would defeat the
|
|
1271
|
+
// zero-mutation adoption for the overwhelming common case (plain
|
|
1272
|
+
// text/number/date/badge/... cells) for no correctness benefit.
|
|
1273
|
+
if (typeDef.attachesListeners) RENDERER_OWNED.add(cell);
|
|
919
1274
|
} else {
|
|
920
1275
|
const t = value != null ? String(value) : '';
|
|
921
1276
|
cell.appendChild(Object.assign(document.createElement('span'), { textContent: t }));
|
|
@@ -949,12 +1304,13 @@ export class UITable extends UIElement {
|
|
|
949
1304
|
cells.push(cell);
|
|
950
1305
|
}
|
|
951
1306
|
|
|
952
|
-
// Reconcile cells in row
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1307
|
+
// Reconcile cells in row — gh#1678: adopt-or-diff (see
|
|
1308
|
+
// adoptOrDiffChildren()'s doc comment). This is the fix's main hot
|
|
1309
|
+
// path: `#updateRow` is called on every already-adopted row found by
|
|
1310
|
+
// `data-index` at connect, so this is what stops a byte-identical
|
|
1311
|
+
// SSR-rendered row from having every one of its cells rebuilt at
|
|
1312
|
+
// upgrade.
|
|
1313
|
+
adoptOrDiffChildren(row, cells);
|
|
958
1314
|
}
|
|
959
1315
|
|
|
960
1316
|
// ── Overlays ───────────────────────────────────────────────────────────────
|
|
@@ -1084,10 +1440,14 @@ export class UITable extends UIElement {
|
|
|
1084
1440
|
default: result = '';
|
|
1085
1441
|
}
|
|
1086
1442
|
|
|
1087
|
-
// Format using cell type if available
|
|
1443
|
+
// Format using cell type if available. gh#1678 CodeRabbit follow-up:
|
|
1444
|
+
// same attachesListeners-only scoping as the body-row cell path
|
|
1445
|
+
// above — only a cell type that declares it attaches listeners
|
|
1446
|
+
// needs the always-replace guard.
|
|
1088
1447
|
const typeDef = cellTypes[col.type || 'text'];
|
|
1089
1448
|
if (typeof result === 'number' && typeDef?.render) {
|
|
1090
1449
|
typeDef.render(result, {}, cell, col.meta);
|
|
1450
|
+
if (typeDef.attachesListeners) RENDERER_OWNED.add(cell);
|
|
1091
1451
|
} else {
|
|
1092
1452
|
cell.textContent = typeof result === 'number' ? result.toLocaleString() : result;
|
|
1093
1453
|
}
|
|
@@ -1096,17 +1456,16 @@ export class UITable extends UIElement {
|
|
|
1096
1456
|
cells.push(cell);
|
|
1097
1457
|
}
|
|
1098
1458
|
|
|
1099
|
-
// Reconcile cells
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
}
|
|
1459
|
+
// Reconcile cells — gh#1678: adopt-or-diff, same helper as the header
|
|
1460
|
+
// and body-row cell reconciliation above (consistency; the aggregation
|
|
1461
|
+
// row is built from the exact same per-cell full-rebuild shape those
|
|
1462
|
+
// had).
|
|
1463
|
+
adoptOrDiffChildren(aggRow, cells);
|
|
1105
1464
|
}
|
|
1106
1465
|
|
|
1107
1466
|
// ── Pagination ─────────────────────────────────────────────────────────────
|
|
1108
1467
|
|
|
1109
|
-
#renderPagination(footer
|
|
1468
|
+
#renderPagination(footer) {
|
|
1110
1469
|
let pag = footer.querySelector('pagination-ui');
|
|
1111
1470
|
if (!pag) {
|
|
1112
1471
|
footer.innerHTML = '';
|
|
@@ -1121,10 +1480,16 @@ export class UITable extends UIElement {
|
|
|
1121
1480
|
});
|
|
1122
1481
|
footer.appendChild(pag);
|
|
1123
1482
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1483
|
+
// gh#1754/ADR-0082 — #pageCount is now the single calc site (client AND
|
|
1484
|
+
// server math); the duplicated `ceil(filteredTotal / paginate)` this
|
|
1485
|
+
// function used to recompute is gone — that dual-math drift is exactly
|
|
1486
|
+
// gh#1754's evidence. Clamp only the RENDERED page attribute (a display
|
|
1487
|
+
// concern, e.g. the server total shrank below the current page) — never
|
|
1488
|
+
// mutate `#page` or dispatch `page` from render (REQ-D-005: a render must
|
|
1489
|
+
// never self-trigger a fetch).
|
|
1490
|
+
const pageCount = this.#pageCount;
|
|
1491
|
+
const p = Math.min(this.#page, pageCount - 1);
|
|
1492
|
+
pag.setAttribute('page', String(p + 1));
|
|
1128
1493
|
pag.setAttribute('total', String(pageCount));
|
|
1129
1494
|
}
|
|
1130
1495
|
|