@fluixi-ui/menubar 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.
package/dist/menubar.js CHANGED
@@ -1,493 +1,644 @@
1
- import { delegateEvents, splitProps, createComponent, createNativeElement, spread, mergeProps, insert, handleRef } from "@fluixi/dom";
2
- import { createMemo, createSignal, createEffect, createRenderEffect, onCleanup } from "@fluixi/reactive/signal";
1
+ import { createComponent, createNativeElement, delegateEvents, handleRef, insert, mergeProps, splitProps, spread } from "@fluixi/dom";
2
+ import { createEffect, createMemo, createRenderEffect, createSignal, onCleanup } from "@fluixi/reactive/signal";
3
3
  import { fxProps } from "@fluixi-ui/fx";
4
- import { useRef, Show, onMount } from "@fluixi/core";
5
- import { createSafeContext, createControllableSignal, createId, composeId, composeEventHandlers, dataAttr, focusWithoutScrolling } from "@fluixi-ui/kit";
6
- import { createCollection, createTypeahead, Portal, createFloatingPosition, createDismissableLayer, createFocusScope, getNextEnabledIndex } from "@fluixi-ui/primitives";
4
+ import { Show, onMount, useRef } from "@fluixi/core";
5
+ import { composeEventHandlers, composeId, createControllableSignal, createId, createSafeContext, dataAttr, focusWithoutScrolling } from "@fluixi-ui/kit";
6
+ import { Portal, createCollection, createDismissableLayer, createFloatingPosition, createFocusScope, createTypeahead, getNextEnabledIndex } from "@fluixi-ui/primitives";
7
+ //#region src/menubar.tsx
7
8
  delegateEvents(["keydown", "click"]);
8
- const [MenubarProvider, useMenubarContext] = createSafeContext("Menubar");
9
+ var [MenubarProvider, useMenubarContext] = createSafeContext("Menubar");
10
+ /**
11
+ * Root of a menubar — a horizontal row of menus (like a desktop app's
12
+ * File/Edit/View bar). Left/Right arrows rove between triggers, and once one
13
+ * menu is open moving to another opens it; each menu navigates like a
14
+ * {@link DropdownMenu}.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * import {
19
+ * Menubar, MenubarMenu, MenubarTrigger, MenubarContent,
20
+ * MenubarItem, MenubarSeparator,
21
+ * } from '@fluixi-ui/menubar';
22
+ *
23
+ * <Menubar>
24
+ * <MenubarMenu>
25
+ * <MenubarTrigger>File</MenubarTrigger>
26
+ * <MenubarContent>
27
+ * <MenubarItem onSelect={newFile}>New</MenubarItem>
28
+ * <MenubarSeparator />
29
+ * <MenubarItem onSelect={save}>Save</MenubarItem>
30
+ * </MenubarContent>
31
+ * </MenubarMenu>
32
+ * <MenubarMenu>
33
+ * <MenubarTrigger>Edit</MenubarTrigger>
34
+ * <MenubarContent>
35
+ * <MenubarItem onSelect={undo}>Undo</MenubarItem>
36
+ * </MenubarContent>
37
+ * </MenubarMenu>
38
+ * </Menubar>
39
+ * ```
40
+ */
9
41
  function Menubar(props) {
10
- const [local, others] = splitProps(props, ["value", "defaultValue", "onValueChange", "children", "fx", "style"]);
11
- const [value, setValue] = createControllableSignal({
12
- value: () => local.value,
13
- defaultValue: () => local.defaultValue,
14
- onChange: (next) => local.onValueChange?.(next)
15
- });
16
- const triggers = createCollection();
17
- const adjacent = (from, dir) => {
18
- const items = triggers.items();
19
- const index = items.findIndex((item) => item.value === from);
20
- if (index === -1) return void 0;
21
- const step = dir === "next" ? 1 : -1;
22
- return items[(index + step + items.length) % items.length];
23
- };
24
- const context = {
25
- value,
26
- setValue: (next) => setValue(next),
27
- triggers,
28
- moveTrigger: (from, dir) => {
29
- const next = adjacent(from, dir);
30
- if (!next) return;
31
- focusWithoutScrolling(next.ref);
32
- if (value() !== void 0 && next.value !== void 0) setValue(next.value);
33
- },
34
- switchTo: (from, dir) => {
35
- const next = adjacent(from, dir);
36
- if (next?.value !== void 0) setValue(next.value);
37
- }
38
- };
39
- return createMemo(() => createComponent(MenubarProvider, { get value() {
40
- return context;
41
- }, get children() {
42
- return (() => {
43
- const _el$ = createNativeElement("div");
44
- spread({ element: _el$, props: mergeProps(fxProps(
45
- () => local.fx,
46
- () => local.style
47
- ), others, { role: "menubar" }) });
48
- insert(_el$, local.children);
49
- return _el$;
50
- })();
51
- } }));
42
+ const [local, others] = splitProps(props, [
43
+ "value",
44
+ "defaultValue",
45
+ "onValueChange",
46
+ "children",
47
+ "fx",
48
+ "style"
49
+ ]);
50
+ const [value, setValue] = createControllableSignal({
51
+ value: () => local.value,
52
+ defaultValue: () => local.defaultValue,
53
+ onChange: (next) => local.onValueChange?.(next)
54
+ });
55
+ const triggers = createCollection();
56
+ const adjacent = (from, dir) => {
57
+ const items = triggers.items();
58
+ const index = items.findIndex((item) => item.value === from);
59
+ if (index === -1) return void 0;
60
+ return items[(index + (dir === "next" ? 1 : -1) + items.length) % items.length];
61
+ };
62
+ const context = {
63
+ value,
64
+ setValue: (next) => setValue(next),
65
+ triggers,
66
+ moveTrigger: (from, dir) => {
67
+ const next = adjacent(from, dir);
68
+ if (!next) return;
69
+ focusWithoutScrolling(next.ref);
70
+ if (value() !== void 0 && next.value !== void 0) setValue(next.value);
71
+ },
72
+ switchTo: (from, dir) => {
73
+ const next = adjacent(from, dir);
74
+ if (next?.value !== void 0) setValue(next.value);
75
+ }
76
+ };
77
+ return createMemo(() => createComponent(MenubarProvider, {
78
+ get value() {
79
+ return context;
80
+ },
81
+ get children() {
82
+ return (() => {
83
+ const _el$ = createNativeElement("div");
84
+ spread({
85
+ element: _el$,
86
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, { role: "menubar" })
87
+ });
88
+ insert(_el$, local.children, null);
89
+ return _el$;
90
+ })();
91
+ }
92
+ }));
52
93
  }
53
- const [MenuProvider, useMenuContext] = createSafeContext("MenubarMenu");
94
+ var [MenuProvider, useMenuContext] = createSafeContext("MenubarMenu");
95
+ /** One menu within a {@link Menubar}, pairing a trigger with its content. */
54
96
  function MenubarMenu(props) {
55
- const menubar = useMenubarContext();
56
- const baseId = createId("menubar-menu");
57
- const [triggerRef, setTriggerRef] = createSignal(void 0);
58
- const itemCollection = createCollection();
59
- const [focusedIndex, setFocusedIndex] = createSignal(-1);
60
- let activeSubClose;
61
- const focusIndex = (index) => {
62
- const item = itemCollection.items()[index];
63
- if (!item) return;
64
- setFocusedIndex(index);
65
- focusWithoutScrolling(item.ref);
66
- };
67
- const move = (action) => {
68
- const next = getNextEnabledIndex(itemCollection.items(), focusedIndex(), action, true);
69
- if (next !== -1) focusIndex(next);
70
- };
71
- const focusFirst = () => {
72
- const i = itemCollection.items().findIndex((item) => !item.disabled);
73
- if (i !== -1) focusIndex(i);
74
- };
75
- const focusLast = () => {
76
- const items = itemCollection.items();
77
- for (let i = items.length - 1; i >= 0; i--) if (!items[i].disabled) return focusIndex(i);
78
- };
79
- const typeahead = createTypeahead({
80
- getItems: () => itemCollection.items().map((item) => ({ textValue: item.ref.textContent ?? "", disabled: item.disabled })),
81
- getActiveIndex: () => focusedIndex(),
82
- onMatch: (index) => focusIndex(index)
83
- });
84
- const onContentKeyDown = (event) => {
85
- switch (event.key) {
86
- case "ArrowDown":
87
- event.preventDefault();
88
- move("next");
89
- break;
90
- case "ArrowUp":
91
- event.preventDefault();
92
- move("previous");
93
- break;
94
- case "ArrowRight":
95
- event.preventDefault();
96
- menubar.switchTo(props.value, "next");
97
- break;
98
- case "ArrowLeft":
99
- event.preventDefault();
100
- menubar.switchTo(props.value, "previous");
101
- break;
102
- case "Home":
103
- event.preventDefault();
104
- focusFirst();
105
- break;
106
- case "End":
107
- event.preventDefault();
108
- focusLast();
109
- break;
110
- default:
111
- typeahead.onKeyDown(event);
112
- }
113
- };
114
- const context = {
115
- value: props.value,
116
- // Guard the `undefined === undefined` case so a menu never reads as open
117
- // when the menubar has no active menu (and defends against a missing value).
118
- isOpen: () => menubar.value() !== void 0 && menubar.value() === props.value,
119
- ids: { trigger: composeId(baseId, "trigger"), content: composeId(baseId, "content") },
120
- triggerRef,
121
- setTriggerRef,
122
- items: itemCollection,
123
- onContentKeyDown,
124
- focusFirst,
125
- select: () => {
126
- activeSubClose?.();
127
- activeSubClose = void 0;
128
- menubar.setValue(void 0);
129
- },
130
- openSub: (closeFn) => {
131
- activeSubClose?.();
132
- activeSubClose = closeFn;
133
- }
134
- };
135
- return createMemo(() => createComponent(MenuProvider, { get value() {
136
- return context;
137
- }, get children() {
138
- return props.children;
139
- } }));
97
+ const menubar = useMenubarContext();
98
+ const baseId = createId("menubar-menu");
99
+ const [triggerRef, setTriggerRef] = createSignal(void 0);
100
+ const itemCollection = createCollection();
101
+ const [focusedIndex, setFocusedIndex] = createSignal(-1);
102
+ let activeSubClose;
103
+ const focusIndex = (index) => {
104
+ const item = itemCollection.items()[index];
105
+ if (!item) return;
106
+ setFocusedIndex(index);
107
+ focusWithoutScrolling(item.ref);
108
+ };
109
+ const move = (action) => {
110
+ const next = getNextEnabledIndex(itemCollection.items(), focusedIndex(), action, true);
111
+ if (next !== -1) focusIndex(next);
112
+ };
113
+ const focusFirst = () => {
114
+ const i = itemCollection.items().findIndex((item) => !item.disabled);
115
+ if (i !== -1) focusIndex(i);
116
+ };
117
+ const focusLast = () => {
118
+ const items = itemCollection.items();
119
+ for (let i = items.length - 1; i >= 0; i--) if (!items[i].disabled) return focusIndex(i);
120
+ };
121
+ const typeahead = createTypeahead({
122
+ getItems: () => itemCollection.items().map((item) => ({
123
+ textValue: item.ref.textContent ?? "",
124
+ disabled: item.disabled
125
+ })),
126
+ getActiveIndex: () => focusedIndex(),
127
+ onMatch: (index) => focusIndex(index)
128
+ });
129
+ const onContentKeyDown = (event) => {
130
+ switch (event.key) {
131
+ case "ArrowDown":
132
+ event.preventDefault();
133
+ move("next");
134
+ break;
135
+ case "ArrowUp":
136
+ event.preventDefault();
137
+ move("previous");
138
+ break;
139
+ case "ArrowRight":
140
+ event.preventDefault();
141
+ menubar.switchTo(props.value, "next");
142
+ break;
143
+ case "ArrowLeft":
144
+ event.preventDefault();
145
+ menubar.switchTo(props.value, "previous");
146
+ break;
147
+ case "Home":
148
+ event.preventDefault();
149
+ focusFirst();
150
+ break;
151
+ case "End":
152
+ event.preventDefault();
153
+ focusLast();
154
+ break;
155
+ default: typeahead.onKeyDown(event);
156
+ }
157
+ };
158
+ const context = {
159
+ value: props.value,
160
+ isOpen: () => menubar.value() !== void 0 && menubar.value() === props.value,
161
+ ids: {
162
+ trigger: composeId(baseId, "trigger"),
163
+ content: composeId(baseId, "content")
164
+ },
165
+ triggerRef,
166
+ setTriggerRef,
167
+ items: itemCollection,
168
+ onContentKeyDown,
169
+ focusFirst,
170
+ select: () => {
171
+ activeSubClose?.();
172
+ activeSubClose = void 0;
173
+ menubar.setValue(void 0);
174
+ },
175
+ openSub: (closeFn) => {
176
+ activeSubClose?.();
177
+ activeSubClose = closeFn;
178
+ }
179
+ };
180
+ return createMemo(() => createComponent(MenuProvider, {
181
+ get value() {
182
+ return context;
183
+ },
184
+ get children() {
185
+ return props.children;
186
+ }
187
+ }));
140
188
  }
189
+ /** The top-level button that opens its {@link MenubarMenu}. */
141
190
  function MenubarTrigger(props) {
142
- const menubar = useMenubarContext();
143
- const menu = useMenuContext();
144
- const [local, others] = splitProps(props, ["onClick", "onKeyDown", "ref", "children", "fx", "style"]);
145
- const setRef = (node) => {
146
- menu.setTriggerRef(node);
147
- const remove = menubar.triggers.register({ ref: node, value: menu.value });
148
- onCleanup(remove);
149
- handleRef(local.ref, node);
150
- };
151
- return (() => {
152
- const _el$ = createNativeElement("button");
153
- spread({ element: _el$, props: mergeProps(fxProps(
154
- () => local.fx,
155
- () => local.style
156
- ), others, { ref: setRef, type: "button", id: () => menu.ids.trigger, role: "menuitem", "aria-haspopup": "menu", "aria-expanded": () => menu.isOpen() ? "true" : "false", "aria-controls": () => menu.ids.content, tabindex: -1, "data-state": () => menu.isOpen() ? "open" : "closed", onClick: composeEventHandlers([
157
- local.onClick,
158
- () => menubar.setValue(menu.isOpen() ? void 0 : menu.value)
159
- ]), onKeyDown: composeEventHandlers([
160
- local.onKeyDown,
161
- (event) => {
162
- switch (event.key) {
163
- case "ArrowRight":
164
- event.preventDefault();
165
- menubar.moveTrigger(menu.value, "next");
166
- break;
167
- case "ArrowLeft":
168
- event.preventDefault();
169
- menubar.moveTrigger(menu.value, "previous");
170
- break;
171
- case "ArrowDown":
172
- case "Enter":
173
- case " ":
174
- event.preventDefault();
175
- menubar.setValue(menu.value);
176
- break;
177
- }
178
- }
179
- ]) }) });
180
- insert(_el$, local.children);
181
- return _el$;
182
- })();
191
+ const menubar = useMenubarContext();
192
+ const menu = useMenuContext();
193
+ const [local, others] = splitProps(props, [
194
+ "onClick",
195
+ "onKeyDown",
196
+ "ref",
197
+ "children",
198
+ "fx",
199
+ "style"
200
+ ]);
201
+ const setRef = (node) => {
202
+ menu.setTriggerRef(node);
203
+ const remove = menubar.triggers.register({
204
+ ref: node,
205
+ value: menu.value
206
+ });
207
+ onCleanup(remove);
208
+ handleRef(local.ref, node);
209
+ };
210
+ return (() => {
211
+ const _el$ = createNativeElement("button");
212
+ spread({
213
+ element: _el$,
214
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
215
+ ref: setRef,
216
+ type: "button",
217
+ id: () => menu.ids.trigger,
218
+ role: "menuitem",
219
+ "aria-haspopup": "menu",
220
+ "aria-expanded": () => menu.isOpen() ? "true" : "false",
221
+ "aria-controls": () => menu.ids.content,
222
+ tabindex: -1,
223
+ "data-state": () => menu.isOpen() ? "open" : "closed",
224
+ onClick: composeEventHandlers([local.onClick, () => menubar.setValue(menu.isOpen() ? void 0 : menu.value)]),
225
+ onKeyDown: composeEventHandlers([local.onKeyDown, (event) => {
226
+ switch (event.key) {
227
+ case "ArrowRight":
228
+ event.preventDefault();
229
+ menubar.moveTrigger(menu.value, "next");
230
+ break;
231
+ case "ArrowLeft":
232
+ event.preventDefault();
233
+ menubar.moveTrigger(menu.value, "previous");
234
+ break;
235
+ case "ArrowDown":
236
+ case "Enter":
237
+ case " ":
238
+ event.preventDefault();
239
+ menubar.setValue(menu.value);
240
+ }
241
+ }])
242
+ })
243
+ });
244
+ insert(_el$, local.children, null);
245
+ return _el$;
246
+ })();
183
247
  }
248
+ /** The floating `role="menu"` surface for one {@link MenubarMenu}. */
184
249
  function MenubarContent(props) {
185
- const menu = useMenuContext();
186
- const ref = useRef(null);
187
- createEffect(() => {
188
- if (!menu.isOpen() && ref.current) {
189
- ref.current.remove();
190
- }
191
- });
192
- return createMemo(() => createComponent(Show, { get when() {
193
- return menu.isOpen();
194
- }, get children() {
195
- return createMemo(() => createComponent(Portal, { get ref() {
196
- return ref;
197
- }, get children() {
198
- return createMemo(() => createComponent(MenubarContentImpl, mergeProps(props, {})));
199
- } }));
200
- } }));
250
+ const menu = useMenuContext();
251
+ const ref = useRef(null);
252
+ createEffect(() => {
253
+ if (!menu.isOpen() && ref.current) ref.current.remove();
254
+ });
255
+ return createMemo(() => createComponent(Show, {
256
+ get when() {
257
+ return menu.isOpen();
258
+ },
259
+ get children() {
260
+ return createMemo(() => createComponent(Portal, {
261
+ get ref() {
262
+ return ref;
263
+ },
264
+ get children() {
265
+ return createMemo(() => createComponent(MenubarContentImpl, mergeProps(props, {})));
266
+ }
267
+ }));
268
+ }
269
+ }));
201
270
  }
202
271
  function MenubarContentImpl(props) {
203
- const menu = useMenuContext();
204
- const [local, others] = splitProps(props, ["ref", "children", "style", "fx", "style"]);
205
- const [element, setElement] = createSignal(void 0);
206
- const position = createFloatingPosition(menu.triggerRef, element, {
207
- placement: "bottom-start",
208
- offset: 4
209
- });
210
- createDismissableLayer(element, {
211
- shouldExcludeTarget: (target) => {
212
- const trigger = menu.triggerRef();
213
- if (trigger && (trigger === target || trigger.contains(target))) return true;
214
- const el = target instanceof Element ? target : target.parentElement;
215
- return !!el?.closest(`[data-menu-owner="${menu.ids.content}"]`);
216
- },
217
- onDismiss: () => menu.select()
218
- });
219
- createFocusScope(element, { trap: false, autoFocus: false, restoreFocus: true });
220
- createRenderEffect(() => {
221
- const el = element();
222
- if (!el) return;
223
- el.style.position = "absolute";
224
- el.style.left = `${Math.round(position.x())}px`;
225
- el.style.top = `${Math.round(position.y())}px`;
226
- });
227
- onMount(() => queueMicrotask(() => menu.focusFirst()));
228
- const setRef = (node) => {
229
- setElement(node);
230
- handleRef(local.ref, node);
231
- };
232
- return (() => {
233
- const _el$ = createNativeElement("div");
234
- spread({ element: _el$, props: mergeProps(fxProps(
235
- () => local.fx,
236
- () => local.style
237
- ), others, { ref: setRef, role: "menu", id: () => menu.ids.content, "aria-labelledby": () => menu.ids.trigger, "data-state": "open", tabindex: -1, onKeyDown: menu.onContentKeyDown }) });
238
- insert(_el$, local.children);
239
- return _el$;
240
- })();
272
+ const menu = useMenuContext();
273
+ const [local, others] = splitProps(props, [
274
+ "ref",
275
+ "children",
276
+ "style",
277
+ "fx",
278
+ "style"
279
+ ]);
280
+ const [element, setElement] = createSignal(void 0);
281
+ const position = createFloatingPosition(menu.triggerRef, element, {
282
+ placement: "bottom-start",
283
+ offset: 4
284
+ });
285
+ createDismissableLayer(element, {
286
+ shouldExcludeTarget: (target) => {
287
+ const trigger = menu.triggerRef();
288
+ if (trigger && (trigger === target || trigger.contains(target))) return true;
289
+ return !!(target instanceof Element ? target : target.parentElement)?.closest(`[data-menu-owner="${menu.ids.content}"]`);
290
+ },
291
+ onDismiss: () => menu.select()
292
+ });
293
+ createFocusScope(element, {
294
+ trap: false,
295
+ autoFocus: false,
296
+ restoreFocus: true
297
+ });
298
+ createRenderEffect(() => {
299
+ const el = element();
300
+ if (!el) return;
301
+ el.style.position = "absolute";
302
+ el.style.left = `${Math.round(position.x())}px`;
303
+ el.style.top = `${Math.round(position.y())}px`;
304
+ });
305
+ onMount(() => queueMicrotask(() => menu.focusFirst()));
306
+ const setRef = (node) => {
307
+ setElement(node);
308
+ handleRef(local.ref, node);
309
+ };
310
+ return (() => {
311
+ const _el$ = createNativeElement("div");
312
+ spread({
313
+ element: _el$,
314
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
315
+ ref: setRef,
316
+ role: "menu",
317
+ id: () => menu.ids.content,
318
+ "aria-labelledby": () => menu.ids.trigger,
319
+ "data-state": "open",
320
+ tabindex: -1,
321
+ onKeyDown: menu.onContentKeyDown
322
+ })
323
+ });
324
+ insert(_el$, local.children, null);
325
+ return _el$;
326
+ })();
241
327
  }
328
+ /** A menu action; runs `onSelect` and closes the menu when chosen. */
242
329
  function MenubarItem(props) {
243
- const menu = useMenuContext();
244
- const [local, others] = splitProps(props, ["disabled", "onSelect", "onClick", "onPointerEnter", "ref", "children", "fx", "style"]);
245
- const setRef = (node) => {
246
- const remove = menu.items.register({ ref: node, disabled: local.disabled });
247
- onCleanup(remove);
248
- handleRef(local.ref, node);
249
- };
250
- const select = () => {
251
- if (local.disabled) return;
252
- local.onSelect?.();
253
- menu.select();
254
- };
255
- return (() => {
256
- const _el$ = createNativeElement("div");
257
- spread({ element: _el$, props: mergeProps(fxProps(
258
- () => local.fx,
259
- () => local.style
260
- ), others, { ref: setRef, role: "menuitem", tabindex: -1, "aria-disabled": () => local.disabled ? "true" : void 0, "data-disabled": () => dataAttr(Boolean(local.disabled)), onClick: composeEventHandlers([
261
- local.onClick,
262
- () => select()
263
- ]), onKeyDown: (event) => {
264
- if (event.key === "Enter" || event.key === " ") {
265
- event.preventDefault();
266
- select();
267
- }
268
- }, onPointerEnter: composeEventHandlers([
269
- local.onPointerEnter,
270
- (e) => {
271
- if (local.disabled) return;
272
- e.currentTarget.focus();
273
- }
274
- ]) }) });
275
- insert(_el$, local.children);
276
- return _el$;
277
- })();
330
+ const menu = useMenuContext();
331
+ const [local, others] = splitProps(props, [
332
+ "disabled",
333
+ "onSelect",
334
+ "onClick",
335
+ "onPointerEnter",
336
+ "ref",
337
+ "children",
338
+ "fx",
339
+ "style"
340
+ ]);
341
+ const setRef = (node) => {
342
+ const remove = menu.items.register({
343
+ ref: node,
344
+ disabled: local.disabled
345
+ });
346
+ onCleanup(remove);
347
+ handleRef(local.ref, node);
348
+ };
349
+ const select = () => {
350
+ if (local.disabled) return;
351
+ local.onSelect?.();
352
+ menu.select();
353
+ };
354
+ return (() => {
355
+ const _el$ = createNativeElement("div");
356
+ spread({
357
+ element: _el$,
358
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
359
+ ref: setRef,
360
+ role: "menuitem",
361
+ tabindex: -1,
362
+ "aria-disabled": () => local.disabled ? "true" : void 0,
363
+ "data-disabled": () => dataAttr(Boolean(local.disabled)),
364
+ onClick: composeEventHandlers([local.onClick, () => select()]),
365
+ onKeyDown: (event) => {
366
+ if (event.key === "Enter" || event.key === " ") {
367
+ event.preventDefault();
368
+ select();
369
+ }
370
+ },
371
+ onPointerEnter: composeEventHandlers([local.onPointerEnter, (e) => {
372
+ if (local.disabled) return;
373
+ e.currentTarget.focus();
374
+ }])
375
+ })
376
+ });
377
+ insert(_el$, local.children, null);
378
+ return _el$;
379
+ })();
278
380
  }
381
+ /** A visual divider between groups of menu items. */
279
382
  function MenubarSeparator(props) {
280
- return (() => {
281
- const _el$ = createNativeElement("div");
282
- spread({ element: _el$, props: mergeProps(props, { role: "separator", "aria-orientation": "horizontal" }) });
283
- return _el$;
284
- })();
383
+ return (() => {
384
+ const _el$ = createNativeElement("div");
385
+ spread({
386
+ element: _el$,
387
+ props: mergeProps(props, {
388
+ role: "separator",
389
+ "aria-orientation": "horizontal"
390
+ })
391
+ });
392
+ return _el$;
393
+ })();
285
394
  }
286
- const [SubMenuProvider, useSubMenuContext] = createSafeContext("MenubarSub");
395
+ var [SubMenuProvider, useSubMenuContext] = createSafeContext("MenubarSub");
396
+ /** Wraps a sub-trigger + sub-content pair to create a nested flyout inside a menubar menu. */
287
397
  function MenubarSub(props) {
288
- const menuCtx = useMenuContext();
289
- const baseId = createId("menubar-sub");
290
- const [isOpen, setIsOpen] = createSignal(false);
291
- const [triggerRef, setTriggerRef] = createSignal(void 0);
292
- const subCollection = createCollection();
293
- const [focusedIndex, setFocusedIndex] = createSignal(-1);
294
- let openTimer;
295
- let closeTimer;
296
- onCleanup(() => {
297
- clearTimeout(openTimer);
298
- clearTimeout(closeTimer);
299
- });
300
- const focusIndex = (index) => {
301
- const item = subCollection.items()[index];
302
- if (!item) return;
303
- setFocusedIndex(index);
304
- focusWithoutScrolling(item.ref);
305
- };
306
- const focusFirst = () => {
307
- const i = subCollection.items().findIndex((item) => !item.disabled);
308
- if (i !== -1) focusIndex(i);
309
- };
310
- const openSub = () => {
311
- menuCtx.openSub(() => setIsOpen(false));
312
- setIsOpen(true);
313
- };
314
- createEffect(() => {
315
- if (!menuCtx.isOpen()) setIsOpen(false);
316
- });
317
- const context = {
318
- isOpen,
319
- setOpen: (open) => {
320
- if (open) openSub();
321
- else setIsOpen(false);
322
- },
323
- ids: { trigger: composeId(baseId, "trigger"), content: composeId(baseId, "content") },
324
- triggerRef,
325
- setTriggerRef,
326
- collection: subCollection,
327
- focusedIndex,
328
- focusIndex,
329
- focusFirst,
330
- scheduleOpen: () => {
331
- clearTimeout(closeTimer);
332
- openTimer = setTimeout(openSub, 200);
333
- },
334
- scheduleClose: (movingTowardSub = false) => {
335
- clearTimeout(openTimer);
336
- closeTimer = setTimeout(() => setIsOpen(false), movingTowardSub ? 500 : 200);
337
- },
338
- cancelClose: () => clearTimeout(closeTimer),
339
- closeRoot: () => menuCtx.select()
340
- };
341
- return createMemo(() => createComponent(SubMenuProvider, { get value() {
342
- return context;
343
- }, get children() {
344
- return props.children;
345
- } }));
398
+ const menuCtx = useMenuContext();
399
+ const baseId = createId("menubar-sub");
400
+ const [isOpen, setIsOpen] = createSignal(false);
401
+ const [triggerRef, setTriggerRef] = createSignal(void 0);
402
+ const subCollection = createCollection();
403
+ const [focusedIndex, setFocusedIndex] = createSignal(-1);
404
+ let openTimer;
405
+ let closeTimer;
406
+ onCleanup(() => {
407
+ clearTimeout(openTimer);
408
+ clearTimeout(closeTimer);
409
+ });
410
+ const focusIndex = (index) => {
411
+ const item = subCollection.items()[index];
412
+ if (!item) return;
413
+ setFocusedIndex(index);
414
+ focusWithoutScrolling(item.ref);
415
+ };
416
+ const focusFirst = () => {
417
+ const i = subCollection.items().findIndex((item) => !item.disabled);
418
+ if (i !== -1) focusIndex(i);
419
+ };
420
+ const openSub = () => {
421
+ menuCtx.openSub(() => setIsOpen(false));
422
+ setIsOpen(true);
423
+ };
424
+ createEffect(() => {
425
+ if (!menuCtx.isOpen()) setIsOpen(false);
426
+ });
427
+ const context = {
428
+ isOpen,
429
+ setOpen: (open) => {
430
+ if (open) openSub();
431
+ else setIsOpen(false);
432
+ },
433
+ ids: {
434
+ trigger: composeId(baseId, "trigger"),
435
+ content: composeId(baseId, "content")
436
+ },
437
+ triggerRef,
438
+ setTriggerRef,
439
+ collection: subCollection,
440
+ focusedIndex,
441
+ focusIndex,
442
+ focusFirst,
443
+ scheduleOpen: () => {
444
+ clearTimeout(closeTimer);
445
+ openTimer = setTimeout(openSub, 200);
446
+ },
447
+ scheduleClose: (movingTowardSub = false) => {
448
+ clearTimeout(openTimer);
449
+ closeTimer = setTimeout(() => setIsOpen(false), movingTowardSub ? 500 : 200);
450
+ },
451
+ cancelClose: () => clearTimeout(closeTimer),
452
+ closeRoot: () => menuCtx.select()
453
+ };
454
+ return createMemo(() => createComponent(SubMenuProvider, {
455
+ get value() {
456
+ return context;
457
+ },
458
+ get children() {
459
+ return props.children;
460
+ }
461
+ }));
346
462
  }
463
+ /** A `menuitem` inside a menubar menu that opens a nested flyout on hover or `ArrowRight`. */
347
464
  function MenubarSubTrigger(props) {
348
- const menu = useMenuContext();
349
- const sub = useSubMenuContext();
350
- const [local, others] = splitProps(props, ["disabled", "onKeyDown", "onPointerEnter", "onPointerLeave", "ref", "children", "fx", "style"]);
351
- const setRef = (node) => {
352
- sub.setTriggerRef(node);
353
- const remove = menu.items.register({ ref: node, disabled: local.disabled });
354
- onCleanup(remove);
355
- handleRef(local.ref, node);
356
- };
357
- const open = () => {
358
- sub.setOpen(true);
359
- queueMicrotask(() => sub.focusFirst());
360
- };
361
- return (() => {
362
- const _el$ = createNativeElement("div");
363
- spread({ element: _el$, props: mergeProps(fxProps(
364
- () => local.fx,
365
- () => local.style
366
- ), others, { ref: setRef, role: "menuitem", tabindex: -1, "aria-haspopup": "menu", "aria-expanded": () => sub.isOpen() ? "true" : "false", "aria-controls": () => sub.ids.content, "data-state": () => sub.isOpen() ? "open" : "closed", "data-disabled": () => dataAttr(Boolean(local.disabled)), "aria-disabled": () => local.disabled ? "true" : void 0, onPointerEnter: composeEventHandlers([
367
- local.onPointerEnter,
368
- (e) => {
369
- if (local.disabled) return;
370
- e.currentTarget.focus();
371
- sub.scheduleOpen();
372
- }
373
- ]), onPointerLeave: composeEventHandlers([
374
- local.onPointerLeave,
375
- (e) => sub.scheduleClose(e.movementX > 0)
376
- ]), onKeyDown: composeEventHandlers([
377
- local.onKeyDown,
378
- (e) => {
379
- if (local.disabled) return;
380
- if (e.key === "ArrowRight" || e.key === "Enter" || e.key === " ") {
381
- e.preventDefault();
382
- e.stopPropagation();
383
- open();
384
- }
385
- }
386
- ]) }) });
387
- insert(_el$, local.children);
388
- return _el$;
389
- })();
465
+ const menu = useMenuContext();
466
+ const sub = useSubMenuContext();
467
+ const [local, others] = splitProps(props, [
468
+ "disabled",
469
+ "onKeyDown",
470
+ "onPointerEnter",
471
+ "onPointerLeave",
472
+ "ref",
473
+ "children",
474
+ "fx",
475
+ "style"
476
+ ]);
477
+ const setRef = (node) => {
478
+ sub.setTriggerRef(node);
479
+ const remove = menu.items.register({
480
+ ref: node,
481
+ disabled: local.disabled
482
+ });
483
+ onCleanup(remove);
484
+ handleRef(local.ref, node);
485
+ };
486
+ const open = () => {
487
+ sub.setOpen(true);
488
+ queueMicrotask(() => sub.focusFirst());
489
+ };
490
+ return (() => {
491
+ const _el$ = createNativeElement("div");
492
+ spread({
493
+ element: _el$,
494
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
495
+ ref: setRef,
496
+ role: "menuitem",
497
+ tabindex: -1,
498
+ "aria-haspopup": "menu",
499
+ "aria-expanded": () => sub.isOpen() ? "true" : "false",
500
+ "aria-controls": () => sub.ids.content,
501
+ "data-state": () => sub.isOpen() ? "open" : "closed",
502
+ "data-disabled": () => dataAttr(Boolean(local.disabled)),
503
+ "aria-disabled": () => local.disabled ? "true" : void 0,
504
+ onPointerEnter: composeEventHandlers([local.onPointerEnter, (e) => {
505
+ if (local.disabled) return;
506
+ e.currentTarget.focus();
507
+ sub.scheduleOpen();
508
+ }]),
509
+ onPointerLeave: composeEventHandlers([local.onPointerLeave, (e) => sub.scheduleClose(e.movementX > 0)]),
510
+ onKeyDown: composeEventHandlers([local.onKeyDown, (e) => {
511
+ if (local.disabled) return;
512
+ if (e.key === "ArrowRight" || e.key === "Enter" || e.key === " ") {
513
+ e.preventDefault();
514
+ e.stopPropagation();
515
+ open();
516
+ }
517
+ }])
518
+ })
519
+ });
520
+ insert(_el$, local.children, null);
521
+ return _el$;
522
+ })();
390
523
  }
524
+ /** The floating `role="menu"` surface for a nested menubar submenu. Self-portals while open. */
391
525
  function MenubarSubContent(props) {
392
- const sub = useSubMenuContext();
393
- const root = useMenuContext();
394
- const [local, others] = splitProps(props, ["ref", "children", "style", "fx", "style"]);
395
- const [element, setElement] = createSignal(void 0);
396
- const portalRef = useRef(null);
397
- createEffect(() => {
398
- if (!sub.isOpen() && portalRef.current) portalRef.current.remove();
399
- });
400
- const position = createFloatingPosition(sub.triggerRef, element, {
401
- placement: "right-start",
402
- offset: -2
403
- });
404
- createRenderEffect(() => {
405
- const el = element();
406
- if (!el) return;
407
- el.style.position = "absolute";
408
- el.style.left = `${Math.round(position.x())}px`;
409
- el.style.top = `${Math.round(position.y())}px`;
410
- });
411
- onMount(() => queueMicrotask(() => sub.focusFirst()));
412
- const move = (action) => {
413
- const next = getNextEnabledIndex(sub.collection.items(), sub.focusedIndex(), action, true);
414
- if (next !== -1) sub.focusIndex(next);
415
- };
416
- const focusLast = () => {
417
- const items = sub.collection.items();
418
- for (let i = items.length - 1; i >= 0; i--) if (!items[i].disabled) {
419
- sub.focusIndex(i);
420
- return;
421
- }
422
- };
423
- const typeahead = createTypeahead({
424
- getItems: () => sub.collection.items().map((item) => ({ textValue: item.ref.textContent ?? "", disabled: item.disabled })),
425
- getActiveIndex: () => sub.focusedIndex(),
426
- onMatch: (index) => sub.focusIndex(index)
427
- });
428
- const onContentKeyDown = (event) => {
429
- switch (event.key) {
430
- case "ArrowDown":
431
- event.preventDefault();
432
- move("next");
433
- break;
434
- case "ArrowUp":
435
- event.preventDefault();
436
- move("previous");
437
- break;
438
- case "Home":
439
- event.preventDefault();
440
- sub.focusFirst();
441
- break;
442
- case "End":
443
- event.preventDefault();
444
- focusLast();
445
- break;
446
- case "ArrowLeft":
447
- case "Escape":
448
- event.preventDefault();
449
- event.stopPropagation();
450
- sub.setOpen(false);
451
- focusWithoutScrolling(sub.triggerRef());
452
- break;
453
- case "Tab":
454
- event.preventDefault();
455
- sub.closeRoot();
456
- break;
457
- default:
458
- typeahead.onKeyDown(event);
459
- }
460
- };
461
- const setRef = (node) => {
462
- setElement(node);
463
- handleRef(local.ref, node);
464
- };
465
- return createMemo(() => createComponent(Show, { get when() {
466
- return sub.isOpen();
467
- }, get children() {
468
- return createMemo(() => createComponent(Portal, { get ref() {
469
- return portalRef;
470
- }, get children() {
471
- return (() => {
472
- const _el$ = createNativeElement("div");
473
- spread({ element: _el$, props: mergeProps(fxProps(
474
- () => local.fx,
475
- () => local.style
476
- ), others, { ref: setRef, role: "menu", id: () => sub.ids.content, "data-menu-owner": () => root.ids.content, "aria-labelledby": () => sub.ids.trigger, "data-state": "open", "data-placement": () => position.placement(), tabindex: -1, onKeyDown: onContentKeyDown, onPointerEnter: () => sub.cancelClose(), onPointerLeave: () => sub.scheduleClose() }) });
477
- insert(_el$, local.children);
478
- return _el$;
479
- })();
480
- } }));
481
- } }));
526
+ const sub = useSubMenuContext();
527
+ const root = useMenuContext();
528
+ const [local, others] = splitProps(props, [
529
+ "ref",
530
+ "children",
531
+ "style",
532
+ "fx",
533
+ "style"
534
+ ]);
535
+ const [element, setElement] = createSignal(void 0);
536
+ const portalRef = useRef(null);
537
+ createEffect(() => {
538
+ if (!sub.isOpen() && portalRef.current) portalRef.current.remove();
539
+ });
540
+ const position = createFloatingPosition(sub.triggerRef, element, {
541
+ placement: "right-start",
542
+ offset: -2
543
+ });
544
+ createRenderEffect(() => {
545
+ const el = element();
546
+ if (!el) return;
547
+ el.style.position = "absolute";
548
+ el.style.left = `${Math.round(position.x())}px`;
549
+ el.style.top = `${Math.round(position.y())}px`;
550
+ });
551
+ onMount(() => queueMicrotask(() => sub.focusFirst()));
552
+ const move = (action) => {
553
+ const next = getNextEnabledIndex(sub.collection.items(), sub.focusedIndex(), action, true);
554
+ if (next !== -1) sub.focusIndex(next);
555
+ };
556
+ const focusLast = () => {
557
+ const items = sub.collection.items();
558
+ for (let i = items.length - 1; i >= 0; i--) if (!items[i].disabled) {
559
+ sub.focusIndex(i);
560
+ return;
561
+ }
562
+ };
563
+ const typeahead = createTypeahead({
564
+ getItems: () => sub.collection.items().map((item) => ({
565
+ textValue: item.ref.textContent ?? "",
566
+ disabled: item.disabled
567
+ })),
568
+ getActiveIndex: () => sub.focusedIndex(),
569
+ onMatch: (index) => sub.focusIndex(index)
570
+ });
571
+ const onContentKeyDown = (event) => {
572
+ switch (event.key) {
573
+ case "ArrowDown":
574
+ event.preventDefault();
575
+ move("next");
576
+ break;
577
+ case "ArrowUp":
578
+ event.preventDefault();
579
+ move("previous");
580
+ break;
581
+ case "Home":
582
+ event.preventDefault();
583
+ sub.focusFirst();
584
+ break;
585
+ case "End":
586
+ event.preventDefault();
587
+ focusLast();
588
+ break;
589
+ case "ArrowLeft":
590
+ case "Escape":
591
+ event.preventDefault();
592
+ event.stopPropagation();
593
+ sub.setOpen(false);
594
+ focusWithoutScrolling(sub.triggerRef());
595
+ break;
596
+ case "Tab":
597
+ event.preventDefault();
598
+ sub.closeRoot();
599
+ break;
600
+ default: typeahead.onKeyDown(event);
601
+ }
602
+ };
603
+ const setRef = (node) => {
604
+ setElement(node);
605
+ handleRef(local.ref, node);
606
+ };
607
+ return createMemo(() => createComponent(Show, {
608
+ get when() {
609
+ return sub.isOpen();
610
+ },
611
+ get children() {
612
+ return createMemo(() => createComponent(Portal, {
613
+ get ref() {
614
+ return portalRef;
615
+ },
616
+ get children() {
617
+ return (() => {
618
+ const _el$ = createNativeElement("div");
619
+ spread({
620
+ element: _el$,
621
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
622
+ ref: setRef,
623
+ role: "menu",
624
+ id: () => sub.ids.content,
625
+ "data-menu-owner": () => root.ids.content,
626
+ "aria-labelledby": () => sub.ids.trigger,
627
+ "data-state": "open",
628
+ "data-placement": () => position.placement(),
629
+ tabindex: -1,
630
+ onKeyDown: onContentKeyDown,
631
+ onPointerEnter: () => sub.cancelClose(),
632
+ onPointerLeave: () => sub.scheduleClose()
633
+ })
634
+ });
635
+ insert(_el$, local.children, null);
636
+ return _el$;
637
+ })();
638
+ }
639
+ }));
640
+ }
641
+ }));
482
642
  }
483
- export {
484
- Menubar,
485
- MenubarContent,
486
- MenubarItem,
487
- MenubarMenu,
488
- MenubarSeparator,
489
- MenubarSub,
490
- MenubarSubContent,
491
- MenubarSubTrigger,
492
- MenubarTrigger
493
- };
643
+ //#endregion
644
+ export { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger };