@adia-ai/web-components 0.8.5 → 0.8.7
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 +19 -1
- package/components/field/field.css +21 -0
- package/components/table/table.a2ui.json +1 -1
- package/components/table/table.class.js +17 -0
- package/components/table/table.css +15 -2
- package/components/table/table.d.ts +1 -1
- package/components/table/table.test.js +51 -0
- package/components/table/table.yaml +6 -1
- package/core/element.js +24 -0
- package/core/element.test.js +71 -0
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +1 -1
- package/dist/theme-provider.min.js +2 -2
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +50 -50
- package/dist/web-components.sheet.js +1 -1
- package/package.json +2 -1
- package/patterns/access-requests/access-requests.examples.html +132 -0
- package/patterns/access-requests/access-requests.html +60 -0
- package/patterns/admin-keys-and-export/admin-keys-and-export.examples.html +236 -0
- package/patterns/admin-keys-and-export/admin-keys-and-export.html +64 -0
- package/patterns/admin-shell/admin-shell.examples.html +327 -0
- package/patterns/admin-shell/admin-shell.html +44 -0
- package/patterns/agent-cost/agent-cost.examples.html +111 -0
- package/patterns/agent-cost/agent-cost.html +62 -0
- package/patterns/agent-memory/agent-memory.examples.html +155 -0
- package/patterns/agent-memory/agent-memory.html +64 -0
- package/patterns/agent-prompt-library/agent-prompt-library.examples.html +181 -0
- package/patterns/agent-prompt-library/agent-prompt-library.html +64 -0
- package/patterns/agent-tool-call/agent-tool-call.examples.html +123 -0
- package/patterns/agent-tool-call/agent-tool-call.html +60 -0
- package/patterns/alert-dialog/alert-dialog.examples.html +105 -0
- package/patterns/alert-dialog/alert-dialog.html +57 -0
- package/patterns/app-nav/app-nav.examples.html +421 -0
- package/patterns/app-nav/app-nav.html +50 -0
- package/patterns/approvals/approvals.examples.html +131 -0
- package/patterns/approvals/approvals.html +58 -0
- package/patterns/audit-log/audit-log.examples.html +74 -0
- package/patterns/audit-log/audit-log.examples.js +60 -0
- package/patterns/audit-log/audit-log.html +63 -0
- package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +89 -0
- package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.js +69 -0
- package/patterns/bulk-action-toolbar/bulk-action-toolbar.html +62 -0
- package/patterns/changelog-feed/changelog-feed.examples.html +151 -0
- package/patterns/changelog-feed/changelog-feed.html +57 -0
- package/patterns/chart-in-card/chart-in-card.examples.html +193 -0
- package/patterns/chart-in-card/chart-in-card.examples.js +46 -0
- package/patterns/chart-in-card/chart-in-card.html +66 -0
- package/patterns/chat-shell/chat-shell.examples.html +129 -0
- package/patterns/chat-shell/chat-shell.html +46 -0
- package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +241 -0
- package/patterns/comments-and-collaboration/comments-and-collaboration.html +66 -0
- package/patterns/component-tokens/component-tokens.examples.html +483 -0
- package/patterns/component-tokens/component-tokens.html +42 -0
- package/patterns/conversion-funnel/conversion-funnel.examples.html +158 -0
- package/patterns/conversion-funnel/conversion-funnel.examples.js +4 -0
- package/patterns/conversion-funnel/conversion-funnel.html +64 -0
- package/patterns/data-tables-inline-edit-and-tree/data-tables-inline-edit-and-tree.examples.html +196 -0
- package/patterns/data-tables-inline-edit-and-tree/data-tables-inline-edit-and-tree.html +58 -0
- package/patterns/diff-review/diff-review.examples.html +147 -0
- package/patterns/diff-review/diff-review.html +56 -0
- package/patterns/editor-shell/editor-shell.examples.html +139 -0
- package/patterns/editor-shell/editor-shell.html +62 -0
- package/patterns/export-flow/export-flow.examples.html +152 -0
- package/patterns/export-flow/export-flow.examples.js +37 -0
- package/patterns/export-flow/export-flow.html +65 -0
- package/patterns/filter-bar/filter-bar.examples.html +116 -0
- package/patterns/filter-bar/filter-bar.examples.js +23 -0
- package/patterns/filter-bar/filter-bar.html +70 -0
- package/patterns/form-drawer/form-drawer.examples.html +210 -0
- package/patterns/form-drawer/form-drawer.html +64 -0
- package/patterns/form-system/form-system.examples.html +383 -0
- package/patterns/form-system/form-system.html +48 -0
- package/patterns/forms-conditional-and-autocomplete/forms-conditional-and-autocomplete.examples.html +189 -0
- package/patterns/forms-conditional-and-autocomplete/forms-conditional-and-autocomplete.html +64 -0
- package/patterns/inline-dialog/inline-dialog.examples.html +143 -0
- package/patterns/inline-dialog/inline-dialog.html +59 -0
- package/patterns/kanban-board/kanban-board.examples.html +224 -0
- package/patterns/kanban-board/kanban-board.html +58 -0
- package/patterns/layout/layout.examples.html +365 -0
- package/patterns/layout/layout.html +54 -0
- package/patterns/marketing-engagement/marketing-engagement.examples.html +280 -0
- package/patterns/marketing-engagement/marketing-engagement.html +64 -0
- package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +213 -0
- package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.html +58 -0
- package/patterns/overview/overview.examples.html +119 -0
- package/patterns/overview/overview.html +42 -0
- package/patterns/permissions-matrix/permissions-matrix.examples.html +91 -0
- package/patterns/permissions-matrix/permissions-matrix.examples.js +28 -0
- package/patterns/permissions-matrix/permissions-matrix.html +56 -0
- package/patterns/permissions-role-picker/permissions-role-picker.examples.html +81 -0
- package/patterns/permissions-role-picker/permissions-role-picker.html +56 -0
- package/patterns/permissions-sharing/permissions-sharing.examples.html +175 -0
- package/patterns/permissions-sharing/permissions-sharing.html +66 -0
- package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +227 -0
- package/patterns/profile-public-and-verification/profile-public-and-verification.html +70 -0
- package/patterns/record-detail-drawer/record-detail-drawer.examples.html +164 -0
- package/patterns/record-detail-drawer/record-detail-drawer.html +64 -0
- package/patterns/responsive-shell-sidebar/README.md +37 -0
- package/patterns/responsive-shell-sidebar/responsive-shell-sidebar.css +39 -0
- package/patterns/responsive-shell-sidebar/responsive-shell-sidebar.examples.html +208 -0
- package/patterns/responsive-shell-sidebar/responsive-shell-sidebar.html +47 -0
- package/patterns/retention-cohort/retention-cohort.examples.html +149 -0
- package/patterns/retention-cohort/retention-cohort.examples.js +37 -0
- package/patterns/retention-cohort/retention-cohort.html +62 -0
- package/patterns/search-discovery/search-discovery.examples.html +263 -0
- package/patterns/search-discovery/search-discovery.html +58 -0
- package/patterns/system-banners/system-banners.examples.html +150 -0
- package/patterns/system-banners/system-banners.html +52 -0
- package/patterns/timezone-selector/timezone-selector.examples.html +82 -0
- package/patterns/timezone-selector/timezone-selector.examples.js +72 -0
- package/patterns/timezone-selector/timezone-selector.html +57 -0
- package/patterns/wizard-drawer/wizard-drawer.examples.html +144 -0
- package/patterns/wizard-drawer/wizard-drawer.examples.js +33 -0
- package/patterns/wizard-drawer/wizard-drawer.html +72 -0
- package/styles/colors/semantics/core.css +1 -1
- package/styles/colors/semantics/features.css +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-components
|
|
2
2
|
|
|
3
|
-
## [0.8.
|
|
3
|
+
## [0.8.7] — 2026-07-19
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **`dist/theme-provider.min.js`, `dist/web-components.min.js` rebuilt** (`npm run build:bundle-js`) — both had drifted stale relative to source; regenerated as part of 0.8.7 release pre-flight. No source change; `patterns/` also joins the package's published `files` (see `package.json`) so a consumer install resolves pattern source from `node_modules` (`adia-patterns` skill, `adia-ui-factory` `CHANGELOG.md`).
|
|
7
|
+
|
|
8
|
+
## [0.8.6] — 2026-07-18
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **SSR/late-upgrade attribute replay** (`core/element.js`, `core/element.test.js`; gh#284, PR #309): the custom-elements spec's "upgrade an element" algorithm (§4.13.5 step 6) requires replaying `attributeChangedCallback` for attributes already present before `connectedCallback` fires — happy-dom (this repo's test DOM) skips that replay entirely, and linkedom (the real SSR consumers' shim) is a similarly from-scratch registry. Net effect: any `reflect: true` property seeded only from server-rendered/pre-parsed HTML stayed at its class default after upgrade (`<nav-item-ui text="Profile">` rendered an empty label). `connectedCallback` now re-syncs every declared property from its live attribute before `connected()` runs — a no-op in spec-compliant environments (the setter's `Object.is` check short-circuits, no extra render pass). Regression tests: `describe('UIElement — SSR attribute-upgrade replay (gh#284)')`.
|
|
12
|
+
- **`table-ui` row-divider doubling** (`components/table/table.css`; TKT-0026, PR #247): body-cell dividers moved from `border-bottom` (every row, so the last row doubled against the container's own inset box-shadow perimeter) to `border-top` scoped `[role="row"]:not(:first-child)` (so the first row doesn't instead double against the header's border). The `[striped]` variant's stale `border-bottom: none` override updated to the new property in the same pass.
|
|
13
|
+
- **`field-ui` min-width floor** (`components/field/field.css`; gh#305, PR #315): a field sitting next to a grow field in a row no longer collapses to zero width (the State/City layout).
|
|
14
|
+
- **`--a-ui-bg-selected`** (`styles/colors/semantics/features.css`; gh#307, PR #315): resolves to the selected-container role instead of a scrim — nav-item selection reads as a surface again.
|
|
15
|
+
- **`--a-bg-muted`** (`styles/colors/semantics/core.css`; PR #314): resolves to `--md-sys-color-neutral-container-low` instead of the plain `-container` tier (which is `--a-canvas-well-strong`'s distinct-object tier) — muted backgrounds are subtler than "own object" surfaces. `verify:contrast` still 42/42 WCAG AA pairs, both schemes.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`table-ui` declarative `data="[…]"` attribute** (`components/table/table.{class.js,yaml}`; gh#288, PR #311): row records can now be supplied as a JSON-array HTML attribute, hydrated once at connect — the same pattern `chart-ui`'s `data` attribute already used — so a static-HTML or SSR-authored table renders its rows without a post-hydration wiring script. A programmatic `.data =` set before connect always wins; malformed JSON is ignored silently. Works together with `<col-def>` children for a fully static table. (`select-ui` and `chart-ui` already had declarative forms — `table-ui`'s row data was the actual gap in gh#288's scope.)
|
|
19
|
+
|
|
20
|
+
### Maintenance
|
|
21
|
+
- `dist/` bundles (CSS + JS + sheet + theme-provider) regenerated for the source changes above (PRs #309, #311, #316).
|
|
4
22
|
|
|
5
23
|
### Added
|
|
6
24
|
- **`<adia-mark-ui>`** (`components/adia-mark/adia-mark.{class.js,css,yaml}`; gh#283): the Adia brand mark as a token-driven primitive — background always reads `--a-canvas-surface-inverse`, the wordmark always reads `--a-fg-inverse`, so one component replaces the four hand-maintained static `agent-ui-{light,dark,light-transparent,dark-transparent}.svg` exports and adapts to scheme changes with zero asset swapping. Delivery shape (inlined custom element, not a bare SVG file or `icon-ui` registry entry) was constrained by the ticket's own finding: an `<img src="...svg">` or CSS `background-image: url(...)` reference cannot resolve the host page's CSS custom properties — only inlined markup can. `size` prop (xs/sm/md/lg/xl) matches `avatar-ui`'s scale. Verified live in both color schemes (Playwright) — background/foreground correctly invert together with no code change. Existing static-asset consumers (`public/icons/*.svg`, the a2ui corpus chunks, `apps/embedded-app`'s CSS-background brand mark) were deliberately left untouched — the static assets aren't deleted, so nothing breaks; migrating those ~35+ call sites is a separate, larger follow-up, out of scope for this ticket's `size:small` label.
|
|
@@ -43,6 +43,27 @@
|
|
|
43
43
|
width: 100%;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/* Exception: when a row-ui sibling is marked [grow], width:100% on the
|
|
47
|
+
OTHER fields becomes each one's flex main-size (the row's full width).
|
|
48
|
+
Two or more of those overflow the row, and under the negative-free-
|
|
49
|
+
space shrink algorithm the [grow] sibling's flex-basis:0% share gets
|
|
50
|
+
zeroed out entirely instead of receiving the leftover space (issue
|
|
51
|
+
305). Scoped to :has(> field-ui[grow]) so rows with NO grow winner —
|
|
52
|
+
where every field intentionally shares the row evenly via this same
|
|
53
|
+
width:100% overflow-shrink split (e.g. a 2-up expiry/CVC pair) — keep
|
|
54
|
+
that existing, relied-upon behavior.
|
|
55
|
+
|
|
56
|
+
min-width is also needed here, not just width:auto: an empty control's
|
|
57
|
+
own content-based min size is ~0 (input-ui's [slot="text"] is a bare
|
|
58
|
+
contenteditable span — the placeholder renders via an out-of-flow
|
|
59
|
+
`::before`, contributing no intrinsic width), so without a floor this
|
|
60
|
+
field can still collapse past legibility once the [grow] sibling
|
|
61
|
+
starts claiming space. */
|
|
62
|
+
:where(row-ui):has(> field-ui[grow]) > :scope:not([inline]):not([grow]) {
|
|
63
|
+
width: auto;
|
|
64
|
+
min-width: 4.5rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
46
67
|
/* Gate `row-gap` on actual message presence — otherwise the grid
|
|
47
68
|
reserves --field-gap (~8px) between the control row and a
|
|
48
69
|
zero-content message row, making field-ui[inline] visibly taller
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"const": "Table"
|
|
22
22
|
},
|
|
23
23
|
"data": {
|
|
24
|
-
"description": "
|
|
24
|
+
"description": "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.",
|
|
25
25
|
"$ref": "common_types.json#/$defs/DynamicObjectList"
|
|
26
26
|
},
|
|
27
27
|
"density": {
|
|
@@ -341,6 +341,23 @@ export class UITable extends UIElement {
|
|
|
341
341
|
// Parse declarative <col-def> children
|
|
342
342
|
this.#parseColDefs();
|
|
343
343
|
|
|
344
|
+
/* gh#288 — hydrate from an inline `data="[…]"` HTML attribute, same
|
|
345
|
+
pattern as chart-ui's `data` attribute (chart.class.js connected()).
|
|
346
|
+
The canonical entry point is the `.data` property (set
|
|
347
|
+
programmatically), but a static-HTML/SSR-authored table has no other
|
|
348
|
+
way to seed rows — `columns` has `<col-def>` children as its
|
|
349
|
+
declarative form; `data` had none. JSON-parse once at connect; a
|
|
350
|
+
malformed payload is ignored silently, and a later property
|
|
351
|
+
assignment still wins (guarded by the `#data.length === 0` check, so
|
|
352
|
+
a real programmatic `.data = [...]` set before connect is never
|
|
353
|
+
overwritten by a stale attribute). */
|
|
354
|
+
if (this.#data.length === 0 && this.hasAttribute('data')) {
|
|
355
|
+
try {
|
|
356
|
+
const parsed = JSON.parse(this.getAttribute('data'));
|
|
357
|
+
if (Array.isArray(parsed)) this.data = parsed;
|
|
358
|
+
} catch (_) { /* malformed JSON — leave empty, render() shows empty state */ }
|
|
359
|
+
}
|
|
360
|
+
|
|
344
361
|
this.setAttribute('role', 'grid');
|
|
345
362
|
this.setAttribute('tabindex', '0');
|
|
346
363
|
|
|
@@ -250,7 +250,6 @@
|
|
|
250
250
|
align-items: center;
|
|
251
251
|
align-self: stretch;
|
|
252
252
|
padding: var(--table-py) var(--table-px);
|
|
253
|
-
border-bottom: 1px solid var(--table-border);
|
|
254
253
|
min-width: 0;
|
|
255
254
|
/* Default: single line with ellipsis. Authors opt in to wrapping
|
|
256
255
|
via [wrap] on the host (whole table) or [data-wrap] on a column /
|
|
@@ -262,6 +261,17 @@
|
|
|
262
261
|
text-overflow: ellipsis;
|
|
263
262
|
}
|
|
264
263
|
|
|
264
|
+
/* Row divider as border-top, not border-bottom (TKT-0026) — a
|
|
265
|
+
border-bottom on every cell put the LAST row's own border at the
|
|
266
|
+
same coordinate as :scope's inset box-shadow perimeter (:133),
|
|
267
|
+
doubling the table's bottom edge. border-top divides every OTHER
|
|
268
|
+
adjacent row pair identically; :not(:first-child) keeps the first
|
|
269
|
+
body row from doubling against [data-header]'s own border-bottom
|
|
270
|
+
(:170) instead — the same class of collision, just relocated. */
|
|
271
|
+
[data-body] [role="row"]:not(:first-child) [role="gridcell"] {
|
|
272
|
+
border-top: 1px solid var(--table-border);
|
|
273
|
+
}
|
|
274
|
+
|
|
265
275
|
/* text-overflow on a flex container doesn't reach text nodes (anonymous
|
|
266
276
|
flex items). Built-in cell types now wrap text in <span>; link type
|
|
267
277
|
creates <a>; avatar/progress composite types use row-ui > span. */
|
|
@@ -342,8 +352,11 @@
|
|
|
342
352
|
|
|
343
353
|
/* ═══════ Striped ═══════ */
|
|
344
354
|
|
|
355
|
+
/* border-top, not border-bottom (TKT-0026) — matches the base rule's
|
|
356
|
+
own divider property now; striping uses background alone to
|
|
357
|
+
distinguish rows, no divider lines. */
|
|
345
358
|
:scope[striped] [data-body] [role="gridcell"] {
|
|
346
|
-
border-
|
|
359
|
+
border-top: none;
|
|
347
360
|
}
|
|
348
361
|
|
|
349
362
|
:scope[striped] [data-body] > [role="row"]:nth-child(even) {
|
|
@@ -88,7 +88,7 @@ export type TableSortEvent = CustomEvent<TableSortEventDetail>;
|
|
|
88
88
|
export class UITable extends UIElement {
|
|
89
89
|
/** Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, wrap?, accessor?, format?, render?, sortFn?, filterType?, meta?}. Alternative to declarative <col-def> children. */
|
|
90
90
|
columns: string;
|
|
91
|
-
/**
|
|
91
|
+
/** 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
92
|
data: string;
|
|
93
93
|
/** Controls cell padding and row height. 'compact' for dense data, 'standard' for default spacing, 'comfortable' for spacious rows. */
|
|
94
94
|
density: 'compact' | 'standard' | 'comfortable';
|
|
@@ -426,3 +426,54 @@ describe('table-ui — "markdown" cell type (TKT-0008)', () => {
|
|
|
426
426
|
expect(cell.textContent).toBe('Plain sentence, no special characters.');
|
|
427
427
|
});
|
|
428
428
|
});
|
|
429
|
+
|
|
430
|
+
// gh#288 — declarative `data="[…]"` attribute, mirroring chart-ui's existing
|
|
431
|
+
// pattern. Row data previously had no declarative form at all — `columns`
|
|
432
|
+
// already had `<col-def>` children; `data` had nothing, so a static-HTML or
|
|
433
|
+
// SSR-authored table always rendered its data empty.
|
|
434
|
+
describe('table-ui — declarative data="[…]" attribute (gh#288)', () => {
|
|
435
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
436
|
+
|
|
437
|
+
it('hydrates .data from a JSON-array data attribute at connect', async () => {
|
|
438
|
+
const el = mount(
|
|
439
|
+
`<table-ui data='${JSON.stringify(ROWS)}'></table-ui>`,
|
|
440
|
+
);
|
|
441
|
+
el.columns = COLS;
|
|
442
|
+
await raf();
|
|
443
|
+
expect(el.data).toEqual(ROWS);
|
|
444
|
+
expect(el.querySelectorAll('[role="gridcell"][data-key="name"]').length).toBe(2);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('a malformed data attribute is ignored silently, not thrown', async () => {
|
|
448
|
+
expect(() => mount(`<table-ui data='not json'></table-ui>`)).not.toThrow();
|
|
449
|
+
const el = document.querySelector('table-ui');
|
|
450
|
+
expect(el.data).toEqual([]);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('a non-array JSON value in the attribute is ignored, not coerced', async () => {
|
|
454
|
+
const el = mount(`<table-ui data='{"not":"an array"}'></table-ui>`);
|
|
455
|
+
await raf();
|
|
456
|
+
expect(el.data).toEqual([]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('a programmatic .data set before connect wins over the attribute', () => {
|
|
460
|
+
const el = document.createElement('table-ui');
|
|
461
|
+
el.setAttribute('data', JSON.stringify(ROWS));
|
|
462
|
+
el.data = [{ id: 99, name: 'Override', email: 'override@acme.com' }];
|
|
463
|
+
document.body.appendChild(el);
|
|
464
|
+
expect(el.data).toEqual([{ id: 99, name: 'Override', email: 'override@acme.com' }]);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('works together with declarative <col-def> children — fully static-HTML table', async () => {
|
|
468
|
+
const el = mount(`
|
|
469
|
+
<table-ui data='${JSON.stringify(ROWS)}'>
|
|
470
|
+
<col-def key="id" label="ID"></col-def>
|
|
471
|
+
<col-def key="name" label="Name"></col-def>
|
|
472
|
+
</table-ui>
|
|
473
|
+
`);
|
|
474
|
+
await raf();
|
|
475
|
+
expect(el.data).toEqual(ROWS);
|
|
476
|
+
expect(el.columns.length).toBe(2);
|
|
477
|
+
expect(el.querySelectorAll('[role="gridcell"][data-key="name"]').length).toBe(2);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
@@ -25,7 +25,12 @@ props:
|
|
|
25
25
|
default: []
|
|
26
26
|
dynamic: true # JS-set collection prop with a custom setter — deliberately not in static properties
|
|
27
27
|
data:
|
|
28
|
-
description:
|
|
28
|
+
description: >-
|
|
29
|
+
JS property (set programmatically — `el.data = [...]`) — row records,
|
|
30
|
+
an array of plain objects keyed to columns[].key. May also be supplied
|
|
31
|
+
declaratively as a JSON-array `data="[…]"` attribute, hydrated once at
|
|
32
|
+
connect (gh#288) — the same pattern chart-ui's `data` attribute uses.
|
|
33
|
+
Custom accessor on the element class, not a reflected attribute.
|
|
29
34
|
type: array
|
|
30
35
|
items:
|
|
31
36
|
type: object
|
package/core/element.js
CHANGED
|
@@ -158,6 +158,30 @@ export class UIElement extends HTMLElement {
|
|
|
158
158
|
if (!ctor._tag) ctor._tag = this.localName;
|
|
159
159
|
adoptStyles(ctor);
|
|
160
160
|
prepareParts(ctor);
|
|
161
|
+
// gh#284 — the custom-elements spec's "upgrade an element" algorithm
|
|
162
|
+
// (§4.13.5 step 6) requires replaying attributeChangedCallback for
|
|
163
|
+
// every attribute already present on the element BEFORE
|
|
164
|
+
// connectedCallback fires. Several DOM shims used for SSR (linkedom)
|
|
165
|
+
// and this repo's own happy-dom test environment skip that replay —
|
|
166
|
+
// confirmed directly (element.test.js, "SSR attribute-upgrade replay
|
|
167
|
+
// (gh#284)"): a bare custom element upgraded with a pre-existing
|
|
168
|
+
// observed attribute gets connectedCallback but never the matching
|
|
169
|
+
// attributeChangedCallback.
|
|
170
|
+
// Any reflected property whose only value came from server-rendered
|
|
171
|
+
// or otherwise pre-parsed HTML is silently stuck at its class
|
|
172
|
+
// default — e.g. <nav-item-ui text="Profile"> renders an empty
|
|
173
|
+
// label. connectedCallback fires reliably everywhere, so re-sync
|
|
174
|
+
// every declared property from its live attribute here rather than
|
|
175
|
+
// trusting the shim's upgrade-reaction queue. A no-op in spec-
|
|
176
|
+
// compliant environments: the value already matches (the property
|
|
177
|
+
// setter's Object.is check short-circuits), so this never causes an
|
|
178
|
+
// extra render pass on browsers/environments that already got it right.
|
|
179
|
+
for (const [key, cfg] of Object.entries(ctor.properties)) {
|
|
180
|
+
const attr = cfg.attribute ?? key.toLowerCase();
|
|
181
|
+
if (this.hasAttribute(attr)) {
|
|
182
|
+
this[key] = parseAttr(this.getAttribute(attr), cfg.type ?? String);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
161
185
|
// connected() commonly reads this element's reactive properties via
|
|
162
186
|
// template-literal interpolation (`${this.label}` etc.) to stamp its
|
|
163
187
|
// inner DOM. If the outer call path is inside another effect (e.g.
|
package/core/element.test.js
CHANGED
|
@@ -316,3 +316,74 @@ describe('UIElement — SSR browser-API absence (gh#285)', () => {
|
|
|
316
316
|
}
|
|
317
317
|
});
|
|
318
318
|
});
|
|
319
|
+
|
|
320
|
+
// gh#284 — the custom-elements spec's "upgrade an element" algorithm
|
|
321
|
+
// (§4.13.5 step 6) requires replaying attributeChangedCallback for every
|
|
322
|
+
// attribute already present on an element BEFORE connectedCallback fires
|
|
323
|
+
// on upgrade. happy-dom (this repo's own test DOM) skips that replay —
|
|
324
|
+
// confirmed directly: a bare custom element upgraded with a pre-existing
|
|
325
|
+
// observed attribute gets connectedCallback but never the matching
|
|
326
|
+
// attributeChangedCallback. linkedom (the real SSR consumer's DOM shim,
|
|
327
|
+
// per adia-ssr) is a similarly from-scratch custom-elements registry, so
|
|
328
|
+
// the same gap is expected there too. Simulating the absence directly
|
|
329
|
+
// (a real late-upgrade sequence, not a mock) rather than trusting that
|
|
330
|
+
// happy-dom already reproduces the SSR shape — it does, which is exactly
|
|
331
|
+
// how this was found.
|
|
332
|
+
describe('UIElement — SSR attribute-upgrade replay (gh#284)', () => {
|
|
333
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
334
|
+
|
|
335
|
+
// Parses markup into an UNDEFINED custom element (so its attributes land
|
|
336
|
+
// on the element the way a server-rendered / pre-parsed document would),
|
|
337
|
+
// THEN defines the tag — matching the "define after parse" shape SSR
|
|
338
|
+
// hydration and lazy client registration both produce.
|
|
339
|
+
function mountLateUpgrade(tag, BaseImpl, html) {
|
|
340
|
+
const container = document.createElement('div');
|
|
341
|
+
container.innerHTML = html;
|
|
342
|
+
document.body.appendChild(container);
|
|
343
|
+
customElements.define(tag, BaseImpl);
|
|
344
|
+
return container.querySelector(tag);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
it('confirms the environment gap this fix compensates for', () => {
|
|
348
|
+
const tag = `test-plain-${++registrationCounter}`;
|
|
349
|
+
const calls = [];
|
|
350
|
+
class Plain extends HTMLElement {
|
|
351
|
+
static get observedAttributes() { return ['foo']; }
|
|
352
|
+
attributeChangedCallback(name, oldV, newV) { calls.push([name, oldV, newV]); }
|
|
353
|
+
connectedCallback() { calls.push(['connected']); }
|
|
354
|
+
}
|
|
355
|
+
mountLateUpgrade(tag, Plain, `<${tag} foo="bar"></${tag}>`);
|
|
356
|
+
expect(calls).toEqual([['connected']]); // NOT ['foo', null, 'bar'] first, per spec
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('a reflected property set only via pre-parsed HTML is not stuck at its class default', () => {
|
|
360
|
+
const tag = `test-late-${++registrationCounter}`;
|
|
361
|
+
class El extends UIElement {
|
|
362
|
+
static properties = { label: { type: String, default: '', reflect: true } };
|
|
363
|
+
}
|
|
364
|
+
const el = mountLateUpgrade(tag, El, `<${tag} label="Profile"></${tag}>`);
|
|
365
|
+
expect(el.label).toBe('Profile');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('a reflected boolean attribute present-with-no-value resolves true, not the class default', () => {
|
|
369
|
+
const tag = `test-late-bool-${++registrationCounter}`;
|
|
370
|
+
class El extends UIElement {
|
|
371
|
+
static properties = { active: { type: Boolean, default: false, reflect: true } };
|
|
372
|
+
}
|
|
373
|
+
const el = mountLateUpgrade(tag, El, `<${tag} active></${tag}>`);
|
|
374
|
+
expect(el.active).toBe(true);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('no regression: normal define-before-parse still gets attributeChangedCallback (no extra render)', async () => {
|
|
378
|
+
let renders = 0;
|
|
379
|
+
class El extends UIElement {
|
|
380
|
+
static properties = { label: { type: String, default: '', reflect: true } };
|
|
381
|
+
render() { renders++; }
|
|
382
|
+
}
|
|
383
|
+
const tag = registerTestElement(El);
|
|
384
|
+
const el = mount(tag, { label: 'Profile' });
|
|
385
|
+
await tick();
|
|
386
|
+
expect(el.label).toBe('Profile');
|
|
387
|
+
expect(renders).toBe(1); // the connectedCallback re-sync is a no-op here, not a second render
|
|
388
|
+
});
|
|
389
|
+
});
|