@huin-core/react-menu 1.0.1 → 1.0.3

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