@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.
@@ -1,12 +1,15 @@
1
1
  import {
2
- anchorNameFor,
3
- anchoredDataAttributes,
4
- anchoredPopupStyle
5
- } from "../chunk-HCOCPQXS.js";
2
+ fold
3
+ } from "../chunk-ETNICT5M.js";
6
4
  import {
7
5
  byDocumentOrder,
8
6
  rovingIndex
9
7
  } from "../chunk-2S4XG4VD.js";
8
+ import {
9
+ anchorNameFor,
10
+ anchoredDataAttributes,
11
+ anchoredPopupStyle
12
+ } from "../chunk-HCOCPQXS.js";
10
13
  import {
11
14
  useControlled
12
15
  } from "../chunk-UEIB3I3T.js";
@@ -18,9 +21,17 @@ import {
18
21
  // src/menu/index.parts.ts
19
22
  var index_parts_exports = {};
20
23
  __export(index_parts_exports, {
24
+ CheckboxItem: () => MenuCheckboxItem,
25
+ Group: () => MenuGroup,
26
+ GroupLabel: () => MenuGroupLabel,
21
27
  Item: () => MenuItem,
28
+ ItemIndicator: () => MenuItemIndicator,
22
29
  Popup: () => MenuPopup,
30
+ RadioGroup: () => MenuRadioGroup,
31
+ RadioItem: () => MenuRadioItem,
23
32
  Root: () => MenuRoot,
33
+ SubmenuRoot: () => MenuSubmenuRoot,
34
+ SubmenuTrigger: () => MenuSubmenuTrigger,
24
35
  Trigger: () => MenuTrigger
25
36
  });
26
37
 
@@ -40,17 +51,20 @@ function useMenuRootContext(part) {
40
51
 
41
52
  // src/menu/MenuRoot.tsx
42
53
  import { jsx } from "react/jsx-runtime";
43
- 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, {
44
59
  open: openProp,
45
60
  defaultOpen,
46
61
  onOpenChange,
47
- focusableWhenDisabled = false,
48
- children
49
- }) {
62
+ focusableWhenDisabled = false
63
+ }, parent) {
50
64
  const [open, setOpenState] = useControlled({
51
65
  controlled: openProp,
52
66
  default: defaultOpen ?? false,
53
- name: "Menu.Root",
67
+ name,
54
68
  state: "open"
55
69
  });
56
70
  const setOpen = useCallback(
@@ -61,6 +75,10 @@ function MenuRoot({
61
75
  },
62
76
  [open, setOpenState, onOpenChange]
63
77
  );
78
+ const closeAll = useCallback(() => {
79
+ setOpen(false);
80
+ parent?.closeAll();
81
+ }, [setOpen, parent]);
64
82
  const [items, setItems] = useState([]);
65
83
  const register = useCallback((entry) => {
66
84
  setItems((current) => [...current, entry].sort(byDocumentOrder));
@@ -73,10 +91,14 @@ function MenuRoot({
73
91
  const id = useId();
74
92
  const triggerRef = useRef(null);
75
93
  const focusOnOpenRef = useRef("first");
76
- const context = useMemo(
94
+ const popupRef = useRef(null);
95
+ const openSubmenuRef = useRef(null);
96
+ const graceRef = useRef(null);
97
+ return useMemo(
77
98
  () => ({
78
99
  open,
79
100
  setOpen,
101
+ closeAll,
80
102
  navigableItems,
81
103
  focusableWhenDisabled,
82
104
  register,
@@ -84,11 +106,14 @@ function MenuRoot({
84
106
  triggerId: `${id}-trigger`,
85
107
  popupId: `${id}-popup`,
86
108
  anchorName: anchorNameFor(id),
87
- triggerRef
109
+ triggerRef,
110
+ parent,
111
+ popupRef,
112
+ openSubmenuRef,
113
+ graceRef
88
114
  }),
89
- [open, setOpen, navigableItems, focusableWhenDisabled, register, id]
115
+ [open, setOpen, closeAll, navigableItems, focusableWhenDisabled, register, id, parent]
90
116
  );
91
- return /* @__PURE__ */ jsx(MenuRootContext.Provider, { value: context, children });
92
117
  }
93
118
 
94
119
  // src/menu/menuDataAttributes.ts
@@ -98,9 +123,6 @@ function menuDataAttributes({ open }) {
98
123
  "data-closed": open ? void 0 : ""
99
124
  };
100
125
  }
101
- function menuItemDataAttributes(disabled) {
102
- return { "data-disabled": disabled ? "" : void 0 };
103
- }
104
126
 
105
127
  // src/menu/MenuTrigger.tsx
106
128
  function MenuTrigger({ className, children, render, ...rest }) {
@@ -135,9 +157,53 @@ function MenuTrigger({ className, children, render, ...rest }) {
135
157
  }
136
158
 
137
159
  // src/menu/MenuPopup.tsx
138
- import { useEffect, useReducer, useRef as useRef2 } from "react";
160
+ import { useEffect as useEffect2, useReducer, useRef as useRef3 } from "react";
161
+
162
+ // src/core/typeahead.ts
163
+ import { Children, isValidElement, useCallback as useCallback2, useEffect, useRef as useRef2 } from "react";
164
+ var TYPEAHEAD_TIMEOUT = 500;
165
+ function textOf(node) {
166
+ let text = "";
167
+ Children.forEach(node, (child) => {
168
+ if (typeof child === "string" || typeof child === "number") text += child;
169
+ else if (isValidElement(child)) text += textOf(child.props.children);
170
+ });
171
+ return text;
172
+ }
173
+ function typeaheadIndex(labels, current, search) {
174
+ const count = labels.length;
175
+ if (count === 0 || search === "") return null;
176
+ const [first = "", ...others] = [...search];
177
+ const repeated = others.every((char) => char === first);
178
+ const needle = fold(repeated ? first : search);
179
+ const start = current < 0 ? 0 : repeated ? current + 1 : current;
180
+ for (let step = 0; step < count; step += 1) {
181
+ const index = (start + step) % count;
182
+ if (fold(labels[index] ?? "").startsWith(needle)) return index;
183
+ }
184
+ return null;
185
+ }
186
+ function useTypeahead() {
187
+ const searchRef = useRef2("");
188
+ const timerRef = useRef2(void 0);
189
+ useEffect(() => () => clearTimeout(timerRef.current), []);
190
+ return useCallback2((event) => {
191
+ const { key } = event;
192
+ if (event.ctrlKey || event.metaKey || event.altKey) return null;
193
+ if ([...key].length !== 1) return null;
194
+ if (key === " " && searchRef.current === "") return null;
195
+ searchRef.current += key;
196
+ clearTimeout(timerRef.current);
197
+ timerRef.current = setTimeout(() => {
198
+ searchRef.current = "";
199
+ }, TYPEAHEAD_TIMEOUT);
200
+ return searchRef.current;
201
+ }, []);
202
+ }
203
+
204
+ // src/menu/MenuPopup.tsx
139
205
  function MenuPopup({
140
- side = "bottom",
206
+ side: sideProp,
141
207
  align = "start",
142
208
  className,
143
209
  children,
@@ -152,12 +218,16 @@ function MenuPopup({
152
218
  triggerId,
153
219
  popupId,
154
220
  anchorName,
155
- triggerRef
221
+ triggerRef,
222
+ parent,
223
+ popupRef: elementRef,
224
+ openSubmenuRef
156
225
  } = useMenuRootContext("Popup");
157
- const elementRef = useRef2(null);
158
- const focusPendingRef = useRef2(false);
226
+ const side = sideProp ?? (parent ? "right" : "bottom");
227
+ const focusPendingRef = useRef3(false);
159
228
  const [, resync] = useReducer((n) => n + 1, 0);
160
- useEffect(() => {
229
+ const typeahead = useTypeahead();
230
+ useEffect2(() => {
161
231
  const popup = elementRef.current;
162
232
  if (!popup || typeof popup.showPopover !== "function") return;
163
233
  const shown = popup.matches(":popover-open");
@@ -171,7 +241,8 @@ function MenuPopup({
171
241
  }
172
242
  if (focusPendingRef.current && navigableItems.length > 0) {
173
243
  focusPendingRef.current = false;
174
- 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;
175
246
  target?.ref.current?.focus();
176
247
  }
177
248
  });
@@ -196,15 +267,35 @@ function MenuPopup({
196
267
  }
197
268
  const current = navigableItems.findIndex((i) => i.ref.current === event.target);
198
269
  if (current < 0 && event.target !== event.currentTarget) return;
199
- const next = rovingIndex(event.key, {
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
+ }
276
+ const search = typeahead(event);
277
+ const next = search === null ? rovingIndex(event.key, {
200
278
  count: navigableItems.length,
201
279
  current,
202
280
  orientation: "vertical",
203
281
  rtl: false
204
- });
282
+ }) : typeaheadIndex(
283
+ navigableItems.map((i) => i.textValue),
284
+ current,
285
+ search
286
+ );
287
+ if (search !== null) event.preventDefault();
205
288
  if (next === null) return;
206
289
  event.preventDefault();
207
- 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();
208
299
  },
209
300
  onToggle(event) {
210
301
  if (event.target !== event.currentTarget) return;
@@ -217,34 +308,415 @@ function MenuPopup({
217
308
  });
218
309
  }
219
310
 
311
+ // src/menu/useMenuItem.ts
312
+ import { createContext as createContext2, useContext as useContext2, useLayoutEffect, useRef as useRef4 } from "react";
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;
343
+ const ref = useRef4(null);
344
+ const text = textValue ?? textOf(children);
345
+ useLayoutEffect(() => register({ disabled, textValue: text, ref }), [register, disabled, text]);
346
+ return {
347
+ closeAll,
348
+ itemProps: {
349
+ type: "button",
350
+ tabIndex: -1,
351
+ disabled: disabled && !focusableWhenDisabled || void 0,
352
+ "aria-disabled": disabled && focusableWhenDisabled || void 0,
353
+ "data-disabled": disabled ? "" : void 0,
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
+ }
360
+ }
361
+ };
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
+ }
367
+ var MenuItemIndicatorContext = createContext2(null);
368
+ function useMenuItemIndicatorContext() {
369
+ const context = useContext2(MenuItemIndicatorContext);
370
+ if (context === null) {
371
+ throw new Error(
372
+ "<Menu.ItemIndicator> must be rendered inside <Menu.CheckboxItem> or <Menu.RadioItem>."
373
+ );
374
+ }
375
+ return context;
376
+ }
377
+ function menuCheckableDataAttributes({ checked, disabled }) {
378
+ return {
379
+ "data-checked": checked ? "" : void 0,
380
+ "data-unchecked": checked ? void 0 : "",
381
+ "data-disabled": disabled ? "" : void 0
382
+ };
383
+ }
384
+
220
385
  // src/menu/MenuItem.tsx
221
- import { useLayoutEffect, useRef as useRef3 } from "react";
222
386
  function MenuItem({
223
387
  disabled = false,
388
+ textValue,
224
389
  onClick,
225
390
  className,
226
391
  children,
227
392
  render,
228
393
  ...rest
229
394
  }) {
230
- const { setOpen, focusableWhenDisabled, register } = useMenuRootContext("Item");
231
- const elementRef = useRef3(null);
232
- useLayoutEffect(() => register({ disabled, ref: elementRef }), [register, disabled]);
395
+ const { closeAll, itemProps } = useMenuItem("Item", { disabled, textValue, children });
233
396
  return useRender({
234
397
  render,
235
398
  defaultTagName: "button",
236
399
  props: {
237
- type: "button",
400
+ ...itemProps,
238
401
  role: "menuitem",
239
- tabIndex: -1,
240
- disabled: disabled && !focusableWhenDisabled || void 0,
241
- "aria-disabled": disabled && focusableWhenDisabled || void 0,
242
- ...menuItemDataAttributes(disabled),
243
402
  className,
244
403
  children,
245
- ref: elementRef,
246
404
  onClick() {
247
- if (!disabled) setOpen(false);
405
+ if (!disabled) closeAll();
406
+ }
407
+ },
408
+ consumerProps: disabled ? rest : { ...rest, onClick }
409
+ });
410
+ }
411
+
412
+ // src/menu/MenuCheckboxItem.tsx
413
+ import { useMemo as useMemo2 } from "react";
414
+ import { jsx as jsx2 } from "react/jsx-runtime";
415
+ function MenuCheckboxItem({
416
+ checked: checkedProp,
417
+ defaultChecked,
418
+ onCheckedChange,
419
+ closeOnClick = false,
420
+ disabled = false,
421
+ textValue,
422
+ onClick,
423
+ className,
424
+ children,
425
+ render,
426
+ ...rest
427
+ }) {
428
+ const { closeAll, itemProps } = useMenuItem("CheckboxItem", { disabled, textValue, children });
429
+ const [checked, setChecked] = useControlled({
430
+ controlled: checkedProp,
431
+ default: defaultChecked ?? false,
432
+ name: "Menu.CheckboxItem",
433
+ state: "checked"
434
+ });
435
+ const state = useMemo2(() => ({ checked, disabled }), [checked, disabled]);
436
+ const element = useRender({
437
+ render,
438
+ defaultTagName: "button",
439
+ props: {
440
+ ...itemProps,
441
+ role: "menuitemcheckbox",
442
+ "aria-checked": checked,
443
+ ...menuCheckableDataAttributes(state),
444
+ className,
445
+ children,
446
+ onClick() {
447
+ if (disabled) return;
448
+ setChecked(!checked);
449
+ onCheckedChange?.(!checked);
450
+ if (closeOnClick) closeAll();
451
+ }
452
+ },
453
+ consumerProps: disabled ? rest : { ...rest, onClick }
454
+ });
455
+ return /* @__PURE__ */ jsx2(MenuItemIndicatorContext.Provider, { value: state, children: element });
456
+ }
457
+
458
+ // src/menu/MenuRadioGroup.tsx
459
+ import { createContext as createContext4, useCallback as useCallback4, useContext as useContext4, useMemo as useMemo4 } from "react";
460
+
461
+ // src/menu/MenuGroup.tsx
462
+ import { createContext as createContext3, useCallback as useCallback3, useContext as useContext3, useId as useId2, useMemo as useMemo3, useState as useState2 } from "react";
463
+ import { jsx as jsx3 } from "react/jsx-runtime";
464
+ var MenuGroupContext = createContext3(null);
465
+ function useMenuGroupContext() {
466
+ return useContext3(MenuGroupContext);
467
+ }
468
+ function useMenuGroupElement(part, {
469
+ render,
470
+ props,
471
+ consumerProps
472
+ }) {
473
+ useMenuRootContext(part);
474
+ const labelId = `${useId2()}-label`;
475
+ const [hasLabel, setHasLabel] = useState2(false);
476
+ const registerLabel = useCallback3(() => {
477
+ setHasLabel(true);
478
+ return () => setHasLabel(false);
479
+ }, []);
480
+ const context = useMemo3(() => ({ labelId, registerLabel }), [labelId, registerLabel]);
481
+ const element = useRender({
482
+ render,
483
+ defaultTagName: "div",
484
+ props: { role: "group", "aria-labelledby": hasLabel ? labelId : void 0, ...props },
485
+ consumerProps
486
+ });
487
+ return /* @__PURE__ */ jsx3(MenuGroupContext.Provider, { value: context, children: element });
488
+ }
489
+ function MenuGroup({ className, children, render, ...rest }) {
490
+ return useMenuGroupElement("Group", {
491
+ render,
492
+ props: { className, children },
493
+ consumerProps: rest
494
+ });
495
+ }
496
+
497
+ // src/menu/MenuRadioGroup.tsx
498
+ import { jsx as jsx4 } from "react/jsx-runtime";
499
+ var MenuRadioGroupContext = createContext4(null);
500
+ function useMenuRadioGroupContext() {
501
+ const context = useContext4(MenuRadioGroupContext);
502
+ if (context === null) {
503
+ throw new Error("<Menu.RadioItem> must be rendered inside <Menu.RadioGroup>.");
504
+ }
505
+ return context;
506
+ }
507
+ function MenuRadioGroup({
508
+ value: valueProp,
509
+ defaultValue,
510
+ onValueChange,
511
+ disabled = false,
512
+ className,
513
+ children,
514
+ render,
515
+ ...rest
516
+ }) {
517
+ const [value, setValue] = useControlled({
518
+ controlled: valueProp,
519
+ default: defaultValue ?? null,
520
+ name: "Menu.RadioGroup",
521
+ state: "value"
522
+ });
523
+ const select = useCallback4(
524
+ (next) => {
525
+ if (next === value) return;
526
+ setValue(next);
527
+ onValueChange?.(next);
528
+ },
529
+ [value, setValue, onValueChange]
530
+ );
531
+ const context = useMemo4(() => ({ value, disabled, select }), [value, disabled, select]);
532
+ const element = useMenuGroupElement("RadioGroup", {
533
+ render,
534
+ props: { "data-disabled": disabled ? "" : void 0, className, children },
535
+ consumerProps: rest
536
+ });
537
+ return /* @__PURE__ */ jsx4(MenuRadioGroupContext.Provider, { value: context, children: element });
538
+ }
539
+
540
+ // src/menu/MenuRadioItem.tsx
541
+ import { useMemo as useMemo5 } from "react";
542
+ import { jsx as jsx5 } from "react/jsx-runtime";
543
+ function MenuRadioItem({
544
+ value,
545
+ closeOnClick = false,
546
+ disabled: disabledProp = false,
547
+ textValue,
548
+ onClick,
549
+ className,
550
+ children,
551
+ render,
552
+ ...rest
553
+ }) {
554
+ const group = useMenuRadioGroupContext();
555
+ const disabled = disabledProp || group.disabled;
556
+ const { closeAll, itemProps } = useMenuItem("RadioItem", { disabled, textValue, children });
557
+ const checked = group.value === value;
558
+ const state = useMemo5(() => ({ checked, disabled }), [checked, disabled]);
559
+ const element = useRender({
560
+ render,
561
+ defaultTagName: "button",
562
+ props: {
563
+ ...itemProps,
564
+ role: "menuitemradio",
565
+ "aria-checked": checked,
566
+ ...menuCheckableDataAttributes(state),
567
+ className,
568
+ children,
569
+ onClick() {
570
+ if (disabled) return;
571
+ group.select(value);
572
+ if (closeOnClick) closeAll();
573
+ }
574
+ },
575
+ consumerProps: disabled ? rest : { ...rest, onClick }
576
+ });
577
+ return /* @__PURE__ */ jsx5(MenuItemIndicatorContext.Provider, { value: state, children: element });
578
+ }
579
+
580
+ // src/menu/MenuItemIndicator.tsx
581
+ function MenuItemIndicator({
582
+ className,
583
+ children,
584
+ render,
585
+ ...rest
586
+ }) {
587
+ const state = useMenuItemIndicatorContext();
588
+ return useRender({
589
+ render,
590
+ defaultTagName: "span",
591
+ props: { "aria-hidden": true, ...menuCheckableDataAttributes(state), className, children },
592
+ consumerProps: rest
593
+ });
594
+ }
595
+
596
+ // src/menu/MenuGroupLabel.tsx
597
+ import { useLayoutEffect as useLayoutEffect2 } from "react";
598
+ function MenuGroupLabel({ className, children, render, ...rest }) {
599
+ const group = useMenuGroupContext();
600
+ if (group === null) {
601
+ throw new Error("<Menu.GroupLabel> must be rendered inside <Menu.Group> or <Menu.RadioGroup>.");
602
+ }
603
+ const { labelId, registerLabel } = group;
604
+ useLayoutEffect2(registerLabel, [registerLabel]);
605
+ return useRender({
606
+ render,
607
+ defaultTagName: "div",
608
+ props: { id: labelId, className, children },
609
+ consumerProps: rest
610
+ });
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
+ };
248
720
  }
249
721
  },
250
722
  consumerProps: disabled ? rest : { ...rest, onClick }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "4.14.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",