@adia-ai/web-components 0.8.44 → 0.8.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/components/context-menu/context-menu.a2ui.json +8 -3
- package/components/context-menu/context-menu.class.js +46 -5
- package/components/context-menu/context-menu.d.ts +6 -3
- package/components/context-menu/context-menu.examples.md +2 -2
- package/components/context-menu/context-menu.yaml +22 -5
- package/components/nav/nav.a2ui.json +2 -2
- package/components/nav/nav.css +1 -1
- package/components/nav/nav.d.ts +1 -1
- package/components/nav/nav.yaml +14 -3
- package/components/nav-group/nav-group.css +37 -3
- package/components/pagination/pagination.class.js +52 -22
- package/components/search/search.class.js +39 -5
- package/components/select/select.a2ui.json +5 -0
- package/components/select/select.class.js +20 -0
- package/components/select/select.css +25 -0
- package/components/select/select.d.ts +2 -0
- package/components/select/select.yaml +12 -0
- package/components/table/cell-types.js +9 -0
- package/components/table/table.class.js +247 -33
- package/components/table/table.css +7 -4
- package/components/table/table.yaml +6 -1
- package/components/table-toolbar/table-toolbar.a2ui.json +15 -0
- package/components/table-toolbar/table-toolbar.class.js +61 -17
- package/components/table-toolbar/table-toolbar.css +34 -0
- package/components/table-toolbar/table-toolbar.d.ts +10 -0
- package/components/table-toolbar/table-toolbar.yaml +41 -9
- package/core/data-stream.js +37 -2
- package/core/index.d.ts +1 -0
- package/core/index.js +1 -0
- package/core/provider.d.ts +9 -13
- package/core/provider.js +9 -113
- package/core/store.d.ts +46 -0
- package/core/store.js +89 -0
- package/custom-elements.json +54 -4
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +1 -1
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +93 -93
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/styles/api/sizing.css +46 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-components
|
|
2
2
|
|
|
3
|
+
## [0.8.45] — 2026-08-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`createStore()` — a signal-backed store primitive in `core/store.js` (gh#1761).** `subscribe(cb) -> unsubscribe`, an `Object.is` no-notify guard, and interop with `UIElement.controller`; ships alongside a new `check-no-hand-rolled-stores.mjs` lint gate (gh#1779) that rejects new hand-rolled Set-of-listeners stores outside `core/`.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- **`for=`-carrying components normalized to an events-only interaction contract (gh#1764/#1780), ADR-0079.** `table-toolbar-ui` → `table-ui` direct writes/calls (`paginate=`, `search=`, `setFilter()`, `clearFilters()`, `columns=`) became five `toolbar-*` command CustomEvents with matching `table-ui` listeners — additive, zero observable-behavior change. `context-menu-ui`'s `for` (selector-flavored) renamed to `target-selector`, `for` kept as a deprecated back-compat alias.
|
|
10
|
+
- **`RouteController` deduplicated into `core/controller.js` (gh#1766).** `provider.js` now imports/re-exports the canonical location.
|
|
11
|
+
- **`[density="compact"|"spacious"]` re-declares `--a-space-*` at the scoped selector, not only `:root` (gh#1771, gh#570).** Element-scoped density was globally inert; `components/table/table.css`'s stale ADR-0054 comment corrected alongside.
|
|
12
|
+
- **Billing composites (`billing-overview`, `invoice-history`, `invoice-detail`, `plan-picker`) route fetch through `core/transport.js`, opting out of the `core/data-stream.js` auto-observer via a new `data-stream-managed="false"` marker (gh#1760).**
|
|
13
|
+
- **`select`/`table-toolbar` — visually-hidden `select` label (`[label-hidden]`) + actions-leading icon-only Filter/Columns reduction (gh#1748).**
|
|
14
|
+
- **`table-ui` adopts an `isEqualNode()`-based `adoptOrDiffChildren()` for SSR adopt-in-place, with `RENDERER_OWNED` `WeakSet` tagging for unconditional replacement of renderer-owned cells (gh#1678).**
|
|
15
|
+
- **`search-ui`/`pagination-ui` post-adopt attribute writes are idempotent under SSR zero-mutation (gh#1755).**
|
|
16
|
+
- **Three independently-authored path-walker implementations in `packages/gen-ui/a2ui/path-pointer.js` unified, preserving each call site's real divergent edge-case semantics rather than silently collapsing them (empty-path, primitive mid-path, doubled-slash) — mechanical staging toward ADR-0078's vendored-model migration.**
|
|
17
|
+
|
|
18
|
+
### Docs
|
|
19
|
+
- **New `table-with-toolbar-in-card-header` demo** — `table-toolbar-ui` composed inside `card-ui`'s header (sole-child case; a real `card.css` grid/centering conflict documented for the heading+action-slot case, not yet supported).
|
|
20
|
+
- **`nav-group-ui`'s `[data-nav-label]` group-label hook documented (gh#1736).**
|
|
21
|
+
- **Render clock & external-store wiring doctrine ratified in `.claude/docs/specs/component-implementation-patterns.md` (gh#1777/#1778)** — `table-ui`'s rAF-batched render clock named as the sole verified exception to the microtask-flush contract; `BaseController` promoted as a `createStore()` adapter via its existing optional-chaining `controller` setter (no new adapter code needed — the contract was already satisfied).
|
|
22
|
+
|
|
23
|
+
### Maintenance
|
|
24
|
+
- **`dist/` bundles rebuilt** in this cut's window (6 file(s)) — regenerated from the source changes described above, not independent edits.
|
|
25
|
+
- **`styles/` touched in this release window** (2 file(s), e.g. `api/sizing.css`) — carried by the entries above.
|
|
26
|
+
|
|
3
27
|
## [0.8.44] — 2026-08-20
|
|
4
28
|
|
|
5
29
|
### Added
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://adiaui.dev/a2ui/v0_9/components/ContextMenu.json",
|
|
4
4
|
"title": "ContextMenu",
|
|
5
|
-
"description": "Right-click activated menu — the OS-native context-menu pattern as a\nweb component. Distinct from `menu-ui` (which is button-triggered):\nsame item shape (`menu-item-ui` children), different trigger surface\n(`contextmenu` event), and pointer-anchored positioning instead of\nelement-anchored. Pattern: WAI-APG Menu.\n\nTwo binding modes:\n **A. Wrap.** Default-slot child becomes the target:\n `<context-menu-ui><my-table>...</my-table>...items</context-menu-ui>`.\n **B. Selector.** Point at one or more existing elements via [
|
|
5
|
+
"description": "Right-click activated menu — the OS-native context-menu pattern as a\nweb component. Distinct from `menu-ui` (which is button-triggered):\nsame item shape (`menu-item-ui` children), different trigger surface\n(`contextmenu` event), and pointer-anchored positioning instead of\nelement-anchored. Pattern: WAI-APG Menu.\n\nTwo binding modes:\n **A. Wrap.** Default-slot child becomes the target:\n `<context-menu-ui><my-table>...</my-table>...items</context-menu-ui>`.\n **B. Selector.** Point at one or more existing elements via\n [target-selector]:\n `<context-menu-ui target-selector=\"#my-table\">...items</context-menu-ui>`.\n\nOn `contextmenu` event on a target: `preventDefault()`, position the\nmenu at the pointer coords, show via Popover API. Touch long-press\n(configurable via [long-press-ms]) does the same. Shift+F10 / Menu\nkey opens at the focused target's center for keyboard users.\n",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"allOf": [
|
|
8
8
|
{
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"const": "ContextMenu"
|
|
18
18
|
},
|
|
19
19
|
"for": {
|
|
20
|
-
"description": "CSS selector
|
|
20
|
+
"description": "DEPRECATED alias for [target-selector] — still a CSS selector, NOT an id-ref. Honored when [target-selector] is unset, with a one-shot console.warn pointing consumers at the replacement. New authoring should use [target-selector].",
|
|
21
21
|
"type": "string",
|
|
22
22
|
"default": ""
|
|
23
23
|
},
|
|
@@ -30,6 +30,11 @@
|
|
|
30
30
|
"description": "Programmatic open state. Set true to open at target center.",
|
|
31
31
|
"type": "boolean",
|
|
32
32
|
"default": false
|
|
33
|
+
},
|
|
34
|
+
"targetSelector": {
|
|
35
|
+
"description": "CSS selector(s) for target element(s). Empty = use default-slot child. Ratified name (gh#1764/#1780, ADR-0079) for what [for] used to carry — [for] means an id-ref everywhere else in AdiaUI (table-toolbar-ui, chart-legend-ui, tooltip-ui); context-menu-ui's own selector meaning predates that convention and was a same-name/ different-contract collision (ADR-0053's no-shadowing doctrine).",
|
|
36
|
+
"type": "string",
|
|
37
|
+
"default": ""
|
|
33
38
|
}
|
|
34
39
|
},
|
|
35
40
|
"required": [
|
|
@@ -39,7 +44,7 @@
|
|
|
39
44
|
"x-adiaui": {
|
|
40
45
|
"anti_patterns": [
|
|
41
46
|
{
|
|
42
|
-
"fix": "Wrap a target: `<context-menu-ui><my-target></my-target>...items</context-menu-ui>` OR point at one: `<context-menu-ui
|
|
47
|
+
"fix": "Wrap a target: `<context-menu-ui><my-target></my-target>...items</context-menu-ui>` OR point at one: `<context-menu-ui target-selector=\"#my-target\">...items</context-menu-ui>`.",
|
|
43
48
|
"why": "No target binding — the menu never opens.",
|
|
44
49
|
"wrong": "<context-menu-ui>...just items...</context-menu-ui>"
|
|
45
50
|
}
|
|
@@ -4,8 +4,16 @@
|
|
|
4
4
|
* Two binding shapes:
|
|
5
5
|
* A. Wrap a target — first non-menu-item-ui default-slot child becomes
|
|
6
6
|
* the contextmenu host.
|
|
7
|
-
* B. Point at targets via [
|
|
8
|
-
* / whole-canvas menus where wrapping isn't practical.
|
|
7
|
+
* B. Point at targets via [target-selector="<css-selector>"] — useful for
|
|
8
|
+
* whole-table / whole-canvas menus where wrapping isn't practical.
|
|
9
|
+
* [for] is a DEPRECATED alias for this (gh#1764/#1780, ADR-0079): `for=`
|
|
10
|
+
* means an id-ref everywhere else in AdiaUI (table-toolbar-ui,
|
|
11
|
+
* chart-legend-ui, tooltip-ui) — context-menu-ui's own pre-ruling [for]
|
|
12
|
+
* carried a CSS-selector meaning instead, the exact same-attribute-
|
|
13
|
+
* different-contract shape ADR-0053's no-shadowing doctrine forbids.
|
|
14
|
+
* [target-selector] still resolves via `document.querySelectorAll()`
|
|
15
|
+
* (multiple targets); a still-set legacy [for] is honored with a
|
|
16
|
+
* one-shot console.warn pointing at the replacement.
|
|
9
17
|
*
|
|
10
18
|
* Architecture: reuses the existing anchor pattern (core/anchor.js) by
|
|
11
19
|
* creating a virtual 1×1 anchor element at the pointer coords on each
|
|
@@ -23,8 +31,21 @@ import { anchorPopover } from '../../core/anchor.js';
|
|
|
23
31
|
|
|
24
32
|
const LONG_PRESS_DEFAULT = 500;
|
|
25
33
|
|
|
34
|
+
// DEPRECATED [for]-alias one-shot warning guard (gh#1764/#1780, ADR-0079) —
|
|
35
|
+
// module-scope, matching this file's own idiom (a plain top-level const
|
|
36
|
+
// rather than a lazily-created public static field, which stayed mutable
|
|
37
|
+
// and externally visible for no reason).
|
|
38
|
+
const warnedForDeprecated = new WeakSet();
|
|
39
|
+
|
|
26
40
|
export class UIContextMenu extends UIElement {
|
|
27
41
|
static properties = {
|
|
42
|
+
// gh#1764/#1780, ADR-0079 — the ratified replacement for the
|
|
43
|
+
// selector-flavored [for] below; a plain CSS selector, not an id-ref.
|
|
44
|
+
targetSelector: { type: String, default: '', reflect: true, attribute: 'target-selector' },
|
|
45
|
+
// DEPRECATED — kept as a back-compat alias for `targetSelector` only.
|
|
46
|
+
// Honored (with a one-shot console.warn, see #legacyForSelector())
|
|
47
|
+
// whenever [target-selector] is unset. Never given id-ref semantics;
|
|
48
|
+
// the sweep did not attempt to dual-purpose this attribute.
|
|
28
49
|
for: { type: String, default: '', reflect: true },
|
|
29
50
|
open: { type: Boolean, default: false, reflect: true },
|
|
30
51
|
longPressMs: { type: Number, default: LONG_PRESS_DEFAULT, reflect: false, attribute: 'long-press-ms' },
|
|
@@ -97,11 +118,12 @@ export class UIContextMenu extends UIElement {
|
|
|
97
118
|
}
|
|
98
119
|
|
|
99
120
|
#resolveTargets() {
|
|
100
|
-
|
|
101
|
-
|
|
121
|
+
const selector = this.targetSelector || this.#legacyForSelector();
|
|
122
|
+
if (selector) {
|
|
123
|
+
try { return [...document.querySelectorAll(selector)]; }
|
|
102
124
|
catch (e) {
|
|
103
125
|
// eslint-disable-next-line no-console
|
|
104
|
-
console.warn(`[context-menu-ui] invalid [
|
|
126
|
+
console.warn(`[context-menu-ui] invalid [target-selector] selector: ${selector}`, e);
|
|
105
127
|
return [];
|
|
106
128
|
}
|
|
107
129
|
}
|
|
@@ -113,6 +135,25 @@ export class UIContextMenu extends UIElement {
|
|
|
113
135
|
return [];
|
|
114
136
|
}
|
|
115
137
|
|
|
138
|
+
// DEPRECATED alias path (gh#1764/#1780, ADR-0079) — [for] used to carry
|
|
139
|
+
// context-menu-ui's own CSS-selector binding; [target-selector] is now
|
|
140
|
+
// the canonical name. Warn once per element, never per resolve (this
|
|
141
|
+
// runs on every #bindTargets() call, including reconnects).
|
|
142
|
+
#legacyForSelector() {
|
|
143
|
+
if (!this.for) return '';
|
|
144
|
+
if (!warnedForDeprecated.has(this)) {
|
|
145
|
+
warnedForDeprecated.add(this);
|
|
146
|
+
// eslint-disable-next-line no-console
|
|
147
|
+
console.warn(
|
|
148
|
+
`[context-menu-ui] [for="${this.for}"] is deprecated — use ` +
|
|
149
|
+
`[target-selector="${this.for}"] instead. [for] now means an ` +
|
|
150
|
+
'id-ref everywhere else in AdiaUI; context-menu-ui\'s own prior ' +
|
|
151
|
+
'[for] carried a CSS-selector meaning (ADR-0079).',
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return this.for;
|
|
155
|
+
}
|
|
156
|
+
|
|
116
157
|
/* ── Trigger event handlers ──────────────────────────────────────── */
|
|
117
158
|
|
|
118
159
|
#onContextMenu = (e) => {
|
|
@@ -8,8 +8,9 @@ element-anchored. Pattern: WAI-APG Menu.
|
|
|
8
8
|
Two binding modes:
|
|
9
9
|
**A. Wrap.** Default-slot child becomes the target:
|
|
10
10
|
`<context-menu-ui><my-table>...</my-table>...items</context-menu-ui>`.
|
|
11
|
-
**B. Selector.** Point at one or more existing elements via
|
|
12
|
-
|
|
11
|
+
**B. Selector.** Point at one or more existing elements via
|
|
12
|
+
[target-selector]:
|
|
13
|
+
`<context-menu-ui target-selector="#my-table">...items</context-menu-ui>`.
|
|
13
14
|
|
|
14
15
|
On `contextmenu` event on a target: `preventDefault()`, position the
|
|
15
16
|
menu at the pointer coords, show via Popover API. Touch long-press
|
|
@@ -54,12 +55,14 @@ export interface ContextMenuSelectEventDetail {
|
|
|
54
55
|
export type ContextMenuSelectEvent = CustomEvent<ContextMenuSelectEventDetail>;
|
|
55
56
|
|
|
56
57
|
export class UIContextMenu extends UIElement {
|
|
57
|
-
/** CSS selector
|
|
58
|
+
/** DEPRECATED alias for [target-selector] — still a CSS selector, NOT an id-ref. Honored when [target-selector] is unset, with a one-shot console.warn pointing consumers at the replacement. New authoring should use [target-selector]. */
|
|
58
59
|
for: string;
|
|
59
60
|
/** Long-press duration (ms) on touch devices to open the menu. */
|
|
60
61
|
longPressMs: number;
|
|
61
62
|
/** Programmatic open state. Set true to open at target center. */
|
|
62
63
|
open: boolean;
|
|
64
|
+
/** CSS selector(s) for target element(s). Empty = use default-slot child. Ratified name (gh#1764/#1780, ADR-0079) for what [for] used to carry — [for] means an id-ref everywhere else in AdiaUI (table-toolbar-ui, chart-legend-ui, tooltip-ui); context-menu-ui's own selector meaning predates that convention and was a same-name/ different-contract collision (ADR-0053's no-shadowing doctrine). */
|
|
65
|
+
targetSelector: string;
|
|
63
66
|
|
|
64
67
|
addEventListener(type: 'context-menu-close', listener: (ev: ContextMenuCloseEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
65
68
|
addEventListener(type: 'context-menu-open', listener: (ev: ContextMenuOpenEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
</context-menu-ui>
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
## [
|
|
20
|
+
## [target-selector] mode — point at existing targets
|
|
21
21
|
|
|
22
22
|
```html
|
|
23
23
|
<div style="display: grid; gap: var(--a-space-2);">
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<div data-row tabindex="0" style="padding: var(--a-space-3); border: 1px solid var(--md-sys-color-neutral-outline); border-radius: var(--a-radius-sm); background: var(--md-sys-color-neutral-background);">Row B — right-click</div>
|
|
26
26
|
<div data-row tabindex="0" style="padding: var(--a-space-3); border: 1px solid var(--md-sys-color-neutral-outline); border-radius: var(--a-radius-sm); background: var(--md-sys-color-neutral-background);">Row C — right-click</div>
|
|
27
27
|
</div>
|
|
28
|
-
<context-menu-ui
|
|
28
|
+
<context-menu-ui target-selector='[data-chunk="context-menu-for"] [data-row]'>
|
|
29
29
|
<menu-item-ui value="edit" text="Edit"></menu-item-ui>
|
|
30
30
|
<menu-item-ui value="duplicate" text="Duplicate"></menu-item-ui>
|
|
31
31
|
<menu-divider-ui></menu-divider-ui>
|
|
@@ -15,8 +15,9 @@ description: |
|
|
|
15
15
|
Two binding modes:
|
|
16
16
|
**A. Wrap.** Default-slot child becomes the target:
|
|
17
17
|
`<context-menu-ui><my-table>...</my-table>...items</context-menu-ui>`.
|
|
18
|
-
**B. Selector.** Point at one or more existing elements via
|
|
19
|
-
|
|
18
|
+
**B. Selector.** Point at one or more existing elements via
|
|
19
|
+
[target-selector]:
|
|
20
|
+
`<context-menu-ui target-selector="#my-table">...items</context-menu-ui>`.
|
|
20
21
|
|
|
21
22
|
On `contextmenu` event on a target: `preventDefault()`, position the
|
|
22
23
|
menu at the pointer coords, show via Popover API. Touch long-press
|
|
@@ -25,8 +26,24 @@ description: |
|
|
|
25
26
|
composes:
|
|
26
27
|
- menu-item-ui
|
|
27
28
|
props:
|
|
29
|
+
targetSelector:
|
|
30
|
+
description: >-
|
|
31
|
+
CSS selector(s) for target element(s). Empty = use default-slot
|
|
32
|
+
child. Ratified name (gh#1764/#1780, ADR-0079) for what [for] used
|
|
33
|
+
to carry — [for] means an id-ref everywhere else in AdiaUI
|
|
34
|
+
(table-toolbar-ui, chart-legend-ui, tooltip-ui); context-menu-ui's
|
|
35
|
+
own selector meaning predates that convention and was a same-name/
|
|
36
|
+
different-contract collision (ADR-0053's no-shadowing doctrine).
|
|
37
|
+
type: string
|
|
38
|
+
default: ""
|
|
39
|
+
reflect: true
|
|
40
|
+
attribute: target-selector
|
|
28
41
|
for:
|
|
29
|
-
description:
|
|
42
|
+
description: >-
|
|
43
|
+
DEPRECATED alias for [target-selector] — still a CSS selector, NOT
|
|
44
|
+
an id-ref. Honored when [target-selector] is unset, with a one-shot
|
|
45
|
+
console.warn pointing consumers at the replacement. New authoring
|
|
46
|
+
should use [target-selector].
|
|
30
47
|
type: string
|
|
31
48
|
default: ""
|
|
32
49
|
reflect: true
|
|
@@ -101,12 +118,12 @@ a2ui:
|
|
|
101
118
|
reason: 'Trigger surface boundary.'
|
|
102
119
|
- rule: 'Items are <menu-item-ui> children inside the default slot — same shape as <menu-ui> items.'
|
|
103
120
|
reason: 'Single menu vocabulary.'
|
|
104
|
-
- rule: 'Bind target via wrap (default-slot first non-menu-item-ui child) OR [
|
|
121
|
+
- rule: 'Bind target via wrap (default-slot first non-menu-item-ui child) OR [target-selector]. The selector form is useful for whole-table or whole-canvas menus where wrapping isn''t practical. [for] still works as a deprecated alias but new authoring should use [target-selector] (gh#1764/#1780, ADR-0079).'
|
|
105
122
|
reason: 'Two binding shapes.'
|
|
106
123
|
anti_patterns:
|
|
107
124
|
- wrong: '<context-menu-ui>...just items...</context-menu-ui>'
|
|
108
125
|
why: 'No target binding — the menu never opens.'
|
|
109
|
-
fix: 'Wrap a target: `<context-menu-ui><my-target></my-target>...items</context-menu-ui>` OR point at one: `<context-menu-ui
|
|
126
|
+
fix: 'Wrap a target: `<context-menu-ui><my-target></my-target>...items</context-menu-ui>` OR point at one: `<context-menu-ui target-selector="#my-target">...items</context-menu-ui>`.'
|
|
110
127
|
examples:
|
|
111
128
|
- name: file-actions
|
|
112
129
|
description: Right-click a file row for Open / Rename / Delete.
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"default": false
|
|
28
28
|
},
|
|
29
29
|
"heading": {
|
|
30
|
-
"description": "Optional kicker label. Section variant renders it via ::before; primary uses it as aria-label only.",
|
|
30
|
+
"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.",
|
|
31
31
|
"type": "string",
|
|
32
32
|
"default": ""
|
|
33
33
|
},
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
],
|
|
109
109
|
"slots": {
|
|
110
110
|
"default": {
|
|
111
|
-
"description": "Primary slot — accepts <nav-group-ui> + <nav-item-ui> children, plus <hr data-nav-divider> for hand-placed dividers."
|
|
111
|
+
"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)."
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
"states": [
|
package/components/nav/nav.css
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
--nav-label-fg: var(--a-fg-muted);
|
|
33
33
|
--nav-label-font-size: var(--a-kicker-sm);
|
|
34
34
|
--nav-label-weight: var(--a-weight-medium);
|
|
35
|
-
--nav-label-px: var(--a-space-
|
|
35
|
+
--nav-label-px: var(--a-space-4);
|
|
36
36
|
--nav-label-py: var(--a-space-3) var(--a-space-1);
|
|
37
37
|
}
|
|
38
38
|
|
package/components/nav/nav.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export class UINav extends UIElement {
|
|
|
36
36
|
collapsed: boolean;
|
|
37
37
|
/** Auto-place dividers between adjacent groups + items. */
|
|
38
38
|
divider: boolean;
|
|
39
|
-
/** Optional kicker label. Section variant renders it via ::before; primary uses it as aria-label only. */
|
|
39
|
+
/** 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. */
|
|
40
40
|
heading: string;
|
|
41
41
|
/** Opt out of the default single-expanded-group behavior. When unset, selecting a nav-item-ui collapses every nav-group-ui except the one containing the newly selected item (all, when the selection is ungrouped); manual multi-expansion still works up until the next selection. */
|
|
42
42
|
multiExpand: boolean;
|
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);
|
|
@@ -24,6 +24,32 @@
|
|
|
24
24
|
import { UIElement } from '../../core/element.js';
|
|
25
25
|
import { KEY_MAP } from '../../core/template.js';
|
|
26
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
|
+
}
|
|
52
|
+
|
|
27
53
|
export class UIPagination extends UIElement {
|
|
28
54
|
static properties = {
|
|
29
55
|
page: { type: Number, default: 1, reflect: true },
|
|
@@ -251,21 +277,21 @@ export class UIPagination extends UIElement {
|
|
|
251
277
|
}
|
|
252
278
|
|
|
253
279
|
const btn = document.createElement('button-ui');
|
|
254
|
-
btn
|
|
280
|
+
setAttrIfChanged(btn, 'size', this.size);
|
|
255
281
|
|
|
256
282
|
if (item.type === 'prev') {
|
|
257
283
|
btn.setAttribute('data-prev', '');
|
|
258
|
-
btn
|
|
259
|
-
btn
|
|
284
|
+
setAttrIfChanged(btn, 'icon', 'caret-left');
|
|
285
|
+
setAttrIfChanged(btn, 'aria-label', 'Previous page');
|
|
260
286
|
} else if (item.type === 'next') {
|
|
261
287
|
btn.setAttribute('data-next', '');
|
|
262
|
-
btn
|
|
263
|
-
btn
|
|
288
|
+
setAttrIfChanged(btn, 'icon', 'caret-right');
|
|
289
|
+
setAttrIfChanged(btn, 'aria-label', 'Next page');
|
|
264
290
|
} else {
|
|
265
291
|
btn.setAttribute('data-page', '');
|
|
266
|
-
btn
|
|
267
|
-
btn
|
|
268
|
-
btn
|
|
292
|
+
setAttrIfChanged(btn, 'data-value', String(item.value));
|
|
293
|
+
setAttrIfChanged(btn, 'text', String(item.value));
|
|
294
|
+
setAttrIfChanged(btn, 'aria-label', `Page ${item.value}`);
|
|
269
295
|
}
|
|
270
296
|
|
|
271
297
|
this.#updateItem(btn, item, page);
|
|
@@ -276,32 +302,36 @@ export class UIPagination extends UIElement {
|
|
|
276
302
|
if (item.type === 'ellipsis') return;
|
|
277
303
|
|
|
278
304
|
// Keep size in sync \u2014 the host's [size] may change between renders.
|
|
279
|
-
|
|
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);
|
|
280
310
|
|
|
281
311
|
if (item.type === 'prev') {
|
|
282
|
-
el
|
|
283
|
-
if (page <= 1) { el
|
|
284
|
-
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'); }
|
|
285
315
|
} else if (item.type === 'next') {
|
|
286
|
-
el
|
|
287
|
-
if (page >= this.total) { el
|
|
288
|
-
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'); }
|
|
289
319
|
} else {
|
|
290
|
-
el
|
|
291
|
-
el
|
|
292
|
-
el
|
|
320
|
+
setAttrIfChanged(el, 'data-value', String(item.value));
|
|
321
|
+
setAttrIfChanged(el, 'text', String(item.value));
|
|
322
|
+
setAttrIfChanged(el, 'aria-label', `Page ${item.value}`);
|
|
293
323
|
if (item.value === page) {
|
|
294
324
|
// Active page reads as `variant="primary"` so the filled-accent
|
|
295
325
|
// state comes from button-ui's primary surface matrix (the
|
|
296
326
|
// canonical token chain) \u2014 not a pagination-tier re-impl.
|
|
297
|
-
el
|
|
327
|
+
setAttrIfChanged(el, 'variant', 'primary');
|
|
298
328
|
// aria-current="page" alone carries the active-page state — no
|
|
299
329
|
// private data-active mirror (gh#1332 Category A; admin-roster's
|
|
300
330
|
// aria-checked precedent from PR #1438).
|
|
301
|
-
el
|
|
331
|
+
setAttrIfChanged(el, 'aria-current', 'page');
|
|
302
332
|
} else {
|
|
303
|
-
el
|
|
304
|
-
el
|
|
333
|
+
setAttrIfChanged(el, 'variant', this.#restVariant());
|
|
334
|
+
removeAttrIfPresent(el, 'aria-current');
|
|
305
335
|
}
|
|
306
336
|
}
|
|
307
337
|
}
|
|
@@ -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",
|
|
@@ -47,6 +47,11 @@ export class UISelect extends UIFormElement {
|
|
|
47
47
|
size: { type: String, default: 'md', reflect: true },
|
|
48
48
|
open: { type: Boolean, default: false, reflect: true },
|
|
49
49
|
label: { type: String, default: '', reflect: true },
|
|
50
|
+
// gh#1748: keeps [label] as the accessible name (bridged to an explicit
|
|
51
|
+
// aria-label in #syncAccessibleName below) while suppressing the VISIBLE
|
|
52
|
+
// `::before` text (select.css) — for compositions that already show the
|
|
53
|
+
// name elsewhere. Mirrors check-ui's [label-hidden] (gh#1010).
|
|
54
|
+
labelHidden: { type: Boolean, default: false, reflect: true, attribute: 'label-hidden' },
|
|
50
55
|
icon: { type: String, default: '', reflect: true },
|
|
51
56
|
avatar: { type: String, default: '', reflect: true },
|
|
52
57
|
mark: { type: Boolean, default: false, reflect: true },
|
|
@@ -361,6 +366,21 @@ export class UISelect extends UIFormElement {
|
|
|
361
366
|
*/
|
|
362
367
|
#syncAccessibleName() {
|
|
363
368
|
const DEFAULT_PLACEHOLDER = 'Select...'; // matches static properties.placeholder.default
|
|
369
|
+
// gh#1748: [label-hidden] suppresses the visible `::before` text
|
|
370
|
+
// (select.css's `attr(label)` content) but [label] must still name the
|
|
371
|
+
// control — bridge it through the SAME tracked-ownership pipeline used
|
|
372
|
+
// below for the placeholder-only case, so a consumer's own directly-set
|
|
373
|
+
// aria-label is never clobbered (gh#1646 guarantee extends for free) and
|
|
374
|
+
// toggling [label-hidden] back off cleanly releases the bridged value
|
|
375
|
+
// (falls through to the visible-label path, which sets no aria-label —
|
|
376
|
+
// the generated `::before` content supplies the name instead, unchanged
|
|
377
|
+
// from today). Mirrors check-ui's [label-hidden] (gh#1010), which
|
|
378
|
+
// instead sets aria-label unconditionally since check-ui never relies
|
|
379
|
+
// on generated-content naming.
|
|
380
|
+
if (this.labelHidden && this.label) {
|
|
381
|
+
this.#lastAutoAriaLabel = syncAutoAriaLabel(this, this.#lastAutoAriaLabel, this.label);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
364
384
|
const labeledElsewhere = !!this.label || this.hasAttribute('aria-labelledby');
|
|
365
385
|
const namable = !labeledElsewhere
|
|
366
386
|
&& this.placeholder && this.placeholder !== DEFAULT_PLACEHOLDER;
|