@arun-dev/headless 4.15.0 → 4.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  fold
3
3
  } from "../chunk-ETNICT5M.js";
4
+ import {
5
+ byDocumentOrder,
6
+ rovingIndex
7
+ } from "../chunk-2S4XG4VD.js";
4
8
  import {
5
9
  anchorNameFor,
6
10
  anchoredDataAttributes,
7
11
  anchoredPopupStyle
8
12
  } from "../chunk-HCOCPQXS.js";
9
- import {
10
- byDocumentOrder,
11
- rovingIndex
12
- } from "../chunk-2S4XG4VD.js";
13
13
  import {
14
14
  useControlled
15
15
  } from "../chunk-UEIB3I3T.js";
@@ -36,6 +36,8 @@ __export(index_parts_exports, {
36
36
  RadioGroup: () => MenuRadioGroup,
37
37
  RadioItem: () => MenuRadioItem,
38
38
  Root: () => MenuRoot,
39
+ SubmenuRoot: () => MenuSubmenuRoot,
40
+ SubmenuTrigger: () => MenuSubmenuTrigger,
39
41
  Trigger: () => MenuTrigger
40
42
  });
41
43
 
@@ -139,17 +141,20 @@ function useMenuRootContext(part) {
139
141
 
140
142
  // src/menu/MenuRoot.tsx
141
143
  var import_jsx_runtime = require("react/jsx-runtime");
142
- function MenuRoot({
144
+ function MenuRoot({ children, ...props }) {
145
+ const context = useMenuRootValue("Menu.Root", props, null);
146
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MenuRootContext.Provider, { value: context, children });
147
+ }
148
+ function useMenuRootValue(name, {
143
149
  open: openProp,
144
150
  defaultOpen,
145
151
  onOpenChange,
146
- focusableWhenDisabled = false,
147
- children
148
- }) {
152
+ focusableWhenDisabled = false
153
+ }, parent) {
149
154
  const [open, setOpenState] = useControlled({
150
155
  controlled: openProp,
151
156
  default: defaultOpen ?? false,
152
- name: "Menu.Root",
157
+ name,
153
158
  state: "open"
154
159
  });
155
160
  const setOpen = (0, import_react3.useCallback)(
@@ -160,6 +165,10 @@ function MenuRoot({
160
165
  },
161
166
  [open, setOpenState, onOpenChange]
162
167
  );
168
+ const closeAll = (0, import_react3.useCallback)(() => {
169
+ setOpen(false);
170
+ parent?.closeAll();
171
+ }, [setOpen, parent]);
163
172
  const [items, setItems] = (0, import_react3.useState)([]);
164
173
  const register = (0, import_react3.useCallback)((entry) => {
165
174
  setItems((current) => [...current, entry].sort(byDocumentOrder));
@@ -172,10 +181,14 @@ function MenuRoot({
172
181
  const id = (0, import_react3.useId)();
173
182
  const triggerRef = (0, import_react3.useRef)(null);
174
183
  const focusOnOpenRef = (0, import_react3.useRef)("first");
175
- const context = (0, import_react3.useMemo)(
184
+ const popupRef = (0, import_react3.useRef)(null);
185
+ const openSubmenuRef = (0, import_react3.useRef)(null);
186
+ const graceRef = (0, import_react3.useRef)(null);
187
+ return (0, import_react3.useMemo)(
176
188
  () => ({
177
189
  open,
178
190
  setOpen,
191
+ closeAll,
179
192
  navigableItems,
180
193
  focusableWhenDisabled,
181
194
  register,
@@ -183,11 +196,14 @@ function MenuRoot({
183
196
  triggerId: `${id}-trigger`,
184
197
  popupId: `${id}-popup`,
185
198
  anchorName: anchorNameFor(id),
186
- triggerRef
199
+ triggerRef,
200
+ parent,
201
+ popupRef,
202
+ openSubmenuRef,
203
+ graceRef
187
204
  }),
188
- [open, setOpen, navigableItems, focusableWhenDisabled, register, id]
205
+ [open, setOpen, closeAll, navigableItems, focusableWhenDisabled, register, id, parent]
189
206
  );
190
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MenuRootContext.Provider, { value: context, children });
191
207
  }
192
208
 
193
209
  // src/core/useRender.ts
@@ -373,7 +389,7 @@ function useTypeahead() {
373
389
 
374
390
  // src/menu/MenuPopup.tsx
375
391
  function MenuPopup({
376
- side = "bottom",
392
+ side: sideProp,
377
393
  align = "start",
378
394
  className,
379
395
  children,
@@ -388,9 +404,12 @@ function MenuPopup({
388
404
  triggerId,
389
405
  popupId,
390
406
  anchorName,
391
- triggerRef
407
+ triggerRef,
408
+ parent,
409
+ popupRef: elementRef,
410
+ openSubmenuRef
392
411
  } = useMenuRootContext("Popup");
393
- const elementRef = (0, import_react6.useRef)(null);
412
+ const side = sideProp ?? (parent ? "right" : "bottom");
394
413
  const focusPendingRef = (0, import_react6.useRef)(false);
395
414
  const [, resync] = (0, import_react6.useReducer)((n) => n + 1, 0);
396
415
  const typeahead = useTypeahead();
@@ -408,7 +427,8 @@ function MenuPopup({
408
427
  }
409
428
  if (focusPendingRef.current && navigableItems.length > 0) {
410
429
  focusPendingRef.current = false;
411
- const target = focusOnOpenRef.current === "last" ? navigableItems.at(-1) : navigableItems[0];
430
+ const focus = focusOnOpenRef.current;
431
+ const target = focus === "last" ? navigableItems.at(-1) : focus === "first" ? navigableItems[0] : null;
412
432
  target?.ref.current?.focus();
413
433
  }
414
434
  });
@@ -433,6 +453,12 @@ function MenuPopup({
433
453
  }
434
454
  const current = navigableItems.findIndex((i) => i.ref.current === event.target);
435
455
  if (current < 0 && event.target !== event.currentTarget) return;
456
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
457
+ if (parent && event.key === (rtl ? "ArrowRight" : "ArrowLeft")) {
458
+ event.preventDefault();
459
+ setOpen(false);
460
+ return;
461
+ }
436
462
  const search = typeahead(event);
437
463
  const next = search === null ? rovingIndex(event.key, {
438
464
  count: navigableItems.length,
@@ -447,7 +473,15 @@ function MenuPopup({
447
473
  if (search !== null) event.preventDefault();
448
474
  if (next === null) return;
449
475
  event.preventDefault();
450
- navigableItems[next]?.ref.current?.focus();
476
+ const target = navigableItems[next]?.ref.current;
477
+ const submenu = openSubmenuRef.current;
478
+ if (submenu && submenu.triggerRef.current !== target) submenu.close();
479
+ target?.focus();
480
+ },
481
+ onBeforeToggle(event) {
482
+ if (event.target !== event.currentTarget || !parent) return;
483
+ if (event.nativeEvent.newState !== "closed") return;
484
+ if (event.currentTarget.contains(document.activeElement)) triggerRef.current?.focus();
451
485
  },
452
486
  onToggle(event) {
453
487
  if (event.target !== event.currentTarget) return;
@@ -462,23 +496,60 @@ function MenuPopup({
462
496
 
463
497
  // src/menu/useMenuItem.ts
464
498
  var import_react7 = require("react");
465
- function useMenuItem(part, { disabled, textValue, children }) {
466
- const { setOpen, focusableWhenDisabled, register } = useMenuRootContext(part);
499
+
500
+ // src/core/pointerIntent.ts
501
+ function graceArea(exit, popup) {
502
+ const toRight = popup.left >= exit.x;
503
+ const edge = toRight ? popup.left : popup.right;
504
+ const back = toRight ? -5 : 5;
505
+ return [
506
+ { x: exit.x + back, y: exit.y - 5 },
507
+ { x: edge, y: popup.top },
508
+ { x: edge, y: popup.bottom },
509
+ { x: exit.x + back, y: exit.y + 5 }
510
+ ];
511
+ }
512
+ function isPointInPolygon({ x, y }, polygon) {
513
+ let inside = false;
514
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i, i += 1) {
515
+ const a = polygon[i];
516
+ const b = polygon[j];
517
+ if (!a || !b) continue;
518
+ if (a.y > y !== b.y > y && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x) {
519
+ inside = !inside;
520
+ }
521
+ }
522
+ return inside;
523
+ }
524
+
525
+ // src/menu/useMenuItem.ts
526
+ function useMenuItem(part, { disabled, textValue, children }, menu) {
527
+ const own = useMenuRootContext(part);
528
+ const { closeAll, focusableWhenDisabled, register, openSubmenuRef, graceRef } = menu ?? own;
467
529
  const ref = (0, import_react7.useRef)(null);
468
530
  const text = textValue ?? textOf(children);
469
531
  (0, import_react7.useLayoutEffect)(() => register({ disabled, textValue: text, ref }), [register, disabled, text]);
470
532
  return {
471
- setOpen,
533
+ closeAll,
472
534
  itemProps: {
473
535
  type: "button",
474
536
  tabIndex: -1,
475
537
  disabled: disabled && !focusableWhenDisabled || void 0,
476
538
  "aria-disabled": disabled && focusableWhenDisabled || void 0,
477
539
  "data-disabled": disabled ? "" : void 0,
478
- ref
540
+ ref,
541
+ onPointerMove(event) {
542
+ if (event.pointerType !== "mouse" || !openSubmenuRef.current) return;
543
+ if (isInGrace(graceRef.current, event)) return;
544
+ openSubmenuRef.current.close();
545
+ }
479
546
  }
480
547
  };
481
548
  }
549
+ function isInGrace(grace, event) {
550
+ if (!grace || performance.now() > grace.until) return false;
551
+ return isPointInPolygon({ x: event.clientX, y: event.clientY }, grace.area);
552
+ }
482
553
  var MenuItemIndicatorContext = (0, import_react7.createContext)(null);
483
554
  function useMenuItemIndicatorContext() {
484
555
  const context = (0, import_react7.useContext)(MenuItemIndicatorContext);
@@ -507,7 +578,7 @@ function MenuItem({
507
578
  render,
508
579
  ...rest
509
580
  }) {
510
- const { setOpen, itemProps } = useMenuItem("Item", { disabled, textValue, children });
581
+ const { closeAll, itemProps } = useMenuItem("Item", { disabled, textValue, children });
511
582
  return useRender({
512
583
  render,
513
584
  defaultTagName: "button",
@@ -517,7 +588,7 @@ function MenuItem({
517
588
  className,
518
589
  children,
519
590
  onClick() {
520
- if (!disabled) setOpen(false);
591
+ if (!disabled) closeAll();
521
592
  }
522
593
  },
523
594
  consumerProps: disabled ? rest : { ...rest, onClick }
@@ -540,7 +611,7 @@ function MenuCheckboxItem({
540
611
  render,
541
612
  ...rest
542
613
  }) {
543
- const { setOpen, itemProps } = useMenuItem("CheckboxItem", { disabled, textValue, children });
614
+ const { closeAll, itemProps } = useMenuItem("CheckboxItem", { disabled, textValue, children });
544
615
  const [checked, setChecked] = useControlled({
545
616
  controlled: checkedProp,
546
617
  default: defaultChecked ?? false,
@@ -562,7 +633,7 @@ function MenuCheckboxItem({
562
633
  if (disabled) return;
563
634
  setChecked(!checked);
564
635
  onCheckedChange?.(!checked);
565
- if (closeOnClick) setOpen(false);
636
+ if (closeOnClick) closeAll();
566
637
  }
567
638
  },
568
639
  consumerProps: disabled ? rest : { ...rest, onClick }
@@ -668,7 +739,7 @@ function MenuRadioItem({
668
739
  }) {
669
740
  const group = useMenuRadioGroupContext();
670
741
  const disabled = disabledProp || group.disabled;
671
- const { setOpen, itemProps } = useMenuItem("RadioItem", { disabled, textValue, children });
742
+ const { closeAll, itemProps } = useMenuItem("RadioItem", { disabled, textValue, children });
672
743
  const checked = group.value === value;
673
744
  const state = (0, import_react11.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
674
745
  const element = useRender({
@@ -684,7 +755,7 @@ function MenuRadioItem({
684
755
  onClick() {
685
756
  if (disabled) return;
686
757
  group.select(value);
687
- if (closeOnClick) setOpen(false);
758
+ if (closeOnClick) closeAll();
688
759
  }
689
760
  },
690
761
  consumerProps: disabled ? rest : { ...rest, onClick }
@@ -724,6 +795,119 @@ function MenuGroupLabel({ className, children, render, ...rest }) {
724
795
  consumerProps: rest
725
796
  });
726
797
  }
798
+
799
+ // src/menu/MenuSubmenuRoot.tsx
800
+ var import_react13 = require("react");
801
+ var import_jsx_runtime6 = require("react/jsx-runtime");
802
+ function MenuSubmenuRoot({
803
+ focusableWhenDisabled,
804
+ children,
805
+ ...props
806
+ }) {
807
+ const parent = useMenuRootContext("SubmenuRoot");
808
+ const context = useMenuRootValue(
809
+ "Menu.SubmenuRoot",
810
+ { ...props, focusableWhenDisabled: focusableWhenDisabled ?? parent.focusableWhenDisabled },
811
+ parent
812
+ );
813
+ const { open, setOpen, triggerRef } = context;
814
+ (0, import_react13.useEffect)(() => {
815
+ if (!open) return;
816
+ const entry = { close: () => setOpen(false), triggerRef };
817
+ parent.openSubmenuRef.current = entry;
818
+ return () => {
819
+ if (parent.openSubmenuRef.current === entry) parent.openSubmenuRef.current = null;
820
+ };
821
+ }, [open, setOpen, triggerRef, parent]);
822
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MenuRootContext.Provider, { value: context, children });
823
+ }
824
+
825
+ // src/menu/MenuSubmenuTrigger.tsx
826
+ var import_react14 = require("react");
827
+ var SUBMENU_OPEN_DELAY = 100;
828
+ var SUBMENU_GRACE_PERIOD = 300;
829
+ function MenuSubmenuTrigger({
830
+ disabled = false,
831
+ textValue,
832
+ onClick,
833
+ className,
834
+ children,
835
+ render,
836
+ ...rest
837
+ }) {
838
+ const submenu = useMenuRootContext("SubmenuTrigger");
839
+ const { parent } = submenu;
840
+ if (parent === null) {
841
+ throw new Error("<Menu.SubmenuTrigger> must be rendered inside <Menu.SubmenuRoot>.");
842
+ }
843
+ const { itemProps } = useMenuItem("SubmenuTrigger", { disabled, textValue, children }, parent);
844
+ const { open, setOpen, focusOnOpenRef, navigableItems, triggerRef, popupRef } = submenu;
845
+ const timerRef = (0, import_react14.useRef)(void 0);
846
+ (0, import_react14.useEffect)(() => () => clearTimeout(timerRef.current), []);
847
+ const openAt = (focus) => {
848
+ if (disabled) return;
849
+ focusOnOpenRef.current = focus;
850
+ if (!open) setOpen(true);
851
+ else if (focus === "first") navigableItems[0]?.ref.current?.focus();
852
+ };
853
+ const openAtRef = (0, import_react14.useRef)(openAt);
854
+ openAtRef.current = openAt;
855
+ const itemRef = itemProps.ref;
856
+ const ref = (0, import_react14.useCallback)(
857
+ (element) => {
858
+ itemRef.current = element;
859
+ triggerRef.current = element;
860
+ },
861
+ [itemRef, triggerRef]
862
+ );
863
+ return useRender({
864
+ render,
865
+ defaultTagName: "button",
866
+ props: {
867
+ ...itemProps,
868
+ id: submenu.triggerId,
869
+ role: "menuitem",
870
+ "aria-haspopup": "menu",
871
+ "aria-expanded": open,
872
+ "aria-controls": submenu.popupId,
873
+ ...menuDataAttributes({ open }),
874
+ style: { anchorName: submenu.anchorName },
875
+ className,
876
+ children,
877
+ ref,
878
+ onClick(event) {
879
+ openAt(event.detail === 0 ? "first" : "none");
880
+ },
881
+ onKeyDown(event) {
882
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
883
+ if (event.key !== (rtl ? "ArrowLeft" : "ArrowRight")) return;
884
+ event.preventDefault();
885
+ openAt("first");
886
+ },
887
+ onPointerMove(event) {
888
+ if (event.pointerType !== "mouse" || disabled) return;
889
+ const sibling = parent.openSubmenuRef.current;
890
+ if (sibling && sibling.triggerRef !== triggerRef) sibling.close();
891
+ if (open || timerRef.current !== void 0) return;
892
+ timerRef.current = setTimeout(() => {
893
+ timerRef.current = void 0;
894
+ openAtRef.current("none");
895
+ }, SUBMENU_OPEN_DELAY);
896
+ },
897
+ onPointerLeave(event) {
898
+ clearTimeout(timerRef.current);
899
+ timerRef.current = void 0;
900
+ const popup = popupRef.current;
901
+ if (!open || !popup) return;
902
+ parent.graceRef.current = {
903
+ area: graceArea({ x: event.clientX, y: event.clientY }, popup.getBoundingClientRect()),
904
+ until: performance.now() + SUBMENU_GRACE_PERIOD
905
+ };
906
+ }
907
+ },
908
+ consumerProps: disabled ? rest : { ...rest, onClick }
909
+ });
910
+ }
727
911
  // Annotate the CommonJS export names for ESM import in node:
728
912
  0 && (module.exports = {
729
913
  Menu
@@ -2,6 +2,11 @@ import * as react from 'react';
2
2
  import { ReactNode, ReactElement, Ref, ComponentPropsWithRef } from 'react';
3
3
  import { A as AnchorSide, a as AnchorAlign } from '../anchoring-Btxi0QwC.cjs';
4
4
 
5
+ /** The state Menu.Root shares with its parts, and projects as `data-*` attributes. */
6
+ type MenuState = {
7
+ open: boolean;
8
+ };
9
+
5
10
  type MenuRootProps = {
6
11
  /**
7
12
  * Controlled state. Provide `onOpenChange` alongside it.
@@ -34,7 +39,7 @@ type MenuRootProps = {
34
39
  * Each Item registers its own `disabled` from its props; the Root never reads it back from
35
40
  * the DOM (decision 10). Elements are kept only to move focus to.
36
41
  */
37
- declare function MenuRoot({ open: openProp, defaultOpen, onOpenChange, focusableWhenDisabled, children, }: MenuRootProps): react.JSX.Element;
42
+ declare function MenuRoot({ children, ...props }: MenuRootProps): react.JSX.Element;
38
43
 
39
44
  type MenuTriggerOwnProps = {
40
45
  /** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
@@ -53,7 +58,10 @@ declare function MenuTrigger({ className, children, render, ...rest }: MenuTrigg
53
58
  type MenuSide = AnchorSide;
54
59
  type MenuAlign = AnchorAlign;
55
60
  type MenuPopupOwnProps = {
56
- /** Which side of the trigger to open on. Flips to the opposite side when there is no room. */
61
+ /**
62
+ * Which side of the trigger to open on. Flips to the opposite side when there is no room.
63
+ * Defaults to `'bottom'`, and to `'right'` in a submenu.
64
+ */
57
65
  side?: MenuSide;
58
66
  /** Where along that side: flush with the trigger's start or end edge, or centred on it. */
59
67
  align?: MenuAlign;
@@ -72,11 +80,15 @@ type MenuPopupProps = MenuPopupOwnProps & Omit<ComponentPropsWithRef<'div'>, key
72
80
  * whose text starts with what was typed. Disabled items are skipped by both, unless the Root
73
81
  * sets `focusableWhenDisabled`. Tab closes the menu and lets focus move on.
74
82
  *
83
+ * Inside a SubmenuRoot it is that submenu's popup: it opens beside its SubmenuTrigger, ← (→ in
84
+ * a right-to-left menu) closes it, and Tab closes every level. Closing with focus inside returns
85
+ * it to the SubmenuTrigger.
86
+ *
75
87
  * Light dismiss and Esc close the element before anyone is asked; the `toggle` event
76
88
  * reports it through the Root, and the element reopens if the state stays open. Focus
77
89
  * returns to the Trigger, the invoker, whenever the menu hides with focus inside it.
78
90
  */
79
- declare function MenuPopup({ side, align, className, children, render, ...rest }: MenuPopupProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
91
+ declare function MenuPopup({ side: sideProp, align, className, children, render, ...rest }: MenuPopupProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
80
92
 
81
93
  type MenuItemOwnProps = {
82
94
  /**
@@ -97,8 +109,8 @@ type MenuItemOwnProps = {
97
109
  type MenuItemProps = MenuItemOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof MenuItemOwnProps>;
98
110
  /**
99
111
  * One action. A native `<button>`, so Enter, Space and a click activate it with no key
100
- * handling here: put the action in `onClick`. Activating it closes the menu — call
101
- * `event.preventComponentHandler()` in your `onClick` to keep it open.
112
+ * handling here: put the action in `onClick`. Activating it closes the menu, and every menu
113
+ * it is nested in — call `event.preventComponentHandler()` in your `onClick` to keep it open.
102
114
  *
103
115
  * Disabled, your `onClick` is not attached, so an `aria-disabled` item under the Root's
104
116
  * `focusableWhenDisabled` cannot run it either.
@@ -237,13 +249,63 @@ type MenuGroupLabelProps = MenuGroupLabelOwnProps & Omit<ComponentPropsWithRef<'
237
249
  */
238
250
  declare function MenuGroupLabel({ className, children, render, ...rest }: MenuGroupLabelProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
239
251
 
240
- declare namespace index_parts {
241
- export { MenuCheckboxItem as CheckboxItem, MenuGroup as Group, MenuGroupLabel as GroupLabel, MenuItem as Item, MenuItemIndicator as ItemIndicator, MenuPopup as Popup, MenuRadioGroup as RadioGroup, MenuRadioItem as RadioItem, MenuRoot as Root, MenuTrigger as Trigger };
242
- }
252
+ type MenuSubmenuRootProps = {
253
+ /**
254
+ * Controlled state. Provide `onOpenChange` alongside it.
255
+ *
256
+ * Never `undefined` once mounted: the mode is latched at mount, as for Menu.Root.
257
+ */
258
+ open?: boolean;
259
+ /** Initial state when uncontrolled. Read once, at mount. */
260
+ defaultOpen?: boolean;
261
+ /**
262
+ * Called on every request to open or close — its SubmenuTrigger, the arrow keys, an Item,
263
+ * Esc, a click elsewhere, or the pointer moving to another item of the parent menu.
264
+ */
265
+ onOpenChange?: (open: boolean) => void;
266
+ /** As on Menu.Root. Defaults to the parent menu's setting. */
267
+ focusableWhenDisabled?: boolean;
268
+ children?: ReactNode;
269
+ };
270
+ /**
271
+ * A menu inside a menu: holds the submenu's state, as Root does, and renders no element. Put a
272
+ * SubmenuTrigger and a Popup inside it; the Popup opens beside the trigger, on the right by
273
+ * default.
274
+ *
275
+ * The popup is a native `popover="auto"` shown with the trigger as its `source`, which nests it
276
+ * in the parent's popover: the parent stays open while it is, Esc closes one level at a time,
277
+ * a click in the parent closes only the submenu, and focus returns to the trigger — all the
278
+ * platform's. Activating an item closes every level.
279
+ */
280
+ declare function MenuSubmenuRoot({ focusableWhenDisabled, children, ...props }: MenuSubmenuRootProps): react.JSX.Element;
243
281
 
244
- /** The state Menu.Root shares with its parts, and projects as `data-*` attributes. */
245
- type MenuState = {
246
- open: boolean;
282
+ type MenuSubmenuTriggerOwnProps = {
283
+ /**
284
+ * Cannot open its submenu. Skipped by the arrow keys, unless the Root sets
285
+ * `focusableWhenDisabled`.
286
+ */
287
+ disabled?: boolean;
288
+ /** The text typeahead matches in the parent menu. Defaults to the text in `children`. */
289
+ textValue?: string;
290
+ /** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
291
+ render?: ReactElement;
292
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
293
+ ref?: Ref<HTMLElement>;
247
294
  };
295
+ type MenuSubmenuTriggerProps = MenuSubmenuTriggerOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof MenuSubmenuTriggerOwnProps>;
296
+ /**
297
+ * The item that opens a submenu: a `menuitem` of the parent menu with `aria-haspopup="menu"`,
298
+ * which the submenu is anchored to and named by. It registers with the parent, so the parent's
299
+ * arrow keys and typeahead reach it.
300
+ *
301
+ * Enter, Space and → (← in a right-to-left menu) open the submenu at its first item. Resting the
302
+ * pointer on it opens it without moving focus; moving to another item of the parent closes it,
303
+ * unless the pointer is crossing toward the submenu.
304
+ */
305
+ declare function MenuSubmenuTrigger({ disabled, textValue, onClick, className, children, render, ...rest }: MenuSubmenuTriggerProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
306
+
307
+ declare namespace index_parts {
308
+ export { MenuCheckboxItem as CheckboxItem, MenuGroup as Group, MenuGroupLabel as GroupLabel, MenuItem as Item, MenuItemIndicator as ItemIndicator, MenuPopup as Popup, MenuRadioGroup as RadioGroup, MenuRadioItem as RadioItem, MenuRoot as Root, MenuSubmenuRoot as SubmenuRoot, MenuSubmenuTrigger as SubmenuTrigger, MenuTrigger as Trigger };
309
+ }
248
310
 
249
- export { index_parts as Menu, type MenuAlign, type MenuCheckboxItemProps, type MenuGroupLabelProps, type MenuGroupProps, type MenuItemIndicatorProps, type MenuItemProps, type MenuPopupProps, type MenuRadioGroupProps, type MenuRadioItemProps, type MenuRootProps, type MenuSide, type MenuState, type MenuTriggerProps };
311
+ export { index_parts as Menu, type MenuAlign, type MenuCheckboxItemProps, type MenuGroupLabelProps, type MenuGroupProps, type MenuItemIndicatorProps, type MenuItemProps, type MenuPopupProps, type MenuRadioGroupProps, type MenuRadioItemProps, type MenuRootProps, type MenuSide, type MenuState, type MenuSubmenuRootProps, type MenuSubmenuTriggerProps, type MenuTriggerProps };
@@ -2,6 +2,11 @@ import * as react from 'react';
2
2
  import { ReactNode, ReactElement, Ref, ComponentPropsWithRef } from 'react';
3
3
  import { A as AnchorSide, a as AnchorAlign } from '../anchoring-Btxi0QwC.js';
4
4
 
5
+ /** The state Menu.Root shares with its parts, and projects as `data-*` attributes. */
6
+ type MenuState = {
7
+ open: boolean;
8
+ };
9
+
5
10
  type MenuRootProps = {
6
11
  /**
7
12
  * Controlled state. Provide `onOpenChange` alongside it.
@@ -34,7 +39,7 @@ type MenuRootProps = {
34
39
  * Each Item registers its own `disabled` from its props; the Root never reads it back from
35
40
  * the DOM (decision 10). Elements are kept only to move focus to.
36
41
  */
37
- declare function MenuRoot({ open: openProp, defaultOpen, onOpenChange, focusableWhenDisabled, children, }: MenuRootProps): react.JSX.Element;
42
+ declare function MenuRoot({ children, ...props }: MenuRootProps): react.JSX.Element;
38
43
 
39
44
  type MenuTriggerOwnProps = {
40
45
  /** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
@@ -53,7 +58,10 @@ declare function MenuTrigger({ className, children, render, ...rest }: MenuTrigg
53
58
  type MenuSide = AnchorSide;
54
59
  type MenuAlign = AnchorAlign;
55
60
  type MenuPopupOwnProps = {
56
- /** Which side of the trigger to open on. Flips to the opposite side when there is no room. */
61
+ /**
62
+ * Which side of the trigger to open on. Flips to the opposite side when there is no room.
63
+ * Defaults to `'bottom'`, and to `'right'` in a submenu.
64
+ */
57
65
  side?: MenuSide;
58
66
  /** Where along that side: flush with the trigger's start or end edge, or centred on it. */
59
67
  align?: MenuAlign;
@@ -72,11 +80,15 @@ type MenuPopupProps = MenuPopupOwnProps & Omit<ComponentPropsWithRef<'div'>, key
72
80
  * whose text starts with what was typed. Disabled items are skipped by both, unless the Root
73
81
  * sets `focusableWhenDisabled`. Tab closes the menu and lets focus move on.
74
82
  *
83
+ * Inside a SubmenuRoot it is that submenu's popup: it opens beside its SubmenuTrigger, ← (→ in
84
+ * a right-to-left menu) closes it, and Tab closes every level. Closing with focus inside returns
85
+ * it to the SubmenuTrigger.
86
+ *
75
87
  * Light dismiss and Esc close the element before anyone is asked; the `toggle` event
76
88
  * reports it through the Root, and the element reopens if the state stays open. Focus
77
89
  * returns to the Trigger, the invoker, whenever the menu hides with focus inside it.
78
90
  */
79
- declare function MenuPopup({ side, align, className, children, render, ...rest }: MenuPopupProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
91
+ declare function MenuPopup({ side: sideProp, align, className, children, render, ...rest }: MenuPopupProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
80
92
 
81
93
  type MenuItemOwnProps = {
82
94
  /**
@@ -97,8 +109,8 @@ type MenuItemOwnProps = {
97
109
  type MenuItemProps = MenuItemOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof MenuItemOwnProps>;
98
110
  /**
99
111
  * One action. A native `<button>`, so Enter, Space and a click activate it with no key
100
- * handling here: put the action in `onClick`. Activating it closes the menu — call
101
- * `event.preventComponentHandler()` in your `onClick` to keep it open.
112
+ * handling here: put the action in `onClick`. Activating it closes the menu, and every menu
113
+ * it is nested in — call `event.preventComponentHandler()` in your `onClick` to keep it open.
102
114
  *
103
115
  * Disabled, your `onClick` is not attached, so an `aria-disabled` item under the Root's
104
116
  * `focusableWhenDisabled` cannot run it either.
@@ -237,13 +249,63 @@ type MenuGroupLabelProps = MenuGroupLabelOwnProps & Omit<ComponentPropsWithRef<'
237
249
  */
238
250
  declare function MenuGroupLabel({ className, children, render, ...rest }: MenuGroupLabelProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
239
251
 
240
- declare namespace index_parts {
241
- export { MenuCheckboxItem as CheckboxItem, MenuGroup as Group, MenuGroupLabel as GroupLabel, MenuItem as Item, MenuItemIndicator as ItemIndicator, MenuPopup as Popup, MenuRadioGroup as RadioGroup, MenuRadioItem as RadioItem, MenuRoot as Root, MenuTrigger as Trigger };
242
- }
252
+ type MenuSubmenuRootProps = {
253
+ /**
254
+ * Controlled state. Provide `onOpenChange` alongside it.
255
+ *
256
+ * Never `undefined` once mounted: the mode is latched at mount, as for Menu.Root.
257
+ */
258
+ open?: boolean;
259
+ /** Initial state when uncontrolled. Read once, at mount. */
260
+ defaultOpen?: boolean;
261
+ /**
262
+ * Called on every request to open or close — its SubmenuTrigger, the arrow keys, an Item,
263
+ * Esc, a click elsewhere, or the pointer moving to another item of the parent menu.
264
+ */
265
+ onOpenChange?: (open: boolean) => void;
266
+ /** As on Menu.Root. Defaults to the parent menu's setting. */
267
+ focusableWhenDisabled?: boolean;
268
+ children?: ReactNode;
269
+ };
270
+ /**
271
+ * A menu inside a menu: holds the submenu's state, as Root does, and renders no element. Put a
272
+ * SubmenuTrigger and a Popup inside it; the Popup opens beside the trigger, on the right by
273
+ * default.
274
+ *
275
+ * The popup is a native `popover="auto"` shown with the trigger as its `source`, which nests it
276
+ * in the parent's popover: the parent stays open while it is, Esc closes one level at a time,
277
+ * a click in the parent closes only the submenu, and focus returns to the trigger — all the
278
+ * platform's. Activating an item closes every level.
279
+ */
280
+ declare function MenuSubmenuRoot({ focusableWhenDisabled, children, ...props }: MenuSubmenuRootProps): react.JSX.Element;
243
281
 
244
- /** The state Menu.Root shares with its parts, and projects as `data-*` attributes. */
245
- type MenuState = {
246
- open: boolean;
282
+ type MenuSubmenuTriggerOwnProps = {
283
+ /**
284
+ * Cannot open its submenu. Skipped by the arrow keys, unless the Root sets
285
+ * `focusableWhenDisabled`.
286
+ */
287
+ disabled?: boolean;
288
+ /** The text typeahead matches in the parent menu. Defaults to the text in `children`. */
289
+ textValue?: string;
290
+ /** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
291
+ render?: ReactElement;
292
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
293
+ ref?: Ref<HTMLElement>;
247
294
  };
295
+ type MenuSubmenuTriggerProps = MenuSubmenuTriggerOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof MenuSubmenuTriggerOwnProps>;
296
+ /**
297
+ * The item that opens a submenu: a `menuitem` of the parent menu with `aria-haspopup="menu"`,
298
+ * which the submenu is anchored to and named by. It registers with the parent, so the parent's
299
+ * arrow keys and typeahead reach it.
300
+ *
301
+ * Enter, Space and → (← in a right-to-left menu) open the submenu at its first item. Resting the
302
+ * pointer on it opens it without moving focus; moving to another item of the parent closes it,
303
+ * unless the pointer is crossing toward the submenu.
304
+ */
305
+ declare function MenuSubmenuTrigger({ disabled, textValue, onClick, className, children, render, ...rest }: MenuSubmenuTriggerProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
306
+
307
+ declare namespace index_parts {
308
+ export { MenuCheckboxItem as CheckboxItem, MenuGroup as Group, MenuGroupLabel as GroupLabel, MenuItem as Item, MenuItemIndicator as ItemIndicator, MenuPopup as Popup, MenuRadioGroup as RadioGroup, MenuRadioItem as RadioItem, MenuRoot as Root, MenuSubmenuRoot as SubmenuRoot, MenuSubmenuTrigger as SubmenuTrigger, MenuTrigger as Trigger };
309
+ }
248
310
 
249
- export { index_parts as Menu, type MenuAlign, type MenuCheckboxItemProps, type MenuGroupLabelProps, type MenuGroupProps, type MenuItemIndicatorProps, type MenuItemProps, type MenuPopupProps, type MenuRadioGroupProps, type MenuRadioItemProps, type MenuRootProps, type MenuSide, type MenuState, type MenuTriggerProps };
311
+ export { index_parts as Menu, type MenuAlign, type MenuCheckboxItemProps, type MenuGroupLabelProps, type MenuGroupProps, type MenuItemIndicatorProps, type MenuItemProps, type MenuPopupProps, type MenuRadioGroupProps, type MenuRadioItemProps, type MenuRootProps, type MenuSide, type MenuState, type MenuSubmenuRootProps, type MenuSubmenuTriggerProps, type MenuTriggerProps };
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  fold
3
3
  } from "../chunk-ETNICT5M.js";
4
+ import {
5
+ byDocumentOrder,
6
+ rovingIndex
7
+ } from "../chunk-2S4XG4VD.js";
4
8
  import {
5
9
  anchorNameFor,
6
10
  anchoredDataAttributes,
7
11
  anchoredPopupStyle
8
12
  } from "../chunk-HCOCPQXS.js";
9
- import {
10
- byDocumentOrder,
11
- rovingIndex
12
- } from "../chunk-2S4XG4VD.js";
13
13
  import {
14
14
  useControlled
15
15
  } from "../chunk-UEIB3I3T.js";
@@ -30,6 +30,8 @@ __export(index_parts_exports, {
30
30
  RadioGroup: () => MenuRadioGroup,
31
31
  RadioItem: () => MenuRadioItem,
32
32
  Root: () => MenuRoot,
33
+ SubmenuRoot: () => MenuSubmenuRoot,
34
+ SubmenuTrigger: () => MenuSubmenuTrigger,
33
35
  Trigger: () => MenuTrigger
34
36
  });
35
37
 
@@ -49,17 +51,20 @@ function useMenuRootContext(part) {
49
51
 
50
52
  // src/menu/MenuRoot.tsx
51
53
  import { jsx } from "react/jsx-runtime";
52
- function MenuRoot({
54
+ function MenuRoot({ children, ...props }) {
55
+ const context = useMenuRootValue("Menu.Root", props, null);
56
+ return /* @__PURE__ */ jsx(MenuRootContext.Provider, { value: context, children });
57
+ }
58
+ function useMenuRootValue(name, {
53
59
  open: openProp,
54
60
  defaultOpen,
55
61
  onOpenChange,
56
- focusableWhenDisabled = false,
57
- children
58
- }) {
62
+ focusableWhenDisabled = false
63
+ }, parent) {
59
64
  const [open, setOpenState] = useControlled({
60
65
  controlled: openProp,
61
66
  default: defaultOpen ?? false,
62
- name: "Menu.Root",
67
+ name,
63
68
  state: "open"
64
69
  });
65
70
  const setOpen = useCallback(
@@ -70,6 +75,10 @@ function MenuRoot({
70
75
  },
71
76
  [open, setOpenState, onOpenChange]
72
77
  );
78
+ const closeAll = useCallback(() => {
79
+ setOpen(false);
80
+ parent?.closeAll();
81
+ }, [setOpen, parent]);
73
82
  const [items, setItems] = useState([]);
74
83
  const register = useCallback((entry) => {
75
84
  setItems((current) => [...current, entry].sort(byDocumentOrder));
@@ -82,10 +91,14 @@ function MenuRoot({
82
91
  const id = useId();
83
92
  const triggerRef = useRef(null);
84
93
  const focusOnOpenRef = useRef("first");
85
- const context = useMemo(
94
+ const popupRef = useRef(null);
95
+ const openSubmenuRef = useRef(null);
96
+ const graceRef = useRef(null);
97
+ return useMemo(
86
98
  () => ({
87
99
  open,
88
100
  setOpen,
101
+ closeAll,
89
102
  navigableItems,
90
103
  focusableWhenDisabled,
91
104
  register,
@@ -93,11 +106,14 @@ function MenuRoot({
93
106
  triggerId: `${id}-trigger`,
94
107
  popupId: `${id}-popup`,
95
108
  anchorName: anchorNameFor(id),
96
- triggerRef
109
+ triggerRef,
110
+ parent,
111
+ popupRef,
112
+ openSubmenuRef,
113
+ graceRef
97
114
  }),
98
- [open, setOpen, navigableItems, focusableWhenDisabled, register, id]
115
+ [open, setOpen, closeAll, navigableItems, focusableWhenDisabled, register, id, parent]
99
116
  );
100
- return /* @__PURE__ */ jsx(MenuRootContext.Provider, { value: context, children });
101
117
  }
102
118
 
103
119
  // src/menu/menuDataAttributes.ts
@@ -187,7 +203,7 @@ function useTypeahead() {
187
203
 
188
204
  // src/menu/MenuPopup.tsx
189
205
  function MenuPopup({
190
- side = "bottom",
206
+ side: sideProp,
191
207
  align = "start",
192
208
  className,
193
209
  children,
@@ -202,9 +218,12 @@ function MenuPopup({
202
218
  triggerId,
203
219
  popupId,
204
220
  anchorName,
205
- triggerRef
221
+ triggerRef,
222
+ parent,
223
+ popupRef: elementRef,
224
+ openSubmenuRef
206
225
  } = useMenuRootContext("Popup");
207
- const elementRef = useRef3(null);
226
+ const side = sideProp ?? (parent ? "right" : "bottom");
208
227
  const focusPendingRef = useRef3(false);
209
228
  const [, resync] = useReducer((n) => n + 1, 0);
210
229
  const typeahead = useTypeahead();
@@ -222,7 +241,8 @@ function MenuPopup({
222
241
  }
223
242
  if (focusPendingRef.current && navigableItems.length > 0) {
224
243
  focusPendingRef.current = false;
225
- const target = focusOnOpenRef.current === "last" ? navigableItems.at(-1) : navigableItems[0];
244
+ const focus = focusOnOpenRef.current;
245
+ const target = focus === "last" ? navigableItems.at(-1) : focus === "first" ? navigableItems[0] : null;
226
246
  target?.ref.current?.focus();
227
247
  }
228
248
  });
@@ -247,6 +267,12 @@ function MenuPopup({
247
267
  }
248
268
  const current = navigableItems.findIndex((i) => i.ref.current === event.target);
249
269
  if (current < 0 && event.target !== event.currentTarget) return;
270
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
271
+ if (parent && event.key === (rtl ? "ArrowRight" : "ArrowLeft")) {
272
+ event.preventDefault();
273
+ setOpen(false);
274
+ return;
275
+ }
250
276
  const search = typeahead(event);
251
277
  const next = search === null ? rovingIndex(event.key, {
252
278
  count: navigableItems.length,
@@ -261,7 +287,15 @@ function MenuPopup({
261
287
  if (search !== null) event.preventDefault();
262
288
  if (next === null) return;
263
289
  event.preventDefault();
264
- navigableItems[next]?.ref.current?.focus();
290
+ const target = navigableItems[next]?.ref.current;
291
+ const submenu = openSubmenuRef.current;
292
+ if (submenu && submenu.triggerRef.current !== target) submenu.close();
293
+ target?.focus();
294
+ },
295
+ onBeforeToggle(event) {
296
+ if (event.target !== event.currentTarget || !parent) return;
297
+ if (event.nativeEvent.newState !== "closed") return;
298
+ if (event.currentTarget.contains(document.activeElement)) triggerRef.current?.focus();
265
299
  },
266
300
  onToggle(event) {
267
301
  if (event.target !== event.currentTarget) return;
@@ -276,23 +310,60 @@ function MenuPopup({
276
310
 
277
311
  // src/menu/useMenuItem.ts
278
312
  import { createContext as createContext2, useContext as useContext2, useLayoutEffect, useRef as useRef4 } from "react";
279
- function useMenuItem(part, { disabled, textValue, children }) {
280
- const { setOpen, focusableWhenDisabled, register } = useMenuRootContext(part);
313
+
314
+ // src/core/pointerIntent.ts
315
+ function graceArea(exit, popup) {
316
+ const toRight = popup.left >= exit.x;
317
+ const edge = toRight ? popup.left : popup.right;
318
+ const back = toRight ? -5 : 5;
319
+ return [
320
+ { x: exit.x + back, y: exit.y - 5 },
321
+ { x: edge, y: popup.top },
322
+ { x: edge, y: popup.bottom },
323
+ { x: exit.x + back, y: exit.y + 5 }
324
+ ];
325
+ }
326
+ function isPointInPolygon({ x, y }, polygon) {
327
+ let inside = false;
328
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i, i += 1) {
329
+ const a = polygon[i];
330
+ const b = polygon[j];
331
+ if (!a || !b) continue;
332
+ if (a.y > y !== b.y > y && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x) {
333
+ inside = !inside;
334
+ }
335
+ }
336
+ return inside;
337
+ }
338
+
339
+ // src/menu/useMenuItem.ts
340
+ function useMenuItem(part, { disabled, textValue, children }, menu) {
341
+ const own = useMenuRootContext(part);
342
+ const { closeAll, focusableWhenDisabled, register, openSubmenuRef, graceRef } = menu ?? own;
281
343
  const ref = useRef4(null);
282
344
  const text = textValue ?? textOf(children);
283
345
  useLayoutEffect(() => register({ disabled, textValue: text, ref }), [register, disabled, text]);
284
346
  return {
285
- setOpen,
347
+ closeAll,
286
348
  itemProps: {
287
349
  type: "button",
288
350
  tabIndex: -1,
289
351
  disabled: disabled && !focusableWhenDisabled || void 0,
290
352
  "aria-disabled": disabled && focusableWhenDisabled || void 0,
291
353
  "data-disabled": disabled ? "" : void 0,
292
- ref
354
+ ref,
355
+ onPointerMove(event) {
356
+ if (event.pointerType !== "mouse" || !openSubmenuRef.current) return;
357
+ if (isInGrace(graceRef.current, event)) return;
358
+ openSubmenuRef.current.close();
359
+ }
293
360
  }
294
361
  };
295
362
  }
363
+ function isInGrace(grace, event) {
364
+ if (!grace || performance.now() > grace.until) return false;
365
+ return isPointInPolygon({ x: event.clientX, y: event.clientY }, grace.area);
366
+ }
296
367
  var MenuItemIndicatorContext = createContext2(null);
297
368
  function useMenuItemIndicatorContext() {
298
369
  const context = useContext2(MenuItemIndicatorContext);
@@ -321,7 +392,7 @@ function MenuItem({
321
392
  render,
322
393
  ...rest
323
394
  }) {
324
- const { setOpen, itemProps } = useMenuItem("Item", { disabled, textValue, children });
395
+ const { closeAll, itemProps } = useMenuItem("Item", { disabled, textValue, children });
325
396
  return useRender({
326
397
  render,
327
398
  defaultTagName: "button",
@@ -331,7 +402,7 @@ function MenuItem({
331
402
  className,
332
403
  children,
333
404
  onClick() {
334
- if (!disabled) setOpen(false);
405
+ if (!disabled) closeAll();
335
406
  }
336
407
  },
337
408
  consumerProps: disabled ? rest : { ...rest, onClick }
@@ -354,7 +425,7 @@ function MenuCheckboxItem({
354
425
  render,
355
426
  ...rest
356
427
  }) {
357
- const { setOpen, itemProps } = useMenuItem("CheckboxItem", { disabled, textValue, children });
428
+ const { closeAll, itemProps } = useMenuItem("CheckboxItem", { disabled, textValue, children });
358
429
  const [checked, setChecked] = useControlled({
359
430
  controlled: checkedProp,
360
431
  default: defaultChecked ?? false,
@@ -376,7 +447,7 @@ function MenuCheckboxItem({
376
447
  if (disabled) return;
377
448
  setChecked(!checked);
378
449
  onCheckedChange?.(!checked);
379
- if (closeOnClick) setOpen(false);
450
+ if (closeOnClick) closeAll();
380
451
  }
381
452
  },
382
453
  consumerProps: disabled ? rest : { ...rest, onClick }
@@ -482,7 +553,7 @@ function MenuRadioItem({
482
553
  }) {
483
554
  const group = useMenuRadioGroupContext();
484
555
  const disabled = disabledProp || group.disabled;
485
- const { setOpen, itemProps } = useMenuItem("RadioItem", { disabled, textValue, children });
556
+ const { closeAll, itemProps } = useMenuItem("RadioItem", { disabled, textValue, children });
486
557
  const checked = group.value === value;
487
558
  const state = useMemo5(() => ({ checked, disabled }), [checked, disabled]);
488
559
  const element = useRender({
@@ -498,7 +569,7 @@ function MenuRadioItem({
498
569
  onClick() {
499
570
  if (disabled) return;
500
571
  group.select(value);
501
- if (closeOnClick) setOpen(false);
572
+ if (closeOnClick) closeAll();
502
573
  }
503
574
  },
504
575
  consumerProps: disabled ? rest : { ...rest, onClick }
@@ -538,6 +609,119 @@ function MenuGroupLabel({ className, children, render, ...rest }) {
538
609
  consumerProps: rest
539
610
  });
540
611
  }
612
+
613
+ // src/menu/MenuSubmenuRoot.tsx
614
+ import { useEffect as useEffect3 } from "react";
615
+ import { jsx as jsx6 } from "react/jsx-runtime";
616
+ function MenuSubmenuRoot({
617
+ focusableWhenDisabled,
618
+ children,
619
+ ...props
620
+ }) {
621
+ const parent = useMenuRootContext("SubmenuRoot");
622
+ const context = useMenuRootValue(
623
+ "Menu.SubmenuRoot",
624
+ { ...props, focusableWhenDisabled: focusableWhenDisabled ?? parent.focusableWhenDisabled },
625
+ parent
626
+ );
627
+ const { open, setOpen, triggerRef } = context;
628
+ useEffect3(() => {
629
+ if (!open) return;
630
+ const entry = { close: () => setOpen(false), triggerRef };
631
+ parent.openSubmenuRef.current = entry;
632
+ return () => {
633
+ if (parent.openSubmenuRef.current === entry) parent.openSubmenuRef.current = null;
634
+ };
635
+ }, [open, setOpen, triggerRef, parent]);
636
+ return /* @__PURE__ */ jsx6(MenuRootContext.Provider, { value: context, children });
637
+ }
638
+
639
+ // src/menu/MenuSubmenuTrigger.tsx
640
+ import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef5 } from "react";
641
+ var SUBMENU_OPEN_DELAY = 100;
642
+ var SUBMENU_GRACE_PERIOD = 300;
643
+ function MenuSubmenuTrigger({
644
+ disabled = false,
645
+ textValue,
646
+ onClick,
647
+ className,
648
+ children,
649
+ render,
650
+ ...rest
651
+ }) {
652
+ const submenu = useMenuRootContext("SubmenuTrigger");
653
+ const { parent } = submenu;
654
+ if (parent === null) {
655
+ throw new Error("<Menu.SubmenuTrigger> must be rendered inside <Menu.SubmenuRoot>.");
656
+ }
657
+ const { itemProps } = useMenuItem("SubmenuTrigger", { disabled, textValue, children }, parent);
658
+ const { open, setOpen, focusOnOpenRef, navigableItems, triggerRef, popupRef } = submenu;
659
+ const timerRef = useRef5(void 0);
660
+ useEffect4(() => () => clearTimeout(timerRef.current), []);
661
+ const openAt = (focus) => {
662
+ if (disabled) return;
663
+ focusOnOpenRef.current = focus;
664
+ if (!open) setOpen(true);
665
+ else if (focus === "first") navigableItems[0]?.ref.current?.focus();
666
+ };
667
+ const openAtRef = useRef5(openAt);
668
+ openAtRef.current = openAt;
669
+ const itemRef = itemProps.ref;
670
+ const ref = useCallback5(
671
+ (element) => {
672
+ itemRef.current = element;
673
+ triggerRef.current = element;
674
+ },
675
+ [itemRef, triggerRef]
676
+ );
677
+ return useRender({
678
+ render,
679
+ defaultTagName: "button",
680
+ props: {
681
+ ...itemProps,
682
+ id: submenu.triggerId,
683
+ role: "menuitem",
684
+ "aria-haspopup": "menu",
685
+ "aria-expanded": open,
686
+ "aria-controls": submenu.popupId,
687
+ ...menuDataAttributes({ open }),
688
+ style: { anchorName: submenu.anchorName },
689
+ className,
690
+ children,
691
+ ref,
692
+ onClick(event) {
693
+ openAt(event.detail === 0 ? "first" : "none");
694
+ },
695
+ onKeyDown(event) {
696
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
697
+ if (event.key !== (rtl ? "ArrowLeft" : "ArrowRight")) return;
698
+ event.preventDefault();
699
+ openAt("first");
700
+ },
701
+ onPointerMove(event) {
702
+ if (event.pointerType !== "mouse" || disabled) return;
703
+ const sibling = parent.openSubmenuRef.current;
704
+ if (sibling && sibling.triggerRef !== triggerRef) sibling.close();
705
+ if (open || timerRef.current !== void 0) return;
706
+ timerRef.current = setTimeout(() => {
707
+ timerRef.current = void 0;
708
+ openAtRef.current("none");
709
+ }, SUBMENU_OPEN_DELAY);
710
+ },
711
+ onPointerLeave(event) {
712
+ clearTimeout(timerRef.current);
713
+ timerRef.current = void 0;
714
+ const popup = popupRef.current;
715
+ if (!open || !popup) return;
716
+ parent.graceRef.current = {
717
+ area: graceArea({ x: event.clientX, y: event.clientY }, popup.getBoundingClientRect()),
718
+ until: performance.now() + SUBMENU_GRACE_PERIOD
719
+ };
720
+ }
721
+ },
722
+ consumerProps: disabled ? rest : { ...rest, onClick }
723
+ });
724
+ }
541
725
  export {
542
726
  index_parts_exports as Menu
543
727
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "4.15.0",
3
+ "version": "4.16.0",
4
4
  "description": "Unstyled React behaviour primitives — render engine, controlled/uncontrolled state, and data-* state projection. Ships no CSS and no class names.",
5
5
  "license": "MIT",
6
6
  "type": "module",