@funstack/ui 0.1.0-alpha.4 → 0.1.0-alpha.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.
|
@@ -24,29 +24,56 @@ function isNestedOut(el, root) {
|
|
|
24
24
|
for (let node = el; node !== null && node !== root; node = node.parentElement) if (node.getAttribute("role") === "toolbar" || node.hasAttribute("popover") || node.tagName === "DIALOG") return true;
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
|
+
function isRadio(el) {
|
|
28
|
+
return el instanceof HTMLInputElement && el.type === "radio";
|
|
29
|
+
}
|
|
30
|
+
/** The unit's representative: its checked radio, else its first (design 17.2 item 2). */
|
|
31
|
+
function representative(unit) {
|
|
32
|
+
return unit.find((radio) => radio.checked) ?? unit[0];
|
|
33
|
+
}
|
|
27
34
|
/**
|
|
28
|
-
* The items of `root`
|
|
35
|
+
* The items of `root` (design 6.2 and 17.2): its focusable descendants minus `:disabled`,
|
|
29
36
|
* `inert`, invisible, nested-toolbar / popover / dialog content, and elements whose own
|
|
30
37
|
* `tabindex="-1"` was not written by this engine (`managed`). `aria-disabled` elements are items.
|
|
38
|
+
* Native radios that share a `name` form one unit, represented in `items` by its checked radio
|
|
39
|
+
* (else its first), at the position of the unit's first radio.
|
|
31
40
|
*/
|
|
32
|
-
function
|
|
33
|
-
const
|
|
41
|
+
function collectItems(root, managed) {
|
|
42
|
+
const candidates = [];
|
|
43
|
+
const byName = /* @__PURE__ */ new Map();
|
|
44
|
+
const units = /* @__PURE__ */ new Map();
|
|
34
45
|
for (const el of root.querySelectorAll(FOCUSABLE)) {
|
|
35
46
|
if (el.matches(":disabled")) continue;
|
|
36
47
|
if (el.closest("[inert]") !== null) continue;
|
|
37
48
|
if (el.getAttribute("tabindex") === "-1" && !managed.has(el)) continue;
|
|
38
49
|
if (isNestedOut(el, root)) continue;
|
|
39
50
|
if (!isVisible(el)) continue;
|
|
40
|
-
|
|
51
|
+
if (isRadio(el)) {
|
|
52
|
+
const unit = el.name === "" ? void 0 : byName.get(el.name);
|
|
53
|
+
if (unit !== void 0) {
|
|
54
|
+
unit.push(el);
|
|
55
|
+
units.set(el, unit);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const created = [el];
|
|
59
|
+
if (el.name !== "") byName.set(el.name, created);
|
|
60
|
+
units.set(el, created);
|
|
61
|
+
}
|
|
62
|
+
candidates.push(el);
|
|
41
63
|
}
|
|
42
|
-
return
|
|
64
|
+
return {
|
|
65
|
+
items: candidates.map((el) => {
|
|
66
|
+
const unit = units.get(el);
|
|
67
|
+
return unit === void 0 ? el : representative(unit);
|
|
68
|
+
}),
|
|
69
|
+
units
|
|
70
|
+
};
|
|
43
71
|
}
|
|
44
72
|
const BUTTON_LIKE_INPUTS = /* @__PURE__ */ new Set([
|
|
45
73
|
"button",
|
|
46
74
|
"submit",
|
|
47
75
|
"reset",
|
|
48
76
|
"checkbox",
|
|
49
|
-
"radio",
|
|
50
77
|
"image",
|
|
51
78
|
"color",
|
|
52
79
|
"file"
|
|
@@ -58,10 +85,12 @@ const ARROW_OWNING_ROLES = /* @__PURE__ */ new Set([
|
|
|
58
85
|
"slider"
|
|
59
86
|
]);
|
|
60
87
|
/**
|
|
61
|
-
* An item that owns the arrow keys (design 6.3): text-like `input`s, `textarea`,
|
|
62
|
-
* `[contenteditable]`,
|
|
88
|
+
* An item that owns the arrow keys (design 6.3 and 17.2): text-like `input`s, `textarea`,
|
|
89
|
+
* `select`, `[contenteditable]`, `role` `textbox` / `combobox` / `spinbutton` / `slider`, and a
|
|
90
|
+
* native `input[type=radio]` (its group's own arrows check as they move). A custom
|
|
91
|
+
* `[role="radio"]` is an ordinary item, as it is under native `focusgroup`.
|
|
63
92
|
*/
|
|
64
|
-
function
|
|
93
|
+
function ownsArrows(el) {
|
|
65
94
|
const role = el.getAttribute("role");
|
|
66
95
|
if (role !== null && ARROW_OWNING_ROLES.has(role)) return true;
|
|
67
96
|
if (el instanceof HTMLInputElement) return !BUTTON_LIKE_INPUTS.has(el.type);
|
|
@@ -114,16 +143,24 @@ function createEngine() {
|
|
|
114
143
|
release();
|
|
115
144
|
return;
|
|
116
145
|
}
|
|
117
|
-
const items =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
for (const
|
|
146
|
+
const { items, units } = collectItems(root, managed);
|
|
147
|
+
/** Every element the engine writes: the items and the other radios of their units. */
|
|
148
|
+
const all = new Set(items);
|
|
149
|
+
for (const radio of units.keys()) all.add(radio);
|
|
150
|
+
/** The item standing for `el`: its unit's representative for a radio, else itself. */
|
|
151
|
+
const itemOf = (el) => {
|
|
152
|
+
const unit = units.get(el);
|
|
153
|
+
return unit === void 0 ? el : representative(unit);
|
|
154
|
+
};
|
|
155
|
+
if (!options.memory) current = focused instanceof HTMLElement && all.has(focused) ? focused : options.entry(items) ?? null;
|
|
156
|
+
else if (current === null || !all.has(current) || !items.includes(itemOf(current))) current = options.entry(items) ?? null;
|
|
157
|
+
for (const el of all) {
|
|
121
158
|
if (!managed.has(el)) managed.set(el, el.getAttribute("tabindex"));
|
|
122
159
|
const want = el === current ? "0" : "-1";
|
|
123
160
|
if (el.getAttribute("tabindex") !== want) el.setAttribute("tabindex", want);
|
|
124
161
|
}
|
|
125
162
|
for (const el of [...managed.keys()]) {
|
|
126
|
-
if (
|
|
163
|
+
if (all.has(el)) continue;
|
|
127
164
|
if (!root.contains(el)) restore(el);
|
|
128
165
|
else if (el.getAttribute("tabindex") !== "-1") el.setAttribute("tabindex", "-1");
|
|
129
166
|
}
|
|
@@ -133,7 +170,8 @@ function createEngine() {
|
|
|
133
170
|
if (!active(options) || options.native) return;
|
|
134
171
|
const target = event.target;
|
|
135
172
|
if (!(target instanceof HTMLElement)) return;
|
|
136
|
-
|
|
173
|
+
const { items, units } = collectItems(event.currentTarget, managed);
|
|
174
|
+
if (!items.includes(target) && !units.has(target)) return;
|
|
137
175
|
current = target;
|
|
138
176
|
apply(event.currentTarget, target);
|
|
139
177
|
};
|
|
@@ -149,7 +187,8 @@ function createEngine() {
|
|
|
149
187
|
if (!active(options) || options.native) return;
|
|
150
188
|
const target = event.target;
|
|
151
189
|
if (!(target instanceof Node)) return;
|
|
152
|
-
const
|
|
190
|
+
const { items, units } = collectItems(event.currentTarget, managed);
|
|
191
|
+
const item = [...items, ...units.keys()].find((el) => el.contains(target));
|
|
153
192
|
if (item === void 0) return;
|
|
154
193
|
current = item;
|
|
155
194
|
apply(event.currentTarget, options.memory ? item : event.currentTarget.ownerDocument.activeElement);
|
|
@@ -161,11 +200,18 @@ function createEngine() {
|
|
|
161
200
|
const root = event.currentTarget;
|
|
162
201
|
const target = event.target;
|
|
163
202
|
if (!(target instanceof HTMLElement)) return;
|
|
164
|
-
const items =
|
|
165
|
-
const
|
|
203
|
+
const { items, units } = collectItems(root, managed);
|
|
204
|
+
const unit = units.get(target);
|
|
205
|
+
const index = items.indexOf(unit === void 0 ? target : representative(unit));
|
|
166
206
|
if (index < 0) return;
|
|
167
|
-
if (
|
|
207
|
+
if (ownsArrows(target)) {
|
|
168
208
|
if (event.key !== "Tab") return;
|
|
209
|
+
const checked = unit?.find((radio) => radio.checked);
|
|
210
|
+
if (checked !== void 0 && checked !== target) {
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
focusItem(checked);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
169
215
|
const next = items[index + (event.shiftKey ? -1 : 1)];
|
|
170
216
|
if (next === void 0) return;
|
|
171
217
|
event.preventDefault();
|
|
@@ -250,6 +296,6 @@ function useRovingFocus(ref, options) {
|
|
|
250
296
|
return engine.handlers;
|
|
251
297
|
}
|
|
252
298
|
//#endregion
|
|
253
|
-
export {
|
|
299
|
+
export { collectItems, ownsArrows, useRovingFocus };
|
|
254
300
|
|
|
255
301
|
//# sourceMappingURL=use-roving-focus.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-roving-focus.mjs","names":[],"sources":["../../src/internal/use-roving-focus.ts"],"sourcesContent":["\"use client\";\nimport { useEffect, useInsertionEffect, useLayoutEffect, useState } from \"react\";\nimport type { FocusEvent, KeyboardEvent, PointerEvent, RefObject } from \"react\";\n\n// The roving-focus engine shared by `Toolbar.Root` and a standalone `ToggleGroup.Root`\n// (design/components/toggle-group.md section 6, conventions 3.2 and 7.4). Items are the focusable\n// descendants of the container, found in the DOM; which one is the Tab stop is urgent library\n// state kept outside React and written to the items as `tabindex` through the DOM.\n\nexport type RovingOrientation = \"horizontal\" | \"vertical\";\n\nexport interface RovingFocusOptions {\n /** `false`: the engine writes nothing and handles nothing (a ToggleGroup inside a Toolbar). Default `true`. */\n readonly enabled?: boolean;\n readonly orientation: RovingOrientation;\n readonly loop: boolean;\n /** The native `focusgroup` path is active: write no `tabindex`, handle no navigation key. */\n readonly native: boolean;\n /** Which item is the Tab stop when there is no usable memory. */\n readonly entry: (items: readonly HTMLElement[]) => HTMLElement | undefined;\n /**\n * `true`: the last focused item stays the Tab stop after focus leaves (Toolbar). `false`: while\n * focus is inside, the focused item is the stop (so Tab and Shift+Tab leave the container);\n * once it leaves, the stop is re-derived from `entry` (ToggleGroup: the pressed Item).\n */\n readonly memory: boolean;\n}\n\nexport interface RovingFocusHandlers {\n readonly onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;\n readonly onFocus: (event: FocusEvent<HTMLElement>) => void;\n readonly onBlur: (event: FocusEvent<HTMLElement>) => void;\n readonly onPointerDown: (event: PointerEvent<HTMLElement>) => void;\n}\n\nconst FOCUSABLE = [\n \"button\",\n \"a[href]\",\n 'input:not([type=\"hidden\"])',\n \"select\",\n \"textarea\",\n \"summary\",\n '[contenteditable]:not([contenteditable=\"false\"])',\n \"[tabindex]\",\n].join(\",\");\n\nfunction isVisible(el: HTMLElement): boolean {\n // jsdom has no `checkVisibility`; `hidden` is the part of it a test can observe there.\n if (typeof el.checkVisibility === \"function\") return el.checkVisibility();\n return el.closest(\"[hidden]\") === null;\n}\n\n/**\n * Whether `el` or an ancestor below `root` takes it out of `root`'s sequence: a nested toolbar\n * (its own sequence), a popover or a dialog rendered inside the container (a `Menu.Popup`'s items\n * are the menu's).\n */\nfunction isNestedOut(el: HTMLElement, root: HTMLElement): boolean {\n for (let node: Element | null = el; node !== null && node !== root; node = node.parentElement) {\n if (\n node.getAttribute(\"role\") === \"toolbar\" ||\n node.hasAttribute(\"popover\") ||\n node.tagName === \"DIALOG\"\n ) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * The items of `root` in DOM order (design 6.2): its focusable descendants minus `:disabled`,\n * `inert`, invisible, nested-toolbar / popover / dialog content, and elements whose own\n * `tabindex=\"-1\"` was not written by this engine (`managed`). `aria-disabled` elements are items.\n */\nexport function itemsOf(root: HTMLElement, managed: { has(el: Element): boolean }): HTMLElement[] {\n const items: HTMLElement[] = [];\n for (const el of root.querySelectorAll<HTMLElement>(FOCUSABLE)) {\n if (el.matches(\":disabled\")) continue;\n if (el.closest(\"[inert]\") !== null) continue;\n if (el.getAttribute(\"tabindex\") === \"-1\" && !managed.has(el)) continue;\n if (isNestedOut(el, root)) continue;\n if (!isVisible(el)) continue;\n items.push(el);\n }\n return items;\n}\n\nconst BUTTON_LIKE_INPUTS = new Set([\n \"button\",\n \"submit\",\n \"reset\",\n \"checkbox\",\n \"radio\",\n \"image\",\n \"color\",\n \"file\",\n]);\nconst ARROW_OWNING_ROLES = new Set([\"textbox\", \"combobox\", \"spinbutton\", \"slider\"]);\n\n/**\n * An item that owns the arrow keys (design 6.3): text-like `input`s, `textarea`, `select`,\n * `[contenteditable]`, and `role` `textbox` / `combobox` / `spinbutton` / `slider`.\n */\nexport function isTextEntryItem(el: Element): boolean {\n const role = el.getAttribute(\"role\");\n if (role !== null && ARROW_OWNING_ROLES.has(role)) return true;\n if (el instanceof HTMLInputElement) return !BUTTON_LIKE_INPUTS.has(el.type);\n if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) return true;\n const editable = el.getAttribute(\"contenteditable\");\n if (editable !== null && editable !== \"false\") return true;\n return el instanceof HTMLElement && el.isContentEditable === true;\n}\n\ntype Move = \"next\" | \"prev\" | \"first\" | \"last\";\n\nfunction resolveKey(key: string, orientation: RovingOrientation, rtl: boolean): Move | null {\n switch (key) {\n case \"Home\":\n return \"first\";\n case \"End\":\n return \"last\";\n case \"ArrowRight\":\n return orientation === \"horizontal\" ? (rtl ? \"prev\" : \"next\") : null;\n case \"ArrowLeft\":\n return orientation === \"horizontal\" ? (rtl ? \"next\" : \"prev\") : null;\n case \"ArrowDown\":\n return orientation === \"vertical\" ? \"next\" : null;\n case \"ArrowUp\":\n return orientation === \"vertical\" ? \"prev\" : null;\n default:\n return null;\n }\n}\n\nfunction focusItem(item: HTMLElement): void {\n // Keyboard focus: the ring shows even where the engine's heuristic would not (as Tabs).\n item.focus({ focusVisible: true } as FocusOptions);\n}\n\ninterface Engine {\n /** The options of the last committed render (written from an insertion effect). */\n readonly setOptions: (options: RovingFocusOptions) => void;\n /**\n * Re-derives the Tab stop of `root` and writes `tabindex`. `focused`: the element focus is on,\n * if any.\n */\n readonly apply: (root: HTMLElement | null, focused: Element | null) => void;\n readonly handlers: RovingFocusHandlers;\n}\n\nfunction createEngine(): Engine {\n let current: HTMLElement | null = null;\n let latestOptions: RovingFocusOptions | null = null;\n /**\n * Elements this engine wrote `tabindex` on, with the attribute value they had before the first\n * write (`null`: none). Restored when the engine lets go of an element (native path, element\n * left the container): React does not re-write a `tabIndex` prop it believes it rendered, so a\n * `[tabindex=\"0\"]` widget would otherwise become unreachable.\n */\n const managed = new Map<HTMLElement, string | null>();\n\n const restore = (el: HTMLElement) => {\n const original = managed.get(el);\n managed.delete(el);\n if (!el.isConnected || original === undefined) return;\n if (original === null) el.removeAttribute(\"tabindex\");\n else el.setAttribute(\"tabindex\", original);\n };\n\n const active = (options: RovingFocusOptions | null): options is RovingFocusOptions =>\n options !== null && options.enabled !== false;\n\n /** Native path: give back every `tabindex` this engine wrote (a hydration render may have). */\n const release = () => {\n for (const el of [...managed.keys()]) restore(el);\n current = null;\n };\n\n const apply = (root: HTMLElement | null, focused: Element | null) => {\n const options = latestOptions;\n if (root === null || !active(options)) return;\n if (options.native) {\n release();\n return;\n }\n const items = itemsOf(root, managed);\n if (!options.memory) {\n current =\n focused instanceof HTMLElement && items.includes(focused)\n ? focused\n : (options.entry(items) ?? null);\n } else if (current === null || !items.includes(current)) {\n current = options.entry(items) ?? null;\n }\n for (const el of items) {\n if (!managed.has(el)) managed.set(el, el.getAttribute(\"tabindex\"));\n const want = el === current ? \"0\" : \"-1\";\n if (el.getAttribute(\"tabindex\") !== want) el.setAttribute(\"tabindex\", want);\n }\n // Elements written before that are no longer items: a stale `0` must not stay a Tab stop\n // (it would be one again when re-enabled); one that left the container gets its own back.\n for (const el of [...managed.keys()]) {\n if (items.includes(el)) continue;\n if (!root.contains(el)) {\n restore(el);\n } else if (el.getAttribute(\"tabindex\") !== \"-1\") {\n el.setAttribute(\"tabindex\", \"-1\");\n }\n }\n };\n\n const onFocus = (event: FocusEvent<HTMLElement>) => {\n const options = latestOptions;\n if (!active(options) || options.native) return;\n const target = event.target;\n if (!(target instanceof HTMLElement)) return;\n const items = itemsOf(event.currentTarget, managed);\n if (!items.includes(target)) return;\n current = target;\n apply(event.currentTarget, target);\n };\n\n const onBlur = (event: FocusEvent<HTMLElement>) => {\n const options = latestOptions;\n if (!active(options) || options.native || options.memory) return;\n // Focus moving to another item is handled by its focus event.\n const next = event.relatedTarget;\n if (next instanceof Node && event.currentTarget.contains(next)) return;\n apply(event.currentTarget, null);\n };\n\n const onPointerDown = (event: PointerEvent<HTMLElement>) => {\n const options = latestOptions;\n if (!active(options) || options.native) return;\n const target = event.target;\n if (!(target instanceof Node)) return;\n const items = itemsOf(event.currentTarget, managed);\n // Safari does not focus a button on click; the pointer still makes it the stop.\n const item = items.find((el) => el.contains(target));\n if (item === undefined) return;\n current = item;\n apply(\n event.currentTarget,\n options.memory ? item : event.currentTarget.ownerDocument.activeElement,\n );\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;\n const options = latestOptions;\n // The browser moves focus itself where `focusgroup` is implemented.\n if (!active(options) || options.native) return;\n const root = event.currentTarget;\n const target = event.target;\n if (!(target instanceof HTMLElement)) return;\n const items = itemsOf(root, managed);\n const index = items.indexOf(target);\n if (index < 0) return;\n\n if (isTextEntryItem(target)) {\n // The item keeps its arrows, Home and End; Tab / Shift+Tab move to the next / previous item\n // while one exists, as Chromium's `focusgroup=\"toolbar\"` does (design 6.3).\n if (event.key !== \"Tab\") return;\n const next = items[index + (event.shiftKey ? -1 : 1)];\n if (next === undefined) return;\n event.preventDefault();\n focusItem(next);\n return;\n }\n\n const rtl = options.orientation === \"horizontal\" && getComputedStyle(root).direction === \"rtl\";\n const move = resolveKey(event.key, options.orientation, rtl);\n if (move === null) return;\n event.preventDefault();\n const last = items.length - 1;\n let nextIndex: number;\n switch (move) {\n case \"first\":\n nextIndex = 0;\n break;\n case \"last\":\n nextIndex = last;\n break;\n case \"next\":\n nextIndex = index >= last ? (options.loop ? 0 : last) : index + 1;\n break;\n case \"prev\":\n nextIndex = index <= 0 ? (options.loop ? last : 0) : index - 1;\n break;\n }\n const next = items[nextIndex];\n if (next !== undefined && next !== target) focusItem(next);\n };\n\n return {\n setOptions: (options) => {\n latestOptions = options;\n },\n apply,\n handlers: { onFocus, onBlur, onPointerDown, onKeyDown },\n };\n}\n\n/**\n * Roving focus over the focusable descendants of `ref`'s element (design 6.2). Writes `tabindex`\n * (`0` on the Tab stop, `-1` on the other items) from a layout effect after every commit of the\n * calling component, from a `MutationObserver` on the subtree and from focus / pointer events;\n * the items render none. Returns the container's handlers for its library props.\n */\nexport function useRovingFocus(\n ref: RefObject<HTMLElement | null>,\n options: RovingFocusOptions,\n): RovingFocusHandlers {\n const [engine] = useState(createEngine);\n // Before every layout effect of the same commit (as `useLatest`): handlers and the observer\n // read the options of the last committed render.\n useInsertionEffect(() => {\n engine.setOptions(options);\n });\n const enabled = options.enabled !== false;\n\n // After every commit of the container (a deps-less layout effect).\n useLayoutEffect(() => {\n const root = ref.current;\n engine.apply(root, root?.ownerDocument.activeElement ?? null);\n });\n\n // Children that change without the container re-rendering.\n useEffect(() => {\n const root = ref.current;\n if (!enabled || root === null) return;\n const observer = new MutationObserver(() => {\n engine.apply(root, root.ownerDocument.activeElement);\n });\n observer.observe(root, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"disabled\", \"hidden\", \"inert\", \"aria-disabled\"],\n });\n return () => {\n observer.disconnect();\n };\n }, [engine, enabled, ref]);\n\n return engine.handlers;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-roving-focus.mjs","names":[],"sources":["../../src/internal/use-roving-focus.ts"],"sourcesContent":["\"use client\";\nimport { useEffect, useInsertionEffect, useLayoutEffect, useState } from \"react\";\nimport type { FocusEvent, KeyboardEvent, PointerEvent, RefObject } from \"react\";\n\n// The roving-focus engine shared by `Toolbar.Root` and a standalone `ToggleGroup.Root`\n// (design/components/toggle-group.md section 6, conventions 3.2 and 7.4). Items are the focusable\n// descendants of the container, found in the DOM; which one is the Tab stop is urgent library\n// state kept outside React and written to the items as `tabindex` through the DOM.\n\nexport type RovingOrientation = \"horizontal\" | \"vertical\";\n\nexport interface RovingFocusOptions {\n /** `false`: the engine writes nothing and handles nothing (a ToggleGroup inside a Toolbar). Default `true`. */\n readonly enabled?: boolean;\n readonly orientation: RovingOrientation;\n readonly loop: boolean;\n /** The native `focusgroup` path is active: write no `tabindex`, handle no navigation key. */\n readonly native: boolean;\n /** Which item is the Tab stop when there is no usable memory. */\n readonly entry: (items: readonly HTMLElement[]) => HTMLElement | undefined;\n /**\n * `true`: the last focused item stays the Tab stop after focus leaves (Toolbar). `false`: while\n * focus is inside, the focused item is the stop (so Tab and Shift+Tab leave the container);\n * once it leaves, the stop is re-derived from `entry` (ToggleGroup: the pressed Item).\n */\n readonly memory: boolean;\n}\n\nexport interface RovingFocusHandlers {\n readonly onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;\n readonly onFocus: (event: FocusEvent<HTMLElement>) => void;\n readonly onBlur: (event: FocusEvent<HTMLElement>) => void;\n readonly onPointerDown: (event: PointerEvent<HTMLElement>) => void;\n}\n\nconst FOCUSABLE = [\n \"button\",\n \"a[href]\",\n 'input:not([type=\"hidden\"])',\n \"select\",\n \"textarea\",\n \"summary\",\n '[contenteditable]:not([contenteditable=\"false\"])',\n \"[tabindex]\",\n].join(\",\");\n\nfunction isVisible(el: HTMLElement): boolean {\n // jsdom has no `checkVisibility`; `hidden` is the part of it a test can observe there.\n if (typeof el.checkVisibility === \"function\") return el.checkVisibility();\n return el.closest(\"[hidden]\") === null;\n}\n\n/**\n * Whether `el` or an ancestor below `root` takes it out of `root`'s sequence: a nested toolbar\n * (its own sequence), a popover or a dialog rendered inside the container (a `Menu.Popup`'s items\n * are the menu's).\n */\nfunction isNestedOut(el: HTMLElement, root: HTMLElement): boolean {\n for (let node: Element | null = el; node !== null && node !== root; node = node.parentElement) {\n if (\n node.getAttribute(\"role\") === \"toolbar\" ||\n node.hasAttribute(\"popover\") ||\n node.tagName === \"DIALOG\"\n ) {\n return true;\n }\n }\n return false;\n}\n\nfunction isRadio(el: Element): el is HTMLInputElement {\n return el instanceof HTMLInputElement && el.type === \"radio\";\n}\n\n/** The items of a container, with native radio groups collapsed into units (design 17.2). */\nexport interface RovingItems {\n /** The toolbar's own destinations in DOM order; a radio unit appears as its representative. */\n readonly items: HTMLElement[];\n /**\n * Every native radio among the candidates → the radios of its unit (same `name` under the\n * container, DOM order). A radio without a `name` is a unit of one.\n */\n readonly units: Map<HTMLElement, readonly HTMLInputElement[]>;\n}\n\n/** The unit's representative: its checked radio, else its first (design 17.2 item 2). */\nfunction representative(unit: readonly HTMLInputElement[]): HTMLInputElement {\n return unit.find((radio) => radio.checked) ?? unit[0]!;\n}\n\n/**\n * The items of `root` (design 6.2 and 17.2): its focusable descendants minus `:disabled`,\n * `inert`, invisible, nested-toolbar / popover / dialog content, and elements whose own\n * `tabindex=\"-1\"` was not written by this engine (`managed`). `aria-disabled` elements are items.\n * Native radios that share a `name` form one unit, represented in `items` by its checked radio\n * (else its first), at the position of the unit's first radio.\n */\nexport function collectItems(\n root: HTMLElement,\n managed: { has(el: Element): boolean },\n): RovingItems {\n const candidates: HTMLElement[] = [];\n const byName = new Map<string, HTMLInputElement[]>();\n const units = new Map<HTMLElement, readonly HTMLInputElement[]>();\n for (const el of root.querySelectorAll<HTMLElement>(FOCUSABLE)) {\n if (el.matches(\":disabled\")) continue;\n if (el.closest(\"[inert]\") !== null) continue;\n if (el.getAttribute(\"tabindex\") === \"-1\" && !managed.has(el)) continue;\n if (isNestedOut(el, root)) continue;\n if (!isVisible(el)) continue;\n if (isRadio(el)) {\n // Same form owner assumed: a `form=` attribute that splits a `name` is not handled (v1).\n const unit = el.name === \"\" ? undefined : byName.get(el.name);\n if (unit !== undefined) {\n unit.push(el);\n units.set(el, unit);\n continue;\n }\n const created = [el];\n if (el.name !== \"\") byName.set(el.name, created);\n units.set(el, created);\n }\n candidates.push(el);\n }\n // `candidates` holds each unit's first radio in its place; swap in the representative.\n const items = candidates.map((el) => {\n const unit = units.get(el);\n return unit === undefined ? el : representative(unit);\n });\n return { items, units };\n}\n\n/** {@link collectItems}'s `items`: the container's destinations in DOM order. */\nexport function itemsOf(root: HTMLElement, managed: { has(el: Element): boolean }): HTMLElement[] {\n return collectItems(root, managed).items;\n}\n\nconst BUTTON_LIKE_INPUTS = new Set([\n \"button\",\n \"submit\",\n \"reset\",\n \"checkbox\",\n \"image\",\n \"color\",\n \"file\",\n]);\nconst ARROW_OWNING_ROLES = new Set([\"textbox\", \"combobox\", \"spinbutton\", \"slider\"]);\n\n/**\n * An item that owns the arrow keys (design 6.3 and 17.2): text-like `input`s, `textarea`,\n * `select`, `[contenteditable]`, `role` `textbox` / `combobox` / `spinbutton` / `slider`, and a\n * native `input[type=radio]` (its group's own arrows check as they move). A custom\n * `[role=\"radio\"]` is an ordinary item, as it is under native `focusgroup`.\n */\nexport function ownsArrows(el: Element): boolean {\n const role = el.getAttribute(\"role\");\n if (role !== null && ARROW_OWNING_ROLES.has(role)) return true;\n if (el instanceof HTMLInputElement) return !BUTTON_LIKE_INPUTS.has(el.type);\n if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) return true;\n const editable = el.getAttribute(\"contenteditable\");\n if (editable !== null && editable !== \"false\") return true;\n return el instanceof HTMLElement && el.isContentEditable === true;\n}\n\ntype Move = \"next\" | \"prev\" | \"first\" | \"last\";\n\nfunction resolveKey(key: string, orientation: RovingOrientation, rtl: boolean): Move | null {\n switch (key) {\n case \"Home\":\n return \"first\";\n case \"End\":\n return \"last\";\n case \"ArrowRight\":\n return orientation === \"horizontal\" ? (rtl ? \"prev\" : \"next\") : null;\n case \"ArrowLeft\":\n return orientation === \"horizontal\" ? (rtl ? \"next\" : \"prev\") : null;\n case \"ArrowDown\":\n return orientation === \"vertical\" ? \"next\" : null;\n case \"ArrowUp\":\n return orientation === \"vertical\" ? \"prev\" : null;\n default:\n return null;\n }\n}\n\nfunction focusItem(item: HTMLElement): void {\n // Keyboard focus: the ring shows even where the engine's heuristic would not (as Tabs).\n item.focus({ focusVisible: true } as FocusOptions);\n}\n\ninterface Engine {\n /** The options of the last committed render (written from an insertion effect). */\n readonly setOptions: (options: RovingFocusOptions) => void;\n /**\n * Re-derives the Tab stop of `root` and writes `tabindex`. `focused`: the element focus is on,\n * if any.\n */\n readonly apply: (root: HTMLElement | null, focused: Element | null) => void;\n readonly handlers: RovingFocusHandlers;\n}\n\nfunction createEngine(): Engine {\n let current: HTMLElement | null = null;\n let latestOptions: RovingFocusOptions | null = null;\n /**\n * Elements this engine wrote `tabindex` on, with the attribute value they had before the first\n * write (`null`: none). Restored when the engine lets go of an element (native path, element\n * left the container): React does not re-write a `tabIndex` prop it believes it rendered, so a\n * `[tabindex=\"0\"]` widget would otherwise become unreachable.\n */\n const managed = new Map<HTMLElement, string | null>();\n\n const restore = (el: HTMLElement) => {\n const original = managed.get(el);\n managed.delete(el);\n if (!el.isConnected || original === undefined) return;\n if (original === null) el.removeAttribute(\"tabindex\");\n else el.setAttribute(\"tabindex\", original);\n };\n\n const active = (options: RovingFocusOptions | null): options is RovingFocusOptions =>\n options !== null && options.enabled !== false;\n\n /** Native path: give back every `tabindex` this engine wrote (a hydration render may have). */\n const release = () => {\n for (const el of [...managed.keys()]) restore(el);\n current = null;\n };\n\n const apply = (root: HTMLElement | null, focused: Element | null) => {\n const options = latestOptions;\n if (root === null || !active(options)) return;\n if (options.native) {\n release();\n return;\n }\n const { items, units } = collectItems(root, managed);\n /** Every element the engine writes: the items and the other radios of their units. */\n const all = new Set<HTMLElement>(items);\n for (const radio of units.keys()) all.add(radio);\n /** The item standing for `el`: its unit's representative for a radio, else itself. */\n const itemOf = (el: HTMLElement): HTMLElement => {\n const unit = units.get(el);\n return unit === undefined ? el : representative(unit);\n };\n if (!options.memory) {\n current =\n focused instanceof HTMLElement && all.has(focused)\n ? focused\n : (options.entry(items) ?? null);\n } else if (current === null || !all.has(current) || !items.includes(itemOf(current))) {\n current = options.entry(items) ?? null;\n }\n // `current` may be any radio of its unit (focus landed on an unchecked one): `0` on it, `-1`\n // on the unit's other radios and on every other element (design 17.2 item 5).\n for (const el of all) {\n if (!managed.has(el)) managed.set(el, el.getAttribute(\"tabindex\"));\n const want = el === current ? \"0\" : \"-1\";\n if (el.getAttribute(\"tabindex\") !== want) el.setAttribute(\"tabindex\", want);\n }\n // Elements written before that are no longer items: a stale `0` must not stay a Tab stop\n // (it would be one again when re-enabled); one that left the container gets its own back.\n for (const el of [...managed.keys()]) {\n if (all.has(el)) continue;\n if (!root.contains(el)) {\n restore(el);\n } else if (el.getAttribute(\"tabindex\") !== \"-1\") {\n el.setAttribute(\"tabindex\", \"-1\");\n }\n }\n };\n\n const onFocus = (event: FocusEvent<HTMLElement>) => {\n const options = latestOptions;\n if (!active(options) || options.native) return;\n const target = event.target;\n if (!(target instanceof HTMLElement)) return;\n const { items, units } = collectItems(event.currentTarget, managed);\n if (!items.includes(target) && !units.has(target)) return;\n current = target;\n apply(event.currentTarget, target);\n };\n\n const onBlur = (event: FocusEvent<HTMLElement>) => {\n const options = latestOptions;\n if (!active(options) || options.native || options.memory) return;\n // Focus moving to another item is handled by its focus event.\n const next = event.relatedTarget;\n if (next instanceof Node && event.currentTarget.contains(next)) return;\n apply(event.currentTarget, null);\n };\n\n const onPointerDown = (event: PointerEvent<HTMLElement>) => {\n const options = latestOptions;\n if (!active(options) || options.native) return;\n const target = event.target;\n if (!(target instanceof Node)) return;\n const { items, units } = collectItems(event.currentTarget, managed);\n // Safari does not focus a button on click; the pointer still makes it the stop.\n const item = [...items, ...units.keys()].find((el) => el.contains(target));\n if (item === undefined) return;\n current = item;\n apply(\n event.currentTarget,\n options.memory ? item : event.currentTarget.ownerDocument.activeElement,\n );\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;\n const options = latestOptions;\n // The browser moves focus itself where `focusgroup` is implemented.\n if (!active(options) || options.native) return;\n const root = event.currentTarget;\n const target = event.target;\n if (!(target instanceof HTMLElement)) return;\n const { items, units } = collectItems(root, managed);\n const unit = units.get(target);\n // A radio stands in `items` through its unit's representative.\n const index = items.indexOf(unit === undefined ? target : representative(unit));\n if (index < 0) return;\n\n if (ownsArrows(target)) {\n // The item keeps its arrows, Home and End; Tab / Shift+Tab move to the next / previous item\n // while one exists, as Chromium's `focusgroup=\"toolbar\"` does (design 6.3, 17.2 item 4).\n if (event.key !== \"Tab\") return;\n // From an unchecked radio whose sibling is checked: that sibling first (only the checked\n // radio of a group is sequentially focusable natively).\n const checked = unit?.find((radio) => radio.checked);\n if (checked !== undefined && checked !== target) {\n event.preventDefault();\n focusItem(checked);\n return;\n }\n const next = items[index + (event.shiftKey ? -1 : 1)];\n if (next === undefined) return;\n event.preventDefault();\n focusItem(next);\n return;\n }\n\n const rtl = options.orientation === \"horizontal\" && getComputedStyle(root).direction === \"rtl\";\n const move = resolveKey(event.key, options.orientation, rtl);\n if (move === null) return;\n event.preventDefault();\n const last = items.length - 1;\n let nextIndex: number;\n switch (move) {\n case \"first\":\n nextIndex = 0;\n break;\n case \"last\":\n nextIndex = last;\n break;\n case \"next\":\n nextIndex = index >= last ? (options.loop ? 0 : last) : index + 1;\n break;\n case \"prev\":\n nextIndex = index <= 0 ? (options.loop ? last : 0) : index - 1;\n break;\n }\n const next = items[nextIndex];\n if (next !== undefined && next !== target) focusItem(next);\n };\n\n return {\n setOptions: (options) => {\n latestOptions = options;\n },\n apply,\n handlers: { onFocus, onBlur, onPointerDown, onKeyDown },\n };\n}\n\n/**\n * Roving focus over the focusable descendants of `ref`'s element (design 6.2). Writes `tabindex`\n * (`0` on the Tab stop, `-1` on the other items) from a layout effect after every commit of the\n * calling component, from a `MutationObserver` on the subtree and from focus / pointer events;\n * the items render none. Returns the container's handlers for its library props.\n */\nexport function useRovingFocus(\n ref: RefObject<HTMLElement | null>,\n options: RovingFocusOptions,\n): RovingFocusHandlers {\n const [engine] = useState(createEngine);\n // Before every layout effect of the same commit (as `useLatest`): handlers and the observer\n // read the options of the last committed render.\n useInsertionEffect(() => {\n engine.setOptions(options);\n });\n const enabled = options.enabled !== false;\n\n // After every commit of the container (a deps-less layout effect).\n useLayoutEffect(() => {\n const root = ref.current;\n engine.apply(root, root?.ownerDocument.activeElement ?? null);\n });\n\n // Children that change without the container re-rendering.\n useEffect(() => {\n const root = ref.current;\n if (!enabled || root === null) return;\n const observer = new MutationObserver(() => {\n engine.apply(root, root.ownerDocument.activeElement);\n });\n observer.observe(root, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"disabled\", \"hidden\", \"inert\", \"aria-disabled\"],\n });\n return () => {\n observer.disconnect();\n };\n }, [engine, enabled, ref]);\n\n return engine.handlers;\n}\n"],"mappings":"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"}
|
|
@@ -52,9 +52,11 @@ function TooltipRoot({ delay = 250, closeDelay = 150, disabled = false, provides
|
|
|
52
52
|
TooltipRoot.displayName = "TooltipRoot";
|
|
53
53
|
/**
|
|
54
54
|
* The containers that move focus among their items with the arrow keys (roving tabindex, or a
|
|
55
|
-
* native `focusgroup`): Menu, Tabs, Toolbar, a radio group, and the other APG composites
|
|
55
|
+
* native `focusgroup`): Menu, Tabs, Toolbar, a radio group, and the other APG composites, plus
|
|
56
|
+
* `role="group"`, which a standalone `ToggleGroup.Root` renders while its roving engine writes
|
|
57
|
+
* the Items' `tabindex` (#75, design/components/toggle-group.md 18).
|
|
56
58
|
*/
|
|
57
|
-
const COMPOSITE = "[role=\"menu\"],[role=\"menubar\"],[role=\"tablist\"],[role=\"toolbar\"],[role=\"radiogroup\"],[role=\"listbox\"],[role=\"grid\"],[role=\"treegrid\"],[role=\"tree\"],[focusgroup]";
|
|
59
|
+
const COMPOSITE = "[role=\"menu\"],[role=\"menubar\"],[role=\"tablist\"],[role=\"toolbar\"],[role=\"group\"],[role=\"radiogroup\"],[role=\"listbox\"],[role=\"grid\"],[role=\"treegrid\"],[role=\"tree\"],[focusgroup]";
|
|
58
60
|
/**
|
|
59
61
|
* Whether keyboard users can focus the trigger: it is in the tab order, or it is a
|
|
60
62
|
* roving-tabindex item (`tabindex="-1"` set explicitly, so script can focus it) inside a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tooltip-root.mjs","names":[],"sources":["../../src/tooltip/tooltip-root.tsx"],"sourcesContent":["\"use client\";\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport type { StateValue } from \"../internal/types.js\";\nimport { resolveStateValue } from \"../internal/use-render.js\";\nimport { TooltipContext } from \"./tooltip-context.js\";\nimport type { TooltipContextValue, TooltipProvides } from \"./tooltip-context.js\";\nimport { useTooltipInteraction } from \"./use-tooltip-interaction.js\";\n\nexport type { TooltipProvides } from \"./tooltip-context.js\";\n\nexport interface TooltipRootState {\n readonly disabled: boolean;\n}\n\nexport interface TooltipRootProps {\n /**\n * Milliseconds from hover or keyboard focus to open. Default 250 (Chromium's\n * `interest-delay-start`). The same delay applies to hover and focus.\n */\n delay?: number;\n /** Milliseconds from de-hover or blur to close. Default 150 (Chromium's `interest-delay-end`). */\n closeDelay?: number;\n /** When true the tooltip never opens. The ARIA relation stays in place. */\n disabled?: boolean;\n /**\n * Which accessibility relation the tooltip provides for the trigger.\n * `\"description\"` (default): `aria-describedby`, for triggers that have their own name.\n * `\"name\"`: `aria-labelledby`, for icon-only triggers; do not also set `aria-label`.\n */\n provides?: TooltipProvides;\n children?: StateValue<ReactNode, TooltipRootState>;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nconst FOCUSABLE =\n \"a[href],area[href],button,input,select,textarea,iframe,[tabindex],[contenteditable]\";\n\n/**\n * Owns the popup id, the options and the one interaction effect. Renders no DOM, so it has no\n * `ref`, `className` or `style`. The open state is browser-owned (the popover's top-layer state):\n * there is no `open` prop, no action, and no `data-open`; style with `:popover-open`.\n *\n * Render `Tooltip.Popup` immediately after `Tooltip.Trigger` in DOM order: the HTML spec\n * recommends that reading order for popovers, and some screen readers attach an adjacent tooltip\n * to the wrong control otherwise. This is documented, not enforced.\n */\nexport function TooltipRoot({\n delay = 250,\n closeDelay = 150,\n disabled = false,\n provides = \"description\",\n children,\n}: TooltipRootProps) {\n const popupId = useId();\n const [trigger, setTriggerElement] = useState<HTMLElement | null>(null);\n const [popup, setPopupElement] = useState<HTMLElement | null>(null);\n const [interestForRequested, setInterestForRequested] = useState(false);\n\n useTooltipInteraction(trigger, popup, { delay, closeDelay, disabled, interestForRequested });\n useTooltipDevWarnings(trigger, popup, provides);\n\n const context = useMemo<TooltipContextValue>(\n () => ({\n popupId,\n disabled,\n delay,\n closeDelay,\n provides,\n setTriggerElement,\n setPopupElement,\n setInterestForRequested,\n }),\n [popupId, disabled, delay, closeDelay, provides],\n );\n\n const state: TooltipRootState = { disabled };\n return <TooltipContext value={context}>{resolveStateValue(children, state)}</TooltipContext>;\n}\n\nTooltipRoot.displayName = \"TooltipRoot\";\n\n/**\n * The containers that move focus among their items with the arrow keys (roving tabindex, or a\n * native `focusgroup`): Menu, Tabs, Toolbar, a radio group, and the other APG composites.\n */\nconst COMPOSITE =\n '[role=\"menu\"],[role=\"menubar\"],[role=\"tablist\"],[role=\"toolbar\"],[role=\"radiogroup\"],[role=\"listbox\"],[role=\"grid\"],[role=\"treegrid\"],[role=\"tree\"],[focusgroup]';\n\n/**\n * Whether keyboard users can focus the trigger: it is in the tab order, or it is a\n * roving-tabindex item (`tabindex=\"-1\"` set explicitly, so script can focus it) inside a\n * composite widget that moves focus to it (#57).\n */\nfunction isKeyboardReachable(trigger: HTMLElement): boolean {\n if (trigger.tabIndex >= 0) return true;\n return trigger.hasAttribute(\"tabindex\") && trigger.parentElement?.closest(COMPOSITE) != null;\n}\n\n/** Development-only checks that need the mounted elements (design 12.5). Each fires once. */\nfunction useTooltipDevWarnings(\n trigger: HTMLElement | null,\n popup: HTMLElement | null,\n provides: TooltipProvides,\n): void {\n const warned = useRef({ label: false, focusable: false, interactive: false });\n useEffect(() => {\n if (!isDev) return;\n const w = warned.current;\n if (trigger !== null) {\n if (provides === \"name\" && trigger.hasAttribute(\"aria-label\") && !w.label) {\n w.label = true;\n console.error(\n '[Tooltip] provides=\"name\" together with aria-label on the trigger announces the name twice. Remove aria-label.',\n );\n }\n if (!isKeyboardReachable(trigger) && !w.focusable) {\n w.focusable = true;\n console.error(\n \"[Tooltip] The trigger is not focusable; keyboard users cannot open the tooltip. Render a <button> or a link, or set tabIndex={0}.\",\n );\n }\n }\n if (popup !== null && popup.querySelector(FOCUSABLE) !== null && !w.interactive) {\n w.interactive = true;\n console.error(\n \"[Tooltip] Tooltips must not contain interactive content. Use a non-modal dialog or a popover instead.\",\n );\n }\n }, [trigger, popup, provides]);\n}\n"],"mappings":";;;;;;;AAkCA,MAAM,QAAQ,QAAQ,IAAI,aAAa;AAEvC,MAAM,YACJ;;;;;;;;;;AAWF,SAAgB,YAAY,EAC1B,QAAQ,KACR,aAAa,KACb,WAAW,OACX,WAAW,eACX,YACmB;CACnB,MAAM,UAAU,MAAM;CACtB,MAAM,CAAC,SAAS,qBAAqB,SAA6B,IAAI;CACtE,MAAM,CAAC,OAAO,mBAAmB,SAA6B,IAAI;CAClE,MAAM,CAAC,sBAAsB,2BAA2B,SAAS,KAAK;CAEtE,sBAAsB,SAAS,OAAO;EAAE;EAAO;EAAY;EAAU;CAAqB,CAAC;CAC3F,sBAAsB,SAAS,OAAO,QAAQ;CAE9C,MAAM,UAAU,eACP;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,IACA;EAAC;EAAS;EAAU;EAAO;EAAY;CAAQ,CACjD;CAGA,OAAO,oBAAC,gBAAD;EAAgB,OAAO;EAAU,UAAA,kBAAkB,UAAU,EADlC,SACsC,CAAC;CAAkB,CAAA;AAC7F;AAEA,YAAY,cAAc
|
|
1
|
+
{"version":3,"file":"tooltip-root.mjs","names":[],"sources":["../../src/tooltip/tooltip-root.tsx"],"sourcesContent":["\"use client\";\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport type { StateValue } from \"../internal/types.js\";\nimport { resolveStateValue } from \"../internal/use-render.js\";\nimport { TooltipContext } from \"./tooltip-context.js\";\nimport type { TooltipContextValue, TooltipProvides } from \"./tooltip-context.js\";\nimport { useTooltipInteraction } from \"./use-tooltip-interaction.js\";\n\nexport type { TooltipProvides } from \"./tooltip-context.js\";\n\nexport interface TooltipRootState {\n readonly disabled: boolean;\n}\n\nexport interface TooltipRootProps {\n /**\n * Milliseconds from hover or keyboard focus to open. Default 250 (Chromium's\n * `interest-delay-start`). The same delay applies to hover and focus.\n */\n delay?: number;\n /** Milliseconds from de-hover or blur to close. Default 150 (Chromium's `interest-delay-end`). */\n closeDelay?: number;\n /** When true the tooltip never opens. The ARIA relation stays in place. */\n disabled?: boolean;\n /**\n * Which accessibility relation the tooltip provides for the trigger.\n * `\"description\"` (default): `aria-describedby`, for triggers that have their own name.\n * `\"name\"`: `aria-labelledby`, for icon-only triggers; do not also set `aria-label`.\n */\n provides?: TooltipProvides;\n children?: StateValue<ReactNode, TooltipRootState>;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nconst FOCUSABLE =\n \"a[href],area[href],button,input,select,textarea,iframe,[tabindex],[contenteditable]\";\n\n/**\n * Owns the popup id, the options and the one interaction effect. Renders no DOM, so it has no\n * `ref`, `className` or `style`. The open state is browser-owned (the popover's top-layer state):\n * there is no `open` prop, no action, and no `data-open`; style with `:popover-open`.\n *\n * Render `Tooltip.Popup` immediately after `Tooltip.Trigger` in DOM order: the HTML spec\n * recommends that reading order for popovers, and some screen readers attach an adjacent tooltip\n * to the wrong control otherwise. This is documented, not enforced.\n */\nexport function TooltipRoot({\n delay = 250,\n closeDelay = 150,\n disabled = false,\n provides = \"description\",\n children,\n}: TooltipRootProps) {\n const popupId = useId();\n const [trigger, setTriggerElement] = useState<HTMLElement | null>(null);\n const [popup, setPopupElement] = useState<HTMLElement | null>(null);\n const [interestForRequested, setInterestForRequested] = useState(false);\n\n useTooltipInteraction(trigger, popup, { delay, closeDelay, disabled, interestForRequested });\n useTooltipDevWarnings(trigger, popup, provides);\n\n const context = useMemo<TooltipContextValue>(\n () => ({\n popupId,\n disabled,\n delay,\n closeDelay,\n provides,\n setTriggerElement,\n setPopupElement,\n setInterestForRequested,\n }),\n [popupId, disabled, delay, closeDelay, provides],\n );\n\n const state: TooltipRootState = { disabled };\n return <TooltipContext value={context}>{resolveStateValue(children, state)}</TooltipContext>;\n}\n\nTooltipRoot.displayName = \"TooltipRoot\";\n\n/**\n * The containers that move focus among their items with the arrow keys (roving tabindex, or a\n * native `focusgroup`): Menu, Tabs, Toolbar, a radio group, and the other APG composites, plus\n * `role=\"group\"`, which a standalone `ToggleGroup.Root` renders while its roving engine writes\n * the Items' `tabindex` (#75, design/components/toggle-group.md 18).\n */\nconst COMPOSITE =\n '[role=\"menu\"],[role=\"menubar\"],[role=\"tablist\"],[role=\"toolbar\"],[role=\"group\"],[role=\"radiogroup\"],[role=\"listbox\"],[role=\"grid\"],[role=\"treegrid\"],[role=\"tree\"],[focusgroup]';\n\n/**\n * Whether keyboard users can focus the trigger: it is in the tab order, or it is a\n * roving-tabindex item (`tabindex=\"-1\"` set explicitly, so script can focus it) inside a\n * composite widget that moves focus to it (#57).\n */\nfunction isKeyboardReachable(trigger: HTMLElement): boolean {\n if (trigger.tabIndex >= 0) return true;\n return trigger.hasAttribute(\"tabindex\") && trigger.parentElement?.closest(COMPOSITE) != null;\n}\n\n/** Development-only checks that need the mounted elements (design 12.5). Each fires once. */\nfunction useTooltipDevWarnings(\n trigger: HTMLElement | null,\n popup: HTMLElement | null,\n provides: TooltipProvides,\n): void {\n const warned = useRef({ label: false, focusable: false, interactive: false });\n useEffect(() => {\n if (!isDev) return;\n const w = warned.current;\n if (trigger !== null) {\n if (provides === \"name\" && trigger.hasAttribute(\"aria-label\") && !w.label) {\n w.label = true;\n console.error(\n '[Tooltip] provides=\"name\" together with aria-label on the trigger announces the name twice. Remove aria-label.',\n );\n }\n if (!isKeyboardReachable(trigger) && !w.focusable) {\n w.focusable = true;\n console.error(\n \"[Tooltip] The trigger is not focusable; keyboard users cannot open the tooltip. Render a <button> or a link, or set tabIndex={0}.\",\n );\n }\n }\n if (popup !== null && popup.querySelector(FOCUSABLE) !== null && !w.interactive) {\n w.interactive = true;\n console.error(\n \"[Tooltip] Tooltips must not contain interactive content. Use a non-modal dialog or a popover instead.\",\n );\n }\n }, [trigger, popup, provides]);\n}\n"],"mappings":";;;;;;;AAkCA,MAAM,QAAQ,QAAQ,IAAI,aAAa;AAEvC,MAAM,YACJ;;;;;;;;;;AAWF,SAAgB,YAAY,EAC1B,QAAQ,KACR,aAAa,KACb,WAAW,OACX,WAAW,eACX,YACmB;CACnB,MAAM,UAAU,MAAM;CACtB,MAAM,CAAC,SAAS,qBAAqB,SAA6B,IAAI;CACtE,MAAM,CAAC,OAAO,mBAAmB,SAA6B,IAAI;CAClE,MAAM,CAAC,sBAAsB,2BAA2B,SAAS,KAAK;CAEtE,sBAAsB,SAAS,OAAO;EAAE;EAAO;EAAY;EAAU;CAAqB,CAAC;CAC3F,sBAAsB,SAAS,OAAO,QAAQ;CAE9C,MAAM,UAAU,eACP;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,IACA;EAAC;EAAS;EAAU;EAAO;EAAY;CAAQ,CACjD;CAGA,OAAO,oBAAC,gBAAD;EAAgB,OAAO;EAAU,UAAA,kBAAkB,UAAU,EADlC,SACsC,CAAC;CAAkB,CAAA;AAC7F;AAEA,YAAY,cAAc;;;;;;;AAQ1B,MAAM,YACJ;;;;;;AAOF,SAAS,oBAAoB,SAA+B;CAC1D,IAAI,QAAQ,YAAY,GAAG,OAAO;CAClC,OAAO,QAAQ,aAAa,UAAU,KAAK,QAAQ,eAAe,QAAQ,SAAS,KAAK;AAC1F;;AAGA,SAAS,sBACP,SACA,OACA,UACM;CACN,MAAM,SAAS,OAAO;EAAE,OAAO;EAAO,WAAW;EAAO,aAAa;CAAM,CAAC;CAC5E,gBAAgB;EACd,IAAI,CAAC,OAAO;EACZ,MAAM,IAAI,OAAO;EACjB,IAAI,YAAY,MAAM;GACpB,IAAI,aAAa,UAAU,QAAQ,aAAa,YAAY,KAAK,CAAC,EAAE,OAAO;IACzE,EAAE,QAAQ;IACV,QAAQ,MACN,kHACF;GACF;GACA,IAAI,CAAC,oBAAoB,OAAO,KAAK,CAAC,EAAE,WAAW;IACjD,EAAE,YAAY;IACd,QAAQ,MACN,mIACF;GACF;EACF;EACA,IAAI,UAAU,QAAQ,MAAM,cAAc,SAAS,MAAM,QAAQ,CAAC,EAAE,aAAa;GAC/E,EAAE,cAAc;GAChB,QAAQ,MACN,uGACF;EACF;CACF,GAAG;EAAC;EAAS;EAAO;CAAQ,CAAC;AAC/B"}
|