@arun-dev/headless 4.14.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.
@@ -27,9 +27,17 @@ module.exports = __toCommonJS(menu_exports);
27
27
  // src/menu/index.parts.ts
28
28
  var index_parts_exports = {};
29
29
  __export(index_parts_exports, {
30
+ CheckboxItem: () => MenuCheckboxItem,
31
+ Group: () => MenuGroup,
32
+ GroupLabel: () => MenuGroupLabel,
30
33
  Item: () => MenuItem,
34
+ ItemIndicator: () => MenuItemIndicator,
31
35
  Popup: () => MenuPopup,
36
+ RadioGroup: () => MenuRadioGroup,
37
+ RadioItem: () => MenuRadioItem,
32
38
  Root: () => MenuRoot,
39
+ SubmenuRoot: () => MenuSubmenuRoot,
40
+ SubmenuTrigger: () => MenuSubmenuTrigger,
33
41
  Trigger: () => MenuTrigger
34
42
  });
35
43
 
@@ -133,17 +141,20 @@ function useMenuRootContext(part) {
133
141
 
134
142
  // src/menu/MenuRoot.tsx
135
143
  var import_jsx_runtime = require("react/jsx-runtime");
136
- 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, {
137
149
  open: openProp,
138
150
  defaultOpen,
139
151
  onOpenChange,
140
- focusableWhenDisabled = false,
141
- children
142
- }) {
152
+ focusableWhenDisabled = false
153
+ }, parent) {
143
154
  const [open, setOpenState] = useControlled({
144
155
  controlled: openProp,
145
156
  default: defaultOpen ?? false,
146
- name: "Menu.Root",
157
+ name,
147
158
  state: "open"
148
159
  });
149
160
  const setOpen = (0, import_react3.useCallback)(
@@ -154,6 +165,10 @@ function MenuRoot({
154
165
  },
155
166
  [open, setOpenState, onOpenChange]
156
167
  );
168
+ const closeAll = (0, import_react3.useCallback)(() => {
169
+ setOpen(false);
170
+ parent?.closeAll();
171
+ }, [setOpen, parent]);
157
172
  const [items, setItems] = (0, import_react3.useState)([]);
158
173
  const register = (0, import_react3.useCallback)((entry) => {
159
174
  setItems((current) => [...current, entry].sort(byDocumentOrder));
@@ -166,10 +181,14 @@ function MenuRoot({
166
181
  const id = (0, import_react3.useId)();
167
182
  const triggerRef = (0, import_react3.useRef)(null);
168
183
  const focusOnOpenRef = (0, import_react3.useRef)("first");
169
- 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)(
170
188
  () => ({
171
189
  open,
172
190
  setOpen,
191
+ closeAll,
173
192
  navigableItems,
174
193
  focusableWhenDisabled,
175
194
  register,
@@ -177,11 +196,14 @@ function MenuRoot({
177
196
  triggerId: `${id}-trigger`,
178
197
  popupId: `${id}-popup`,
179
198
  anchorName: anchorNameFor(id),
180
- triggerRef
199
+ triggerRef,
200
+ parent,
201
+ popupRef,
202
+ openSubmenuRef,
203
+ graceRef
181
204
  }),
182
- [open, setOpen, navigableItems, focusableWhenDisabled, register, id]
205
+ [open, setOpen, closeAll, navigableItems, focusableWhenDisabled, register, id, parent]
183
206
  );
184
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MenuRootContext.Provider, { value: context, children });
185
207
  }
186
208
 
187
209
  // src/core/useRender.ts
@@ -280,9 +302,6 @@ function menuDataAttributes({ open }) {
280
302
  "data-closed": open ? void 0 : ""
281
303
  };
282
304
  }
283
- function menuItemDataAttributes(disabled) {
284
- return { "data-disabled": disabled ? "" : void 0 };
285
- }
286
305
 
287
306
  // src/menu/MenuTrigger.tsx
288
307
  function MenuTrigger({ className, children, render, ...rest }) {
@@ -317,9 +336,60 @@ function MenuTrigger({ className, children, render, ...rest }) {
317
336
  }
318
337
 
319
338
  // src/menu/MenuPopup.tsx
339
+ var import_react6 = require("react");
340
+
341
+ // src/core/typeahead.ts
320
342
  var import_react5 = require("react");
343
+
344
+ // src/core/fold.ts
345
+ function fold(text) {
346
+ return text.normalize("NFD").replace(/\p{M}/gu, "").toLocaleLowerCase();
347
+ }
348
+
349
+ // src/core/typeahead.ts
350
+ var TYPEAHEAD_TIMEOUT = 500;
351
+ function textOf(node) {
352
+ let text = "";
353
+ import_react5.Children.forEach(node, (child) => {
354
+ if (typeof child === "string" || typeof child === "number") text += child;
355
+ else if ((0, import_react5.isValidElement)(child)) text += textOf(child.props.children);
356
+ });
357
+ return text;
358
+ }
359
+ function typeaheadIndex(labels, current, search) {
360
+ const count = labels.length;
361
+ if (count === 0 || search === "") return null;
362
+ const [first = "", ...others] = [...search];
363
+ const repeated = others.every((char) => char === first);
364
+ const needle = fold(repeated ? first : search);
365
+ const start = current < 0 ? 0 : repeated ? current + 1 : current;
366
+ for (let step = 0; step < count; step += 1) {
367
+ const index = (start + step) % count;
368
+ if (fold(labels[index] ?? "").startsWith(needle)) return index;
369
+ }
370
+ return null;
371
+ }
372
+ function useTypeahead() {
373
+ const searchRef = (0, import_react5.useRef)("");
374
+ const timerRef = (0, import_react5.useRef)(void 0);
375
+ (0, import_react5.useEffect)(() => () => clearTimeout(timerRef.current), []);
376
+ return (0, import_react5.useCallback)((event) => {
377
+ const { key } = event;
378
+ if (event.ctrlKey || event.metaKey || event.altKey) return null;
379
+ if ([...key].length !== 1) return null;
380
+ if (key === " " && searchRef.current === "") return null;
381
+ searchRef.current += key;
382
+ clearTimeout(timerRef.current);
383
+ timerRef.current = setTimeout(() => {
384
+ searchRef.current = "";
385
+ }, TYPEAHEAD_TIMEOUT);
386
+ return searchRef.current;
387
+ }, []);
388
+ }
389
+
390
+ // src/menu/MenuPopup.tsx
321
391
  function MenuPopup({
322
- side = "bottom",
392
+ side: sideProp,
323
393
  align = "start",
324
394
  className,
325
395
  children,
@@ -334,12 +404,16 @@ function MenuPopup({
334
404
  triggerId,
335
405
  popupId,
336
406
  anchorName,
337
- triggerRef
407
+ triggerRef,
408
+ parent,
409
+ popupRef: elementRef,
410
+ openSubmenuRef
338
411
  } = useMenuRootContext("Popup");
339
- const elementRef = (0, import_react5.useRef)(null);
340
- const focusPendingRef = (0, import_react5.useRef)(false);
341
- const [, resync] = (0, import_react5.useReducer)((n) => n + 1, 0);
342
- (0, import_react5.useEffect)(() => {
412
+ const side = sideProp ?? (parent ? "right" : "bottom");
413
+ const focusPendingRef = (0, import_react6.useRef)(false);
414
+ const [, resync] = (0, import_react6.useReducer)((n) => n + 1, 0);
415
+ const typeahead = useTypeahead();
416
+ (0, import_react6.useEffect)(() => {
343
417
  const popup = elementRef.current;
344
418
  if (!popup || typeof popup.showPopover !== "function") return;
345
419
  const shown = popup.matches(":popover-open");
@@ -353,7 +427,8 @@ function MenuPopup({
353
427
  }
354
428
  if (focusPendingRef.current && navigableItems.length > 0) {
355
429
  focusPendingRef.current = false;
356
- 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;
357
432
  target?.ref.current?.focus();
358
433
  }
359
434
  });
@@ -378,15 +453,35 @@ function MenuPopup({
378
453
  }
379
454
  const current = navigableItems.findIndex((i) => i.ref.current === event.target);
380
455
  if (current < 0 && event.target !== event.currentTarget) return;
381
- const next = rovingIndex(event.key, {
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
+ }
462
+ const search = typeahead(event);
463
+ const next = search === null ? rovingIndex(event.key, {
382
464
  count: navigableItems.length,
383
465
  current,
384
466
  orientation: "vertical",
385
467
  rtl: false
386
- });
468
+ }) : typeaheadIndex(
469
+ navigableItems.map((i) => i.textValue),
470
+ current,
471
+ search
472
+ );
473
+ if (search !== null) event.preventDefault();
387
474
  if (next === null) return;
388
475
  event.preventDefault();
389
- 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();
390
485
  },
391
486
  onToggle(event) {
392
487
  if (event.target !== event.currentTarget) return;
@@ -399,34 +494,415 @@ function MenuPopup({
399
494
  });
400
495
  }
401
496
 
497
+ // src/menu/useMenuItem.ts
498
+ var import_react7 = require("react");
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;
529
+ const ref = (0, import_react7.useRef)(null);
530
+ const text = textValue ?? textOf(children);
531
+ (0, import_react7.useLayoutEffect)(() => register({ disabled, textValue: text, ref }), [register, disabled, text]);
532
+ return {
533
+ closeAll,
534
+ itemProps: {
535
+ type: "button",
536
+ tabIndex: -1,
537
+ disabled: disabled && !focusableWhenDisabled || void 0,
538
+ "aria-disabled": disabled && focusableWhenDisabled || void 0,
539
+ "data-disabled": disabled ? "" : void 0,
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
+ }
546
+ }
547
+ };
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
+ }
553
+ var MenuItemIndicatorContext = (0, import_react7.createContext)(null);
554
+ function useMenuItemIndicatorContext() {
555
+ const context = (0, import_react7.useContext)(MenuItemIndicatorContext);
556
+ if (context === null) {
557
+ throw new Error(
558
+ "<Menu.ItemIndicator> must be rendered inside <Menu.CheckboxItem> or <Menu.RadioItem>."
559
+ );
560
+ }
561
+ return context;
562
+ }
563
+ function menuCheckableDataAttributes({ checked, disabled }) {
564
+ return {
565
+ "data-checked": checked ? "" : void 0,
566
+ "data-unchecked": checked ? void 0 : "",
567
+ "data-disabled": disabled ? "" : void 0
568
+ };
569
+ }
570
+
402
571
  // src/menu/MenuItem.tsx
403
- var import_react6 = require("react");
404
572
  function MenuItem({
405
573
  disabled = false,
574
+ textValue,
406
575
  onClick,
407
576
  className,
408
577
  children,
409
578
  render,
410
579
  ...rest
411
580
  }) {
412
- const { setOpen, focusableWhenDisabled, register } = useMenuRootContext("Item");
413
- const elementRef = (0, import_react6.useRef)(null);
414
- (0, import_react6.useLayoutEffect)(() => register({ disabled, ref: elementRef }), [register, disabled]);
581
+ const { closeAll, itemProps } = useMenuItem("Item", { disabled, textValue, children });
415
582
  return useRender({
416
583
  render,
417
584
  defaultTagName: "button",
418
585
  props: {
419
- type: "button",
586
+ ...itemProps,
420
587
  role: "menuitem",
421
- tabIndex: -1,
422
- disabled: disabled && !focusableWhenDisabled || void 0,
423
- "aria-disabled": disabled && focusableWhenDisabled || void 0,
424
- ...menuItemDataAttributes(disabled),
425
588
  className,
426
589
  children,
427
- ref: elementRef,
428
590
  onClick() {
429
- if (!disabled) setOpen(false);
591
+ if (!disabled) closeAll();
592
+ }
593
+ },
594
+ consumerProps: disabled ? rest : { ...rest, onClick }
595
+ });
596
+ }
597
+
598
+ // src/menu/MenuCheckboxItem.tsx
599
+ var import_react8 = require("react");
600
+ var import_jsx_runtime2 = require("react/jsx-runtime");
601
+ function MenuCheckboxItem({
602
+ checked: checkedProp,
603
+ defaultChecked,
604
+ onCheckedChange,
605
+ closeOnClick = false,
606
+ disabled = false,
607
+ textValue,
608
+ onClick,
609
+ className,
610
+ children,
611
+ render,
612
+ ...rest
613
+ }) {
614
+ const { closeAll, itemProps } = useMenuItem("CheckboxItem", { disabled, textValue, children });
615
+ const [checked, setChecked] = useControlled({
616
+ controlled: checkedProp,
617
+ default: defaultChecked ?? false,
618
+ name: "Menu.CheckboxItem",
619
+ state: "checked"
620
+ });
621
+ const state = (0, import_react8.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
622
+ const element = useRender({
623
+ render,
624
+ defaultTagName: "button",
625
+ props: {
626
+ ...itemProps,
627
+ role: "menuitemcheckbox",
628
+ "aria-checked": checked,
629
+ ...menuCheckableDataAttributes(state),
630
+ className,
631
+ children,
632
+ onClick() {
633
+ if (disabled) return;
634
+ setChecked(!checked);
635
+ onCheckedChange?.(!checked);
636
+ if (closeOnClick) closeAll();
637
+ }
638
+ },
639
+ consumerProps: disabled ? rest : { ...rest, onClick }
640
+ });
641
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MenuItemIndicatorContext.Provider, { value: state, children: element });
642
+ }
643
+
644
+ // src/menu/MenuRadioGroup.tsx
645
+ var import_react10 = require("react");
646
+
647
+ // src/menu/MenuGroup.tsx
648
+ var import_react9 = require("react");
649
+ var import_jsx_runtime3 = require("react/jsx-runtime");
650
+ var MenuGroupContext = (0, import_react9.createContext)(null);
651
+ function useMenuGroupContext() {
652
+ return (0, import_react9.useContext)(MenuGroupContext);
653
+ }
654
+ function useMenuGroupElement(part, {
655
+ render,
656
+ props,
657
+ consumerProps
658
+ }) {
659
+ useMenuRootContext(part);
660
+ const labelId = `${(0, import_react9.useId)()}-label`;
661
+ const [hasLabel, setHasLabel] = (0, import_react9.useState)(false);
662
+ const registerLabel = (0, import_react9.useCallback)(() => {
663
+ setHasLabel(true);
664
+ return () => setHasLabel(false);
665
+ }, []);
666
+ const context = (0, import_react9.useMemo)(() => ({ labelId, registerLabel }), [labelId, registerLabel]);
667
+ const element = useRender({
668
+ render,
669
+ defaultTagName: "div",
670
+ props: { role: "group", "aria-labelledby": hasLabel ? labelId : void 0, ...props },
671
+ consumerProps
672
+ });
673
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MenuGroupContext.Provider, { value: context, children: element });
674
+ }
675
+ function MenuGroup({ className, children, render, ...rest }) {
676
+ return useMenuGroupElement("Group", {
677
+ render,
678
+ props: { className, children },
679
+ consumerProps: rest
680
+ });
681
+ }
682
+
683
+ // src/menu/MenuRadioGroup.tsx
684
+ var import_jsx_runtime4 = require("react/jsx-runtime");
685
+ var MenuRadioGroupContext = (0, import_react10.createContext)(null);
686
+ function useMenuRadioGroupContext() {
687
+ const context = (0, import_react10.useContext)(MenuRadioGroupContext);
688
+ if (context === null) {
689
+ throw new Error("<Menu.RadioItem> must be rendered inside <Menu.RadioGroup>.");
690
+ }
691
+ return context;
692
+ }
693
+ function MenuRadioGroup({
694
+ value: valueProp,
695
+ defaultValue,
696
+ onValueChange,
697
+ disabled = false,
698
+ className,
699
+ children,
700
+ render,
701
+ ...rest
702
+ }) {
703
+ const [value, setValue] = useControlled({
704
+ controlled: valueProp,
705
+ default: defaultValue ?? null,
706
+ name: "Menu.RadioGroup",
707
+ state: "value"
708
+ });
709
+ const select = (0, import_react10.useCallback)(
710
+ (next) => {
711
+ if (next === value) return;
712
+ setValue(next);
713
+ onValueChange?.(next);
714
+ },
715
+ [value, setValue, onValueChange]
716
+ );
717
+ const context = (0, import_react10.useMemo)(() => ({ value, disabled, select }), [value, disabled, select]);
718
+ const element = useMenuGroupElement("RadioGroup", {
719
+ render,
720
+ props: { "data-disabled": disabled ? "" : void 0, className, children },
721
+ consumerProps: rest
722
+ });
723
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MenuRadioGroupContext.Provider, { value: context, children: element });
724
+ }
725
+
726
+ // src/menu/MenuRadioItem.tsx
727
+ var import_react11 = require("react");
728
+ var import_jsx_runtime5 = require("react/jsx-runtime");
729
+ function MenuRadioItem({
730
+ value,
731
+ closeOnClick = false,
732
+ disabled: disabledProp = false,
733
+ textValue,
734
+ onClick,
735
+ className,
736
+ children,
737
+ render,
738
+ ...rest
739
+ }) {
740
+ const group = useMenuRadioGroupContext();
741
+ const disabled = disabledProp || group.disabled;
742
+ const { closeAll, itemProps } = useMenuItem("RadioItem", { disabled, textValue, children });
743
+ const checked = group.value === value;
744
+ const state = (0, import_react11.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
745
+ const element = useRender({
746
+ render,
747
+ defaultTagName: "button",
748
+ props: {
749
+ ...itemProps,
750
+ role: "menuitemradio",
751
+ "aria-checked": checked,
752
+ ...menuCheckableDataAttributes(state),
753
+ className,
754
+ children,
755
+ onClick() {
756
+ if (disabled) return;
757
+ group.select(value);
758
+ if (closeOnClick) closeAll();
759
+ }
760
+ },
761
+ consumerProps: disabled ? rest : { ...rest, onClick }
762
+ });
763
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(MenuItemIndicatorContext.Provider, { value: state, children: element });
764
+ }
765
+
766
+ // src/menu/MenuItemIndicator.tsx
767
+ function MenuItemIndicator({
768
+ className,
769
+ children,
770
+ render,
771
+ ...rest
772
+ }) {
773
+ const state = useMenuItemIndicatorContext();
774
+ return useRender({
775
+ render,
776
+ defaultTagName: "span",
777
+ props: { "aria-hidden": true, ...menuCheckableDataAttributes(state), className, children },
778
+ consumerProps: rest
779
+ });
780
+ }
781
+
782
+ // src/menu/MenuGroupLabel.tsx
783
+ var import_react12 = require("react");
784
+ function MenuGroupLabel({ className, children, render, ...rest }) {
785
+ const group = useMenuGroupContext();
786
+ if (group === null) {
787
+ throw new Error("<Menu.GroupLabel> must be rendered inside <Menu.Group> or <Menu.RadioGroup>.");
788
+ }
789
+ const { labelId, registerLabel } = group;
790
+ (0, import_react12.useLayoutEffect)(registerLabel, [registerLabel]);
791
+ return useRender({
792
+ render,
793
+ defaultTagName: "div",
794
+ props: { id: labelId, className, children },
795
+ consumerProps: rest
796
+ });
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
+ };
430
906
  }
431
907
  },
432
908
  consumerProps: disabled ? rest : { ...rest, onClick }