@adia-ai/web-components 0.8.43 → 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 +54 -0
- package/components/avatar/avatar.class.js +12 -1
- package/components/button/button.d.ts +4 -0
- package/components/chart/chart.a2ui.json +26 -0
- package/components/chart/chart.class.js +1304 -222
- package/components/chart/chart.css +151 -1
- package/components/chart/chart.d.ts +6 -0
- package/components/chart/chart.yaml +57 -0
- package/components/chart-legend/chart-legend.a2ui.json +17 -2
- package/components/chart-legend/chart-legend.class.js +69 -4
- package/components/chart-legend/chart-legend.css +65 -1
- package/components/chart-legend/chart-legend.d.ts +4 -2
- package/components/chart-legend/chart-legend.examples.md +4 -4
- package/components/chart-legend/chart-legend.yaml +43 -3
- package/components/combobox/combobox.a2ui.json +3 -0
- package/components/combobox/combobox.class.js +9 -8
- package/components/combobox/combobox.css +25 -4
- package/components/combobox/combobox.yaml +6 -0
- package/components/command/command.css +17 -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 +8 -3
- package/components/context-menu/context-menu.examples.md +2 -2
- package/components/context-menu/context-menu.yaml +22 -5
- package/components/heatmap/heatmap.a2ui.json +5 -0
- package/components/heatmap/heatmap.d.ts +2 -0
- package/components/heatmap/heatmap.yaml +8 -0
- package/components/input/input.a2ui.json +2 -2
- package/components/input/input.class.js +12 -7
- package/components/input/input.yaml +6 -5
- package/components/integration-card/integration-card.d.ts +2 -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/noodles/noodles.a2ui.json +0 -15
- package/components/noodles/noodles.yaml +0 -12
- package/components/number-format/number-format.class.js +10 -0
- package/components/pagination/pagination.class.js +140 -26
- package/components/popover/popover.d.ts +2 -0
- package/components/progress/progress.class.js +11 -6
- package/components/range/range.class.js +9 -1
- package/components/search/search.class.js +39 -5
- package/components/select/select.a2ui.json +10 -1
- package/components/select/select.class.js +34 -14
- package/components/select/select.css +39 -1
- package/components/select/select.d.ts +2 -0
- package/components/select/select.yaml +19 -1
- package/components/stream/stream.d.ts +2 -0
- package/components/table/cell-types.js +9 -0
- package/components/table/table.a2ui.json +6 -1
- package/components/table/table.class.js +265 -34
- package/components/table/table.css +38 -10
- package/components/table/table.d.ts +3 -1
- package/components/table/table.examples.md +2 -2
- package/components/table/table.yaml +15 -2
- package/components/table-toolbar/table-toolbar.a2ui.json +43 -4
- package/components/table-toolbar/table-toolbar.class.js +407 -28
- package/components/table-toolbar/table-toolbar.css +299 -0
- package/components/table-toolbar/table-toolbar.d.ts +15 -1
- package/components/table-toolbar/table-toolbar.yaml +162 -15
- package/components/tag/tag.class.js +13 -1
- package/components/text/text.a2ui.json +3 -3
- package/components/text/text.d.ts +5 -3
- package/components/text/text.yaml +3 -3
- package/components/textarea/textarea.class.js +9 -1
- package/components/tour/tour.d.ts +10 -0
- package/core/a11y.d.ts +22 -0
- package/core/a11y.js +64 -0
- package/core/anchor.js +9 -5
- package/core/data-stream.js +37 -2
- package/core/element.js +1 -1
- 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 +127 -33
- 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 +119 -119
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/patterns/access-requests/access-requests.examples.html +1 -1
- package/patterns/admin-keys-and-export/admin-keys-and-export.examples.html +7 -7
- package/patterns/agent-cost/agent-cost.examples.html +6 -6
- package/patterns/agent-memory/agent-memory.examples.html +3 -3
- package/patterns/agent-prompt-library/agent-prompt-library.examples.html +8 -8
- package/patterns/agent-tool-call/agent-tool-call.examples.html +1 -1
- package/patterns/approvals/approvals.examples.html +6 -6
- package/patterns/audit-log/audit-log.examples.html +1 -1
- package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +4 -4
- package/patterns/chart-in-card/chart-in-card.examples.html +114 -0
- package/patterns/chart-in-card/chart-in-card.examples.js +37 -0
- package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +5 -5
- package/patterns/data-tables-inline-edit-and-tree/data-tables-inline-edit-and-tree.examples.html +2 -2
- package/patterns/diff-review/diff-review.examples.html +6 -6
- package/patterns/export-flow/export-flow.examples.html +2 -2
- package/patterns/filter-bar/filter-bar.examples.html +2 -2
- package/patterns/forms-conditional-and-autocomplete/forms-conditional-and-autocomplete.examples.html +2 -2
- package/patterns/inline-dialog/inline-dialog.examples.html +2 -2
- package/patterns/marketing-engagement/marketing-engagement.examples.html +4 -4
- package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +1 -1
- package/patterns/permissions-matrix/permissions-matrix.examples.html +1 -1
- package/patterns/permissions-sharing/permissions-sharing.examples.html +2 -2
- package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +4 -4
- package/patterns/search-discovery/search-discovery.examples.html +2 -2
- package/styles/api/sizing.css +46 -0
- package/styles/type/roles.css +29 -9
|
@@ -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 {
|
|
@@ -125,6 +245,11 @@ export class UITable extends UIElement {
|
|
|
125
245
|
paginate: { type: Number, default: 0, reflect: true },
|
|
126
246
|
loading: { type: Boolean, default: false, reflect: true },
|
|
127
247
|
search: { type: String, default: '', reflect: true },
|
|
248
|
+
// adiav2 zero-overrides purge (gh#1680) deleted an app-side [data-clickable]
|
|
249
|
+
// cursor rule with no kit-side replacement — row-click is always-emitted
|
|
250
|
+
// (fires whether or not a listener cares), so cursor can't be inferred
|
|
251
|
+
// from listener presence; this is an explicit opt-in affordance instead.
|
|
252
|
+
clickableRows: { type: Boolean, default: false, reflect: true, attribute: 'clickable-rows' },
|
|
128
253
|
};
|
|
129
254
|
|
|
130
255
|
static template = () => null;
|
|
@@ -245,6 +370,52 @@ export class UITable extends UIElement {
|
|
|
245
370
|
|
|
246
371
|
get filters() { return Object.fromEntries(this.#filters); }
|
|
247
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
|
+
|
|
248
419
|
// ── Public API: expansion ──
|
|
249
420
|
|
|
250
421
|
toggleExpand(index) {
|
|
@@ -364,8 +535,12 @@ export class UITable extends UIElement {
|
|
|
364
535
|
} catch (_) { /* malformed JSON — leave empty, render() shows empty state */ }
|
|
365
536
|
}
|
|
366
537
|
|
|
367
|
-
|
|
368
|
-
|
|
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');
|
|
369
544
|
|
|
370
545
|
// Restore persisted state
|
|
371
546
|
this.#restoreState();
|
|
@@ -381,6 +556,18 @@ export class UITable extends UIElement {
|
|
|
381
556
|
// pen automatically; Playwright's page.mouse.* synthesizes pointer
|
|
382
557
|
// events (not mouse events), so a mousedown-only handler tests dead.
|
|
383
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);
|
|
384
571
|
}
|
|
385
572
|
}
|
|
386
573
|
|
|
@@ -388,6 +575,11 @@ export class UITable extends UIElement {
|
|
|
388
575
|
this.removeEventListener('click', this.#onClick);
|
|
389
576
|
this.removeEventListener('keydown', this.#onKeydown);
|
|
390
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);
|
|
391
583
|
this.#bound = false;
|
|
392
584
|
this.#cleanupResize();
|
|
393
585
|
if (this.#renderRaf) {
|
|
@@ -624,8 +816,13 @@ export class UITable extends UIElement {
|
|
|
624
816
|
|
|
625
817
|
const visCols = this.#visibleColumns;
|
|
626
818
|
|
|
627
|
-
// Set grid template
|
|
628
|
-
|
|
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
|
+
}
|
|
629
826
|
|
|
630
827
|
// ── Header row ──
|
|
631
828
|
|
|
@@ -664,13 +861,12 @@ export class UITable extends UIElement {
|
|
|
664
861
|
const row = existing || this.#createRow(idx, visCols);
|
|
665
862
|
if (existing) this.#updateRow(existing, idx, visCols);
|
|
666
863
|
|
|
667
|
-
// Expand toggle in first cell
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
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', '');
|
|
672
868
|
} else {
|
|
673
|
-
row
|
|
869
|
+
removeAttrIfPresent(row, 'data-expanded');
|
|
674
870
|
}
|
|
675
871
|
|
|
676
872
|
bodyChildren.push(row);
|
|
@@ -735,6 +931,18 @@ export class UITable extends UIElement {
|
|
|
735
931
|
const allSelected = this.#data.length > 0 && this.#selected.size === this.#data.length;
|
|
736
932
|
const cells = [];
|
|
737
933
|
|
|
934
|
+
// Expand column header — an empty placeholder for the reserved
|
|
935
|
+
// expand-toggle track. Must precede the checkbox header, matching the
|
|
936
|
+
// grid template and body-row cell order (gh#1654: with no placeholder,
|
|
937
|
+
// every header label sat one column left of its data).
|
|
938
|
+
if (this.expandable) {
|
|
939
|
+
const cell = document.createElement('div');
|
|
940
|
+
cell.setAttribute('role', 'columnheader');
|
|
941
|
+
cell.setAttribute('data-expand-col', '');
|
|
942
|
+
cell.setAttribute('aria-label', 'Expand');
|
|
943
|
+
cells.push(cell);
|
|
944
|
+
}
|
|
945
|
+
|
|
738
946
|
// Checkbox column header
|
|
739
947
|
if (this.selectable) {
|
|
740
948
|
const cell = document.createElement('div');
|
|
@@ -813,12 +1021,10 @@ export class UITable extends UIElement {
|
|
|
813
1021
|
cells.push(cell);
|
|
814
1022
|
}
|
|
815
1023
|
|
|
816
|
-
// Reconcile header cells
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
else header.appendChild(cells[i]);
|
|
821
|
-
}
|
|
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);
|
|
822
1028
|
}
|
|
823
1029
|
|
|
824
1030
|
// ── Row Builders ───────────────────────────────────────────────────────────
|
|
@@ -833,12 +1039,16 @@ export class UITable extends UIElement {
|
|
|
833
1039
|
#updateRow(row, dataIndex, visCols) {
|
|
834
1040
|
const data = this.#data[dataIndex];
|
|
835
1041
|
const isSelected = this.#selected.has(dataIndex);
|
|
836
|
-
|
|
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));
|
|
837
1047
|
|
|
838
1048
|
if (isSelected) {
|
|
839
|
-
row
|
|
1049
|
+
setAttrIfChanged(row, 'aria-selected', 'true');
|
|
840
1050
|
} else {
|
|
841
|
-
row
|
|
1051
|
+
removeAttrIfPresent(row, 'aria-selected');
|
|
842
1052
|
}
|
|
843
1053
|
|
|
844
1054
|
const cells = [];
|
|
@@ -884,6 +1094,12 @@ export class UITable extends UIElement {
|
|
|
884
1094
|
const value = getCellValue(data, col);
|
|
885
1095
|
|
|
886
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.
|
|
887
1103
|
if (typeof col.render === 'function') {
|
|
888
1104
|
const result = col.render(value, data, cell, dataIndex);
|
|
889
1105
|
if (isNode(result)) {
|
|
@@ -891,6 +1107,7 @@ export class UITable extends UIElement {
|
|
|
891
1107
|
} else if (typeof result === 'string') {
|
|
892
1108
|
cell.innerHTML = result;
|
|
893
1109
|
}
|
|
1110
|
+
RENDERER_OWNED.add(cell);
|
|
894
1111
|
} else if (typeof col.format === 'function') {
|
|
895
1112
|
const t = col.format(value, data);
|
|
896
1113
|
cell.appendChild(Object.assign(document.createElement('span'), { textContent: t }));
|
|
@@ -899,6 +1116,16 @@ export class UITable extends UIElement {
|
|
|
899
1116
|
const typeDef = cellTypes[col.type || 'text'];
|
|
900
1117
|
if (typeDef?.render) {
|
|
901
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);
|
|
902
1129
|
} else {
|
|
903
1130
|
const t = value != null ? String(value) : '';
|
|
904
1131
|
cell.appendChild(Object.assign(document.createElement('span'), { textContent: t }));
|
|
@@ -932,12 +1159,13 @@ export class UITable extends UIElement {
|
|
|
932
1159
|
cells.push(cell);
|
|
933
1160
|
}
|
|
934
1161
|
|
|
935
|
-
// Reconcile cells in row
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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);
|
|
941
1169
|
}
|
|
942
1170
|
|
|
943
1171
|
// ── Overlays ───────────────────────────────────────────────────────────────
|
|
@@ -1067,10 +1295,14 @@ export class UITable extends UIElement {
|
|
|
1067
1295
|
default: result = '';
|
|
1068
1296
|
}
|
|
1069
1297
|
|
|
1070
|
-
// 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.
|
|
1071
1302
|
const typeDef = cellTypes[col.type || 'text'];
|
|
1072
1303
|
if (typeof result === 'number' && typeDef?.render) {
|
|
1073
1304
|
typeDef.render(result, {}, cell, col.meta);
|
|
1305
|
+
if (typeDef.attachesListeners) RENDERER_OWNED.add(cell);
|
|
1074
1306
|
} else {
|
|
1075
1307
|
cell.textContent = typeof result === 'number' ? result.toLocaleString() : result;
|
|
1076
1308
|
}
|
|
@@ -1079,12 +1311,11 @@ export class UITable extends UIElement {
|
|
|
1079
1311
|
cells.push(cell);
|
|
1080
1312
|
}
|
|
1081
1313
|
|
|
1082
|
-
// Reconcile cells
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
}
|
|
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);
|
|
1088
1319
|
}
|
|
1089
1320
|
|
|
1090
1321
|
// ── Pagination ─────────────────────────────────────────────────────────────
|
|
@@ -1505,7 +1736,7 @@ export class UITable extends UIElement {
|
|
|
1505
1736
|
|
|
1506
1737
|
#onKeydown = (e) => {
|
|
1507
1738
|
const visCols = this.#visibleColumns;
|
|
1508
|
-
const totalCols = visCols.length + (this.selectable ? 1 : 0);
|
|
1739
|
+
const totalCols = visCols.length + (this.expandable ? 1 : 0) + (this.selectable ? 1 : 0);
|
|
1509
1740
|
const body = this.querySelector(':scope > [data-body]');
|
|
1510
1741
|
const totalRows = body ? body.children.length : 0;
|
|
1511
1742
|
|
|
@@ -303,9 +303,18 @@
|
|
|
303
303
|
|
|
304
304
|
/* text-overflow on a flex container doesn't reach text nodes (anonymous
|
|
305
305
|
flex items). Built-in cell types now wrap text in <span>; link type
|
|
306
|
-
creates <a>; avatar/progress composite types use row-ui > span.
|
|
306
|
+
creates <a>; avatar/progress composite types use row-ui > span.
|
|
307
|
+
|
|
308
|
+
`[data-truncate]` (gh#1681) extends the identical contract to
|
|
309
|
+
consumer-rendered `column.render` cells, whose output shape isn't
|
|
310
|
+
guaranteed to be a bare <span>/<a> — a render() function sets it on
|
|
311
|
+
whichever element (any tag, including a custom element) should
|
|
312
|
+
single-line-ellipsis, the same way a column author sets [data-wrap] to
|
|
313
|
+
opt a built-in cell INTO wrapping. Undocumented before this: `render`
|
|
314
|
+
cells clipped with no ellipsis and no opt-in path. */
|
|
307
315
|
[data-body] [role="gridcell"] > span,
|
|
308
|
-
[data-body] [role="gridcell"] > a
|
|
316
|
+
[data-body] [role="gridcell"] > a,
|
|
317
|
+
[data-body] [role="gridcell"] > [data-truncate] {
|
|
309
318
|
min-width: 0;
|
|
310
319
|
flex: 1;
|
|
311
320
|
overflow: hidden;
|
|
@@ -351,8 +360,10 @@
|
|
|
351
360
|
|
|
352
361
|
:scope[wrap] [data-body] [role="gridcell"] > span,
|
|
353
362
|
:scope[wrap] [data-body] [role="gridcell"] > a,
|
|
363
|
+
:scope[wrap] [data-body] [role="gridcell"] > [data-truncate],
|
|
354
364
|
[data-body] [role="gridcell"][data-wrap] > span,
|
|
355
|
-
[data-body] [role="gridcell"][data-wrap] > a
|
|
365
|
+
[data-body] [role="gridcell"][data-wrap] > a,
|
|
366
|
+
[data-body] [role="gridcell"][data-wrap] > [data-truncate] {
|
|
356
367
|
white-space: normal;
|
|
357
368
|
overflow: visible;
|
|
358
369
|
text-overflow: clip;
|
|
@@ -417,6 +428,15 @@
|
|
|
417
428
|
color: var(--table-fg-hover);
|
|
418
429
|
}
|
|
419
430
|
|
|
431
|
+
/* Clickable-row affordance (gh#1680) — adiav2's zero-overrides purge
|
|
432
|
+
deleted an app-side [data-clickable] cursor rule with no kit-side
|
|
433
|
+
replacement. `row-click` fires unconditionally regardless of whether a
|
|
434
|
+
listener cares, so cursor can't be inferred from listener presence;
|
|
435
|
+
`clickable-rows` is an explicit opt-in instead. */
|
|
436
|
+
:scope[clickable-rows] [data-body] > [role="row"] {
|
|
437
|
+
cursor: pointer;
|
|
438
|
+
}
|
|
439
|
+
|
|
420
440
|
/* Selected row — must out-specify the base row rule `[data-body] >
|
|
421
441
|
[role="row"]` (0,2,0) and the hover rule (0,2,1), or the highlight never
|
|
422
442
|
paints. Scoping to `[data-body] > [role="row"][aria-selected="true"]` lifts
|
|
@@ -466,10 +486,13 @@
|
|
|
466
486
|
/* ═══════ Density ═══════ */
|
|
467
487
|
|
|
468
488
|
/* No local override (ADR-0054 convergence) — --table-py/-px above already
|
|
469
|
-
derive from --a-space-* steps, which the
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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). */
|
|
473
496
|
|
|
474
497
|
/* ═══════ Pinned cells ═══════ */
|
|
475
498
|
|
|
@@ -534,9 +557,14 @@
|
|
|
534
557
|
|
|
535
558
|
/* ═══════ Overlays (empty / loading / error) ═══════ */
|
|
536
559
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
560
|
+
/* Scoped to the host's direct children — gh#1700, hygiene pass matching
|
|
561
|
+
the command-ui (gh#1697/#1698) and combobox-ui (gh#1700) fix shape.
|
|
562
|
+
Single-consumer today (only table.class.js's own `:scope >
|
|
563
|
+
[data-empty]` / `:scope > [data-loading]` overlay markers), but a bare
|
|
564
|
+
selector here is one nested consumer away from the same leak. */
|
|
565
|
+
:scope > [data-overlay],
|
|
566
|
+
:scope > [data-empty],
|
|
567
|
+
:scope > [data-loading] {
|
|
540
568
|
grid-column: 1 / -1;
|
|
541
569
|
display: flex;
|
|
542
570
|
flex-direction: column;
|
|
@@ -86,7 +86,9 @@ export interface TableSortEventDetail {
|
|
|
86
86
|
export type TableSortEvent = CustomEvent<TableSortEventDetail>;
|
|
87
87
|
|
|
88
88
|
export class UITable extends UIElement {
|
|
89
|
-
/**
|
|
89
|
+
/** Renders body rows with `cursor: pointer`. `row-click` fires unconditionally whether or not a listener is attached, so the cursor can't be inferred from listener presence — set this explicitly when row-click behavior is wired up (master-detail, open-flyout, navigation) so rows read as actionable. */
|
|
90
|
+
clickableRows: boolean;
|
|
91
|
+
/** Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, wrap?, valign?, accessor?, format?, render?, sortFn?, filterType?, meta?}. `valign` ('top'|'center') overrides the host [valign] for that column's body cells only — header cells always stay centered. Alternative to declarative <col-def> children (<col-def valign="top">); col-def also accepts `meta` as a JSON attribute (gh#925) — e.g. <col-def type="badge" meta='{"variants":{"Online":"success"}}'> — so cell-type meta (badge variant maps, currency codes) is expressible in static HTML. Truncation contract for `render` cells (gh#1681) — a `render(value, row, cell, dataIndex)` function's returned markup isn't guaranteed to be a bare <span>/<a> the way built-in cell types are, so single-line ellipsis truncation isn't automatic; set `data-truncate` on whichever rendered element (any tag, including a custom element) should single-line-ellipsis to opt it into the same contract built-in cells get for free. `[wrap]` (host) / `[data-wrap]` (per-column) override it back to multi-line, matching built-in cells. */
|
|
90
92
|
columns: string;
|
|
91
93
|
/** JS property (set programmatically — `el.data = [...]`) — row records, an array of plain objects keyed to columns[].key. May also be supplied declaratively as a JSON-array `data="[…]"` attribute, hydrated once at connect (gh#288) — the same pattern chart-ui's `data` attribute uses. Custom accessor on the element class, not a reflected attribute. */
|
|
92
94
|
data: string;
|
|
@@ -20,7 +20,7 @@ composes:
|
|
|
20
20
|
- badge-ui
|
|
21
21
|
props:
|
|
22
22
|
columns:
|
|
23
|
-
description: Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, wrap?, valign?, accessor?, format?, render?, sortFn?, filterType?, meta?}. `valign` ('top'|'center') overrides the host [valign] for that column's body cells only — header cells always stay centered. Alternative to declarative <col-def> children (<col-def valign="top">); col-def also accepts `meta` as a JSON attribute (gh#925) — e.g. <col-def type="badge" meta='{"variants":{"Online":"success"}}'> — so cell-type meta (badge variant maps, currency codes) is expressible in static HTML.
|
|
23
|
+
description: Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, wrap?, valign?, accessor?, format?, render?, sortFn?, filterType?, meta?}. `valign` ('top'|'center') overrides the host [valign] for that column's body cells only — header cells always stay centered. Alternative to declarative <col-def> children (<col-def valign="top">); col-def also accepts `meta` as a JSON attribute (gh#925) — e.g. <col-def type="badge" meta='{"variants":{"Online":"success"}}'> — so cell-type meta (badge variant maps, currency codes) is expressible in static HTML. Truncation contract for `render` cells (gh#1681) — a `render(value, row, cell, dataIndex)` function's returned markup isn't guaranteed to be a bare <span>/<a> the way built-in cell types are, so single-line ellipsis truncation isn't automatic; set `data-truncate` on whichever rendered element (any tag, including a custom element) should single-line-ellipsis to opt it into the same contract built-in cells get for free. `[wrap]` (host) / `[data-wrap]` (per-column) override it back to multi-line, matching built-in cells.
|
|
24
24
|
type: array
|
|
25
25
|
default: []
|
|
26
26
|
dynamic: true # JS-set collection prop with a custom setter — deliberately not in static properties
|
|
@@ -85,6 +85,14 @@ props:
|
|
|
85
85
|
range select.
|
|
86
86
|
type: boolean
|
|
87
87
|
default: false
|
|
88
|
+
clickable-rows:
|
|
89
|
+
description: >-
|
|
90
|
+
Renders body rows with `cursor: pointer`. `row-click` fires unconditionally
|
|
91
|
+
whether or not a listener is attached, so the cursor can't be inferred from
|
|
92
|
+
listener presence — set this explicitly when row-click behavior is wired up
|
|
93
|
+
(master-detail, open-flyout, navigation) so rows read as actionable.
|
|
94
|
+
type: boolean
|
|
95
|
+
default: false
|
|
88
96
|
sortable:
|
|
89
97
|
description: Enable click-to-sort on column headers. Supports multi-sort via Shift+click.
|
|
90
98
|
type: boolean
|
|
@@ -294,7 +302,12 @@ a2ui:
|
|
|
294
302
|
needs search / filter / sort / columns visibility. Do NOT
|
|
295
303
|
re-implement those affordances in the card header — the toolbar
|
|
296
304
|
auto-wires search/filter/sort/columns changes into the bound
|
|
297
|
-
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.
|
|
298
311
|
- >-
|
|
299
312
|
Cells truncate single-line by default (v0.6.21 §403
|
|
300
313
|
truncate-default). Opt out per-table with [wrap] for whole-table
|