@fluixi-ui/navigation-menu 0.2.0-alpha.18 → 0.2.0-alpha.20

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.
@@ -1,428 +1,564 @@
1
- import { delegateEvents, splitProps, createComponent, createNativeElement, spread, mergeProps, insert, templateNode, holeEnd, holeScope, holeContent, handleRef } from "@fluixi/dom";
2
- import { createSignal, onCleanup, batch, createMemo, createRenderEffect, createEffect } from "@fluixi/reactive/signal";
1
+ import { createComponent, createNativeElement, delegateEvents, handleRef, holeContent, holeEnd, holeScope, insert, mergeProps, splitProps, spread, templateNode } from "@fluixi/dom";
2
+ import { batch, createEffect, createMemo, createRenderEffect, createSignal, onCleanup } from "@fluixi/reactive/signal";
3
3
  import { fxProps } from "@fluixi-ui/fx";
4
4
  import { Show, useRef } from "@fluixi/core";
5
- import { createSafeContext, createControllableSignal, createId, composeId, composeEventHandlers, dataAttr, focusWithoutScrolling } from "@fluixi-ui/kit";
6
- import { createCollection, Portal, createFloatingPosition, createDismissableLayer, createResizeObserver } from "@fluixi-ui/primitives";
7
- const _fxTmpl$0 = '<li data-navigation-menu-item=""><!--fx--><!--fx/--></li>';
5
+ import { composeEventHandlers, composeId, createControllableSignal, createId, createSafeContext, dataAttr, focusWithoutScrolling } from "@fluixi-ui/kit";
6
+ import { Portal, createCollection, createDismissableLayer, createFloatingPosition, createResizeObserver } from "@fluixi-ui/primitives";
7
+ //#region src/navigation-menu.tsx
8
+ var _fxTmpl$0 = "<li data-navigation-menu-item=\"\"><!--fx--><!--fx/--></li>";
8
9
  delegateEvents(["click", "keydown"]);
9
- const [NavigationMenuProvider, useNavigationMenuContext] = createSafeContext("NavigationMenu");
10
+ var [NavigationMenuProvider, useNavigationMenuContext] = createSafeContext("NavigationMenu");
11
+ /**
12
+ * Root of a navigation menu — a row (or column) of links, some of which reveal a
13
+ * rich panel of further links on hover or activation. Only one panel is open at a
14
+ * time; hovering between triggers switches instantly, and the panels are plain
15
+ * navigation regions (links), not action menus.
16
+ *
17
+ * Add a {@link NavigationMenuViewport} to route every panel into one shared
18
+ * surface that morphs between items; omit it and each panel floats under its own
19
+ * trigger.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * import {
24
+ * NavigationMenu, NavigationMenuList, NavigationMenuItem,
25
+ * NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink,
26
+ * NavigationMenuViewport,
27
+ * } from '@fluixi-ui/navigation-menu';
28
+ *
29
+ * <NavigationMenu aria-label="Main">
30
+ * <NavigationMenuList>
31
+ * <NavigationMenuItem value="products">
32
+ * <NavigationMenuTrigger>Products</NavigationMenuTrigger>
33
+ * <NavigationMenuContent>
34
+ * <NavigationMenuLink href="/analytics">Analytics</NavigationMenuLink>
35
+ * <NavigationMenuLink href="/automation">Automation</NavigationMenuLink>
36
+ * </NavigationMenuContent>
37
+ * </NavigationMenuItem>
38
+ * <NavigationMenuItem value="pricing">
39
+ * <NavigationMenuLink href="/pricing">Pricing</NavigationMenuLink>
40
+ * </NavigationMenuItem>
41
+ * </NavigationMenuList>
42
+ * <NavigationMenuViewport />
43
+ * </NavigationMenu>
44
+ * ```
45
+ */
10
46
  function NavigationMenu(props) {
11
- const [local, others] = splitProps(props, [
12
- "value",
13
- "defaultValue",
14
- "onValueChange",
15
- "orientation",
16
- "delayDuration",
17
- "closeDelay",
18
- "children",
19
- "fx",
20
- "style"
21
- ]);
22
- const [value, setValue] = createControllableSignal({
23
- value: () => local.value,
24
- defaultValue: () => local.defaultValue,
25
- onChange: (next) => local.onValueChange?.(next)
26
- });
27
- const orientation = local.orientation ?? "horizontal";
28
- const baseId = createId("navigation-menu");
29
- const triggers = createCollection();
30
- const [listRef, setListRef] = createSignal(void 0);
31
- const [viewportNode, setViewportNode] = createSignal(void 0);
32
- const [viewportSize, setViewportSize] = createSignal(void 0);
33
- let openTimer;
34
- let closeTimer;
35
- const clearOpen = () => openTimer && clearTimeout(openTimer);
36
- const clearClose = () => closeTimer && clearTimeout(closeTimer);
37
- onCleanup(() => {
38
- batch(() => {
39
- clearOpen();
40
- clearClose();
41
- });
42
- });
43
- const open = (next) => {
44
- batch(() => {
45
- clearOpen();
46
- clearClose();
47
- setToggle(true);
48
- setValue(next);
49
- });
50
- };
51
- const close = () => {
52
- batch(() => {
53
- clearOpen();
54
- clearClose();
55
- setToggle(false);
56
- setValue(void 0);
57
- });
58
- };
59
- const adjacent = (from, dir) => {
60
- const items = triggers.items();
61
- const index = items.findIndex((item) => item.value === from);
62
- if (index === -1) return void 0;
63
- const step = dir === "next" ? 1 : -1;
64
- return items[(index + step + items.length) % items.length];
65
- };
66
- const [onToggle, setToggle] = createSignal(false);
67
- const context = {
68
- value,
69
- orientation,
70
- baseId,
71
- triggers,
72
- onToggle,
73
- activeTrigger: () => {
74
- const active = value();
75
- if (active === void 0) return void 0;
76
- return triggers.items().find((item) => item.value === active)?.ref;
77
- },
78
- listRef,
79
- setListRef,
80
- viewportNode,
81
- setViewportNode,
82
- viewportSize,
83
- setViewportSize,
84
- moveTrigger: (from, dir) => {
85
- const next = adjacent(from, dir);
86
- if (!next) return;
87
- focusWithoutScrolling(next.ref);
88
- if (value() !== void 0 && next.value !== void 0) open(next.value);
89
- },
90
- requestOpen: (next) => {
91
- clearClose();
92
- if (value() !== void 0) {
93
- open(next);
94
- return;
95
- }
96
- clearOpen();
97
- openTimer = setTimeout(() => open(next), local.delayDuration ?? 200);
98
- },
99
- open,
100
- requestClose: () => {
101
- clearOpen();
102
- clearClose();
103
- setToggle(false);
104
- closeTimer = setTimeout(() => setValue(void 0), local.closeDelay ?? 300);
105
- },
106
- cancelClose: () => clearClose(),
107
- close,
108
- toggle: (next) => value() === next ? close() : open(next)
109
- };
110
- return createMemo(() => createComponent(NavigationMenuProvider, { get value() {
111
- return context;
112
- }, get children() {
113
- return (() => {
114
- const _el$ = createNativeElement("nav");
115
- spread({ element: _el$, props: mergeProps(fxProps(
116
- () => local.fx,
117
- () => local.style
118
- ), others, { "data-orientation": orientation, onPointerLeave: (event) => {
119
- if (event.pointerType !== "touch") context.requestClose();
120
- } }) });
121
- insert(_el$, local.children);
122
- return _el$;
123
- })();
124
- } }));
47
+ const [local, others] = splitProps(props, [
48
+ "value",
49
+ "defaultValue",
50
+ "onValueChange",
51
+ "orientation",
52
+ "delayDuration",
53
+ "closeDelay",
54
+ "children",
55
+ "fx",
56
+ "style"
57
+ ]);
58
+ const [value, setValue] = createControllableSignal({
59
+ value: () => local.value,
60
+ defaultValue: () => local.defaultValue,
61
+ onChange: (next) => local.onValueChange?.(next)
62
+ });
63
+ const orientation = local.orientation ?? "horizontal";
64
+ const baseId = createId("navigation-menu");
65
+ const triggers = createCollection();
66
+ const [listRef, setListRef] = createSignal(void 0);
67
+ const [viewportNode, setViewportNode] = createSignal(void 0);
68
+ const [viewportSize, setViewportSize] = createSignal(void 0);
69
+ let openTimer;
70
+ let closeTimer;
71
+ const clearOpen = () => openTimer && clearTimeout(openTimer);
72
+ const clearClose = () => closeTimer && clearTimeout(closeTimer);
73
+ onCleanup(() => {
74
+ batch(() => {
75
+ clearOpen();
76
+ clearClose();
77
+ });
78
+ });
79
+ const open = (next) => {
80
+ batch(() => {
81
+ clearOpen();
82
+ clearClose();
83
+ setToggle(true);
84
+ setValue(next);
85
+ });
86
+ };
87
+ const close = () => {
88
+ batch(() => {
89
+ clearOpen();
90
+ clearClose();
91
+ setToggle(false);
92
+ setValue(void 0);
93
+ });
94
+ };
95
+ const adjacent = (from, dir) => {
96
+ const items = triggers.items();
97
+ const index = items.findIndex((item) => item.value === from);
98
+ if (index === -1) return void 0;
99
+ return items[(index + (dir === "next" ? 1 : -1) + items.length) % items.length];
100
+ };
101
+ const [onToggle, setToggle] = createSignal(false);
102
+ const context = {
103
+ value,
104
+ orientation,
105
+ baseId,
106
+ triggers,
107
+ onToggle,
108
+ activeTrigger: () => {
109
+ const active = value();
110
+ if (active === void 0) return void 0;
111
+ return triggers.items().find((item) => item.value === active)?.ref;
112
+ },
113
+ listRef,
114
+ setListRef,
115
+ viewportNode,
116
+ setViewportNode,
117
+ viewportSize,
118
+ setViewportSize,
119
+ moveTrigger: (from, dir) => {
120
+ const next = adjacent(from, dir);
121
+ if (!next) return;
122
+ focusWithoutScrolling(next.ref);
123
+ if (value() !== void 0 && next.value !== void 0) open(next.value);
124
+ },
125
+ requestOpen: (next) => {
126
+ clearClose();
127
+ if (value() !== void 0) {
128
+ open(next);
129
+ return;
130
+ }
131
+ clearOpen();
132
+ openTimer = setTimeout(() => open(next), local.delayDuration ?? 200);
133
+ },
134
+ open,
135
+ requestClose: () => {
136
+ clearOpen();
137
+ clearClose();
138
+ setToggle(false);
139
+ closeTimer = setTimeout(() => setValue(void 0), local.closeDelay ?? 300);
140
+ },
141
+ cancelClose: () => clearClose(),
142
+ close,
143
+ toggle: (next) => value() === next ? close() : open(next)
144
+ };
145
+ return createMemo(() => createComponent(NavigationMenuProvider, {
146
+ get value() {
147
+ return context;
148
+ },
149
+ get children() {
150
+ return (() => {
151
+ const _el$ = createNativeElement("nav");
152
+ spread({
153
+ element: _el$,
154
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
155
+ "data-orientation": orientation,
156
+ onPointerLeave: (event) => {
157
+ if (event.pointerType !== "touch") context.requestClose();
158
+ }
159
+ })
160
+ });
161
+ insert(_el$, local.children, null);
162
+ return _el$;
163
+ })();
164
+ }
165
+ }));
125
166
  }
167
+ /** The list wrapping the menu's items. */
126
168
  function NavigationMenuList(props) {
127
- const ctx = useNavigationMenuContext();
128
- const [local, others] = splitProps(props, ["ref", "children", "fx", "style"]);
129
- const setRef = (node) => {
130
- ctx.setListRef(node);
131
- handleRef(local.ref, node);
132
- };
133
- return (() => {
134
- const _el$ = createNativeElement("ul");
135
- spread({ element: _el$, props: mergeProps(fxProps(
136
- () => local.fx,
137
- () => local.style
138
- ), others, { ref: setRef, "data-navigation-menu-list": "", "data-orientation": () => ctx.orientation }) });
139
- insert(_el$, local.children);
140
- return _el$;
141
- })();
169
+ const ctx = useNavigationMenuContext();
170
+ const [local, others] = splitProps(props, [
171
+ "ref",
172
+ "children",
173
+ "fx",
174
+ "style"
175
+ ]);
176
+ const setRef = (node) => {
177
+ ctx.setListRef(node);
178
+ handleRef(local.ref, node);
179
+ };
180
+ return (() => {
181
+ const _el$ = createNativeElement("ul");
182
+ spread({
183
+ element: _el$,
184
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
185
+ ref: setRef,
186
+ "data-navigation-menu-list": "",
187
+ "data-orientation": () => ctx.orientation
188
+ })
189
+ });
190
+ insert(_el$, local.children, null);
191
+ return _el$;
192
+ })();
142
193
  }
143
- const [NavigationMenuItemProvider, useNavigationMenuItemContext] = createSafeContext("NavigationMenuItem");
194
+ var [NavigationMenuItemProvider, useNavigationMenuItemContext] = createSafeContext("NavigationMenuItem");
195
+ /** One item — a trigger + its content, or a bare {@link NavigationMenuLink}. */
144
196
  function NavigationMenuItem(props) {
145
- const menu = useNavigationMenuContext();
146
- const fallback = createId("navigation-menu-item");
147
- const value = () => props.value ?? fallback;
148
- const [triggerRef, setTriggerRef] = createSignal(void 0);
149
- const [contentRef, setContentRef] = createSignal(void 0);
150
- const context = {
151
- // Item value is stable for a given mount; read once.
152
- value: value(),
153
- isOpen: () => menu.value() !== void 0 && menu.value() === value(),
154
- ids: {
155
- trigger: composeId(menu.baseId, `trigger-${value()}`),
156
- content: composeId(menu.baseId, `content-${value()}`)
157
- },
158
- triggerRef,
159
- setTriggerRef,
160
- contentRef,
161
- setContentRef,
162
- focusContent: () => {
163
- const content = contentRef();
164
- if (!content) return;
165
- const focusable = content.querySelector(
166
- 'a[href],button:not([disabled]),[tabindex]:not([tabindex="-1"])'
167
- );
168
- focusWithoutScrolling(focusable ?? content);
169
- },
170
- focusTrigger: () => focusWithoutScrolling(triggerRef())
171
- };
172
- return (() => {
173
- const _el$ = templateNode(_fxTmpl$0, "li", true);
174
- const _n$0 = _el$.firstChild;
175
- const _n$1 = holeEnd(_n$0);
176
- insert(_el$, holeScope(_n$0, () => createMemo(() => createComponent(NavigationMenuItemProvider, { get value() {
177
- return context;
178
- }, get children() {
179
- return props.children;
180
- } }))), _n$1, holeContent(_n$0, _n$1));
181
- return _el$;
182
- })();
197
+ const menu = useNavigationMenuContext();
198
+ const fallback = createId("navigation-menu-item");
199
+ const value = () => props.value ?? fallback;
200
+ const [triggerRef, setTriggerRef] = createSignal(void 0);
201
+ const [contentRef, setContentRef] = createSignal(void 0);
202
+ const context = {
203
+ value: value(),
204
+ isOpen: () => menu.value() !== void 0 && menu.value() === value(),
205
+ ids: {
206
+ trigger: composeId(menu.baseId, `trigger-${value()}`),
207
+ content: composeId(menu.baseId, `content-${value()}`)
208
+ },
209
+ triggerRef,
210
+ setTriggerRef,
211
+ contentRef,
212
+ setContentRef,
213
+ focusContent: () => {
214
+ const content = contentRef();
215
+ if (!content) return;
216
+ const focusable = content.querySelector("a[href],button:not([disabled]),[tabindex]:not([tabindex=\"-1\"])");
217
+ focusWithoutScrolling(focusable ?? content);
218
+ },
219
+ focusTrigger: () => focusWithoutScrolling(triggerRef())
220
+ };
221
+ return (() => {
222
+ const _el$ = templateNode(_fxTmpl$0, "li", true);
223
+ const _n$0 = _el$.firstChild;
224
+ const _n$1 = holeEnd(_n$0);
225
+ insert(_el$, holeScope(_n$0, () => createMemo(() => createComponent(NavigationMenuItemProvider, {
226
+ get value() {
227
+ return context;
228
+ },
229
+ get children() {
230
+ return props.children;
231
+ }
232
+ }))), _n$1, holeContent(_n$0, _n$1));
233
+ return _el$;
234
+ })();
183
235
  }
236
+ /** The button that reveals its item's {@link NavigationMenuContent}. */
184
237
  function NavigationMenuTrigger(props) {
185
- const menu = useNavigationMenuContext();
186
- const item = useNavigationMenuItemContext();
187
- const [local, others] = splitProps(props, ["onClick", "onKeyDown", "ref", "children", "fx", "style"]);
188
- const setRef = (node) => {
189
- item.setTriggerRef(node);
190
- const remove = menu.triggers.register({ ref: node, value: item.value });
191
- onCleanup(remove);
192
- const onEnter = (event) => {
193
- if (event.pointerType !== "touch") menu.requestOpen(item.value);
194
- };
195
- node.addEventListener("pointerenter", onEnter);
196
- onCleanup(() => node.removeEventListener("pointerenter", onEnter));
197
- handleRef(local.ref, node);
198
- };
199
- const forward = menu.orientation === "horizontal" ? "ArrowRight" : "ArrowDown";
200
- const backward = menu.orientation === "horizontal" ? "ArrowLeft" : "ArrowUp";
201
- const openKey = menu.orientation === "horizontal" ? "ArrowDown" : "ArrowRight";
202
- return (() => {
203
- const _el$ = createNativeElement("button");
204
- spread({ element: _el$, props: mergeProps(fxProps(
205
- () => local.fx,
206
- () => local.style
207
- ), others, { ref: setRef, type: "button", id: () => item.ids.trigger, "data-navigation-menu-trigger": "", "aria-expanded": () => item.isOpen() ? "true" : "false", "aria-controls": () => item.isOpen() ? item.ids.content : void 0, "data-state": () => item.isOpen() ? "open" : "closed", onClick: composeEventHandlers([
208
- local.onClick,
209
- () => menu.toggle(item.value)
210
- ]), onKeyDown: composeEventHandlers([
211
- local.onKeyDown,
212
- (event) => {
213
- switch (event.key) {
214
- case forward:
215
- event.preventDefault();
216
- menu.moveTrigger(item.value, "next");
217
- break;
218
- case backward:
219
- event.preventDefault();
220
- menu.moveTrigger(item.value, "previous");
221
- break;
222
- case openKey:
223
- case "Enter":
224
- case " ":
225
- event.preventDefault();
226
- menu.open(item.value);
227
- queueMicrotask(() => item.focusContent());
228
- break;
229
- }
230
- }
231
- ]) }) });
232
- insert(_el$, local.children);
233
- return _el$;
234
- })();
238
+ const menu = useNavigationMenuContext();
239
+ const item = useNavigationMenuItemContext();
240
+ const [local, others] = splitProps(props, [
241
+ "onClick",
242
+ "onKeyDown",
243
+ "ref",
244
+ "children",
245
+ "fx",
246
+ "style"
247
+ ]);
248
+ const setRef = (node) => {
249
+ item.setTriggerRef(node);
250
+ const remove = menu.triggers.register({
251
+ ref: node,
252
+ value: item.value
253
+ });
254
+ onCleanup(remove);
255
+ const onEnter = (event) => {
256
+ if (event.pointerType !== "touch") menu.requestOpen(item.value);
257
+ };
258
+ node.addEventListener("pointerenter", onEnter);
259
+ onCleanup(() => node.removeEventListener("pointerenter", onEnter));
260
+ handleRef(local.ref, node);
261
+ };
262
+ const forward = menu.orientation === "horizontal" ? "ArrowRight" : "ArrowDown";
263
+ const backward = menu.orientation === "horizontal" ? "ArrowLeft" : "ArrowUp";
264
+ const openKey = menu.orientation === "horizontal" ? "ArrowDown" : "ArrowRight";
265
+ return (() => {
266
+ const _el$ = createNativeElement("button");
267
+ spread({
268
+ element: _el$,
269
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
270
+ ref: setRef,
271
+ type: "button",
272
+ id: () => item.ids.trigger,
273
+ "data-navigation-menu-trigger": "",
274
+ "aria-expanded": () => item.isOpen() ? "true" : "false",
275
+ "aria-controls": () => item.isOpen() ? item.ids.content : void 0,
276
+ "data-state": () => item.isOpen() ? "open" : "closed",
277
+ onClick: composeEventHandlers([local.onClick, () => menu.toggle(item.value)]),
278
+ onKeyDown: composeEventHandlers([local.onKeyDown, (event) => {
279
+ switch (event.key) {
280
+ case forward:
281
+ event.preventDefault();
282
+ menu.moveTrigger(item.value, "next");
283
+ break;
284
+ case backward:
285
+ event.preventDefault();
286
+ menu.moveTrigger(item.value, "previous");
287
+ break;
288
+ case openKey:
289
+ case "Enter":
290
+ case " ":
291
+ event.preventDefault();
292
+ menu.open(item.value);
293
+ queueMicrotask(() => item.focusContent());
294
+ }
295
+ }])
296
+ })
297
+ });
298
+ insert(_el$, local.children, null);
299
+ return _el$;
300
+ })();
235
301
  }
302
+ /**
303
+ * The panel of links revealed by a {@link NavigationMenuTrigger}. Renders into a
304
+ * {@link NavigationMenuViewport} when one exists, else floats under its trigger.
305
+ */
236
306
  function NavigationMenuContent(props) {
237
- const menu = useNavigationMenuContext();
238
- const item = useNavigationMenuItemContext();
239
- return createMemo(() => createComponent(Show, { get when() {
240
- return menu.viewportNode();
241
- }, get fallback() {
242
- return createMemo(() => createComponent(Portal, { get children() {
243
- return createMemo(() => createComponent(Show, { get when() {
244
- return item.isOpen();
245
- }, get children() {
246
- return createMemo(() => createComponent(NavigationMenuContentImpl, mergeProps(props, { get floating() {
247
- return true;
248
- } })));
249
- } }));
250
- } }));
251
- }, get children() {
252
- return createMemo(() => createComponent(Portal, { get mount() {
253
- return menu.viewportNode();
254
- }, get children() {
255
- return createMemo(() => createComponent(Show, { get when() {
256
- return item.isOpen();
257
- }, get children() {
258
- return createMemo(() => createComponent(NavigationMenuContentImpl, mergeProps(props, {})));
259
- } }));
260
- } }));
261
- } }));
307
+ const menu = useNavigationMenuContext();
308
+ const item = useNavigationMenuItemContext();
309
+ return createMemo(() => createComponent(Show, {
310
+ get when() {
311
+ return menu.viewportNode();
312
+ },
313
+ get fallback() {
314
+ return createMemo(() => createComponent(Portal, { get children() {
315
+ return createMemo(() => createComponent(Show, {
316
+ get when() {
317
+ return item.isOpen();
318
+ },
319
+ get children() {
320
+ return createMemo(() => createComponent(NavigationMenuContentImpl, mergeProps(props, { get floating() {
321
+ return true;
322
+ } })));
323
+ }
324
+ }));
325
+ } }));
326
+ },
327
+ get children() {
328
+ return createMemo(() => createComponent(Portal, {
329
+ get mount() {
330
+ return menu.viewportNode();
331
+ },
332
+ get children() {
333
+ return createMemo(() => createComponent(Show, {
334
+ get when() {
335
+ return item.isOpen();
336
+ },
337
+ get children() {
338
+ return createMemo(() => createComponent(NavigationMenuContentImpl, mergeProps(props, {})));
339
+ }
340
+ }));
341
+ }
342
+ }));
343
+ }
344
+ }));
262
345
  }
263
346
  function NavigationMenuContentImpl(props) {
264
- const menu = useNavigationMenuContext();
265
- const item = useNavigationMenuItemContext();
266
- const [local, others] = splitProps(props, ["ref", "children", "style", "floating", "fx", "style"]);
267
- const [element, setElement] = createSignal(void 0);
268
- if (local.floating) {
269
- const position = createFloatingPosition(item.triggerRef, element, {
270
- placement: menu.orientation === "horizontal" ? "bottom-start" : "right-start",
271
- offset: 8
272
- });
273
- createDismissableLayer(element, {
274
- shouldExcludeTarget: (target) => {
275
- const trigger = item.triggerRef();
276
- return !!trigger && (trigger === target || trigger.contains(target));
277
- },
278
- onDismiss: () => menu.close()
279
- });
280
- createRenderEffect(() => {
281
- const el = element();
282
- if (!el) return;
283
- el.style.position = "absolute";
284
- el.style.left = `${Math.round(position.x())}px`;
285
- el.style.top = `${Math.round(position.y())}px`;
286
- el.style.visibility = position.isPositioned() ? "visible" : "hidden";
287
- });
288
- } else {
289
- createResizeObserver(element, (_entry, el) => {
290
- const box = el;
291
- menu.setViewportSize({ width: box.offsetWidth, height: box.offsetHeight });
292
- });
293
- }
294
- const setRef = (node) => {
295
- setElement(node);
296
- item.setContentRef(node);
297
- handleRef(local.ref, node);
298
- };
299
- return (() => {
300
- const _el$ = createNativeElement("div");
301
- spread({ element: _el$, props: mergeProps(fxProps(
302
- () => local.fx,
303
- () => local.style
304
- ), others, { ref: setRef, id: () => item.ids.content, "data-navigation-menu-content": "", "data-floating": () => dataAttr(Boolean(local.floating)), "aria-labelledby": () => item.ids.trigger, "data-state": "open", onPointerEnter: () => menu.cancelClose(), onKeyDown: (event) => {
305
- if (event.key === "Escape") {
306
- event.preventDefault();
307
- menu.close();
308
- item.focusTrigger();
309
- }
310
- } }) });
311
- insert(_el$, local.children);
312
- return _el$;
313
- })();
347
+ const menu = useNavigationMenuContext();
348
+ const item = useNavigationMenuItemContext();
349
+ const [local, others] = splitProps(props, [
350
+ "ref",
351
+ "children",
352
+ "style",
353
+ "floating",
354
+ "fx",
355
+ "style"
356
+ ]);
357
+ const [element, setElement] = createSignal(void 0);
358
+ if (local.floating) {
359
+ const position = createFloatingPosition(item.triggerRef, element, {
360
+ placement: menu.orientation === "horizontal" ? "bottom-start" : "right-start",
361
+ offset: 8
362
+ });
363
+ createDismissableLayer(element, {
364
+ shouldExcludeTarget: (target) => {
365
+ const trigger = item.triggerRef();
366
+ return !!trigger && (trigger === target || trigger.contains(target));
367
+ },
368
+ onDismiss: () => menu.close()
369
+ });
370
+ createRenderEffect(() => {
371
+ const el = element();
372
+ if (!el) return;
373
+ el.style.position = "absolute";
374
+ el.style.left = `${Math.round(position.x())}px`;
375
+ el.style.top = `${Math.round(position.y())}px`;
376
+ el.style.visibility = position.isPositioned() ? "visible" : "hidden";
377
+ });
378
+ } else createResizeObserver(element, (_entry, el) => {
379
+ const box = el;
380
+ menu.setViewportSize({
381
+ width: box.offsetWidth,
382
+ height: box.offsetHeight
383
+ });
384
+ });
385
+ const setRef = (node) => {
386
+ setElement(node);
387
+ item.setContentRef(node);
388
+ handleRef(local.ref, node);
389
+ };
390
+ return (() => {
391
+ const _el$ = createNativeElement("div");
392
+ spread({
393
+ element: _el$,
394
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
395
+ ref: setRef,
396
+ id: () => item.ids.content,
397
+ "data-navigation-menu-content": "",
398
+ "data-floating": () => dataAttr(Boolean(local.floating)),
399
+ "aria-labelledby": () => item.ids.trigger,
400
+ "data-state": "open",
401
+ onPointerEnter: () => menu.cancelClose(),
402
+ onKeyDown: (event) => {
403
+ if (event.key === "Escape") {
404
+ event.preventDefault();
405
+ menu.close();
406
+ item.focusTrigger();
407
+ }
408
+ }
409
+ })
410
+ });
411
+ insert(_el$, local.children, null);
412
+ return _el$;
413
+ })();
314
414
  }
415
+ /**
416
+ * The single shared surface that every {@link NavigationMenuContent} renders
417
+ * into. It anchors below the active trigger and animates its size between panels.
418
+ * Optional — without it, each panel floats under its own trigger.
419
+ */
315
420
  function NavigationMenuViewport(props) {
316
- const menu = useNavigationMenuContext();
317
- const [local, others] = splitProps(props, ["ref", "style", "fx", "style"]);
318
- const [element, setElement] = createSignal(void 0);
319
- const position = createFloatingPosition(menu.activeTrigger, element, {
320
- placement: menu.orientation === "horizontal" ? "bottom-start" : "right-start",
321
- offset: 8
322
- });
323
- createDismissableLayer(element, {
324
- // Clicking another trigger should switch, not just close — let its own handler run.
325
- shouldExcludeTarget: (target) => target instanceof Element && !!target.closest("[data-navigation-menu-trigger]"),
326
- onDismiss: () => menu.close()
327
- });
328
- createRenderEffect(() => {
329
- const el = element();
330
- if (!el) return;
331
- el.style.position = "absolute";
332
- el.style.left = `${Math.round(position.x())}px`;
333
- el.style.top = `${Math.round(position.y())}px`;
334
- const size = menu.viewportSize();
335
- if (size) {
336
- el.style.setProperty("--flx-ui-navigation-menu-viewport-width", `${size.width}px`);
337
- el.style.setProperty("--flx-ui-navigation-menu-viewport-height", `${size.height}px`);
338
- }
339
- });
340
- const setRef = (node) => {
341
- setElement(node);
342
- menu.setViewportNode(node);
343
- handleRef(local.ref, node);
344
- };
345
- const portalRef = useRef(null);
346
- onCleanup(() => {
347
- menu.setViewportNode(void 0);
348
- });
349
- createEffect(() => {
350
- if (!menu.onToggle() && portalRef.current) {
351
- portalRef.current?.remove();
352
- }
353
- });
354
- return createMemo(() => createComponent(Show, { get when() {
355
- return menu.onToggle();
356
- }, get children() {
357
- return createMemo(() => createComponent(Portal, { get ref() {
358
- return portalRef;
359
- }, get children() {
360
- return (() => {
361
- const _el$ = createNativeElement("div");
362
- spread({ element: _el$, props: mergeProps(fxProps(
363
- () => local.fx,
364
- () => local.style
365
- ), others, { ref: setRef, "data-navigation-menu-viewport": "", "data-state": () => menu.value() !== void 0 ? "open" : "closed", onPointerEnter: () => menu.cancelClose() }) });
366
- return _el$;
367
- })();
368
- } }));
369
- } }));
421
+ const menu = useNavigationMenuContext();
422
+ const [local, others] = splitProps(props, [
423
+ "ref",
424
+ "style",
425
+ "fx",
426
+ "style"
427
+ ]);
428
+ const [element, setElement] = createSignal(void 0);
429
+ const position = createFloatingPosition(menu.activeTrigger, element, {
430
+ placement: menu.orientation === "horizontal" ? "bottom-start" : "right-start",
431
+ offset: 8
432
+ });
433
+ createDismissableLayer(element, {
434
+ shouldExcludeTarget: (target) => target instanceof Element && !!target.closest("[data-navigation-menu-trigger]"),
435
+ onDismiss: () => menu.close()
436
+ });
437
+ createRenderEffect(() => {
438
+ const el = element();
439
+ if (!el) return;
440
+ el.style.position = "absolute";
441
+ el.style.left = `${Math.round(position.x())}px`;
442
+ el.style.top = `${Math.round(position.y())}px`;
443
+ const size = menu.viewportSize();
444
+ if (size) {
445
+ el.style.setProperty("--flx-ui-navigation-menu-viewport-width", `${size.width}px`);
446
+ el.style.setProperty("--flx-ui-navigation-menu-viewport-height", `${size.height}px`);
447
+ }
448
+ });
449
+ const setRef = (node) => {
450
+ setElement(node);
451
+ menu.setViewportNode(node);
452
+ handleRef(local.ref, node);
453
+ };
454
+ const portalRef = useRef(null);
455
+ onCleanup(() => {
456
+ menu.setViewportNode(void 0);
457
+ });
458
+ createEffect(() => {
459
+ if (!menu.onToggle() && portalRef.current) portalRef.current?.remove();
460
+ });
461
+ return createMemo(() => createComponent(Show, {
462
+ get when() {
463
+ return menu.onToggle();
464
+ },
465
+ get children() {
466
+ return createMemo(() => createComponent(Portal, {
467
+ get ref() {
468
+ return portalRef;
469
+ },
470
+ get children() {
471
+ return (() => {
472
+ const _el$ = createNativeElement("div");
473
+ spread({
474
+ element: _el$,
475
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
476
+ ref: setRef,
477
+ "data-navigation-menu-viewport": "",
478
+ "data-state": () => menu.value() !== void 0 ? "open" : "closed",
479
+ onPointerEnter: () => menu.cancelClose()
480
+ })
481
+ });
482
+ return _el$;
483
+ })();
484
+ }
485
+ }));
486
+ }
487
+ }));
370
488
  }
489
+ /**
490
+ * A marker (typically an arrow) that tracks the active trigger — place it inside
491
+ * a {@link NavigationMenuList}, positioned relative to the list.
492
+ */
371
493
  function NavigationMenuIndicator(props) {
372
- const menu = useNavigationMenuContext();
373
- const [local, others] = splitProps(props, ["ref", "children", "fx", "style"]);
374
- const [element, setElement] = createSignal(void 0);
375
- createRenderEffect(() => {
376
- const el = element();
377
- const trigger = menu.activeTrigger();
378
- const list = menu.listRef();
379
- if (!el || !trigger || !list) return;
380
- const t = trigger.getBoundingClientRect();
381
- const l = list.getBoundingClientRect();
382
- el.style.setProperty("--flx-ui-navigation-menu-indicator-x", `${Math.round(t.left - l.left)}px`);
383
- el.style.setProperty("--flx-ui-navigation-menu-indicator-size", `${Math.round(t.width)}px`);
384
- });
385
- const setRef = (node) => {
386
- setElement(node);
387
- handleRef(local.ref, node);
388
- };
389
- return createMemo(() => createComponent(Show, { get when() {
390
- return () => menu.value() !== void 0;
391
- }, get children() {
392
- return (() => {
393
- const _el$ = createNativeElement("li");
394
- spread({ element: _el$, props: mergeProps(fxProps(
395
- () => local.fx,
396
- () => local.style
397
- ), others, { ref: setRef, "data-navigation-menu-indicator": "", "aria-hidden": "true", "data-orientation": () => menu.orientation }) });
398
- insert(_el$, local.children);
399
- return _el$;
400
- })();
401
- } }));
494
+ const menu = useNavigationMenuContext();
495
+ const [local, others] = splitProps(props, [
496
+ "ref",
497
+ "children",
498
+ "fx",
499
+ "style"
500
+ ]);
501
+ const [element, setElement] = createSignal(void 0);
502
+ createRenderEffect(() => {
503
+ const el = element();
504
+ const trigger = menu.activeTrigger();
505
+ const list = menu.listRef();
506
+ if (!el || !trigger || !list) return;
507
+ const t = trigger.getBoundingClientRect();
508
+ const l = list.getBoundingClientRect();
509
+ el.style.setProperty("--flx-ui-navigation-menu-indicator-x", `${Math.round(t.left - l.left)}px`);
510
+ el.style.setProperty("--flx-ui-navigation-menu-indicator-size", `${Math.round(t.width)}px`);
511
+ });
512
+ const setRef = (node) => {
513
+ setElement(node);
514
+ handleRef(local.ref, node);
515
+ };
516
+ return createMemo(() => createComponent(Show, {
517
+ get when() {
518
+ return () => menu.value() !== void 0;
519
+ },
520
+ get children() {
521
+ return (() => {
522
+ const _el$ = createNativeElement("li");
523
+ spread({
524
+ element: _el$,
525
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
526
+ ref: setRef,
527
+ "data-navigation-menu-indicator": "",
528
+ "aria-hidden": "true",
529
+ "data-orientation": () => menu.orientation
530
+ })
531
+ });
532
+ insert(_el$, local.children, null);
533
+ return _el$;
534
+ })();
535
+ }
536
+ }));
402
537
  }
538
+ /** A navigation link. Selecting it closes any open panel. */
403
539
  function NavigationMenuLink(props) {
404
- const menu = useNavigationMenuContext();
405
- const [local, others] = splitProps(props, ["active", "onClick", "children", "fx", "style"]);
406
- return (() => {
407
- const _el$ = createNativeElement("a");
408
- spread({ element: _el$, props: mergeProps(fxProps(
409
- () => local.fx,
410
- () => local.style
411
- ), others, { "data-navigation-menu-link": "", "data-active": () => dataAttr(Boolean(local.active)), "aria-current": () => local.active ? "page" : void 0, onClick: composeEventHandlers([
412
- local.onClick,
413
- () => menu.close()
414
- ]) }) });
415
- insert(_el$, local.children);
416
- return _el$;
417
- })();
540
+ const menu = useNavigationMenuContext();
541
+ const [local, others] = splitProps(props, [
542
+ "active",
543
+ "onClick",
544
+ "children",
545
+ "fx",
546
+ "style"
547
+ ]);
548
+ return (() => {
549
+ const _el$ = createNativeElement("a");
550
+ spread({
551
+ element: _el$,
552
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
553
+ "data-navigation-menu-link": "",
554
+ "data-active": () => dataAttr(Boolean(local.active)),
555
+ "aria-current": () => local.active ? "page" : void 0,
556
+ onClick: composeEventHandlers([local.onClick, () => menu.close()])
557
+ })
558
+ });
559
+ insert(_el$, local.children, null);
560
+ return _el$;
561
+ })();
418
562
  }
419
- export {
420
- NavigationMenu,
421
- NavigationMenuContent,
422
- NavigationMenuIndicator,
423
- NavigationMenuItem,
424
- NavigationMenuLink,
425
- NavigationMenuList,
426
- NavigationMenuTrigger,
427
- NavigationMenuViewport
428
- };
563
+ //#endregion
564
+ export { NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport };