@aihu/primitives 0.1.4 → 0.1.5
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/README.md +7 -7
- package/dist/{button-Cha1gKlr.js → button-RgkdWi4q.js} +2 -2
- package/dist/{button-Cha1gKlr.js.map → button-RgkdWi4q.js.map} +1 -1
- package/dist/button.d.ts +1 -1
- package/dist/button.js +1 -1
- package/dist/checkbox.js +1 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +11 -7
- package/dist/collection.js.map +1 -1
- package/dist/composed-tree-OMbp4NQn.js +297 -0
- package/dist/composed-tree-OMbp4NQn.js.map +1 -0
- package/dist/{dialog-VDL-W3Vy.js → dialog-BHtBlVHN.js} +12 -14
- package/dist/dialog-BHtBlVHN.js.map +1 -0
- package/dist/dialog.d.ts +1 -1
- package/dist/dialog.js +1 -1
- package/dist/{form-control-B_BO9j7_.js → form-control-CRF3PWcK.js} +12 -7
- package/dist/form-control-CRF3PWcK.js.map +1 -0
- package/dist/form-control.d.ts +1 -1
- package/dist/form-control.js +1 -1
- package/dist/{index-BvFa1Y-Z.d.ts → index-BODOUcDC.d.ts} +4 -2
- package/dist/{index-BvFa1Y-Z.d.ts.map → index-BODOUcDC.d.ts.map} +1 -1
- package/dist/{index-D7hJLfnb.d.ts → index-CyG_gCVH.d.ts} +6 -4
- package/dist/index-CyG_gCVH.d.ts.map +1 -0
- package/dist/{index-yPv3StRL.d.ts → index-Dgli1Q03.d.ts} +1 -1
- package/dist/{index-yPv3StRL.d.ts.map → index-Dgli1Q03.d.ts.map} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/input.d.ts +1 -1
- package/dist/input.js +1 -1
- package/dist/label.d.ts.map +1 -1
- package/dist/label.js +9 -7
- package/dist/label.js.map +1 -1
- package/dist/radio-group.js +1 -1
- package/dist/switch.js +1 -1
- package/dist/{text-control-BBX7s8Oe.js → text-control-CUbTuk5L.js} +2 -2
- package/dist/{text-control-BBX7s8Oe.js.map → text-control-CUbTuk5L.js.map} +1 -1
- package/dist/{text-control-Brv5fUEX.d.ts → text-control-wKoKU_--.d.ts} +1 -1
- package/dist/{text-control-Brv5fUEX.d.ts.map → text-control-wKoKU_--.d.ts.map} +1 -1
- package/dist/textarea.d.ts +1 -1
- package/dist/textarea.js +1 -1
- package/package.json +3 -3
- package/dist/dialog-VDL-W3Vy.js.map +0 -1
- package/dist/form-control-B_BO9j7_.js.map +0 -1
- package/dist/index-D7hJLfnb.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ npm install @aihu/primitives
|
|
|
48
48
|
bun add @aihu/primitives
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
<sub><i>Auto-generated against `@aihu/primitives@0.1.
|
|
51
|
+
<sub><i>Auto-generated against `@aihu/primitives@0.1.5`.</i></sub>
|
|
52
52
|
|
|
53
53
|
<!-- END_AUTOGEN: install -->
|
|
54
54
|
|
|
@@ -59,12 +59,12 @@ bun add @aihu/primitives
|
|
|
59
59
|
|
|
60
60
|
| | |
|
|
61
61
|
|---|---|
|
|
62
|
-
| **Version** | `0.1.
|
|
62
|
+
| **Version** | `0.1.5` |
|
|
63
63
|
| **Tier** | F — UI — headless WAI-ARIA APG behavior primitives (zero CSS) |
|
|
64
64
|
| **Published files** | 3 entries |
|
|
65
65
|
| **License** | MIT |
|
|
66
66
|
|
|
67
|
-
<sub><i>Auto-generated against `@aihu/primitives@0.1.
|
|
67
|
+
<sub><i>Auto-generated against `@aihu/primitives@0.1.5`.</i></sub>
|
|
68
68
|
|
|
69
69
|
<!-- END_AUTOGEN: stats -->
|
|
70
70
|
|
|
@@ -93,7 +93,7 @@ bun add @aihu/primitives
|
|
|
93
93
|
| `./switch` | `./dist/switch.js` | `—` |
|
|
94
94
|
| `./radio-group` | `./dist/radio-group.js` | `—` |
|
|
95
95
|
|
|
96
|
-
<sub><i>Auto-generated against `@aihu/primitives@0.1.
|
|
96
|
+
<sub><i>Auto-generated against `@aihu/primitives@0.1.5`.</i></sub>
|
|
97
97
|
|
|
98
98
|
<!-- END_AUTOGEN: exports -->
|
|
99
99
|
|
|
@@ -108,7 +108,7 @@ bun add @aihu/primitives
|
|
|
108
108
|
- `@aihu/arbor` — `workspace:*`
|
|
109
109
|
- `@aihu/css-engine` — `workspace:*`
|
|
110
110
|
|
|
111
|
-
<sub><i>Auto-generated against `@aihu/primitives@0.1.
|
|
111
|
+
<sub><i>Auto-generated against `@aihu/primitives@0.1.5`.</i></sub>
|
|
112
112
|
|
|
113
113
|
<!-- END_AUTOGEN: deps -->
|
|
114
114
|
|
|
@@ -121,7 +121,7 @@ bun add @aihu/primitives
|
|
|
121
121
|
- [@aihu/ui](../ui)
|
|
122
122
|
- [Aihu framework root](../../README.md)
|
|
123
123
|
|
|
124
|
-
<sub><i>Auto-generated against `@aihu/primitives@0.1.
|
|
124
|
+
<sub><i>Auto-generated against `@aihu/primitives@0.1.5`.</i></sub>
|
|
125
125
|
|
|
126
126
|
<!-- END_AUTOGEN: see-also -->
|
|
127
127
|
|
|
@@ -132,6 +132,6 @@ bun add @aihu/primitives
|
|
|
132
132
|
|
|
133
133
|
MIT — see [LICENSE](../../LICENSE).
|
|
134
134
|
|
|
135
|
-
<sub><i>Auto-generated against `@aihu/primitives@0.1.
|
|
135
|
+
<sub><i>Auto-generated against `@aihu/primitives@0.1.5`.</i></sub>
|
|
136
136
|
|
|
137
137
|
<!-- END_AUTOGEN: license -->
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { injectValue } from "./dom-context.js";
|
|
2
|
-
import { r as formControlContext } from "./form-control-
|
|
2
|
+
import { r as formControlContext } from "./form-control-CRF3PWcK.js";
|
|
3
3
|
import { effect, signal } from "@aihu/signals";
|
|
4
4
|
//#region src/button/button.ts
|
|
5
5
|
/**
|
|
@@ -123,4 +123,4 @@ function defineButton(tag) {
|
|
|
123
123
|
//#endregion
|
|
124
124
|
export { defineButton as n, AihuButton as t };
|
|
125
125
|
|
|
126
|
-
//# sourceMappingURL=button-
|
|
126
|
+
//# sourceMappingURL=button-RgkdWi4q.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button-
|
|
1
|
+
{"version":3,"file":"button-RgkdWi4q.js","names":[],"sources":["../src/button/button.ts"],"sourcesContent":["/**\n * `AihuButton` — headless button base class (WAI-ARIA APG Button). Consumers\n * (or Plan 5 recipes) extend it; it is NOT a registered tag in this package.\n * Use `defineButton(tag)` to register a concrete element for tests/consumers.\n *\n * Behavior:\n * - If the host is NOT a native `<button>`, sets `role=\"button\"` +\n * `tabindex=\"0\"` and handles Enter / Space to fire a synthetic `click`\n * (the APG Button keyboard contract). A native `<button>` defers to native\n * semantics.\n * - `aria-pressed` reflects the `pressed` toggle state.\n * - `aria-disabled` + suppressed activation when `disabled`.\n * - Inherits `disabled` from a `FormControlContext` ancestor (Task 5) so a\n * button inside a disabled `form-control` is disabled too.\n * - `data-state` reflects `pressed` / `disabled`. Emits NO CSS.\n */\n\nimport { effect, type Read, signal } from '@aihu/signals'\nimport { injectValue } from '../dom-context.ts'\nimport { formControlContext } from '../form-control/index.ts'\n\nexport type ButtonType = 'button' | 'submit' | 'reset'\n\nexport class AihuButton extends HTMLElement {\n static readonly observedAttributes = ['disabled', 'pressed', 'type']\n\n private readonly _disabled = signal(false)\n private readonly _pressed = signal<boolean | null>(null)\n private _inheritedDisabled: Read<boolean> | null = null\n private _disposers: Array<() => void> = []\n\n /** True when this is a toggle button (the `pressed` attribute is present). */\n private _isToggle = false\n\n get disabled(): Read<boolean> {\n return this._disabled[0]\n }\n get pressed(): Read<boolean | null> {\n return this._pressed[0]\n }\n\n private get _isNativeButton(): boolean {\n return this.tagName === 'BUTTON'\n }\n\n connectedCallback(): void {\n this._isToggle = this.hasAttribute('pressed')\n this._syncFromAttrs()\n\n if (!this._isNativeButton) {\n if (!this.hasAttribute('role')) this.setAttribute('role', 'button')\n if (!this.hasAttribute('tabindex')) this.setAttribute('tabindex', '0')\n this.addEventListener('keydown', this._onKeydown)\n }\n // Suppress activation when disabled (capture so it beats consumer handlers).\n this.addEventListener('click', this._onClickCapture, true)\n\n // Inherit disabled from a FormControlContext ancestor, if any.\n try {\n this._inheritedDisabled = injectValue(this, formControlContext).disabled\n } catch {\n this._inheritedDisabled = null\n }\n\n this._disposers.push(\n effect(() => {\n const disabled = this._effectiveDisabled()\n if (disabled) this.setAttribute('aria-disabled', 'true')\n else this.removeAttribute('aria-disabled')\n\n if (this._isToggle) {\n this.setAttribute('aria-pressed', String(this._pressed[0]() === true))\n }\n this.setAttribute('data-state', this._stateString(disabled))\n }),\n )\n }\n\n disconnectedCallback(): void {\n this.removeEventListener('keydown', this._onKeydown)\n this.removeEventListener('click', this._onClickCapture, true)\n for (const d of this._disposers) d()\n this._disposers = []\n }\n\n attributeChangedCallback(name: string, _old: string | null, value: string | null): void {\n switch (name) {\n case 'disabled':\n this._disabled[1](value !== null)\n break\n case 'pressed':\n this._isToggle = value !== null ? true : this._isToggle\n this._pressed[1](value !== null)\n break\n }\n }\n\n /** Toggle a toggle-button's pressed state. */\n toggle(): void {\n if (!this._isToggle) return\n this._pressed[1](!(this._pressed[0]() === true))\n }\n\n private _effectiveDisabled(): boolean {\n if (this._disabled[0]()) return true\n return this._inheritedDisabled ? this._inheritedDisabled() : false\n }\n\n private _stateString(disabled: boolean): string {\n if (disabled) return 'disabled'\n if (this._isToggle) return this._pressed[0]() === true ? 'on' : 'off'\n return 'idle'\n }\n\n private readonly _onKeydown = (ev: KeyboardEvent): void => {\n if (this._effectiveDisabled()) return\n if (ev.key === 'Enter' || ev.key === ' ' || ev.key === 'Spacebar') {\n ev.preventDefault()\n this.click()\n }\n }\n\n private readonly _onClickCapture = (ev: Event): void => {\n if (this._effectiveDisabled()) {\n ev.preventDefault()\n ev.stopImmediatePropagation()\n return\n }\n if (this._isToggle) this.toggle()\n }\n\n private _syncFromAttrs(): void {\n this._disabled[1](this.hasAttribute('disabled'))\n if (this._isToggle) this._pressed[1](this.getAttribute('pressed') !== 'false')\n }\n}\n\n/** Register a concrete button tag backed by `AihuButton` (idempotent). */\nexport function defineButton(tag: string): typeof AihuButton {\n if (!customElements.get(tag)) {\n // A fresh subclass per tag so multiple tags can be registered.\n class TaggedButton extends AihuButton {}\n customElements.define(tag, TaggedButton)\n }\n return AihuButton\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAuBA,IAAa,aAAb,cAAgC,YAAY;CAC1C,OAAgB,qBAAqB;EAAC;EAAY;EAAW;EAAO;CAEpE,YAA6B,OAAO,MAAM;CAC1C,WAA4B,OAAuB,KAAK;CACxD,qBAAmD;CACnD,aAAwC,EAAE;;CAG1C,YAAoB;CAEpB,IAAI,WAA0B;EAC5B,OAAO,KAAK,UAAU;;CAExB,IAAI,UAAgC;EAClC,OAAO,KAAK,SAAS;;CAGvB,IAAY,kBAA2B;EACrC,OAAO,KAAK,YAAY;;CAG1B,oBAA0B;EACxB,KAAK,YAAY,KAAK,aAAa,UAAU;EAC7C,KAAK,gBAAgB;EAErB,IAAI,CAAC,KAAK,iBAAiB;GACzB,IAAI,CAAC,KAAK,aAAa,OAAO,EAAE,KAAK,aAAa,QAAQ,SAAS;GACnE,IAAI,CAAC,KAAK,aAAa,WAAW,EAAE,KAAK,aAAa,YAAY,IAAI;GACtE,KAAK,iBAAiB,WAAW,KAAK,WAAW;;EAGnD,KAAK,iBAAiB,SAAS,KAAK,iBAAiB,KAAK;EAG1D,IAAI;GACF,KAAK,qBAAqB,YAAY,MAAM,mBAAmB,CAAC;UAC1D;GACN,KAAK,qBAAqB;;EAG5B,KAAK,WAAW,KACd,aAAa;GACX,MAAM,WAAW,KAAK,oBAAoB;GAC1C,IAAI,UAAU,KAAK,aAAa,iBAAiB,OAAO;QACnD,KAAK,gBAAgB,gBAAgB;GAE1C,IAAI,KAAK,WACP,KAAK,aAAa,gBAAgB,OAAO,KAAK,SAAS,IAAI,KAAK,KAAK,CAAC;GAExE,KAAK,aAAa,cAAc,KAAK,aAAa,SAAS,CAAC;IAC5D,CACH;;CAGH,uBAA6B;EAC3B,KAAK,oBAAoB,WAAW,KAAK,WAAW;EACpD,KAAK,oBAAoB,SAAS,KAAK,iBAAiB,KAAK;EAC7D,KAAK,MAAM,KAAK,KAAK,YAAY,GAAG;EACpC,KAAK,aAAa,EAAE;;CAGtB,yBAAyB,MAAc,MAAqB,OAA4B;EACtF,QAAQ,MAAR;GACE,KAAK;IACH,KAAK,UAAU,GAAG,UAAU,KAAK;IACjC;GACF,KAAK;IACH,KAAK,YAAY,UAAU,OAAO,OAAO,KAAK;IAC9C,KAAK,SAAS,GAAG,UAAU,KAAK;IAChC;;;;CAKN,SAAe;EACb,IAAI,CAAC,KAAK,WAAW;EACrB,KAAK,SAAS,GAAG,EAAE,KAAK,SAAS,IAAI,KAAK,MAAM;;CAGlD,qBAAsC;EACpC,IAAI,KAAK,UAAU,IAAI,EAAE,OAAO;EAChC,OAAO,KAAK,qBAAqB,KAAK,oBAAoB,GAAG;;CAG/D,aAAqB,UAA2B;EAC9C,IAAI,UAAU,OAAO;EACrB,IAAI,KAAK,WAAW,OAAO,KAAK,SAAS,IAAI,KAAK,OAAO,OAAO;EAChE,OAAO;;CAGT,cAA+B,OAA4B;EACzD,IAAI,KAAK,oBAAoB,EAAE;EAC/B,IAAI,GAAG,QAAQ,WAAW,GAAG,QAAQ,OAAO,GAAG,QAAQ,YAAY;GACjE,GAAG,gBAAgB;GACnB,KAAK,OAAO;;;CAIhB,mBAAoC,OAAoB;EACtD,IAAI,KAAK,oBAAoB,EAAE;GAC7B,GAAG,gBAAgB;GACnB,GAAG,0BAA0B;GAC7B;;EAEF,IAAI,KAAK,WAAW,KAAK,QAAQ;;CAGnC,iBAA+B;EAC7B,KAAK,UAAU,GAAG,KAAK,aAAa,WAAW,CAAC;EAChD,IAAI,KAAK,WAAW,KAAK,SAAS,GAAG,KAAK,aAAa,UAAU,KAAK,QAAQ;;;;AAKlF,SAAgB,aAAa,KAAgC;CAC3D,IAAI,CAAC,eAAe,IAAI,IAAI,EAAE;EAE5B,MAAM,qBAAqB,WAAW;EACtC,eAAe,OAAO,KAAK,aAAa;;CAE1C,OAAO"}
|
package/dist/button.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as ButtonType, r as defineButton, t as AihuButton } from "./index-
|
|
1
|
+
import { n as ButtonType, r as defineButton, t as AihuButton } from "./index-Dgli1Q03.js";
|
|
2
2
|
export { AihuButton, ButtonType, defineButton };
|
package/dist/button.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as defineButton, t as AihuButton } from "./button-
|
|
1
|
+
import { n as defineButton, t as AihuButton } from "./button-RgkdWi4q.js";
|
|
2
2
|
export { AihuButton, defineButton };
|
package/dist/checkbox.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createDomContext, injectValue, provideContext } from "./dom-context.js";
|
|
2
|
-
import { i as attachHiddenInput, r as formControlContext } from "./form-control-
|
|
2
|
+
import { i as attachHiddenInput, r as formControlContext } from "./form-control-CRF3PWcK.js";
|
|
3
3
|
import { effect, signal } from "@aihu/signals";
|
|
4
4
|
//#region src/checkbox/index.ts
|
|
5
5
|
/**
|
package/dist/collection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.d.ts","names":[],"sources":["../src/collection/index.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"collection.d.ts","names":[],"sources":["../src/collection/index.ts"],"mappings":";;;;UAgBiB,sBAAA;EAEF;EAAb,QAAA,CAAS,EAAA,EAAI,OAAA;EAEJ;EAAA,SAAA,KAAA,EAAO,IAAA,CAAK,OAAA;AAAA;AAAA,cAGV,iBAAA,EAAiB,UAAA,CAAA,sBAAA;;iBAgBd,gBAAA,CAAA,GAAoB,sBAAA;AAAA,cAmBvB,cAAA,SAAuB,WAAA;EAAA,iBACjB,WAAA;EApCW;EAAA,IAuCxB,KAAA,CAAA,GAAS,IAAA,CAAK,OAAA;EAvBJ;EA4Bd,QAAA,CAAS,EAAA,EAAI,OAAA;;;;iBAYC,gBAAA,CAAiB,GAAA"}
|
package/dist/collection.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createDomContext, provideContext } from "./dom-context.js";
|
|
2
|
+
import { r as composedCompareOrder } from "./composed-tree-OMbp4NQn.js";
|
|
2
3
|
import { signal } from "@aihu/signals";
|
|
3
4
|
//#region src/collection/index.ts
|
|
4
5
|
/**
|
|
@@ -13,14 +14,17 @@ import { signal } from "@aihu/signals";
|
|
|
13
14
|
* Context: provides `collectionContext` exposing `{ register, items }`.
|
|
14
15
|
*/
|
|
15
16
|
const collectionContext = createDomContext("collection");
|
|
16
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Sort a set of elements into composed (rendered) order. Uses
|
|
19
|
+
* `composedCompareOrder` rather than raw `compareDocumentPosition`: members
|
|
20
|
+
* that live in different shadow roots (e.g. each roving-focus/radio-group item
|
|
21
|
+
* is itself shadow-DOM'd) are "disconnected" per the DOM spec from each
|
|
22
|
+
* other's perspective, so `compareDocumentPosition` alone is implementation-
|
|
23
|
+
* specific there — the composed-tree comparator walks both elements' ancestor
|
|
24
|
+
* chains (crossing shadow boundaries) to a common ancestor first.
|
|
25
|
+
*/
|
|
17
26
|
function sortDomOrder(els) {
|
|
18
|
-
return [...els].sort(
|
|
19
|
-
const rel = a.compareDocumentPosition(b);
|
|
20
|
-
if (rel & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
|
|
21
|
-
if (rel & Node.DOCUMENT_POSITION_PRECEDING) return 1;
|
|
22
|
-
return 0;
|
|
23
|
-
});
|
|
27
|
+
return [...els].sort(composedCompareOrder);
|
|
24
28
|
}
|
|
25
29
|
/** The registration mechanism, usable standalone (roving-focus reuses it). */
|
|
26
30
|
function createCollection() {
|
package/dist/collection.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.js","names":[],"sources":["../src/collection/index.ts"],"sourcesContent":["/**\n * `<aihu-collection>` — descendant-registration substrate (the Radix\n * `Collection` pattern) implemented over the DOM-walk context. Descendants call\n * `register(el)` on connect (getting back an unregister disposer for\n * disconnect); the collection keeps its `items` signal in DOM order. The\n * substrate `roving-focus`, menu-style, and future list primitives build on.\n * Structural — no ARIA, no CSS.\n *\n * Owned signals: `items` — ordered signal array of registered descendants.\n * Context: provides `collectionContext` exposing `{ register, items }`.\n */\n\nimport { type Read, signal } from '@aihu/signals'\nimport { createDomContext, provideContext } from '../dom-context.ts'\n\nexport interface CollectionContextValue {\n /** Register `el`; returns a disposer that unregisters it. */\n register(el: Element): () => void\n /** Registered descendants, kept in DOM order. */\n readonly items: Read<Element[]>\n}\n\nexport const collectionContext = createDomContext<CollectionContextValue>('collection')\n\n
|
|
1
|
+
{"version":3,"file":"collection.js","names":[],"sources":["../src/collection/index.ts"],"sourcesContent":["/**\n * `<aihu-collection>` — descendant-registration substrate (the Radix\n * `Collection` pattern) implemented over the DOM-walk context. Descendants call\n * `register(el)` on connect (getting back an unregister disposer for\n * disconnect); the collection keeps its `items` signal in DOM order. The\n * substrate `roving-focus`, menu-style, and future list primitives build on.\n * Structural — no ARIA, no CSS.\n *\n * Owned signals: `items` — ordered signal array of registered descendants.\n * Context: provides `collectionContext` exposing `{ register, items }`.\n */\n\nimport { type Read, signal } from '@aihu/signals'\nimport { composedCompareOrder } from '../composed-tree.ts'\nimport { createDomContext, provideContext } from '../dom-context.ts'\n\nexport interface CollectionContextValue {\n /** Register `el`; returns a disposer that unregisters it. */\n register(el: Element): () => void\n /** Registered descendants, kept in DOM order. */\n readonly items: Read<Element[]>\n}\n\nexport const collectionContext = createDomContext<CollectionContextValue>('collection')\n\n/**\n * Sort a set of elements into composed (rendered) order. Uses\n * `composedCompareOrder` rather than raw `compareDocumentPosition`: members\n * that live in different shadow roots (e.g. each roving-focus/radio-group item\n * is itself shadow-DOM'd) are \"disconnected\" per the DOM spec from each\n * other's perspective, so `compareDocumentPosition` alone is implementation-\n * specific there — the composed-tree comparator walks both elements' ancestor\n * chains (crossing shadow boundaries) to a common ancestor first.\n */\nfunction sortDomOrder(els: Set<Element>): Element[] {\n return [...els].sort(composedCompareOrder)\n}\n\n/** The registration mechanism, usable standalone (roving-focus reuses it). */\nexport function createCollection(): CollectionContextValue {\n const members = new Set<Element>()\n const [items, setItems] = signal<Element[]>([])\n const resync = (): void => {\n setItems(sortDomOrder(members))\n }\n return {\n items,\n register(el: Element): () => void {\n members.add(el)\n resync()\n return () => {\n members.delete(el)\n resync()\n }\n },\n }\n}\n\nexport class AihuCollection extends HTMLElement {\n private readonly _collection = createCollection()\n\n /** Registered descendants in DOM order. */\n get items(): Read<Element[]> {\n return this._collection.items\n }\n\n /** Register `el` into this collection; returns an unregister disposer. */\n register(el: Element): () => void {\n return this._collection.register(el)\n }\n\n constructor() {\n super()\n provideContext(this, collectionContext, this._collection)\n }\n}\n\nlet _defined = false\n/** Register `<aihu-collection>` (idempotent). */\nexport function defineCollection(tag = 'aihu-collection'): void {\n if (_defined || customElements.get(tag)) {\n _defined = true\n return\n }\n customElements.define(tag, AihuCollection)\n _defined = true\n}\n"],"mappings":";;;;;;;;;;;;;;;AAuBA,MAAa,oBAAoB,iBAAyC,aAAa;;;;;;;;;;AAWvF,SAAS,aAAa,KAA8B;CAClD,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,qBAAqB;;;AAI5C,SAAgB,mBAA2C;CACzD,MAAM,0BAAU,IAAI,KAAc;CAClC,MAAM,CAAC,OAAO,YAAY,OAAkB,EAAE,CAAC;CAC/C,MAAM,eAAqB;EACzB,SAAS,aAAa,QAAQ,CAAC;;CAEjC,OAAO;EACL;EACA,SAAS,IAAyB;GAChC,QAAQ,IAAI,GAAG;GACf,QAAQ;GACR,aAAa;IACX,QAAQ,OAAO,GAAG;IAClB,QAAQ;;;EAGb;;AAGH,IAAa,iBAAb,cAAoC,YAAY;CAC9C,cAA+B,kBAAkB;;CAGjD,IAAI,QAAyB;EAC3B,OAAO,KAAK,YAAY;;;CAI1B,SAAS,IAAyB;EAChC,OAAO,KAAK,YAAY,SAAS,GAAG;;CAGtC,cAAc;EACZ,OAAO;EACP,eAAe,MAAM,mBAAmB,KAAK,YAAY;;;AAI7D,IAAI,WAAW;;AAEf,SAAgB,iBAAiB,MAAM,mBAAyB;CAC9D,IAAI,YAAY,eAAe,IAAI,IAAI,EAAE;EACvC,WAAW;EACX;;CAEF,eAAe,OAAO,KAAK,eAAe;CAC1C,WAAW"}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
//#region src/composed-tree.ts
|
|
2
|
+
function isElement(node) {
|
|
3
|
+
return node.nodeType === Node.ELEMENT_NODE;
|
|
4
|
+
}
|
|
5
|
+
function childElements(node) {
|
|
6
|
+
const out = [];
|
|
7
|
+
for (const child of Array.from(node.childNodes)) if (isElement(child)) out.push(child);
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The composed-tree children of `node`: an open shadow root's tree supersedes
|
|
12
|
+
* light-DOM children entirely; a `<slot>` resolves to its assigned elements in
|
|
13
|
+
* rendered order (falling back to its own light-DOM "fallback content" when
|
|
14
|
+
* nothing is assigned); a `<template>` has none. Everything else (Element,
|
|
15
|
+
* ShadowRoot, Document, DocumentFragment) yields its ordinary element children.
|
|
16
|
+
*/
|
|
17
|
+
function composedChildren(node) {
|
|
18
|
+
if (!isElement(node)) return childElements(node);
|
|
19
|
+
if (node.tagName === "TEMPLATE") return [];
|
|
20
|
+
const shadow = node.shadowRoot;
|
|
21
|
+
if (shadow !== null) return composedChildren(shadow);
|
|
22
|
+
if (node.tagName === "SLOT") {
|
|
23
|
+
const assigned = node.assignedElements({ flatten: true });
|
|
24
|
+
return assigned.length > 0 ? assigned : childElements(node);
|
|
25
|
+
}
|
|
26
|
+
return childElements(node);
|
|
27
|
+
}
|
|
28
|
+
/** `Element`/`Text` both implement the `Slotable` mixin (`assignedSlot`). */
|
|
29
|
+
function assignedSlotOf(node) {
|
|
30
|
+
if (isElement(node)) return node.assignedSlot;
|
|
31
|
+
if (node.nodeType === Node.TEXT_NODE) return node.assignedSlot;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Composed-tree parent hop: if `node` is slotted (has a non-null
|
|
36
|
+
* `assignedSlot`), the composed parent is that `<slot>` — NOT its light-DOM
|
|
37
|
+
* parentNode — since that's where it's actually rendered. Otherwise, the
|
|
38
|
+
* ordinary DOM parent, EXCEPT when that parent is itself a `ShadowRoot` — in
|
|
39
|
+
* that case hop out to `.host` and keep going. This is the upward mirror of
|
|
40
|
+
* `composedChildren`'s slot resolution: without it, walking up from slotted
|
|
41
|
+
* content disagrees with walking down via `composedChildren`/`walkComposedTree`,
|
|
42
|
+
* so `composedContains`/`composedClosest`/`composedCompareOrder` (all built on
|
|
43
|
+
* this) silently fail across a slot boundary even though the downward walk
|
|
44
|
+
* finds the same nodes. Mirrors `dom-context.ts`'s `injectContext` walk.
|
|
45
|
+
* Returns `null` at the top.
|
|
46
|
+
*/
|
|
47
|
+
function composedParent(node) {
|
|
48
|
+
const slot = assignedSlotOf(node);
|
|
49
|
+
if (slot !== null) return slot;
|
|
50
|
+
const parent = node.parentNode;
|
|
51
|
+
if (parent === null) return null;
|
|
52
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
53
|
+
if (isElement(parent) && parent.shadowRoot !== null) return null;
|
|
54
|
+
return parent;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Depth-first, pre-order walk of the composed tree rooted at `root`, in
|
|
58
|
+
* rendered order (slotted content appears where its `<slot>` is, not where it
|
|
59
|
+
* was authored). Does not filter for focusability/visibility — see
|
|
60
|
+
* `isTabbable`/`queryTabbables` for that.
|
|
61
|
+
*/
|
|
62
|
+
function* walkComposedTree(root) {
|
|
63
|
+
for (const child of composedChildren(root)) {
|
|
64
|
+
yield child;
|
|
65
|
+
yield* walkComposedTree(child);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** First composed-tree descendant of `root` matching `selector`, or `null`. */
|
|
69
|
+
function composedQuerySelector(root, selector) {
|
|
70
|
+
for (const el of walkComposedTree(root)) if (el.matches(selector)) return el;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/** All composed-tree descendants of `root` matching `selector`, in rendered order. */
|
|
74
|
+
function composedQuerySelectorAll(root, selector) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const el of walkComposedTree(root)) if (el.matches(selector)) out.push(el);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Composed-tree `closest()`: walks UP from `el` (inclusive) via
|
|
81
|
+
* `composedParent`, crossing shadow boundaries, returning the first element
|
|
82
|
+
* matching `selector`. Unlike the native `Element.closest`, this does not stop
|
|
83
|
+
* at a shadow root — a `<form>` ancestor "outside" an intervening shadow-DOM
|
|
84
|
+
* wrapper is still found.
|
|
85
|
+
*/
|
|
86
|
+
function composedClosest(el, selector) {
|
|
87
|
+
let node = el;
|
|
88
|
+
while (node !== null) {
|
|
89
|
+
if (isElement(node) && node.matches(selector)) return node;
|
|
90
|
+
node = composedParent(node);
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Composed-tree containment: is `node` reachable from `container` by walking
|
|
96
|
+
* UP via `composedParent`? Unlike `Element.contains`, this crosses shadow
|
|
97
|
+
* boundaries (multi-hop — any number of nested shadow roots), so a
|
|
98
|
+
* doubly-nested shadow descendant is still correctly "contained".
|
|
99
|
+
*/
|
|
100
|
+
function composedContains(container, node) {
|
|
101
|
+
let cur = node;
|
|
102
|
+
while (cur !== null) {
|
|
103
|
+
if (cur === container) return true;
|
|
104
|
+
cur = composedParent(cur);
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Composed-tree `document.activeElement`: recursively drills through OPEN
|
|
110
|
+
* shadow roots' own `.activeElement` to find the truly-focused leaf, rather
|
|
111
|
+
* than stopping at the first shadow host (which is all `document.activeElement`
|
|
112
|
+
* or a single un-recursed `root.activeElement` hop gives you). Stops (by
|
|
113
|
+
* necessity) at a closed shadow root, same limitation as the rest of this module.
|
|
114
|
+
*/
|
|
115
|
+
function composedActiveElement(root = document) {
|
|
116
|
+
let active = root.activeElement;
|
|
117
|
+
while (active !== null && active.shadowRoot !== null) {
|
|
118
|
+
const inner = active.shadowRoot.activeElement;
|
|
119
|
+
if (inner === null) break;
|
|
120
|
+
active = inner;
|
|
121
|
+
}
|
|
122
|
+
return active;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Order-comparator for two elements that may live in different shadow trees
|
|
126
|
+
* (or even different, disconnected documents/fragments) — the composed-tree
|
|
127
|
+
* generalization of `a.compareDocumentPosition(b)`. Walks both elements'
|
|
128
|
+
* composed-ancestor chains to their common ancestor, then orders by that
|
|
129
|
+
* ancestor's composed-children order. Returns `0` for equal elements or for
|
|
130
|
+
* genuinely disconnected trees (no common ancestor — `compareDocumentPosition`
|
|
131
|
+
* itself is implementation-specific in that case, so a stable tie is at least
|
|
132
|
+
* as correct and doesn't throw).
|
|
133
|
+
*/
|
|
134
|
+
function composedCompareOrder(a, b) {
|
|
135
|
+
if (a === b) return 0;
|
|
136
|
+
const chainA = composedAncestorChain(a);
|
|
137
|
+
const chainB = composedAncestorChain(b);
|
|
138
|
+
let i = 0;
|
|
139
|
+
while (i < chainA.length && i < chainB.length && chainA[i] === chainB[i]) i++;
|
|
140
|
+
if (i === 0) return 0;
|
|
141
|
+
const common = chainA[i - 1];
|
|
142
|
+
const branchA = chainA[i];
|
|
143
|
+
const branchB = chainB[i];
|
|
144
|
+
if (branchA === void 0) return -1;
|
|
145
|
+
if (branchB === void 0) return 1;
|
|
146
|
+
const siblings = composedChildren(common);
|
|
147
|
+
const idxA = siblings.indexOf(branchA);
|
|
148
|
+
const idxB = siblings.indexOf(branchB);
|
|
149
|
+
if (idxA === -1 || idxB === -1) return 0;
|
|
150
|
+
return idxA - idxB;
|
|
151
|
+
}
|
|
152
|
+
/** `[root, ..., node]` composed-ancestor chain (root-first) via `composedParent`. */
|
|
153
|
+
function composedAncestorChain(node) {
|
|
154
|
+
const chain = [];
|
|
155
|
+
let cur = node;
|
|
156
|
+
while (cur !== null) {
|
|
157
|
+
chain.push(cur);
|
|
158
|
+
cur = composedParent(cur);
|
|
159
|
+
}
|
|
160
|
+
return chain.reverse();
|
|
161
|
+
}
|
|
162
|
+
const FOCUSABLE_SELECTOR = "a[href], area[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex], [contenteditable=\"\"], [contenteditable=\"true\"], [contenteditable=\"plaintext-only\"], audio[controls], video[controls], details>summary:first-of-type";
|
|
163
|
+
/** jsdom does not lay out, so `offsetParent` is always null — treat all as visible there. */
|
|
164
|
+
function isInJsdom() {
|
|
165
|
+
return typeof navigator !== "undefined" && navigator.userAgent.includes("jsdom");
|
|
166
|
+
}
|
|
167
|
+
function hasNegativeTabindex(el) {
|
|
168
|
+
const attr = el.getAttribute("tabindex");
|
|
169
|
+
if (attr === null) return false;
|
|
170
|
+
const n = Number.parseInt(attr, 10);
|
|
171
|
+
return Number.isFinite(n) && n < 0;
|
|
172
|
+
}
|
|
173
|
+
function isDisabled(el) {
|
|
174
|
+
if ("disabled" in el) return Boolean(el.disabled);
|
|
175
|
+
return el.hasAttribute("disabled");
|
|
176
|
+
}
|
|
177
|
+
/** True if `el` or any composed ancestor is `inert`. */
|
|
178
|
+
function isInertOrAncestorInert(el) {
|
|
179
|
+
let node = el;
|
|
180
|
+
while (node !== null) {
|
|
181
|
+
if (isElement(node)) {
|
|
182
|
+
if (node.inert === true || node.hasAttribute("inert")) return true;
|
|
183
|
+
}
|
|
184
|
+
node = composedParent(node);
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
function isVisible(el) {
|
|
189
|
+
if (isInJsdom()) return true;
|
|
190
|
+
return el.offsetParent !== null;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Is `el` currently in the tab order? Checks native focusable
|
|
194
|
+
* tags/attributes, non-negative `tabindex`, `disabled`, `inert` (own or any
|
|
195
|
+
* composed ancestor's), and visibility (`offsetParent`, with a jsdom-always-
|
|
196
|
+
* visible carve-out since jsdom never lays out). Elements inside `<template>`
|
|
197
|
+
* are never reachable here because `walkComposedTree`/`composedChildren` never
|
|
198
|
+
* descend into template content.
|
|
199
|
+
*/
|
|
200
|
+
function isTabbable(el, opts = {}) {
|
|
201
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
202
|
+
if (!el.matches(FOCUSABLE_SELECTOR)) return false;
|
|
203
|
+
if (hasNegativeTabindex(el)) return false;
|
|
204
|
+
if (isDisabled(el)) return false;
|
|
205
|
+
if (isInertOrAncestorInert(el)) return false;
|
|
206
|
+
if (el === opts.includeElement) return true;
|
|
207
|
+
return isVisible(el);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Positive `tabindex` value of `el`, or `null` if absent/zero/negative/
|
|
211
|
+
* non-numeric. The platform visits positive-`tabindex` elements FIRST, in
|
|
212
|
+
* ascending order — see `orderScope`.
|
|
213
|
+
*/
|
|
214
|
+
function getPositiveTabindex(el) {
|
|
215
|
+
const attr = el.getAttribute("tabindex");
|
|
216
|
+
if (attr === null) return null;
|
|
217
|
+
const n = Number.parseInt(attr, 10);
|
|
218
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Recursively builds real sequential-focus-navigation order for the
|
|
222
|
+
* focus-navigation scope rooted at `root` — the HTML spec's flattened,
|
|
223
|
+
* tabindex-ordered construction, NOT a plain composed-DFS walk reordered
|
|
224
|
+
* in place.
|
|
225
|
+
*
|
|
226
|
+
* A new scope begins at each open shadow root, exactly where
|
|
227
|
+
* `composedChildren` crosses a shadow boundary. The shadow HOST is a member
|
|
228
|
+
* of the PARENT scope (participating in that scope's tabindex ordering via
|
|
229
|
+
* its OWN `tabindex` attribute, even when the host itself isn't tabbable) —
|
|
230
|
+
* but critically, the host's entire nested scope travels WITH it: once a
|
|
231
|
+
* scope's direct members are ordered (positive `tabindex` ascending, ties in
|
|
232
|
+
* tree order, then naturals in tree order), each host's already-ordered
|
|
233
|
+
* nested contents are spliced in immediately after that host in the
|
|
234
|
+
* PARENT's ordered sequence. A nested scope never stays pinned at the
|
|
235
|
+
* document position it happened to occupy — it moves with its host, which is
|
|
236
|
+
* what makes a positive-`tabindex` shadow host's contents visited right
|
|
237
|
+
* after the host, matching the platform's real Tab sequence. (Slotted
|
|
238
|
+
* content keeps the scope it is composed INTO, i.e. the shadow tree owning
|
|
239
|
+
* the `<slot>`, not its light-DOM origin — consistent with how
|
|
240
|
+
* `composedChildren`/`walkComposedTree` already define scope boundaries
|
|
241
|
+
* elsewhere in this file.)
|
|
242
|
+
*
|
|
243
|
+
* Returns every scope member in final order, INCLUDING non-tabbable hosts
|
|
244
|
+
* and non-tabbable plain elements — callers filter with `isTabbable`
|
|
245
|
+
* afterward (see `queryTabbables`), since a host's position must be kept as
|
|
246
|
+
* the splice point for its nested scope even when the host itself is never
|
|
247
|
+
* actually focusable.
|
|
248
|
+
*/
|
|
249
|
+
function orderScope(root) {
|
|
250
|
+
const members = [];
|
|
251
|
+
let index = 0;
|
|
252
|
+
function walk(node) {
|
|
253
|
+
for (const child of composedChildren(node)) if (child.shadowRoot !== null) members.push({
|
|
254
|
+
el: child,
|
|
255
|
+
index: index++,
|
|
256
|
+
nested: orderScope(child.shadowRoot)
|
|
257
|
+
});
|
|
258
|
+
else {
|
|
259
|
+
members.push({
|
|
260
|
+
el: child,
|
|
261
|
+
index: index++,
|
|
262
|
+
nested: []
|
|
263
|
+
});
|
|
264
|
+
walk(child);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
walk(root);
|
|
268
|
+
const positive = members.filter((m) => getPositiveTabindex(m.el) !== null);
|
|
269
|
+
positive.sort((a, b) => {
|
|
270
|
+
const diff = getPositiveTabindex(a.el) - getPositiveTabindex(b.el);
|
|
271
|
+
return diff !== 0 ? diff : a.index - b.index;
|
|
272
|
+
});
|
|
273
|
+
const natural = members.filter((m) => getPositiveTabindex(m.el) === null);
|
|
274
|
+
const out = [];
|
|
275
|
+
for (const m of [...positive, ...natural]) out.push(m.el, ...m.nested);
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* All tabbable elements in `container`'s composed subtree, in real
|
|
280
|
+
* sequential-focus-navigation order: the shadow-DOM/slot-aware replacement
|
|
281
|
+
* for `container.querySelectorAll(FOCUSABLE_SELECTOR)` — but ALSO correct
|
|
282
|
+
* about ordering, not just reach. The platform does not visit tabbables in
|
|
283
|
+
* plain document order: within each focus-navigation scope (the container
|
|
284
|
+
* itself, and independently within each open shadow root), positive-
|
|
285
|
+
* `tabindex` elements are visited first, ascending, and a scope nested
|
|
286
|
+
* inside a positive-`tabindex` host travels WITH that host rather than
|
|
287
|
+
* staying pinned at its original document position — see `orderScope`.
|
|
288
|
+
*/
|
|
289
|
+
function queryTabbables(container, opts = {}) {
|
|
290
|
+
const out = [];
|
|
291
|
+
for (const el of orderScope(container)) if (isTabbable(el, opts)) out.push(el);
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
//#endregion
|
|
295
|
+
export { composedParent as a, queryTabbables as c, composedContains as i, composedClosest as n, composedQuerySelector as o, composedCompareOrder as r, composedQuerySelectorAll as s, composedActiveElement as t };
|
|
296
|
+
|
|
297
|
+
//# sourceMappingURL=composed-tree-OMbp4NQn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composed-tree-OMbp4NQn.js","names":["asHtml"],"sources":["../src/composed-tree.ts"],"sourcesContent":["/**\n * `@aihu/primitives` composed-tree substrate — the shared, dependency-free\n * DOM-walk internals every primitive needs to be shadow-DOM-correct. aihu is\n * moving to a light-DOM default with shadow DOM opt-in *per consumer*, so any\n * primitive that assumes a single tree (via `querySelector`, `closest`,\n * `Element.contains`, `document.activeElement`, or `compareDocumentPosition`)\n * silently breaks the moment a consumer opts a nested piece into shadow DOM.\n *\n * This module is the ONE place that walk lives. Every primitive that needs to\n * find, order, or contain elements across a shadow boundary should build on\n * these functions rather than re-deriving the walk locally (see `label/`'s\n * pre-existing hand-rolled ancestor loop for what NOT to do — `dom-context.ts`\n * and this module are the two reference-correct walks in the codebase).\n *\n * Composed-tree rules implemented here:\n * - An element with an OPEN shadow root is walked via its shadow tree, not\n * its light-DOM children — the light DOM only reappears where a `<slot>`\n * inside that shadow tree projects it (in RENDERED order, not source\n * order — `assignedElements({ flatten: true })`).\n * - `<template>` content is never part of the render tree and is never\n * descended into or matched.\n * - `ShadowRoot -> .host` is the up-hop at a shadow boundary (mirrors\n * `dom-context.ts`'s `injectContext` and `define-component.ts`'s\n * `_enterOwnerContext`).\n *\n * Known, unavoidable limitation: a CLOSED shadow root cannot be detected or\n * descended into via public DOM APIs — `element.shadowRoot` is `null` for\n * both \"no shadow root\" and \"closed shadow root\", so a closed-mode host is\n * walked as an ordinary element (its real internal focusables stay invisible\n * to this walk; its light-DOM children are treated as visible even though\n * whether they're actually slotted anywhere inside the closed tree is\n * unknowable). Consumers that need composed-tree correctness should use\n * `mode: 'open'` shadow roots.\n *\n * Kept internal for now (not re-exported from `index.ts`) — the tabbable\n * pieces (`isTabbable`/`queryTabbables`) are the shape a future public\n * `InteractivityChecker` would build on, but the public surface stays minimal\n * until that primitive actually ships.\n */\n\n/** Any node that behaves like a composed-tree \"container\" we can enumerate children of. */\ntype ComposedRoot = Node\n\nfunction isElement(node: Node): node is Element {\n return node.nodeType === Node.ELEMENT_NODE\n}\n\nfunction childElements(node: Node): Element[] {\n const out: Element[] = []\n for (const child of Array.from(node.childNodes)) {\n if (isElement(child)) out.push(child)\n }\n return out\n}\n\n/**\n * The composed-tree children of `node`: an open shadow root's tree supersedes\n * light-DOM children entirely; a `<slot>` resolves to its assigned elements in\n * rendered order (falling back to its own light-DOM \"fallback content\" when\n * nothing is assigned); a `<template>` has none. Everything else (Element,\n * ShadowRoot, Document, DocumentFragment) yields its ordinary element children.\n */\nexport function composedChildren(node: ComposedRoot): Element[] {\n if (!isElement(node)) return childElements(node)\n if (node.tagName === 'TEMPLATE') return []\n const shadow = node.shadowRoot\n if (shadow !== null) return composedChildren(shadow)\n if (node.tagName === 'SLOT') {\n const assigned = (node as HTMLSlotElement).assignedElements({ flatten: true })\n return assigned.length > 0 ? assigned : childElements(node)\n }\n return childElements(node)\n}\n\n/** `Element`/`Text` both implement the `Slotable` mixin (`assignedSlot`). */\nfunction assignedSlotOf(node: Node): HTMLSlotElement | null {\n if (isElement(node)) return node.assignedSlot\n if (node.nodeType === Node.TEXT_NODE) return (node as unknown as Text).assignedSlot\n return null\n}\n\n/**\n * Composed-tree parent hop: if `node` is slotted (has a non-null\n * `assignedSlot`), the composed parent is that `<slot>` — NOT its light-DOM\n * parentNode — since that's where it's actually rendered. Otherwise, the\n * ordinary DOM parent, EXCEPT when that parent is itself a `ShadowRoot` — in\n * that case hop out to `.host` and keep going. This is the upward mirror of\n * `composedChildren`'s slot resolution: without it, walking up from slotted\n * content disagrees with walking down via `composedChildren`/`walkComposedTree`,\n * so `composedContains`/`composedClosest`/`composedCompareOrder` (all built on\n * this) silently fail across a slot boundary even though the downward walk\n * finds the same nodes. Mirrors `dom-context.ts`'s `injectContext` walk.\n * Returns `null` at the top.\n */\nexport function composedParent(node: Node): Node | null {\n const slot = assignedSlotOf(node)\n if (slot !== null) return slot\n const parent = node.parentNode\n if (parent === null) return null\n if (parent instanceof ShadowRoot) return parent.host\n // `parent` is an ordinary element whose OWN shadow root supersedes its\n // light-DOM children entirely (see `composedChildren`). If `node` reached\n // here, it has no `assignedSlot` — it is UNSLOTTED light-DOM content, which\n // is invisible to the downward walk (`composedChildren(parent)` resolves to\n // the shadow root's children, never `parent`'s light children). Chosen\n // semantic: composed-tree containment/closest/order-comparison must agree\n // with the downward walk rather than silently diverge from it, so an\n // unslotted light child has NO composed-tree parent either — it is off the\n // composed tree in both directions, not \"contained via its host\" in one\n // direction only. (The alternative — treating it as still contained via its\n // host — would make `composedContains` report `true` for elements that\n // `queryTabbables`/`composedQuerySelectorAll`/etc. can never reach, which is\n // the exact inconsistency this module exists to avoid.)\n if (isElement(parent) && parent.shadowRoot !== null) return null\n return parent\n}\n\n/**\n * Depth-first, pre-order walk of the composed tree rooted at `root`, in\n * rendered order (slotted content appears where its `<slot>` is, not where it\n * was authored). Does not filter for focusability/visibility — see\n * `isTabbable`/`queryTabbables` for that.\n */\nexport function* walkComposedTree(root: ComposedRoot): Generator<Element> {\n for (const child of composedChildren(root)) {\n yield child\n yield* walkComposedTree(child)\n }\n}\n\n/** First composed-tree descendant of `root` matching `selector`, or `null`. */\nexport function composedQuerySelector<T extends Element = Element>(\n root: ComposedRoot,\n selector: string,\n): T | null {\n for (const el of walkComposedTree(root)) {\n if (el.matches(selector)) return el as T\n }\n return null\n}\n\n/** All composed-tree descendants of `root` matching `selector`, in rendered order. */\nexport function composedQuerySelectorAll<T extends Element = Element>(\n root: ComposedRoot,\n selector: string,\n): T[] {\n const out: T[] = []\n for (const el of walkComposedTree(root)) {\n if (el.matches(selector)) out.push(el as T)\n }\n return out\n}\n\n/**\n * Composed-tree `closest()`: walks UP from `el` (inclusive) via\n * `composedParent`, crossing shadow boundaries, returning the first element\n * matching `selector`. Unlike the native `Element.closest`, this does not stop\n * at a shadow root — a `<form>` ancestor \"outside\" an intervening shadow-DOM\n * wrapper is still found.\n */\nexport function composedClosest<T extends Element = Element>(\n el: Element,\n selector: string,\n): T | null {\n let node: Node | null = el\n while (node !== null) {\n if (isElement(node) && node.matches(selector)) return node as T\n node = composedParent(node)\n }\n return null\n}\n\n/**\n * Composed-tree containment: is `node` reachable from `container` by walking\n * UP via `composedParent`? Unlike `Element.contains`, this crosses shadow\n * boundaries (multi-hop — any number of nested shadow roots), so a\n * doubly-nested shadow descendant is still correctly \"contained\".\n */\nexport function composedContains(container: Node, node: Node | null): boolean {\n let cur: Node | null = node\n while (cur !== null) {\n if (cur === container) return true\n cur = composedParent(cur)\n }\n return false\n}\n\n/**\n * Composed-tree `document.activeElement`: recursively drills through OPEN\n * shadow roots' own `.activeElement` to find the truly-focused leaf, rather\n * than stopping at the first shadow host (which is all `document.activeElement`\n * or a single un-recursed `root.activeElement` hop gives you). Stops (by\n * necessity) at a closed shadow root, same limitation as the rest of this module.\n */\nexport function composedActiveElement(root: Document | ShadowRoot = document): Element | null {\n let active: Element | null = root.activeElement\n while (active !== null && active.shadowRoot !== null) {\n const inner: Element | null = active.shadowRoot.activeElement\n if (inner === null) break\n active = inner\n }\n return active\n}\n\n/**\n * Order-comparator for two elements that may live in different shadow trees\n * (or even different, disconnected documents/fragments) — the composed-tree\n * generalization of `a.compareDocumentPosition(b)`. Walks both elements'\n * composed-ancestor chains to their common ancestor, then orders by that\n * ancestor's composed-children order. Returns `0` for equal elements or for\n * genuinely disconnected trees (no common ancestor — `compareDocumentPosition`\n * itself is implementation-specific in that case, so a stable tie is at least\n * as correct and doesn't throw).\n */\nexport function composedCompareOrder(a: Element, b: Element): number {\n if (a === b) return 0\n\n const chainA = composedAncestorChain(a)\n const chainB = composedAncestorChain(b)\n\n let i = 0\n while (i < chainA.length && i < chainB.length && chainA[i] === chainB[i]) i++\n if (i === 0) return 0 // disconnected — no defined composed order\n\n const common = chainA[i - 1]\n const branchA = chainA[i]\n const branchB = chainB[i]\n // One chain ended exactly at the common ancestor: that element IS an\n // ancestor of the other, so it opens first.\n if (branchA === undefined) return -1\n if (branchB === undefined) return 1\n\n const siblings = composedChildren(common as Node)\n const idxA = siblings.indexOf(branchA as Element)\n const idxB = siblings.indexOf(branchB as Element)\n if (idxA === -1 || idxB === -1) return 0\n return idxA - idxB\n}\n\n/** `[root, ..., node]` composed-ancestor chain (root-first) via `composedParent`. */\nfunction composedAncestorChain(node: Node): Node[] {\n const chain: Node[] = []\n let cur: Node | null = node\n while (cur !== null) {\n chain.push(cur)\n cur = composedParent(cur)\n }\n return chain.reverse()\n}\n\n// ─── event hit-testing ────────────────────────────────────────────────────────\n// Retargeting-proof event layer. Everything ABOVE this line answers \"where is\n// this node in the tree?\"; this section answers \"did this EVENT come from\n// inside that node?\" — a strictly different question that the up-walk above\n// CANNOT answer. `event.target` is retargeted UP to the outermost shadow host\n// visible to the listener's root, so the container being tested sits BELOW it\n// and is never on `composedParent`'s ancestor chain: `composedContains(panel,\n// event.target)` returns `false` for a click that genuinely happened inside\n// `panel`, exactly like the native `panel.contains(event.target)` does. Only\n// `event.composedPath()` — built by the browser BEFORE retargeting — recovers\n// it. See `docs/plans/2026-07-24-composed-tree-helper.md` §2b.\n//\n// Mirrored verbatim in `@aihu/use`'s `src/shared/composed-tree.ts`, which\n// cannot import this module (CORE is signals-only, founder ruling A, enforced\n// by `scripts/dep-check.ts`). `packages/use/tests/composed-tree-parity.test.ts`\n// runs one behavioural table against both so drift is a red test.\n//\n// IMPORTANT: `composedPath()` is only populated DURING dispatch — afterwards\n// the platform returns an empty array. `isEventInside`/`composedEventTarget`\n// must therefore be called SYNCHRONOUSLY inside the listener; a deferred call\n// degrades silently to the (broken) `event.target` up-walk. Hit-test in the\n// handler and store the boolean, never the event.\n\n/**\n * The event's composed path — every node the event passes through that is\n * VISIBLE to the listener's root, innermost first, before retargeting.\n * Returns `[]` when the event has no `composedPath` (hand-built or legacy\n * events) **or when dispatch has already finished**. Callers treat `[]` as\n * \"fall back to an up-walk\", so a deferred call degrades silently.\n */\nexport function composedPathOf(event: Event): EventTarget[] {\n return typeof event.composedPath === 'function' ? event.composedPath() : []\n}\n\n/**\n * The TRUE originating node of `event` (`composedPath()[0]`), before\n * retargeting rewrote `event.target` to an outer shadow host. Falls back to\n * `event.target` when no composed path is available. (`label/index.ts` already\n * hand-rolls exactly this — see its `composedPath()[0]` read.)\n */\nexport function composedEventTarget(event: Event): EventTarget | null {\n // `path[0] ?? …`: an empty path (no composedPath, or dispatch already\n // finished) means the pre-retargeting origin is unrecoverable, so the\n // retargeted `event.target` is the best available answer.\n return composedPathOf(event)[0] ?? event.target\n}\n\n/**\n * All ancestor hosts of `node` whose shadow root is CLOSED. `ShadowRoot.host`\n * is readable regardless of mode, so we can always walk OUT of a closed tree\n * even though nothing can walk into one; a closed root is recognised by the\n * hop target's own `shadowRoot` being `null` despite us standing in its tree.\n */\nfunction* closedShadowHostsAbove(node: Node): Generator<Element> {\n let cur: Node | null = node\n while (cur !== null) {\n const parent = cur.parentNode\n if (parent instanceof ShadowRoot && parent.host.shadowRoot === null) yield parent.host\n cur = composedParent(cur)\n }\n}\n\n/**\n * Did `event` originate inside `node`'s composed subtree? The shadow-correct\n * replacement for `node.contains(event.target)`, and the hit-tester\n * dismiss-on-outside behaviours (`useContextMenu`, dialog/tooltip light\n * dismiss) are blocked on.\n *\n * Resolution order:\n * 1. `composedPath().includes(node)` — the precise answer whenever the path is\n * available and every boundary in between is open.\n * 2. If `node` itself lives inside a CLOSED shadow tree, the path is truncated\n * at that tree's host and `node` can never appear on it. We instead ask\n * whether the event's true origin (`path[0]`) IS that host, which the\n * platform reports exactly when the event came from inside the closed tree.\n * That cannot distinguish \"inside `node`\" from \"elsewhere inside the same\n * closed tree, or on the host itself\" — it is the most precise answer\n * closed mode allows, and it errs toward `true` (for a dismiss-on-outside\n * caller, the conservative direction: don't dismiss).\n * 3. No composed path at all (synthetic event): fall back to an up-walk from\n * `event.target`, which is correct whenever the event was not retargeted.\n */\nexport function isEventInside(event: Event, node: Node | null | undefined): boolean {\n if (node === null || node === undefined) return false\n const path = composedPathOf(event)\n if (path.length === 0) return composedContains(node, (event.target as Node | null) ?? null)\n if (path.includes(node)) return true\n for (const host of closedShadowHostsAbove(node)) {\n if (path[0] === host) return true\n }\n return false\n}\n\n/**\n * {@link isEventInside} over several nodes — a dismiss-on-outside \"ignore\"\n * list (a trigger button plus the overlay it opens, etc.). `null`/`undefined`\n * entries are skipped.\n */\nexport function isEventInsideAny(event: Event, nodes: Iterable<Node | null | undefined>): boolean {\n for (const node of nodes) {\n if (isEventInside(event, node)) return true\n }\n return false\n}\n\n// ─── tabbability ──────────────────────────────────────────────────────────────\n\nconst FOCUSABLE_SELECTOR =\n 'a[href], area[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), ' +\n 'select:not([disabled]), textarea:not([disabled]), [tabindex], ' +\n // `contenteditable=\"\"` (including the bare `contenteditable` attribute,\n // which HTML parses to an empty-string value) means \"true\" per spec — only\n // `[contenteditable=\"true\"]` missed that. `\"plaintext-only\"` is also an\n // editing-host state that participates in the tab order.\n '[contenteditable=\"\"], [contenteditable=\"true\"], [contenteditable=\"plaintext-only\"], ' +\n 'audio[controls], video[controls], details>summary:first-of-type'\n\n/** jsdom does not lay out, so `offsetParent` is always null — treat all as visible there. */\nfunction isInJsdom(): boolean {\n return typeof navigator !== 'undefined' && navigator.userAgent.includes('jsdom')\n}\n\nfunction hasNegativeTabindex(el: HTMLElement): boolean {\n const attr = el.getAttribute('tabindex')\n if (attr === null) return false\n const n = Number.parseInt(attr, 10)\n return Number.isFinite(n) && n < 0\n}\n\nfunction isDisabled(el: HTMLElement): boolean {\n if ('disabled' in el) return Boolean((el as unknown as { disabled: unknown }).disabled)\n return el.hasAttribute('disabled')\n}\n\n/** True if `el` or any composed ancestor is `inert`. */\nfunction isInertOrAncestorInert(el: Element): boolean {\n let node: Node | null = el\n while (node !== null) {\n if (isElement(node)) {\n const asHtml = node as HTMLElement & { inert?: boolean }\n if (asHtml.inert === true || node.hasAttribute('inert')) return true\n }\n node = composedParent(node)\n }\n return false\n}\n\nfunction isVisible(el: HTMLElement): boolean {\n if (isInJsdom()) return true\n return el.offsetParent !== null\n}\n\nexport interface TabbableOptions {\n /**\n * Treat this element as tabbable even if it fails the visibility check\n * (e.g. the current `composedActiveElement()` mid-transition/zero-layout).\n * Does not bypass the disabled/inert/negative-tabindex checks.\n */\n includeElement?: Element | null\n}\n\n/**\n * Is `el` currently in the tab order? Checks native focusable\n * tags/attributes, non-negative `tabindex`, `disabled`, `inert` (own or any\n * composed ancestor's), and visibility (`offsetParent`, with a jsdom-always-\n * visible carve-out since jsdom never lays out). Elements inside `<template>`\n * are never reachable here because `walkComposedTree`/`composedChildren` never\n * descend into template content.\n */\nexport function isTabbable(el: Element, opts: TabbableOptions = {}): boolean {\n if (!(el instanceof HTMLElement)) return false\n if (!el.matches(FOCUSABLE_SELECTOR)) return false\n if (hasNegativeTabindex(el)) return false\n if (isDisabled(el)) return false\n if (isInertOrAncestorInert(el)) return false\n if (el === opts.includeElement) return true\n return isVisible(el)\n}\n\n/**\n * Positive `tabindex` value of `el`, or `null` if absent/zero/negative/\n * non-numeric. The platform visits positive-`tabindex` elements FIRST, in\n * ascending order — see `orderScope`.\n */\nfunction getPositiveTabindex(el: Element): number | null {\n const attr = el.getAttribute('tabindex')\n if (attr === null) return null\n const n = Number.parseInt(attr, 10)\n return Number.isFinite(n) && n > 0 ? n : null\n}\n\n/** A scope member during `orderScope`'s construction, before within-scope sorting. */\ninterface ScopeMember {\n /** The member itself — a plain scope participant, or the HOST anchor of a nested scope. */\n el: Element\n /** Original tree-order index within this scope, for stable tie-breaking. */\n index: number\n /**\n * Already-ordered contents of the nested scope this member hosts (its own\n * open shadow root), or `[]` if `el` isn't a shadow host. This is what\n * travels WITH `el` once `el`'s position in the scope is decided — see\n * `orderScope`.\n */\n nested: Element[]\n}\n\n/**\n * Recursively builds real sequential-focus-navigation order for the\n * focus-navigation scope rooted at `root` — the HTML spec's flattened,\n * tabindex-ordered construction, NOT a plain composed-DFS walk reordered\n * in place.\n *\n * A new scope begins at each open shadow root, exactly where\n * `composedChildren` crosses a shadow boundary. The shadow HOST is a member\n * of the PARENT scope (participating in that scope's tabindex ordering via\n * its OWN `tabindex` attribute, even when the host itself isn't tabbable) —\n * but critically, the host's entire nested scope travels WITH it: once a\n * scope's direct members are ordered (positive `tabindex` ascending, ties in\n * tree order, then naturals in tree order), each host's already-ordered\n * nested contents are spliced in immediately after that host in the\n * PARENT's ordered sequence. A nested scope never stays pinned at the\n * document position it happened to occupy — it moves with its host, which is\n * what makes a positive-`tabindex` shadow host's contents visited right\n * after the host, matching the platform's real Tab sequence. (Slotted\n * content keeps the scope it is composed INTO, i.e. the shadow tree owning\n * the `<slot>`, not its light-DOM origin — consistent with how\n * `composedChildren`/`walkComposedTree` already define scope boundaries\n * elsewhere in this file.)\n *\n * Returns every scope member in final order, INCLUDING non-tabbable hosts\n * and non-tabbable plain elements — callers filter with `isTabbable`\n * afterward (see `queryTabbables`), since a host's position must be kept as\n * the splice point for its nested scope even when the host itself is never\n * actually focusable.\n */\nfunction orderScope(root: ComposedRoot): Element[] {\n const members: ScopeMember[] = []\n let index = 0\n\n function walk(node: ComposedRoot): void {\n for (const child of composedChildren(node)) {\n if (child.shadowRoot !== null) {\n // `child` hosts a new nested scope: it is itself a member of THIS\n // scope (ordered below by its own tabindex), and its shadow content\n // is a fully-ordered nested scope that gets spliced in right after\n // it once this scope's own ordering is decided.\n members.push({ el: child, index: index++, nested: orderScope(child.shadowRoot) })\n } else {\n members.push({ el: child, index: index++, nested: [] })\n walk(child) // descend further within the SAME scope\n }\n }\n }\n walk(root)\n\n const positive = members.filter((m) => getPositiveTabindex(m.el) !== null)\n positive.sort((a, b) => {\n const diff = getPositiveTabindex(a.el)! - getPositiveTabindex(b.el)!\n return diff !== 0 ? diff : a.index - b.index\n })\n const natural = members.filter((m) => getPositiveTabindex(m.el) === null)\n\n const out: Element[] = []\n for (const m of [...positive, ...natural]) {\n out.push(m.el, ...m.nested)\n }\n return out\n}\n\n/**\n * All tabbable elements in `container`'s composed subtree, in real\n * sequential-focus-navigation order: the shadow-DOM/slot-aware replacement\n * for `container.querySelectorAll(FOCUSABLE_SELECTOR)` — but ALSO correct\n * about ordering, not just reach. The platform does not visit tabbables in\n * plain document order: within each focus-navigation scope (the container\n * itself, and independently within each open shadow root), positive-\n * `tabindex` elements are visited first, ascending, and a scope nested\n * inside a positive-`tabindex` host travels WITH that host rather than\n * staying pinned at its original document position — see `orderScope`.\n */\nexport function queryTabbables(container: ComposedRoot, opts: TabbableOptions = {}): HTMLElement[] {\n const out: HTMLElement[] = []\n for (const el of orderScope(container)) {\n if (isTabbable(el, opts)) out.push(el as HTMLElement)\n }\n return out\n}\n"],"mappings":"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"}
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { createDomContext, injectValue, provideContext } from "./dom-context.js";
|
|
2
|
+
import { c as queryTabbables, i as composedContains, t as composedActiveElement } from "./composed-tree-OMbp4NQn.js";
|
|
2
3
|
import { effect, signal } from "@aihu/signals";
|
|
3
4
|
//#region src/dialog/focus-trap.ts
|
|
4
5
|
/**
|
|
5
6
|
* `createFocusTrap` — a tiny native-DOM focus trap (no library). Queries the
|
|
6
|
-
* focusable descendants of a container
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* focusable descendants of a container via the composed-tree substrate (so it
|
|
8
|
+
* correctly descends into any nested custom element's OPEN shadow root), wraps
|
|
9
|
+
* Tab at the edges, moves focus to the first focusable (or the container) on
|
|
10
|
+
* activate, and restores focus to the previously-active element on deactivate.
|
|
11
|
+
* Used by `dialog-content`.
|
|
9
12
|
*/
|
|
10
|
-
const FOCUSABLE = "a[href], area[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"]), [contenteditable=\"true\"], audio[controls], video[controls], details>summary:first-of-type";
|
|
11
13
|
function focusables(container) {
|
|
12
|
-
return
|
|
13
|
-
}
|
|
14
|
-
/** jsdom does not lay out, so `offsetParent` is always null — treat all as visible there. */
|
|
15
|
-
function isInJsdom(_el) {
|
|
16
|
-
return typeof navigator !== "undefined" && navigator.userAgent.includes("jsdom");
|
|
14
|
+
return queryTabbables(container, { includeElement: composedActiveElement(container.getRootNode()) });
|
|
17
15
|
}
|
|
18
16
|
function createFocusTrap(container) {
|
|
19
17
|
let previouslyFocused = null;
|
|
@@ -27,11 +25,11 @@ function createFocusTrap(container) {
|
|
|
27
25
|
ev.preventDefault();
|
|
28
26
|
return;
|
|
29
27
|
}
|
|
30
|
-
const current = container.getRootNode()
|
|
31
|
-
if (ev.shiftKey && (current === first || !container
|
|
28
|
+
const current = composedActiveElement(container.getRootNode());
|
|
29
|
+
if (ev.shiftKey && (current === first || !composedContains(container, current))) {
|
|
32
30
|
ev.preventDefault();
|
|
33
31
|
last.focus();
|
|
34
|
-
} else if (!ev.shiftKey && (current === last || !container
|
|
32
|
+
} else if (!ev.shiftKey && (current === last || !composedContains(container, current))) {
|
|
35
33
|
ev.preventDefault();
|
|
36
34
|
first.focus();
|
|
37
35
|
}
|
|
@@ -40,7 +38,7 @@ function createFocusTrap(container) {
|
|
|
40
38
|
activate() {
|
|
41
39
|
if (active) return;
|
|
42
40
|
active = true;
|
|
43
|
-
previouslyFocused = document
|
|
41
|
+
previouslyFocused = composedActiveElement(document);
|
|
44
42
|
const items = focusables(container);
|
|
45
43
|
const target = items[0] ?? container;
|
|
46
44
|
if (!items.length && !container.hasAttribute("tabindex")) container.setAttribute("tabindex", "-1");
|
|
@@ -260,4 +258,4 @@ function defineDialog(prefix = "aihu") {
|
|
|
260
258
|
//#endregion
|
|
261
259
|
export { AihuDialogRoot as a, defineDialog as c, AihuDialogDescription as i, dialogContext as l, AihuDialogClose as n, AihuDialogTitle as o, AihuDialogContent as r, AihuDialogTrigger as s, AihuDialogBackdrop as t, createFocusTrap as u };
|
|
262
260
|
|
|
263
|
-
//# sourceMappingURL=dialog-
|
|
261
|
+
//# sourceMappingURL=dialog-BHtBlVHN.js.map
|