@seed-design/react-menu 0.0.0-alpha-20260414104312
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/Menu-12s-DcQqm37B.js +189 -0
- package/lib/Menu-12s-KCTCu-et.cjs +202 -0
- package/lib/index.cjs +25 -0
- package/lib/index.d.ts +2711 -0
- package/lib/index.js +16 -0
- package/lib/useMenu-12s-DN0SpRKs.cjs +269 -0
- package/lib/useMenu-12s-WINwYlwH.js +269 -0
- package/package.json +53 -0
- package/src/Menu.namespace.ts +18 -0
- package/src/Menu.tsx +221 -0
- package/src/index.ts +31 -0
- package/src/useMenu.ts +411 -0
- package/src/useMenu.vitest.tsx +895 -0
- package/src/useMenuContext.tsx +21 -0
- package/src/useMenuItemContext.tsx +21 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { M as MenuAnchor, a as MenuContent, b as MenuGroup, c as MenuGroupLabel, d as MenuItem, e as MenuPositioner, f as MenuRoot, g as MenuTrigger } from './Menu-12s-DcQqm37B.js';
|
|
2
|
+
export { u as useMenuContext, h as useMenuItemContext } from './Menu-12s-DcQqm37B.js';
|
|
3
|
+
|
|
4
|
+
var Menu_namespace = {
|
|
5
|
+
__proto__: null,
|
|
6
|
+
Anchor: MenuAnchor,
|
|
7
|
+
Content: MenuContent,
|
|
8
|
+
Group: MenuGroup,
|
|
9
|
+
GroupLabel: MenuGroupLabel,
|
|
10
|
+
Item: MenuItem,
|
|
11
|
+
Positioner: MenuPositioner,
|
|
12
|
+
Root: MenuRoot,
|
|
13
|
+
Trigger: MenuTrigger
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export { Menu_namespace as Menu, MenuAnchor, MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuPositioner, MenuRoot, MenuTrigger };
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
var react = require('@floating-ui/react');
|
|
3
|
+
var reactUseControllableState = require('@seed-design/react-use-controllable-state');
|
|
4
|
+
var domUtils = require('@seed-design/dom-utils');
|
|
5
|
+
var React = require('react');
|
|
6
|
+
|
|
7
|
+
const MIN_HEIGHT = 200;
|
|
8
|
+
function getTransformOrigin(placement) {
|
|
9
|
+
const [side, align] = placement.split("-");
|
|
10
|
+
const y = {
|
|
11
|
+
top: "bottom",
|
|
12
|
+
bottom: "top",
|
|
13
|
+
left: "center",
|
|
14
|
+
right: "center"
|
|
15
|
+
}[side] ?? "top";
|
|
16
|
+
const x = {
|
|
17
|
+
start: "left",
|
|
18
|
+
end: "right"
|
|
19
|
+
}[align] ?? "center";
|
|
20
|
+
return `${x} ${y}`;
|
|
21
|
+
}
|
|
22
|
+
function useMenuState(props) {
|
|
23
|
+
const [open = false, setOpenState] = reactUseControllableState.useControllableState({
|
|
24
|
+
prop: props.open,
|
|
25
|
+
defaultProp: props.defaultOpen ?? false,
|
|
26
|
+
onChange: props.onOpenChange
|
|
27
|
+
});
|
|
28
|
+
const [activeIndex, setActiveIndex] = React.useState(null);
|
|
29
|
+
const elementsRef = React.useRef([]);
|
|
30
|
+
const labelsRef = React.useRef([]);
|
|
31
|
+
const triggerRef = React.useRef(null);
|
|
32
|
+
const groupIndexCounter = React.useRef(0);
|
|
33
|
+
groupIndexCounter.current = 0;
|
|
34
|
+
const menuId = React.useId();
|
|
35
|
+
return {
|
|
36
|
+
open,
|
|
37
|
+
setOpenState,
|
|
38
|
+
activeIndex,
|
|
39
|
+
setActiveIndex,
|
|
40
|
+
elementsRef,
|
|
41
|
+
labelsRef,
|
|
42
|
+
triggerRef,
|
|
43
|
+
groupIndexCounter,
|
|
44
|
+
menuId
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function useMenu(props) {
|
|
48
|
+
const { open, setOpenState, activeIndex, setActiveIndex, elementsRef, labelsRef, triggerRef, groupIndexCounter, menuId } = useMenuState(props);
|
|
49
|
+
const { disabled = false, placement = "bottom", gutter = 8, overflowPadding = 8, strategy = "absolute", matchReferenceWidth = false } = props;
|
|
50
|
+
const setOpen = React.useCallback((nextOpen, details)=>{
|
|
51
|
+
if (disabled && nextOpen) return;
|
|
52
|
+
setOpenState(nextOpen, details);
|
|
53
|
+
}, [
|
|
54
|
+
disabled,
|
|
55
|
+
setOpenState
|
|
56
|
+
]);
|
|
57
|
+
const handleFloatingOpenChange = React.useCallback((nextOpen, event, reason)=>{
|
|
58
|
+
if (reason === "click" && event) {
|
|
59
|
+
// NOTE: floating-ui passes click/mousedown/keydown on "click" reason
|
|
60
|
+
setOpen(nextOpen, {
|
|
61
|
+
reason: "trigger",
|
|
62
|
+
event: event
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
setOpen(nextOpen);
|
|
67
|
+
}, [
|
|
68
|
+
setOpen
|
|
69
|
+
]);
|
|
70
|
+
const { refs: floatingRefs, context, floatingStyles } = react.useFloating({
|
|
71
|
+
open,
|
|
72
|
+
onOpenChange: handleFloatingOpenChange,
|
|
73
|
+
strategy,
|
|
74
|
+
placement,
|
|
75
|
+
middleware: [
|
|
76
|
+
react.offset(gutter),
|
|
77
|
+
react.size({
|
|
78
|
+
padding: overflowPadding,
|
|
79
|
+
apply ({ availableHeight, rects, elements }) {
|
|
80
|
+
elements.floating.style.setProperty("--seed-menu-available-height", `${Math.max(MIN_HEIGHT, availableHeight)}px`);
|
|
81
|
+
if (matchReferenceWidth) {
|
|
82
|
+
elements.floating.style.setProperty("--seed-menu-reference-width", `${rects.reference.width}px`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}),
|
|
86
|
+
react.flip({
|
|
87
|
+
padding: overflowPadding,
|
|
88
|
+
fallbackStrategy: "initialPlacement"
|
|
89
|
+
}),
|
|
90
|
+
react.shift({
|
|
91
|
+
padding: overflowPadding
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
const { status } = react.useTransitionStatus(context);
|
|
96
|
+
// Keep autoUpdate alive during exit animation so the positioner
|
|
97
|
+
// follows the trigger even while the menu is animating out.
|
|
98
|
+
const mounted = status !== "unmounted";
|
|
99
|
+
React.useEffect(()=>{
|
|
100
|
+
if (!mounted) return;
|
|
101
|
+
if (!floatingRefs.reference.current || !floatingRefs.floating.current) return;
|
|
102
|
+
return react.autoUpdate(floatingRefs.reference.current, floatingRefs.floating.current, context.update);
|
|
103
|
+
}, [
|
|
104
|
+
mounted,
|
|
105
|
+
floatingRefs.reference,
|
|
106
|
+
floatingRefs.floating,
|
|
107
|
+
context
|
|
108
|
+
]);
|
|
109
|
+
const click = react.useClick(context, {
|
|
110
|
+
enabled: !disabled
|
|
111
|
+
});
|
|
112
|
+
const role = react.useRole(context, {
|
|
113
|
+
role: "menu"
|
|
114
|
+
});
|
|
115
|
+
const listNavigation = react.useListNavigation(context, {
|
|
116
|
+
listRef: elementsRef,
|
|
117
|
+
activeIndex,
|
|
118
|
+
onNavigate: setActiveIndex,
|
|
119
|
+
loop: true,
|
|
120
|
+
// Hover and focus states should be differentiated; disable focus-on-hover
|
|
121
|
+
// so that hover styling can be handled separately via CSS :hover.
|
|
122
|
+
focusItemOnHover: false
|
|
123
|
+
});
|
|
124
|
+
const typeahead = react.useTypeahead(context, {
|
|
125
|
+
listRef: labelsRef,
|
|
126
|
+
activeIndex,
|
|
127
|
+
onMatch: setActiveIndex
|
|
128
|
+
});
|
|
129
|
+
const triggerInteractions = react.useInteractions([
|
|
130
|
+
click,
|
|
131
|
+
role,
|
|
132
|
+
listNavigation,
|
|
133
|
+
typeahead
|
|
134
|
+
]);
|
|
135
|
+
const anchorInteractions = react.useInteractions([
|
|
136
|
+
role,
|
|
137
|
+
listNavigation,
|
|
138
|
+
typeahead
|
|
139
|
+
]);
|
|
140
|
+
const stateProps = React.useMemo(()=>domUtils.elementProps({
|
|
141
|
+
"data-hidden": domUtils.dataAttr(status === "unmounted"),
|
|
142
|
+
"data-open": domUtils.dataAttr(status === "open" || status === "initial")
|
|
143
|
+
}), [
|
|
144
|
+
status
|
|
145
|
+
]);
|
|
146
|
+
return {
|
|
147
|
+
open,
|
|
148
|
+
setOpen,
|
|
149
|
+
activeIndex,
|
|
150
|
+
stateProps,
|
|
151
|
+
floatingContext: context,
|
|
152
|
+
elementsRef,
|
|
153
|
+
labelsRef,
|
|
154
|
+
refs: {
|
|
155
|
+
anchor: (node)=>{
|
|
156
|
+
floatingRefs.setReference(node);
|
|
157
|
+
},
|
|
158
|
+
trigger: (node)=>{
|
|
159
|
+
floatingRefs.setReference(node);
|
|
160
|
+
triggerRef.current = node;
|
|
161
|
+
},
|
|
162
|
+
positioner: floatingRefs.setFloating
|
|
163
|
+
},
|
|
164
|
+
anchorProps: domUtils.elementProps({
|
|
165
|
+
"data-open": domUtils.dataAttr(open),
|
|
166
|
+
...anchorInteractions.getReferenceProps()
|
|
167
|
+
}),
|
|
168
|
+
triggerProps: domUtils.buttonProps({
|
|
169
|
+
"aria-haspopup": "menu",
|
|
170
|
+
"aria-expanded": open,
|
|
171
|
+
"data-open": domUtils.dataAttr(open),
|
|
172
|
+
...triggerInteractions.getReferenceProps()
|
|
173
|
+
}),
|
|
174
|
+
positionerProps: domUtils.elementProps({
|
|
175
|
+
...stateProps,
|
|
176
|
+
style: floatingStyles
|
|
177
|
+
}),
|
|
178
|
+
contentProps: domUtils.elementProps({
|
|
179
|
+
...stateProps,
|
|
180
|
+
role: "menu",
|
|
181
|
+
style: {
|
|
182
|
+
"--seed-menu-transform-origin": getTransformOrigin(context.placement)
|
|
183
|
+
},
|
|
184
|
+
...triggerInteractions.getFloatingProps()
|
|
185
|
+
}),
|
|
186
|
+
getItemProps: (itemProps, index)=>{
|
|
187
|
+
const isActive = activeIndex === index;
|
|
188
|
+
const itemStateProps = domUtils.elementProps({
|
|
189
|
+
// implement when submenu is needed
|
|
190
|
+
// "data-highlighted": dataAttr(isActive),
|
|
191
|
+
"data-disabled": domUtils.dataAttr(itemProps.disabled)
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
// implement when submenu is needed
|
|
195
|
+
// isHighlighted: isActive,
|
|
196
|
+
isDisabled: itemProps.disabled,
|
|
197
|
+
stateProps: itemStateProps,
|
|
198
|
+
rootProps: domUtils.elementProps({
|
|
199
|
+
...itemStateProps,
|
|
200
|
+
...triggerInteractions.getItemProps({
|
|
201
|
+
onClick (event) {
|
|
202
|
+
if (itemProps.disabled) return;
|
|
203
|
+
if (event.defaultPrevented) return;
|
|
204
|
+
itemProps.onClick?.(event);
|
|
205
|
+
setOpen(false, {
|
|
206
|
+
reason: "itemClick",
|
|
207
|
+
event: event.nativeEvent
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
onKeyDown (event) {
|
|
211
|
+
if (itemProps.disabled) return;
|
|
212
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
if (!(event.currentTarget instanceof HTMLElement)) return;
|
|
215
|
+
event.currentTarget.click();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}),
|
|
219
|
+
role: "menuitem",
|
|
220
|
+
tabIndex: isActive ? 0 : -1,
|
|
221
|
+
"aria-disabled": itemProps.disabled ? "true" : undefined
|
|
222
|
+
})
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
// implement when submenu is needed
|
|
226
|
+
// getSubmenuTriggerProps: (itemProps: UseMenuSubmenuTriggerProps, index: number) => {
|
|
227
|
+
// const isActive = activeIndex === index;
|
|
228
|
+
//
|
|
229
|
+
// const itemStateProps = elementProps({
|
|
230
|
+
// "data-highlighted": dataAttr(isActive),
|
|
231
|
+
// "data-disabled": dataAttr(itemProps.disabled),
|
|
232
|
+
// });
|
|
233
|
+
//
|
|
234
|
+
// return {
|
|
235
|
+
// isHighlighted: isActive,
|
|
236
|
+
// isDisabled: itemProps.disabled,
|
|
237
|
+
//
|
|
238
|
+
// stateProps: itemStateProps,
|
|
239
|
+
//
|
|
240
|
+
// rootProps: elementProps({
|
|
241
|
+
// ...itemStateProps,
|
|
242
|
+
// ...triggerInteractions.getItemProps(),
|
|
243
|
+
// role: "menuitem",
|
|
244
|
+
// tabIndex: isActive ? 0 : -1,
|
|
245
|
+
// "aria-disabled": itemProps.disabled ? "true" : undefined,
|
|
246
|
+
// }),
|
|
247
|
+
// };
|
|
248
|
+
// },
|
|
249
|
+
getGroupProps: ()=>{
|
|
250
|
+
const groupIndex = groupIndexCounter.current++;
|
|
251
|
+
const labelId = `menu:${menuId}:group-${groupIndex}:label`;
|
|
252
|
+
return {
|
|
253
|
+
labelId,
|
|
254
|
+
rootProps: domUtils.elementProps({
|
|
255
|
+
role: "group",
|
|
256
|
+
"aria-labelledby": labelId
|
|
257
|
+
})
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
getGroupLabelProps: (labelId)=>{
|
|
261
|
+
return domUtils.elementProps({
|
|
262
|
+
role: "presentation",
|
|
263
|
+
id: labelId
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
exports.useMenu = useMenu;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useFloating, offset, size, flip, shift, useTransitionStatus, autoUpdate, useClick, useRole, useListNavigation, useTypeahead, useInteractions } from '@floating-ui/react';
|
|
3
|
+
import { useControllableState } from '@seed-design/react-use-controllable-state';
|
|
4
|
+
import { elementProps, dataAttr, buttonProps } from '@seed-design/dom-utils';
|
|
5
|
+
import { useCallback, useEffect, useMemo, useState, useRef, useId } from 'react';
|
|
6
|
+
|
|
7
|
+
const MIN_HEIGHT = 200;
|
|
8
|
+
function getTransformOrigin(placement) {
|
|
9
|
+
const [side, align] = placement.split("-");
|
|
10
|
+
const y = {
|
|
11
|
+
top: "bottom",
|
|
12
|
+
bottom: "top",
|
|
13
|
+
left: "center",
|
|
14
|
+
right: "center"
|
|
15
|
+
}[side] ?? "top";
|
|
16
|
+
const x = {
|
|
17
|
+
start: "left",
|
|
18
|
+
end: "right"
|
|
19
|
+
}[align] ?? "center";
|
|
20
|
+
return `${x} ${y}`;
|
|
21
|
+
}
|
|
22
|
+
function useMenuState(props) {
|
|
23
|
+
const [open = false, setOpenState] = useControllableState({
|
|
24
|
+
prop: props.open,
|
|
25
|
+
defaultProp: props.defaultOpen ?? false,
|
|
26
|
+
onChange: props.onOpenChange
|
|
27
|
+
});
|
|
28
|
+
const [activeIndex, setActiveIndex] = useState(null);
|
|
29
|
+
const elementsRef = useRef([]);
|
|
30
|
+
const labelsRef = useRef([]);
|
|
31
|
+
const triggerRef = useRef(null);
|
|
32
|
+
const groupIndexCounter = useRef(0);
|
|
33
|
+
groupIndexCounter.current = 0;
|
|
34
|
+
const menuId = useId();
|
|
35
|
+
return {
|
|
36
|
+
open,
|
|
37
|
+
setOpenState,
|
|
38
|
+
activeIndex,
|
|
39
|
+
setActiveIndex,
|
|
40
|
+
elementsRef,
|
|
41
|
+
labelsRef,
|
|
42
|
+
triggerRef,
|
|
43
|
+
groupIndexCounter,
|
|
44
|
+
menuId
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function useMenu(props) {
|
|
48
|
+
const { open, setOpenState, activeIndex, setActiveIndex, elementsRef, labelsRef, triggerRef, groupIndexCounter, menuId } = useMenuState(props);
|
|
49
|
+
const { disabled = false, placement = "bottom", gutter = 8, overflowPadding = 8, strategy = "absolute", matchReferenceWidth = false } = props;
|
|
50
|
+
const setOpen = useCallback((nextOpen, details)=>{
|
|
51
|
+
if (disabled && nextOpen) return;
|
|
52
|
+
setOpenState(nextOpen, details);
|
|
53
|
+
}, [
|
|
54
|
+
disabled,
|
|
55
|
+
setOpenState
|
|
56
|
+
]);
|
|
57
|
+
const handleFloatingOpenChange = useCallback((nextOpen, event, reason)=>{
|
|
58
|
+
if (reason === "click" && event) {
|
|
59
|
+
// NOTE: floating-ui passes click/mousedown/keydown on "click" reason
|
|
60
|
+
setOpen(nextOpen, {
|
|
61
|
+
reason: "trigger",
|
|
62
|
+
event: event
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
setOpen(nextOpen);
|
|
67
|
+
}, [
|
|
68
|
+
setOpen
|
|
69
|
+
]);
|
|
70
|
+
const { refs: floatingRefs, context, floatingStyles } = useFloating({
|
|
71
|
+
open,
|
|
72
|
+
onOpenChange: handleFloatingOpenChange,
|
|
73
|
+
strategy,
|
|
74
|
+
placement,
|
|
75
|
+
middleware: [
|
|
76
|
+
offset(gutter),
|
|
77
|
+
size({
|
|
78
|
+
padding: overflowPadding,
|
|
79
|
+
apply ({ availableHeight, rects, elements }) {
|
|
80
|
+
elements.floating.style.setProperty("--seed-menu-available-height", `${Math.max(MIN_HEIGHT, availableHeight)}px`);
|
|
81
|
+
if (matchReferenceWidth) {
|
|
82
|
+
elements.floating.style.setProperty("--seed-menu-reference-width", `${rects.reference.width}px`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}),
|
|
86
|
+
flip({
|
|
87
|
+
padding: overflowPadding,
|
|
88
|
+
fallbackStrategy: "initialPlacement"
|
|
89
|
+
}),
|
|
90
|
+
shift({
|
|
91
|
+
padding: overflowPadding
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
const { status } = useTransitionStatus(context);
|
|
96
|
+
// Keep autoUpdate alive during exit animation so the positioner
|
|
97
|
+
// follows the trigger even while the menu is animating out.
|
|
98
|
+
const mounted = status !== "unmounted";
|
|
99
|
+
useEffect(()=>{
|
|
100
|
+
if (!mounted) return;
|
|
101
|
+
if (!floatingRefs.reference.current || !floatingRefs.floating.current) return;
|
|
102
|
+
return autoUpdate(floatingRefs.reference.current, floatingRefs.floating.current, context.update);
|
|
103
|
+
}, [
|
|
104
|
+
mounted,
|
|
105
|
+
floatingRefs.reference,
|
|
106
|
+
floatingRefs.floating,
|
|
107
|
+
context
|
|
108
|
+
]);
|
|
109
|
+
const click = useClick(context, {
|
|
110
|
+
enabled: !disabled
|
|
111
|
+
});
|
|
112
|
+
const role = useRole(context, {
|
|
113
|
+
role: "menu"
|
|
114
|
+
});
|
|
115
|
+
const listNavigation = useListNavigation(context, {
|
|
116
|
+
listRef: elementsRef,
|
|
117
|
+
activeIndex,
|
|
118
|
+
onNavigate: setActiveIndex,
|
|
119
|
+
loop: true,
|
|
120
|
+
// Hover and focus states should be differentiated; disable focus-on-hover
|
|
121
|
+
// so that hover styling can be handled separately via CSS :hover.
|
|
122
|
+
focusItemOnHover: false
|
|
123
|
+
});
|
|
124
|
+
const typeahead = useTypeahead(context, {
|
|
125
|
+
listRef: labelsRef,
|
|
126
|
+
activeIndex,
|
|
127
|
+
onMatch: setActiveIndex
|
|
128
|
+
});
|
|
129
|
+
const triggerInteractions = useInteractions([
|
|
130
|
+
click,
|
|
131
|
+
role,
|
|
132
|
+
listNavigation,
|
|
133
|
+
typeahead
|
|
134
|
+
]);
|
|
135
|
+
const anchorInteractions = useInteractions([
|
|
136
|
+
role,
|
|
137
|
+
listNavigation,
|
|
138
|
+
typeahead
|
|
139
|
+
]);
|
|
140
|
+
const stateProps = useMemo(()=>elementProps({
|
|
141
|
+
"data-hidden": dataAttr(status === "unmounted"),
|
|
142
|
+
"data-open": dataAttr(status === "open" || status === "initial")
|
|
143
|
+
}), [
|
|
144
|
+
status
|
|
145
|
+
]);
|
|
146
|
+
return {
|
|
147
|
+
open,
|
|
148
|
+
setOpen,
|
|
149
|
+
activeIndex,
|
|
150
|
+
stateProps,
|
|
151
|
+
floatingContext: context,
|
|
152
|
+
elementsRef,
|
|
153
|
+
labelsRef,
|
|
154
|
+
refs: {
|
|
155
|
+
anchor: (node)=>{
|
|
156
|
+
floatingRefs.setReference(node);
|
|
157
|
+
},
|
|
158
|
+
trigger: (node)=>{
|
|
159
|
+
floatingRefs.setReference(node);
|
|
160
|
+
triggerRef.current = node;
|
|
161
|
+
},
|
|
162
|
+
positioner: floatingRefs.setFloating
|
|
163
|
+
},
|
|
164
|
+
anchorProps: elementProps({
|
|
165
|
+
"data-open": dataAttr(open),
|
|
166
|
+
...anchorInteractions.getReferenceProps()
|
|
167
|
+
}),
|
|
168
|
+
triggerProps: buttonProps({
|
|
169
|
+
"aria-haspopup": "menu",
|
|
170
|
+
"aria-expanded": open,
|
|
171
|
+
"data-open": dataAttr(open),
|
|
172
|
+
...triggerInteractions.getReferenceProps()
|
|
173
|
+
}),
|
|
174
|
+
positionerProps: elementProps({
|
|
175
|
+
...stateProps,
|
|
176
|
+
style: floatingStyles
|
|
177
|
+
}),
|
|
178
|
+
contentProps: elementProps({
|
|
179
|
+
...stateProps,
|
|
180
|
+
role: "menu",
|
|
181
|
+
style: {
|
|
182
|
+
"--seed-menu-transform-origin": getTransformOrigin(context.placement)
|
|
183
|
+
},
|
|
184
|
+
...triggerInteractions.getFloatingProps()
|
|
185
|
+
}),
|
|
186
|
+
getItemProps: (itemProps, index)=>{
|
|
187
|
+
const isActive = activeIndex === index;
|
|
188
|
+
const itemStateProps = elementProps({
|
|
189
|
+
// implement when submenu is needed
|
|
190
|
+
// "data-highlighted": dataAttr(isActive),
|
|
191
|
+
"data-disabled": dataAttr(itemProps.disabled)
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
// implement when submenu is needed
|
|
195
|
+
// isHighlighted: isActive,
|
|
196
|
+
isDisabled: itemProps.disabled,
|
|
197
|
+
stateProps: itemStateProps,
|
|
198
|
+
rootProps: elementProps({
|
|
199
|
+
...itemStateProps,
|
|
200
|
+
...triggerInteractions.getItemProps({
|
|
201
|
+
onClick (event) {
|
|
202
|
+
if (itemProps.disabled) return;
|
|
203
|
+
if (event.defaultPrevented) return;
|
|
204
|
+
itemProps.onClick?.(event);
|
|
205
|
+
setOpen(false, {
|
|
206
|
+
reason: "itemClick",
|
|
207
|
+
event: event.nativeEvent
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
onKeyDown (event) {
|
|
211
|
+
if (itemProps.disabled) return;
|
|
212
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
if (!(event.currentTarget instanceof HTMLElement)) return;
|
|
215
|
+
event.currentTarget.click();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}),
|
|
219
|
+
role: "menuitem",
|
|
220
|
+
tabIndex: isActive ? 0 : -1,
|
|
221
|
+
"aria-disabled": itemProps.disabled ? "true" : undefined
|
|
222
|
+
})
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
// implement when submenu is needed
|
|
226
|
+
// getSubmenuTriggerProps: (itemProps: UseMenuSubmenuTriggerProps, index: number) => {
|
|
227
|
+
// const isActive = activeIndex === index;
|
|
228
|
+
//
|
|
229
|
+
// const itemStateProps = elementProps({
|
|
230
|
+
// "data-highlighted": dataAttr(isActive),
|
|
231
|
+
// "data-disabled": dataAttr(itemProps.disabled),
|
|
232
|
+
// });
|
|
233
|
+
//
|
|
234
|
+
// return {
|
|
235
|
+
// isHighlighted: isActive,
|
|
236
|
+
// isDisabled: itemProps.disabled,
|
|
237
|
+
//
|
|
238
|
+
// stateProps: itemStateProps,
|
|
239
|
+
//
|
|
240
|
+
// rootProps: elementProps({
|
|
241
|
+
// ...itemStateProps,
|
|
242
|
+
// ...triggerInteractions.getItemProps(),
|
|
243
|
+
// role: "menuitem",
|
|
244
|
+
// tabIndex: isActive ? 0 : -1,
|
|
245
|
+
// "aria-disabled": itemProps.disabled ? "true" : undefined,
|
|
246
|
+
// }),
|
|
247
|
+
// };
|
|
248
|
+
// },
|
|
249
|
+
getGroupProps: ()=>{
|
|
250
|
+
const groupIndex = groupIndexCounter.current++;
|
|
251
|
+
const labelId = `menu:${menuId}:group-${groupIndex}:label`;
|
|
252
|
+
return {
|
|
253
|
+
labelId,
|
|
254
|
+
rootProps: elementProps({
|
|
255
|
+
role: "group",
|
|
256
|
+
"aria-labelledby": labelId
|
|
257
|
+
})
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
getGroupLabelProps: (labelId)=>{
|
|
261
|
+
return elementProps({
|
|
262
|
+
role: "presentation",
|
|
263
|
+
id: labelId
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export { useMenu as u };
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@seed-design/react-menu",
|
|
3
|
+
"version": "0.0.0-alpha-20260414104312",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/daangn/seed-design.git",
|
|
7
|
+
"directory": "packages/react-headless/menu"
|
|
8
|
+
},
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
14
|
+
"import": "./lib/index.js",
|
|
15
|
+
"require": "./lib/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"main": "./lib/index.cjs",
|
|
20
|
+
"files": [
|
|
21
|
+
"lib",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "rm -rf lib",
|
|
26
|
+
"build": "bunchee",
|
|
27
|
+
"lint:publish": "bun publint",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@floating-ui/react": "^0.27.0",
|
|
32
|
+
"@radix-ui/react-compose-refs": "^1.1.2",
|
|
33
|
+
"@radix-ui/react-focus-scope": "^1.1.8",
|
|
34
|
+
"@seed-design/dom-utils": "1.0.0",
|
|
35
|
+
"@seed-design/react-dismissible-layer": "0.0.0-alpha-20260414104312",
|
|
36
|
+
"@seed-design/react-primitive": "0.0.0-alpha-20260414104312",
|
|
37
|
+
"@seed-design/react-use-controllable-state": "1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/react": "^19.1.6",
|
|
41
|
+
"jsdom": "^29.0.2",
|
|
42
|
+
"react": "^19.1.0",
|
|
43
|
+
"react-dom": "^19.1.0",
|
|
44
|
+
"vitest": "^4.1.3"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": ">=18.0.0",
|
|
48
|
+
"react-dom": ">=18.0.0"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
MenuRoot as Root,
|
|
3
|
+
MenuAnchor as Anchor,
|
|
4
|
+
MenuTrigger as Trigger,
|
|
5
|
+
MenuPositioner as Positioner,
|
|
6
|
+
MenuContent as Content,
|
|
7
|
+
MenuItem as Item,
|
|
8
|
+
MenuGroup as Group,
|
|
9
|
+
MenuGroupLabel as GroupLabel,
|
|
10
|
+
type MenuRootProps as RootProps,
|
|
11
|
+
type MenuAnchorProps as AnchorProps,
|
|
12
|
+
type MenuTriggerProps as TriggerProps,
|
|
13
|
+
type MenuPositionerProps as PositionerProps,
|
|
14
|
+
type MenuContentProps as ContentProps,
|
|
15
|
+
type MenuItemProps as ItemProps,
|
|
16
|
+
type MenuGroupProps as GroupProps,
|
|
17
|
+
type MenuGroupLabelProps as GroupLabelProps,
|
|
18
|
+
} from "./Menu";
|