@arun-dev/headless 4.15.0 → 4.17.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.
- package/dist/index.d.cts +1 -3
- package/dist/index.d.ts +1 -3
- package/dist/menu/index.cjs +208 -24
- package/dist/menu/index.d.cts +74 -12
- package/dist/menu/index.d.ts +74 -12
- package/dist/menu/index.js +208 -24
- package/dist/toast/index.d.cts +5 -0
- package/dist/toast/index.d.ts +5 -0
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -50,9 +50,7 @@ declare function mergeProps(...objects: (UnknownProps | undefined)[]): UnknownPr
|
|
|
50
50
|
* default element and the props it wants; the consumer can replace the element
|
|
51
51
|
* entirely via `render` without losing any of it.
|
|
52
52
|
*
|
|
53
|
-
*
|
|
54
|
-
* (memoised merging, so parts stop re-rendering their children needlessly) and keeps
|
|
55
|
-
* call sites stable when that lands.
|
|
53
|
+
* Uses no hooks; the `use` prefix keeps its call sites those of a hook.
|
|
56
54
|
*/
|
|
57
55
|
interface UseRenderParams {
|
|
58
56
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -50,9 +50,7 @@ declare function mergeProps(...objects: (UnknownProps | undefined)[]): UnknownPr
|
|
|
50
50
|
* default element and the props it wants; the consumer can replace the element
|
|
51
51
|
* entirely via `render` without losing any of it.
|
|
52
52
|
*
|
|
53
|
-
*
|
|
54
|
-
* (memoised merging, so parts stop re-rendering their children needlessly) and keeps
|
|
55
|
-
* call sites stable when that lands.
|
|
53
|
+
* Uses no hooks; the `use` prefix keeps its call sites those of a hook.
|
|
56
54
|
*/
|
|
57
55
|
interface UseRenderParams {
|
|
58
56
|
/**
|
package/dist/menu/index.cjs
CHANGED
|
@@ -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
|
-
|
|
148
|
-
}) {
|
|
152
|
+
focusableWhenDisabled = false
|
|
153
|
+
}, parent) {
|
|
149
154
|
const [open, setOpenState] = useControlled({
|
|
150
155
|
controlled: openProp,
|
|
151
156
|
default: defaultOpen ?? false,
|
|
152
|
-
name
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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 {
|
|
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)
|
|
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 {
|
|
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)
|
|
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 {
|
|
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)
|
|
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
|
package/dist/menu/index.d.cts
CHANGED
|
@@ -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({
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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 };
|
package/dist/menu/index.d.ts
CHANGED
|
@@ -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({
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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 };
|
package/dist/menu/index.js
CHANGED
|
@@ -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
|
-
|
|
58
|
-
}) {
|
|
62
|
+
focusableWhenDisabled = false
|
|
63
|
+
}, parent) {
|
|
59
64
|
const [open, setOpenState] = useControlled({
|
|
60
65
|
controlled: openProp,
|
|
61
66
|
default: defaultOpen ?? false,
|
|
62
|
-
name
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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 {
|
|
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)
|
|
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 {
|
|
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)
|
|
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 {
|
|
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)
|
|
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/dist/toast/index.d.cts
CHANGED
|
@@ -27,6 +27,11 @@ type ToastOptions<Data = unknown> = {
|
|
|
27
27
|
/** Milliseconds before it closes on its own. `0` keeps it until closed. Defaults to the Provider's. */
|
|
28
28
|
timeout?: number;
|
|
29
29
|
priority?: ToastPriority;
|
|
30
|
+
/**
|
|
31
|
+
* Where this toast shows, for the ui's default rendering, overriding its Viewport's
|
|
32
|
+
* `position`. Headless keeps it and does nothing with it: your own layout places toasts.
|
|
33
|
+
*/
|
|
34
|
+
position?: string;
|
|
30
35
|
/** A button on the toast, for the ui's default rendering. The headless `Action` part is yours to place. */
|
|
31
36
|
action?: {
|
|
32
37
|
label: ReactNode;
|
package/dist/toast/index.d.ts
CHANGED
|
@@ -27,6 +27,11 @@ type ToastOptions<Data = unknown> = {
|
|
|
27
27
|
/** Milliseconds before it closes on its own. `0` keeps it until closed. Defaults to the Provider's. */
|
|
28
28
|
timeout?: number;
|
|
29
29
|
priority?: ToastPriority;
|
|
30
|
+
/**
|
|
31
|
+
* Where this toast shows, for the ui's default rendering, overriding its Viewport's
|
|
32
|
+
* `position`. Headless keeps it and does nothing with it: your own layout places toasts.
|
|
33
|
+
*/
|
|
34
|
+
position?: string;
|
|
30
35
|
/** A button on the toast, for the ui's default rendering. The headless `Action` part is yours to place. */
|
|
31
36
|
action?: {
|
|
32
37
|
label: ReactNode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arun-dev/headless",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.17.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",
|