@versini/ui-menu 5.3.3 → 6.0.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/README.md +91 -58
- package/dist/index.d.ts +86 -17
- package/dist/index.js +821 -348
- package/package.json +7 -8
package/dist/index.js
CHANGED
|
@@ -1,49 +1,179 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-menu
|
|
3
|
-
©
|
|
2
|
+
@versini/ui-menu v6.0.0
|
|
3
|
+
© 2026 gizmette.com
|
|
4
4
|
*/
|
|
5
|
-
try {
|
|
6
|
-
if (!window.__VERSINI_UI_MENU__) {
|
|
7
|
-
window.__VERSINI_UI_MENU__ = {
|
|
8
|
-
version: "5.3.3",
|
|
9
|
-
buildTime: "12/10/2025 08:22 AM EST",
|
|
10
|
-
homepage: "https://www.npmjs.com/package/@versini/ui-menu",
|
|
11
|
-
license: "MIT",
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
} catch (error) {
|
|
15
|
-
// nothing to declare officer
|
|
16
|
-
}
|
|
17
5
|
|
|
18
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { useUniqueId } from "@versini/ui-hooks/use-unique-id";
|
|
8
|
+
import { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
21
9
|
import clsx from "clsx";
|
|
22
|
-
import
|
|
10
|
+
import { IconNext, IconSelected, IconUnSelected } from "@versini/ui-icons";
|
|
23
11
|
|
|
24
|
-
;// CONCATENATED MODULE: external "react/jsx-runtime"
|
|
25
12
|
|
|
26
|
-
;// CONCATENATED MODULE: external "@floating-ui/react"
|
|
27
13
|
|
|
28
|
-
;// CONCATENATED MODULE: external "@versini/ui-icons"
|
|
29
14
|
|
|
30
|
-
;// CONCATENATED MODULE: external "clsx"
|
|
31
15
|
|
|
32
|
-
|
|
16
|
+
/* v8 ignore start - default context values are fallbacks, never used directly */ const MenuRootContext = createContext({
|
|
17
|
+
closeAll: ()=>{}
|
|
18
|
+
});
|
|
19
|
+
const MenuContentContext = createContext({
|
|
20
|
+
registerItem: ()=>{},
|
|
21
|
+
unregisterItem: ()=>{},
|
|
22
|
+
getItems: ()=>[],
|
|
23
|
+
activeIndex: -1,
|
|
24
|
+
setActiveIndex: ()=>{},
|
|
25
|
+
openSubMenuId: null,
|
|
26
|
+
setOpenSubMenuId: ()=>{},
|
|
27
|
+
close: ()=>{},
|
|
28
|
+
isSubMenu: false
|
|
29
|
+
}); /* v8 ignore stop */
|
|
33
30
|
|
|
34
|
-
;// CONCATENATED MODULE: ./src/components/Menu/MenuContext.tsx
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
function getNextEnabledIndex(items, currentIndex, direction) {
|
|
33
|
+
const count = items.length;
|
|
34
|
+
/* v8 ignore start - empty items edge case */ if (count === 0) {
|
|
35
|
+
return -1;
|
|
36
|
+
}
|
|
37
|
+
/* v8 ignore stop */ let index = currentIndex;
|
|
38
|
+
for(let i = 0; i < count; i++){
|
|
39
|
+
index = (index + direction + count) % count;
|
|
40
|
+
if (!items[index].disabled) {
|
|
41
|
+
return index;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/* v8 ignore start - all items disabled edge case */ return currentIndex;
|
|
45
|
+
/* v8 ignore stop */ }
|
|
46
|
+
/* v8 ignore start - Home/End handlers with disabled item edge cases */ function getFirstEnabledIndex(items) {
|
|
47
|
+
for(let i = 0; i < items.length; i++){
|
|
48
|
+
if (!items[i].disabled) {
|
|
49
|
+
return i;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return -1;
|
|
53
|
+
}
|
|
54
|
+
function getLastEnabledIndex(items) {
|
|
55
|
+
for(let i = items.length - 1; i >= 0; i--){
|
|
56
|
+
if (!items[i].disabled) {
|
|
57
|
+
return i;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return -1;
|
|
61
|
+
}
|
|
62
|
+
/* v8 ignore stop */ function useMenuKeyboard({ menuRef, isOpen, activeIndex, setActiveIndex, getItems, onClose, isSubMenu = false, onOpenSubMenu, onCloseToParent }) {
|
|
63
|
+
/* v8 ignore start - focusItem bounds guard */ const focusItem = useCallback((index)=>{
|
|
64
|
+
const items = getItems();
|
|
65
|
+
if (index >= 0 && index < items.length) {
|
|
66
|
+
items[index].element.focus();
|
|
67
|
+
}
|
|
68
|
+
}, [
|
|
69
|
+
getItems
|
|
70
|
+
]);
|
|
71
|
+
/* v8 ignore stop */ useEffect(()=>{
|
|
72
|
+
if (!isOpen || !menuRef.current) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const handleKeyDown = (event)=>{
|
|
76
|
+
const items = getItems();
|
|
77
|
+
/* v8 ignore start - items always exist when menu is open */ if (items.length === 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
/* v8 ignore stop */ /* v8 ignore start - switch default branch (unmatched keys) */ switch(event.key){
|
|
81
|
+
/* v8 ignore stop */ case "ArrowDown":
|
|
82
|
+
{
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
const nextIndex = getNextEnabledIndex(items, activeIndex, 1);
|
|
85
|
+
setActiveIndex(nextIndex);
|
|
86
|
+
focusItem(nextIndex);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "ArrowUp":
|
|
90
|
+
{
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
const prevIndex = getNextEnabledIndex(items, activeIndex, -1);
|
|
93
|
+
setActiveIndex(prevIndex);
|
|
94
|
+
focusItem(prevIndex);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case "Home":
|
|
98
|
+
{
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
const firstIndex = getFirstEnabledIndex(items);
|
|
101
|
+
setActiveIndex(firstIndex);
|
|
102
|
+
focusItem(firstIndex);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "End":
|
|
106
|
+
{
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
const lastIndex = getLastEnabledIndex(items);
|
|
109
|
+
setActiveIndex(lastIndex);
|
|
110
|
+
focusItem(lastIndex);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "Enter":
|
|
114
|
+
case " ":
|
|
115
|
+
{
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
/* v8 ignore start - activeIndex bounds and disabled guard */ if (activeIndex >= 0 && activeIndex < items.length && !items[activeIndex].disabled) {
|
|
118
|
+
items[activeIndex].element.click();
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "Escape":
|
|
123
|
+
{
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
onClose();
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
/* v8 ignore start - sub-menu keyboard nav requires full browser env */ case "ArrowRight":
|
|
129
|
+
{
|
|
130
|
+
if (activeIndex >= 0 && activeIndex < items.length && onOpenSubMenu) {
|
|
131
|
+
const item = items[activeIndex].element;
|
|
132
|
+
if (item.getAttribute("aria-haspopup") === "menu") {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
onOpenSubMenu(item);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case "ArrowLeft":
|
|
140
|
+
{
|
|
141
|
+
if (isSubMenu && onCloseToParent) {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
onCloseToParent();
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "Tab":
|
|
148
|
+
{
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
onClose();
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const menuElement = menuRef.current;
|
|
156
|
+
menuElement.addEventListener("keydown", handleKeyDown);
|
|
157
|
+
return ()=>{
|
|
158
|
+
menuElement.removeEventListener("keydown", handleKeyDown);
|
|
159
|
+
};
|
|
160
|
+
}, [
|
|
161
|
+
isOpen,
|
|
162
|
+
activeIndex,
|
|
163
|
+
setActiveIndex,
|
|
164
|
+
getItems,
|
|
165
|
+
onClose,
|
|
166
|
+
focusItem,
|
|
167
|
+
menuRef,
|
|
168
|
+
isSubMenu,
|
|
169
|
+
onOpenSubMenu,
|
|
170
|
+
onCloseToParent
|
|
171
|
+
]);
|
|
172
|
+
return {
|
|
173
|
+
focusItem
|
|
174
|
+
};
|
|
175
|
+
}
|
|
45
176
|
|
|
46
|
-
;// CONCATENATED MODULE: ./src/components/Menu/utilities.ts
|
|
47
177
|
const getDisplayName = (element)=>{
|
|
48
178
|
if (typeof element === "string") {
|
|
49
179
|
return element;
|
|
@@ -54,13 +184,131 @@ const getDisplayName = (element)=>{
|
|
|
54
184
|
if (typeof element === "object" && element !== null && "type" in element) {
|
|
55
185
|
const type = element.type;
|
|
56
186
|
if (typeof type === "function" || typeof type === "object") {
|
|
57
|
-
return type.displayName || type.name || "Component";
|
|
58
|
-
}
|
|
187
|
+
/* v8 ignore start */ return type.displayName || type.name || "Component";
|
|
188
|
+
/* v8 ignore stop */ }
|
|
59
189
|
}
|
|
60
190
|
return "Element";
|
|
61
191
|
};
|
|
192
|
+
const calculatePosition = (triggerRect, menuRect, placement, sideOffset, viewportWidth, viewportHeight)=>{
|
|
193
|
+
/* v8 ignore start - split always returns non-empty for valid placements */ const side = placement.split("-")[0] || "bottom";
|
|
194
|
+
/* v8 ignore stop */ const alignment = placement.includes("-") ? placement.split("-")[1] : "center";
|
|
195
|
+
let top = 0;
|
|
196
|
+
let left = 0;
|
|
197
|
+
// Calculate base position based on side
|
|
198
|
+
switch(side){
|
|
199
|
+
case "bottom":
|
|
200
|
+
top = triggerRect.bottom + sideOffset;
|
|
201
|
+
break;
|
|
202
|
+
case "top":
|
|
203
|
+
top = triggerRect.top - menuRect.height - sideOffset;
|
|
204
|
+
break;
|
|
205
|
+
case "right":
|
|
206
|
+
left = triggerRect.right + sideOffset;
|
|
207
|
+
break;
|
|
208
|
+
case "left":
|
|
209
|
+
left = triggerRect.left - menuRect.width - sideOffset;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
// Calculate alignment
|
|
213
|
+
if (side === "bottom" || side === "top") {
|
|
214
|
+
switch(alignment){
|
|
215
|
+
case "start":
|
|
216
|
+
left = triggerRect.left;
|
|
217
|
+
break;
|
|
218
|
+
case "end":
|
|
219
|
+
left = triggerRect.right - menuRect.width;
|
|
220
|
+
break;
|
|
221
|
+
default:
|
|
222
|
+
left = triggerRect.left + (triggerRect.width - menuRect.width) / 2;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
switch(alignment){
|
|
227
|
+
case "start":
|
|
228
|
+
top = triggerRect.top;
|
|
229
|
+
break;
|
|
230
|
+
case "end":
|
|
231
|
+
top = triggerRect.bottom - menuRect.height;
|
|
232
|
+
break;
|
|
233
|
+
default:
|
|
234
|
+
top = triggerRect.top + (triggerRect.height - menuRect.height) / 2;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Auto-flip if overflowing viewport
|
|
239
|
+
/* v8 ignore start - auto-flip with fallback when flipped position also overflows */ if (side === "bottom" && top + menuRect.height > viewportHeight) {
|
|
240
|
+
const flippedTop = triggerRect.top - menuRect.height - sideOffset;
|
|
241
|
+
if (flippedTop >= 0) {
|
|
242
|
+
top = flippedTop;
|
|
243
|
+
}
|
|
244
|
+
} else if (side === "top" && top < 0) {
|
|
245
|
+
const flippedTop = triggerRect.bottom + sideOffset;
|
|
246
|
+
if (flippedTop + menuRect.height <= viewportHeight) {
|
|
247
|
+
top = flippedTop;
|
|
248
|
+
}
|
|
249
|
+
} else if (side === "right" && left + menuRect.width > viewportWidth) {
|
|
250
|
+
const flippedLeft = triggerRect.left - menuRect.width - sideOffset;
|
|
251
|
+
if (flippedLeft >= 0) {
|
|
252
|
+
left = flippedLeft;
|
|
253
|
+
}
|
|
254
|
+
} else if (side === "left" && left < 0) {
|
|
255
|
+
const flippedLeft = triggerRect.right + sideOffset;
|
|
256
|
+
if (flippedLeft + menuRect.width <= viewportWidth) {
|
|
257
|
+
left = flippedLeft;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/* v8 ignore stop */ // Clamp to viewport bounds
|
|
261
|
+
left = Math.max(0, Math.min(left, viewportWidth - menuRect.width));
|
|
262
|
+
top = Math.max(0, Math.min(top, viewportHeight - menuRect.height));
|
|
263
|
+
return {
|
|
264
|
+
top,
|
|
265
|
+
left
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
function useMenuPosition({ triggerRef, menuRef, placement, sideOffset, isOpen }) {
|
|
272
|
+
const positionRef = useRef({
|
|
273
|
+
top: 0,
|
|
274
|
+
left: 0
|
|
275
|
+
});
|
|
276
|
+
const updatePosition = useCallback(()=>{
|
|
277
|
+
/* v8 ignore start - positioning requires real DOM layout */ if (!triggerRef.current || !menuRef.current) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const triggerRect = triggerRef.current.getBoundingClientRect();
|
|
281
|
+
const menuRect = menuRef.current.getBoundingClientRect();
|
|
282
|
+
const viewportWidth = window.innerWidth;
|
|
283
|
+
const viewportHeight = window.innerHeight;
|
|
284
|
+
const position = calculatePosition(triggerRect, menuRect, placement, sideOffset, viewportWidth, viewportHeight);
|
|
285
|
+
positionRef.current = position;
|
|
286
|
+
menuRef.current.style.position = "fixed";
|
|
287
|
+
menuRef.current.style.top = `${position.top}px`;
|
|
288
|
+
menuRef.current.style.left = `${position.left}px`;
|
|
289
|
+
menuRef.current.style.margin = "0";
|
|
290
|
+
/* v8 ignore stop */ }, [
|
|
291
|
+
triggerRef,
|
|
292
|
+
menuRef,
|
|
293
|
+
placement,
|
|
294
|
+
sideOffset
|
|
295
|
+
]);
|
|
296
|
+
useEffect(()=>{
|
|
297
|
+
if (isOpen) {
|
|
298
|
+
requestAnimationFrame(()=>{
|
|
299
|
+
updatePosition();
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}, [
|
|
303
|
+
isOpen,
|
|
304
|
+
updatePosition
|
|
305
|
+
]);
|
|
306
|
+
return {
|
|
307
|
+
updatePosition,
|
|
308
|
+
positionRef
|
|
309
|
+
};
|
|
310
|
+
}
|
|
62
311
|
|
|
63
|
-
;// CONCATENATED MODULE: ./src/components/Menu/Menu.tsx
|
|
64
312
|
|
|
65
313
|
|
|
66
314
|
|
|
@@ -68,362 +316,359 @@ const getDisplayName = (element)=>{
|
|
|
68
316
|
|
|
69
317
|
|
|
70
318
|
|
|
71
|
-
|
|
319
|
+
|
|
320
|
+
const CONTENT_CLASS = "z-100 rounded-md bg-surface-light shadow-sm shadow-border-dark outline-hidden p-3 sm:p-2 plume plume-dark";
|
|
321
|
+
const Menu = ({ trigger, children, label = "Open menu", defaultPlacement = "bottom-start", onOpenChange, mode = "system", focusMode = "system", sideOffset = 10 })=>{
|
|
72
322
|
const [isOpen, setIsOpen] = useState(false);
|
|
73
|
-
const [
|
|
74
|
-
const [
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const { floatingStyles, refs, context } = useFloating({
|
|
87
|
-
nodeId,
|
|
88
|
-
open: isOpen,
|
|
89
|
-
onOpenChange: (args)=>{
|
|
90
|
-
setIsOpen(args);
|
|
91
|
-
onOpenChange?.(args);
|
|
92
|
-
},
|
|
93
|
-
placement: isNested ? "right-start" : defaultPlacement,
|
|
94
|
-
strategy: "fixed",
|
|
95
|
-
middleware: [
|
|
96
|
-
offset(()=>{
|
|
97
|
-
/**
|
|
98
|
-
* For nested menus, account for responsive padding differences.
|
|
99
|
-
* The menu has p-4 (16px) on mobile and p-2 (8px) on sm+ breakpoints.
|
|
100
|
-
* At 640px (Tailwind's sm breakpoint), padding changes from 16px to 8px.
|
|
101
|
-
* We increase the offset at mobile sizes to compensate for the larger padding.
|
|
102
|
-
*/ if (isNested) {
|
|
103
|
-
const isMobile = window.innerWidth < 640;
|
|
104
|
-
return {
|
|
105
|
-
mainAxis: isMobile ? 22 : 14,
|
|
106
|
-
alignmentAxis: -4
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
return {
|
|
110
|
-
mainAxis: 10,
|
|
111
|
-
alignmentAxis: 0
|
|
112
|
-
};
|
|
113
|
-
}),
|
|
114
|
-
flip(),
|
|
115
|
-
shift()
|
|
116
|
-
],
|
|
117
|
-
whileElementsMounted: autoUpdate
|
|
118
|
-
});
|
|
119
|
-
const hover = useHover(context, {
|
|
120
|
-
enabled: isNested && allowHover,
|
|
121
|
-
delay: {
|
|
122
|
-
open: 75
|
|
123
|
-
},
|
|
124
|
-
handleClose: safePolygon({
|
|
125
|
-
blockPointerEvents: true
|
|
126
|
-
})
|
|
323
|
+
const [activeIndex, setActiveIndex] = useState(-1);
|
|
324
|
+
const [openSubMenuId, setOpenSubMenuId] = useState(null);
|
|
325
|
+
const menuId = useUniqueId("av-menu-");
|
|
326
|
+
const triggerRef = useRef(null);
|
|
327
|
+
const menuRef = useRef(null);
|
|
328
|
+
const itemsRef = useRef([]);
|
|
329
|
+
const isTogglingRef = useRef(false);
|
|
330
|
+
useMenuPosition({
|
|
331
|
+
triggerRef,
|
|
332
|
+
menuRef,
|
|
333
|
+
/* v8 ignore start - defaultPlacement always has a default value */ placement: defaultPlacement || "bottom-start",
|
|
334
|
+
/* v8 ignore stop */ sideOffset,
|
|
335
|
+
isOpen
|
|
127
336
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const listNavigation = useListNavigation(context, {
|
|
140
|
-
listRef: elementsRef,
|
|
141
|
-
activeIndex,
|
|
142
|
-
nested: isNested,
|
|
143
|
-
onNavigate: setActiveIndex,
|
|
144
|
-
loop: true
|
|
145
|
-
});
|
|
146
|
-
const typeahead = useTypeahead(context, {
|
|
147
|
-
listRef: labelsRef,
|
|
148
|
-
onMatch: isOpen ? setActiveIndex : undefined,
|
|
149
|
-
activeIndex
|
|
150
|
-
});
|
|
151
|
-
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
|
|
152
|
-
hover,
|
|
153
|
-
click,
|
|
154
|
-
role,
|
|
155
|
-
dismiss,
|
|
156
|
-
listNavigation,
|
|
157
|
-
typeahead
|
|
158
|
-
]);
|
|
159
|
-
const mergedRef = useMergeRefs([
|
|
160
|
-
refs.setReference,
|
|
161
|
-
item.ref,
|
|
162
|
-
forwardedRef
|
|
337
|
+
// Show/hide popover after the menu element is mounted/unmounted
|
|
338
|
+
/* v8 ignore start - popover API may not be available in test env */ useEffect(()=>{
|
|
339
|
+
if (isOpen && menuRef.current && typeof menuRef.current.showPopover === "function") {
|
|
340
|
+
try {
|
|
341
|
+
menuRef.current.showPopover();
|
|
342
|
+
} catch {
|
|
343
|
+
// Popover might already be shown
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}, [
|
|
347
|
+
isOpen
|
|
163
348
|
]);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
useEffect(()=>{
|
|
167
|
-
/* v8 ignore next 3 */ if (!tree) {
|
|
349
|
+
/* v8 ignore stop */ const handleClose = useCallback(()=>{
|
|
350
|
+
/* v8 ignore start - guard against double-close */ if (!isOpen) {
|
|
168
351
|
return;
|
|
169
352
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
353
|
+
/* v8 ignore stop */ setIsOpen(false);
|
|
354
|
+
setActiveIndex(-1);
|
|
355
|
+
setOpenSubMenuId(null);
|
|
356
|
+
onOpenChange?.(false);
|
|
357
|
+
/* v8 ignore start - popover API may not be available in test env */ if (menuRef.current && typeof menuRef.current.hidePopover === "function") {
|
|
358
|
+
try {
|
|
359
|
+
menuRef.current.hidePopover();
|
|
360
|
+
} catch {
|
|
361
|
+
// Popover might already be hidden
|
|
362
|
+
}
|
|
173
363
|
}
|
|
174
|
-
|
|
175
|
-
|
|
364
|
+
/* v8 ignore stop */ triggerRef.current?.focus();
|
|
365
|
+
}, [
|
|
366
|
+
isOpen,
|
|
367
|
+
onOpenChange
|
|
368
|
+
]);
|
|
369
|
+
// Click-outside detection: only active when menu is open
|
|
370
|
+
/* v8 ignore start - click-outside detection requires full browser env */ useEffect(()=>{
|
|
371
|
+
if (!isOpen) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const handleClickOutside = (event)=>{
|
|
375
|
+
const target = event.target;
|
|
376
|
+
if (menuRef.current && !menuRef.current.contains(target) && triggerRef.current && !triggerRef.current.contains(target)) {
|
|
176
377
|
setIsOpen(false);
|
|
378
|
+
setActiveIndex(-1);
|
|
379
|
+
setOpenSubMenuId(null);
|
|
380
|
+
onOpenChange?.(false);
|
|
381
|
+
triggerRef.current?.focus();
|
|
177
382
|
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
383
|
+
};
|
|
384
|
+
// Use a timeout to avoid catching the opening click itself
|
|
385
|
+
const timeoutId = setTimeout(()=>{
|
|
386
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
387
|
+
}, 0);
|
|
181
388
|
return ()=>{
|
|
182
|
-
|
|
183
|
-
|
|
389
|
+
clearTimeout(timeoutId);
|
|
390
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
184
391
|
};
|
|
185
392
|
}, [
|
|
186
|
-
|
|
187
|
-
nodeId,
|
|
188
|
-
parentId,
|
|
393
|
+
isOpen,
|
|
189
394
|
onOpenChange
|
|
190
395
|
]);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
396
|
+
/* v8 ignore stop */ const getItems = useCallback(()=>itemsRef.current, []);
|
|
397
|
+
const registerItem = useCallback((element, disabled)=>{
|
|
398
|
+
// Maintain DOM order by using compareDocumentPosition
|
|
399
|
+
const existing = itemsRef.current.findIndex((item)=>item.element === element);
|
|
400
|
+
/* v8 ignore start - re-registration on re-render */ if (existing >= 0) {
|
|
401
|
+
itemsRef.current[existing].disabled = disabled;
|
|
402
|
+
return;
|
|
197
403
|
}
|
|
404
|
+
/* v8 ignore stop */ const newItem = {
|
|
405
|
+
element,
|
|
406
|
+
disabled
|
|
407
|
+
};
|
|
408
|
+
// Insert in DOM order
|
|
409
|
+
const insertIndex = itemsRef.current.findIndex((item)=>item.element.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING);
|
|
410
|
+
/* v8 ignore start */ if (insertIndex === -1) {
|
|
411
|
+
itemsRef.current.push(newItem);
|
|
412
|
+
} else {
|
|
413
|
+
itemsRef.current.splice(insertIndex, 0, newItem);
|
|
414
|
+
}
|
|
415
|
+
/* v8 ignore stop */ }, []);
|
|
416
|
+
const unregisterItem = useCallback((element)=>{
|
|
417
|
+
itemsRef.current = itemsRef.current.filter((item)=>item.element !== element);
|
|
418
|
+
}, []);
|
|
419
|
+
/* v8 ignore start - sub-menu keyboard nav requires full browser env */ const handleOpenSubMenu = useCallback((element)=>{
|
|
420
|
+
element.click();
|
|
421
|
+
}, []);
|
|
422
|
+
/* v8 ignore stop */ const { focusItem } = useMenuKeyboard({
|
|
423
|
+
menuRef,
|
|
424
|
+
isOpen,
|
|
425
|
+
activeIndex,
|
|
426
|
+
setActiveIndex,
|
|
427
|
+
getItems,
|
|
428
|
+
onClose: handleClose,
|
|
429
|
+
onOpenSubMenu: handleOpenSubMenu
|
|
430
|
+
});
|
|
431
|
+
const handleOpen = useCallback(()=>{
|
|
432
|
+
setIsOpen(true);
|
|
433
|
+
onOpenChange?.(true);
|
|
434
|
+
// Focus first enabled item after menu renders
|
|
435
|
+
/* v8 ignore start - rAF does not execute in test env */ requestAnimationFrame(()=>{
|
|
436
|
+
const items = getItems();
|
|
437
|
+
for(let i = 0; i < items.length; i++){
|
|
438
|
+
if (!items[i].disabled) {
|
|
439
|
+
setActiveIndex(i);
|
|
440
|
+
focusItem(i);
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
/* v8 ignore stop */ }, [
|
|
446
|
+
onOpenChange,
|
|
447
|
+
getItems,
|
|
448
|
+
focusItem
|
|
449
|
+
]);
|
|
450
|
+
const handleToggle = useCallback(()=>{
|
|
451
|
+
/* v8 ignore start - re-entrant guard for synthetic click dispatch */ if (isTogglingRef.current) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
/* v8 ignore stop */ isTogglingRef.current = true;
|
|
455
|
+
/* v8 ignore start - toggle close when clicking trigger while menu is open */ if (isOpen) {
|
|
456
|
+
handleClose();
|
|
457
|
+
} else {
|
|
458
|
+
/* v8 ignore stop */ handleOpen();
|
|
459
|
+
/**
|
|
460
|
+
* Dispatch a click event to parent elements.
|
|
461
|
+
* This ensures that parent components like Tooltip can detect the
|
|
462
|
+
* interaction and respond appropriately (e.g., disable tooltip display).
|
|
463
|
+
*/ /* v8 ignore start */ if (triggerRef.current) {
|
|
464
|
+
const clickEvent = new MouseEvent("click", {
|
|
465
|
+
bubbles: true,
|
|
466
|
+
cancelable: true,
|
|
467
|
+
view: window
|
|
468
|
+
});
|
|
469
|
+
triggerRef.current.dispatchEvent(clickEvent);
|
|
470
|
+
}
|
|
471
|
+
/* v8 ignore stop */ }
|
|
472
|
+
isTogglingRef.current = false;
|
|
198
473
|
}, [
|
|
199
|
-
tree,
|
|
200
474
|
isOpen,
|
|
201
|
-
|
|
202
|
-
|
|
475
|
+
handleClose,
|
|
476
|
+
handleOpen
|
|
203
477
|
]);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
setAllowHover(true);
|
|
478
|
+
/* v8 ignore start - trigger keyboard handler tested via integration */ const handleTriggerKeyDown = useCallback((event)=>{
|
|
479
|
+
if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown") {
|
|
480
|
+
event.preventDefault();
|
|
481
|
+
if (!isOpen) {
|
|
482
|
+
handleOpen();
|
|
210
483
|
}
|
|
211
484
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
capture: true
|
|
218
|
-
});
|
|
219
|
-
window.addEventListener("keydown", onKeyDown, true);
|
|
220
|
-
return ()=>{
|
|
221
|
-
window.removeEventListener("pointermove", onPointerMove, {
|
|
222
|
-
capture: true
|
|
223
|
-
});
|
|
224
|
-
window.removeEventListener("keydown", onKeyDown, true);
|
|
225
|
-
};
|
|
226
|
-
}, []);
|
|
485
|
+
}, [
|
|
486
|
+
isOpen,
|
|
487
|
+
handleOpen
|
|
488
|
+
]);
|
|
489
|
+
/* v8 ignore stop */ // Build trigger element with props
|
|
227
490
|
const noInternalClick = getDisplayName(trigger) === "Button" || getDisplayName(trigger) === "ButtonIcon";
|
|
228
491
|
const uiButtonsExtraProps = noInternalClick ? {
|
|
229
492
|
noInternalClick,
|
|
230
493
|
focusMode,
|
|
231
494
|
mode
|
|
232
495
|
} : {};
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
496
|
+
/* v8 ignore start - trigger is required in practice */ const triggerElement = /*#__PURE__*/ isValidElement(trigger) ? /*#__PURE__*/ cloneElement(trigger, {
|
|
497
|
+
...uiButtonsExtraProps,
|
|
498
|
+
"aria-label": label,
|
|
499
|
+
"aria-haspopup": "menu",
|
|
500
|
+
"aria-expanded": isOpen,
|
|
501
|
+
"aria-controls": menuId,
|
|
502
|
+
"data-state": isOpen ? "open" : "closed",
|
|
503
|
+
ref: triggerRef,
|
|
504
|
+
onClick: handleToggle,
|
|
505
|
+
onKeyDown: handleTriggerKeyDown
|
|
506
|
+
}) : null;
|
|
507
|
+
/* v8 ignore stop */ const contentContextValue = {
|
|
508
|
+
registerItem,
|
|
509
|
+
unregisterItem,
|
|
510
|
+
getItems,
|
|
511
|
+
activeIndex,
|
|
512
|
+
setActiveIndex,
|
|
513
|
+
openSubMenuId,
|
|
514
|
+
setOpenSubMenuId,
|
|
515
|
+
close: handleClose,
|
|
516
|
+
isSubMenu: false
|
|
517
|
+
};
|
|
518
|
+
return /*#__PURE__*/ jsx(MenuRootContext.Provider, {
|
|
519
|
+
value: {
|
|
520
|
+
closeAll: handleClose
|
|
521
|
+
},
|
|
522
|
+
children: /*#__PURE__*/ jsxs(MenuContentContext.Provider, {
|
|
523
|
+
value: contentContextValue,
|
|
240
524
|
children: [
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
setHasFocusInside(false);
|
|
250
|
-
parent.setHasFocusInside(true);
|
|
251
|
-
},
|
|
252
|
-
onMouseEnter (event) {
|
|
253
|
-
props.onMouseEnter?.(event);
|
|
254
|
-
if (parent.allowHover && parent.isOpen) {
|
|
255
|
-
parent.setActiveIndex(item.index);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
})),
|
|
259
|
-
children: [
|
|
260
|
-
/*#__PURE__*/ jsx("span", {
|
|
261
|
-
children: label
|
|
262
|
-
}),
|
|
263
|
-
/*#__PURE__*/ jsx(IconNext, {
|
|
264
|
-
className: "ml-2",
|
|
265
|
-
size: "size-3",
|
|
266
|
-
monotone: true
|
|
267
|
-
})
|
|
268
|
-
]
|
|
269
|
-
}),
|
|
270
|
-
/*#__PURE__*/ jsx(MenuContext.Provider, {
|
|
271
|
-
value: {
|
|
272
|
-
activeIndex,
|
|
273
|
-
setActiveIndex,
|
|
274
|
-
getItemProps,
|
|
275
|
-
setHasFocusInside,
|
|
276
|
-
isOpen,
|
|
277
|
-
allowHover,
|
|
278
|
-
parent
|
|
279
|
-
},
|
|
280
|
-
children: /*#__PURE__*/ jsx(FloatingList, {
|
|
281
|
-
elementsRef: elementsRef,
|
|
282
|
-
labelsRef: labelsRef,
|
|
283
|
-
children: isOpen && /*#__PURE__*/ jsx(FloatingPortal, {
|
|
284
|
-
children: /*#__PURE__*/ jsx(FloatingFocusManager, {
|
|
285
|
-
context: context,
|
|
286
|
-
modal: false,
|
|
287
|
-
initialFocus: -1,
|
|
288
|
-
returnFocus: false,
|
|
289
|
-
children: /*#__PURE__*/ jsx("div", {
|
|
290
|
-
ref: refs.setFloating,
|
|
291
|
-
className: "rounded-md bg-surface-light shadow-sm shadow-border-dark outline-hidden p-3 sm:p-2",
|
|
292
|
-
style: floatingStyles,
|
|
293
|
-
...getFloatingProps(),
|
|
294
|
-
children: children
|
|
295
|
-
})
|
|
296
|
-
})
|
|
297
|
-
})
|
|
298
|
-
})
|
|
525
|
+
triggerElement,
|
|
526
|
+
isOpen && /*#__PURE__*/ jsx("div", {
|
|
527
|
+
ref: menuRef,
|
|
528
|
+
id: menuId,
|
|
529
|
+
popover: "manual",
|
|
530
|
+
role: "menu",
|
|
531
|
+
className: CONTENT_CLASS,
|
|
532
|
+
children: children
|
|
299
533
|
})
|
|
300
534
|
]
|
|
301
|
-
})
|
|
302
|
-
}
|
|
303
|
-
const triggerElement = /*#__PURE__*/ react.cloneElement(trigger, {
|
|
304
|
-
...uiButtonsExtraProps,
|
|
305
|
-
"aria-label": label,
|
|
306
|
-
"data-open": isOpen ? "" : undefined,
|
|
307
|
-
"data-focus-inside": hasFocusInside ? "" : undefined,
|
|
308
|
-
ref: mergedRef,
|
|
309
|
-
...getReferenceProps(parent.getItemProps({
|
|
310
|
-
...props
|
|
311
|
-
}))
|
|
535
|
+
})
|
|
312
536
|
});
|
|
313
|
-
|
|
314
|
-
|
|
537
|
+
};
|
|
538
|
+
Menu.displayName = "Menu";
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
const MenuGroupLabel = ({ className, icon, children, ...props })=>{
|
|
544
|
+
const groupLabelClass = clsx(className, "pt-1 pb-2 mb-2", "text-sm text-copy-dark font-bold", "border-b border-border-medium", {
|
|
545
|
+
"flex items-center": icon
|
|
546
|
+
});
|
|
547
|
+
const labelSpanClass = icon ? "px-2" : "";
|
|
548
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
549
|
+
className: groupLabelClass,
|
|
550
|
+
...props,
|
|
315
551
|
children: [
|
|
316
|
-
|
|
317
|
-
/*#__PURE__*/ jsx(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
setActiveIndex,
|
|
321
|
-
getItemProps,
|
|
322
|
-
setHasFocusInside,
|
|
323
|
-
isOpen,
|
|
324
|
-
allowHover,
|
|
325
|
-
parent
|
|
326
|
-
},
|
|
327
|
-
children: /*#__PURE__*/ jsx(FloatingList, {
|
|
328
|
-
elementsRef: elementsRef,
|
|
329
|
-
labelsRef: labelsRef,
|
|
330
|
-
children: isOpen && /*#__PURE__*/ jsx(FloatingPortal, {
|
|
331
|
-
children: /*#__PURE__*/ jsx(FloatingFocusManager, {
|
|
332
|
-
context: context,
|
|
333
|
-
modal: false,
|
|
334
|
-
initialFocus: 0,
|
|
335
|
-
returnFocus: true,
|
|
336
|
-
children: /*#__PURE__*/ jsx("div", {
|
|
337
|
-
ref: refs.setFloating,
|
|
338
|
-
className: "rounded-md bg-surface-light shadow-sm shadow-border-dark outline-hidden p-3 sm:p-2",
|
|
339
|
-
style: floatingStyles,
|
|
340
|
-
...getFloatingProps(),
|
|
341
|
-
children: children
|
|
342
|
-
})
|
|
343
|
-
})
|
|
344
|
-
})
|
|
345
|
-
})
|
|
552
|
+
icon,
|
|
553
|
+
/*#__PURE__*/ jsx("span", {
|
|
554
|
+
className: labelSpanClass,
|
|
555
|
+
children: children
|
|
346
556
|
})
|
|
347
557
|
]
|
|
348
558
|
});
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const parentId = useFloatingParentNodeId();
|
|
352
|
-
// If parentId is null, this is a root menu - wrap in FloatingTree
|
|
353
|
-
if (parentId === null) {
|
|
354
|
-
return /*#__PURE__*/ jsx(FloatingTree, {
|
|
355
|
-
children: /*#__PURE__*/ jsx(MenuComponent, {
|
|
356
|
-
...props,
|
|
357
|
-
ref: ref
|
|
358
|
-
})
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
// This is a nested sub-menu - don't wrap in FloatingTree
|
|
362
|
-
return /*#__PURE__*/ jsx(MenuComponent, {
|
|
363
|
-
...props,
|
|
364
|
-
ref: ref
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
Menu.displayName = "Menu";
|
|
368
|
-
MenuComponent.displayName = "MenuComponent";
|
|
559
|
+
};
|
|
560
|
+
MenuGroupLabel.displayName = "MenuGroupLabel";
|
|
369
561
|
|
|
370
|
-
;// CONCATENATED MODULE: ./src/components/Menu/MenuItem.tsx
|
|
371
562
|
|
|
372
563
|
|
|
373
564
|
|
|
374
565
|
|
|
375
566
|
|
|
376
567
|
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
568
|
+
const ITEM_CLASS = clsx("flex flex-row items-center", "w-full", "m-0 first:mt-0 mt-2 sm:mt-1 px-2 py-1", "rounded-md border border-transparent", "text-left text-base select-none cursor-pointer", "outline-hidden focus:border focus:border-border-medium focus:bg-surface-lighter focus:underline", "disabled:cursor-not-allowed disabled:text-copy-medium", "data-highlighted:bg-surface-lighter data-highlighted:border-border-medium data-highlighted:underline", "data-disabled:cursor-not-allowed data-disabled:text-copy-medium");
|
|
569
|
+
const MenuItem = ({ label, disabled, icon, raw = false, children, ignoreClick = false, selected, onSelect, onClick, onFocus, onMouseEnter, ...props })=>{
|
|
570
|
+
const itemRef = useRef(null);
|
|
571
|
+
const { closeAll } = useContext(MenuRootContext);
|
|
572
|
+
const { registerItem, unregisterItem, getItems, activeIndex, setActiveIndex, setOpenSubMenuId } = useContext(MenuContentContext);
|
|
573
|
+
useEffect(()=>{
|
|
574
|
+
const element = itemRef.current;
|
|
575
|
+
/* v8 ignore start - ref is always set after mount */ if (element) {
|
|
576
|
+
registerItem(element, !!disabled);
|
|
577
|
+
return ()=>{
|
|
578
|
+
unregisterItem(element);
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
/* v8 ignore stop */ }, [
|
|
582
|
+
disabled,
|
|
583
|
+
registerItem,
|
|
584
|
+
unregisterItem
|
|
387
585
|
]);
|
|
388
|
-
|
|
586
|
+
const getMyIndex = ()=>{
|
|
587
|
+
const items = getItems();
|
|
588
|
+
return items.findIndex((item)=>item.element === itemRef.current);
|
|
589
|
+
};
|
|
590
|
+
const myIndex = getMyIndex();
|
|
591
|
+
const isHighlighted = myIndex >= 0 && activeIndex === myIndex;
|
|
592
|
+
const handleSelect = (event)=>{
|
|
593
|
+
/* v8 ignore start - disabled items are not clickable in practice */ if (disabled) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
/* v8 ignore stop */ const syntheticEvent = new Event("select", {
|
|
597
|
+
bubbles: true,
|
|
598
|
+
cancelable: true
|
|
599
|
+
});
|
|
600
|
+
if (ignoreClick) {
|
|
601
|
+
syntheticEvent.preventDefault();
|
|
602
|
+
}
|
|
603
|
+
onSelect?.(syntheticEvent);
|
|
604
|
+
/* v8 ignore start - optional onClick may not be provided */ onClick?.(event);
|
|
605
|
+
/* v8 ignore stop */ if (!ignoreClick) {
|
|
606
|
+
closeAll();
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
const handleMouseEnter = (event)=>{
|
|
610
|
+
/* v8 ignore start - disabled items do not receive hover highlight */ if (disabled) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
/* v8 ignore stop */ const myIndex = getMyIndex();
|
|
614
|
+
/* v8 ignore start - item is always registered when mouse enters */ if (myIndex >= 0) {
|
|
615
|
+
setActiveIndex(myIndex);
|
|
616
|
+
itemRef.current?.focus();
|
|
617
|
+
}
|
|
618
|
+
/* v8 ignore stop */ // Close any open sub-menu when hovering a regular item
|
|
619
|
+
setOpenSubMenuId(null);
|
|
620
|
+
onMouseEnter?.(event);
|
|
621
|
+
};
|
|
622
|
+
/* v8 ignore start - raw items with disabled attribute */ if (raw && children) {
|
|
389
623
|
return /*#__PURE__*/ jsx("div", {
|
|
624
|
+
ref: itemRef,
|
|
390
625
|
role: "menuitem",
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
626
|
+
tabIndex: isHighlighted ? 0 : -1,
|
|
627
|
+
className: "outline-hidden",
|
|
628
|
+
"data-highlighted": isHighlighted ? "" : undefined,
|
|
629
|
+
"data-disabled": disabled ? "" : undefined,
|
|
630
|
+
"aria-disabled": disabled || undefined,
|
|
631
|
+
onClick: (event)=>{
|
|
632
|
+
/* v8 ignore start - disabled items are not clickable in practice */ if (disabled) {
|
|
633
|
+
return;
|
|
397
634
|
}
|
|
398
|
-
|
|
635
|
+
/* v8 ignore stop */ const syntheticEvent = new Event("select", {
|
|
636
|
+
bubbles: true,
|
|
637
|
+
cancelable: true
|
|
638
|
+
});
|
|
639
|
+
if (ignoreClick) {
|
|
640
|
+
syntheticEvent.preventDefault();
|
|
641
|
+
}
|
|
642
|
+
onSelect?.(syntheticEvent);
|
|
643
|
+
/* v8 ignore start - optional onClick may not be provided */ onClick?.(event);
|
|
644
|
+
/* v8 ignore stop */ if (!ignoreClick) {
|
|
645
|
+
closeAll();
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
onMouseEnter: handleMouseEnter,
|
|
649
|
+
...props,
|
|
399
650
|
children: children
|
|
400
651
|
});
|
|
401
652
|
}
|
|
653
|
+
/* v8 ignore stop */ let buttonSpanClass = "";
|
|
402
654
|
if (icon) {
|
|
403
655
|
buttonSpanClass = "pl-2";
|
|
404
656
|
}
|
|
405
|
-
const
|
|
657
|
+
const itemClass = clsx(ITEM_CLASS, {
|
|
406
658
|
"bg-none": !disabled && !selected
|
|
407
659
|
});
|
|
408
|
-
return /*#__PURE__*/ jsxs("
|
|
409
|
-
|
|
410
|
-
ref: mergedRef,
|
|
660
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
661
|
+
ref: itemRef,
|
|
411
662
|
role: "menuitem",
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
},
|
|
422
|
-
onFocus (event) {
|
|
423
|
-
props.onFocus?.(event);
|
|
424
|
-
menu.setHasFocusInside(true);
|
|
425
|
-
}
|
|
426
|
-
}),
|
|
663
|
+
tabIndex: isHighlighted ? 0 : -1,
|
|
664
|
+
className: itemClass,
|
|
665
|
+
"data-highlighted": isHighlighted ? "" : undefined,
|
|
666
|
+
"data-disabled": disabled ? "" : undefined,
|
|
667
|
+
"aria-disabled": disabled || undefined,
|
|
668
|
+
onClick: handleSelect,
|
|
669
|
+
onFocus: onFocus,
|
|
670
|
+
onMouseEnter: handleMouseEnter,
|
|
671
|
+
...props,
|
|
427
672
|
children: [
|
|
428
673
|
selected === true && /*#__PURE__*/ jsx(IconSelected, {
|
|
429
674
|
className: "text-copy-success mr-2",
|
|
@@ -440,25 +685,253 @@ const MenuItem = /*#__PURE__*/ forwardRef(({ label, disabled, icon, raw = false,
|
|
|
440
685
|
})
|
|
441
686
|
]
|
|
442
687
|
});
|
|
443
|
-
}
|
|
688
|
+
};
|
|
444
689
|
MenuItem.displayName = "MenuItem";
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
|
|
445
693
|
const MenuSeparator = ({ className, ...props })=>{
|
|
446
694
|
const separatorClass = clsx(className, "my-1 border-t border-border-medium");
|
|
447
695
|
return /*#__PURE__*/ jsx("div", {
|
|
696
|
+
role: "separator",
|
|
448
697
|
className: separatorClass,
|
|
449
698
|
...props
|
|
450
699
|
});
|
|
451
700
|
};
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
701
|
+
MenuSeparator.displayName = "MenuSeparator";
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
const SUB_CONTENT_CLASS = "z-[60] rounded-md bg-surface-light shadow-sm shadow-border-dark outline-hidden p-3 sm:p-2 mx-3";
|
|
712
|
+
const SUB_TRIGGER_CLASS = clsx("flex items-center flex-row justify-between", "w-full", "m-0 first:mt-0 mt-2 sm:mt-1 px-2 py-1", "rounded-md border border-transparent", "text-left text-base select-none cursor-pointer", "outline-hidden focus:border focus:border-border-medium focus:bg-surface-lighter focus:underline", "disabled:cursor-not-allowed disabled:text-copy-medium", "data-highlighted:bg-surface-lighter data-highlighted:border-border-medium data-highlighted:underline", "data-[state=open]:bg-surface-lighter");
|
|
713
|
+
const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 14 })=>{
|
|
714
|
+
const subMenuId = useUniqueId("av-menu-sub-");
|
|
715
|
+
const [isSubOpen, setIsSubOpen] = useState(false);
|
|
716
|
+
const [subActiveIndex, setSubActiveIndex] = useState(-1);
|
|
717
|
+
const [subOpenSubMenuId, setSubOpenSubMenuId] = useState(null);
|
|
718
|
+
const triggerItemRef = useRef(null);
|
|
719
|
+
const subMenuRef = useRef(null);
|
|
720
|
+
const subItemsRef = useRef([]);
|
|
721
|
+
const parentContext = useContext(MenuContentContext);
|
|
722
|
+
const rootContext = useContext(MenuRootContext);
|
|
723
|
+
// Register as an item in the parent menu
|
|
724
|
+
useEffect(()=>{
|
|
725
|
+
const element = triggerItemRef.current;
|
|
726
|
+
/* v8 ignore start - ref is always set after mount */ if (element) {
|
|
727
|
+
parentContext.registerItem(element, !!disabled);
|
|
728
|
+
return ()=>{
|
|
729
|
+
parentContext.unregisterItem(element);
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
/* v8 ignore stop */ }, [
|
|
733
|
+
disabled,
|
|
734
|
+
parentContext.registerItem,
|
|
735
|
+
parentContext.unregisterItem
|
|
736
|
+
]);
|
|
737
|
+
// Close if a sibling sub-menu opens or parent signals close (openSubMenuId set to null)
|
|
738
|
+
useEffect(()=>{
|
|
739
|
+
if (isSubOpen && parentContext.openSubMenuId !== subMenuId) {
|
|
740
|
+
setIsSubOpen(false);
|
|
741
|
+
setSubActiveIndex(-1);
|
|
742
|
+
/* v8 ignore start */ if (subMenuRef.current && typeof subMenuRef.current.hidePopover === "function") {
|
|
743
|
+
try {
|
|
744
|
+
subMenuRef.current.hidePopover();
|
|
745
|
+
} catch {
|
|
746
|
+
// Already hidden
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/* v8 ignore stop */ }
|
|
750
|
+
}, [
|
|
751
|
+
parentContext.openSubMenuId,
|
|
752
|
+
subMenuId,
|
|
753
|
+
isSubOpen
|
|
754
|
+
]);
|
|
755
|
+
useMenuPosition({
|
|
756
|
+
triggerRef: triggerItemRef,
|
|
757
|
+
menuRef: subMenuRef,
|
|
758
|
+
placement: "right-start",
|
|
759
|
+
sideOffset,
|
|
760
|
+
isOpen: isSubOpen
|
|
761
|
+
});
|
|
762
|
+
// Show popover after the sub-menu element is mounted
|
|
763
|
+
/* v8 ignore start - popover API may not be available in test env */ useEffect(()=>{
|
|
764
|
+
if (isSubOpen && subMenuRef.current && typeof subMenuRef.current.showPopover === "function") {
|
|
765
|
+
try {
|
|
766
|
+
subMenuRef.current.showPopover();
|
|
767
|
+
} catch {
|
|
768
|
+
// Popover might already be shown
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}, [
|
|
772
|
+
isSubOpen
|
|
773
|
+
]);
|
|
774
|
+
/* v8 ignore stop */ const getSubItems = useCallback(()=>subItemsRef.current, []);
|
|
775
|
+
const registerSubItem = useCallback((element, itemDisabled)=>{
|
|
776
|
+
const existing = subItemsRef.current.findIndex((item)=>item.element === element);
|
|
777
|
+
/* v8 ignore start - re-registration on re-render */ if (existing >= 0) {
|
|
778
|
+
subItemsRef.current[existing].disabled = itemDisabled;
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
/* v8 ignore stop */ const newItem = {
|
|
782
|
+
element,
|
|
783
|
+
disabled: itemDisabled
|
|
784
|
+
};
|
|
785
|
+
const insertIndex = subItemsRef.current.findIndex((item)=>item.element.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING);
|
|
786
|
+
/* v8 ignore start */ if (insertIndex === -1) {
|
|
787
|
+
subItemsRef.current.push(newItem);
|
|
788
|
+
} else {
|
|
789
|
+
subItemsRef.current.splice(insertIndex, 0, newItem);
|
|
790
|
+
}
|
|
791
|
+
/* v8 ignore stop */ }, []);
|
|
792
|
+
const unregisterSubItem = useCallback((element)=>{
|
|
793
|
+
subItemsRef.current = subItemsRef.current.filter((item)=>item.element !== element);
|
|
794
|
+
}, []);
|
|
795
|
+
/* v8 ignore start - closeSubMenu requires full browser env */ const closeSubMenu = useCallback(()=>{
|
|
796
|
+
setIsSubOpen(false);
|
|
797
|
+
setSubActiveIndex(-1);
|
|
798
|
+
setSubOpenSubMenuId(null);
|
|
799
|
+
if (subMenuRef.current && typeof subMenuRef.current.hidePopover === "function") {
|
|
800
|
+
try {
|
|
801
|
+
subMenuRef.current.hidePopover();
|
|
802
|
+
} catch {
|
|
803
|
+
// Already hidden
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
triggerItemRef.current?.focus();
|
|
807
|
+
}, []);
|
|
808
|
+
/* v8 ignore stop */ const openSubMenu = useCallback(()=>{
|
|
809
|
+
/* v8 ignore start - disabled sub-triggers cannot be opened */ if (disabled) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
/* v8 ignore stop */ setIsSubOpen(true);
|
|
813
|
+
parentContext.setOpenSubMenuId(subMenuId);
|
|
814
|
+
// Focus first enabled item after render
|
|
815
|
+
/* v8 ignore start - rAF does not execute in test env */ requestAnimationFrame(()=>{
|
|
816
|
+
const items = getSubItems();
|
|
817
|
+
for(let i = 0; i < items.length; i++){
|
|
818
|
+
if (!items[i].disabled) {
|
|
819
|
+
setSubActiveIndex(i);
|
|
820
|
+
items[i].element.focus();
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
/* v8 ignore stop */ }, [
|
|
826
|
+
disabled,
|
|
827
|
+
parentContext,
|
|
828
|
+
subMenuId,
|
|
829
|
+
getSubItems
|
|
830
|
+
]);
|
|
831
|
+
/* v8 ignore start - sub-menu keyboard nav requires full browser env */ const handleSubOpenSubMenu = useCallback((element)=>{
|
|
832
|
+
element.click();
|
|
833
|
+
}, []);
|
|
834
|
+
/* v8 ignore stop */ useMenuKeyboard({
|
|
835
|
+
menuRef: subMenuRef,
|
|
836
|
+
isOpen: isSubOpen,
|
|
837
|
+
activeIndex: subActiveIndex,
|
|
838
|
+
setActiveIndex: setSubActiveIndex,
|
|
839
|
+
getItems: getSubItems,
|
|
840
|
+
/* v8 ignore start - sub-menu escape close */ onClose: ()=>{
|
|
841
|
+
rootContext.closeAll();
|
|
842
|
+
},
|
|
843
|
+
/* v8 ignore stop */ isSubMenu: true,
|
|
844
|
+
onOpenSubMenu: handleSubOpenSubMenu,
|
|
845
|
+
onCloseToParent: closeSubMenu
|
|
846
|
+
});
|
|
847
|
+
const handleTriggerClick = ()=>{
|
|
848
|
+
/* v8 ignore start - disabled sub-triggers are not clickable in practice */ if (disabled) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
/* v8 ignore stop */ /* v8 ignore start - click always follows mouseenter which already opens */ if (!isSubOpen) {
|
|
852
|
+
openSubMenu();
|
|
853
|
+
}
|
|
854
|
+
/* v8 ignore stop */ };
|
|
855
|
+
/* v8 ignore start - mouse enter always finds registered item */ const handleTriggerMouseEnter = ()=>{
|
|
856
|
+
const items = parentContext.getItems();
|
|
857
|
+
const myIndex = items.findIndex((item)=>item.element === triggerItemRef.current);
|
|
858
|
+
if (myIndex >= 0) {
|
|
859
|
+
parentContext.setActiveIndex(myIndex);
|
|
860
|
+
triggerItemRef.current?.focus();
|
|
861
|
+
}
|
|
862
|
+
// Open sub-menu on hover (WAI-ARIA: hover opens sub-menus when parent menu is already open)
|
|
863
|
+
if (!isSubOpen && !disabled) {
|
|
864
|
+
openSubMenu();
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
/* v8 ignore stop */ const myIndex = parentContext.getItems().findIndex((item)=>item.element === triggerItemRef.current);
|
|
868
|
+
const isHighlighted = myIndex >= 0 && parentContext.activeIndex === myIndex;
|
|
869
|
+
const labelSpanClass = icon ? "pl-2" : "";
|
|
870
|
+
const subContentContextValue = {
|
|
871
|
+
registerItem: registerSubItem,
|
|
872
|
+
unregisterItem: unregisterSubItem,
|
|
873
|
+
getItems: getSubItems,
|
|
874
|
+
activeIndex: subActiveIndex,
|
|
875
|
+
setActiveIndex: setSubActiveIndex,
|
|
876
|
+
openSubMenuId: subOpenSubMenuId,
|
|
877
|
+
setOpenSubMenuId: setSubOpenSubMenuId,
|
|
878
|
+
close: closeSubMenu,
|
|
879
|
+
isSubMenu: true
|
|
880
|
+
};
|
|
881
|
+
return /*#__PURE__*/ jsxs(Fragment, {
|
|
882
|
+
children: [
|
|
883
|
+
/*#__PURE__*/ jsxs("div", {
|
|
884
|
+
ref: triggerItemRef,
|
|
885
|
+
role: "menuitem",
|
|
886
|
+
"aria-haspopup": "menu",
|
|
887
|
+
"aria-expanded": isSubOpen,
|
|
888
|
+
"aria-controls": subMenuId,
|
|
889
|
+
tabIndex: isHighlighted ? 0 : -1,
|
|
890
|
+
className: SUB_TRIGGER_CLASS,
|
|
891
|
+
"data-state": isSubOpen ? "open" : "closed",
|
|
892
|
+
"data-highlighted": isHighlighted ? "" : undefined,
|
|
893
|
+
/* v8 ignore start - disabled sub-trigger not tested */ "data-disabled": disabled ? "" : undefined,
|
|
894
|
+
"aria-disabled": disabled || undefined,
|
|
895
|
+
/* v8 ignore stop */ onClick: handleTriggerClick,
|
|
896
|
+
onMouseEnter: handleTriggerMouseEnter,
|
|
897
|
+
children: [
|
|
898
|
+
/*#__PURE__*/ jsxs("span", {
|
|
899
|
+
className: "flex items-center",
|
|
900
|
+
children: [
|
|
901
|
+
icon,
|
|
902
|
+
/*#__PURE__*/ jsx("span", {
|
|
903
|
+
className: labelSpanClass,
|
|
904
|
+
children: label
|
|
905
|
+
})
|
|
906
|
+
]
|
|
907
|
+
}),
|
|
908
|
+
/*#__PURE__*/ jsx(IconNext, {
|
|
909
|
+
className: "ml-2",
|
|
910
|
+
size: "size-3",
|
|
911
|
+
monotone: true
|
|
912
|
+
})
|
|
913
|
+
]
|
|
914
|
+
}),
|
|
915
|
+
isSubOpen && /*#__PURE__*/ jsx("div", {
|
|
916
|
+
ref: subMenuRef,
|
|
917
|
+
id: subMenuId,
|
|
918
|
+
popover: "manual",
|
|
919
|
+
role: "menu",
|
|
920
|
+
className: SUB_CONTENT_CLASS,
|
|
921
|
+
children: /*#__PURE__*/ jsx(MenuContentContext.Provider, {
|
|
922
|
+
value: subContentContextValue,
|
|
923
|
+
children: children
|
|
924
|
+
})
|
|
925
|
+
})
|
|
926
|
+
]
|
|
457
927
|
});
|
|
458
928
|
};
|
|
929
|
+
MenuSub.displayName = "MenuSub";
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
|
|
459
933
|
|
|
460
|
-
;// CONCATENATED MODULE: ./src/components/index.ts
|
|
461
934
|
|
|
462
935
|
|
|
463
936
|
|
|
464
|
-
export { Menu, MenuGroupLabel, MenuItem, MenuSeparator };
|
|
937
|
+
export { Menu, MenuGroupLabel, MenuItem, MenuSeparator, MenuSub };
|