@adia-ai/web-components 0.8.44 → 0.8.45
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 +24 -0
- 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/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.class.js +247 -33
- package/components/table/table.css +7 -4
- package/components/table/table.yaml +6 -1
- 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 +41 -9
- 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 +54 -4
- 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 +93 -93
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/styles/api/sizing.css +46 -0
|
@@ -98,6 +98,31 @@
|
|
|
98
98
|
:scope:not([label])::before,
|
|
99
99
|
:scope[label=""]::before { display: none; }
|
|
100
100
|
|
|
101
|
+
/* Visually-hidden label (gh#1748) — [label-hidden] keeps the a11y name
|
|
102
|
+
(aria-label, bridged from [label] in select.class.js's
|
|
103
|
+
#syncAccessibleName()) but drops the visible text, for compositions
|
|
104
|
+
that already show the name elsewhere (e.g. a table-toolbar-ui
|
|
105
|
+
[slot="scope"] select whose scope/view name is redundant chrome next
|
|
106
|
+
to the toolbar's own title). Same canonical sr-only recipe as
|
|
107
|
+
<visually-hidden-ui> (visually-hidden.css) and check-ui's own
|
|
108
|
+
[label-hidden] (gh#1010) — clip-path does the real hiding, the rest is
|
|
109
|
+
belt-and-suspenders for cross-UA. More specific than the bare
|
|
110
|
+
`::before` rule above, so it wins whenever both [label] and
|
|
111
|
+
[label-hidden] are set. */
|
|
112
|
+
:scope[label][label-hidden]::before {
|
|
113
|
+
content: attr(label);
|
|
114
|
+
position: absolute !important;
|
|
115
|
+
width: 1px !important;
|
|
116
|
+
height: 1px !important;
|
|
117
|
+
padding: 0 !important;
|
|
118
|
+
margin: -1px !important;
|
|
119
|
+
overflow: hidden !important;
|
|
120
|
+
clip: rect(0, 0, 0, 0) !important;
|
|
121
|
+
clip-path: inset(100%) !important;
|
|
122
|
+
white-space: nowrap !important;
|
|
123
|
+
border: 0 !important;
|
|
124
|
+
}
|
|
125
|
+
|
|
101
126
|
/* Trigger */
|
|
102
127
|
[slot="trigger"] {
|
|
103
128
|
display: flex;
|
|
@@ -45,6 +45,8 @@ export class UISelect extends UIFormElement {
|
|
|
45
45
|
/** Open/closed reflected attribute — toggled by trigger click / keyboard. */
|
|
46
46
|
open: boolean;
|
|
47
47
|
label: string;
|
|
48
|
+
/** When true, [label] still sets the accessible name but the visible text is sr-only hidden (gh#1748, mirrors check-ui's [label-hidden], gh#1010). */
|
|
49
|
+
labelHidden: boolean;
|
|
48
50
|
/** Leading icon name (Phosphor registry). */
|
|
49
51
|
icon: string;
|
|
50
52
|
/** Leading avatar URL or name. */
|
|
@@ -104,6 +104,18 @@ props:
|
|
|
104
104
|
description: Label text above the trigger
|
|
105
105
|
type: string
|
|
106
106
|
default: ""
|
|
107
|
+
label-hidden:
|
|
108
|
+
description: >-
|
|
109
|
+
When true, [label] still sets the accessible name (aria-label) but the
|
|
110
|
+
visible `::before` text is suppressed via the canonical sr-only
|
|
111
|
+
technique (gh#1748, mirrors check-ui's [label-hidden], gh#1010). Use
|
|
112
|
+
when a sibling/ancestor composition already conveys the same name
|
|
113
|
+
visually (e.g. a scope/view-switcher select in a table-toolbar-ui
|
|
114
|
+
[slot="scope"], where the toolbar's own title already names the
|
|
115
|
+
view) and a second visible "Scope"-style label would paint it twice.
|
|
116
|
+
type: boolean
|
|
117
|
+
default: false
|
|
118
|
+
reflect: true
|
|
107
119
|
hint:
|
|
108
120
|
description: |-
|
|
109
121
|
§184 (v0.5.5, FEEDBACK-08 §7): small caption rendered beneath the select. Sets `aria-describedby` on the host so screen readers announce it as a description (distinct from `aria-label`, which comes from `label`). Does not conflict with the in-component `label`.
|
|
@@ -312,6 +312,15 @@ registerCellType('progress', {
|
|
|
312
312
|
|
|
313
313
|
// 13. actions
|
|
314
314
|
registerCellType('actions', {
|
|
315
|
+
// gh#1678 CodeRabbit follow-up: this render() calls addEventListener()
|
|
316
|
+
// directly on a plain node it creates — unlike every other cell type
|
|
317
|
+
// above (which only set attributes on custom elements, safe to adopt
|
|
318
|
+
// structurally), a discarded 'actions' cell loses a real listener with
|
|
319
|
+
// no way for isEqualNode() to see the loss. `attachesListeners: true`
|
|
320
|
+
// tells table.class.js's adoptOrDiffChildren() (via RENDERER_OWNED) to
|
|
321
|
+
// always replace this cell rather than ever adopt it on structural
|
|
322
|
+
// equality alone. See ssr-compatibility's references/guard-patterns.md §4.1.
|
|
323
|
+
attachesListeners: true,
|
|
315
324
|
render(value, row, cell, meta) {
|
|
316
325
|
const actions = meta?.actions || [];
|
|
317
326
|
let wrapper = cell.querySelector('row-ui');
|
|
@@ -45,13 +45,22 @@
|
|
|
45
45
|
* .sortState → [{key, dir}] (read-only)
|
|
46
46
|
* .exportCSV(filename?)
|
|
47
47
|
*
|
|
48
|
-
* Events:
|
|
48
|
+
* Events (fired):
|
|
49
49
|
* sort — { detail: { key, dir, sortState } }
|
|
50
50
|
* select — { detail: { selected: [...indices] } }
|
|
51
51
|
* page — { detail: { page } }
|
|
52
52
|
* resize — { detail: { key, width } }
|
|
53
53
|
* cell-click — { detail: { key, row, value, dataIndex } }
|
|
54
54
|
* row-click — { detail: { row, dataIndex } } — row-level companion to cell-click
|
|
55
|
+
*
|
|
56
|
+
* Events (listened) — gh#1764/#1780, ADR-0079. table-toolbar-ui dispatches
|
|
57
|
+
* these directly at its [for]-resolved table-ui target instead of writing
|
|
58
|
+
* a property or calling a method; any other consumer may dispatch them too:
|
|
59
|
+
* toolbar-search — { detail: { value } } → this.search =
|
|
60
|
+
* toolbar-filter-set — { detail: { key, value, op } } → this.setFilter(key, value, op)
|
|
61
|
+
* toolbar-filter-clear — { detail: {} } → this.clearFilters()
|
|
62
|
+
* toolbar-columns-set — { detail: { columns } } → this.columns =
|
|
63
|
+
* toolbar-paginate — { detail: { pageSize } } → this.paginate =
|
|
55
64
|
*/
|
|
56
65
|
|
|
57
66
|
import { UIElement } from '../../core/element.js';
|
|
@@ -94,6 +103,117 @@ function csvEscape(val) {
|
|
|
94
103
|
return str;
|
|
95
104
|
}
|
|
96
105
|
|
|
106
|
+
// ── SSR adopt-in-place (gh#1678) ─────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Idempotent attribute set — skip the DOM write entirely when the value
|
|
110
|
+
* already matches. `setAttribute` mutates (and queues a MutationObserver
|
|
111
|
+
* record) even when the new value is byte-identical to the old one, so a
|
|
112
|
+
* host/row-level attribute this render path sets on EVERY call (`role`,
|
|
113
|
+
* `tabindex`, `data-index`, `aria-selected`) must compare-before-write to
|
|
114
|
+
* let a byte-identical SSR-rendered subtree survive upgrade with zero
|
|
115
|
+
* subtree mutations (AC-004a-equivalent probe) — bare `setAttribute` cannot
|
|
116
|
+
* give that guarantee even when the value never actually changes.
|
|
117
|
+
*/
|
|
118
|
+
function setAttrIfChanged(el, name, value) {
|
|
119
|
+
if (el.getAttribute(name) !== value) el.setAttribute(name, value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Idempotent attribute removal — same rationale as setAttrIfChanged. */
|
|
123
|
+
function removeAttrIfPresent(el, name) {
|
|
124
|
+
if (el.hasAttribute(name)) el.removeAttribute(name);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Marks a freshly-built candidate cell as renderer-owned — its content came
|
|
129
|
+
* from something free to attach an event listener or otherwise stash
|
|
130
|
+
* runtime state on the node it returns, where `Node.isEqualNode()` (the
|
|
131
|
+
* adopt-or-diff structural check below) has no way to see the difference
|
|
132
|
+
* (it compares tag/attributes/text/descendants only). Two, and only two,
|
|
133
|
+
* sources are marked:
|
|
134
|
+
*
|
|
135
|
+
* 1. `col.render()` — arbitrary consumer code. It can do anything to the
|
|
136
|
+
* node it hands back, so it's always marked; there's no way to inspect
|
|
137
|
+
* an opaque function for safety.
|
|
138
|
+
* 2. A built-in cell-type renderer (`typeDef.render`) that declares
|
|
139
|
+
* `attachesListeners: true` (currently only `cellTypes.actions`, which
|
|
140
|
+
* calls `addEventListener()` directly — see cell-types.js).
|
|
141
|
+
*
|
|
142
|
+
* Deliberately NOT marked: `col.format()`, the plain-text fallback, and
|
|
143
|
+
* every OTHER built-in cell-type renderer (text/number/currency/percent/
|
|
144
|
+
* date/datetime/boolean/badge/avatar/link/markdown/progress) — each of
|
|
145
|
+
* those only sets attributes on already-declarative custom elements
|
|
146
|
+
* (badge-ui, avatar-ui, check-ui, progress-ui, …) or plain nodes with no
|
|
147
|
+
* listeners; their own runtime behavior lives in their OWN
|
|
148
|
+
* `connectedCallback`, which fires independent of whether table.class.js
|
|
149
|
+
* discards or adopts the cell that contains them. Marking every
|
|
150
|
+
* `typeDef.render` cell unconditionally (an earlier draft of this fix) was
|
|
151
|
+
* tried and reverted — it broke the AC-004a zero-mutation acceptance
|
|
152
|
+
* criterion for the overwhelmingly common case (plain text/number/date
|
|
153
|
+
* cells) for no correctness benefit, since none of those renderers ever
|
|
154
|
+
* attach a listener. A cell that discards without ever being marked here
|
|
155
|
+
* (and turns out later to have needed it) is exactly the gap
|
|
156
|
+
* `guard-patterns.md` §4.1 asks a new interactive cell type to close by
|
|
157
|
+
* declaring `attachesListeners: true` on itself.
|
|
158
|
+
*
|
|
159
|
+
* A renderer-owned cell that happens to be structurally identical to the
|
|
160
|
+
* existing DOM would, without this guard, silently keep the OLD node (and
|
|
161
|
+
* its stale/missing listener) instead of the fresh one the renderer just
|
|
162
|
+
* built (gh#1678 CodeRabbit finding, table.class.js `adoptOrDiffChildren`
|
|
163
|
+
* L141-150 as originally shipped).
|
|
164
|
+
*/
|
|
165
|
+
const RENDERER_OWNED = new WeakSet();
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Adopt-or-diff child reconciliation — the core of table-ui's SSR
|
|
169
|
+
* adopt-in-place fix. `freshChildren` are already-built, detached candidate
|
|
170
|
+
* nodes (header cells, row cells, …) computed exactly as this file always
|
|
171
|
+
* computed them. Rather than unconditionally `replaceChild`-ing every
|
|
172
|
+
* position (the old, SSR-hostile behavior — a byte-identical server-
|
|
173
|
+
* rendered subtree got fully rebuilt at upgrade regardless), each position
|
|
174
|
+
* is compared against its EXISTING child with `Node.isEqualNode()` — a
|
|
175
|
+
* standard, structural + attribute + text deep-equality check present in
|
|
176
|
+
* every environment this framework runs under (real browsers, happy-dom,
|
|
177
|
+
* and linkedom under SSR — confirmed directly; unlike the browser-only APIs
|
|
178
|
+
* `ssr-compatibility`'s guard-patterns.md §1 guards, `isEqualNode` is core
|
|
179
|
+
* DOM and never absent). A match discards the freshly-built candidate and
|
|
180
|
+
* leaves the live DOM completely untouched — the "adopt" half. A mismatch
|
|
181
|
+
* replaces the position wholesale exactly as before — the "fall back to
|
|
182
|
+
* full rebuild" half. Never a partial/half-adopt: a position either
|
|
183
|
+
* survives byte-for-byte or gets fully rebuilt, matching the shape
|
|
184
|
+
* pagination-ui's own first-connect adoption fix (gh#1687) established for
|
|
185
|
+
* its flat, keyed item list — generalized here to an arbitrary positional
|
|
186
|
+
* child (header cells, row cells) via a value check instead of a
|
|
187
|
+
* shape/key check, since these cells have no stable identity key of their
|
|
188
|
+
* own the way a pagination item does.
|
|
189
|
+
*
|
|
190
|
+
* **Renderer-owned exception (gh#1678 CodeRabbit follow-up):** structural
|
|
191
|
+
* equality is a necessary but not sufficient adoption test — it says
|
|
192
|
+
* nothing about listeners or other runtime state a renderer attached to the
|
|
193
|
+
* node it returned. A cell in `RENDERER_OWNED` is therefore ALWAYS replaced
|
|
194
|
+
* with its fresh candidate, never adopted via the structural-equality path,
|
|
195
|
+
* even when `isEqualNode()` would have reported a match — correctness over
|
|
196
|
+
* the zero-mutation optimization for exactly the cells where structural
|
|
197
|
+
* equality can't prove the node is safe to keep. See
|
|
198
|
+
* `ssr-compatibility`'s `references/failure-shapes.md` §5.1 and
|
|
199
|
+
* `references/guard-patterns.md` §4.1 for the general shape.
|
|
200
|
+
*/
|
|
201
|
+
function adoptOrDiffChildren(container, freshChildren) {
|
|
202
|
+
while (container.children.length > freshChildren.length) container.lastChild.remove();
|
|
203
|
+
for (let i = 0; i < freshChildren.length; i++) {
|
|
204
|
+
const existing = container.children[i];
|
|
205
|
+
const fresh = freshChildren[i];
|
|
206
|
+
if (!existing) {
|
|
207
|
+
container.appendChild(fresh);
|
|
208
|
+
} else if (RENDERER_OWNED.has(fresh) || !existing.isEqualNode(fresh)) {
|
|
209
|
+
container.replaceChild(fresh, existing);
|
|
210
|
+
}
|
|
211
|
+
// else: existing already matches fresh byte-for-byte AND fresh carries
|
|
212
|
+
// no renderer-owned runtime state — adopt in place, discard the
|
|
213
|
+
// candidate, touch nothing.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
97
217
|
// ── Component ────────────────────────────────────────────────────────────────
|
|
98
218
|
|
|
99
219
|
export class UITable extends UIElement {
|
|
@@ -250,6 +370,52 @@ export class UITable extends UIElement {
|
|
|
250
370
|
|
|
251
371
|
get filters() { return Object.fromEntries(this.#filters); }
|
|
252
372
|
|
|
373
|
+
// ── table-toolbar-ui command-event listeners (gh#1764/#1780, ADR-0079) ──
|
|
374
|
+
// Each handler applies the exact call table-toolbar.class.js used to make
|
|
375
|
+
// directly on this element before the events-only normalization — no
|
|
376
|
+
// behavior change, only the interaction shape.
|
|
377
|
+
//
|
|
378
|
+
// `e.target !== this` guard (code-checker finding, 2026-08-20): these
|
|
379
|
+
// events are dispatched `bubbles: true` (mirroring the ratified
|
|
380
|
+
// chart-legend-ui↔chart-ui model), so one fired at an INNER table-ui
|
|
381
|
+
// (e.g. inside an expanded row — table-ui's own row-expansion API makes
|
|
382
|
+
// a nested table a real composition) would otherwise also reach and
|
|
383
|
+
// apply to every ANCESTOR table-ui's listener. The old direct-write code
|
|
384
|
+
// had no such hazard — it targeted the resolved element precisely. The
|
|
385
|
+
// ratified model guards this exact case (`chart.class.js`'s
|
|
386
|
+
// `#onLegendToggle` returns unless the bubbled event's own [for] matches
|
|
387
|
+
// `this.id`); here the equivalent guard is simpler since these events are
|
|
388
|
+
// always dispatched directly at their intended table-ui, never through a
|
|
389
|
+
// [for] filter — `e.target !== this` is the correct, minimal check.
|
|
390
|
+
|
|
391
|
+
#onToolbarSearch = (e) => {
|
|
392
|
+
if (e.target !== this) return;
|
|
393
|
+
this.search = e.detail?.value ?? '';
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
#onToolbarFilterSet = (e) => {
|
|
397
|
+
if (e.target !== this) return;
|
|
398
|
+
const { key, value, op } = e.detail || {};
|
|
399
|
+
if (!key) return;
|
|
400
|
+
this.setFilter(key, value, op);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
#onToolbarFilterClear = (e) => {
|
|
404
|
+
if (e.target !== this) return;
|
|
405
|
+
this.clearFilters();
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
#onToolbarColumnsSet = (e) => {
|
|
409
|
+
if (e.target !== this) return;
|
|
410
|
+
const columns = e.detail?.columns;
|
|
411
|
+
if (Array.isArray(columns)) this.columns = columns;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
#onToolbarPaginate = (e) => {
|
|
415
|
+
if (e.target !== this) return;
|
|
416
|
+
this.paginate = Number(e.detail?.pageSize) || 0;
|
|
417
|
+
};
|
|
418
|
+
|
|
253
419
|
// ── Public API: expansion ──
|
|
254
420
|
|
|
255
421
|
toggleExpand(index) {
|
|
@@ -369,8 +535,12 @@ export class UITable extends UIElement {
|
|
|
369
535
|
} catch (_) { /* malformed JSON — leave empty, render() shows empty state */ }
|
|
370
536
|
}
|
|
371
537
|
|
|
372
|
-
|
|
373
|
-
|
|
538
|
+
// gh#1678 — idempotent: a byte-identical SSR-rendered host already
|
|
539
|
+
// carries both attributes, and an unconditional setAttribute() here
|
|
540
|
+
// would still queue a mutation record even though the value never
|
|
541
|
+
// actually changes.
|
|
542
|
+
setAttrIfChanged(this, 'role', 'grid');
|
|
543
|
+
setAttrIfChanged(this, 'tabindex', '0');
|
|
374
544
|
|
|
375
545
|
// Restore persisted state
|
|
376
546
|
this.#restoreState();
|
|
@@ -386,6 +556,18 @@ export class UITable extends UIElement {
|
|
|
386
556
|
// pen automatically; Playwright's page.mouse.* synthesizes pointer
|
|
387
557
|
// events (not mouse events), so a mousedown-only handler tests dead.
|
|
388
558
|
this.addEventListener('pointerdown', this.#onPointerdown);
|
|
559
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) — the
|
|
560
|
+
// `toolbar-*` command events table-toolbar-ui dispatches AT this
|
|
561
|
+
// element in place of the direct property writes / method calls it
|
|
562
|
+
// used to make. Each handler applies the exact same public-API call
|
|
563
|
+
// the toolbar previously made directly, so observable behavior is
|
|
564
|
+
// unchanged; only the interaction shape moved to events (mirrors
|
|
565
|
+
// chart-legend-ui↔chart-ui's own bubbling-CustomEvent model).
|
|
566
|
+
this.addEventListener('toolbar-search', this.#onToolbarSearch);
|
|
567
|
+
this.addEventListener('toolbar-filter-set', this.#onToolbarFilterSet);
|
|
568
|
+
this.addEventListener('toolbar-filter-clear', this.#onToolbarFilterClear);
|
|
569
|
+
this.addEventListener('toolbar-columns-set', this.#onToolbarColumnsSet);
|
|
570
|
+
this.addEventListener('toolbar-paginate', this.#onToolbarPaginate);
|
|
389
571
|
}
|
|
390
572
|
}
|
|
391
573
|
|
|
@@ -393,6 +575,11 @@ export class UITable extends UIElement {
|
|
|
393
575
|
this.removeEventListener('click', this.#onClick);
|
|
394
576
|
this.removeEventListener('keydown', this.#onKeydown);
|
|
395
577
|
this.removeEventListener('pointerdown', this.#onPointerdown);
|
|
578
|
+
this.removeEventListener('toolbar-search', this.#onToolbarSearch);
|
|
579
|
+
this.removeEventListener('toolbar-filter-set', this.#onToolbarFilterSet);
|
|
580
|
+
this.removeEventListener('toolbar-filter-clear', this.#onToolbarFilterClear);
|
|
581
|
+
this.removeEventListener('toolbar-columns-set', this.#onToolbarColumnsSet);
|
|
582
|
+
this.removeEventListener('toolbar-paginate', this.#onToolbarPaginate);
|
|
396
583
|
this.#bound = false;
|
|
397
584
|
this.#cleanupResize();
|
|
398
585
|
if (this.#renderRaf) {
|
|
@@ -629,8 +816,13 @@ export class UITable extends UIElement {
|
|
|
629
816
|
|
|
630
817
|
const visCols = this.#visibleColumns;
|
|
631
818
|
|
|
632
|
-
// Set grid template
|
|
633
|
-
|
|
819
|
+
// Set grid template — gh#1678: guard the write, same rationale as the
|
|
820
|
+
// host attribute sets in connected(). Assigning the identical string
|
|
821
|
+
// again is still a real style-attribute mutation, not a no-op.
|
|
822
|
+
const gridTemplate = this.#buildGridTemplate();
|
|
823
|
+
if (this.style.gridTemplateColumns !== gridTemplate) {
|
|
824
|
+
this.style.gridTemplateColumns = gridTemplate;
|
|
825
|
+
}
|
|
634
826
|
|
|
635
827
|
// ── Header row ──
|
|
636
828
|
|
|
@@ -669,13 +861,12 @@ export class UITable extends UIElement {
|
|
|
669
861
|
const row = existing || this.#createRow(idx, visCols);
|
|
670
862
|
if (existing) this.#updateRow(existing, idx, visCols);
|
|
671
863
|
|
|
672
|
-
// Expand toggle in first cell
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
else row.removeAttribute('data-expanded');
|
|
864
|
+
// Expand toggle in first cell — gh#1678: idempotent (see the
|
|
865
|
+
// module-level helpers' own rationale).
|
|
866
|
+
if (this.#isRowExpandable(idx) && this.#expanded.has(idx)) {
|
|
867
|
+
setAttrIfChanged(row, 'data-expanded', '');
|
|
677
868
|
} else {
|
|
678
|
-
row
|
|
869
|
+
removeAttrIfPresent(row, 'data-expanded');
|
|
679
870
|
}
|
|
680
871
|
|
|
681
872
|
bodyChildren.push(row);
|
|
@@ -830,12 +1021,10 @@ export class UITable extends UIElement {
|
|
|
830
1021
|
cells.push(cell);
|
|
831
1022
|
}
|
|
832
1023
|
|
|
833
|
-
// Reconcile header cells
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
else header.appendChild(cells[i]);
|
|
838
|
-
}
|
|
1024
|
+
// Reconcile header cells — gh#1678: adopt-or-diff instead of an
|
|
1025
|
+
// unconditional replaceChild per position (see the module-level
|
|
1026
|
+
// adoptOrDiffChildren() doc comment for why this is safe under SSR).
|
|
1027
|
+
adoptOrDiffChildren(header, cells);
|
|
839
1028
|
}
|
|
840
1029
|
|
|
841
1030
|
// ── Row Builders ───────────────────────────────────────────────────────────
|
|
@@ -850,12 +1039,16 @@ export class UITable extends UIElement {
|
|
|
850
1039
|
#updateRow(row, dataIndex, visCols) {
|
|
851
1040
|
const data = this.#data[dataIndex];
|
|
852
1041
|
const isSelected = this.#selected.has(dataIndex);
|
|
853
|
-
|
|
1042
|
+
// gh#1678 — idempotent: `#updateRow` runs on an ADOPTED (SSR-matched)
|
|
1043
|
+
// row exactly as often as on a freshly-created one, and a plain
|
|
1044
|
+
// `row.dataset.index = …` / `setAttribute` always mutates regardless of
|
|
1045
|
+
// whether the value already matches.
|
|
1046
|
+
setAttrIfChanged(row, 'data-index', String(dataIndex));
|
|
854
1047
|
|
|
855
1048
|
if (isSelected) {
|
|
856
|
-
row
|
|
1049
|
+
setAttrIfChanged(row, 'aria-selected', 'true');
|
|
857
1050
|
} else {
|
|
858
|
-
row
|
|
1051
|
+
removeAttrIfPresent(row, 'aria-selected');
|
|
859
1052
|
}
|
|
860
1053
|
|
|
861
1054
|
const cells = [];
|
|
@@ -901,6 +1094,12 @@ export class UITable extends UIElement {
|
|
|
901
1094
|
const value = getCellValue(data, col);
|
|
902
1095
|
|
|
903
1096
|
// Render priority: column.render > column.format > cellType.render > text
|
|
1097
|
+
// gh#1678 CodeRabbit follow-up: `col.render()` and a cell-type renderer
|
|
1098
|
+
// are free to attach listeners / runtime state to the node they hand
|
|
1099
|
+
// back — mark the cell RENDERER_OWNED so adoptOrDiffChildren() never
|
|
1100
|
+
// adopts it on structural equality alone (see that function's own doc
|
|
1101
|
+
// comment). `col.format()` and the plain-text fallback below build
|
|
1102
|
+
// nothing but a static <span>, so they're never marked.
|
|
904
1103
|
if (typeof col.render === 'function') {
|
|
905
1104
|
const result = col.render(value, data, cell, dataIndex);
|
|
906
1105
|
if (isNode(result)) {
|
|
@@ -908,6 +1107,7 @@ export class UITable extends UIElement {
|
|
|
908
1107
|
} else if (typeof result === 'string') {
|
|
909
1108
|
cell.innerHTML = result;
|
|
910
1109
|
}
|
|
1110
|
+
RENDERER_OWNED.add(cell);
|
|
911
1111
|
} else if (typeof col.format === 'function') {
|
|
912
1112
|
const t = col.format(value, data);
|
|
913
1113
|
cell.appendChild(Object.assign(document.createElement('span'), { textContent: t }));
|
|
@@ -916,6 +1116,16 @@ export class UITable extends UIElement {
|
|
|
916
1116
|
const typeDef = cellTypes[col.type || 'text'];
|
|
917
1117
|
if (typeDef?.render) {
|
|
918
1118
|
typeDef.render(value, data, cell, col.meta);
|
|
1119
|
+
// Most built-in cell-type renderers only set attributes on
|
|
1120
|
+
// custom elements (badge-ui, avatar-ui, check-ui, …) — safe to
|
|
1121
|
+
// adopt structurally, since their own runtime behavior lives in
|
|
1122
|
+
// their own connectedCallback, independent of node identity. Only
|
|
1123
|
+
// a type that declares `attachesListeners` (currently: 'actions',
|
|
1124
|
+
// which calls addEventListener() directly) is marked renderer-
|
|
1125
|
+
// owned; marking every typeDef.render() cell would defeat the
|
|
1126
|
+
// zero-mutation adoption for the overwhelming common case (plain
|
|
1127
|
+
// text/number/date/badge/... cells) for no correctness benefit.
|
|
1128
|
+
if (typeDef.attachesListeners) RENDERER_OWNED.add(cell);
|
|
919
1129
|
} else {
|
|
920
1130
|
const t = value != null ? String(value) : '';
|
|
921
1131
|
cell.appendChild(Object.assign(document.createElement('span'), { textContent: t }));
|
|
@@ -949,12 +1159,13 @@ export class UITable extends UIElement {
|
|
|
949
1159
|
cells.push(cell);
|
|
950
1160
|
}
|
|
951
1161
|
|
|
952
|
-
// Reconcile cells in row
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1162
|
+
// Reconcile cells in row — gh#1678: adopt-or-diff (see
|
|
1163
|
+
// adoptOrDiffChildren()'s doc comment). This is the fix's main hot
|
|
1164
|
+
// path: `#updateRow` is called on every already-adopted row found by
|
|
1165
|
+
// `data-index` at connect, so this is what stops a byte-identical
|
|
1166
|
+
// SSR-rendered row from having every one of its cells rebuilt at
|
|
1167
|
+
// upgrade.
|
|
1168
|
+
adoptOrDiffChildren(row, cells);
|
|
958
1169
|
}
|
|
959
1170
|
|
|
960
1171
|
// ── Overlays ───────────────────────────────────────────────────────────────
|
|
@@ -1084,10 +1295,14 @@ export class UITable extends UIElement {
|
|
|
1084
1295
|
default: result = '';
|
|
1085
1296
|
}
|
|
1086
1297
|
|
|
1087
|
-
// Format using cell type if available
|
|
1298
|
+
// Format using cell type if available. gh#1678 CodeRabbit follow-up:
|
|
1299
|
+
// same attachesListeners-only scoping as the body-row cell path
|
|
1300
|
+
// above — only a cell type that declares it attaches listeners
|
|
1301
|
+
// needs the always-replace guard.
|
|
1088
1302
|
const typeDef = cellTypes[col.type || 'text'];
|
|
1089
1303
|
if (typeof result === 'number' && typeDef?.render) {
|
|
1090
1304
|
typeDef.render(result, {}, cell, col.meta);
|
|
1305
|
+
if (typeDef.attachesListeners) RENDERER_OWNED.add(cell);
|
|
1091
1306
|
} else {
|
|
1092
1307
|
cell.textContent = typeof result === 'number' ? result.toLocaleString() : result;
|
|
1093
1308
|
}
|
|
@@ -1096,12 +1311,11 @@ export class UITable extends UIElement {
|
|
|
1096
1311
|
cells.push(cell);
|
|
1097
1312
|
}
|
|
1098
1313
|
|
|
1099
|
-
// Reconcile cells
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
}
|
|
1314
|
+
// Reconcile cells — gh#1678: adopt-or-diff, same helper as the header
|
|
1315
|
+
// and body-row cell reconciliation above (consistency; the aggregation
|
|
1316
|
+
// row is built from the exact same per-cell full-rebuild shape those
|
|
1317
|
+
// had).
|
|
1318
|
+
adoptOrDiffChildren(aggRow, cells);
|
|
1105
1319
|
}
|
|
1106
1320
|
|
|
1107
1321
|
// ── Pagination ─────────────────────────────────────────────────────────────
|
|
@@ -486,10 +486,13 @@
|
|
|
486
486
|
/* ═══════ Density ═══════ */
|
|
487
487
|
|
|
488
488
|
/* No local override (ADR-0054 convergence) — --table-py/-px above already
|
|
489
|
-
derive from --a-space-* steps, which the
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
489
|
+
derive from --a-space-* steps, which the global [density="compact"|
|
|
490
|
+
"spacious"] attribute scales via --a-density on ANY host, root or
|
|
491
|
+
scoped (gh#1771 — api/sizing.css re-declares the --a-space-* formulas
|
|
492
|
+
at the [density] boundary itself, the gh#570 cure; previously this
|
|
493
|
+
comment's "already scales" claim only held for root-level [density]).
|
|
494
|
+
Do not reintroduce a component-local :scope[density=…] block; that
|
|
495
|
+
would re-shadow the global grammar (ADR-0053). */
|
|
493
496
|
|
|
494
497
|
/* ═══════ Pinned cells ═══════ */
|
|
495
498
|
|
|
@@ -302,7 +302,12 @@ a2ui:
|
|
|
302
302
|
needs search / filter / sort / columns visibility. Do NOT
|
|
303
303
|
re-implement those affordances in the card header — the toolbar
|
|
304
304
|
auto-wires search/filter/sort/columns changes into the bound
|
|
305
|
-
table
|
|
305
|
+
table via `toolbar-*` CustomEvents (gh#1764/#1780, ADR-0079 —
|
|
306
|
+
events-only interaction contract; table-ui listens for
|
|
307
|
+
toolbar-search/toolbar-filter-set/toolbar-filter-clear/
|
|
308
|
+
toolbar-columns-set/toolbar-paginate on itself). Any consumer, not
|
|
309
|
+
only table-toolbar-ui, may dispatch these events directly at a
|
|
310
|
+
table-ui instance.
|
|
306
311
|
- >-
|
|
307
312
|
Cells truncate single-line by default (v0.6.21 §403
|
|
308
313
|
truncate-default). Opt out per-table with [wrap] for whole-table
|
|
@@ -142,6 +142,21 @@
|
|
|
142
142
|
},
|
|
143
143
|
"sort-change": {
|
|
144
144
|
"description": "Sort state changed. Detail: { sortState }."
|
|
145
|
+
},
|
|
146
|
+
"toolbar-columns-set": {
|
|
147
|
+
"description": "gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for] target in place of the pre-#1780 direct `target.columns =` write. table-ui listens for this on itself. Detail: { columns }."
|
|
148
|
+
},
|
|
149
|
+
"toolbar-filter-clear": {
|
|
150
|
+
"description": "gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for] target in place of the pre-#1780 direct `target.clearFilters()` call. table-ui listens for this on itself. No detail."
|
|
151
|
+
},
|
|
152
|
+
"toolbar-filter-set": {
|
|
153
|
+
"description": "gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for] target in place of the pre-#1780 direct `target.setFilter()` call. table-ui listens for this on itself. Detail: { key, value, op }; a null `value` clears that one column's filter."
|
|
154
|
+
},
|
|
155
|
+
"toolbar-paginate": {
|
|
156
|
+
"description": "gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for] target in place of the pre-#1780 direct `target.paginate =` write. table-ui listens for this on itself. Detail: { pageSize }."
|
|
157
|
+
},
|
|
158
|
+
"toolbar-search": {
|
|
159
|
+
"description": "gh#1764/#1780, ADR-0079 (events-only interaction contract) — dispatched directly at the resolved [for] target (not bubbled from this element) in place of the pre-#1780 direct `.search =` write. table-ui listens for this on itself. Detail: { value }."
|
|
145
160
|
}
|
|
146
161
|
},
|
|
147
162
|
"examples": [
|