@intentface/ui 0.0.0 → 0.7.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.
- package/LICENSE +21 -0
- package/README.md +145 -2
- package/dist/ask-user/ask-user.d.ts +104 -0
- package/dist/ask-user/ask-user.js +245 -0
- package/dist/ask-user/index.d.ts +3 -0
- package/dist/ask-user/index.js +3 -0
- package/dist/ask-user/index.parts.d.ts +1 -0
- package/dist/ask-user/index.parts.js +5 -0
- package/dist/attachments/attachments.d.ts +73 -0
- package/dist/attachments/attachments.js +121 -0
- package/dist/attachments/index.d.ts +3 -0
- package/dist/attachments/index.js +3 -0
- package/dist/attachments/index.parts.d.ts +1 -0
- package/dist/attachments/index.parts.js +5 -0
- package/dist/chip/chip.d.ts +45 -0
- package/dist/chip/chip.js +43 -0
- package/dist/chip/index.d.ts +2 -0
- package/dist/chip/index.js +2 -0
- package/dist/chip/index.parts.d.ts +1 -0
- package/dist/chip/index.parts.js +5 -0
- package/dist/chip-markdown.d.ts +25 -0
- package/dist/chip-markdown.js +54 -0
- package/dist/composer/actions.d.ts +35 -0
- package/dist/composer/actions.js +76 -0
- package/dist/composer/ask-user-machine.d.ts +52 -0
- package/dist/composer/ask-user-machine.js +164 -0
- package/dist/composer/attachments-machine.d.ts +34 -0
- package/dist/composer/attachments-machine.js +91 -0
- package/dist/composer/attachments.d.ts +16 -0
- package/dist/composer/attachments.js +32 -0
- package/dist/composer/command-list.d.ts +49 -0
- package/dist/composer/command-list.js +360 -0
- package/dist/composer/commands.d.ts +28 -0
- package/dist/composer/commands.js +56 -0
- package/dist/composer/container.d.ts +3 -0
- package/dist/composer/container.js +32 -0
- package/dist/composer/controller.d.ts +3 -0
- package/dist/composer/controller.js +6 -0
- package/dist/composer/editor-dom.d.ts +57 -0
- package/dist/composer/editor-dom.js +517 -0
- package/dist/composer/fuzzy.d.ts +4 -0
- package/dist/composer/fuzzy.js +49 -0
- package/dist/composer/index.d.ts +25 -0
- package/dist/composer/index.js +13 -0
- package/dist/composer/index.parts.d.ts +11 -0
- package/dist/composer/index.parts.js +15 -0
- package/dist/composer/internals.d.ts +30 -0
- package/dist/composer/internals.js +102 -0
- package/dist/composer/keyboard.d.ts +61 -0
- package/dist/composer/keyboard.js +75 -0
- package/dist/composer/panel.d.ts +28 -0
- package/dist/composer/panel.js +76 -0
- package/dist/composer/placeholder.d.ts +10 -0
- package/dist/composer/placeholder.js +8 -0
- package/dist/composer/popover.d.ts +12 -0
- package/dist/composer/popover.js +62 -0
- package/dist/composer/prefix-detection.d.ts +23 -0
- package/dist/composer/prefix-detection.js +67 -0
- package/dist/composer/root.d.ts +21 -0
- package/dist/composer/root.js +124 -0
- package/dist/composer/segments.d.ts +108 -0
- package/dist/composer/segments.js +366 -0
- package/dist/composer/store.d.ts +98 -0
- package/dist/composer/store.js +457 -0
- package/dist/composer/textarea.d.ts +39 -0
- package/dist/composer/textarea.js +64 -0
- package/dist/composer/trigger-tracker.d.ts +12 -0
- package/dist/composer/trigger-tracker.js +90 -0
- package/dist/composer/types.d.ts +127 -0
- package/dist/composer/types.js +4 -0
- package/dist/composer/use-composer-editor.d.ts +43 -0
- package/dist/composer/use-composer-editor.js +653 -0
- package/dist/generative/catalog.d.ts +177 -0
- package/dist/generative/catalog.js +20 -0
- package/dist/generative/index.d.ts +26 -0
- package/dist/generative/index.js +20 -0
- package/dist/generative/json-schema.d.ts +45 -0
- package/dist/generative/json-schema.js +216 -0
- package/dist/generative/markdown.d.ts +21 -0
- package/dist/generative/markdown.js +76 -0
- package/dist/generative/tree.d.ts +56 -0
- package/dist/generative/tree.js +365 -0
- package/dist/generative/use-tree.d.ts +12 -0
- package/dist/generative/use-tree.js +64 -0
- package/dist/internal/change-event-details.d.ts +12 -0
- package/dist/internal/change-event-details.js +16 -0
- package/dist/internal/collapsible.d.ts +22 -0
- package/dist/internal/collapsible.js +94 -0
- package/dist/internal/collection.d.ts +32 -0
- package/dist/internal/collection.js +72 -0
- package/dist/internal/iso-layout-effect.d.ts +2 -0
- package/dist/internal/iso-layout-effect.js +6 -0
- package/dist/internal/portal.d.ts +6 -0
- package/dist/internal/portal.js +18 -0
- package/dist/internal/primitive-props.d.ts +4 -0
- package/dist/internal/primitive-props.js +3 -0
- package/dist/internal/render/baseTypes.d.ts +18 -0
- package/dist/internal/render/baseTypes.js +4 -0
- package/dist/internal/render/empty.d.ts +3 -0
- package/dist/internal/render/empty.js +5 -0
- package/dist/internal/render/getReactElementRef.d.ts +5 -0
- package/dist/internal/render/getReactElementRef.js +15 -0
- package/dist/internal/render/getStateAttributesProps.d.ts +4 -0
- package/dist/internal/render/getStateAttributesProps.js +23 -0
- package/dist/internal/render/mergeObjects.d.ts +1 -0
- package/dist/internal/render/mergeObjects.js +14 -0
- package/dist/internal/render/mergeProps.d.ts +55 -0
- package/dist/internal/render/mergeProps.js +183 -0
- package/dist/internal/render/positioning.d.ts +33 -0
- package/dist/internal/render/positioning.js +112 -0
- package/dist/internal/render/reactVersion.d.ts +3 -0
- package/dist/internal/render/reactVersion.js +7 -0
- package/dist/internal/render/resolveClassName.d.ts +8 -0
- package/dist/internal/render/resolveClassName.js +12 -0
- package/dist/internal/render/resolveStyle.d.ts +8 -0
- package/dist/internal/render/resolveStyle.js +12 -0
- package/dist/internal/render/transition.d.ts +47 -0
- package/dist/internal/render/transition.js +259 -0
- package/dist/internal/render/types.d.ts +74 -0
- package/dist/internal/render/types.js +3 -0
- package/dist/internal/render/useMergedRefs.d.ts +21 -0
- package/dist/internal/render/useMergedRefs.js +102 -0
- package/dist/internal/render/useRefWithInit.d.ts +10 -0
- package/dist/internal/render/useRefWithInit.js +12 -0
- package/dist/internal/render/useRender.d.ts +71 -0
- package/dist/internal/render/useRender.js +12 -0
- package/dist/internal/render/useRenderElement.d.ts +62 -0
- package/dist/internal/render/useRenderElement.js +157 -0
- package/dist/internal/render/warn.d.ts +1 -0
- package/dist/internal/render/warn.js +15 -0
- package/dist/internal/safe-polygon.d.ts +21 -0
- package/dist/internal/safe-polygon.js +179 -0
- package/dist/internal/scroll-into-view-within.d.ts +1 -0
- package/dist/internal/scroll-into-view-within.js +26 -0
- package/dist/internal/standard-schema.d.ts +38 -0
- package/dist/internal/standard-schema.js +4 -0
- package/dist/internal/state-mappings.d.ts +8 -0
- package/dist/internal/state-mappings.js +17 -0
- package/dist/internal/use-controlled.d.ts +19 -0
- package/dist/internal/use-controlled.js +33 -0
- package/dist/message/index.d.ts +3 -0
- package/dist/message/index.js +3 -0
- package/dist/message/index.parts.d.ts +1 -0
- package/dist/message/index.parts.js +5 -0
- package/dist/message/message.d.ts +58 -0
- package/dist/message/message.js +93 -0
- package/dist/message-utils.d.ts +55 -0
- package/dist/message-utils.js +79 -0
- package/dist/nav/index.d.ts +4 -0
- package/dist/nav/index.js +5 -0
- package/dist/nav/index.parts.d.ts +3 -0
- package/dist/nav/index.parts.js +7 -0
- package/dist/nav/nav.d.ts +152 -0
- package/dist/nav/nav.js +660 -0
- package/dist/nav/store.d.ts +50 -0
- package/dist/nav/store.js +95 -0
- package/dist/reasoning/index.d.ts +3 -0
- package/dist/reasoning/index.js +3 -0
- package/dist/reasoning/index.parts.d.ts +1 -0
- package/dist/reasoning/index.parts.js +5 -0
- package/dist/reasoning/reasoning.d.ts +38 -0
- package/dist/reasoning/reasoning.js +67 -0
- package/dist/shell/index.d.ts +5 -0
- package/dist/shell/index.js +5 -0
- package/dist/shell/index.parts.d.ts +3 -0
- package/dist/shell/index.parts.js +7 -0
- package/dist/shell/shell.d.ts +108 -0
- package/dist/shell/shell.js +390 -0
- package/dist/shell/store.d.ts +112 -0
- package/dist/shell/store.js +134 -0
- package/dist/steps/index.d.ts +2 -0
- package/dist/steps/index.js +2 -0
- package/dist/steps/index.parts.d.ts +1 -0
- package/dist/steps/index.parts.js +5 -0
- package/dist/steps/steps.d.ts +90 -0
- package/dist/steps/steps.js +116 -0
- package/dist/tabs/anchored.d.ts +59 -0
- package/dist/tabs/anchored.js +109 -0
- package/dist/tabs/hover.d.ts +33 -0
- package/dist/tabs/hover.js +169 -0
- package/dist/tabs/index.d.ts +5 -0
- package/dist/tabs/index.js +4 -0
- package/dist/tabs/index.parts.d.ts +4 -0
- package/dist/tabs/index.parts.js +8 -0
- package/dist/tabs/store.d.ts +131 -0
- package/dist/tabs/store.js +248 -0
- package/dist/tabs/tabs.d.ts +217 -0
- package/dist/tabs/tabs.js +458 -0
- package/dist/thread/geometry.d.ts +23 -0
- package/dist/thread/geometry.js +48 -0
- package/dist/thread/index.d.ts +4 -0
- package/dist/thread/index.js +3 -0
- package/dist/thread/index.parts.d.ts +1 -0
- package/dist/thread/index.parts.js +5 -0
- package/dist/thread/stores.d.ts +21 -0
- package/dist/thread/stores.js +63 -0
- package/dist/thread/thread.d.ts +57 -0
- package/dist/thread/thread.js +663 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +17 -0
- package/package.json +120 -4
package/dist/nav/nav.js
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, use, useCallback, useId, useMemo, useRef, useState, } from "react";
|
|
4
|
+
import { useIsomorphicLayoutEffect } from "../internal/iso-layout-effect.js";
|
|
5
|
+
import { useAnimationsFinished, useTransitionStatus, } from "../internal/render/transition.js";
|
|
6
|
+
import { useRenderElement } from "../internal/render/useRenderElement.js";
|
|
7
|
+
import { openStateMapping, transitionStatusMapping } from "../internal/state-mappings.js";
|
|
8
|
+
import { createNavStore, NavStoreContext, useNavContextStore, useNavStore } from "./store.js";
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Context
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/** The depth of the current list. Root provides 0; each list provides its own + 1. */
|
|
13
|
+
const NavDepthContext = createContext(0);
|
|
14
|
+
/** The nearest enclosing group. Null at the top level, which is what `nested` means. */
|
|
15
|
+
const NavGroupContext = createContext(null);
|
|
16
|
+
const NavRovingContext = createContext(null);
|
|
17
|
+
const useRoving = () => {
|
|
18
|
+
const roving = use(NavRovingContext);
|
|
19
|
+
if (!roving)
|
|
20
|
+
throw new Error("Nav parts must be used within <Nav.Root>");
|
|
21
|
+
return roving;
|
|
22
|
+
};
|
|
23
|
+
/*
|
|
24
|
+
* `depth: 0` and `open: false` are both falsy, and the default derivation drops
|
|
25
|
+
* falsy values — which would silently delete the attribute for the top level
|
|
26
|
+
* and for every closed group, the two cases most worth styling.
|
|
27
|
+
*/
|
|
28
|
+
const DEPTH_ATTRIBUTES = {
|
|
29
|
+
depth: (value) => ({ "data-depth": String(value) }),
|
|
30
|
+
};
|
|
31
|
+
/*
|
|
32
|
+
* One rung of state, three attributes — cumulative, because the rungs are.
|
|
33
|
+
* A stylesheet asking for `[data-rail]` gets branches too, which is what makes
|
|
34
|
+
* the ladder readable in CSS without `:is()` around every selector.
|
|
35
|
+
*/
|
|
36
|
+
const GUIDE_ATTRIBUTES = {
|
|
37
|
+
guide: (value) => {
|
|
38
|
+
if (value === "none")
|
|
39
|
+
return null;
|
|
40
|
+
return {
|
|
41
|
+
"data-indent": "",
|
|
42
|
+
...(value === "rail" || value === "branches" ? { "data-rail": "" } : {}),
|
|
43
|
+
...(value === "branches" ? { "data-branches": "" } : {}),
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const LIST_ATTRIBUTES = {
|
|
48
|
+
...DEPTH_ATTRIBUTES,
|
|
49
|
+
...openStateMapping,
|
|
50
|
+
...GUIDE_ATTRIBUTES,
|
|
51
|
+
...transitionStatusMapping,
|
|
52
|
+
};
|
|
53
|
+
/** Rows carry their value in the identity attribute — the DOM is the row order. */
|
|
54
|
+
const ROW_SELECTOR = "[data-nav-item],[data-nav-trigger]";
|
|
55
|
+
const visibleRows = (root) => Array.from(root.querySelectorAll(ROW_SELECTOR)).filter(
|
|
56
|
+
// A collapsed list unmounts, so its rows are already gone. `keepMounted`
|
|
57
|
+
// leaves them in the DOM behind `hidden`, where they are not navigable.
|
|
58
|
+
(row) => !row.closest("[data-nav-list][hidden]"));
|
|
59
|
+
const rowValue = (row) => row.getAttribute("data-nav-item") ?? row.getAttribute("data-nav-trigger");
|
|
60
|
+
/** A field inside the nav owns its own arrow keys, or the caret never moves. */
|
|
61
|
+
const ownsTheKeys = (target) => target instanceof HTMLElement &&
|
|
62
|
+
(target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName));
|
|
63
|
+
/**
|
|
64
|
+
* The provider and container. Owns the keyboard handling for the whole tree,
|
|
65
|
+
* so roving focus and typeahead work without any row registering itself.
|
|
66
|
+
* Renders a `<div>` element.
|
|
67
|
+
*/
|
|
68
|
+
export const NavRoot = ({ defaultExpanded, expanded, onExpandedChange, store: storeProp, guide = "none", loop = false, disabled = false, typeahead = true, className, render, style, ...elementProps }) => {
|
|
69
|
+
const [fallback] = useState(createNavStore);
|
|
70
|
+
const store = storeProp ?? fallback;
|
|
71
|
+
const [highlighted, setHighlighted] = useState(null);
|
|
72
|
+
const search = useRef({ query: "", at: 0 });
|
|
73
|
+
store.controlledRef.current = expanded !== undefined;
|
|
74
|
+
store.onExpandedChangeRef.current = onExpandedChange ?? null;
|
|
75
|
+
// Applied before the first paint rather than in an effect: a group that opens
|
|
76
|
+
// one frame late is the flash this whole storage contract exists to avoid.
|
|
77
|
+
const hydrated = useRef(false);
|
|
78
|
+
if (!hydrated.current) {
|
|
79
|
+
hydrated.current = true;
|
|
80
|
+
store.hydrate({ expanded: expanded ?? defaultExpanded });
|
|
81
|
+
}
|
|
82
|
+
// A controlled Root's prop is the state, so keep the snapshot following it.
|
|
83
|
+
// `notify` here and nowhere else: this is the one hydrate that runs after
|
|
84
|
+
// mount, where a memoised subtree would otherwise never learn of the change.
|
|
85
|
+
useIsomorphicLayoutEffect(() => {
|
|
86
|
+
if (expanded === undefined)
|
|
87
|
+
return;
|
|
88
|
+
store.hydrate({ expanded }, { notify: true });
|
|
89
|
+
}, [expanded, store]);
|
|
90
|
+
const move = useCallback((rows, index) => {
|
|
91
|
+
const row = rows[index];
|
|
92
|
+
if (!row)
|
|
93
|
+
return;
|
|
94
|
+
setHighlighted(rowValue(row));
|
|
95
|
+
row.focus();
|
|
96
|
+
}, []);
|
|
97
|
+
const handleKeyDown = (event) => {
|
|
98
|
+
const root = store.rootRef.current;
|
|
99
|
+
if (!root || ownsTheKeys(event.target))
|
|
100
|
+
return;
|
|
101
|
+
const current = event.target.closest(ROW_SELECTOR);
|
|
102
|
+
if (!current)
|
|
103
|
+
return;
|
|
104
|
+
const rows = visibleRows(root);
|
|
105
|
+
const index = rows.indexOf(current);
|
|
106
|
+
if (index === -1)
|
|
107
|
+
return;
|
|
108
|
+
const last = rows.length - 1;
|
|
109
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
110
|
+
const delta = event.key === "ArrowDown" ? 1 : -1;
|
|
111
|
+
const next = index + delta;
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
move(rows, loop ? (next + rows.length) % rows.length : Math.min(Math.max(next, 0), last));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (event.key === "Home" || event.key === "End") {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
move(rows, event.key === "Home" ? 0 : last);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
|
|
122
|
+
const forward = event.key === "ArrowRight";
|
|
123
|
+
const trigger = current.getAttribute("data-nav-trigger");
|
|
124
|
+
// From the store, not aria-expanded: a page row hands that to its Nav.Toggle.
|
|
125
|
+
const isOpen = trigger !== null && store.getSnapshot().expanded.has(trigger);
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
// On a group: open it, then step into it. On a leaf, or a group already
|
|
128
|
+
// closed: step out to the parent — which is where the row came from.
|
|
129
|
+
if (trigger !== null && forward !== isOpen) {
|
|
130
|
+
store.getSnapshot().setOpen(trigger, forward);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (forward) {
|
|
134
|
+
if (trigger !== null)
|
|
135
|
+
move(rows, index + 1);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const parent = current.parentElement?.closest("[data-nav-group]");
|
|
139
|
+
const parentTrigger = parent?.querySelector("[data-nav-trigger]");
|
|
140
|
+
if (!parentTrigger)
|
|
141
|
+
return;
|
|
142
|
+
setHighlighted(rowValue(parentTrigger));
|
|
143
|
+
parentTrigger.focus();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Typeahead. Single printable characters only, so shortcuts still work.
|
|
147
|
+
if (!typeahead || event.key.length !== 1 || event.metaKey || event.ctrlKey || event.altKey) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
search.current = {
|
|
152
|
+
query: (now - search.current.at > 500 ? "" : search.current.query) + event.key.toLowerCase(),
|
|
153
|
+
at: now,
|
|
154
|
+
};
|
|
155
|
+
// Search from the row after the current one, so repeating a letter walks
|
|
156
|
+
// through the matches rather than sticking to the first.
|
|
157
|
+
const ordered = [...rows.slice(index + 1), ...rows.slice(0, index + 1)];
|
|
158
|
+
const match = ordered.find((row) => (row.textContent ?? "").trim().toLowerCase().startsWith(search.current.query));
|
|
159
|
+
if (!match)
|
|
160
|
+
return;
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
setHighlighted(rowValue(match));
|
|
163
|
+
match.focus();
|
|
164
|
+
};
|
|
165
|
+
/*
|
|
166
|
+
* A collapsed group unmounts its rows, so the highlighted one can simply
|
|
167
|
+
* cease to exist — and a tab stop pointing at nothing leaves the whole nav
|
|
168
|
+
* unreachable by keyboard. Re-home it on the first row instead. The same
|
|
169
|
+
* effect seeds the initial value, since before it runs the tab stop is on
|
|
170
|
+
* whichever row is active, and there may not be one.
|
|
171
|
+
*/
|
|
172
|
+
useIsomorphicLayoutEffect(() => {
|
|
173
|
+
const root = store.rootRef.current;
|
|
174
|
+
if (!root)
|
|
175
|
+
return;
|
|
176
|
+
const rows = visibleRows(root);
|
|
177
|
+
if (highlighted !== null && rows.some((row) => rowValue(row) === highlighted))
|
|
178
|
+
return;
|
|
179
|
+
const first = rows[0];
|
|
180
|
+
setHighlighted(first ? rowValue(first) : null);
|
|
181
|
+
});
|
|
182
|
+
const roving = useMemo(() => ({ highlighted, setHighlighted, disabled, guide }), [highlighted, disabled, guide]);
|
|
183
|
+
const element = useRenderElement("div", { className, render, style }, {
|
|
184
|
+
state: { depth: 0, nested: false },
|
|
185
|
+
stateAttributesMapping: DEPTH_ATTRIBUTES,
|
|
186
|
+
ref: store.rootRef,
|
|
187
|
+
props: [{ "data-nav": "", onKeyDown: handleKeyDown }, elementProps],
|
|
188
|
+
});
|
|
189
|
+
return (_jsx(NavStoreContext, { value: store, children: _jsx(NavRovingContext, { value: roving, children: element }) }));
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* The collection of rows — at the top level, and again inside every group.
|
|
193
|
+
* One part for both is what makes the tree recursive: a group holds a list,
|
|
194
|
+
* and a list holds groups.
|
|
195
|
+
*/
|
|
196
|
+
/**
|
|
197
|
+
* Measure the content's true height, with every pinned height lifted.
|
|
198
|
+
*
|
|
199
|
+
* Two reasons the lift cannot stop at this element.
|
|
200
|
+
*
|
|
201
|
+
* Reading `scrollHeight` through its own pinned height is a ratchet: with
|
|
202
|
+
* `overflow: hidden` it returns whichever of the content and the box is larger,
|
|
203
|
+
* so a value once too big measures itself as too big forever.
|
|
204
|
+
*
|
|
205
|
+
* And a *descendant* list may be pinned too — mid-transition, or holding a
|
|
206
|
+
* length it has not released yet. Measuring around one means the parent adopts
|
|
207
|
+
* a height that its content does not actually want, and jumps to the real one
|
|
208
|
+
* the moment the descendant releases. A recursive tree makes that the ordinary
|
|
209
|
+
* case rather than the edge case, which is why this goes further than the
|
|
210
|
+
* single-panel version it is modelled on.
|
|
211
|
+
*/
|
|
212
|
+
const measure = (element) => {
|
|
213
|
+
const pinned = [element, ...element.querySelectorAll("[data-nav-list]")];
|
|
214
|
+
const restore = pinned.map((node) => {
|
|
215
|
+
const height = node.style.height;
|
|
216
|
+
const priority = node.style.getPropertyPriority("height");
|
|
217
|
+
node.style.setProperty("height", "auto", "important");
|
|
218
|
+
return () => {
|
|
219
|
+
if (height)
|
|
220
|
+
node.style.setProperty("height", height, priority);
|
|
221
|
+
else
|
|
222
|
+
node.style.removeProperty("height");
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
const size = { height: element.scrollHeight, width: element.scrollWidth };
|
|
226
|
+
for (const put of restore)
|
|
227
|
+
put();
|
|
228
|
+
return size;
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* A level of the tree. A list inside a group is that group's collapsible
|
|
232
|
+
* panel; a top-level list is not collapsible, having no trigger above it.
|
|
233
|
+
* Renders a `<div>` element.
|
|
234
|
+
*/
|
|
235
|
+
export const NavList = ({ guide, keepMounted = false, className, render, style, ...elementProps }) => {
|
|
236
|
+
const depth = use(NavDepthContext);
|
|
237
|
+
const group = use(NavGroupContext);
|
|
238
|
+
const { guide: fallback } = useRoving();
|
|
239
|
+
const resolved = guide ?? fallback;
|
|
240
|
+
const listRef = useRef(null);
|
|
241
|
+
// Watched with its subtree, because the transition may be on the list or on
|
|
242
|
+
// the group wrapping it, and the primitive does not get to decide which.
|
|
243
|
+
const runWhenAnimationsFinish = useAnimationsFinished(group?.elementRef ?? listRef, false, true, true);
|
|
244
|
+
// A top-level list is not collapsible: there is no trigger above it.
|
|
245
|
+
const open = group ? group.open : true;
|
|
246
|
+
const { mounted, setMounted, transitionStatus } = useTransitionStatus(open);
|
|
247
|
+
/*
|
|
248
|
+
* The measured size, and only while a transition needs one.
|
|
249
|
+
*
|
|
250
|
+
* `height: auto` is not interpolable, so a collapse can only animate between
|
|
251
|
+
* lengths — but a list pinned to a length *permanently* cannot hold a group
|
|
252
|
+
* that expands inside it, and would clip the thing that just opened. So the
|
|
253
|
+
* pin is temporary: taken when a transition starts, released the moment the
|
|
254
|
+
* opening one finishes. An open, settled list is `auto` and grows freely.
|
|
255
|
+
*
|
|
256
|
+
* Base UI's Collapsible does exactly this, and it is the half of the
|
|
257
|
+
* behaviour that is invisible until you nest one inside another.
|
|
258
|
+
*/
|
|
259
|
+
const [size, setSize] = useState(null);
|
|
260
|
+
/*
|
|
261
|
+
* The closing styles, one frame late.
|
|
262
|
+
*
|
|
263
|
+
* `useTransitionStatus` reports `ending` in the same render that closes the
|
|
264
|
+
* list, so pinning the height and setting `height: 0` would land together and
|
|
265
|
+
* the browser would have nothing to transition between. Holding the styles
|
|
266
|
+
* back for a frame lets the pinned length paint first.
|
|
267
|
+
*/
|
|
268
|
+
const [closing, setClosing] = useState(false);
|
|
269
|
+
useIsomorphicLayoutEffect(() => {
|
|
270
|
+
const element = listRef.current;
|
|
271
|
+
if (!element || !mounted || transitionStatus === undefined)
|
|
272
|
+
return;
|
|
273
|
+
setSize(measure(element));
|
|
274
|
+
}, [transitionStatus, mounted]);
|
|
275
|
+
/*
|
|
276
|
+
* Keep the pin honest while it holds.
|
|
277
|
+
*
|
|
278
|
+
* A pinned length is a snapshot, and the content can change after it is
|
|
279
|
+
* taken — a nested list settling, an icon font arriving, a row wrapping to
|
|
280
|
+
* two lines. Nothing there involves a React render, so nothing re-measures,
|
|
281
|
+
* and a pin that ends up *short* strands the last row behind the clip for the
|
|
282
|
+
* whole transition. It then appears the instant the pin is released, which
|
|
283
|
+
* reads as the row popping in from the bottom rather than being revealed.
|
|
284
|
+
*
|
|
285
|
+
* Observing the children rather than the list: the list's own box is pinned,
|
|
286
|
+
* so it is precisely the thing that cannot report the change.
|
|
287
|
+
*/
|
|
288
|
+
useIsomorphicLayoutEffect(() => {
|
|
289
|
+
const element = listRef.current;
|
|
290
|
+
if (!element || size === null || typeof ResizeObserver === "undefined")
|
|
291
|
+
return;
|
|
292
|
+
const observer = new ResizeObserver(() => {
|
|
293
|
+
const next = measure(element);
|
|
294
|
+
// Only when it actually moved, or the observer re-triggers on its own
|
|
295
|
+
// write and never settles.
|
|
296
|
+
setSize((current) => current && current.height === next.height && current.width === next.width ? current : next);
|
|
297
|
+
});
|
|
298
|
+
for (const child of element.children)
|
|
299
|
+
observer.observe(child);
|
|
300
|
+
return () => observer.disconnect();
|
|
301
|
+
}, [size]);
|
|
302
|
+
useIsomorphicLayoutEffect(() => {
|
|
303
|
+
if (transitionStatus !== "ending") {
|
|
304
|
+
setClosing(false);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const frame = requestAnimationFrame(() => setClosing(true));
|
|
308
|
+
return () => cancelAnimationFrame(frame);
|
|
309
|
+
}, [transitionStatus]);
|
|
310
|
+
/*
|
|
311
|
+
* Released once the opening transition settles, so an ancestor is free to
|
|
312
|
+
* grow when a group inside it opens.
|
|
313
|
+
*
|
|
314
|
+
* Keyed off this element's own `height` transition ending rather than off
|
|
315
|
+
* "are any animations running", because at the moment this effect first runs
|
|
316
|
+
* the transition has been triggered in the same commit but not yet
|
|
317
|
+
* registered — so a poll can find nothing, release immediately, and turn the
|
|
318
|
+
* whole reveal into a snap to `auto`. `transitionend` cannot fire early by
|
|
319
|
+
* construction.
|
|
320
|
+
*
|
|
321
|
+
* The animation-frame path stays as the fallback for a consumer who animates
|
|
322
|
+
* with keyframes, or does not animate at all, where no `transitionend` is
|
|
323
|
+
* ever coming.
|
|
324
|
+
*/
|
|
325
|
+
useIsomorphicLayoutEffect(() => {
|
|
326
|
+
const element = listRef.current;
|
|
327
|
+
if (!element || !open || !mounted || transitionStatus !== undefined || size === null)
|
|
328
|
+
return;
|
|
329
|
+
const controller = new AbortController();
|
|
330
|
+
const release = () => setSize(null);
|
|
331
|
+
const onTransitionEnd = (event) => {
|
|
332
|
+
if (event.target !== element || event.propertyName !== "height")
|
|
333
|
+
return;
|
|
334
|
+
release();
|
|
335
|
+
};
|
|
336
|
+
element.addEventListener("transitionend", onTransitionEnd, { signal: controller.signal });
|
|
337
|
+
// Nothing to wait for if no height transition was declared; the frame path
|
|
338
|
+
// resolves immediately in that case, which is the right answer.
|
|
339
|
+
if (getComputedStyle(element)
|
|
340
|
+
.transitionProperty.split(",")
|
|
341
|
+
.every((property) => {
|
|
342
|
+
const name = property.trim();
|
|
343
|
+
return name !== "height" && name !== "all";
|
|
344
|
+
})) {
|
|
345
|
+
runWhenAnimationsFinish(release, controller.signal);
|
|
346
|
+
}
|
|
347
|
+
return () => controller.abort();
|
|
348
|
+
}, [open, mounted, transitionStatus, size, runWhenAnimationsFinish]);
|
|
349
|
+
// Waits on `closing` rather than on `transitionStatus`, because at the moment the
|
|
350
|
+
// transitionStatus flips there is no animation running yet to wait for.
|
|
351
|
+
useIsomorphicLayoutEffect(() => {
|
|
352
|
+
if (!closing)
|
|
353
|
+
return;
|
|
354
|
+
const controller = new AbortController();
|
|
355
|
+
runWhenAnimationsFinish(() => setMounted(false), controller.signal);
|
|
356
|
+
return () => controller.abort();
|
|
357
|
+
}, [closing, runWhenAnimationsFinish, setMounted]);
|
|
358
|
+
const hidden = !open && !mounted;
|
|
359
|
+
// `until-found` is a string the DOM understands and React's boolean `hidden`
|
|
360
|
+
// cannot express, so a kept list gets the attribute imperatively. Without it
|
|
361
|
+
// the list stays navigable while collapsed and roving focus walks into rows
|
|
362
|
+
// the visitor just closed.
|
|
363
|
+
useIsomorphicLayoutEffect(() => {
|
|
364
|
+
const element = listRef.current;
|
|
365
|
+
if (!element || !keepMounted)
|
|
366
|
+
return;
|
|
367
|
+
if (hidden)
|
|
368
|
+
element.setAttribute("hidden", "until-found");
|
|
369
|
+
else
|
|
370
|
+
element.removeAttribute("hidden");
|
|
371
|
+
}, [keepMounted, hidden]);
|
|
372
|
+
// `ending` is withheld until the pinned height has had a frame to paint.
|
|
373
|
+
const published = transitionStatus === "ending" && !closing ? undefined : transitionStatus;
|
|
374
|
+
const element = useRenderElement("div", { className, render, style }, {
|
|
375
|
+
state: {
|
|
376
|
+
depth,
|
|
377
|
+
nested: group !== null,
|
|
378
|
+
guide: resolved,
|
|
379
|
+
open,
|
|
380
|
+
transitionStatus: published,
|
|
381
|
+
},
|
|
382
|
+
stateAttributesMapping: LIST_ATTRIBUTES,
|
|
383
|
+
ref: listRef,
|
|
384
|
+
props: [
|
|
385
|
+
{
|
|
386
|
+
"data-nav-list": "",
|
|
387
|
+
id: group?.listId,
|
|
388
|
+
// A kept list gets `hidden="until-found"` from the effect above; React
|
|
389
|
+
// can only express the boolean form, so it must not fight over it.
|
|
390
|
+
hidden: keepMounted ? undefined : hidden,
|
|
391
|
+
style: {
|
|
392
|
+
"--nav-depth": depth,
|
|
393
|
+
/*
|
|
394
|
+
* `initial` rather than absent, and that distinction is the whole
|
|
395
|
+
* bug it fixes.
|
|
396
|
+
*
|
|
397
|
+
* Custom properties inherit. Leaving this off a settled list lets it
|
|
398
|
+
* pick up an *ancestor's* pinned length — and since the rule that
|
|
399
|
+
* consumes it is `height: var(--nav-list-height)`, a nested list
|
|
400
|
+
* would take its grandparent's height instead of its own content's,
|
|
401
|
+
* shoving everything after it far down the page and snapping back the
|
|
402
|
+
* moment the ancestor releases.
|
|
403
|
+
*
|
|
404
|
+
* `initial` on an unregistered custom property is the
|
|
405
|
+
* guaranteed-invalid value, so `var()` fails, the declaration is
|
|
406
|
+
* invalid at computed-value time, and `height` lands on `auto` —
|
|
407
|
+
* which is what a settled list wants, without inheriting anything.
|
|
408
|
+
*/
|
|
409
|
+
"--nav-list-height": size ? `${size.height}px` : "initial",
|
|
410
|
+
"--nav-list-width": size ? `${size.width}px` : "initial",
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
elementProps,
|
|
414
|
+
],
|
|
415
|
+
});
|
|
416
|
+
if (!mounted && !keepMounted)
|
|
417
|
+
return null;
|
|
418
|
+
return _jsx(NavDepthContext, { value: depth + 1, children: element });
|
|
419
|
+
};
|
|
420
|
+
/**
|
|
421
|
+
* A collapsible branch. Recursive by construction — a group's list may hold
|
|
422
|
+
* further groups, to any depth, with no second set of parts for the nesting.
|
|
423
|
+
*/
|
|
424
|
+
export const NavGroup = ({ value, disabled = false, className, render, style, ...elementProps }) => {
|
|
425
|
+
const store = useNavContextStore();
|
|
426
|
+
const depth = use(NavDepthContext);
|
|
427
|
+
const parent = use(NavGroupContext);
|
|
428
|
+
const { disabled: rootDisabled } = useRoving();
|
|
429
|
+
const listId = useId();
|
|
430
|
+
const elementRef = useRef(null);
|
|
431
|
+
const open = useNavStore(store, (nav) => nav.expanded.has(value));
|
|
432
|
+
const context = useMemo(() => ({ value, open, listId, disabled: disabled || rootDisabled, elementRef }), [value, open, listId, disabled, rootDisabled]);
|
|
433
|
+
const element = useRenderElement("div", { className, render, style }, {
|
|
434
|
+
state: { depth, nested: parent !== null, open },
|
|
435
|
+
stateAttributesMapping: { ...DEPTH_ATTRIBUTES, ...openStateMapping },
|
|
436
|
+
ref: elementRef,
|
|
437
|
+
props: [{ "data-nav-group": value }, elementProps],
|
|
438
|
+
});
|
|
439
|
+
return _jsx(NavGroupContext, { value: context, children: element });
|
|
440
|
+
};
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// Rows — Item and Trigger
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
/** Elements the browser already activates on Enter or Space. */
|
|
445
|
+
/** A button handles Enter and Space itself; a link handles only Enter. */
|
|
446
|
+
const activatesItself = (element, key) => element.tagName === "BUTTON" ||
|
|
447
|
+
(key === "Enter" && element.tagName === "A" && element.hasAttribute("href"));
|
|
448
|
+
/**
|
|
449
|
+
* What `Item` and `Trigger` have in common: one tab stop for the whole nav,
|
|
450
|
+
* focus that follows the arrows, and hand-wired Enter/Space — the price of not
|
|
451
|
+
* being a real `<button>`, which is the price of holding an action inside.
|
|
452
|
+
*/
|
|
453
|
+
const useRow = (value, disabled, active, activate) => {
|
|
454
|
+
const { highlighted, setHighlighted } = useRoving();
|
|
455
|
+
return {
|
|
456
|
+
role: "button",
|
|
457
|
+
// Exactly one row is tabbable. Before the roving state is seeded — a server
|
|
458
|
+
// render, or the frame before hydration — the active row carries it, which
|
|
459
|
+
// is both the conventional place for it and the one that keeps a row
|
|
460
|
+
// rendered as a real anchor reachable without JavaScript.
|
|
461
|
+
tabIndex: disabled ? -1 : (highlighted === null ? active : highlighted === value) ? 0 : -1,
|
|
462
|
+
"aria-disabled": disabled || undefined,
|
|
463
|
+
// `active` is the route being shown; your own `aria-current` still overrides it.
|
|
464
|
+
"aria-current": active ? "page" : undefined,
|
|
465
|
+
onFocus: () => setHighlighted(value),
|
|
466
|
+
onClick: disabled ? (event) => event.preventDefault() : undefined,
|
|
467
|
+
onKeyDown: (event) => {
|
|
468
|
+
if (event.key !== "Enter" && event.key !== " ")
|
|
469
|
+
return;
|
|
470
|
+
if (event.target !== event.currentTarget)
|
|
471
|
+
return;
|
|
472
|
+
if (activatesItself(event.currentTarget, event.key))
|
|
473
|
+
return;
|
|
474
|
+
// Space scrolls the page otherwise, and the row is the thing being
|
|
475
|
+
// pressed — not the document behind it.
|
|
476
|
+
event.preventDefault();
|
|
477
|
+
if (!disabled)
|
|
478
|
+
activate();
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
483
|
+
* A leaf row. A `div[role="button"]` rather than an anchor, so an action can
|
|
484
|
+
* sit inside it — swap the element with `render={<Link href="…" />}` when you
|
|
485
|
+
* want a real navigation, or handle `onClick` and route yourself.
|
|
486
|
+
*/
|
|
487
|
+
export const NavItem = ({ value, active = false, disabled, className, render, style, ...elementProps }) => {
|
|
488
|
+
const depth = use(NavDepthContext);
|
|
489
|
+
const group = use(NavGroupContext);
|
|
490
|
+
const { disabled: rootDisabled } = useRoving();
|
|
491
|
+
const isDisabled = disabled ?? rootDisabled;
|
|
492
|
+
const ref = useRef(null);
|
|
493
|
+
const row = useRow(value, isDisabled, active, () => ref.current?.click());
|
|
494
|
+
return useRenderElement("div", { className, render, style }, {
|
|
495
|
+
state: { depth, nested: group !== null, active, disabled: isDisabled },
|
|
496
|
+
stateAttributesMapping: DEPTH_ATTRIBUTES,
|
|
497
|
+
ref,
|
|
498
|
+
props: [{ ...row, "data-nav-item": value }, elementProps],
|
|
499
|
+
});
|
|
500
|
+
};
|
|
501
|
+
/** The trigger a `Nav.Toggle` sits in: its disabled state, and how the caret announces itself. */
|
|
502
|
+
const NavTriggerContext = createContext(null);
|
|
503
|
+
/**
|
|
504
|
+
* The row that opens a group — its heading and its disclosure in one, because
|
|
505
|
+
* in a sidebar they are the same thing you click. Takes no `value`: it belongs
|
|
506
|
+
* to the group it is written inside.
|
|
507
|
+
*
|
|
508
|
+
* Put a `Nav.Toggle` inside it and the two come apart: pressing the row
|
|
509
|
+
* activates it like `Nav.Item`, and the caret opens the group and carries
|
|
510
|
+
* `aria-expanded`. The arrow keys open and close it from the row either way.
|
|
511
|
+
*/
|
|
512
|
+
export const NavTrigger = ({ active = false, disabled, className, render, style, ...elementProps }) => {
|
|
513
|
+
const store = useNavContextStore();
|
|
514
|
+
const depth = use(NavDepthContext);
|
|
515
|
+
const group = use(NavGroupContext);
|
|
516
|
+
if (!group)
|
|
517
|
+
throw new Error("<Nav.Trigger> must be used within <Nav.Group>");
|
|
518
|
+
const ref = useRef(null);
|
|
519
|
+
const isDisabled = disabled ?? group.disabled;
|
|
520
|
+
const toggle = () => store.getSnapshot().toggle(group.value);
|
|
521
|
+
// A row holding a Nav.Toggle is a page: a press activates it, and the caret is the disclosure.
|
|
522
|
+
const [hasToggle, setHasToggle] = useState(false);
|
|
523
|
+
// Enter and Space activate a page row as a click would, reaching your onClick or the link.
|
|
524
|
+
const row = useRow(group.value, isDisabled, active, () => hasToggle ? ref.current?.click() : toggle());
|
|
525
|
+
const context = useMemo(() => ({ disabled: isDisabled, setHasToggle }), [isDisabled]);
|
|
526
|
+
const element = useRenderElement("div", { className, render, style }, {
|
|
527
|
+
state: {
|
|
528
|
+
depth,
|
|
529
|
+
nested: depth > 0,
|
|
530
|
+
open: group.open,
|
|
531
|
+
active,
|
|
532
|
+
disabled: isDisabled,
|
|
533
|
+
},
|
|
534
|
+
stateAttributesMapping: { ...DEPTH_ATTRIBUTES, ...openStateMapping },
|
|
535
|
+
ref,
|
|
536
|
+
props: [
|
|
537
|
+
{
|
|
538
|
+
...row,
|
|
539
|
+
"data-nav-trigger": group.value,
|
|
540
|
+
// On a page row the caret announces the open state, not the row.
|
|
541
|
+
"aria-expanded": hasToggle ? undefined : group.open,
|
|
542
|
+
"aria-controls": hasToggle ? undefined : group.listId,
|
|
543
|
+
onClick: (event) => {
|
|
544
|
+
if (isDisabled)
|
|
545
|
+
event.preventDefault();
|
|
546
|
+
else if (!hasToggle)
|
|
547
|
+
toggle();
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
elementProps,
|
|
551
|
+
],
|
|
552
|
+
});
|
|
553
|
+
return _jsx(NavTriggerContext, { value: context, children: element });
|
|
554
|
+
};
|
|
555
|
+
/**
|
|
556
|
+
* The caret inside a row — the control that opens a branch whose row is also a
|
|
557
|
+
* page. Mounting it makes pressing the row activate the page. Out of the roving
|
|
558
|
+
* order, like `Nav.Action`, since the arrow keys already open the group from
|
|
559
|
+
* the row, and it stops every event it handles: anything that escaped would
|
|
560
|
+
* activate the row, or follow its link, on its way out of opening the group.
|
|
561
|
+
*/
|
|
562
|
+
export const NavToggle = ({ className, render, style, ...elementProps }) => {
|
|
563
|
+
const store = useNavContextStore();
|
|
564
|
+
const depth = use(NavDepthContext);
|
|
565
|
+
const group = use(NavGroupContext);
|
|
566
|
+
const trigger = use(NavTriggerContext);
|
|
567
|
+
if (!group || !trigger)
|
|
568
|
+
throw new Error("<Nav.Toggle> must be used within <Nav.Trigger>");
|
|
569
|
+
// Mounting it is what makes its row a page.
|
|
570
|
+
const { setHasToggle } = trigger;
|
|
571
|
+
const ref = useCallback((element) => {
|
|
572
|
+
if (!element)
|
|
573
|
+
return;
|
|
574
|
+
setHasToggle(true);
|
|
575
|
+
return () => setHasToggle(false);
|
|
576
|
+
}, [setHasToggle]);
|
|
577
|
+
const stop = (event) => {
|
|
578
|
+
event.stopPropagation();
|
|
579
|
+
};
|
|
580
|
+
const toggle = (event) => {
|
|
581
|
+
stop(event);
|
|
582
|
+
event.preventDefault();
|
|
583
|
+
if (!trigger.disabled)
|
|
584
|
+
store.getSnapshot().toggle(group.value);
|
|
585
|
+
};
|
|
586
|
+
return useRenderElement("div", { className, render, style }, {
|
|
587
|
+
state: { depth, nested: depth > 0, open: group.open, disabled: trigger.disabled },
|
|
588
|
+
stateAttributesMapping: { ...DEPTH_ATTRIBUTES, ...openStateMapping },
|
|
589
|
+
ref,
|
|
590
|
+
props: [
|
|
591
|
+
{
|
|
592
|
+
"data-nav-toggle": "",
|
|
593
|
+
role: "button",
|
|
594
|
+
tabIndex: -1,
|
|
595
|
+
"aria-expanded": group.open,
|
|
596
|
+
"aria-controls": group.listId,
|
|
597
|
+
"aria-disabled": trigger.disabled || undefined,
|
|
598
|
+
onClick: toggle,
|
|
599
|
+
onPointerDown: stop,
|
|
600
|
+
onDoubleClick: stop,
|
|
601
|
+
onKeyDown: (event) => {
|
|
602
|
+
if (event.key === "Enter" || event.key === " ")
|
|
603
|
+
toggle(event);
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
elementProps,
|
|
607
|
+
],
|
|
608
|
+
});
|
|
609
|
+
};
|
|
610
|
+
/** The row's text. Its own element so it can truncate while the row does not. */
|
|
611
|
+
export const NavLabel = ({ className, render, style, ...elementProps }) => {
|
|
612
|
+
const depth = use(NavDepthContext);
|
|
613
|
+
const group = use(NavGroupContext);
|
|
614
|
+
return useRenderElement("span", { className, render, style }, {
|
|
615
|
+
state: { depth, nested: group !== null },
|
|
616
|
+
stateAttributesMapping: DEPTH_ATTRIBUTES,
|
|
617
|
+
props: [{ "data-nav-label": "" }, elementProps],
|
|
618
|
+
});
|
|
619
|
+
};
|
|
620
|
+
/** Decoration. `aria-hidden`, so it stays out of the row's accessible name. */
|
|
621
|
+
export const NavIcon = ({ className, render, style, ...elementProps }) => {
|
|
622
|
+
const depth = use(NavDepthContext);
|
|
623
|
+
const group = use(NavGroupContext);
|
|
624
|
+
return useRenderElement("span", { className, render, style }, {
|
|
625
|
+
state: { depth, nested: group !== null },
|
|
626
|
+
stateAttributesMapping: DEPTH_ATTRIBUTES,
|
|
627
|
+
props: [{ "data-nav-icon": "", "aria-hidden": true }, elementProps],
|
|
628
|
+
});
|
|
629
|
+
};
|
|
630
|
+
/**
|
|
631
|
+
* A control inside a row — the "…" that opens a menu. Out of the roving order,
|
|
632
|
+
* so arrowing walks rows rather than stopping at every affordance, and it stops
|
|
633
|
+
* every event it handles: anything that escaped would activate the row on its
|
|
634
|
+
* way out of opening the menu.
|
|
635
|
+
*/
|
|
636
|
+
export const NavAction = ({ className, render, style, ...elementProps }) => {
|
|
637
|
+
const depth = use(NavDepthContext);
|
|
638
|
+
const group = use(NavGroupContext);
|
|
639
|
+
const stop = (event) => {
|
|
640
|
+
event.stopPropagation();
|
|
641
|
+
};
|
|
642
|
+
return useRenderElement("div", { className, render, style }, {
|
|
643
|
+
state: { depth, nested: group !== null },
|
|
644
|
+
stateAttributesMapping: DEPTH_ATTRIBUTES,
|
|
645
|
+
props: [
|
|
646
|
+
{
|
|
647
|
+
"data-nav-action": "",
|
|
648
|
+
role: "button",
|
|
649
|
+
tabIndex: -1,
|
|
650
|
+
onClick: stop,
|
|
651
|
+
onPointerDown: stop,
|
|
652
|
+
onKeyDown: (event) => {
|
|
653
|
+
if (event.key === "Enter" || event.key === " ")
|
|
654
|
+
event.stopPropagation();
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
elementProps,
|
|
658
|
+
],
|
|
659
|
+
});
|
|
660
|
+
};
|