@diwauhris/ui 1.7.19 → 1.7.21
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/lib/index.cjs +88 -23
- package/lib/index.mjs +88 -23
- package/lib/types/ui-library/gallery/menu/Menu.d.ts +21 -4
- package/package.json +1 -1
package/lib/index.cjs
CHANGED
|
@@ -3924,9 +3924,15 @@ function Dropdown({ trigger, isOpen, onToggle, onClose, children, triggerClassNa
|
|
|
3924
3924
|
* Menu — Design System Component
|
|
3925
3925
|
*
|
|
3926
3926
|
* A dropdown menu anchored to a trigger. Opens on trigger click,
|
|
3927
|
-
* closes on outside-click or
|
|
3928
|
-
* ArrowDown/ArrowUp
|
|
3929
|
-
* Escape
|
|
3927
|
+
* closes on outside-click, Escape, or scroll. Full keyboard navigation:
|
|
3928
|
+
* ArrowDown/ArrowUp move between items (skipping disabled), Home/End jump to
|
|
3929
|
+
* the first/last enabled item, Escape closes. When focus is on a closed
|
|
3930
|
+
* trigger, Enter/Space/ArrowDown open it and focus the first item; ArrowUp
|
|
3931
|
+
* opens it and focuses the last item.
|
|
3932
|
+
*
|
|
3933
|
+
* The panel renders in a body portal with fixed positioning: it flips above
|
|
3934
|
+
* the trigger when there isn't room below, and clamps horizontally so it never
|
|
3935
|
+
* overflows the viewport.
|
|
3930
3936
|
*
|
|
3931
3937
|
* Follows WAI-ARIA Menu Button pattern:
|
|
3932
3938
|
* - trigger: role="button", aria-haspopup="menu", aria-expanded
|
|
@@ -3942,7 +3948,7 @@ function Dropdown({ trigger, isOpen, onToggle, onClose, children, triggerClassNa
|
|
|
3942
3948
|
* ]}
|
|
3943
3949
|
* />
|
|
3944
3950
|
*/
|
|
3945
|
-
function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: controlledOpen, onOpenChange, panelClassName = "" }) {
|
|
3951
|
+
function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: controlledOpen, onOpenChange, closeOnSelect = true, panelClassName = "" }) {
|
|
3946
3952
|
const [internalOpen, setInternalOpen] = (0, react.useState)(false);
|
|
3947
3953
|
const isControlled = controlledOpen !== void 0;
|
|
3948
3954
|
const open = isControlled ? controlledOpen : internalOpen;
|
|
@@ -3955,18 +3961,45 @@ function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: contr
|
|
|
3955
3961
|
const panelRef = (0, react.useRef)(null);
|
|
3956
3962
|
const itemRefs = (0, react.useRef)([]);
|
|
3957
3963
|
const [panelStyle, setPanelStyle] = (0, react.useState)({});
|
|
3964
|
+
const pendingFocusRef = (0, react.useRef)("first");
|
|
3958
3965
|
(0, react.useEffect)(() => {
|
|
3959
3966
|
if (!open || !triggerRef.current) return;
|
|
3960
|
-
const
|
|
3967
|
+
const GAP = 4;
|
|
3968
|
+
const MARGIN = 8;
|
|
3961
3969
|
const PANEL_MIN_WIDTH = 176;
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
+
const ROW_HEIGHT = 36;
|
|
3971
|
+
const HEADER_HEIGHT = 8;
|
|
3972
|
+
const reposition = () => {
|
|
3973
|
+
const trigger = triggerRef.current;
|
|
3974
|
+
if (!trigger) return;
|
|
3975
|
+
const rect = trigger.getBoundingClientRect();
|
|
3976
|
+
const panelHeight = panelRef.current?.offsetHeight ?? items.length * ROW_HEIGHT + HEADER_HEIGHT;
|
|
3977
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
3978
|
+
const flipUp = spaceBelow < panelHeight + GAP + MARGIN && rect.top > spaceBelow;
|
|
3979
|
+
const panelWidth = Math.max(panelRef.current?.offsetWidth ?? 0, rect.width, PANEL_MIN_WIDTH);
|
|
3980
|
+
const desiredLeft = align === "left" ? rect.left : rect.right - panelWidth;
|
|
3981
|
+
const maxLeft = window.innerWidth - panelWidth - MARGIN;
|
|
3982
|
+
const left = Math.min(Math.max(MARGIN, desiredLeft), Math.max(MARGIN, maxLeft));
|
|
3983
|
+
setPanelStyle({
|
|
3984
|
+
position: "fixed",
|
|
3985
|
+
...flipUp ? { bottom: Math.max(MARGIN, window.innerHeight - rect.top + GAP) } : { top: rect.bottom + GAP },
|
|
3986
|
+
left,
|
|
3987
|
+
zIndex: 9999,
|
|
3988
|
+
minWidth: Math.max(rect.width, PANEL_MIN_WIDTH)
|
|
3989
|
+
});
|
|
3990
|
+
};
|
|
3991
|
+
reposition();
|
|
3992
|
+
const raf = window.requestAnimationFrame(reposition);
|
|
3993
|
+
window.addEventListener("resize", reposition);
|
|
3994
|
+
return () => {
|
|
3995
|
+
window.cancelAnimationFrame(raf);
|
|
3996
|
+
window.removeEventListener("resize", reposition);
|
|
3997
|
+
};
|
|
3998
|
+
}, [
|
|
3999
|
+
open,
|
|
4000
|
+
align,
|
|
4001
|
+
items.length
|
|
4002
|
+
]);
|
|
3970
4003
|
(0, react.useEffect)(() => {
|
|
3971
4004
|
if (!open) return;
|
|
3972
4005
|
const handlePointerDown = (e) => {
|
|
@@ -3983,33 +4016,64 @@ function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: contr
|
|
|
3983
4016
|
(triggerRef.current?.querySelector("button,a,[tabindex]"))?.focus();
|
|
3984
4017
|
}
|
|
3985
4018
|
};
|
|
4019
|
+
const handleScroll = (e) => {
|
|
4020
|
+
if (panelRef.current && e.target instanceof Node && panelRef.current.contains(e.target)) return;
|
|
4021
|
+
setOpen(false);
|
|
4022
|
+
};
|
|
3986
4023
|
document.addEventListener("mousedown", handlePointerDown);
|
|
3987
4024
|
document.addEventListener("keydown", handleKeyDown);
|
|
4025
|
+
window.addEventListener("scroll", handleScroll, true);
|
|
3988
4026
|
window.setTimeout(() => {
|
|
3989
|
-
itemRefs.current.
|
|
4027
|
+
const enabled = itemRefs.current.map((node, i) => ({
|
|
4028
|
+
node,
|
|
4029
|
+
i
|
|
4030
|
+
})).filter((x) => x.node && !x.node.disabled);
|
|
4031
|
+
(pendingFocusRef.current === "last" ? enabled[enabled.length - 1] : enabled[0])?.node?.focus();
|
|
4032
|
+
pendingFocusRef.current = "first";
|
|
3990
4033
|
}, 0);
|
|
3991
4034
|
return () => {
|
|
3992
4035
|
document.removeEventListener("mousedown", handlePointerDown);
|
|
3993
4036
|
document.removeEventListener("keydown", handleKeyDown);
|
|
4037
|
+
window.removeEventListener("scroll", handleScroll, true);
|
|
3994
4038
|
};
|
|
3995
4039
|
}, [open]);
|
|
3996
|
-
const
|
|
3997
|
-
const
|
|
3998
|
-
|
|
4040
|
+
const focusFrom = (index, dir) => {
|
|
4041
|
+
const n = items.length;
|
|
4042
|
+
if (n === 0) return;
|
|
4043
|
+
for (let step = 0; step < n; step++) {
|
|
4044
|
+
const next = ((index + dir * step) % n + n) % n;
|
|
4045
|
+
const node = itemRefs.current[next];
|
|
4046
|
+
if (node && !node.disabled) {
|
|
4047
|
+
node.focus();
|
|
4048
|
+
return;
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
3999
4051
|
};
|
|
4000
4052
|
const handleItemKeyDown = (e, index) => {
|
|
4001
4053
|
if (e.key === "ArrowDown") {
|
|
4002
4054
|
e.preventDefault();
|
|
4003
|
-
|
|
4055
|
+
focusFrom(index + 1, 1);
|
|
4004
4056
|
} else if (e.key === "ArrowUp") {
|
|
4005
4057
|
e.preventDefault();
|
|
4006
|
-
|
|
4058
|
+
focusFrom(index - 1, -1);
|
|
4007
4059
|
} else if (e.key === "Home") {
|
|
4008
4060
|
e.preventDefault();
|
|
4009
|
-
|
|
4061
|
+
focusFrom(0, 1);
|
|
4010
4062
|
} else if (e.key === "End") {
|
|
4011
4063
|
e.preventDefault();
|
|
4012
|
-
|
|
4064
|
+
focusFrom(items.length - 1, -1);
|
|
4065
|
+
}
|
|
4066
|
+
};
|
|
4067
|
+
const handleTriggerKeyDown = (e) => {
|
|
4068
|
+
if (open) return;
|
|
4069
|
+
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
|
|
4070
|
+
e.preventDefault();
|
|
4071
|
+
pendingFocusRef.current = "first";
|
|
4072
|
+
setOpen(true);
|
|
4073
|
+
} else if (e.key === "ArrowUp") {
|
|
4074
|
+
e.preventDefault();
|
|
4075
|
+
pendingFocusRef.current = "last";
|
|
4076
|
+
setOpen(true);
|
|
4013
4077
|
}
|
|
4014
4078
|
};
|
|
4015
4079
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -4018,6 +4082,7 @@ function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: contr
|
|
|
4018
4082
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4019
4083
|
ref: triggerRef,
|
|
4020
4084
|
onClick: () => setOpen(!open),
|
|
4085
|
+
onKeyDown: handleTriggerKeyDown,
|
|
4021
4086
|
className: "cursor-pointer",
|
|
4022
4087
|
children: (0, react.isValidElement)(trigger) ? (0, react.cloneElement)(trigger, {
|
|
4023
4088
|
"aria-expanded": open,
|
|
@@ -4037,7 +4102,7 @@ function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: contr
|
|
|
4037
4102
|
role: "menuitem",
|
|
4038
4103
|
disabled: item.disabled,
|
|
4039
4104
|
onClick: () => {
|
|
4040
|
-
setOpen(false);
|
|
4105
|
+
if (closeOnSelect) setOpen(false);
|
|
4041
4106
|
item.onClick();
|
|
4042
4107
|
},
|
|
4043
4108
|
onKeyDown: (e) => handleItemKeyDown(e, index),
|
|
@@ -4053,7 +4118,7 @@ function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: contr
|
|
|
4053
4118
|
"aria-hidden": "true",
|
|
4054
4119
|
children: item.icon
|
|
4055
4120
|
}), item.label]
|
|
4056
|
-
}, item.label))
|
|
4121
|
+
}, item.id ?? item.label ?? index))
|
|
4057
4122
|
}), document.body)]
|
|
4058
4123
|
});
|
|
4059
4124
|
}
|
package/lib/index.mjs
CHANGED
|
@@ -3900,9 +3900,15 @@ function Dropdown({ trigger, isOpen, onToggle, onClose, children, triggerClassNa
|
|
|
3900
3900
|
* Menu — Design System Component
|
|
3901
3901
|
*
|
|
3902
3902
|
* A dropdown menu anchored to a trigger. Opens on trigger click,
|
|
3903
|
-
* closes on outside-click or
|
|
3904
|
-
* ArrowDown/ArrowUp
|
|
3905
|
-
* Escape
|
|
3903
|
+
* closes on outside-click, Escape, or scroll. Full keyboard navigation:
|
|
3904
|
+
* ArrowDown/ArrowUp move between items (skipping disabled), Home/End jump to
|
|
3905
|
+
* the first/last enabled item, Escape closes. When focus is on a closed
|
|
3906
|
+
* trigger, Enter/Space/ArrowDown open it and focus the first item; ArrowUp
|
|
3907
|
+
* opens it and focuses the last item.
|
|
3908
|
+
*
|
|
3909
|
+
* The panel renders in a body portal with fixed positioning: it flips above
|
|
3910
|
+
* the trigger when there isn't room below, and clamps horizontally so it never
|
|
3911
|
+
* overflows the viewport.
|
|
3906
3912
|
*
|
|
3907
3913
|
* Follows WAI-ARIA Menu Button pattern:
|
|
3908
3914
|
* - trigger: role="button", aria-haspopup="menu", aria-expanded
|
|
@@ -3918,7 +3924,7 @@ function Dropdown({ trigger, isOpen, onToggle, onClose, children, triggerClassNa
|
|
|
3918
3924
|
* ]}
|
|
3919
3925
|
* />
|
|
3920
3926
|
*/
|
|
3921
|
-
function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: controlledOpen, onOpenChange, panelClassName = "" }) {
|
|
3927
|
+
function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: controlledOpen, onOpenChange, closeOnSelect = true, panelClassName = "" }) {
|
|
3922
3928
|
const [internalOpen, setInternalOpen] = useState(false);
|
|
3923
3929
|
const isControlled = controlledOpen !== void 0;
|
|
3924
3930
|
const open = isControlled ? controlledOpen : internalOpen;
|
|
@@ -3931,18 +3937,45 @@ function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: contr
|
|
|
3931
3937
|
const panelRef = useRef(null);
|
|
3932
3938
|
const itemRefs = useRef([]);
|
|
3933
3939
|
const [panelStyle, setPanelStyle] = useState({});
|
|
3940
|
+
const pendingFocusRef = useRef("first");
|
|
3934
3941
|
useEffect(() => {
|
|
3935
3942
|
if (!open || !triggerRef.current) return;
|
|
3936
|
-
const
|
|
3943
|
+
const GAP = 4;
|
|
3944
|
+
const MARGIN = 8;
|
|
3937
3945
|
const PANEL_MIN_WIDTH = 176;
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
+
const ROW_HEIGHT = 36;
|
|
3947
|
+
const HEADER_HEIGHT = 8;
|
|
3948
|
+
const reposition = () => {
|
|
3949
|
+
const trigger = triggerRef.current;
|
|
3950
|
+
if (!trigger) return;
|
|
3951
|
+
const rect = trigger.getBoundingClientRect();
|
|
3952
|
+
const panelHeight = panelRef.current?.offsetHeight ?? items.length * ROW_HEIGHT + HEADER_HEIGHT;
|
|
3953
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
3954
|
+
const flipUp = spaceBelow < panelHeight + GAP + MARGIN && rect.top > spaceBelow;
|
|
3955
|
+
const panelWidth = Math.max(panelRef.current?.offsetWidth ?? 0, rect.width, PANEL_MIN_WIDTH);
|
|
3956
|
+
const desiredLeft = align === "left" ? rect.left : rect.right - panelWidth;
|
|
3957
|
+
const maxLeft = window.innerWidth - panelWidth - MARGIN;
|
|
3958
|
+
const left = Math.min(Math.max(MARGIN, desiredLeft), Math.max(MARGIN, maxLeft));
|
|
3959
|
+
setPanelStyle({
|
|
3960
|
+
position: "fixed",
|
|
3961
|
+
...flipUp ? { bottom: Math.max(MARGIN, window.innerHeight - rect.top + GAP) } : { top: rect.bottom + GAP },
|
|
3962
|
+
left,
|
|
3963
|
+
zIndex: 9999,
|
|
3964
|
+
minWidth: Math.max(rect.width, PANEL_MIN_WIDTH)
|
|
3965
|
+
});
|
|
3966
|
+
};
|
|
3967
|
+
reposition();
|
|
3968
|
+
const raf = window.requestAnimationFrame(reposition);
|
|
3969
|
+
window.addEventListener("resize", reposition);
|
|
3970
|
+
return () => {
|
|
3971
|
+
window.cancelAnimationFrame(raf);
|
|
3972
|
+
window.removeEventListener("resize", reposition);
|
|
3973
|
+
};
|
|
3974
|
+
}, [
|
|
3975
|
+
open,
|
|
3976
|
+
align,
|
|
3977
|
+
items.length
|
|
3978
|
+
]);
|
|
3946
3979
|
useEffect(() => {
|
|
3947
3980
|
if (!open) return;
|
|
3948
3981
|
const handlePointerDown = (e) => {
|
|
@@ -3959,33 +3992,64 @@ function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: contr
|
|
|
3959
3992
|
(triggerRef.current?.querySelector("button,a,[tabindex]"))?.focus();
|
|
3960
3993
|
}
|
|
3961
3994
|
};
|
|
3995
|
+
const handleScroll = (e) => {
|
|
3996
|
+
if (panelRef.current && e.target instanceof Node && panelRef.current.contains(e.target)) return;
|
|
3997
|
+
setOpen(false);
|
|
3998
|
+
};
|
|
3962
3999
|
document.addEventListener("mousedown", handlePointerDown);
|
|
3963
4000
|
document.addEventListener("keydown", handleKeyDown);
|
|
4001
|
+
window.addEventListener("scroll", handleScroll, true);
|
|
3964
4002
|
window.setTimeout(() => {
|
|
3965
|
-
itemRefs.current.
|
|
4003
|
+
const enabled = itemRefs.current.map((node, i) => ({
|
|
4004
|
+
node,
|
|
4005
|
+
i
|
|
4006
|
+
})).filter((x) => x.node && !x.node.disabled);
|
|
4007
|
+
(pendingFocusRef.current === "last" ? enabled[enabled.length - 1] : enabled[0])?.node?.focus();
|
|
4008
|
+
pendingFocusRef.current = "first";
|
|
3966
4009
|
}, 0);
|
|
3967
4010
|
return () => {
|
|
3968
4011
|
document.removeEventListener("mousedown", handlePointerDown);
|
|
3969
4012
|
document.removeEventListener("keydown", handleKeyDown);
|
|
4013
|
+
window.removeEventListener("scroll", handleScroll, true);
|
|
3970
4014
|
};
|
|
3971
4015
|
}, [open]);
|
|
3972
|
-
const
|
|
3973
|
-
const
|
|
3974
|
-
|
|
4016
|
+
const focusFrom = (index, dir) => {
|
|
4017
|
+
const n = items.length;
|
|
4018
|
+
if (n === 0) return;
|
|
4019
|
+
for (let step = 0; step < n; step++) {
|
|
4020
|
+
const next = ((index + dir * step) % n + n) % n;
|
|
4021
|
+
const node = itemRefs.current[next];
|
|
4022
|
+
if (node && !node.disabled) {
|
|
4023
|
+
node.focus();
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
3975
4027
|
};
|
|
3976
4028
|
const handleItemKeyDown = (e, index) => {
|
|
3977
4029
|
if (e.key === "ArrowDown") {
|
|
3978
4030
|
e.preventDefault();
|
|
3979
|
-
|
|
4031
|
+
focusFrom(index + 1, 1);
|
|
3980
4032
|
} else if (e.key === "ArrowUp") {
|
|
3981
4033
|
e.preventDefault();
|
|
3982
|
-
|
|
4034
|
+
focusFrom(index - 1, -1);
|
|
3983
4035
|
} else if (e.key === "Home") {
|
|
3984
4036
|
e.preventDefault();
|
|
3985
|
-
|
|
4037
|
+
focusFrom(0, 1);
|
|
3986
4038
|
} else if (e.key === "End") {
|
|
3987
4039
|
e.preventDefault();
|
|
3988
|
-
|
|
4040
|
+
focusFrom(items.length - 1, -1);
|
|
4041
|
+
}
|
|
4042
|
+
};
|
|
4043
|
+
const handleTriggerKeyDown = (e) => {
|
|
4044
|
+
if (open) return;
|
|
4045
|
+
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
|
|
4046
|
+
e.preventDefault();
|
|
4047
|
+
pendingFocusRef.current = "first";
|
|
4048
|
+
setOpen(true);
|
|
4049
|
+
} else if (e.key === "ArrowUp") {
|
|
4050
|
+
e.preventDefault();
|
|
4051
|
+
pendingFocusRef.current = "last";
|
|
4052
|
+
setOpen(true);
|
|
3989
4053
|
}
|
|
3990
4054
|
};
|
|
3991
4055
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -3994,6 +4058,7 @@ function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: contr
|
|
|
3994
4058
|
children: [/* @__PURE__ */ jsx("div", {
|
|
3995
4059
|
ref: triggerRef,
|
|
3996
4060
|
onClick: () => setOpen(!open),
|
|
4061
|
+
onKeyDown: handleTriggerKeyDown,
|
|
3997
4062
|
className: "cursor-pointer",
|
|
3998
4063
|
children: isValidElement(trigger) ? cloneElement(trigger, {
|
|
3999
4064
|
"aria-expanded": open,
|
|
@@ -4013,7 +4078,7 @@ function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: contr
|
|
|
4013
4078
|
role: "menuitem",
|
|
4014
4079
|
disabled: item.disabled,
|
|
4015
4080
|
onClick: () => {
|
|
4016
|
-
setOpen(false);
|
|
4081
|
+
if (closeOnSelect) setOpen(false);
|
|
4017
4082
|
item.onClick();
|
|
4018
4083
|
},
|
|
4019
4084
|
onKeyDown: (e) => handleItemKeyDown(e, index),
|
|
@@ -4029,7 +4094,7 @@ function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: contr
|
|
|
4029
4094
|
"aria-hidden": "true",
|
|
4030
4095
|
children: item.icon
|
|
4031
4096
|
}), item.label]
|
|
4032
|
-
}, item.label))
|
|
4097
|
+
}, item.id ?? item.label ?? index))
|
|
4033
4098
|
}), document.body)]
|
|
4034
4099
|
});
|
|
4035
4100
|
}
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
* Menu — Design System Component
|
|
3
3
|
*
|
|
4
4
|
* A dropdown menu anchored to a trigger. Opens on trigger click,
|
|
5
|
-
* closes on outside-click or
|
|
6
|
-
* ArrowDown/ArrowUp
|
|
7
|
-
* Escape
|
|
5
|
+
* closes on outside-click, Escape, or scroll. Full keyboard navigation:
|
|
6
|
+
* ArrowDown/ArrowUp move between items (skipping disabled), Home/End jump to
|
|
7
|
+
* the first/last enabled item, Escape closes. When focus is on a closed
|
|
8
|
+
* trigger, Enter/Space/ArrowDown open it and focus the first item; ArrowUp
|
|
9
|
+
* opens it and focuses the last item.
|
|
10
|
+
*
|
|
11
|
+
* The panel renders in a body portal with fixed positioning: it flips above
|
|
12
|
+
* the trigger when there isn't room below, and clamps horizontally so it never
|
|
13
|
+
* overflows the viewport.
|
|
8
14
|
*
|
|
9
15
|
* Follows WAI-ARIA Menu Button pattern:
|
|
10
16
|
* - trigger: role="button", aria-haspopup="menu", aria-expanded
|
|
@@ -23,6 +29,12 @@
|
|
|
23
29
|
import { type ReactNode } from 'react';
|
|
24
30
|
export type MenuItemTone = 'default' | 'danger';
|
|
25
31
|
export interface MenuItem {
|
|
32
|
+
/**
|
|
33
|
+
* Stable identity for the item. Used for the React key and focus tracking.
|
|
34
|
+
* Recommended when labels can repeat or the item list is dynamic; falls back
|
|
35
|
+
* to `label` then index when omitted.
|
|
36
|
+
*/
|
|
37
|
+
id?: string;
|
|
26
38
|
label: string;
|
|
27
39
|
icon?: ReactNode;
|
|
28
40
|
onClick: () => void;
|
|
@@ -46,7 +58,12 @@ export interface MenuProps {
|
|
|
46
58
|
open?: boolean;
|
|
47
59
|
/** Called when the menu should open or close. */
|
|
48
60
|
onOpenChange?: (open: boolean) => void;
|
|
61
|
+
/**
|
|
62
|
+
* Close the menu after an item is selected. Default `true`.
|
|
63
|
+
* Set `false` for menus that stay open across clicks (e.g. multi-toggle filters).
|
|
64
|
+
*/
|
|
65
|
+
closeOnSelect?: boolean;
|
|
49
66
|
/** Additional class on the floating panel. */
|
|
50
67
|
panelClassName?: string;
|
|
51
68
|
}
|
|
52
|
-
export declare function Menu({ trigger, items, ariaLabel, align, open: controlledOpen, onOpenChange, panelClassName, }: MenuProps): import("react").JSX.Element;
|
|
69
|
+
export declare function Menu({ trigger, items, ariaLabel, align, open: controlledOpen, onOpenChange, closeOnSelect, panelClassName, }: MenuProps): import("react").JSX.Element;
|