@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
package/components/nav/nav.yaml
CHANGED
|
@@ -38,7 +38,7 @@ props:
|
|
|
38
38
|
heading:
|
|
39
39
|
type: string
|
|
40
40
|
default: ''
|
|
41
|
-
description: "Optional kicker label. Section variant renders it via ::before; primary uses it as aria-label only."
|
|
41
|
+
description: "Optional kicker label. Section variant renders it via ::before; primary uses it as aria-label only. For a VISIBLE kicker inside a primary-variant rail — or more than one kicker per <nav-ui> — hand-place <span data-nav-label> in the default slot instead; see slots below."
|
|
42
42
|
multiExpand:
|
|
43
43
|
type: boolean
|
|
44
44
|
default: false
|
|
@@ -62,7 +62,7 @@ events:
|
|
|
62
62
|
|
|
63
63
|
slots:
|
|
64
64
|
default:
|
|
65
|
-
description: "Primary slot — accepts <nav-group-ui> + <nav-item-ui> children, plus <hr data-nav-divider> for hand-placed dividers."
|
|
65
|
+
description: "Primary slot — accepts <nav-group-ui> + <nav-item-ui> children, plus <hr data-nav-divider> for hand-placed dividers and <span data-nav-label> for hand-placed group-label kickers (titled runs of items/groups that aren't wrapped in a <nav-group-ui>). <span data-nav-label> renders with the same uppercase/tracking/muted kicker treatment as the [heading] ::before kicker, and is hidden alongside dividers whenever the primary-variant rail collapses ([collapsed] or ≤96px container width)."
|
|
66
66
|
|
|
67
67
|
states:
|
|
68
68
|
- name: idle
|
|
@@ -79,7 +79,18 @@ a2ui:
|
|
|
79
79
|
wrapped in <section-ui> for app sidebars; inside an
|
|
80
80
|
<aside data-subnav> with variant="section" for section / subnav
|
|
81
81
|
rails; standalone on docs / auth pages. Children: <nav-group-ui>,
|
|
82
|
-
<nav-item-ui>, optional <hr data-nav-divider
|
|
82
|
+
<nav-item-ui>, optional <hr data-nav-divider>, optional
|
|
83
|
+
<span data-nav-label> (hand-placed titled section header).
|
|
84
|
+
- >-
|
|
85
|
+
Titled nav-item groups: hand-place <span data-nav-label>Text</span>
|
|
86
|
+
directly in the default slot, before a run of <nav-item-ui> /
|
|
87
|
+
<nav-group-ui> children, to render a section-header kicker — the
|
|
88
|
+
same uppercase/tracking/muted treatment as the [heading] ::before
|
|
89
|
+
kicker on variant="section", but usable anywhere in the child list
|
|
90
|
+
(not limited to one kicker per <nav-ui>) and always visually
|
|
91
|
+
rendered, unlike [heading] which is aria-only on variant="primary".
|
|
92
|
+
Hidden automatically whenever the primary-variant rail collapses
|
|
93
|
+
([collapsed] or ≤96px container width), same as <hr data-nav-divider>.
|
|
83
94
|
- >-
|
|
84
95
|
Variants: variant="primary" (default) — app sidebar; ResizeObserver
|
|
85
96
|
collapses to icon-only ≤96px; collapsible groups open as a popover
|
|
@@ -17,13 +17,31 @@
|
|
|
17
17
|
selected — bg container · icon + text ON-SURFACE
|
|
18
18
|
The selected icon no longer takes primary (supersedes gh#522's
|
|
19
19
|
glyph ruling); Figma's neutral/surfaceBright was considered for the
|
|
20
|
-
selected fill and RULED OUT — container stays (gh#795's mirror).
|
|
21
|
-
|
|
20
|
+
selected fill and RULED OUT — container stays (gh#795's mirror).
|
|
21
|
+
gh#968's ladder above is about ROUTE selection (hover/selected);
|
|
22
|
+
it says nothing about the group's own open/closed disclosure state,
|
|
23
|
+
which --nav-group-fg left pinned unconditionally to one role
|
|
24
|
+
(neutral-high) regardless of [open]. gh#1740 (operator ruling,
|
|
25
|
+
2026-08-19) supersedes that unconditional pin with a second,
|
|
26
|
+
orthogonal axis — the header text and caret glyph now also shift
|
|
27
|
+
role by [open], the same way they already shift by [selected]/
|
|
28
|
+
hover; the two ladders compose (a selected-route header still wins
|
|
29
|
+
its --nav-group-fg-selected treatment regardless of open state —
|
|
30
|
+
see the :scope[open] rule below, kept lower-precedence than
|
|
31
|
+
:scope[selected] by source order). */
|
|
32
|
+
--nav-group-fg: var(--md-sys-color-neutral);
|
|
33
|
+
--nav-group-fg-active: var(--md-sys-color-neutral-high);
|
|
22
34
|
--nav-group-fg-hover: var(--a-fg-strong);
|
|
23
35
|
--nav-group-fg-muted: var(--a-fg-muted);
|
|
24
36
|
--nav-group-fg-selected: var(--a-fg-strong);
|
|
25
37
|
--nav-group-icon-fg: var(--md-sys-color-neutral);
|
|
26
38
|
--nav-group-icon-fg-selected: var(--a-fg-strong);
|
|
39
|
+
/* Caret role ladder (gh#1740) — idle/closed: NEUTRAL-LOW (one step
|
|
40
|
+
quieter than the muted badge/kicker role); open: NEUTRAL, matching
|
|
41
|
+
the icon's own idle role so the caret doesn't outshine the icon
|
|
42
|
+
once the group is expanded. */
|
|
43
|
+
--nav-group-caret-fg: var(--md-sys-color-neutral-low);
|
|
44
|
+
--nav-group-caret-fg-active: var(--md-sys-color-neutral);
|
|
27
45
|
--nav-group-bg-hover: var(--a-bg-muted);
|
|
28
46
|
--nav-group-bg-selected: var(--a-bg-selected); /* mirrors --nav-item-bg-selected (gh#795) */
|
|
29
47
|
--nav-group-icon-size: calc(var(--nav-group-row-height) - var(--a-space-2));
|
|
@@ -71,6 +89,20 @@
|
|
|
71
89
|
background: var(--nav-group-indent-rail-bg, var(--md-sys-color-neutral-outline-variant));
|
|
72
90
|
pointer-events: none;
|
|
73
91
|
}
|
|
92
|
+
/* Disclosure-state ladder (gh#1740, operator ruling 2026-08-19): the
|
|
93
|
+
header text and caret glyph step UP a role when the group is open —
|
|
94
|
+
idle/closed reads NEUTRAL, expanded reads NEUTRAL-HIGH. Placed BEFORE
|
|
95
|
+
the [selected]/[data-selected-within] block below so an equal-
|
|
96
|
+
specificity (0,3,0) tie always resolves to the route-selection color:
|
|
97
|
+
a selected-route group keeps its existing --nav-group-fg-selected
|
|
98
|
+
treatment regardless of its own open/closed state — this rule only
|
|
99
|
+
ever wins when the group is open AND not selected. */
|
|
100
|
+
:scope[open] > [slot="header"] {
|
|
101
|
+
color: var(--nav-group-fg-active);
|
|
102
|
+
}
|
|
103
|
+
:scope[open] > [slot="header"] [slot="caret"] {
|
|
104
|
+
color: var(--nav-group-caret-fg-active);
|
|
105
|
+
}
|
|
74
106
|
/* Selected-look header treatment keys on [selected] — the CSS half of
|
|
75
107
|
gh issue 459 (PR 460 corrected the icon weight): the group CONTAINING
|
|
76
108
|
the selected route reads strong; a merely-expanded group does not.
|
|
@@ -174,7 +206,9 @@
|
|
|
174
206
|
|
|
175
207
|
[slot="header"] [slot="caret"] {
|
|
176
208
|
/*--a-icon-size: 0.5rem;*/
|
|
177
|
-
|
|
209
|
+
/* idle/closed caret = NEUTRAL-LOW (gh#1740 role ladder); steps up to
|
|
210
|
+
--nav-group-caret-fg-active via the :scope[open] rule above. */
|
|
211
|
+
color: var(--nav-group-caret-fg);
|
|
178
212
|
transition:
|
|
179
213
|
transform var(--nav-duration-fast) var(--nav-easing),
|
|
180
214
|
color var(--nav-duration-fast) var(--nav-easing);
|
|
@@ -41,11 +41,6 @@
|
|
|
41
41
|
"type": "boolean",
|
|
42
42
|
"default": false
|
|
43
43
|
},
|
|
44
|
-
"port-size": {
|
|
45
|
-
"description": "Port indicator dot size in pixels",
|
|
46
|
-
"type": "number",
|
|
47
|
-
"default": 10
|
|
48
|
-
},
|
|
49
44
|
"portSize": {
|
|
50
45
|
"description": "Port indicator size",
|
|
51
46
|
"type": "number",
|
|
@@ -56,21 +51,11 @@
|
|
|
56
51
|
"type": "boolean",
|
|
57
52
|
"default": false
|
|
58
53
|
},
|
|
59
|
-
"show-ports": {
|
|
60
|
-
"description": "Show port indicator dots. Defaults to true when editable",
|
|
61
|
-
"type": "boolean",
|
|
62
|
-
"default": false
|
|
63
|
-
},
|
|
64
54
|
"showPorts": {
|
|
65
55
|
"description": "Show port indicators",
|
|
66
56
|
"type": "boolean",
|
|
67
57
|
"default": false
|
|
68
58
|
},
|
|
69
|
-
"stroke-width": {
|
|
70
|
-
"description": "Stroke width in pixels",
|
|
71
|
-
"type": "number",
|
|
72
|
-
"default": 2
|
|
73
|
-
},
|
|
74
59
|
"strokeWidth": {
|
|
75
60
|
"description": "Line stroke width",
|
|
76
61
|
"type": "number",
|
|
@@ -30,10 +30,6 @@ props:
|
|
|
30
30
|
description: Allow interactive creation/deletion of connections via drag
|
|
31
31
|
type: boolean
|
|
32
32
|
default: false
|
|
33
|
-
port-size:
|
|
34
|
-
description: Port indicator dot size in pixels
|
|
35
|
-
type: number
|
|
36
|
-
default: 10
|
|
37
33
|
portSize:
|
|
38
34
|
description: Port indicator size
|
|
39
35
|
type: number
|
|
@@ -44,19 +40,11 @@ props:
|
|
|
44
40
|
type: boolean
|
|
45
41
|
default: false
|
|
46
42
|
reflect: true
|
|
47
|
-
show-ports:
|
|
48
|
-
description: Show port indicator dots. Defaults to true when editable
|
|
49
|
-
type: boolean
|
|
50
|
-
default: false
|
|
51
43
|
showPorts:
|
|
52
44
|
description: Show port indicators
|
|
53
45
|
type: boolean
|
|
54
46
|
default: false
|
|
55
47
|
attribute: show-ports
|
|
56
|
-
stroke-width:
|
|
57
|
-
description: Stroke width in pixels
|
|
58
|
-
type: number
|
|
59
|
-
default: 2
|
|
60
48
|
strokeWidth:
|
|
61
49
|
description: Line stroke width
|
|
62
50
|
type: number
|
|
@@ -87,6 +87,16 @@ export class UINumberFormat extends UIElement {
|
|
|
87
87
|
this.textContent = text;
|
|
88
88
|
// Mirror raw value via aria-label so screen readers can read the
|
|
89
89
|
// underlying number alongside the formatted glyphs.
|
|
90
|
+
//
|
|
91
|
+
// gh#1647 judgment call (triage-corrections comment, 2026-08-18):
|
|
92
|
+
// this aria-label is intentional component-owned chrome, not a
|
|
93
|
+
// consumer-overridable name — it mirrors the RAW underlying value
|
|
94
|
+
// ("42% (raw: 0.42)") specifically for a11y clarity, so a consumer
|
|
95
|
+
// override would likely defeat that purpose. Deliberately NOT ported
|
|
96
|
+
// to the value-tracking `#lastAutoAriaLabel` guard (core/a11y.js)
|
|
97
|
+
// that the other five gh#1647 primitives got — kept unconditional,
|
|
98
|
+
// auto-deriving on every render. Revisit only if a stated consumer
|
|
99
|
+
// need for overriding it surfaces.
|
|
90
100
|
if (text && Number.isFinite(Number(this.value))) {
|
|
91
101
|
this.setAttribute('aria-label', `${text} (raw: ${this.value})`);
|
|
92
102
|
} else {
|
|
@@ -22,6 +22,33 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { UIElement } from '../../core/element.js';
|
|
25
|
+
import { KEY_MAP } from '../../core/template.js';
|
|
26
|
+
|
|
27
|
+
// ── SSR idempotent-write guard (gh#1755, follow-up to table-ui's gh#1678) ──
|
|
28
|
+
//
|
|
29
|
+
// Local copies of table.class.js's own setAttrIfChanged()/removeAttrIfPresent()
|
|
30
|
+
// — module-local there too (not exported), so a shared-utility extraction for
|
|
31
|
+
// two call sites would be a premature abstraction; these mirror that shape.
|
|
32
|
+
//
|
|
33
|
+
/**
|
|
34
|
+
* Idempotent attribute set — skip the DOM write entirely when the value
|
|
35
|
+
* already matches. `setAttribute` mutates (and queues a MutationObserver
|
|
36
|
+
* record) even when the new value is byte-identical to the old one, so a
|
|
37
|
+
* per-item attribute this render path sets on EVERY call (`aria-current`,
|
|
38
|
+
* `variant`, `disabled`, `tabindex`, `text`, `aria-label`, `data-value`,
|
|
39
|
+
* `size`) must compare-before-write to let a byte-identical adopted SSR
|
|
40
|
+
* `<nav>` (gh#1687) survive re-render with zero mutations — bare
|
|
41
|
+
* `setAttribute` cannot give that guarantee even when the value never
|
|
42
|
+
* actually changes.
|
|
43
|
+
*/
|
|
44
|
+
function setAttrIfChanged(el, name, value) {
|
|
45
|
+
if (el.getAttribute(name) !== value) el.setAttribute(name, value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Idempotent attribute removal — same rationale as setAttrIfChanged. */
|
|
49
|
+
function removeAttrIfPresent(el, name) {
|
|
50
|
+
if (el.hasAttribute(name)) el.removeAttribute(name);
|
|
51
|
+
}
|
|
25
52
|
|
|
26
53
|
export class UIPagination extends UIElement {
|
|
27
54
|
static properties = {
|
|
@@ -48,10 +75,32 @@ export class UIPagination extends UIElement {
|
|
|
48
75
|
|
|
49
76
|
connected() {
|
|
50
77
|
if (!this.#nav) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
// Adopt a pre-existing server-rendered <nav slot="nav"> instead of
|
|
79
|
+
// unconditionally appending a fresh one — first-connect adoption
|
|
80
|
+
// (gh#1687). Steady-state reconcile is unaffected; this only guards
|
|
81
|
+
// the construction-time #nav field being null on upgrade.
|
|
82
|
+
this.#nav = this.querySelector(':scope > nav[slot="nav"]');
|
|
83
|
+
if (this.#nav) {
|
|
84
|
+
// Adopting an existing nav is only half the fix — reconcile()'s
|
|
85
|
+
// own keyed-diff (element.js) keys off a WeakMap-shaped
|
|
86
|
+
// `parent[KEY_MAP]` populated by THIS element's own prior render
|
|
87
|
+
// calls, which a freshly-parsed SSR fragment never has. Without
|
|
88
|
+
// seeding it here, the very first render treats every adopted
|
|
89
|
+
// child as unrecognized, stamps a full set of BRAND NEW nodes
|
|
90
|
+
// alongside them, and never removes the orphaned originals —
|
|
91
|
+
// silently doubling the button count inside the (correctly
|
|
92
|
+
// singular) adopted <nav>. Positionally zip the adopted
|
|
93
|
+
// children against the SAME key order #buildRange would
|
|
94
|
+
// produce right now; a shape mismatch (stale/hand-edited SSR
|
|
95
|
+
// markup) is left unseeded so reconcile falls back to its
|
|
96
|
+
// normal rebuild-from-scratch behavior instead of miskeying.
|
|
97
|
+
this.#seedKeyMapFromAdoptedNav();
|
|
98
|
+
} else {
|
|
99
|
+
this.#nav = document.createElement('nav');
|
|
100
|
+
this.#nav.setAttribute('slot', 'nav');
|
|
101
|
+
this.#nav.setAttribute('aria-label', 'Pagination');
|
|
102
|
+
this.appendChild(this.#nav);
|
|
103
|
+
}
|
|
55
104
|
}
|
|
56
105
|
|
|
57
106
|
if (!this.#bound) {
|
|
@@ -69,6 +118,67 @@ export class UIPagination extends UIElement {
|
|
|
69
118
|
this.#bound = false;
|
|
70
119
|
}
|
|
71
120
|
|
|
121
|
+
// Review finding (PR #1692) — the tag/marker a given item.type produces
|
|
122
|
+
// via #createItem(): ellipsis is a bare <span data-ellipsis>; prev/next/
|
|
123
|
+
// page are all <button-ui> distinguished by their own data-* marker
|
|
124
|
+
// (page additionally carries data-value). A same-LENGTH adopted child
|
|
125
|
+
// that doesn't match this shape (e.g. a plain <span> standing in for a
|
|
126
|
+
// page-cell <button-ui>) would still get SEEDED by a count-only check,
|
|
127
|
+
// then #updateItem() — which only sets attributes, never replaces the
|
|
128
|
+
// element — leaves it permanently the wrong tag. Checking shape here
|
|
129
|
+
// is what makes the fallback in #seedKeyMapFromAdoptedNav trustworthy.
|
|
130
|
+
#childMatchesItem(el, item) {
|
|
131
|
+
if (item.type === 'ellipsis') {
|
|
132
|
+
return el.tagName === 'SPAN' && el.hasAttribute('data-ellipsis');
|
|
133
|
+
}
|
|
134
|
+
if (el.tagName !== 'BUTTON-UI') return false;
|
|
135
|
+
if (item.type === 'prev') return el.hasAttribute('data-prev');
|
|
136
|
+
if (item.type === 'next') return el.hasAttribute('data-next');
|
|
137
|
+
return el.hasAttribute('data-page') && el.dataset.value === String(item.value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// gh#1687 — see the call site's comment in connected(). Positionally
|
|
141
|
+
// zips the adopted nav's existing children against the key order the
|
|
142
|
+
// very next render() will compute, seeding reconcile()'s own
|
|
143
|
+
// `parent[KEY_MAP]` (element.js) so that render finds every item
|
|
144
|
+
// already present instead of stamping a duplicate full set.
|
|
145
|
+
#seedKeyMapFromAdoptedNav() {
|
|
146
|
+
const page = Math.max(1, Math.min(this.page, this.total));
|
|
147
|
+
const total = Math.max(1, this.total);
|
|
148
|
+
const siblings = Math.max(0, this.siblings);
|
|
149
|
+
const range = this.#buildRange(page, total, siblings);
|
|
150
|
+
const children = [...this.#nav.children];
|
|
151
|
+
const shapeMatches = children.length === range.length
|
|
152
|
+
&& range.every((item, i) => this.#childMatchesItem(children[i], item));
|
|
153
|
+
if (!shapeMatches) {
|
|
154
|
+
// Review finding (PR #1692, two rounds) — a bare return here left
|
|
155
|
+
// the mismatch case worse off than doing nothing: reconcile()'s
|
|
156
|
+
// own cleanup loop only removes entries it finds IN
|
|
157
|
+
// `parent[KEY_MAP]`, so an unseeded map means every existing
|
|
158
|
+
// child is invisible to it — the next render() then stamps a
|
|
159
|
+
// full fresh set AND the stale adopted children survive alongside
|
|
160
|
+
// it (reproduced: 3 stale placeholder buttons + a correct fresh
|
|
161
|
+
// 9-item render = 12 nodes). Length alone isn't a reliable
|
|
162
|
+
// correlation signal either — a same-length fixture with the
|
|
163
|
+
// WRONG tag/marker per position (e.g. N bare <span>s standing in
|
|
164
|
+
// for N <button-ui> page-cells) would pass a count-only check and
|
|
165
|
+
// get seeded, then #updateItem() — attribute-only, never a tag
|
|
166
|
+
// swap — leaves it permanently wrong. Either kind of mismatch
|
|
167
|
+
// means the adopted markup can't be trusted to correlate with the
|
|
168
|
+
// next render's key order at all — abandon adoption for real:
|
|
169
|
+
// clear the stale children so the fallback is an actual clean
|
|
170
|
+
// rebuild, the same shape #nav would be in if it had just been
|
|
171
|
+
// freshly created.
|
|
172
|
+
this.#nav.replaceChildren();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const map = new Map();
|
|
176
|
+
for (let i = 0; i < range.length; i++) {
|
|
177
|
+
map.set(range[i].key, children[i]);
|
|
178
|
+
}
|
|
179
|
+
this.#nav[KEY_MAP] = map;
|
|
180
|
+
}
|
|
181
|
+
|
|
72
182
|
render() {
|
|
73
183
|
if (!this.#nav) return;
|
|
74
184
|
|
|
@@ -167,21 +277,21 @@ export class UIPagination extends UIElement {
|
|
|
167
277
|
}
|
|
168
278
|
|
|
169
279
|
const btn = document.createElement('button-ui');
|
|
170
|
-
btn
|
|
280
|
+
setAttrIfChanged(btn, 'size', this.size);
|
|
171
281
|
|
|
172
282
|
if (item.type === 'prev') {
|
|
173
283
|
btn.setAttribute('data-prev', '');
|
|
174
|
-
btn
|
|
175
|
-
btn
|
|
284
|
+
setAttrIfChanged(btn, 'icon', 'caret-left');
|
|
285
|
+
setAttrIfChanged(btn, 'aria-label', 'Previous page');
|
|
176
286
|
} else if (item.type === 'next') {
|
|
177
287
|
btn.setAttribute('data-next', '');
|
|
178
|
-
btn
|
|
179
|
-
btn
|
|
288
|
+
setAttrIfChanged(btn, 'icon', 'caret-right');
|
|
289
|
+
setAttrIfChanged(btn, 'aria-label', 'Next page');
|
|
180
290
|
} else {
|
|
181
291
|
btn.setAttribute('data-page', '');
|
|
182
|
-
btn
|
|
183
|
-
btn
|
|
184
|
-
btn
|
|
292
|
+
setAttrIfChanged(btn, 'data-value', String(item.value));
|
|
293
|
+
setAttrIfChanged(btn, 'text', String(item.value));
|
|
294
|
+
setAttrIfChanged(btn, 'aria-label', `Page ${item.value}`);
|
|
185
295
|
}
|
|
186
296
|
|
|
187
297
|
this.#updateItem(btn, item, page);
|
|
@@ -192,32 +302,36 @@ export class UIPagination extends UIElement {
|
|
|
192
302
|
if (item.type === 'ellipsis') return;
|
|
193
303
|
|
|
194
304
|
// Keep size in sync \u2014 the host's [size] may change between renders.
|
|
195
|
-
|
|
305
|
+
// gh#1755 \u2014 compare-before-write: this runs on EVERY render pass
|
|
306
|
+
// (including one triggered by an unrelated reactive dependency), so a
|
|
307
|
+
// bare setAttribute() would re-mutate an already-correct adopted
|
|
308
|
+
// gh#1687 SSR `<nav>` child even when nothing here actually changed.
|
|
309
|
+
setAttrIfChanged(el, 'size', this.size);
|
|
196
310
|
|
|
197
311
|
if (item.type === 'prev') {
|
|
198
|
-
el
|
|
199
|
-
if (page <= 1) { el
|
|
200
|
-
else { el
|
|
312
|
+
setAttrIfChanged(el, 'variant', this.#restVariant());
|
|
313
|
+
if (page <= 1) { setAttrIfChanged(el, 'disabled', ''); setAttrIfChanged(el, 'tabindex', '-1'); }
|
|
314
|
+
else { removeAttrIfPresent(el, 'disabled'); setAttrIfChanged(el, 'tabindex', '0'); }
|
|
201
315
|
} else if (item.type === 'next') {
|
|
202
|
-
el
|
|
203
|
-
if (page >= this.total) { el
|
|
204
|
-
else { el
|
|
316
|
+
setAttrIfChanged(el, 'variant', this.#restVariant());
|
|
317
|
+
if (page >= this.total) { setAttrIfChanged(el, 'disabled', ''); setAttrIfChanged(el, 'tabindex', '-1'); }
|
|
318
|
+
else { removeAttrIfPresent(el, 'disabled'); setAttrIfChanged(el, 'tabindex', '0'); }
|
|
205
319
|
} else {
|
|
206
|
-
el
|
|
207
|
-
el
|
|
208
|
-
el
|
|
320
|
+
setAttrIfChanged(el, 'data-value', String(item.value));
|
|
321
|
+
setAttrIfChanged(el, 'text', String(item.value));
|
|
322
|
+
setAttrIfChanged(el, 'aria-label', `Page ${item.value}`);
|
|
209
323
|
if (item.value === page) {
|
|
210
324
|
// Active page reads as `variant="primary"` so the filled-accent
|
|
211
325
|
// state comes from button-ui's primary surface matrix (the
|
|
212
326
|
// canonical token chain) \u2014 not a pagination-tier re-impl.
|
|
213
|
-
el
|
|
327
|
+
setAttrIfChanged(el, 'variant', 'primary');
|
|
214
328
|
// aria-current="page" alone carries the active-page state — no
|
|
215
329
|
// private data-active mirror (gh#1332 Category A; admin-roster's
|
|
216
330
|
// aria-checked precedent from PR #1438).
|
|
217
|
-
el
|
|
331
|
+
setAttrIfChanged(el, 'aria-current', 'page');
|
|
218
332
|
} else {
|
|
219
|
-
el
|
|
220
|
-
el
|
|
333
|
+
setAttrIfChanged(el, 'variant', this.#restVariant());
|
|
334
|
+
removeAttrIfPresent(el, 'aria-current');
|
|
221
335
|
}
|
|
222
336
|
}
|
|
223
337
|
}
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
import { UIElement } from '../../core/element.js';
|
|
14
14
|
|
|
15
15
|
export class UIPopover extends UIElement {
|
|
16
|
+
/** Size the content panel to the trigger's width instead of max-content. Opt-in — the default (unset) keeps every existing consumer's content-sized panel unchanged. */
|
|
17
|
+
matchWidth: boolean;
|
|
16
18
|
/** Pixel offset between the anchor and the popover. Renamed from `[gap]` (gh#1335) — a raw-px number can't share a name with the global Scale-grammar `[gap]` attribute (ADR-0053). */
|
|
17
19
|
offset: number;
|
|
18
20
|
/** Controls visibility via showPopover()/hidePopover() */
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { UIElement } from '../../core/element.js';
|
|
31
|
+
import { syncAutoAriaLabel } from '../../core/a11y.js';
|
|
31
32
|
|
|
32
33
|
export class UIProgress extends UIElement {
|
|
33
34
|
static properties = {
|
|
@@ -42,6 +43,10 @@ export class UIProgress extends UIElement {
|
|
|
42
43
|
#fill = null;
|
|
43
44
|
#labelEl = null;
|
|
44
45
|
#metaEl = null;
|
|
46
|
+
// gh#1647: tracks the aria-label VALUE this element itself last derived
|
|
47
|
+
// from `label`/`meta` (undefined when none) — value-tracking guard
|
|
48
|
+
// ported from tag.class.js/select.class.js (gh#1644/#1646).
|
|
49
|
+
#lastAutoAriaLabel = undefined;
|
|
45
50
|
|
|
46
51
|
connected() {
|
|
47
52
|
this.setAttribute('role', 'progressbar');
|
|
@@ -111,12 +116,12 @@ export class UIProgress extends UIElement {
|
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
// ARIA: label the control so screen readers announce "CPU, 42 percent"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
// ARIA: label the control so screen readers announce "CPU, 42 percent"
|
|
120
|
+
// — never clobbering a consumer's own aria-label (gh#1647,
|
|
121
|
+
// value-tracking guard ported from tag.class.js/select.class.js,
|
|
122
|
+
// gh#1644/#1646).
|
|
123
|
+
const ariaSource = this.label ? (this.meta ? `${this.label}, ${this.meta}` : this.label) : '';
|
|
124
|
+
this.#lastAutoAriaLabel = syncAutoAriaLabel(this, this.#lastAutoAriaLabel, ariaSource);
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
disconnected() {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { UIFormElement } from '../../core/form.js';
|
|
28
|
+
import { syncAutoAriaLabel } from '../../core/a11y.js';
|
|
28
29
|
|
|
29
30
|
export class UIRange extends UIFormElement {
|
|
30
31
|
// Opt out of UIFormElement's per-control `label` deprecation warning.
|
|
@@ -52,6 +53,10 @@ export class UIRange extends UIFormElement {
|
|
|
52
53
|
#dragging = false;
|
|
53
54
|
#startX = 0;
|
|
54
55
|
#startVal = 0;
|
|
56
|
+
// gh#1647: tracks the aria-label VALUE this element itself last derived
|
|
57
|
+
// from `label` (undefined when none) — value-tracking guard ported from
|
|
58
|
+
// tag.class.js/select.class.js (gh#1644/#1646).
|
|
59
|
+
#lastAutoAriaLabel = undefined;
|
|
55
60
|
|
|
56
61
|
connected() {
|
|
57
62
|
super.connected();
|
|
@@ -96,7 +101,10 @@ export class UIRange extends UIFormElement {
|
|
|
96
101
|
this.setAttribute('aria-valuemax', this.max);
|
|
97
102
|
this.setAttribute('aria-valuenow', this.value);
|
|
98
103
|
this.setAttribute('aria-valuetext', `${this.value}${this.suffix ? ' ' + this.suffix : ''}`);
|
|
99
|
-
|
|
104
|
+
|
|
105
|
+
// A11y (gh#1647): never clobber a consumer's own aria-label
|
|
106
|
+
// (value-tracking guard, gh#1644/#1646).
|
|
107
|
+
this.#lastAutoAriaLabel = syncAutoAriaLabel(this, this.#lastAutoAriaLabel, this.label || '');
|
|
100
108
|
|
|
101
109
|
this.syncValue(String(this.value));
|
|
102
110
|
}
|
|
@@ -22,6 +22,30 @@
|
|
|
22
22
|
|
|
23
23
|
import { UIFormElement } from '../../core/form.js';
|
|
24
24
|
|
|
25
|
+
// ── SSR idempotent-write guard (gh#1755, follow-up to table-ui's gh#1678) ──
|
|
26
|
+
//
|
|
27
|
+
// Local copies of table.class.js's own setAttrIfChanged()/removeAttrIfPresent()
|
|
28
|
+
// — module-local there too (not exported), so a shared-utility extraction for
|
|
29
|
+
// two call sites would be a premature abstraction; these mirror that shape.
|
|
30
|
+
//
|
|
31
|
+
/**
|
|
32
|
+
* Idempotent attribute set — skip the DOM write entirely when the value
|
|
33
|
+
* already matches. `setAttribute` mutates (and queues a MutationObserver
|
|
34
|
+
* record) even when the new value is byte-identical to the old one, so a
|
|
35
|
+
* host/child attribute this render path sets on EVERY call (`placeholder`,
|
|
36
|
+
* `value`) must compare-before-write to let a byte-identical SSR-rendered
|
|
37
|
+
* `<input-ui>` survive upgrade with zero mutations — bare `setAttribute`
|
|
38
|
+
* cannot give that guarantee even when the value never actually changes.
|
|
39
|
+
*/
|
|
40
|
+
function setAttrIfChanged(el, name, value) {
|
|
41
|
+
if (el.getAttribute(name) !== value) el.setAttribute(name, value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Idempotent attribute removal — same rationale as setAttrIfChanged. */
|
|
45
|
+
function removeAttrIfPresent(el, name) {
|
|
46
|
+
if (el.hasAttribute(name)) el.removeAttribute(name);
|
|
47
|
+
}
|
|
48
|
+
|
|
25
49
|
export class UISearch extends UIFormElement {
|
|
26
50
|
static properties = {
|
|
27
51
|
...UIFormElement.properties,
|
|
@@ -37,7 +61,10 @@ export class UISearch extends UIFormElement {
|
|
|
37
61
|
|
|
38
62
|
connected() {
|
|
39
63
|
super.connected();
|
|
40
|
-
this
|
|
64
|
+
// gh#1755 — a byte-identical SSR-rendered host already carries this
|
|
65
|
+
// attribute; an unconditional setAttribute() here would still queue a
|
|
66
|
+
// mutation record even though the value never actually changes.
|
|
67
|
+
setAttrIfChanged(this, 'role', 'search');
|
|
41
68
|
|
|
42
69
|
if (!this.querySelector('input-ui')) {
|
|
43
70
|
const size = this.getAttribute('size');
|
|
@@ -69,12 +96,19 @@ export class UISearch extends UIFormElement {
|
|
|
69
96
|
render() {
|
|
70
97
|
if (!this.#inputEl) return;
|
|
71
98
|
|
|
72
|
-
this
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
// gh#1755 — this render effect re-runs on ANY of this element's
|
|
100
|
+
// reactive properties changing (element.js's connectedCallback effect
|
|
101
|
+
// reads every signal unconditionally), not only placeholder/disabled/
|
|
102
|
+
// value themselves — so a bare setAttribute() here would re-mutate an
|
|
103
|
+
// already-correct adopted `<input-ui>` (or this host's own `value`
|
|
104
|
+
// attribute) on every unrelated re-render, defeating SSR zero-mutation
|
|
105
|
+
// upgrade even when nothing this pass actually cares about changed.
|
|
106
|
+
setAttrIfChanged(this.#inputEl, 'placeholder', this.placeholder);
|
|
107
|
+
if (this.disabled) setAttrIfChanged(this.#inputEl, 'disabled', '');
|
|
108
|
+
else removeAttrIfPresent(this.#inputEl, 'disabled');
|
|
75
109
|
|
|
76
110
|
// Reflect value for CSS (clear button visibility)
|
|
77
|
-
this
|
|
111
|
+
setAttrIfChanged(this, 'value', this.value || '');
|
|
78
112
|
}
|
|
79
113
|
|
|
80
114
|
#onInput = () => {
|
|
@@ -66,6 +66,11 @@
|
|
|
66
66
|
"type": "string",
|
|
67
67
|
"default": ""
|
|
68
68
|
},
|
|
69
|
+
"label-hidden": {
|
|
70
|
+
"description": "When true, [label] still sets the accessible name (aria-label) but the visible `::before` text is suppressed via the canonical sr-only technique (gh#1748, mirrors check-ui's [label-hidden], gh#1010). Use when a sibling/ancestor composition already conveys the same name visually (e.g. a scope/view-switcher select in a table-toolbar-ui [slot=\"scope\"], where the toolbar's own title already names the view) and a second visible \"Scope\"-style label would paint it twice.",
|
|
71
|
+
"type": "boolean",
|
|
72
|
+
"default": false
|
|
73
|
+
},
|
|
69
74
|
"mark": {
|
|
70
75
|
"description": "Renders the Adia brand mark (`<adia-mark-ui>`) as the leading visual — takes precedence over avatar and icon. Token-driven (light/dark handled internally), so it fits a scheme-switching workspace/app switcher where a static logo URL can't invert. Per-option `mark` on an `<option>`/options-array entry works the same way, scoped to that row.",
|
|
71
76
|
"type": "boolean",
|
|
@@ -301,7 +306,11 @@
|
|
|
301
306
|
]
|
|
302
307
|
},
|
|
303
308
|
"tag": "select-ui",
|
|
304
|
-
"tokens": {
|
|
309
|
+
"tokens": {
|
|
310
|
+
"--select-min-width": {
|
|
311
|
+
"description": "Intrinsic min-width floor on the host (default `20ch`, matching input-ui's own `--input-min-width` UA-baseline derivation) so a select in a shrinking flex/grid cell doesn't crush to unusable width. Set to `0` to opt back into full collapse (gh#1633)."
|
|
312
|
+
}
|
|
313
|
+
},
|
|
305
314
|
"traits": [],
|
|
306
315
|
"version": 1
|
|
307
316
|
}
|