@aihu/primitives 0.0.12 → 0.1.0

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.
Files changed (64) hide show
  1. package/README.md +14 -7
  2. package/dist/{button-C-8c-A17.js → button-Cha1gKlr.js} +2 -2
  3. package/dist/{button-C-8c-A17.js.map → button-Cha1gKlr.js.map} +1 -1
  4. package/dist/button.d.ts +1 -1
  5. package/dist/button.js +1 -1
  6. package/dist/checkbox.d.ts +45 -0
  7. package/dist/checkbox.d.ts.map +1 -0
  8. package/dist/checkbox.js +190 -0
  9. package/dist/checkbox.js.map +1 -0
  10. package/dist/{dialog-y7MHc6vf.js → dialog-VDL-W3Vy.js} +17 -7
  11. package/dist/{dialog-y7MHc6vf.js.map → dialog-VDL-W3Vy.js.map} +1 -1
  12. package/dist/dialog.d.ts +1 -1
  13. package/dist/dialog.js +1 -1
  14. package/dist/form-control-B_BO9j7_.js +223 -0
  15. package/dist/form-control-B_BO9j7_.js.map +1 -0
  16. package/dist/form-control.d.ts +2 -41
  17. package/dist/form-control.js +2 -145
  18. package/dist/index-BvFa1Y-Z.d.ts +60 -0
  19. package/dist/index-BvFa1Y-Z.d.ts.map +1 -0
  20. package/dist/{index-D9kf9rVU.d.ts → index-D7hJLfnb.d.ts} +10 -3
  21. package/dist/{index-D9kf9rVU.d.ts.map → index-D7hJLfnb.d.ts.map} +1 -1
  22. package/dist/{index-DPD4L6Nj.d.ts → index-yPv3StRL.d.ts} +1 -1
  23. package/dist/{index-DPD4L6Nj.d.ts.map → index-yPv3StRL.d.ts.map} +1 -1
  24. package/dist/index.d.ts +12 -4
  25. package/dist/index.js +12 -4
  26. package/dist/input.d.ts +13 -0
  27. package/dist/input.d.ts.map +1 -0
  28. package/dist/input.js +42 -0
  29. package/dist/input.js.map +1 -0
  30. package/dist/label.d.ts +24 -0
  31. package/dist/label.d.ts.map +1 -0
  32. package/dist/label.js +136 -0
  33. package/dist/label.js.map +1 -0
  34. package/dist/radio-group.d.ts +82 -0
  35. package/dist/radio-group.d.ts.map +1 -0
  36. package/dist/radio-group.js +298 -0
  37. package/dist/radio-group.js.map +1 -0
  38. package/dist/roving-focus.d.ts +4 -2
  39. package/dist/roving-focus.d.ts.map +1 -1
  40. package/dist/roving-focus.js +5 -3
  41. package/dist/roving-focus.js.map +1 -1
  42. package/dist/separator.d.ts +25 -0
  43. package/dist/separator.d.ts.map +1 -0
  44. package/dist/separator.js +85 -0
  45. package/dist/separator.js.map +1 -0
  46. package/dist/switch.d.ts +44 -0
  47. package/dist/switch.d.ts.map +1 -0
  48. package/dist/switch.js +176 -0
  49. package/dist/switch.js.map +1 -0
  50. package/dist/text-control-BBX7s8Oe.js +181 -0
  51. package/dist/text-control-BBX7s8Oe.js.map +1 -0
  52. package/dist/text-control-Brv5fUEX.d.ts +38 -0
  53. package/dist/text-control-Brv5fUEX.d.ts.map +1 -0
  54. package/dist/textarea.d.ts +13 -0
  55. package/dist/textarea.d.ts.map +1 -0
  56. package/dist/textarea.js +38 -0
  57. package/dist/textarea.js.map +1 -0
  58. package/dist/tooltip.d.ts +8 -2
  59. package/dist/tooltip.d.ts.map +1 -1
  60. package/dist/tooltip.js +15 -6
  61. package/dist/tooltip.js.map +1 -1
  62. package/package.json +33 -2
  63. package/dist/form-control.d.ts.map +0 -1
  64. package/dist/form-control.js.map +0 -1
package/dist/input.js ADDED
@@ -0,0 +1,42 @@
1
+ import { n as TEXT_CONTROL_OBSERVED, t as AihuTextControlBase } from "./text-control-BBX7s8Oe.js";
2
+ //#region src/input/index.ts
3
+ /**
4
+ * `<aihu-input>` — headless single-line text control. Wraps (or creates) a
5
+ * real light-DOM `<input>` and delegates editing/focus/form participation to
6
+ * it (the native-handoff principle — see `text-control.ts`). Ships zero CSS:
7
+ * the host reflects `data-state="disabled" | "readonly" | "idle"` and emits
8
+ * `value-change` CustomEvents for the consumer.
9
+ */
10
+ const INPUT_FORWARDED = [
11
+ "type",
12
+ "name",
13
+ "placeholder",
14
+ "autocomplete",
15
+ "inputmode",
16
+ "pattern",
17
+ "min",
18
+ "max",
19
+ "step",
20
+ "minlength",
21
+ "maxlength",
22
+ "readonly"
23
+ ];
24
+ var AihuInput = class extends AihuTextControlBase {
25
+ static observedAttributes = [...TEXT_CONTROL_OBSERVED, ...INPUT_FORWARDED];
26
+ static FORWARDED = INPUT_FORWARDED;
27
+ nativeTag = "input";
28
+ };
29
+ let _defined = false;
30
+ /** Register `<aihu-input>` (idempotent). */
31
+ function defineInput(tag = "aihu-input") {
32
+ if (_defined || customElements.get(tag)) {
33
+ _defined = true;
34
+ return;
35
+ }
36
+ customElements.define(tag, AihuInput);
37
+ _defined = true;
38
+ }
39
+ //#endregion
40
+ export { AihuInput, AihuTextControlBase, defineInput };
41
+
42
+ //# sourceMappingURL=input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.js","names":[],"sources":["../src/input/index.ts"],"sourcesContent":["/**\n * `<aihu-input>` — headless single-line text control. Wraps (or creates) a\n * real light-DOM `<input>` and delegates editing/focus/form participation to\n * it (the native-handoff principle — see `text-control.ts`). Ships zero CSS:\n * the host reflects `data-state=\"disabled\" | \"readonly\" | \"idle\"` and emits\n * `value-change` CustomEvents for the consumer.\n */\n\nimport { AihuTextControlBase, TEXT_CONTROL_OBSERVED } from './text-control.ts'\n\nconst INPUT_FORWARDED: readonly string[] = [\n 'type',\n 'name',\n 'placeholder',\n 'autocomplete',\n 'inputmode',\n 'pattern',\n 'min',\n 'max',\n 'step',\n 'minlength',\n 'maxlength',\n 'readonly',\n]\n\nexport class AihuInput extends AihuTextControlBase {\n static readonly observedAttributes = [...TEXT_CONTROL_OBSERVED, ...INPUT_FORWARDED]\n protected static override readonly FORWARDED = INPUT_FORWARDED\n\n protected override readonly nativeTag = 'input' as const\n}\n\nlet _defined = false\n/** Register `<aihu-input>` (idempotent). */\nexport function defineInput(tag = 'aihu-input'): void {\n if (_defined || customElements.get(tag)) {\n _defined = true\n return\n }\n customElements.define(tag, AihuInput)\n _defined = true\n}\n\nexport { AihuTextControlBase } from './text-control.ts'\n"],"mappings":";;;;;;;;;AAUA,MAAM,kBAAqC;CACzC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,IAAa,YAAb,cAA+B,oBAAoB;CACjD,OAAgB,qBAAqB,CAAC,GAAG,uBAAuB,GAAG,gBAAgB;CACnF,OAAmC,YAAY;CAE/C,YAAwC;;AAG1C,IAAI,WAAW;;AAEf,SAAgB,YAAY,MAAM,cAAoB;CACpD,IAAI,YAAY,eAAe,IAAI,IAAI,EAAE;EACvC,WAAW;EACX;;CAEF,eAAe,OAAO,KAAK,UAAU;CACrC,WAAW"}
@@ -0,0 +1,24 @@
1
+ import { Read } from "@aihu/signals";
2
+
3
+ //#region src/label/index.d.ts
4
+ declare class AihuLabel extends HTMLElement {
5
+ static readonly observedAttributes: string[];
6
+ private readonly _for;
7
+ private _fc;
8
+ private _disposers;
9
+ /** The explicit target id (`for` attribute) as a signal; null when unset. */
10
+ get forId(): Read<string | null>;
11
+ private get _isNativeLabel();
12
+ connectedCallback(): void;
13
+ disconnectedCallback(): void;
14
+ attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
15
+ /** Resolve the labelled target NOW (per interaction — never cached). */
16
+ private _resolveTarget;
17
+ private readonly _onMousedown;
18
+ private readonly _onClick;
19
+ }
20
+ /** Register `<aihu-label>` (idempotent). */
21
+ declare function defineLabel(tag?: string): void;
22
+ //#endregion
23
+ export { AihuLabel, defineLabel };
24
+ //# sourceMappingURL=label.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label.d.ts","names":[],"sources":["../src/label/index.ts"],"mappings":";;;cA2Ca,SAAA,SAAkB,WAAA;EAAA,gBACb,kBAAA;EAAA,iBAEC,IAAA;EAAA,QACT,GAAA;EAAA,QACA,UAAA;;MAGJ,KAAA,CAAA,GAAS,IAAA;EAAA,YAID,cAAA,CAAA;EAIZ,iBAAA,CAAA;EA4CA,oBAAA,CAAA;EAOA,wBAAA,CAAyB,IAAA,UAAc,IAAA,iBAAqB,KAAA;;UAKpD,cAAA;EAAA,iBAWS,YAAA;EAAA,iBAKA,QAAA;AAAA;;iBAwCH,WAAA,CAAY,GAAA"}
package/dist/label.js ADDED
@@ -0,0 +1,136 @@
1
+ import { injectValue } from "./dom-context.js";
2
+ import { r as formControlContext, t as AihuFormControl } from "./form-control-B_BO9j7_.js";
3
+ import { effect, signal } from "@aihu/signals";
4
+ //#region src/label/index.ts
5
+ /**
6
+ * `<aihu-label>` — headless label (the Radix Label parity primitive). Wires
7
+ * the label↔control association and forwards interactions the way a native
8
+ * `<label>` does, for targets that native labels cannot reference (custom
9
+ * hosts, role=checkbox/switch/radio elements). Ships NO CSS.
10
+ *
11
+ * Behavior:
12
+ * - On connect: ensures a stable `id`, stamps `data-fc-label` on itself so an
13
+ * ancestor `<aihu-form-control>` discovers it, and (when inside one) asks
14
+ * that ancestor to re-wire so the control gains
15
+ * `aria-labelledby="<labelId>"`.
16
+ * - Target resolution (re-resolved per interaction, never cached): the `for`
17
+ * attribute is looked up via `getElementById` in the label's root; with no
18
+ * `for`, a `formControlContext` ancestor's `controlId` is used.
19
+ * - Standalone (no form-control ancestor): sets `aria-labelledby` = own id on
20
+ * the resolved target reactively. Skipped when the target lives in a
21
+ * different root — ARIA IDREFs cannot cross shadow boundaries (see
22
+ * accessibility.md; use `aria-label` on the target for cross-root cases).
23
+ * - Click forwarding (non-native hosts only): double-click `mousedown` is
24
+ * prevented (no accidental text selection); `click` focuses native text
25
+ * controls, forwards `click()` to native checkbox/radio or custom
26
+ * checkbox/switch/radio hosts, and does nothing for disabled targets or
27
+ * clicks originating on nested interactive children.
28
+ * - If the host IS a native `<label>` (tagName `LABEL`), click forwarding is
29
+ * native — only the context wiring above applies.
30
+ *
31
+ * Reflected attributes: `for` (optional explicit target id).
32
+ */
33
+ let _idCounter = 0;
34
+ function nextId() {
35
+ _idCounter += 1;
36
+ return `aihu-label-${_idCounter}`;
37
+ }
38
+ var AihuLabel = class extends HTMLElement {
39
+ static observedAttributes = ["for"];
40
+ _for = signal(null);
41
+ _fc = null;
42
+ _disposers = [];
43
+ /** The explicit target id (`for` attribute) as a signal; null when unset. */
44
+ get forId() {
45
+ return this._for[0];
46
+ }
47
+ get _isNativeLabel() {
48
+ return this.tagName === "LABEL";
49
+ }
50
+ connectedCallback() {
51
+ this._for[1](this.getAttribute("for"));
52
+ if (!this.id) this.id = nextId();
53
+ this.setAttribute("data-fc-label", "");
54
+ try {
55
+ this._fc = injectValue(this, formControlContext);
56
+ } catch {
57
+ this._fc = null;
58
+ }
59
+ if (this._fc) {
60
+ let node = this.parentElement;
61
+ while (node) {
62
+ if (node instanceof AihuFormControl) {
63
+ node.recomputeDescribedBy();
64
+ break;
65
+ }
66
+ node = node.parentElement;
67
+ }
68
+ } else if (!this._isNativeLabel) this._disposers.push(effect(() => {
69
+ this._for[0]();
70
+ const target = this._resolveTarget();
71
+ if (target && target.getRootNode() === this.getRootNode()) target.setAttribute("aria-labelledby", this.id);
72
+ }));
73
+ if (!this._isNativeLabel) {
74
+ this.addEventListener("mousedown", this._onMousedown);
75
+ this.addEventListener("click", this._onClick);
76
+ }
77
+ }
78
+ disconnectedCallback() {
79
+ this.removeEventListener("mousedown", this._onMousedown);
80
+ this.removeEventListener("click", this._onClick);
81
+ for (const d of this._disposers) d();
82
+ this._disposers = [];
83
+ }
84
+ attributeChangedCallback(name, _old, value) {
85
+ if (name === "for") this._for[1](value);
86
+ }
87
+ /** Resolve the labelled target NOW (per interaction — never cached). */
88
+ _resolveTarget() {
89
+ const root = this.getRootNode();
90
+ const forId = this._for[0]();
91
+ if (forId) return root.getElementById(forId) ?? null;
92
+ if (this._fc) {
93
+ const id = this._fc.controlId();
94
+ if (id) return root.getElementById(id) ?? null;
95
+ }
96
+ return null;
97
+ }
98
+ _onMousedown = (ev) => {
99
+ if (ev.detail > 1) ev.preventDefault();
100
+ };
101
+ _onClick = (ev) => {
102
+ const target = this._resolveTarget();
103
+ if (!target) return;
104
+ const origin = ev.target instanceof Element ? ev.target : null;
105
+ const interactive = origin?.closest("button,input,select,textarea,a") ?? null;
106
+ if (interactive !== null && interactive !== target) return;
107
+ if (origin !== null && (origin === target || target.contains(origin))) return;
108
+ if (target.getAttribute("aria-disabled") === "true" || target.disabled === true) return;
109
+ const tag = target.tagName;
110
+ const inputType = tag === "INPUT" ? target.type : null;
111
+ if (inputType === "checkbox" || inputType === "radio") {
112
+ target.click();
113
+ return;
114
+ }
115
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") {
116
+ target.focus();
117
+ return;
118
+ }
119
+ const role = target.getAttribute("role");
120
+ if (role === "checkbox" || role === "switch" || role === "radio") target.click();
121
+ };
122
+ };
123
+ let _defined = false;
124
+ /** Register `<aihu-label>` (idempotent). */
125
+ function defineLabel(tag = "aihu-label") {
126
+ if (_defined || customElements.get(tag)) {
127
+ _defined = true;
128
+ return;
129
+ }
130
+ customElements.define(tag, AihuLabel);
131
+ _defined = true;
132
+ }
133
+ //#endregion
134
+ export { AihuLabel, defineLabel };
135
+
136
+ //# sourceMappingURL=label.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label.js","names":[],"sources":["../src/label/index.ts"],"sourcesContent":["/**\n * `<aihu-label>` — headless label (the Radix Label parity primitive). Wires\n * the label↔control association and forwards interactions the way a native\n * `<label>` does, for targets that native labels cannot reference (custom\n * hosts, role=checkbox/switch/radio elements). Ships NO CSS.\n *\n * Behavior:\n * - On connect: ensures a stable `id`, stamps `data-fc-label` on itself so an\n * ancestor `<aihu-form-control>` discovers it, and (when inside one) asks\n * that ancestor to re-wire so the control gains\n * `aria-labelledby=\"<labelId>\"`.\n * - Target resolution (re-resolved per interaction, never cached): the `for`\n * attribute is looked up via `getElementById` in the label's root; with no\n * `for`, a `formControlContext` ancestor's `controlId` is used.\n * - Standalone (no form-control ancestor): sets `aria-labelledby` = own id on\n * the resolved target reactively. Skipped when the target lives in a\n * different root — ARIA IDREFs cannot cross shadow boundaries (see\n * accessibility.md; use `aria-label` on the target for cross-root cases).\n * - Click forwarding (non-native hosts only): double-click `mousedown` is\n * prevented (no accidental text selection); `click` focuses native text\n * controls, forwards `click()` to native checkbox/radio or custom\n * checkbox/switch/radio hosts, and does nothing for disabled targets or\n * clicks originating on nested interactive children.\n * - If the host IS a native `<label>` (tagName `LABEL`), click forwarding is\n * native — only the context wiring above applies.\n *\n * Reflected attributes: `for` (optional explicit target id).\n */\n\nimport { effect, type Read, signal } from '@aihu/signals'\nimport { injectValue } from '../dom-context.ts'\nimport {\n AihuFormControl,\n type FormControlContextValue,\n formControlContext,\n} from '../form-control/index.ts'\n\nlet _idCounter = 0\nfunction nextId(): string {\n _idCounter += 1\n return `aihu-label-${_idCounter}`\n}\n\nexport class AihuLabel extends HTMLElement {\n static readonly observedAttributes = ['for']\n\n private readonly _for = signal<string | null>(null)\n private _fc: FormControlContextValue | null = null\n private _disposers: Array<() => void> = []\n\n /** The explicit target id (`for` attribute) as a signal; null when unset. */\n get forId(): Read<string | null> {\n return this._for[0]\n }\n\n private get _isNativeLabel(): boolean {\n return this.tagName === 'LABEL'\n }\n\n connectedCallback(): void {\n this._for[1](this.getAttribute('for'))\n if (!this.id) this.id = nextId()\n // Make this label discoverable by an ancestor <aihu-form-control>.\n this.setAttribute('data-fc-label', '')\n\n try {\n this._fc = injectValue(this, formControlContext)\n } catch {\n this._fc = null\n }\n\n if (this._fc) {\n // The form-control connected (and wired) before this label existed in\n // its eyes — ask it to re-derive associations so the control gains\n // aria-labelledby pointing at this label.\n let node: Element | null = this.parentElement\n while (node) {\n if (node instanceof AihuFormControl) {\n node.recomputeDescribedBy()\n break\n }\n node = node.parentElement\n }\n } else if (!this._isNativeLabel) {\n // Standalone: stamp aria-labelledby on the resolved target reactively.\n // Skip cross-root targets — IDREFs cannot cross shadow boundaries.\n this._disposers.push(\n effect(() => {\n this._for[0]() // track `for` changes\n const target = this._resolveTarget()\n if (target && target.getRootNode() === this.getRootNode()) {\n target.setAttribute('aria-labelledby', this.id)\n }\n }),\n )\n }\n\n if (!this._isNativeLabel) {\n this.addEventListener('mousedown', this._onMousedown)\n this.addEventListener('click', this._onClick)\n }\n }\n\n disconnectedCallback(): void {\n this.removeEventListener('mousedown', this._onMousedown)\n this.removeEventListener('click', this._onClick)\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 if (name === 'for') this._for[1](value)\n }\n\n /** Resolve the labelled target NOW (per interaction — never cached). */\n private _resolveTarget(): HTMLElement | null {\n const root = this.getRootNode() as Document | ShadowRoot\n const forId = this._for[0]()\n if (forId) return (root.getElementById(forId) as HTMLElement | null) ?? null\n if (this._fc) {\n const id = this._fc.controlId()\n if (id) return (root.getElementById(id) as HTMLElement | null) ?? null\n }\n return null\n }\n\n private readonly _onMousedown = (ev: MouseEvent): void => {\n // Prevent text selection on double-click (native label parity).\n if (ev.detail > 1) ev.preventDefault()\n }\n\n private readonly _onClick = (ev: MouseEvent): void => {\n const target = this._resolveTarget()\n if (!target) return\n\n const origin = ev.target instanceof Element ? ev.target : null\n // Clicks originating on a nested interactive child (that is not the\n // labelled target) are the child's business — don't forward.\n const interactive = origin?.closest('button,input,select,textarea,a') ?? null\n if (interactive !== null && interactive !== target) return\n // Clicks already on/inside the target need no forwarding (and forwarding\n // a click back to the target would recurse).\n if (origin !== null && (origin === target || target.contains(origin))) return\n\n // Disabled targets don't get forwarded interactions.\n if (\n target.getAttribute('aria-disabled') === 'true' ||\n (target as HTMLInputElement).disabled === true\n ) {\n return\n }\n\n const tag = target.tagName\n const inputType = tag === 'INPUT' ? (target as HTMLInputElement).type : null\n if (inputType === 'checkbox' || inputType === 'radio') {\n target.click()\n return\n }\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {\n target.focus()\n return\n }\n const role = target.getAttribute('role')\n if (role === 'checkbox' || role === 'switch' || role === 'radio') {\n target.click()\n }\n }\n}\n\nlet _defined = false\n/** Register `<aihu-label>` (idempotent). */\nexport function defineLabel(tag = 'aihu-label'): void {\n if (_defined || customElements.get(tag)) {\n _defined = true\n return\n }\n customElements.define(tag, AihuLabel)\n _defined = true\n}\n"],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ import { DomContext } from "./dom-context.js";
2
+ import { AihuRovingFocus } from "./roving-focus.js";
3
+ import { Read } from "@aihu/signals";
4
+
5
+ //#region src/radio-group/index.d.ts
6
+ interface RadioGroupContextValue {
7
+ /** Currently selected item value; null when nothing is selected. */
8
+ readonly value: Read<string | null>;
9
+ /** Group-effective disabled (own attribute ∥ inherited form-control). */
10
+ readonly disabled: Read<boolean>;
11
+ readonly required: Read<boolean>;
12
+ /** Programmatic write: signal + reflected `value` attribute. NO event. */
13
+ setValue(next: string | null): void;
14
+ /** USER-driven selection of `item` (click path): moves the tab stop to the
15
+ * item WITHOUT stealing focus, selects its value, emits `value-change`. */
16
+ selectItem(item: Element): void;
17
+ }
18
+ declare const radioGroupContext: DomContext<RadioGroupContextValue>;
19
+ /** Per-item context so `<aihu-radio-group-indicator>` mirrors its OWN item
20
+ * (nearest-provider-wins walk lands on the enclosing item, not the root). */
21
+ interface RadioGroupItemContextValue {
22
+ readonly checked: Read<boolean>;
23
+ readonly disabled: Read<boolean>;
24
+ }
25
+ declare const radioGroupItemContext: DomContext<RadioGroupItemContextValue>;
26
+ declare class AihuRadioGroupRoot extends AihuRovingFocus {
27
+ static readonly observedAttributes: string[];
28
+ private readonly _value;
29
+ private readonly _name;
30
+ private readonly _groupDisabled;
31
+ private readonly _required;
32
+ private _fc;
33
+ private _rgDisposers;
34
+ private _defaultSeeded;
35
+ constructor();
36
+ get value(): Read<string | null>;
37
+ /** Programmatic write: signal + reflected `value` attribute (two-way,
38
+ * dialog open-attr pattern). Does NOT emit `value-change`. */
39
+ setValue(next: string | null): void;
40
+ /** Move the roving current index. With `focus = true` (the base keyboard
41
+ * path — arrows/Home/End) this ALSO selects the landed item's value (APG:
42
+ * moving focus in a radio group selects). `focus = false` moves the tab
43
+ * stop silently and never selects. */
44
+ setCurrent(index: number, focus?: boolean): void;
45
+ connectedCallback(): void;
46
+ disconnectedCallback(): void;
47
+ attributeChangedCallback(name: string, old: string | null, value: string | null): void;
48
+ private _effectiveDisabled;
49
+ private _effectiveRequired;
50
+ private _syncOwnAttrs;
51
+ private _selectItem;
52
+ private _userSelect;
53
+ }
54
+ declare class AihuRadioGroupItem extends HTMLElement {
55
+ static readonly observedAttributes: string[];
56
+ private readonly _itemValue;
57
+ private readonly _disabled;
58
+ private _ctx;
59
+ private _collection;
60
+ private _unregister;
61
+ private _disposers;
62
+ constructor();
63
+ connectedCallback(): void;
64
+ disconnectedCallback(): void;
65
+ attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
66
+ private _checked;
67
+ private _effectiveDisabled;
68
+ private readonly _onKeydown;
69
+ private readonly _onClickCapture;
70
+ }
71
+ /** Presentational styling hook: mirrors its enclosing ITEM's state (nearest
72
+ * `radioGroupItemContext` provider), hidden from AT. */
73
+ declare class AihuRadioGroupIndicator extends HTMLElement {
74
+ private _disposers;
75
+ connectedCallback(): void;
76
+ disconnectedCallback(): void;
77
+ }
78
+ /** Register all radio-group custom elements (idempotent). */
79
+ declare function defineRadioGroup(): void;
80
+ //#endregion
81
+ export { AihuRadioGroupIndicator, AihuRadioGroupItem, AihuRadioGroupRoot, RadioGroupContextValue, RadioGroupItemContextValue, defineRadioGroup, radioGroupContext, radioGroupItemContext };
82
+ //# sourceMappingURL=radio-group.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"radio-group.d.ts","names":[],"sources":["../src/radio-group/index.ts"],"mappings":";;;;;UAgCiB,sBAAA;EAUJ;EAAA,SARF,KAAA,EAAO,IAAA;EAQQ;EAAA,SANf,QAAA,EAAU,IAAA;EAAA,SACV,QAAA,EAAU,IAAA;;EAEnB,QAAA,CAAS,IAAA;EAMmB;AAI9B;EAPE,UAAA,CAAW,IAAA,EAAM,OAAA;AAAA;AAAA,cAGN,iBAAA,EAAiB,UAAA,CAAA,sBAAA;;;UAIb,0BAAA;EAAA,SACN,OAAA,EAAS,IAAA;EAAA,SACT,QAAA,EAAU,IAAA;AAAA;AAAA,cAGR,qBAAA,EAAqB,UAAA,CAAA,0BAAA;AAAA,cAGrB,kBAAA,SAA2B,eAAA;EAAA,gBACb,kBAAA;EAAA,iBASR,MAAA;EAAA,iBACA,KAAA;EAAA,iBACA,cAAA;EAAA,iBACA,SAAA;EAAA,QACT,GAAA;EAAA,QACA,YAAA;EAAA,QACA,cAAA;;MAaJ,KAAA,CAAA,GAAS,IAAA;EAlBI;;EAwBjB,QAAA,CAAS,IAAA;EArBD;;;;EAgCC,UAAA,CAAW,KAAA,UAAe,KAAA;EAS1B,iBAAA,CAAA;EA0EA,oBAAA,CAAA;EAMA,wBAAA,CAAyB,IAAA,UAAc,GAAA,iBAAoB,KAAA;EAAA,QAuB5D,kBAAA;EAAA,QAKA,kBAAA;EAAA,QAKA,aAAA;EAAA,QAMA,WAAA;EAAA,QASA,WAAA;AAAA;AAAA,cAcG,kBAAA,SAA2B,WAAA;EAAA,gBACtB,kBAAA;EAAA,iBAEC,UAAA;EAAA,iBACA,SAAA;EAAA,QACT,IAAA;EAAA,QACA,WAAA;EAAA,QACA,WAAA;EAAA,QACA,UAAA;;EAUR,iBAAA,CAAA;EAkDA,oBAAA,CAAA;EASA,wBAAA,CAAyB,IAAA,UAAc,IAAA,iBAAqB,KAAA;EAAA,QAKpD,QAAA;EAAA,QAKA,kBAAA;EAAA,iBAKS,UAAA;EAAA,iBAcA,eAAA;AAAA;;;cAYN,uBAAA,SAAgC,WAAA;EAAA,QACnC,UAAA;EAER,iBAAA,CAAA;EAYA,oBAAA,CAAA;AAAA;;iBAcc,gBAAA,CAAA"}
@@ -0,0 +1,298 @@
1
+ import { createDomContext, injectValue, provideContext } from "./dom-context.js";
2
+ import { i as attachHiddenInput, r as formControlContext } from "./form-control-B_BO9j7_.js";
3
+ import { collectionContext } from "./collection.js";
4
+ import { AihuRovingFocus } from "./roving-focus.js";
5
+ import { effect, signal, untrack } from "@aihu/signals";
6
+ //#region src/radio-group/index.ts
7
+ /**
8
+ * Headless radio group — `<aihu-radio-group-root>` (state owner, EXTENDS
9
+ * `AihuRovingFocus`) + `<aihu-radio-group-item>` (role="radio") +
10
+ * presentational `<aihu-radio-group-indicator>`. Implements the WAI-ARIA APG
11
+ * **Radio Group** pattern: `role="radiogroup"` on the root, roving tabindex
12
+ * across the items (exactly one `tabindex="0"`), arrow keys move focus AND
13
+ * select (orientation defaults to `"both"`, loop defaults ON for the APG
14
+ * wrap), Space selects an unchecked item, Enter does NOT activate. Emits NO
15
+ * CSS — each piece reflects `data-state="checked" | "unchecked"`
16
+ * (+ `data-disabled` presence) for the consumer to style.
17
+ *
18
+ * Form participation rides `attachHiddenInput` (ONE root-owned
19
+ * visually-hidden native radio in the root's light DOM): submits
20
+ * `name → value` only when a selection exists — no selection submits nothing
21
+ * (native parity).
22
+ *
23
+ * `value-change` (detail `{ value: string }`, bubbles, composed) is dispatched
24
+ * on USER-driven selection only (clicks/keys via the roving `setCurrent`
25
+ * path) — programmatic `setValue()` / attribute writes do not emit (checkbox
26
+ * precedent).
27
+ */
28
+ const radioGroupContext = createDomContext("radio-group");
29
+ const radioGroupItemContext = createDomContext("radio-group-item");
30
+ var AihuRadioGroupRoot = class extends AihuRovingFocus {
31
+ static observedAttributes = [
32
+ ...AihuRovingFocus.observedAttributes,
33
+ "value",
34
+ "default-value",
35
+ "name",
36
+ "disabled",
37
+ "required"
38
+ ];
39
+ _value = signal(null);
40
+ _name = signal(null);
41
+ _groupDisabled = signal(false);
42
+ _required = signal(false);
43
+ _fc = null;
44
+ _rgDisposers = [];
45
+ _defaultSeeded = false;
46
+ constructor() {
47
+ super();
48
+ provideContext(this, radioGroupContext, {
49
+ value: this._value[0],
50
+ disabled: () => this._effectiveDisabled(),
51
+ required: () => this._effectiveRequired(),
52
+ setValue: (next) => this.setValue(next),
53
+ selectItem: (item) => this._selectItem(item)
54
+ });
55
+ }
56
+ get value() {
57
+ return this._value[0];
58
+ }
59
+ /** Programmatic write: signal + reflected `value` attribute (two-way,
60
+ * dialog open-attr pattern). Does NOT emit `value-change`. */
61
+ setValue(next) {
62
+ if (next === this._value[0]()) return;
63
+ this._value[1](next);
64
+ if (next === null) this.removeAttribute("value");
65
+ else this.setAttribute("value", next);
66
+ }
67
+ /** Move the roving current index. With `focus = true` (the base keyboard
68
+ * path — arrows/Home/End) this ALSO selects the landed item's value (APG:
69
+ * moving focus in a radio group selects). `focus = false` moves the tab
70
+ * stop silently and never selects. */
71
+ setCurrent(index, focus = true) {
72
+ super.setCurrent(index, focus);
73
+ if (!focus) return;
74
+ const v = this.items()[this.currentIndex()]?.getAttribute("value") ?? null;
75
+ if (v === null) return;
76
+ this._userSelect(v);
77
+ }
78
+ connectedCallback() {
79
+ if (!this.hasAttribute("orientation")) this.setAttribute("orientation", "both");
80
+ if (!this.hasAttribute("loop")) this.setAttribute("loop", "");
81
+ if (this.hasAttribute("value")) this._value[1](this.getAttribute("value"));
82
+ else if (!this._defaultSeeded && this.hasAttribute("default-value")) this._value[1](this.getAttribute("default-value"));
83
+ this._defaultSeeded = true;
84
+ this._syncOwnAttrs();
85
+ if (!this.hasAttribute("role")) this.setAttribute("role", "radiogroup");
86
+ this.setAttribute("data-fc-control", "");
87
+ try {
88
+ this._fc = injectValue(this, formControlContext);
89
+ } catch {
90
+ this._fc = null;
91
+ }
92
+ super.connectedCallback();
93
+ this._rgDisposers.push(effect(() => {
94
+ if (this._effectiveRequired()) this.setAttribute("aria-required", "true");
95
+ else this.removeAttribute("aria-required");
96
+ if (this._effectiveDisabled()) {
97
+ this.setAttribute("aria-disabled", "true");
98
+ this.setAttribute("data-disabled", "");
99
+ } else {
100
+ this.removeAttribute("aria-disabled");
101
+ this.removeAttribute("data-disabled");
102
+ }
103
+ }), effect(() => {
104
+ const items = this.items();
105
+ const v = this._value[0]();
106
+ if (v === null) return;
107
+ const idx = items.findIndex((el) => el.getAttribute("value") === v);
108
+ if (idx < 0) return;
109
+ untrack(() => {
110
+ if (idx !== this.currentIndex()) this.setCurrent(idx, false);
111
+ });
112
+ }), attachHiddenInput(this, {
113
+ type: "radio",
114
+ name: this._name[0],
115
+ value: () => this._value[0]() ?? "",
116
+ checked: () => this._value[0]() !== null,
117
+ required: () => this._effectiveRequired(),
118
+ disabled: () => this._effectiveDisabled()
119
+ }));
120
+ }
121
+ disconnectedCallback() {
122
+ super.disconnectedCallback();
123
+ for (const d of this._rgDisposers) d();
124
+ this._rgDisposers = [];
125
+ }
126
+ attributeChangedCallback(name, old, value) {
127
+ switch (name) {
128
+ case "value":
129
+ this._value[1](value);
130
+ break;
131
+ case "name":
132
+ this._name[1](value);
133
+ break;
134
+ case "disabled":
135
+ this._groupDisabled[1](value !== null);
136
+ break;
137
+ case "required":
138
+ this._required[1](value !== null);
139
+ break;
140
+ case "default-value": break;
141
+ default: super.attributeChangedCallback(name, old, value);
142
+ }
143
+ }
144
+ _effectiveDisabled() {
145
+ if (this._groupDisabled[0]()) return true;
146
+ return this._fc ? this._fc.disabled() : false;
147
+ }
148
+ _effectiveRequired() {
149
+ if (this._required[0]()) return true;
150
+ return this._fc ? this._fc.required() : false;
151
+ }
152
+ _syncOwnAttrs() {
153
+ this._groupDisabled[1](this.hasAttribute("disabled"));
154
+ this._required[1](this.hasAttribute("required"));
155
+ this._name[1](this.getAttribute("name"));
156
+ }
157
+ _selectItem(item) {
158
+ if (this._effectiveDisabled()) return;
159
+ const idx = this.items().indexOf(item);
160
+ if (idx >= 0) this.setCurrent(idx, false);
161
+ const v = item.getAttribute("value");
162
+ if (v !== null) this._userSelect(v);
163
+ }
164
+ _userSelect(v) {
165
+ if (this._effectiveDisabled()) return;
166
+ if (v === this._value[0]()) return;
167
+ this.setValue(v);
168
+ this.dispatchEvent(new CustomEvent("value-change", {
169
+ detail: { value: v },
170
+ bubbles: true,
171
+ composed: true
172
+ }));
173
+ }
174
+ };
175
+ var AihuRadioGroupItem = class extends HTMLElement {
176
+ static observedAttributes = ["value", "disabled"];
177
+ _itemValue = signal(null);
178
+ _disabled = signal(false);
179
+ _ctx = null;
180
+ _collection = null;
181
+ _unregister = null;
182
+ _disposers = [];
183
+ constructor() {
184
+ super();
185
+ provideContext(this, radioGroupItemContext, {
186
+ checked: () => this._checked(),
187
+ disabled: () => this._effectiveDisabled()
188
+ });
189
+ }
190
+ connectedCallback() {
191
+ this._itemValue[1](this.getAttribute("value"));
192
+ this._disabled[1](this.hasAttribute("disabled"));
193
+ if (!this.hasAttribute("role")) this.setAttribute("role", "radio");
194
+ try {
195
+ this._ctx = injectValue(this, radioGroupContext);
196
+ } catch {
197
+ this._ctx = null;
198
+ }
199
+ try {
200
+ this._collection = injectValue(this, collectionContext);
201
+ } catch {
202
+ this._collection = null;
203
+ }
204
+ this.addEventListener("keydown", this._onKeydown);
205
+ this.addEventListener("click", this._onClickCapture, true);
206
+ this._disposers.push(effect(() => {
207
+ const checked = this._checked();
208
+ this.setAttribute("aria-checked", checked ? "true" : "false");
209
+ this.setAttribute("data-state", checked ? "checked" : "unchecked");
210
+ if (this._effectiveDisabled()) {
211
+ this.setAttribute("aria-disabled", "true");
212
+ this.setAttribute("data-disabled", "");
213
+ } else {
214
+ this.removeAttribute("aria-disabled");
215
+ this.removeAttribute("data-disabled");
216
+ }
217
+ }), effect(() => {
218
+ if (this._effectiveDisabled()) {
219
+ this._unregister?.();
220
+ this._unregister = null;
221
+ this.setAttribute("tabindex", "-1");
222
+ } else if (this._unregister === null && this._collection !== null) this._unregister = this._collection.register(this);
223
+ }));
224
+ }
225
+ disconnectedCallback() {
226
+ this.removeEventListener("keydown", this._onKeydown);
227
+ this.removeEventListener("click", this._onClickCapture, true);
228
+ this._unregister?.();
229
+ this._unregister = null;
230
+ for (const d of this._disposers) d();
231
+ this._disposers = [];
232
+ }
233
+ attributeChangedCallback(name, _old, value) {
234
+ if (name === "value") this._itemValue[1](value);
235
+ if (name === "disabled") this._disabled[1](value !== null);
236
+ }
237
+ _checked() {
238
+ const v = this._itemValue[0]();
239
+ return v !== null && this._ctx !== null && this._ctx.value() === v;
240
+ }
241
+ _effectiveDisabled() {
242
+ if (this._disabled[0]()) return true;
243
+ return this._ctx ? this._ctx.disabled() : false;
244
+ }
245
+ _onKeydown = (ev) => {
246
+ if (ev.key === "Enter") {
247
+ ev.preventDefault();
248
+ return;
249
+ }
250
+ if (ev.key === " " || ev.key === "Spacebar") {
251
+ ev.preventDefault();
252
+ if (this._effectiveDisabled()) return;
253
+ if (!this._checked()) this.click();
254
+ }
255
+ };
256
+ _onClickCapture = (ev) => {
257
+ if (this._effectiveDisabled()) {
258
+ ev.preventDefault();
259
+ ev.stopImmediatePropagation();
260
+ return;
261
+ }
262
+ this._ctx?.selectItem(this);
263
+ };
264
+ };
265
+ /** Presentational styling hook: mirrors its enclosing ITEM's state (nearest
266
+ * `radioGroupItemContext` provider), hidden from AT. */
267
+ var AihuRadioGroupIndicator = class extends HTMLElement {
268
+ _disposers = [];
269
+ connectedCallback() {
270
+ const ctx = injectValue(this, radioGroupItemContext);
271
+ this.setAttribute("aria-hidden", "true");
272
+ this._disposers.push(effect(() => {
273
+ this.setAttribute("data-state", ctx.checked() ? "checked" : "unchecked");
274
+ if (ctx.disabled()) this.setAttribute("data-disabled", "");
275
+ else this.removeAttribute("data-disabled");
276
+ }));
277
+ }
278
+ disconnectedCallback() {
279
+ for (const d of this._disposers) d();
280
+ this._disposers = [];
281
+ }
282
+ };
283
+ const REGISTRY = [
284
+ ["aihu-radio-group-root", AihuRadioGroupRoot],
285
+ ["aihu-radio-group-item", AihuRadioGroupItem],
286
+ ["aihu-radio-group-indicator", AihuRadioGroupIndicator]
287
+ ];
288
+ let _defined = false;
289
+ /** Register all radio-group custom elements (idempotent). */
290
+ function defineRadioGroup() {
291
+ if (_defined) return;
292
+ for (const [tag, ctor] of REGISTRY) if (!customElements.get(tag)) customElements.define(tag, ctor);
293
+ _defined = true;
294
+ }
295
+ //#endregion
296
+ export { AihuRadioGroupIndicator, AihuRadioGroupItem, AihuRadioGroupRoot, defineRadioGroup, radioGroupContext, radioGroupItemContext };
297
+
298
+ //# sourceMappingURL=radio-group.js.map