@helixui/library 3.4.0 → 3.4.1
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/dist/components/hx-button-group/hx-button-group.d.ts +9 -0
- package/dist/components/hx-button-group/hx-button-group.d.ts.map +1 -1
- package/dist/components/hx-button-group/index.js +1 -1
- package/dist/components/hx-card/hx-card.d.ts +68 -0
- package/dist/components/hx-card/hx-card.d.ts.map +1 -1
- package/dist/components/hx-card/hx-card.styles.d.ts.map +1 -1
- package/dist/components/hx-card/index.js +1 -1
- package/dist/components/hx-checkbox/index.js +1 -1
- package/dist/components/hx-checkbox-group/index.js +1 -1
- package/dist/components/hx-color-picker/index.js +1 -1
- package/dist/components/hx-combobox/index.js +1 -1
- package/dist/components/hx-date-picker/index.js +1 -1
- package/dist/components/hx-dialog/index.js +1 -1
- package/dist/components/hx-drawer/index.js +1 -1
- package/dist/components/hx-dropdown/index.js +1 -1
- package/dist/components/hx-list/index.js +1 -1
- package/dist/components/hx-menu/index.js +1 -1
- package/dist/components/hx-meter/hx-meter.d.ts.map +1 -1
- package/dist/components/hx-meter/index.js +1 -1
- package/dist/components/hx-overflow-menu/index.js +1 -1
- package/dist/components/hx-popover/index.js +1 -1
- package/dist/components/hx-progress-bar/index.js +1 -1
- package/dist/components/hx-radio-group/index.js +1 -1
- package/dist/components/hx-select/index.js +1 -1
- package/dist/components/hx-spinner/hx-spinner.d.ts.map +1 -1
- package/dist/components/hx-spinner/index.js +1 -1
- package/dist/components/hx-split-button/index.js +1 -1
- package/dist/components/hx-stat/index.js +1 -1
- package/dist/components/hx-switch/index.js +1 -1
- package/dist/components/hx-table/hx-td.d.ts.map +1 -1
- package/dist/components/hx-table/hx-th.d.ts +9 -0
- package/dist/components/hx-table/hx-th.d.ts.map +1 -1
- package/dist/components/hx-table/index.js +1 -1
- package/dist/components/hx-tabs/index.js +1 -1
- package/dist/components/hx-time-picker/index.js +1 -1
- package/dist/components/hx-toggle-button/index.js +1 -1
- package/dist/components/hx-tree-view/index.js +1 -1
- package/dist/css/helix-all.css +14 -1
- package/dist/css/helix-core.css +14 -1
- package/dist/css/hx-card.css +14 -1
- package/dist/css/index.css +1 -1
- package/dist/css/manifest.json +1 -1
- package/dist/index.js +27 -27
- package/dist/shared/aria-idref-DCuEaknC.js +131 -0
- package/dist/shared/{aria-idref-CxvyzfQS.js.map → aria-idref-DCuEaknC.js.map} +1 -1
- package/dist/shared/{hx-button-group-DcHP5MBv.js → hx-button-group-4NUBpkyC.js} +22 -22
- package/dist/shared/{hx-button-group-DcHP5MBv.js.map → hx-button-group-4NUBpkyC.js.map} +1 -1
- package/dist/shared/{hx-card-qNAM2QNV.js → hx-card-CswtnYvj.js} +142 -85
- package/dist/shared/hx-card-CswtnYvj.js.map +1 -0
- package/dist/shared/{hx-checkbox-C48KYKFq.js → hx-checkbox-CYd0YV_u.js} +2 -2
- package/dist/shared/{hx-checkbox-C48KYKFq.js.map → hx-checkbox-CYd0YV_u.js.map} +1 -1
- package/dist/shared/{hx-checkbox-group-BJIAX3zU.js → hx-checkbox-group-D5piJLY8.js} +2 -2
- package/dist/shared/{hx-checkbox-group-BJIAX3zU.js.map → hx-checkbox-group-D5piJLY8.js.map} +1 -1
- package/dist/shared/{hx-color-picker-Dk4cBwYQ.js → hx-color-picker-DBwJzT5f.js} +2 -2
- package/dist/shared/{hx-color-picker-Dk4cBwYQ.js.map → hx-color-picker-DBwJzT5f.js.map} +1 -1
- package/dist/shared/{hx-combobox-BTLO9qiK.js → hx-combobox-NgJaLbs2.js} +2 -2
- package/dist/shared/{hx-combobox-BTLO9qiK.js.map → hx-combobox-NgJaLbs2.js.map} +1 -1
- package/dist/shared/{hx-date-picker-CiR7FVnR.js → hx-date-picker-B49yo4Vm.js} +2 -2
- package/dist/shared/{hx-date-picker-CiR7FVnR.js.map → hx-date-picker-B49yo4Vm.js.map} +1 -1
- package/dist/shared/{hx-dialog-AOZpHSuF.js → hx-dialog-B4weoj_1.js} +2 -2
- package/dist/shared/{hx-dialog-AOZpHSuF.js.map → hx-dialog-B4weoj_1.js.map} +1 -1
- package/dist/shared/{hx-drawer-DH6CdAN1.js → hx-drawer-D81tb4BD.js} +2 -2
- package/dist/shared/{hx-drawer-DH6CdAN1.js.map → hx-drawer-D81tb4BD.js.map} +1 -1
- package/dist/shared/{hx-dropdown-DiLd40Lm.js → hx-dropdown-D626S2ZG.js} +2 -2
- package/dist/shared/{hx-dropdown-DiLd40Lm.js.map → hx-dropdown-D626S2ZG.js.map} +1 -1
- package/dist/shared/{hx-list-De66EtAP.js → hx-list-Bp8HeLHh.js} +2 -2
- package/dist/shared/{hx-list-De66EtAP.js.map → hx-list-Bp8HeLHh.js.map} +1 -1
- package/dist/shared/{hx-menu-divider-BjiRIWKq.js → hx-menu-divider-A6Guuzi_.js} +2 -2
- package/dist/shared/{hx-menu-divider-BjiRIWKq.js.map → hx-menu-divider-A6Guuzi_.js.map} +1 -1
- package/dist/shared/{hx-meter-BJdh6nrF.js → hx-meter-BnpmF3Vx.js} +57 -36
- package/dist/shared/{hx-meter-BJdh6nrF.js.map → hx-meter-BnpmF3Vx.js.map} +1 -1
- package/dist/shared/{hx-overflow-menu-BQ4fiMYu.js → hx-overflow-menu-DFjJAziP.js} +2 -2
- package/dist/shared/{hx-overflow-menu-BQ4fiMYu.js.map → hx-overflow-menu-DFjJAziP.js.map} +1 -1
- package/dist/shared/{hx-popover-B9W8-tC0.js → hx-popover-BAlAFOH9.js} +2 -2
- package/dist/shared/{hx-popover-B9W8-tC0.js.map → hx-popover-BAlAFOH9.js.map} +1 -1
- package/dist/shared/{hx-progress-bar-C8nDMdYa.js → hx-progress-bar-CYz9U721.js} +2 -2
- package/dist/shared/{hx-progress-bar-C8nDMdYa.js.map → hx-progress-bar-CYz9U721.js.map} +1 -1
- package/dist/shared/{hx-radio-Z1lV1zTO.js → hx-radio-C7eTj5YI.js} +2 -2
- package/dist/shared/{hx-radio-Z1lV1zTO.js.map → hx-radio-C7eTj5YI.js.map} +1 -1
- package/dist/shared/{hx-select-D18CnJ0e.js → hx-select-DahFehiZ.js} +2 -2
- package/dist/shared/{hx-select-D18CnJ0e.js.map → hx-select-DahFehiZ.js.map} +1 -1
- package/dist/shared/{hx-spinner-BB0h2hKZ.js → hx-spinner-3qBp4jeN.js} +11 -11
- package/dist/shared/{hx-spinner-BB0h2hKZ.js.map → hx-spinner-3qBp4jeN.js.map} +1 -1
- package/dist/shared/{hx-split-button-BoABoEm5.js → hx-split-button-Ddle8iVx.js} +2 -2
- package/dist/shared/{hx-split-button-BoABoEm5.js.map → hx-split-button-Ddle8iVx.js.map} +1 -1
- package/dist/shared/{hx-stat-Dtf9lz-O.js → hx-stat-Gtw_SpK8.js} +2 -2
- package/dist/shared/{hx-stat-Dtf9lz-O.js.map → hx-stat-Gtw_SpK8.js.map} +1 -1
- package/dist/shared/{hx-switch-B6kr-EwE.js → hx-switch-TvKGvZJz.js} +2 -2
- package/dist/shared/{hx-switch-B6kr-EwE.js.map → hx-switch-TvKGvZJz.js.map} +1 -1
- package/dist/shared/{hx-tab-panel-BQtBXKLD.js → hx-tab-panel-Cu--8psg.js} +2 -2
- package/dist/shared/{hx-tab-panel-BQtBXKLD.js.map → hx-tab-panel-Cu--8psg.js.map} +1 -1
- package/dist/shared/{hx-td-BGkFOJEK.js → hx-td-BPsb6OaG.js} +141 -138
- package/dist/shared/hx-td-BPsb6OaG.js.map +1 -0
- package/dist/shared/{hx-time-picker-iwCD7rzW.js → hx-time-picker-Bo7FWzmf.js} +2 -2
- package/dist/shared/{hx-time-picker-iwCD7rzW.js.map → hx-time-picker-Bo7FWzmf.js.map} +1 -1
- package/dist/shared/{hx-toggle-button-BQ81EDkl.js → hx-toggle-button-DwBers3A.js} +2 -2
- package/dist/shared/{hx-toggle-button-BQ81EDkl.js.map → hx-toggle-button-DwBers3A.js.map} +1 -1
- package/dist/shared/{hx-tree-item-CHrUhuZL.js → hx-tree-item-CXyspGxI.js} +2 -2
- package/dist/shared/{hx-tree-item-CHrUhuZL.js.map → hx-tree-item-CXyspGxI.js.map} +1 -1
- package/dist/utils/aria-idref.d.ts.map +1 -1
- package/figma-inventory.json +2 -2
- package/package.json +1 -1
- package/dist/shared/aria-idref-CxvyzfQS.js +0 -126
- package/dist/shared/hx-card-qNAM2QNV.js.map +0 -1
- package/dist/shared/hx-td-BGkFOJEK.js.map +0 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
function g(e, a) {
|
|
2
|
+
if (!a) return [];
|
|
3
|
+
const i = a.split(/\s+/).filter(Boolean);
|
|
4
|
+
if (i.length === 0) return [];
|
|
5
|
+
const t = [];
|
|
6
|
+
S(e, t);
|
|
7
|
+
const o = e.ownerDocument;
|
|
8
|
+
o && !t.includes(o) && t.push(o);
|
|
9
|
+
const n = [];
|
|
10
|
+
for (const d of i)
|
|
11
|
+
for (const l of t) {
|
|
12
|
+
const s = l.getElementById(d);
|
|
13
|
+
if (s) {
|
|
14
|
+
n.push(s);
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return n;
|
|
19
|
+
}
|
|
20
|
+
function S(e, a) {
|
|
21
|
+
const i = /* @__PURE__ */ new Set(), t = (s) => {
|
|
22
|
+
i.has(s) || (i.add(s), a.push(s));
|
|
23
|
+
}, o = /* @__PURE__ */ new Set([e]), n = [];
|
|
24
|
+
let d = !1;
|
|
25
|
+
const l = (s) => {
|
|
26
|
+
let c = s, f = c.getRootNode();
|
|
27
|
+
const b = c.assignedSlot ?? null;
|
|
28
|
+
for (b && !o.has(b) && (o.add(b), n.push(b)); f instanceof ShadowRoot; ) {
|
|
29
|
+
t(f);
|
|
30
|
+
const r = f.host ?? null;
|
|
31
|
+
if (!r) break;
|
|
32
|
+
const u = r.assignedSlot ?? null;
|
|
33
|
+
u && !o.has(u) && (o.add(u), n.push(u)), c = r, f = r.getRootNode();
|
|
34
|
+
}
|
|
35
|
+
f instanceof Document && (d = !0);
|
|
36
|
+
};
|
|
37
|
+
for (l(e); n.length > 0; )
|
|
38
|
+
l(n.shift());
|
|
39
|
+
if (d) {
|
|
40
|
+
const s = e.ownerDocument;
|
|
41
|
+
s && t(s);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function R(e) {
|
|
45
|
+
return "ariaLabelledByElements" in e && "ariaDescribedByElements" in e && typeof e.ariaLabelledByElements < "u";
|
|
46
|
+
}
|
|
47
|
+
const m = [
|
|
48
|
+
"aria-label",
|
|
49
|
+
"aria-labelledby",
|
|
50
|
+
"aria-describedby",
|
|
51
|
+
"data-aria-label",
|
|
52
|
+
"data-aria-labelledby",
|
|
53
|
+
"data-aria-describedby"
|
|
54
|
+
], w = /* @__PURE__ */ new WeakMap();
|
|
55
|
+
function p(e, a) {
|
|
56
|
+
let i = w.get(e);
|
|
57
|
+
if (!i) {
|
|
58
|
+
const t = /* @__PURE__ */ new Set();
|
|
59
|
+
let o = 0;
|
|
60
|
+
const n = () => {
|
|
61
|
+
o = 0, Array.from(t).forEach((s) => {
|
|
62
|
+
s();
|
|
63
|
+
});
|
|
64
|
+
}, d = () => {
|
|
65
|
+
if (o !== 0) return;
|
|
66
|
+
o = (typeof globalThis.requestAnimationFrame == "function" ? globalThis.requestAnimationFrame : (c) => globalThis.setTimeout(() => c(performance.now()), 0))(n);
|
|
67
|
+
}, l = new MutationObserver(() => {
|
|
68
|
+
d();
|
|
69
|
+
});
|
|
70
|
+
l.observe(e, {
|
|
71
|
+
childList: !0,
|
|
72
|
+
subtree: !0,
|
|
73
|
+
attributes: !0,
|
|
74
|
+
attributeFilter: ["id", "hidden", "aria-hidden", "style", "class"],
|
|
75
|
+
characterData: !0
|
|
76
|
+
}), i = { observer: l, subscribers: t }, w.set(e, i);
|
|
77
|
+
}
|
|
78
|
+
return i.subscribers.add(a), () => {
|
|
79
|
+
const t = w.get(e);
|
|
80
|
+
t && (t.subscribers.delete(a), t.subscribers.size === 0 && (t.observer.disconnect(), w.delete(e)));
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function y(e, a, i = {}) {
|
|
84
|
+
const t = i.observedAttributes ?? m, o = i.observeRoot ?? !0;
|
|
85
|
+
let n = e.assignedSlot;
|
|
86
|
+
const d = () => {
|
|
87
|
+
const r = e.assignedSlot;
|
|
88
|
+
r !== n && (n = r, b()), a();
|
|
89
|
+
}, l = new MutationObserver(() => d());
|
|
90
|
+
l.observe(e, {
|
|
91
|
+
attributes: !0,
|
|
92
|
+
attributeFilter: [...t]
|
|
93
|
+
});
|
|
94
|
+
const s = new MutationObserver(() => {
|
|
95
|
+
b(), n = e.assignedSlot, a();
|
|
96
|
+
});
|
|
97
|
+
s.observe(e, {
|
|
98
|
+
attributes: !0,
|
|
99
|
+
attributeFilter: ["slot"]
|
|
100
|
+
});
|
|
101
|
+
const c = /* @__PURE__ */ new Map(), f = () => {
|
|
102
|
+
const r = [];
|
|
103
|
+
S(e, r);
|
|
104
|
+
const u = e.ownerDocument;
|
|
105
|
+
return u && !r.includes(u) && r.push(u), r;
|
|
106
|
+
}, b = () => {
|
|
107
|
+
if (!o) return;
|
|
108
|
+
const r = f(), u = new Set(r);
|
|
109
|
+
for (const [h, v] of c)
|
|
110
|
+
u.has(h) || (v(), c.delete(h));
|
|
111
|
+
for (const h of r)
|
|
112
|
+
c.has(h) || c.set(h, p(h, d));
|
|
113
|
+
};
|
|
114
|
+
return b(), a(), {
|
|
115
|
+
resync() {
|
|
116
|
+
n = e.assignedSlot, b(), a();
|
|
117
|
+
},
|
|
118
|
+
disconnect() {
|
|
119
|
+
l.disconnect(), s.disconnect();
|
|
120
|
+
for (const r of c.values())
|
|
121
|
+
r();
|
|
122
|
+
c.clear();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export {
|
|
127
|
+
y as i,
|
|
128
|
+
g as r,
|
|
129
|
+
R as s
|
|
130
|
+
};
|
|
131
|
+
//# sourceMappingURL=aria-idref-DCuEaknC.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aria-idref-CxvyzfQS.js","sources":["../../src/utils/aria-idref.ts"],"sourcesContent":["/**\n * Shared utilities for resolving `aria-labelledby` and `aria-describedby`\n * IDREF token lists across the Shadow DOM boundary.\n *\n * Selection-control components (`hx-checkbox`, `hx-radio-group`, `hx-switch`,\n * etc.) elevate their semantic surface to the host via `ElementInternals` so\n * that consumer-supplied `aria-labelledby` / `aria-describedby` on the host\n * resolves to light-DOM elements rather than being trapped on inner shadow\n * nodes.\n *\n * Modern Chromium 134+ and Safari 17.4+ expose the IDL element-references API\n * (`internals.ariaLabelledByElements`, `internals.ariaDescribedByElements`).\n * For those engines we resolve token IDs against the host's root node (a\n * Document or ShadowRoot) and assign the resulting elements directly.\n *\n * Older engines fall back to the host-attribute path: the ARIA delegation\n * mixin keeps the original token list in `data-aria-labelledby`/\n * `data-aria-describedby`, which assistive technology cannot follow into the\n * shadow root, but that is the same surface area as the pre-fix code and is\n * not a regression.\n *\n * Component authors do NOT need to wire this directly — see\n * `installAriaIdrefMirror()` for an installable observer that keeps an inner\n * node's `aria-*` attributes in sync with the token list across mutations.\n */\n\n/**\n * Resolves a whitespace-separated IDREF token list to live element references.\n *\n * Searches the host's containing root first (Document or ShadowRoot), then\n * walks up through enclosing shadow hosts, and finally falls back to the\n * top-level Document. Codex round-15 P1: hx-* controls embedded inside an\n * outer component's shadow tree often legitimately reference labels/descriptions\n * declared in the outer document or in an ancestor shadow tree. Restricting\n * resolution to a single root left those controls anonymous on the\n * `ariaLabelledByElements` / `ariaDescribedByElements` path. The IDL\n * element-references API accepts any element regardless of root, so widening\n * the search closes that gap.\n *\n * Codex round-16 P1: when the host is **slotted into** another component\n * (light-DOM child of a shadow-root-bearing element), `host.getRootNode()`\n * is still the document, so IDREF targets declared in the slot owner's\n * shadow root are unreachable through the ancestor-shadow-host chain. We\n * additionally walk `host.assignedSlot.getRootNode()` — that resolves to\n * the slot owner's shadow root — and continue up that root's host chain so\n * cross-shadow IDREF resolution works for the composed-tree slotting\n * pattern. The walk is recursive: a slot owner that is itself slotted into\n * another shadow tree contributes its own ancestor chain too.\n *\n * Tokens that fail to resolve at every level are silently dropped — matching\n * native attribute-string platform behaviour where unresolved tokens are\n * ignored.\n */\nexport function resolveIdrefTokens(host: Element, tokens: string | null): Element[] {\n if (!tokens) return [];\n const ids = tokens.split(/\\s+/).filter(Boolean);\n if (ids.length === 0) return [];\n\n // Build the ordered list of roots to search: host's own root first\n // (closest scope), then walking outward through enclosing shadow hosts,\n // then the top-level Document. Each id resolves at the first root that\n // owns it, mirroring how shadow-encapsulation-aware AT walks the tree.\n const roots: Array<Document | ShadowRoot> = [];\n collectIdrefSearchRoots(host, roots);\n // If host is detached or in an unusual root, also try the top-level\n // ownerDocument as a defensive last resort.\n const ownerDoc = host.ownerDocument;\n if (ownerDoc && !roots.includes(ownerDoc)) {\n roots.push(ownerDoc);\n }\n\n const out: Element[] = [];\n for (const id of ids) {\n for (const root of roots) {\n const el = root.getElementById(id);\n if (el) {\n out.push(el);\n break;\n }\n }\n }\n return out;\n}\n\n/**\n * Walks the composed-tree ancestry of `start` and pushes every Document or\n * ShadowRoot that could legitimately own an IDREF target into `roots` in the\n * order they should be searched (closest scope first). The walk crosses two\n * kinds of boundary:\n *\n * 1. A node sitting inside a ShadowRoot escapes via `root.host`.\n * 2. A node assigned to a `<slot>` in another shadow tree escapes via\n * `node.assignedSlot` — the slot lives in the slot-owner's shadow root,\n * so we hop into that root and keep climbing from there.\n *\n * Both pathways are followed because either may apply at any level. A\n * light-DOM custom element slotted into a shadow component has\n * `getRootNode() === document` AND `assignedSlot !== null`. Codex push-gate\n * round-1 finding 1: the slot-owner shadow root MUST be searched BEFORE the\n * document fallback. Element ids are unique per-tree, not globally, so the\n * same id may legally exist in BOTH the document and a slot-owner shadow\n * root. If the document is searched first, the resolver binds to the wrong\n * element and the slotted control gets the wrong accessible name. The\n * solution is to walk the slot chain (assignedSlot → slot-owner shadow root\n * → its host's chain) before falling through to the owner document.\n *\n * The slot-walk is transitive: a slot owner that is itself slotted into\n * another shadow tree contributes its own slot chain too, all of which is\n * searched ahead of the document. From inside any shadow root we follow\n * `root.host` outward AND, on every hop, check whether that host is itself\n * slotted into yet another shadow tree. De-duplication is by reference\n * identity.\n *\n * @internal\n */\nfunction collectIdrefSearchRoots(start: Element, roots: Array<Document | ShadowRoot>): void {\n const visited = new Set<Document | ShadowRoot>();\n\n const pushRoot = (root: Document | ShadowRoot): void => {\n if (visited.has(root)) return;\n visited.add(root);\n roots.push(root);\n };\n\n // Walk a single ancestor chain starting from `entry`, pushing every\n // ShadowRoot encountered (closest first) and queuing any slot-owner\n // shadow roots we cross via assignedSlot for separate exploration. The\n // owner document encountered at the end of a chain is NOT pushed here —\n // the caller controls when to fall through to the document so the\n // slot-owner trees can be searched first (codex push-gate round-1 #1).\n const visitedEntries = new Set<Element>([start]);\n const slotEntries: Element[] = [];\n let documentSeen = false;\n\n const walkChain = (entry: Element): void => {\n let currentNode: Element = entry;\n let currentRoot: Node | null = currentNode.getRootNode();\n\n // If the entry is itself in document scope and is slotted into a\n // shadow tree, queue the slot for slot-owner-first exploration.\n const slotFromEntry = (currentNode as HTMLElement).assignedSlot ?? null;\n if (slotFromEntry && !visitedEntries.has(slotFromEntry)) {\n visitedEntries.add(slotFromEntry);\n slotEntries.push(slotFromEntry);\n }\n\n while (currentRoot instanceof ShadowRoot) {\n pushRoot(currentRoot);\n const shadowHost: Element | null = currentRoot.host ?? null;\n if (!shadowHost) break;\n // The shadow host itself may be slotted into yet another component.\n // Queue that branch for slot-owner-first exploration.\n const hostSlot = (shadowHost as HTMLElement).assignedSlot ?? null;\n if (hostSlot && !visitedEntries.has(hostSlot)) {\n visitedEntries.add(hostSlot);\n slotEntries.push(hostSlot);\n }\n currentNode = shadowHost;\n currentRoot = shadowHost.getRootNode();\n }\n\n if (currentRoot instanceof Document) {\n // Defer the document push: slot-owner shadow roots queued during\n // this walk must be searched BEFORE the document so duplicate ids\n // resolve in the correct (slot-owner) scope first.\n documentSeen = true;\n }\n };\n\n walkChain(start);\n // Drain the slot-entry queue. Each slot entry contributes its own\n // ancestor chain, whose shadow roots are pushed ahead of the owner\n // document. Slot entries are processed in discovery order (FIFO) so\n // closer slot owners are searched before more distant ones.\n while (slotEntries.length > 0) {\n walkChain(slotEntries.shift() as Element);\n }\n\n if (documentSeen) {\n const ownerDoc = start.ownerDocument;\n if (ownerDoc) pushRoot(ownerDoc);\n }\n}\n\n/**\n * True when the runtime exposes the IDL element-references API on\n * `ElementInternals`. Older Firefox / Safari builds return `undefined`\n * for these accessors.\n */\nexport function supportsIdrefElementReferences(internals: ElementInternals): boolean {\n return (\n 'ariaLabelledByElements' in internals &&\n 'ariaDescribedByElements' in internals &&\n typeof (internals as ElementInternals & { ariaLabelledByElements?: unknown })\n .ariaLabelledByElements !== 'undefined'\n );\n}\n\n/**\n * Mirror snapshot describing what should be applied to inner ARIA-bearing\n * shadow nodes. Components consume this to project the correct attributes\n * onto whichever inner element owns the announced semantic role.\n */\nexport interface AriaIdrefSnapshot {\n /** Computed `aria-labelledby` token list, or `null` to omit. */\n labelledBy: string | null;\n /** Computed `aria-describedby` token list, or `null` to omit. */\n describedBy: string | null;\n}\n\n/**\n * Merges two whitespace-separated token lists, preserving order and removing\n * duplicates. `null`/empty inputs are skipped. Returns `null` when the merged\n * list is empty.\n */\nexport function mergeTokenLists(...lists: Array<string | null | undefined>): string | null {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const list of lists) {\n if (!list) continue;\n for (const token of list.split(/\\s+/)) {\n if (token && !seen.has(token)) {\n seen.add(token);\n out.push(token);\n }\n }\n }\n return out.length > 0 ? out.join(' ') : null;\n}\n\n/**\n * Options accepted by `installAriaIdrefMirror()`.\n */\nexport interface AriaIdrefMirrorOptions {\n /**\n * Attribute names on the host whose mutations should trigger a resync.\n * Defaults to `['aria-labelledby', 'aria-describedby', 'aria-label']` plus\n * the `data-aria-*` mirrors used by `mixinDelegatesAria`.\n */\n observedAttributes?: string[];\n /**\n * Whether to observe the resolved root for `id` attribute and `childList`\n * mutations so that late-inserted IDREF targets and id renames trigger a\n * resync. Defaults to `true`.\n */\n observeRoot?: boolean;\n}\n\n/**\n * Handle returned by `installAriaIdrefMirror()`. Call `disconnect()` from\n * `disconnectedCallback()` to tear the observers down. `resync()` forces an\n * immediate sync — useful from `connectedCallback()` after the host has been\n * re-attached to a new root.\n */\nexport interface AriaIdrefMirrorHandle {\n /** Force an immediate sync. */\n resync(): void;\n /** Tear down all observers and listeners. */\n disconnect(): void;\n}\n\n/**\n * Default attribute set observed on the host for ARIA / data-aria mirroring.\n */\nconst DEFAULT_HOST_OBSERVED_ATTRS: readonly string[] = [\n 'aria-label',\n 'aria-labelledby',\n 'aria-describedby',\n 'data-aria-label',\n 'data-aria-labelledby',\n 'data-aria-describedby',\n];\n\n/**\n * Per-root shared observer registry. Codex round-7 finding #11 (perf).\n *\n * Round-1 created a `subtree: true` MutationObserver per host instance, so a\n * page with N IDREF-aware controls would receive N×M sync callbacks for any\n * unrelated childList/id mutation in the document. This registry collapses\n * the cost: a single observer per Document/ShadowRoot fans mutations out to\n * the registered subscribers (the per-host `sync` callbacks) only.\n *\n * The registry uses a `WeakMap` keyed by root so subscribers are garbage\n * collected with their roots. Subscribers are stored in a `Set` keyed by the\n * `sync` callback identity so re-installation is idempotent.\n *\n * @internal\n */\ninterface SharedRootObserverEntry {\n observer: MutationObserver;\n subscribers: Set<() => void>;\n}\n\nconst sharedRootObservers: WeakMap<Document | ShadowRoot, SharedRootObserverEntry> = new WeakMap();\n\nfunction subscribeToRoot(root: Document | ShadowRoot, sync: () => void): () => void {\n let entry = sharedRootObservers.get(root);\n if (!entry) {\n const subscribers = new Set<() => void>();\n // Codex push-gate round-4 P2: components that flatten `aria-labelledby`\n // to a fallback string (legacy engines without `ariaLabelledByElements`,\n // and string-mirroring callers like hx-menu / hx-menu-item /\n // hx-overflow-menu / hx-split-button) must resync when:\n // - the referenced label's text content mutates in place\n // (`characterData`)\n // - the referenced target is hidden / unhidden via attributes\n // (`hidden`, `aria-hidden`, `style`, `class` — visibility affects\n // accessible-name computation per accname §4.3.2)\n // Without these triggers the mirrored `aria-label` stays stale.\n //\n // Widening the observer surface produces noisier callbacks, so the\n // shared subscriber fan-out is coalesced through a single\n // `requestAnimationFrame` (with a setTimeout fallback for environments\n // where rAF is unavailable, e.g. test-stubbed roots). Subscribers see\n // at most one resync per frame regardless of mutation density.\n let pendingFrame = 0;\n const flush = (): void => {\n pendingFrame = 0;\n // Snapshot subscribers before invocation: a sync() callback may itself\n // resubscribe (e.g. component reattach), and Set iteration over a live\n // collection during mutation is undefined.\n Array.from(subscribers).forEach((fn) => {\n fn();\n });\n };\n const scheduleFlush = (): void => {\n if (pendingFrame !== 0) return;\n const raf =\n typeof globalThis.requestAnimationFrame === 'function'\n ? globalThis.requestAnimationFrame\n : (cb: FrameRequestCallback) => globalThis.setTimeout(() => cb(performance.now()), 0);\n pendingFrame = raf(flush) as unknown as number;\n };\n const observer = new MutationObserver(() => {\n scheduleFlush();\n });\n observer.observe(root, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['id', 'hidden', 'aria-hidden', 'style', 'class'],\n characterData: true,\n });\n entry = { observer, subscribers };\n sharedRootObservers.set(root, entry);\n }\n entry.subscribers.add(sync);\n\n return () => {\n const current = sharedRootObservers.get(root);\n if (!current) return;\n current.subscribers.delete(sync);\n if (current.subscribers.size === 0) {\n current.observer.disconnect();\n sharedRootObservers.delete(root);\n }\n };\n}\n\n/**\n * Installs a `MutationObserver` pair that keeps host ARIA semantics in sync\n * with mutations to consumer-supplied attributes AND late-target / id\n * mutations in the host's resolved root.\n *\n * The `sync` callback is invoked on:\n * 1. Initial install (synchronously)\n * 2. Any change to one of the observed host attributes\n * 3. Any `id` attribute mutation, child insertion, or child removal in the\n * resolved root (Document or ShadowRoot containing the host)\n *\n * Components should call this from `connectedCallback()` and call\n * `handle.disconnect()` from `disconnectedCallback()`. The handle's\n * `resync()` method is safe to call from any lifecycle hook.\n *\n * Costs are bounded: the host observer touches one element; the root\n * observer is shared per `Document`/`ShadowRoot` (codex round-7 #11) so every\n * subscribing host pays a single attach cost regardless of how many other\n * IDREF-aware controls share the root.\n */\nexport function installAriaIdrefMirror(\n host: Element,\n sync: () => void,\n options: AriaIdrefMirrorOptions = {},\n): AriaIdrefMirrorHandle {\n const observedAttributes = options.observedAttributes ?? DEFAULT_HOST_OBSERVED_ATTRS;\n const observeRoot = options.observeRoot ?? true;\n\n // Observe consumer mutations to the host's ARIA / data-aria attributes.\n // We do NOT use `observedAttributes`/`attributeChangedCallback` here because\n // that requires class-level wiring that conflicts with downstream mixins\n // (e.g. `mixinDelegatesAria` already commandeers `attributeChangedCallback`\n // for the same attributes). A scoped `MutationObserver` is reentry-safe.\n const hostObserver = new MutationObserver(() => sync());\n hostObserver.observe(host, {\n attributes: true,\n attributeFilter: [...observedAttributes],\n });\n\n // Codex push-gate round-1 finding 2: when the host's `slot` attribute\n // changes, the host may have just been re-assigned into a different slot\n // owner's shadow tree — and therefore the set of reachable IDREF roots\n // has changed. The shared root observer's callback runs `sync()` (not\n // `resync()`), so observers stay attached to the OLD roots; later id /\n // childList mutations in the NEW slot owner shadow tree never fire\n // resync. A separate observer scoped to the host's `slot` attribute\n // triggers a full reattach via `resync()`.\n const slotAttrObserver = new MutationObserver(() => {\n // Reattach observers to the new reachable-roots set, then sync.\n attachRootObservers();\n sync();\n });\n slotAttrObserver.observe(host, {\n attributes: true,\n attributeFilter: ['slot'],\n });\n\n // Subscribe to the shared per-root observer so late-inserted targets and id\n // renames re-resolve through the IDREF path. Round-7 #11 collapses N\n // per-instance subtree observers into one per root, so on pages with many\n // controls a single mutation produces a single subscriber fan-out instead\n // of `controls × mutations` observer callbacks.\n //\n // Codex round-16 P1: subscribe to every root the resolver can match — the\n // host's own root, every enclosing shadow root, and the owner document.\n // Without this, dynamic IDREF targets in ancestor scopes (legitimate per\n // the round-15 widened resolver) bind correctly on first render but never\n // resync when the outer document mutates. We track active subscriptions\n // by root and incrementally diff on `attachRootObservers()` so resync\n // calls don't churn observers when nothing has changed.\n const rootSubscriptions = new Map<Document | ShadowRoot, () => void>();\n\n const computeRootsToObserve = (): Array<Document | ShadowRoot> => {\n const roots: Array<Document | ShadowRoot> = [];\n // Use the same composed-tree walk as `resolveIdrefTokens` so the\n // observer subscribes to every root the resolver can match — including\n // slot-owner shadow roots when the host is light-DOM-slotted into\n // another component (codex round-17 P1). Without this, a late id\n // mutation inside the slot owner's shadow tree never fires resync.\n collectIdrefSearchRoots(host, roots);\n const ownerDoc = host.ownerDocument;\n if (ownerDoc && !roots.includes(ownerDoc)) {\n roots.push(ownerDoc);\n }\n return roots;\n };\n\n const attachRootObservers = (): void => {\n if (!observeRoot) return;\n const wanted = computeRootsToObserve();\n const wantedSet = new Set(wanted);\n // Remove subscriptions for roots no longer in scope (e.g. when the host\n // is moved between trees and the old ancestor chain no longer applies).\n for (const [root, unsub] of rootSubscriptions) {\n if (!wantedSet.has(root)) {\n unsub();\n rootSubscriptions.delete(root);\n }\n }\n // Add subscriptions for new roots (host's root + ancestor shadow roots\n // + owner document) that the resolver can now match against.\n for (const root of wanted) {\n if (!rootSubscriptions.has(root)) {\n rootSubscriptions.set(root, subscribeToRoot(root, sync));\n }\n }\n };\n\n attachRootObservers();\n // Initial sync — caller's `sync` reads the current attribute snapshot.\n sync();\n\n return {\n resync(): void {\n attachRootObservers();\n sync();\n },\n disconnect(): void {\n hostObserver.disconnect();\n slotAttrObserver.disconnect();\n for (const unsub of rootSubscriptions.values()) {\n unsub();\n }\n rootSubscriptions.clear();\n },\n };\n}\n"],"names":["resolveIdrefTokens","host","tokens","ids","roots","collectIdrefSearchRoots","ownerDoc","out","id","root","el","start","visited","pushRoot","visitedEntries","slotEntries","documentSeen","walkChain","entry","currentNode","currentRoot","slotFromEntry","shadowHost","hostSlot","supportsIdrefElementReferences","internals","DEFAULT_HOST_OBSERVED_ATTRS","sharedRootObservers","subscribeToRoot","sync","subscribers","pendingFrame","flush","fn","scheduleFlush","cb","observer","current","installAriaIdrefMirror","options","observedAttributes","observeRoot","hostObserver","slotAttrObserver","attachRootObservers","rootSubscriptions","computeRootsToObserve","wanted","wantedSet","unsub"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"aria-idref-DCuEaknC.js","sources":["../../src/utils/aria-idref.ts"],"sourcesContent":["/**\n * Shared utilities for resolving `aria-labelledby` and `aria-describedby`\n * IDREF token lists across the Shadow DOM boundary.\n *\n * Selection-control components (`hx-checkbox`, `hx-radio-group`, `hx-switch`,\n * etc.) elevate their semantic surface to the host via `ElementInternals` so\n * that consumer-supplied `aria-labelledby` / `aria-describedby` on the host\n * resolves to light-DOM elements rather than being trapped on inner shadow\n * nodes.\n *\n * Modern Chromium 134+ and Safari 17.4+ expose the IDL element-references API\n * (`internals.ariaLabelledByElements`, `internals.ariaDescribedByElements`).\n * For those engines we resolve token IDs against the host's root node (a\n * Document or ShadowRoot) and assign the resulting elements directly.\n *\n * Older engines fall back to the host-attribute path: the ARIA delegation\n * mixin keeps the original token list in `data-aria-labelledby`/\n * `data-aria-describedby`, which assistive technology cannot follow into the\n * shadow root, but that is the same surface area as the pre-fix code and is\n * not a regression.\n *\n * Component authors do NOT need to wire this directly — see\n * `installAriaIdrefMirror()` for an installable observer that keeps an inner\n * node's `aria-*` attributes in sync with the token list across mutations.\n */\n\n/**\n * Resolves a whitespace-separated IDREF token list to live element references.\n *\n * Searches the host's containing root first (Document or ShadowRoot), then\n * walks up through enclosing shadow hosts, and finally falls back to the\n * top-level Document. Codex round-15 P1: hx-* controls embedded inside an\n * outer component's shadow tree often legitimately reference labels/descriptions\n * declared in the outer document or in an ancestor shadow tree. Restricting\n * resolution to a single root left those controls anonymous on the\n * `ariaLabelledByElements` / `ariaDescribedByElements` path. The IDL\n * element-references API accepts any element regardless of root, so widening\n * the search closes that gap.\n *\n * Codex round-16 P1: when the host is **slotted into** another component\n * (light-DOM child of a shadow-root-bearing element), `host.getRootNode()`\n * is still the document, so IDREF targets declared in the slot owner's\n * shadow root are unreachable through the ancestor-shadow-host chain. We\n * additionally walk `host.assignedSlot.getRootNode()` — that resolves to\n * the slot owner's shadow root — and continue up that root's host chain so\n * cross-shadow IDREF resolution works for the composed-tree slotting\n * pattern. The walk is recursive: a slot owner that is itself slotted into\n * another shadow tree contributes its own ancestor chain too.\n *\n * Tokens that fail to resolve at every level are silently dropped — matching\n * native attribute-string platform behaviour where unresolved tokens are\n * ignored.\n */\nexport function resolveIdrefTokens(host: Element, tokens: string | null): Element[] {\n if (!tokens) return [];\n const ids = tokens.split(/\\s+/).filter(Boolean);\n if (ids.length === 0) return [];\n\n // Build the ordered list of roots to search: host's own root first\n // (closest scope), then walking outward through enclosing shadow hosts,\n // then the top-level Document. Each id resolves at the first root that\n // owns it, mirroring how shadow-encapsulation-aware AT walks the tree.\n const roots: Array<Document | ShadowRoot> = [];\n collectIdrefSearchRoots(host, roots);\n // If host is detached or in an unusual root, also try the top-level\n // ownerDocument as a defensive last resort.\n const ownerDoc = host.ownerDocument;\n if (ownerDoc && !roots.includes(ownerDoc)) {\n roots.push(ownerDoc);\n }\n\n const out: Element[] = [];\n for (const id of ids) {\n for (const root of roots) {\n const el = root.getElementById(id);\n if (el) {\n out.push(el);\n break;\n }\n }\n }\n return out;\n}\n\n/**\n * Walks the composed-tree ancestry of `start` and pushes every Document or\n * ShadowRoot that could legitimately own an IDREF target into `roots` in the\n * order they should be searched (closest scope first). The walk crosses two\n * kinds of boundary:\n *\n * 1. A node sitting inside a ShadowRoot escapes via `root.host`.\n * 2. A node assigned to a `<slot>` in another shadow tree escapes via\n * `node.assignedSlot` — the slot lives in the slot-owner's shadow root,\n * so we hop into that root and keep climbing from there.\n *\n * Both pathways are followed because either may apply at any level. A\n * light-DOM custom element slotted into a shadow component has\n * `getRootNode() === document` AND `assignedSlot !== null`. Codex push-gate\n * round-1 finding 1: the slot-owner shadow root MUST be searched BEFORE the\n * document fallback. Element ids are unique per-tree, not globally, so the\n * same id may legally exist in BOTH the document and a slot-owner shadow\n * root. If the document is searched first, the resolver binds to the wrong\n * element and the slotted control gets the wrong accessible name. The\n * solution is to walk the slot chain (assignedSlot → slot-owner shadow root\n * → its host's chain) before falling through to the owner document.\n *\n * The slot-walk is transitive: a slot owner that is itself slotted into\n * another shadow tree contributes its own slot chain too, all of which is\n * searched ahead of the document. From inside any shadow root we follow\n * `root.host` outward AND, on every hop, check whether that host is itself\n * slotted into yet another shadow tree. De-duplication is by reference\n * identity.\n *\n * @internal\n */\nfunction collectIdrefSearchRoots(start: Element, roots: Array<Document | ShadowRoot>): void {\n const visited = new Set<Document | ShadowRoot>();\n\n const pushRoot = (root: Document | ShadowRoot): void => {\n if (visited.has(root)) return;\n visited.add(root);\n roots.push(root);\n };\n\n // Walk a single ancestor chain starting from `entry`, pushing every\n // ShadowRoot encountered (closest first) and queuing any slot-owner\n // shadow roots we cross via assignedSlot for separate exploration. The\n // owner document encountered at the end of a chain is NOT pushed here —\n // the caller controls when to fall through to the document so the\n // slot-owner trees can be searched first (codex push-gate round-1 #1).\n const visitedEntries = new Set<Element>([start]);\n const slotEntries: Element[] = [];\n let documentSeen = false;\n\n const walkChain = (entry: Element): void => {\n let currentNode: Element = entry;\n let currentRoot: Node | null = currentNode.getRootNode();\n\n // If the entry is itself in document scope and is slotted into a\n // shadow tree, queue the slot for slot-owner-first exploration.\n const slotFromEntry = (currentNode as HTMLElement).assignedSlot ?? null;\n if (slotFromEntry && !visitedEntries.has(slotFromEntry)) {\n visitedEntries.add(slotFromEntry);\n slotEntries.push(slotFromEntry);\n }\n\n while (currentRoot instanceof ShadowRoot) {\n pushRoot(currentRoot);\n const shadowHost: Element | null = currentRoot.host ?? null;\n if (!shadowHost) break;\n // The shadow host itself may be slotted into yet another component.\n // Queue that branch for slot-owner-first exploration.\n const hostSlot = (shadowHost as HTMLElement).assignedSlot ?? null;\n if (hostSlot && !visitedEntries.has(hostSlot)) {\n visitedEntries.add(hostSlot);\n slotEntries.push(hostSlot);\n }\n currentNode = shadowHost;\n currentRoot = shadowHost.getRootNode();\n }\n\n if (currentRoot instanceof Document) {\n // Defer the document push: slot-owner shadow roots queued during\n // this walk must be searched BEFORE the document so duplicate ids\n // resolve in the correct (slot-owner) scope first.\n documentSeen = true;\n }\n };\n\n walkChain(start);\n // Drain the slot-entry queue. Each slot entry contributes its own\n // ancestor chain, whose shadow roots are pushed ahead of the owner\n // document. Slot entries are processed in discovery order (FIFO) so\n // closer slot owners are searched before more distant ones.\n while (slotEntries.length > 0) {\n walkChain(slotEntries.shift() as Element);\n }\n\n if (documentSeen) {\n const ownerDoc = start.ownerDocument;\n if (ownerDoc) pushRoot(ownerDoc);\n }\n}\n\n/**\n * True when the runtime exposes the IDL element-references API on\n * `ElementInternals`. Older Firefox / Safari builds return `undefined`\n * for these accessors.\n */\nexport function supportsIdrefElementReferences(internals: ElementInternals): boolean {\n return (\n 'ariaLabelledByElements' in internals &&\n 'ariaDescribedByElements' in internals &&\n typeof (internals as ElementInternals & { ariaLabelledByElements?: unknown })\n .ariaLabelledByElements !== 'undefined'\n );\n}\n\n/**\n * Mirror snapshot describing what should be applied to inner ARIA-bearing\n * shadow nodes. Components consume this to project the correct attributes\n * onto whichever inner element owns the announced semantic role.\n */\nexport interface AriaIdrefSnapshot {\n /** Computed `aria-labelledby` token list, or `null` to omit. */\n labelledBy: string | null;\n /** Computed `aria-describedby` token list, or `null` to omit. */\n describedBy: string | null;\n}\n\n/**\n * Merges two whitespace-separated token lists, preserving order and removing\n * duplicates. `null`/empty inputs are skipped. Returns `null` when the merged\n * list is empty.\n */\nexport function mergeTokenLists(...lists: Array<string | null | undefined>): string | null {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const list of lists) {\n if (!list) continue;\n for (const token of list.split(/\\s+/)) {\n if (token && !seen.has(token)) {\n seen.add(token);\n out.push(token);\n }\n }\n }\n return out.length > 0 ? out.join(' ') : null;\n}\n\n/**\n * Options accepted by `installAriaIdrefMirror()`.\n */\nexport interface AriaIdrefMirrorOptions {\n /**\n * Attribute names on the host whose mutations should trigger a resync.\n * Defaults to `['aria-labelledby', 'aria-describedby', 'aria-label']` plus\n * the `data-aria-*` mirrors used by `mixinDelegatesAria`.\n */\n observedAttributes?: string[];\n /**\n * Whether to observe the resolved root for `id` attribute and `childList`\n * mutations so that late-inserted IDREF targets and id renames trigger a\n * resync. Defaults to `true`.\n */\n observeRoot?: boolean;\n}\n\n/**\n * Handle returned by `installAriaIdrefMirror()`. Call `disconnect()` from\n * `disconnectedCallback()` to tear the observers down. `resync()` forces an\n * immediate sync — useful from `connectedCallback()` after the host has been\n * re-attached to a new root.\n */\nexport interface AriaIdrefMirrorHandle {\n /** Force an immediate sync. */\n resync(): void;\n /** Tear down all observers and listeners. */\n disconnect(): void;\n}\n\n/**\n * Default attribute set observed on the host for ARIA / data-aria mirroring.\n */\nconst DEFAULT_HOST_OBSERVED_ATTRS: readonly string[] = [\n 'aria-label',\n 'aria-labelledby',\n 'aria-describedby',\n 'data-aria-label',\n 'data-aria-labelledby',\n 'data-aria-describedby',\n];\n\n/**\n * Per-root shared observer registry. Codex round-7 finding #11 (perf).\n *\n * Round-1 created a `subtree: true` MutationObserver per host instance, so a\n * page with N IDREF-aware controls would receive N×M sync callbacks for any\n * unrelated childList/id mutation in the document. This registry collapses\n * the cost: a single observer per Document/ShadowRoot fans mutations out to\n * the registered subscribers (the per-host `sync` callbacks) only.\n *\n * The registry uses a `WeakMap` keyed by root so subscribers are garbage\n * collected with their roots. Subscribers are stored in a `Set` keyed by the\n * `sync` callback identity so re-installation is idempotent.\n *\n * @internal\n */\ninterface SharedRootObserverEntry {\n observer: MutationObserver;\n subscribers: Set<() => void>;\n}\n\nconst sharedRootObservers: WeakMap<Document | ShadowRoot, SharedRootObserverEntry> = new WeakMap();\n\nfunction subscribeToRoot(root: Document | ShadowRoot, sync: () => void): () => void {\n let entry = sharedRootObservers.get(root);\n if (!entry) {\n const subscribers = new Set<() => void>();\n // Codex push-gate round-4 P2: components that flatten `aria-labelledby`\n // to a fallback string (legacy engines without `ariaLabelledByElements`,\n // and string-mirroring callers like hx-menu / hx-menu-item /\n // hx-overflow-menu / hx-split-button) must resync when:\n // - the referenced label's text content mutates in place\n // (`characterData`)\n // - the referenced target is hidden / unhidden via attributes\n // (`hidden`, `aria-hidden`, `style`, `class` — visibility affects\n // accessible-name computation per accname §4.3.2)\n // Without these triggers the mirrored `aria-label` stays stale.\n //\n // Widening the observer surface produces noisier callbacks, so the\n // shared subscriber fan-out is coalesced through a single\n // `requestAnimationFrame` (with a setTimeout fallback for environments\n // where rAF is unavailable, e.g. test-stubbed roots). Subscribers see\n // at most one resync per frame regardless of mutation density.\n let pendingFrame = 0;\n const flush = (): void => {\n pendingFrame = 0;\n // Snapshot subscribers before invocation: a sync() callback may itself\n // resubscribe (e.g. component reattach), and Set iteration over a live\n // collection during mutation is undefined.\n Array.from(subscribers).forEach((fn) => {\n fn();\n });\n };\n const scheduleFlush = (): void => {\n if (pendingFrame !== 0) return;\n const raf =\n typeof globalThis.requestAnimationFrame === 'function'\n ? globalThis.requestAnimationFrame\n : (cb: FrameRequestCallback) => globalThis.setTimeout(() => cb(performance.now()), 0);\n pendingFrame = raf(flush) as unknown as number;\n };\n const observer = new MutationObserver(() => {\n scheduleFlush();\n });\n observer.observe(root, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['id', 'hidden', 'aria-hidden', 'style', 'class'],\n characterData: true,\n });\n entry = { observer, subscribers };\n sharedRootObservers.set(root, entry);\n }\n entry.subscribers.add(sync);\n\n return () => {\n const current = sharedRootObservers.get(root);\n if (!current) return;\n current.subscribers.delete(sync);\n if (current.subscribers.size === 0) {\n current.observer.disconnect();\n sharedRootObservers.delete(root);\n }\n };\n}\n\n/**\n * Installs a `MutationObserver` pair that keeps host ARIA semantics in sync\n * with mutations to consumer-supplied attributes AND late-target / id\n * mutations in the host's resolved root.\n *\n * The `sync` callback is invoked on:\n * 1. Initial install (synchronously)\n * 2. Any change to one of the observed host attributes\n * 3. Any `id` attribute mutation, child insertion, or child removal in the\n * resolved root (Document or ShadowRoot containing the host)\n *\n * Components should call this from `connectedCallback()` and call\n * `handle.disconnect()` from `disconnectedCallback()`. The handle's\n * `resync()` method is safe to call from any lifecycle hook.\n *\n * Costs are bounded: the host observer touches one element; the root\n * observer is shared per `Document`/`ShadowRoot` (codex round-7 #11) so every\n * subscribing host pays a single attach cost regardless of how many other\n * IDREF-aware controls share the root.\n */\nexport function installAriaIdrefMirror(\n host: Element,\n sync: () => void,\n options: AriaIdrefMirrorOptions = {},\n): AriaIdrefMirrorHandle {\n const observedAttributes = options.observedAttributes ?? DEFAULT_HOST_OBSERVED_ATTRS;\n const observeRoot = options.observeRoot ?? true;\n\n // CodeRabbit SHOULD-FIX (PR #1649 follow-up): the `slot` attribute\n // observer below only fires when the host's `slot` attribute mutates.\n // When the host is REPARENTED (e.g. moved into a different shadow tree\n // whose slot has the same name), `assignedSlot` changes but the host's\n // own attributes don't, so the observer never fires and the resolver\n // keeps watching the OLD slot-owner shadow root. Poll `assignedSlot`\n // identity on every sync; when it shifts, reattach root observers so\n // the new slot-owner shadow root is in scope before we resolve idrefs.\n let lastAssignedSlot: HTMLSlotElement | null = host.assignedSlot;\n const reparentAwareSync = (): void => {\n const currentSlot = host.assignedSlot;\n if (currentSlot !== lastAssignedSlot) {\n lastAssignedSlot = currentSlot;\n attachRootObservers();\n }\n sync();\n };\n\n // Observe consumer mutations to the host's ARIA / data-aria attributes.\n // We do NOT use `observedAttributes`/`attributeChangedCallback` here because\n // that requires class-level wiring that conflicts with downstream mixins\n // (e.g. `mixinDelegatesAria` already commandeers `attributeChangedCallback`\n // for the same attributes). A scoped `MutationObserver` is reentry-safe.\n const hostObserver = new MutationObserver(() => reparentAwareSync());\n hostObserver.observe(host, {\n attributes: true,\n attributeFilter: [...observedAttributes],\n });\n\n // Codex push-gate round-1 finding 2: when the host's `slot` attribute\n // changes, the host may have just been re-assigned into a different slot\n // owner's shadow tree — and therefore the set of reachable IDREF roots\n // has changed. The shared root observer's callback runs `sync()` (not\n // `resync()`), so observers stay attached to the OLD roots; later id /\n // childList mutations in the NEW slot owner shadow tree never fire\n // resync. A separate observer scoped to the host's `slot` attribute\n // triggers a full reattach via `resync()`.\n const slotAttrObserver = new MutationObserver(() => {\n // Reattach observers to the new reachable-roots set, then sync.\n attachRootObservers();\n // Refresh the assignedSlot snapshot too — slot attr changes can also\n // cause assignedSlot to flip in the same task tick.\n lastAssignedSlot = host.assignedSlot;\n sync();\n });\n slotAttrObserver.observe(host, {\n attributes: true,\n attributeFilter: ['slot'],\n });\n\n // Subscribe to the shared per-root observer so late-inserted targets and id\n // renames re-resolve through the IDREF path. Round-7 #11 collapses N\n // per-instance subtree observers into one per root, so on pages with many\n // controls a single mutation produces a single subscriber fan-out instead\n // of `controls × mutations` observer callbacks.\n //\n // Codex round-16 P1: subscribe to every root the resolver can match — the\n // host's own root, every enclosing shadow root, and the owner document.\n // Without this, dynamic IDREF targets in ancestor scopes (legitimate per\n // the round-15 widened resolver) bind correctly on first render but never\n // resync when the outer document mutates. We track active subscriptions\n // by root and incrementally diff on `attachRootObservers()` so resync\n // calls don't churn observers when nothing has changed.\n const rootSubscriptions = new Map<Document | ShadowRoot, () => void>();\n\n const computeRootsToObserve = (): Array<Document | ShadowRoot> => {\n const roots: Array<Document | ShadowRoot> = [];\n // Use the same composed-tree walk as `resolveIdrefTokens` so the\n // observer subscribes to every root the resolver can match — including\n // slot-owner shadow roots when the host is light-DOM-slotted into\n // another component (codex round-17 P1). Without this, a late id\n // mutation inside the slot owner's shadow tree never fires resync.\n collectIdrefSearchRoots(host, roots);\n const ownerDoc = host.ownerDocument;\n if (ownerDoc && !roots.includes(ownerDoc)) {\n roots.push(ownerDoc);\n }\n return roots;\n };\n\n const attachRootObservers = (): void => {\n if (!observeRoot) return;\n const wanted = computeRootsToObserve();\n const wantedSet = new Set(wanted);\n // Remove subscriptions for roots no longer in scope (e.g. when the host\n // is moved between trees and the old ancestor chain no longer applies).\n for (const [root, unsub] of rootSubscriptions) {\n if (!wantedSet.has(root)) {\n unsub();\n rootSubscriptions.delete(root);\n }\n }\n // Add subscriptions for new roots (host's root + ancestor shadow roots\n // + owner document) that the resolver can now match against.\n for (const root of wanted) {\n if (!rootSubscriptions.has(root)) {\n rootSubscriptions.set(root, subscribeToRoot(root, reparentAwareSync));\n }\n }\n };\n\n attachRootObservers();\n // Initial sync — caller's `sync` reads the current attribute snapshot.\n sync();\n\n return {\n resync(): void {\n lastAssignedSlot = host.assignedSlot;\n attachRootObservers();\n sync();\n },\n disconnect(): void {\n hostObserver.disconnect();\n slotAttrObserver.disconnect();\n for (const unsub of rootSubscriptions.values()) {\n unsub();\n }\n rootSubscriptions.clear();\n },\n };\n}\n"],"names":["resolveIdrefTokens","host","tokens","ids","roots","collectIdrefSearchRoots","ownerDoc","out","id","root","el","start","visited","pushRoot","visitedEntries","slotEntries","documentSeen","walkChain","entry","currentNode","currentRoot","slotFromEntry","shadowHost","hostSlot","supportsIdrefElementReferences","internals","DEFAULT_HOST_OBSERVED_ATTRS","sharedRootObservers","subscribeToRoot","sync","subscribers","pendingFrame","flush","fn","scheduleFlush","cb","observer","current","installAriaIdrefMirror","options","observedAttributes","observeRoot","lastAssignedSlot","reparentAwareSync","currentSlot","attachRootObservers","hostObserver","slotAttrObserver","rootSubscriptions","computeRootsToObserve","wanted","wantedSet","unsub"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { css as u, html as h } from "lit";
|
|
2
|
-
import { property as
|
|
3
|
-
import { classMap as
|
|
2
|
+
import { property as d, customElement as c } from "lit/decorators.js";
|
|
3
|
+
import { classMap as b } from "lit/directives/class-map.js";
|
|
4
4
|
import { f as p } from "./forced-colors-CTEDFRGa.js";
|
|
5
5
|
import { H as m } from "./helix-element-BNEYeiys.js";
|
|
6
6
|
const g = u`
|
|
@@ -125,14 +125,14 @@ const g = u`
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
`;
|
|
128
|
-
var f = Object.defineProperty, x = Object.getOwnPropertyDescriptor, s = (r,
|
|
129
|
-
for (var t = i > 1 ? void 0 : i ? x(
|
|
130
|
-
(
|
|
131
|
-
return i && t && f(
|
|
128
|
+
var f = Object.defineProperty, x = Object.getOwnPropertyDescriptor, s = (r, o, l, i) => {
|
|
129
|
+
for (var t = i > 1 ? void 0 : i ? x(o, l) : o, a = r.length - 1, n; a >= 0; a--)
|
|
130
|
+
(n = r[a]) && (t = (i ? n(o, l, t) : n(t)) || t);
|
|
131
|
+
return i && t && f(o, l, t), t;
|
|
132
132
|
};
|
|
133
|
-
let
|
|
133
|
+
let e = class extends m {
|
|
134
134
|
constructor() {
|
|
135
|
-
super(...arguments), this._orientation = "horizontal", this.size = "md", this.label = "", this._consumerAriaLabel = null, this._emptyLabelWarnEmitted = !1;
|
|
135
|
+
super(...arguments), this._orientation = "horizontal", this.size = "md", this.label = "", this._consumerAriaLabel = null, this._consumerRole = null, this._emptyLabelWarnEmitted = !1;
|
|
136
136
|
}
|
|
137
137
|
get orientation() {
|
|
138
138
|
return this._orientation;
|
|
@@ -144,14 +144,14 @@ let o = class extends m {
|
|
|
144
144
|
super.updated(r), r.has("size") && this.style.setProperty("--hx-button-group-size", this.size), r.has("label") && (this.label ? this.setAttribute("aria-label", this.label) : this._consumerAriaLabel !== null ? this.setAttribute("aria-label", this._consumerAriaLabel) : this.removeAttribute("aria-label"));
|
|
145
145
|
}
|
|
146
146
|
connectedCallback() {
|
|
147
|
-
super.connectedCallback(), this._consumerAriaLabel === null && this.hasAttribute("aria-label") && (this._consumerAriaLabel = this.getAttribute("aria-label")), this.
|
|
147
|
+
super.connectedCallback(), this._consumerAriaLabel === null && this.hasAttribute("aria-label") && (this._consumerAriaLabel = this.getAttribute("aria-label")), this._consumerRole === null && this.hasAttribute("role") && (this._consumerRole = this.getAttribute("role")), this._consumerRole ? this._internals.role = this._consumerRole : (this._internals.role = "group", this.setAttribute("role", "group")), this.style.setProperty("--hx-button-group-size", this.size), this.label ? this.setAttribute("aria-label", this.label) : this._consumerAriaLabel !== null || this._emptyLabelWarnEmitted || (this._emptyLabelWarnEmitted = !0);
|
|
148
148
|
}
|
|
149
149
|
// ─── Render ───
|
|
150
150
|
render() {
|
|
151
151
|
return h`
|
|
152
152
|
<div
|
|
153
153
|
part="group"
|
|
154
|
-
class=${
|
|
154
|
+
class=${b({
|
|
155
155
|
group: !0,
|
|
156
156
|
"group--horizontal": this.orientation === "horizontal",
|
|
157
157
|
"group--vertical": this.orientation === "vertical"
|
|
@@ -162,20 +162,20 @@ let o = class extends m {
|
|
|
162
162
|
`;
|
|
163
163
|
}
|
|
164
164
|
};
|
|
165
|
-
|
|
165
|
+
e.styles = [g, p];
|
|
166
166
|
s([
|
|
167
|
-
|
|
168
|
-
],
|
|
167
|
+
d({ type: String, reflect: !0 })
|
|
168
|
+
], e.prototype, "orientation", 1);
|
|
169
169
|
s([
|
|
170
|
-
|
|
171
|
-
],
|
|
170
|
+
d({ type: String, reflect: !0, attribute: "hx-size" })
|
|
171
|
+
], e.prototype, "size", 2);
|
|
172
172
|
s([
|
|
173
|
-
|
|
174
|
-
],
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
],
|
|
173
|
+
d({ type: String })
|
|
174
|
+
], e.prototype, "label", 2);
|
|
175
|
+
e = s([
|
|
176
|
+
c("hx-button-group")
|
|
177
|
+
], e);
|
|
178
178
|
export {
|
|
179
|
-
|
|
179
|
+
e as H
|
|
180
180
|
};
|
|
181
|
-
//# sourceMappingURL=hx-button-group-
|
|
181
|
+
//# sourceMappingURL=hx-button-group-4NUBpkyC.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hx-button-group-DcHP5MBv.js","sources":["../../src/components/hx-button-group/hx-button-group.styles.ts","../../src/components/hx-button-group/hx-button-group.ts"],"sourcesContent":["import { css } from 'lit';\n\nexport const helixButtonGroupStyles = css`\n :host {\n display: inline-flex;\n contain: layout style;\n }\n\n .group {\n display: inline-flex;\n align-items: stretch;\n }\n\n /* ─── Orientation Variants ─── */\n\n .group--horizontal {\n flex-direction: row;\n }\n\n .group--vertical {\n flex-direction: column;\n }\n\n /* ─── No Double Borders: Horizontal ─── */\n\n .group--horizontal ::slotted(*:not(:first-child)) {\n margin-inline-start: var(\n --hx-button-group-divider-offset,\n calc(-1 * var(--hx-border-width-thin, 1px))\n );\n }\n\n /* ─── No Double Borders: Vertical ─── */\n\n .group--vertical ::slotted(*:not(:first-child)) {\n margin-top: var(--hx-button-group-divider-offset, calc(-1 * var(--hx-border-width-thin, 1px)));\n }\n\n /* ─── Border Radius: Horizontal — Single child keeps all corners ─── */\n\n .group--horizontal ::slotted(:only-child) {\n --hx-button-border-radius: var(\n --hx-button-group-border-radius,\n var(--hx-border-radius-md, 0.375rem)\n );\n }\n\n /* ─── Border Radius: Horizontal — First child keeps left corners ─── */\n\n .group--horizontal ::slotted(:first-child:not(:only-child)) {\n --hx-button-border-radius: var(\n --hx-button-group-border-radius,\n var(--hx-border-radius-md, 0.375rem)\n )\n 0 0 var(--hx-button-group-border-radius, var(--hx-border-radius-md, 0.375rem));\n }\n\n /* ─── Border Radius: Horizontal — Last child keeps right corners ─── */\n\n .group--horizontal ::slotted(:last-child:not(:only-child)) {\n --hx-button-border-radius: 0\n var(--hx-button-group-border-radius, var(--hx-border-radius-md, 0.375rem))\n var(--hx-button-group-border-radius, var(--hx-border-radius-md, 0.375rem)) 0;\n }\n\n /* ─── Border Radius: Horizontal — Middle children have no radius ─── */\n\n .group--horizontal ::slotted(:not(:first-child):not(:last-child)) {\n --hx-button-border-radius: 0;\n }\n\n /* ─── Border Radius: Vertical — Single child keeps all corners ─── */\n\n .group--vertical ::slotted(:only-child) {\n --hx-button-border-radius: var(\n --hx-button-group-border-radius,\n var(--hx-border-radius-md, 0.375rem)\n );\n }\n\n /* ─── Border Radius: Vertical — First child keeps top corners ─── */\n\n .group--vertical ::slotted(:first-child:not(:only-child)) {\n --hx-button-border-radius: var(\n --hx-button-group-border-radius,\n var(--hx-border-radius-md, 0.375rem)\n )\n var(--hx-button-group-border-radius, var(--hx-border-radius-md, 0.375rem)) 0 0;\n }\n\n /* ─── Border Radius: Vertical — Last child keeps bottom corners ─── */\n\n .group--vertical ::slotted(:last-child:not(:only-child)) {\n --hx-button-border-radius: 0 0\n var(--hx-button-group-border-radius, var(--hx-border-radius-md, 0.375rem))\n var(--hx-button-group-border-radius, var(--hx-border-radius-md, 0.375rem));\n }\n\n /* ─── Border Radius: Vertical — Middle children have no radius ─── */\n\n .group--vertical ::slotted(:not(:first-child):not(:last-child)) {\n --hx-button-border-radius: 0;\n }\n\n /* ─── Z-index: Raise focused child above siblings to show full focus ring ─── */\n\n .group ::slotted(:focus-within) {\n z-index: var(--hx-button-group-focus-z-index, 1);\n position: relative;\n }\n\n /* ─── High Contrast Mode (forced-colors) ─── */\n\n @media (forced-colors: active) {\n /*\n * In forced-colors mode, negative margins that collapse borders between grouped\n * buttons can obscure focus rings. Raise focused children so the Highlight\n * outline from hx-button's own forced-colors block is fully visible.\n */\n .group ::slotted(:focus-within) {\n z-index: var(--hx-button-group-focus-z-index-hc, 2);\n }\n }\n`;\n","import { html, type PropertyValues } from 'lit';\nimport '../../utilities/document-token-adoption.js';\nimport { customElement, property } from 'lit/decorators.js';\nimport { classMap } from 'lit/directives/class-map.js';\nimport { HelixElement } from '../../base/index.js';\nimport { helixButtonGroupStyles } from './hx-button-group.styles.js';\nimport { forcedColorsInteractive } from '../../styles/forced-colors.js';\nimport { devWarn } from '../../utils/dev-warn.js';\n\n/**\n * A container component that groups related hx-button elements into a cohesive\n * horizontal or vertical action set. Eliminates double borders between adjacent\n * buttons and squares off inner border-radius for a unified visual appearance.\n *\n * **Accessibility:** Always provide an accessible label via `aria-label` or\n * `aria-labelledby` so screen readers can announce the group purpose.\n *\n * @summary Groups hx-button elements into a horizontal or vertical action set with shared borders.\n *\n * @tag hx-button-group\n *\n * @slot - Default slot accepting hx-button children.\n *\n * @csspart group - The container div element wrapping all slotted buttons.\n *\n * @cssprop [--hx-button-group-size=md] - Size token forwarded to child buttons. Accepts 'sm', 'md', or 'lg'.\n * @cssprop [--hx-border-width-thin] - Width.\n * @cssprop [--hx-border-radius-md] - CSS custom property.\n */\n@customElement('hx-button-group')\nexport class HelixButtonGroup extends HelixElement {\n static override styles = [helixButtonGroupStyles, forcedColorsInteractive];\n\n /**\n * Layout orientation of the button group.\n * @attr orientation\n */\n @property({ type: String, reflect: true })\n get orientation(): 'horizontal' | 'vertical' {\n return this._orientation;\n }\n set orientation(value: string) {\n if (value !== 'horizontal' && value !== 'vertical') {\n devWarn('hx-button-group', `Invalid orientation \"${value}\", defaulting to \"horizontal\".`);\n value = 'horizontal';\n }\n this._orientation = value as 'horizontal' | 'vertical';\n }\n /**\n * Backing store for the orientation property, holding the validated orientation value.\n * @internal\n */\n private _orientation: 'horizontal' | 'vertical' = 'horizontal';\n\n /**\n * Size applied to the button group and cascaded to child buttons via\n * the --hx-button-group-size CSS custom property.\n * @attr hx-size\n */\n @property({ type: String, reflect: true, attribute: 'hx-size' })\n size: 'sm' | 'md' | 'lg' = 'md';\n\n /**\n * Accessible label for the button group. Sets aria-label on the host element.\n * **Strongly recommended** for WCAG 2.1 AA compliance — without it, screen\n * readers announce an unnamed \"group\". For Drupal/Twig compatibility, prefer\n * applying `aria-label` directly as an HTML attribute instead.\n * @attr label\n */\n @property({ type: String })\n label: string = '';\n\n // ─── Lifecycle ───\n\n /**\n * Tracks whether the consumer set `aria-label` directly as an HTML attribute\n * BEFORE the `label` property fired. Used to avoid clobbering consumer-set\n * aria-label when `label` is empty.\n * @internal\n */\n private _consumerAriaLabel: string | null = null;\n\n /**\n * Tracks whether the no-label devWarn has already fired for this instance,\n * so disconnect/reconnect cycles do not spam the console.\n * @internal\n */\n private _emptyLabelWarnEmitted = false;\n\n override updated(changedProperties: PropertyValues<this>): void {\n super.updated(changedProperties);\n\n if (changedProperties.has('size')) {\n this.style.setProperty('--hx-button-group-size', this.size);\n }\n\n if (changedProperties.has('label')) {\n if (this.label) {\n this.setAttribute('aria-label', this.label);\n } else if (this._consumerAriaLabel !== null) {\n // Restore consumer-set aria-label rather than removing it. The\n // consumer's HTML attribute is the documented Drupal/Twig path\n // (lines 67-68 narrative) and must not be clobbered by an empty\n // `label` property.\n this.setAttribute('aria-label', this._consumerAriaLabel);\n } else {\n this.removeAttribute('aria-label');\n }\n }\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n // Capture any consumer-set aria-label BEFORE the `label` property writes\n // overwrite it. This snapshot wins back the host attribute when `label`\n // is later cleared.\n if (this._consumerAriaLabel === null && this.hasAttribute('aria-label')) {\n this._consumerAriaLabel = this.getAttribute('aria-label');\n }\n // Host-canonical role: use ElementInternals so the role survives in the\n // a11y tree even if a consumer attribute-strips the host. Mirror to the\n // host attribute as well for older AT/devtools that walk attributes.\n this._internals.role = 'group';\n if (!this.hasAttribute('role')) {\n this.setAttribute('role', 'group');\n }\n this.style.setProperty('--hx-button-group-size', this.size);\n if (this.label) {\n this.setAttribute('aria-label', this.label);\n } else if (this._consumerAriaLabel !== null) {\n // Consumer-set aria-label is fine — no warning needed.\n } else if (!this._emptyLabelWarnEmitted) {\n this._emptyLabelWarnEmitted = true;\n devWarn(\n 'hx-button-group',\n 'Missing accessible label. Provide a `label` attribute so screen readers can announce the group purpose (WCAG 4.1.2).',\n );\n }\n }\n\n // ─── Render ───\n\n override render() {\n return html`\n <div\n part=\"group\"\n class=${classMap({\n group: true,\n 'group--horizontal': this.orientation === 'horizontal',\n 'group--vertical': this.orientation === 'vertical',\n })}\n >\n <slot></slot>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'hx-button-group': HelixButtonGroup;\n }\n}\n"],"names":["helixButtonGroupStyles","css","HelixButtonGroup","HelixElement","value","changedProperties","html","classMap","forcedColorsInteractive","__decorateClass","property","customElement"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hx-button-group-4NUBpkyC.js","sources":["../../src/components/hx-button-group/hx-button-group.styles.ts","../../src/components/hx-button-group/hx-button-group.ts"],"sourcesContent":["import { css } from 'lit';\n\nexport const helixButtonGroupStyles = css`\n :host {\n display: inline-flex;\n contain: layout style;\n }\n\n .group {\n display: inline-flex;\n align-items: stretch;\n }\n\n /* ─── Orientation Variants ─── */\n\n .group--horizontal {\n flex-direction: row;\n }\n\n .group--vertical {\n flex-direction: column;\n }\n\n /* ─── No Double Borders: Horizontal ─── */\n\n .group--horizontal ::slotted(*:not(:first-child)) {\n margin-inline-start: var(\n --hx-button-group-divider-offset,\n calc(-1 * var(--hx-border-width-thin, 1px))\n );\n }\n\n /* ─── No Double Borders: Vertical ─── */\n\n .group--vertical ::slotted(*:not(:first-child)) {\n margin-top: var(--hx-button-group-divider-offset, calc(-1 * var(--hx-border-width-thin, 1px)));\n }\n\n /* ─── Border Radius: Horizontal — Single child keeps all corners ─── */\n\n .group--horizontal ::slotted(:only-child) {\n --hx-button-border-radius: var(\n --hx-button-group-border-radius,\n var(--hx-border-radius-md, 0.375rem)\n );\n }\n\n /* ─── Border Radius: Horizontal — First child keeps left corners ─── */\n\n .group--horizontal ::slotted(:first-child:not(:only-child)) {\n --hx-button-border-radius: var(\n --hx-button-group-border-radius,\n var(--hx-border-radius-md, 0.375rem)\n )\n 0 0 var(--hx-button-group-border-radius, var(--hx-border-radius-md, 0.375rem));\n }\n\n /* ─── Border Radius: Horizontal — Last child keeps right corners ─── */\n\n .group--horizontal ::slotted(:last-child:not(:only-child)) {\n --hx-button-border-radius: 0\n var(--hx-button-group-border-radius, var(--hx-border-radius-md, 0.375rem))\n var(--hx-button-group-border-radius, var(--hx-border-radius-md, 0.375rem)) 0;\n }\n\n /* ─── Border Radius: Horizontal — Middle children have no radius ─── */\n\n .group--horizontal ::slotted(:not(:first-child):not(:last-child)) {\n --hx-button-border-radius: 0;\n }\n\n /* ─── Border Radius: Vertical — Single child keeps all corners ─── */\n\n .group--vertical ::slotted(:only-child) {\n --hx-button-border-radius: var(\n --hx-button-group-border-radius,\n var(--hx-border-radius-md, 0.375rem)\n );\n }\n\n /* ─── Border Radius: Vertical — First child keeps top corners ─── */\n\n .group--vertical ::slotted(:first-child:not(:only-child)) {\n --hx-button-border-radius: var(\n --hx-button-group-border-radius,\n var(--hx-border-radius-md, 0.375rem)\n )\n var(--hx-button-group-border-radius, var(--hx-border-radius-md, 0.375rem)) 0 0;\n }\n\n /* ─── Border Radius: Vertical — Last child keeps bottom corners ─── */\n\n .group--vertical ::slotted(:last-child:not(:only-child)) {\n --hx-button-border-radius: 0 0\n var(--hx-button-group-border-radius, var(--hx-border-radius-md, 0.375rem))\n var(--hx-button-group-border-radius, var(--hx-border-radius-md, 0.375rem));\n }\n\n /* ─── Border Radius: Vertical — Middle children have no radius ─── */\n\n .group--vertical ::slotted(:not(:first-child):not(:last-child)) {\n --hx-button-border-radius: 0;\n }\n\n /* ─── Z-index: Raise focused child above siblings to show full focus ring ─── */\n\n .group ::slotted(:focus-within) {\n z-index: var(--hx-button-group-focus-z-index, 1);\n position: relative;\n }\n\n /* ─── High Contrast Mode (forced-colors) ─── */\n\n @media (forced-colors: active) {\n /*\n * In forced-colors mode, negative margins that collapse borders between grouped\n * buttons can obscure focus rings. Raise focused children so the Highlight\n * outline from hx-button's own forced-colors block is fully visible.\n */\n .group ::slotted(:focus-within) {\n z-index: var(--hx-button-group-focus-z-index-hc, 2);\n }\n }\n`;\n","import { html, type PropertyValues } from 'lit';\nimport '../../utilities/document-token-adoption.js';\nimport { customElement, property } from 'lit/decorators.js';\nimport { classMap } from 'lit/directives/class-map.js';\nimport { HelixElement } from '../../base/index.js';\nimport { helixButtonGroupStyles } from './hx-button-group.styles.js';\nimport { forcedColorsInteractive } from '../../styles/forced-colors.js';\nimport { devWarn } from '../../utils/dev-warn.js';\n\n/**\n * A container component that groups related hx-button elements into a cohesive\n * horizontal or vertical action set. Eliminates double borders between adjacent\n * buttons and squares off inner border-radius for a unified visual appearance.\n *\n * **Accessibility:** Always provide an accessible label via `aria-label` or\n * `aria-labelledby` so screen readers can announce the group purpose.\n *\n * @summary Groups hx-button elements into a horizontal or vertical action set with shared borders.\n *\n * @tag hx-button-group\n *\n * @slot - Default slot accepting hx-button children.\n *\n * @csspart group - The container div element wrapping all slotted buttons.\n *\n * @cssprop [--hx-button-group-size=md] - Size token forwarded to child buttons. Accepts 'sm', 'md', or 'lg'.\n * @cssprop [--hx-border-width-thin] - Width.\n * @cssprop [--hx-border-radius-md] - CSS custom property.\n */\n@customElement('hx-button-group')\nexport class HelixButtonGroup extends HelixElement {\n static override styles = [helixButtonGroupStyles, forcedColorsInteractive];\n\n /**\n * Layout orientation of the button group.\n * @attr orientation\n */\n @property({ type: String, reflect: true })\n get orientation(): 'horizontal' | 'vertical' {\n return this._orientation;\n }\n set orientation(value: string) {\n if (value !== 'horizontal' && value !== 'vertical') {\n devWarn('hx-button-group', `Invalid orientation \"${value}\", defaulting to \"horizontal\".`);\n value = 'horizontal';\n }\n this._orientation = value as 'horizontal' | 'vertical';\n }\n /**\n * Backing store for the orientation property, holding the validated orientation value.\n * @internal\n */\n private _orientation: 'horizontal' | 'vertical' = 'horizontal';\n\n /**\n * Size applied to the button group and cascaded to child buttons via\n * the --hx-button-group-size CSS custom property.\n * @attr hx-size\n */\n @property({ type: String, reflect: true, attribute: 'hx-size' })\n size: 'sm' | 'md' | 'lg' = 'md';\n\n /**\n * Accessible label for the button group. Sets aria-label on the host element.\n * **Strongly recommended** for WCAG 2.1 AA compliance — without it, screen\n * readers announce an unnamed \"group\". For Drupal/Twig compatibility, prefer\n * applying `aria-label` directly as an HTML attribute instead.\n * @attr label\n */\n @property({ type: String })\n label: string = '';\n\n // ─── Lifecycle ───\n\n /**\n * Tracks whether the consumer set `aria-label` directly as an HTML attribute\n * BEFORE the `label` property fired. Used to avoid clobbering consumer-set\n * aria-label when `label` is empty.\n * @internal\n */\n private _consumerAriaLabel: string | null = null;\n\n /**\n * Snapshot of the consumer-set `role` attribute taken at connect time.\n * When non-null, the consumer has explicitly set a role (e.g. `toolbar`,\n * `radiogroup`) and the host-canonical mirror MUST defer to that role\n * rather than overwriting `internals.role` with the default `\"group\"`.\n * Mirrors the `_consumerAriaLabel` snapshot pattern above.\n * @internal\n */\n private _consumerRole: string | null = null;\n\n /**\n * Tracks whether the no-label devWarn has already fired for this instance,\n * so disconnect/reconnect cycles do not spam the console.\n * @internal\n */\n private _emptyLabelWarnEmitted = false;\n\n override updated(changedProperties: PropertyValues<this>): void {\n super.updated(changedProperties);\n\n if (changedProperties.has('size')) {\n this.style.setProperty('--hx-button-group-size', this.size);\n }\n\n if (changedProperties.has('label')) {\n if (this.label) {\n this.setAttribute('aria-label', this.label);\n } else if (this._consumerAriaLabel !== null) {\n // Restore consumer-set aria-label rather than removing it. The\n // consumer's HTML attribute is the documented Drupal/Twig path\n // (lines 67-68 narrative) and must not be clobbered by an empty\n // `label` property.\n this.setAttribute('aria-label', this._consumerAriaLabel);\n } else {\n this.removeAttribute('aria-label');\n }\n }\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n // Capture any consumer-set aria-label BEFORE the `label` property writes\n // overwrite it. This snapshot wins back the host attribute when `label`\n // is later cleared.\n if (this._consumerAriaLabel === null && this.hasAttribute('aria-label')) {\n this._consumerAriaLabel = this.getAttribute('aria-label');\n }\n // CodeRabbit SHOULD-FIX (PR #1649 follow-up): snapshot the consumer's\n // explicit `role` BEFORE the host-canonical mirror overwrites it. When\n // a consumer sets `role=\"toolbar\"` (or `radiogroup`, etc.), the mirror\n // must defer to that role; otherwise two surfaces disagree (host attr\n // says toolbar, internals says group) and AT picks one inconsistently.\n if (this._consumerRole === null && this.hasAttribute('role')) {\n this._consumerRole = this.getAttribute('role');\n }\n // Host-canonical role: use ElementInternals so the role survives in the\n // a11y tree even if a consumer attribute-strips the host. Mirror to the\n // host attribute as well for older AT/devtools that walk attributes.\n if (this._consumerRole) {\n // Defer to the consumer's role on both surfaces.\n this._internals.role = this._consumerRole;\n } else {\n this._internals.role = 'group';\n this.setAttribute('role', 'group');\n }\n this.style.setProperty('--hx-button-group-size', this.size);\n if (this.label) {\n this.setAttribute('aria-label', this.label);\n } else if (this._consumerAriaLabel !== null) {\n // Consumer-set aria-label is fine — no warning needed.\n } else if (!this._emptyLabelWarnEmitted) {\n this._emptyLabelWarnEmitted = true;\n devWarn(\n 'hx-button-group',\n 'Missing accessible label. Provide a `label` attribute so screen readers can announce the group purpose (WCAG 4.1.2).',\n );\n }\n }\n\n // ─── Render ───\n\n override render() {\n return html`\n <div\n part=\"group\"\n class=${classMap({\n group: true,\n 'group--horizontal': this.orientation === 'horizontal',\n 'group--vertical': this.orientation === 'vertical',\n })}\n >\n <slot></slot>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'hx-button-group': HelixButtonGroup;\n }\n}\n"],"names":["helixButtonGroupStyles","css","HelixButtonGroup","HelixElement","value","changedProperties","html","classMap","forcedColorsInteractive","__decorateClass","property","customElement"],"mappings":"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"}
|