@radix-solid-js/menu 0.1.2

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/index.js ADDED
@@ -0,0 +1,1037 @@
1
+ import { splitProps, createSignal, createEffect, onCleanup, Show, onMount } from 'solid-js';
2
+ import { composeEventHandlers } from '@radix-solid-js/primitive';
3
+ import { createCollection } from '@radix-solid-js/collection';
4
+ import { mergeRefs } from '@radix-solid-js/compose-refs';
5
+ import { createContextScope } from '@radix-solid-js/context';
6
+ import { useDirection } from '@radix-solid-js/direction';
7
+ import { DismissableLayer } from '@radix-solid-js/dismissable-layer';
8
+ import { useFocusGuards } from '@radix-solid-js/focus-guards';
9
+ import { FocusScope } from '@radix-solid-js/focus-scope';
10
+ import { createId } from '@radix-solid-js/id';
11
+ import { createPopperScope, Popper, PopperAnchor, PopperContent, PopperArrow } from '@radix-solid-js/popper';
12
+ import { Portal } from '@radix-solid-js/portal';
13
+ import { Presence } from '@radix-solid-js/presence';
14
+ import { Primitive, dispatchDiscreteCustomEvent } from '@radix-solid-js/primitive-component';
15
+ import { createRovingFocusGroupScope, RovingFocusGroup, RovingFocusGroupItem } from '@radix-solid-js/roving-focus';
16
+ import { hideOthers } from 'aria-hidden';
17
+
18
+ // src/menu.tsx
19
+ var SELECTION_KEYS = ["Enter", " "];
20
+ var FIRST_KEYS = ["ArrowDown", "PageUp", "Home"];
21
+ var LAST_KEYS = ["ArrowUp", "PageDown", "End"];
22
+ var FIRST_LAST_KEYS = [...FIRST_KEYS, ...LAST_KEYS];
23
+ var SUB_OPEN_KEYS = {
24
+ ltr: [...SELECTION_KEYS, "ArrowRight"],
25
+ rtl: [...SELECTION_KEYS, "ArrowLeft"]
26
+ };
27
+ var SUB_CLOSE_KEYS = {
28
+ ltr: ["ArrowLeft"],
29
+ rtl: ["ArrowRight"]
30
+ };
31
+ var MENU_NAME = "Menu";
32
+ var [Collection, useCollection, createCollectionScope] = createCollection(MENU_NAME);
33
+ var [createMenuContext, createMenuScope] = createContextScope(MENU_NAME, [
34
+ createCollectionScope,
35
+ createPopperScope,
36
+ createRovingFocusGroupScope
37
+ ]);
38
+ var usePopperScope = createPopperScope();
39
+ var useRovingFocusGroupScope = createRovingFocusGroupScope();
40
+ var [MenuProvider, useMenuContext] = createMenuContext(MENU_NAME);
41
+ var [MenuRootProvider, useMenuRootContext] = createMenuContext(MENU_NAME);
42
+ function Menu(props) {
43
+ const [local] = splitProps(props, [
44
+ "__scopeMenu",
45
+ "children",
46
+ "open",
47
+ "onOpenChange",
48
+ "dir",
49
+ "modal"
50
+ ]);
51
+ const popperScope = usePopperScope(local.__scopeMenu);
52
+ const [content, setContent] = createSignal(null);
53
+ const [isUsingKeyboard, setIsUsingKeyboard] = createSignal(false);
54
+ const direction = useDirection(local.dir);
55
+ const open = () => local.open ?? false;
56
+ const modal = () => local.modal ?? true;
57
+ const handleOpenChange = (value) => local.onOpenChange?.(value);
58
+ createEffect(() => {
59
+ const handleKeyDown = () => {
60
+ setIsUsingKeyboard(true);
61
+ document.addEventListener("pointerdown", handlePointer, {
62
+ capture: true,
63
+ once: true
64
+ });
65
+ document.addEventListener("pointermove", handlePointer, {
66
+ capture: true,
67
+ once: true
68
+ });
69
+ };
70
+ const handlePointer = () => setIsUsingKeyboard(false);
71
+ document.addEventListener("keydown", handleKeyDown, { capture: true });
72
+ onCleanup(() => {
73
+ document.removeEventListener("keydown", handleKeyDown, { capture: true });
74
+ document.removeEventListener("pointerdown", handlePointer, {
75
+ capture: true
76
+ });
77
+ document.removeEventListener("pointermove", handlePointer, {
78
+ capture: true
79
+ });
80
+ });
81
+ });
82
+ return /* @__PURE__ */ React.createElement(Popper, { ...popperScope }, /* @__PURE__ */ React.createElement(
83
+ MenuProvider,
84
+ {
85
+ scope: local.__scopeMenu,
86
+ open: open(),
87
+ onOpenChange: handleOpenChange,
88
+ content: content(),
89
+ onContentChange: setContent
90
+ },
91
+ /* @__PURE__ */ React.createElement(
92
+ MenuRootProvider,
93
+ {
94
+ scope: local.__scopeMenu,
95
+ onClose: () => handleOpenChange(false),
96
+ isUsingKeyboard: isUsingKeyboard(),
97
+ onIsUsingKeyboardChange: setIsUsingKeyboard,
98
+ dir: direction,
99
+ modal: modal()
100
+ },
101
+ local.children
102
+ )
103
+ ));
104
+ }
105
+ Menu.displayName = MENU_NAME;
106
+ var ANCHOR_NAME = "MenuAnchor";
107
+ function MenuAnchor(inProps) {
108
+ const [local, rest] = splitProps(inProps, ["__scopeMenu", "ref"]);
109
+ const popperScope = usePopperScope(local.__scopeMenu);
110
+ return /* @__PURE__ */ React.createElement(PopperAnchor, { ...popperScope, ...rest, ref: local.ref });
111
+ }
112
+ MenuAnchor.displayName = ANCHOR_NAME;
113
+ var PORTAL_NAME = "MenuPortal";
114
+ var [PortalProvider, usePortalContext] = createMenuContext(PORTAL_NAME, {
115
+ forceMount: void 0
116
+ });
117
+ function MenuPortal(props) {
118
+ const [local] = splitProps(props, [
119
+ "__scopeMenu",
120
+ "forceMount",
121
+ "children",
122
+ "container"
123
+ ]);
124
+ const context = useMenuContext(PORTAL_NAME, local.__scopeMenu);
125
+ return /* @__PURE__ */ React.createElement(PortalProvider, { scope: local.__scopeMenu, forceMount: local.forceMount }, /* @__PURE__ */ React.createElement(Presence, { present: local.forceMount || context.open }, /* @__PURE__ */ React.createElement(Portal, { container: local.container }, local.children)));
126
+ }
127
+ MenuPortal.displayName = PORTAL_NAME;
128
+ var CONTENT_NAME = "MenuContent";
129
+ var [MenuContentProvider, useMenuContentContext] = createMenuContext(CONTENT_NAME);
130
+ function MenuContent(inProps) {
131
+ const [local, rest] = splitProps(inProps, ["__scopeMenu", "forceMount"]);
132
+ const portalContext = usePortalContext(CONTENT_NAME, local.__scopeMenu);
133
+ const context = useMenuContext(CONTENT_NAME, local.__scopeMenu);
134
+ const rootContext = useMenuRootContext(CONTENT_NAME, local.__scopeMenu);
135
+ const forceMount = () => local.forceMount ?? portalContext.forceMount;
136
+ return /* @__PURE__ */ React.createElement(Collection.Provider, { scope: local.__scopeMenu }, /* @__PURE__ */ React.createElement(Presence, { present: forceMount() || context.open }, /* @__PURE__ */ React.createElement(Collection.Slot, { scope: local.__scopeMenu }, /* @__PURE__ */ React.createElement(
137
+ Show,
138
+ {
139
+ when: rootContext.modal,
140
+ fallback: /* @__PURE__ */ React.createElement(
141
+ MenuRootContentNonModal,
142
+ {
143
+ ...rest,
144
+ __scopeMenu: local.__scopeMenu
145
+ }
146
+ )
147
+ },
148
+ /* @__PURE__ */ React.createElement(MenuRootContentModal, { ...rest, __scopeMenu: local.__scopeMenu })
149
+ ))));
150
+ }
151
+ MenuContent.displayName = CONTENT_NAME;
152
+ function MenuRootContentModal(inProps) {
153
+ const [local, rest] = splitProps(inProps, [
154
+ "__scopeMenu",
155
+ "ref",
156
+ "onFocusOutside"
157
+ ]);
158
+ const context = useMenuContext(CONTENT_NAME, local.__scopeMenu);
159
+ let contentRef;
160
+ createEffect(() => {
161
+ if (contentRef) {
162
+ const cleanup = hideOthers(contentRef);
163
+ onCleanup(() => cleanup?.());
164
+ }
165
+ });
166
+ return /* @__PURE__ */ React.createElement(
167
+ MenuContentImpl,
168
+ {
169
+ ...rest,
170
+ __scopeMenu: local.__scopeMenu,
171
+ ref: mergeRefs(local.ref, (el) => {
172
+ contentRef = el;
173
+ }),
174
+ trapFocus: context.open,
175
+ disableOutsidePointerEvents: context.open,
176
+ disableOutsideScroll: true,
177
+ onFocusOutside: composeEventHandlers(
178
+ local.onFocusOutside,
179
+ (event) => event.preventDefault()
180
+ ),
181
+ onDismiss: () => context.onOpenChange(false)
182
+ }
183
+ );
184
+ }
185
+ function MenuRootContentNonModal(inProps) {
186
+ const [local, rest] = splitProps(inProps, ["__scopeMenu"]);
187
+ const context = useMenuContext(CONTENT_NAME, local.__scopeMenu);
188
+ return /* @__PURE__ */ React.createElement(
189
+ MenuContentImpl,
190
+ {
191
+ ...rest,
192
+ __scopeMenu: local.__scopeMenu,
193
+ trapFocus: false,
194
+ disableOutsidePointerEvents: false,
195
+ disableOutsideScroll: false,
196
+ onDismiss: () => context.onOpenChange(false)
197
+ }
198
+ );
199
+ }
200
+ function MenuContentImpl(inProps) {
201
+ const [local, rest] = splitProps(inProps, [
202
+ "__scopeMenu",
203
+ "ref",
204
+ "loop",
205
+ "trapFocus",
206
+ "onOpenAutoFocus",
207
+ "onCloseAutoFocus",
208
+ "disableOutsidePointerEvents",
209
+ "disableOutsideScroll",
210
+ "onEntryFocus",
211
+ "onEscapeKeyDown",
212
+ "onPointerDownOutside",
213
+ "onFocusOutside",
214
+ "onInteractOutside",
215
+ "onDismiss",
216
+ "side",
217
+ "sideOffset",
218
+ "align",
219
+ "alignOffset",
220
+ "avoidCollisions",
221
+ "collisionBoundary",
222
+ "collisionPadding",
223
+ "arrowPadding",
224
+ "sticky",
225
+ "hideWhenDetached",
226
+ "style",
227
+ "onKeyDown",
228
+ "onBlur",
229
+ "onPointerMove"
230
+ ]);
231
+ const context = useMenuContext(CONTENT_NAME, local.__scopeMenu);
232
+ const rootContext = useMenuRootContext(CONTENT_NAME, local.__scopeMenu);
233
+ const popperScope = usePopperScope(local.__scopeMenu);
234
+ const rovingFocusGroupScope = useRovingFocusGroupScope(local.__scopeMenu);
235
+ const getItems = useCollection(local.__scopeMenu);
236
+ const [currentItemId, setCurrentItemId] = createSignal(null);
237
+ let contentRef;
238
+ let timerRef = 0;
239
+ let searchRef = "";
240
+ let pointerGraceTimerRef = 0;
241
+ let pointerGraceIntentRef = null;
242
+ let pointerDirRef = "right";
243
+ let lastPointerXRef = 0;
244
+ createEffect(() => {
245
+ if (local.disableOutsideScroll) {
246
+ const body = document.body;
247
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
248
+ const originalOverflow = body.style.overflow;
249
+ const originalPaddingRight = body.style.paddingRight;
250
+ body.style.overflow = "hidden";
251
+ if (scrollbarWidth > 0) {
252
+ body.style.paddingRight = `${scrollbarWidth}px`;
253
+ }
254
+ onCleanup(() => {
255
+ body.style.overflow = originalOverflow;
256
+ body.style.paddingRight = originalPaddingRight;
257
+ });
258
+ }
259
+ });
260
+ const handleTypeaheadSearch = (key) => {
261
+ const search = searchRef + key;
262
+ const items = getItems().filter((item) => !item.disabled);
263
+ const currentItem = document.activeElement;
264
+ const currentMatch = items.find(
265
+ (item) => item.ref === currentItem
266
+ )?.textValue;
267
+ const values = items.map((item) => item.textValue);
268
+ const nextMatch = getNextMatch(values, search, currentMatch);
269
+ const newItem = items.find(
270
+ (item) => item.textValue === nextMatch
271
+ )?.ref;
272
+ (function updateSearch(value) {
273
+ searchRef = value;
274
+ window.clearTimeout(timerRef);
275
+ if (value !== "")
276
+ timerRef = window.setTimeout(() => updateSearch(""), 1e3);
277
+ })(search);
278
+ if (newItem) {
279
+ setTimeout(() => newItem.focus());
280
+ }
281
+ };
282
+ onCleanup(() => window.clearTimeout(timerRef));
283
+ useFocusGuards();
284
+ const isPointerMovingToSubmenu = (event) => {
285
+ const isMovingTowards = pointerDirRef === pointerGraceIntentRef?.side;
286
+ return isMovingTowards && isPointerInGraceArea(event, pointerGraceIntentRef?.area);
287
+ };
288
+ return /* @__PURE__ */ React.createElement(
289
+ MenuContentProvider,
290
+ {
291
+ scope: local.__scopeMenu,
292
+ searchRef,
293
+ onSearchChange: (value) => {
294
+ searchRef = value;
295
+ },
296
+ onItemEnter: (event) => {
297
+ if (isPointerMovingToSubmenu(event)) event.preventDefault();
298
+ },
299
+ onItemLeave: (event) => {
300
+ if (isPointerMovingToSubmenu(event)) return;
301
+ contentRef?.focus();
302
+ setCurrentItemId(null);
303
+ },
304
+ onTriggerLeave: (event) => {
305
+ if (isPointerMovingToSubmenu(event)) event.preventDefault();
306
+ },
307
+ pointerGraceTimerRef,
308
+ onPointerGraceTimerChange: (value) => {
309
+ pointerGraceTimerRef = value;
310
+ },
311
+ onPointerGraceIntentChange: (intent) => {
312
+ pointerGraceIntentRef = intent;
313
+ }
314
+ },
315
+ /* @__PURE__ */ React.createElement(
316
+ FocusScope,
317
+ {
318
+ trapped: local.trapFocus,
319
+ onMountAutoFocus: composeEventHandlers(
320
+ local.onOpenAutoFocus,
321
+ (event) => {
322
+ event.preventDefault();
323
+ contentRef?.focus({ preventScroll: true });
324
+ }
325
+ ),
326
+ onUnmountAutoFocus: local.onCloseAutoFocus
327
+ },
328
+ /* @__PURE__ */ React.createElement(
329
+ DismissableLayer,
330
+ {
331
+ asChild: true,
332
+ disableOutsidePointerEvents: local.disableOutsidePointerEvents,
333
+ onEscapeKeyDown: local.onEscapeKeyDown,
334
+ onPointerDownOutside: local.onPointerDownOutside,
335
+ onFocusOutside: local.onFocusOutside,
336
+ onInteractOutside: local.onInteractOutside,
337
+ onDismiss: local.onDismiss
338
+ },
339
+ /* @__PURE__ */ React.createElement(
340
+ RovingFocusGroup,
341
+ {
342
+ asChild: true,
343
+ ...rovingFocusGroupScope,
344
+ dir: rootContext.dir,
345
+ orientation: "vertical",
346
+ loop: local.loop ?? false,
347
+ currentTabStopId: currentItemId(),
348
+ onCurrentTabStopIdChange: setCurrentItemId,
349
+ onEntryFocus: composeEventHandlers(
350
+ local.onEntryFocus,
351
+ (event) => {
352
+ if (!rootContext.isUsingKeyboard) event.preventDefault();
353
+ }
354
+ ),
355
+ preventScrollOnEntryFocus: true
356
+ },
357
+ /* @__PURE__ */ React.createElement(
358
+ PopperContent,
359
+ {
360
+ role: "menu",
361
+ "aria-orientation": "vertical",
362
+ "data-state": getOpenState(context.open),
363
+ "data-radix-menu-content": "",
364
+ dir: rootContext.dir,
365
+ ...popperScope,
366
+ ...rest,
367
+ ref: mergeRefs(local.ref, (el) => {
368
+ contentRef = el;
369
+ context.onContentChange(el);
370
+ }),
371
+ side: local.side,
372
+ sideOffset: local.sideOffset,
373
+ align: local.align,
374
+ alignOffset: local.alignOffset,
375
+ avoidCollisions: local.avoidCollisions,
376
+ collisionBoundary: local.collisionBoundary,
377
+ collisionPadding: local.collisionPadding,
378
+ arrowPadding: local.arrowPadding,
379
+ sticky: local.sticky,
380
+ hideWhenDetached: local.hideWhenDetached,
381
+ style: {
382
+ outline: "none",
383
+ ...typeof local.style === "object" ? local.style : {},
384
+ "--radix-menu-content-transform-origin": "var(--radix-popper-transform-origin)",
385
+ "--radix-menu-content-available-width": "var(--radix-popper-available-width)",
386
+ "--radix-menu-content-available-height": "var(--radix-popper-available-height)",
387
+ "--radix-menu-trigger-width": "var(--radix-popper-anchor-width)",
388
+ "--radix-menu-trigger-height": "var(--radix-popper-anchor-height)"
389
+ },
390
+ onKeyDown: composeEventHandlers(
391
+ local.onKeyDown,
392
+ (event) => {
393
+ const target = event.target;
394
+ const isKeyDownInside = target.closest("[data-radix-menu-content]") === event.currentTarget;
395
+ const isModifierKey = event.ctrlKey || event.altKey || event.metaKey;
396
+ const isCharacterKey = event.key.length === 1;
397
+ if (isKeyDownInside) {
398
+ if (event.key === "Tab") event.preventDefault();
399
+ if (!isModifierKey && isCharacterKey)
400
+ handleTypeaheadSearch(event.key);
401
+ }
402
+ const content = contentRef;
403
+ if (event.target !== content) return;
404
+ if (!FIRST_LAST_KEYS.includes(event.key)) return;
405
+ event.preventDefault();
406
+ const items = getItems().filter(
407
+ (item) => !item.disabled
408
+ );
409
+ const candidateNodes = items.map((item) => item.ref);
410
+ if (LAST_KEYS.includes(event.key)) candidateNodes.reverse();
411
+ focusFirst(candidateNodes);
412
+ }
413
+ ),
414
+ onBlur: composeEventHandlers(
415
+ local.onBlur,
416
+ (event) => {
417
+ if (!event.currentTarget.contains(
418
+ event.target
419
+ )) {
420
+ window.clearTimeout(timerRef);
421
+ searchRef = "";
422
+ }
423
+ }
424
+ ),
425
+ onPointerMove: composeEventHandlers(
426
+ local.onPointerMove,
427
+ whenMouse((event) => {
428
+ const target = event.target;
429
+ const pointerXHasChanged = lastPointerXRef !== event.clientX;
430
+ if (event.currentTarget.contains(target) && pointerXHasChanged) {
431
+ const newDir = event.clientX > lastPointerXRef ? "right" : "left";
432
+ pointerDirRef = newDir;
433
+ lastPointerXRef = event.clientX;
434
+ }
435
+ })
436
+ )
437
+ }
438
+ )
439
+ )
440
+ )
441
+ )
442
+ );
443
+ }
444
+ var GROUP_NAME = "MenuGroup";
445
+ function MenuGroup(inProps) {
446
+ const [local, rest] = splitProps(inProps, ["__scopeMenu", "ref"]);
447
+ return /* @__PURE__ */ React.createElement(Primitive.div, { role: "group", ...rest, ref: local.ref });
448
+ }
449
+ MenuGroup.displayName = GROUP_NAME;
450
+ var LABEL_NAME = "MenuLabel";
451
+ function MenuLabel(inProps) {
452
+ const [local, rest] = splitProps(inProps, ["__scopeMenu", "ref"]);
453
+ return /* @__PURE__ */ React.createElement(Primitive.div, { ...rest, ref: local.ref });
454
+ }
455
+ MenuLabel.displayName = LABEL_NAME;
456
+ var ITEM_NAME = "MenuItem";
457
+ var ITEM_SELECT = "menu.itemSelect";
458
+ function MenuItem(inProps) {
459
+ const [local, rest] = splitProps(inProps, [
460
+ "__scopeMenu",
461
+ "disabled",
462
+ "onSelect",
463
+ "ref",
464
+ "onClick",
465
+ "onPointerDown",
466
+ "onPointerUp",
467
+ "onKeyDown"
468
+ ]);
469
+ const rootContext = useMenuRootContext(ITEM_NAME, local.__scopeMenu);
470
+ const contentContext = useMenuContentContext(ITEM_NAME, local.__scopeMenu);
471
+ let itemRef;
472
+ let isPointerDownRef = false;
473
+ const disabled = () => local.disabled ?? false;
474
+ const handleSelect = () => {
475
+ const menuItem = itemRef;
476
+ if (!disabled() && menuItem) {
477
+ const itemSelectEvent = new CustomEvent(ITEM_SELECT, {
478
+ bubbles: true,
479
+ cancelable: true
480
+ });
481
+ menuItem.addEventListener(
482
+ ITEM_SELECT,
483
+ (event) => local.onSelect?.(event),
484
+ { once: true }
485
+ );
486
+ dispatchDiscreteCustomEvent(menuItem, itemSelectEvent);
487
+ if (itemSelectEvent.defaultPrevented) {
488
+ isPointerDownRef = false;
489
+ } else {
490
+ rootContext.onClose();
491
+ }
492
+ }
493
+ };
494
+ return /* @__PURE__ */ React.createElement(
495
+ MenuItemImpl,
496
+ {
497
+ ...rest,
498
+ __scopeMenu: local.__scopeMenu,
499
+ ref: mergeRefs(local.ref, (el) => {
500
+ itemRef = el;
501
+ }),
502
+ disabled: disabled(),
503
+ onClick: composeEventHandlers(local.onClick, handleSelect),
504
+ onPointerDown: composeEventHandlers(local.onPointerDown, () => {
505
+ isPointerDownRef = true;
506
+ }),
507
+ onPointerUp: composeEventHandlers(
508
+ local.onPointerUp,
509
+ (event) => {
510
+ if (!isPointerDownRef) event.currentTarget?.click();
511
+ }
512
+ ),
513
+ onKeyDown: composeEventHandlers(
514
+ local.onKeyDown,
515
+ (event) => {
516
+ const isTypingAhead = contentContext.searchRef !== "";
517
+ if (disabled() || isTypingAhead && event.key === " ") return;
518
+ if (SELECTION_KEYS.includes(event.key)) {
519
+ event.currentTarget.click();
520
+ event.preventDefault();
521
+ }
522
+ }
523
+ )
524
+ }
525
+ );
526
+ }
527
+ MenuItem.displayName = ITEM_NAME;
528
+ function MenuItemImpl(inProps) {
529
+ const [local, rest] = splitProps(inProps, [
530
+ "__scopeMenu",
531
+ "disabled",
532
+ "textValue",
533
+ "ref",
534
+ "onPointerMove",
535
+ "onPointerLeave",
536
+ "onFocus",
537
+ "onBlur"
538
+ ]);
539
+ const contentContext = useMenuContentContext(ITEM_NAME, local.__scopeMenu);
540
+ const rovingFocusGroupScope = useRovingFocusGroupScope(local.__scopeMenu);
541
+ let itemRef;
542
+ const [isFocused, setIsFocused] = createSignal(false);
543
+ const [textContent, setTextContent] = createSignal("");
544
+ const disabled = () => local.disabled ?? false;
545
+ onMount(() => {
546
+ if (itemRef) {
547
+ setTextContent((itemRef.textContent ?? "").trim());
548
+ }
549
+ });
550
+ return /* @__PURE__ */ React.createElement(
551
+ Collection.ItemSlot,
552
+ {
553
+ scope: local.__scopeMenu,
554
+ disabled: disabled(),
555
+ textValue: local.textValue ?? textContent()
556
+ },
557
+ /* @__PURE__ */ React.createElement(
558
+ RovingFocusGroupItem,
559
+ {
560
+ asChild: true,
561
+ ...rovingFocusGroupScope,
562
+ focusable: !disabled()
563
+ },
564
+ /* @__PURE__ */ React.createElement(
565
+ Primitive.div,
566
+ {
567
+ role: "menuitem",
568
+ "data-highlighted": isFocused() ? "" : void 0,
569
+ "aria-disabled": disabled() || void 0,
570
+ "data-disabled": disabled() ? "" : void 0,
571
+ ...rest,
572
+ ref: mergeRefs(local.ref, (el) => {
573
+ itemRef = el;
574
+ }),
575
+ onPointerMove: composeEventHandlers(
576
+ local.onPointerMove,
577
+ whenMouse((event) => {
578
+ if (disabled()) {
579
+ contentContext.onItemLeave(event);
580
+ } else {
581
+ contentContext.onItemEnter(event);
582
+ if (!event.defaultPrevented) {
583
+ const item = event.currentTarget;
584
+ item.focus({ preventScroll: true });
585
+ }
586
+ }
587
+ })
588
+ ),
589
+ onPointerLeave: composeEventHandlers(
590
+ local.onPointerLeave,
591
+ whenMouse(
592
+ (event) => contentContext.onItemLeave(event)
593
+ )
594
+ ),
595
+ onFocus: composeEventHandlers(
596
+ local.onFocus,
597
+ () => setIsFocused(true)
598
+ ),
599
+ onBlur: composeEventHandlers(
600
+ local.onBlur,
601
+ () => setIsFocused(false)
602
+ )
603
+ }
604
+ )
605
+ )
606
+ );
607
+ }
608
+ var CHECKBOX_ITEM_NAME = "MenuCheckboxItem";
609
+ function MenuCheckboxItem(inProps) {
610
+ const [local, rest] = splitProps(inProps, [
611
+ "__scopeMenu",
612
+ "checked",
613
+ "onCheckedChange",
614
+ "onSelect"
615
+ ]);
616
+ const checked = () => local.checked ?? false;
617
+ return /* @__PURE__ */ React.createElement(ItemIndicatorProvider, { scope: local.__scopeMenu, checked: checked() }, /* @__PURE__ */ React.createElement(
618
+ MenuItem,
619
+ {
620
+ role: "menuitemcheckbox",
621
+ "aria-checked": isIndeterminate(checked()) ? "mixed" : checked(),
622
+ ...rest,
623
+ __scopeMenu: local.__scopeMenu,
624
+ "data-state": getCheckedState(checked()),
625
+ onSelect: composeEventHandlers(
626
+ local.onSelect,
627
+ () => local.onCheckedChange?.(
628
+ isIndeterminate(checked()) ? true : !checked()
629
+ ),
630
+ { checkForDefaultPrevented: false }
631
+ )
632
+ }
633
+ ));
634
+ }
635
+ MenuCheckboxItem.displayName = CHECKBOX_ITEM_NAME;
636
+ var RADIO_GROUP_NAME = "MenuRadioGroup";
637
+ var [RadioGroupProvider, useRadioGroupContext] = createMenuContext(RADIO_GROUP_NAME, {
638
+ value: void 0,
639
+ onValueChange: () => {
640
+ }
641
+ });
642
+ function MenuRadioGroup(inProps) {
643
+ const [local, rest] = splitProps(inProps, [
644
+ "__scopeMenu",
645
+ "value",
646
+ "onValueChange"
647
+ ]);
648
+ return /* @__PURE__ */ React.createElement(
649
+ RadioGroupProvider,
650
+ {
651
+ scope: local.__scopeMenu,
652
+ value: local.value,
653
+ onValueChange: local.onValueChange
654
+ },
655
+ /* @__PURE__ */ React.createElement(MenuGroup, { ...rest, __scopeMenu: local.__scopeMenu })
656
+ );
657
+ }
658
+ MenuRadioGroup.displayName = RADIO_GROUP_NAME;
659
+ var RADIO_ITEM_NAME = "MenuRadioItem";
660
+ function MenuRadioItem(inProps) {
661
+ const [local, rest] = splitProps(inProps, [
662
+ "__scopeMenu",
663
+ "value",
664
+ "onSelect"
665
+ ]);
666
+ const context = useRadioGroupContext(RADIO_ITEM_NAME, local.__scopeMenu);
667
+ const checked = () => local.value === context.value;
668
+ return /* @__PURE__ */ React.createElement(ItemIndicatorProvider, { scope: local.__scopeMenu, checked: checked() }, /* @__PURE__ */ React.createElement(
669
+ MenuItem,
670
+ {
671
+ role: "menuitemradio",
672
+ "aria-checked": checked(),
673
+ ...rest,
674
+ __scopeMenu: local.__scopeMenu,
675
+ "data-state": getCheckedState(checked()),
676
+ onSelect: composeEventHandlers(
677
+ local.onSelect,
678
+ () => context.onValueChange?.(local.value),
679
+ { checkForDefaultPrevented: false }
680
+ )
681
+ }
682
+ ));
683
+ }
684
+ MenuRadioItem.displayName = RADIO_ITEM_NAME;
685
+ var ITEM_INDICATOR_NAME = "MenuItemIndicator";
686
+ var [ItemIndicatorProvider, useItemIndicatorContext] = createMenuContext(ITEM_INDICATOR_NAME, {
687
+ checked: false
688
+ });
689
+ function MenuItemIndicator(inProps) {
690
+ const [local, rest] = splitProps(inProps, [
691
+ "__scopeMenu",
692
+ "forceMount",
693
+ "ref"
694
+ ]);
695
+ const indicatorContext = useItemIndicatorContext(
696
+ ITEM_INDICATOR_NAME,
697
+ local.__scopeMenu
698
+ );
699
+ return /* @__PURE__ */ React.createElement(
700
+ Presence,
701
+ {
702
+ present: local.forceMount || isIndeterminate(indicatorContext.checked) || indicatorContext.checked === true
703
+ },
704
+ /* @__PURE__ */ React.createElement(
705
+ Primitive.span,
706
+ {
707
+ ...rest,
708
+ ref: local.ref,
709
+ "data-state": getCheckedState(indicatorContext.checked)
710
+ }
711
+ )
712
+ );
713
+ }
714
+ MenuItemIndicator.displayName = ITEM_INDICATOR_NAME;
715
+ var SEPARATOR_NAME = "MenuSeparator";
716
+ function MenuSeparator(inProps) {
717
+ const [local, rest] = splitProps(inProps, ["__scopeMenu", "ref"]);
718
+ return /* @__PURE__ */ React.createElement(
719
+ Primitive.div,
720
+ {
721
+ role: "separator",
722
+ "aria-orientation": "horizontal",
723
+ ...rest,
724
+ ref: local.ref
725
+ }
726
+ );
727
+ }
728
+ MenuSeparator.displayName = SEPARATOR_NAME;
729
+ var ARROW_NAME = "MenuArrow";
730
+ function MenuArrow(inProps) {
731
+ const [local, rest] = splitProps(inProps, ["__scopeMenu"]);
732
+ const popperScope = usePopperScope(local.__scopeMenu);
733
+ return /* @__PURE__ */ React.createElement(PopperArrow, { ...popperScope, ...rest });
734
+ }
735
+ MenuArrow.displayName = ARROW_NAME;
736
+ var SUB_NAME = "MenuSub";
737
+ var [MenuSubProvider, useMenuSubContext] = createMenuContext(SUB_NAME);
738
+ function MenuSub(props) {
739
+ const [local] = splitProps(props, [
740
+ "__scopeMenu",
741
+ "children",
742
+ "open",
743
+ "onOpenChange"
744
+ ]);
745
+ const parentMenuContext = useMenuContext(SUB_NAME, local.__scopeMenu);
746
+ const popperScope = usePopperScope(local.__scopeMenu);
747
+ const [trigger, setTrigger] = createSignal(null);
748
+ const [content, setContent] = createSignal(null);
749
+ const open = () => local.open ?? false;
750
+ const handleOpenChange = (value) => local.onOpenChange?.(value);
751
+ createEffect(() => {
752
+ if (parentMenuContext.open === false) handleOpenChange(false);
753
+ onCleanup(() => handleOpenChange(false));
754
+ });
755
+ return /* @__PURE__ */ React.createElement(Popper, { ...popperScope }, /* @__PURE__ */ React.createElement(
756
+ MenuProvider,
757
+ {
758
+ scope: local.__scopeMenu,
759
+ open: open(),
760
+ onOpenChange: handleOpenChange,
761
+ content: content(),
762
+ onContentChange: setContent
763
+ },
764
+ /* @__PURE__ */ React.createElement(
765
+ MenuSubProvider,
766
+ {
767
+ scope: local.__scopeMenu,
768
+ contentId: createId(),
769
+ triggerId: createId(),
770
+ trigger: trigger(),
771
+ onTriggerChange: setTrigger
772
+ },
773
+ local.children
774
+ )
775
+ ));
776
+ }
777
+ MenuSub.displayName = SUB_NAME;
778
+ var SUB_TRIGGER_NAME = "MenuSubTrigger";
779
+ function MenuSubTrigger(inProps) {
780
+ const [local, rest] = splitProps(inProps, [
781
+ "__scopeMenu",
782
+ "ref",
783
+ "onClick",
784
+ "onPointerMove",
785
+ "onPointerLeave",
786
+ "onKeyDown"
787
+ ]);
788
+ const context = useMenuContext(SUB_TRIGGER_NAME, local.__scopeMenu);
789
+ const rootContext = useMenuRootContext(SUB_TRIGGER_NAME, local.__scopeMenu);
790
+ const subContext = useMenuSubContext(SUB_TRIGGER_NAME, local.__scopeMenu);
791
+ const contentContext = useMenuContentContext(
792
+ SUB_TRIGGER_NAME,
793
+ local.__scopeMenu
794
+ );
795
+ let openTimerRef = null;
796
+ const clearOpenTimer = () => {
797
+ if (openTimerRef) window.clearTimeout(openTimerRef);
798
+ openTimerRef = null;
799
+ };
800
+ onCleanup(() => {
801
+ clearOpenTimer();
802
+ window.clearTimeout(contentContext.pointerGraceTimerRef);
803
+ contentContext.onPointerGraceIntentChange(null);
804
+ });
805
+ return /* @__PURE__ */ React.createElement(MenuAnchor, { __scopeMenu: local.__scopeMenu }, /* @__PURE__ */ React.createElement(
806
+ MenuItemImpl,
807
+ {
808
+ id: subContext.triggerId,
809
+ "aria-haspopup": "menu",
810
+ "aria-expanded": context.open,
811
+ "aria-controls": subContext.contentId,
812
+ "data-state": getOpenState(context.open),
813
+ ...rest,
814
+ __scopeMenu: local.__scopeMenu,
815
+ ref: mergeRefs(
816
+ local.ref,
817
+ (el) => subContext.onTriggerChange(el)
818
+ ),
819
+ onClick: composeEventHandlers(
820
+ local.onClick,
821
+ (event) => {
822
+ if (rest.disabled || event.defaultPrevented) return;
823
+ event.currentTarget.focus();
824
+ if (!context.open) context.onOpenChange(true);
825
+ }
826
+ ),
827
+ onPointerMove: composeEventHandlers(
828
+ local.onPointerMove,
829
+ whenMouse((event) => {
830
+ contentContext.onItemEnter(event);
831
+ if (event.defaultPrevented) return;
832
+ if (!rest.disabled && !context.open && !openTimerRef) {
833
+ contentContext.onPointerGraceIntentChange(null);
834
+ openTimerRef = window.setTimeout(() => {
835
+ context.onOpenChange(true);
836
+ clearOpenTimer();
837
+ }, 100);
838
+ }
839
+ })
840
+ ),
841
+ onPointerLeave: composeEventHandlers(
842
+ local.onPointerLeave,
843
+ whenMouse((event) => {
844
+ clearOpenTimer();
845
+ const contentRect = context.content?.getBoundingClientRect();
846
+ if (contentRect) {
847
+ const side = context.content?.dataset.side;
848
+ const rightSide = side === "right";
849
+ const bleed = rightSide ? -5 : 5;
850
+ const contentNearEdge = contentRect[rightSide ? "left" : "right"];
851
+ const contentFarEdge = contentRect[rightSide ? "right" : "left"];
852
+ contentContext.onPointerGraceIntentChange({
853
+ area: [
854
+ { x: event.clientX + bleed, y: event.clientY },
855
+ { x: contentNearEdge, y: contentRect.top },
856
+ { x: contentFarEdge, y: contentRect.top },
857
+ { x: contentFarEdge, y: contentRect.bottom },
858
+ { x: contentNearEdge, y: contentRect.bottom }
859
+ ],
860
+ side
861
+ });
862
+ window.clearTimeout(contentContext.pointerGraceTimerRef);
863
+ contentContext.onPointerGraceTimerChange(
864
+ window.setTimeout(
865
+ () => contentContext.onPointerGraceIntentChange(null),
866
+ 300
867
+ )
868
+ );
869
+ } else {
870
+ contentContext.onTriggerLeave(event);
871
+ if (event.defaultPrevented) return;
872
+ contentContext.onPointerGraceIntentChange(null);
873
+ }
874
+ })
875
+ ),
876
+ onKeyDown: composeEventHandlers(
877
+ local.onKeyDown,
878
+ (event) => {
879
+ const isTypingAhead = contentContext.searchRef !== "";
880
+ if (rest.disabled || isTypingAhead && event.key === " ") return;
881
+ if (SUB_OPEN_KEYS[rootContext.dir].includes(event.key)) {
882
+ context.onOpenChange(true);
883
+ context.content?.focus();
884
+ event.preventDefault();
885
+ }
886
+ }
887
+ )
888
+ }
889
+ ));
890
+ }
891
+ MenuSubTrigger.displayName = SUB_TRIGGER_NAME;
892
+ var SUB_CONTENT_NAME = "MenuSubContent";
893
+ function MenuSubContent(inProps) {
894
+ const [local, rest] = splitProps(inProps, [
895
+ "__scopeMenu",
896
+ "forceMount",
897
+ "ref",
898
+ "onFocusOutside",
899
+ "onEscapeKeyDown",
900
+ "onKeyDown"
901
+ ]);
902
+ const portalContext = usePortalContext(CONTENT_NAME, local.__scopeMenu);
903
+ const context = useMenuContext(CONTENT_NAME, local.__scopeMenu);
904
+ const rootContext = useMenuRootContext(CONTENT_NAME, local.__scopeMenu);
905
+ const subContext = useMenuSubContext(SUB_CONTENT_NAME, local.__scopeMenu);
906
+ const forceMount = () => local.forceMount ?? portalContext.forceMount;
907
+ let contentRef;
908
+ return /* @__PURE__ */ React.createElement(Collection.Provider, { scope: local.__scopeMenu }, /* @__PURE__ */ React.createElement(Presence, { present: forceMount() || context.open }, /* @__PURE__ */ React.createElement(Collection.Slot, { scope: local.__scopeMenu }, /* @__PURE__ */ React.createElement(
909
+ MenuContentImpl,
910
+ {
911
+ id: subContext.contentId,
912
+ "aria-labelledby": subContext.triggerId,
913
+ ...rest,
914
+ __scopeMenu: local.__scopeMenu,
915
+ ref: mergeRefs(local.ref, (el) => {
916
+ contentRef = el;
917
+ }),
918
+ align: "start",
919
+ side: rootContext.dir === "rtl" ? "left" : "right",
920
+ disableOutsidePointerEvents: false,
921
+ disableOutsideScroll: false,
922
+ trapFocus: false,
923
+ onOpenAutoFocus: (event) => {
924
+ if (rootContext.isUsingKeyboard) contentRef?.focus();
925
+ event.preventDefault();
926
+ },
927
+ onCloseAutoFocus: (event) => event.preventDefault(),
928
+ onFocusOutside: composeEventHandlers(
929
+ local.onFocusOutside,
930
+ (event) => {
931
+ if (event.target !== subContext.trigger)
932
+ context.onOpenChange(false);
933
+ }
934
+ ),
935
+ onEscapeKeyDown: composeEventHandlers(
936
+ local.onEscapeKeyDown,
937
+ (event) => {
938
+ rootContext.onClose();
939
+ event.preventDefault();
940
+ }
941
+ ),
942
+ onKeyDown: composeEventHandlers(
943
+ local.onKeyDown,
944
+ (event) => {
945
+ const isKeyDownInside = event.currentTarget.contains(event.target);
946
+ const isCloseKey = SUB_CLOSE_KEYS[rootContext.dir].includes(
947
+ event.key
948
+ );
949
+ if (isKeyDownInside && isCloseKey) {
950
+ context.onOpenChange(false);
951
+ subContext.trigger?.focus();
952
+ event.preventDefault();
953
+ }
954
+ }
955
+ )
956
+ }
957
+ ))));
958
+ }
959
+ MenuSubContent.displayName = SUB_CONTENT_NAME;
960
+ function getOpenState(open) {
961
+ return open ? "open" : "closed";
962
+ }
963
+ function isIndeterminate(checked) {
964
+ return checked === "indeterminate";
965
+ }
966
+ function getCheckedState(checked) {
967
+ return isIndeterminate(checked) ? "indeterminate" : checked ? "checked" : "unchecked";
968
+ }
969
+ function focusFirst(candidates) {
970
+ const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;
971
+ for (const candidate of candidates) {
972
+ if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return;
973
+ candidate.focus();
974
+ if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return;
975
+ }
976
+ }
977
+ function wrapArray(array, startIndex) {
978
+ return array.map(
979
+ (_, index) => array[(startIndex + index) % array.length]
980
+ );
981
+ }
982
+ function getNextMatch(values, search, currentMatch) {
983
+ const isRepeated = search.length > 1 && Array.from(search).every((char) => char === search[0]);
984
+ const normalizedSearch = isRepeated ? search[0] : search;
985
+ const currentMatchIndex = currentMatch ? values.indexOf(currentMatch) : -1;
986
+ let wrappedValues = wrapArray(values, Math.max(currentMatchIndex, 0));
987
+ const excludeCurrentMatch = normalizedSearch.length === 1;
988
+ if (excludeCurrentMatch)
989
+ wrappedValues = wrappedValues.filter((v) => v !== currentMatch);
990
+ const nextMatch = wrappedValues.find(
991
+ (value) => value.toLowerCase().startsWith(normalizedSearch.toLowerCase())
992
+ );
993
+ return nextMatch !== currentMatch ? nextMatch : void 0;
994
+ }
995
+ function isPointInPolygon(point, polygon) {
996
+ const { x, y } = point;
997
+ let inside = false;
998
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
999
+ const ii = polygon[i];
1000
+ const jj = polygon[j];
1001
+ const xi = ii.x;
1002
+ const yi = ii.y;
1003
+ const xj = jj.x;
1004
+ const yj = jj.y;
1005
+ const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi;
1006
+ if (intersect) inside = !inside;
1007
+ }
1008
+ return inside;
1009
+ }
1010
+ function isPointerInGraceArea(event, area) {
1011
+ if (!area) return false;
1012
+ const cursorPos = { x: event.clientX, y: event.clientY };
1013
+ return isPointInPolygon(cursorPos, area);
1014
+ }
1015
+ function whenMouse(handler) {
1016
+ return (event) => event.pointerType === "mouse" ? handler(event) : void 0;
1017
+ }
1018
+ var Root = Menu;
1019
+ var Anchor = MenuAnchor;
1020
+ var PortalExport = MenuPortal;
1021
+ var Content = MenuContent;
1022
+ var Group = MenuGroup;
1023
+ var Label = MenuLabel;
1024
+ var Item = MenuItem;
1025
+ var CheckboxItem = MenuCheckboxItem;
1026
+ var RadioGroup = MenuRadioGroup;
1027
+ var RadioItem = MenuRadioItem;
1028
+ var ItemIndicator = MenuItemIndicator;
1029
+ var Separator = MenuSeparator;
1030
+ var Arrow = MenuArrow;
1031
+ var Sub = MenuSub;
1032
+ var SubTrigger = MenuSubTrigger;
1033
+ var SubContent = MenuSubContent;
1034
+
1035
+ export { Anchor, Arrow, CheckboxItem, Content, Group, Item, ItemIndicator, Label, Menu, MenuAnchor, MenuArrow, MenuCheckboxItem, MenuContent, MenuGroup, MenuItem, MenuItemIndicator, MenuLabel, MenuPortal, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSub, MenuSubContent, MenuSubTrigger, PortalExport as Portal, RadioGroup, RadioItem, Root, Separator, Sub, SubContent, SubTrigger, createMenuScope };
1036
+ //# sourceMappingURL=index.js.map
1037
+ //# sourceMappingURL=index.js.map