@versini/ui-menu 5.3.4 → 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/dist/index.js CHANGED
@@ -1,49 +1,179 @@
1
1
  /*!
2
- @versini/ui-menu v5.3.4
3
- © 2025 gizmette.com
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.4",
9
- buildTime: "12/12/2025 10:29 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 { FloatingFocusManager, FloatingList, FloatingNode, FloatingPortal, FloatingTree, autoUpdate, flip, offset, safePolygon, shift, useClick, useDismiss, useFloating, useFloatingNodeId, useFloatingParentNodeId, useFloatingTree, useHover, useInteractions, useListItem, useListNavigation, useMergeRefs, useRole, useTypeahead } from "@floating-ui/react";
20
- import { IconNext, IconSelected, IconUnSelected } from "@versini/ui-icons";
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 react, { createContext, forwardRef, useContext, useEffect, useRef, useState } from "react";
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
- ;// CONCATENATED MODULE: external "react"
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
- const MenuContext = /*#__PURE__*/ createContext({
37
- getItemProps: ()=>({}),
38
- activeIndex: null,
39
- /* v8 ignore next 2 */ setActiveIndex: ()=>{},
40
- setHasFocusInside: ()=>{},
41
- isOpen: false,
42
- allowHover: false,
43
- parent: null
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
- const MenuComponent = /*#__PURE__*/ forwardRef(({ trigger, children, label = "Open menu", defaultPlacement = "bottom-start", onOpenChange, mode = "system", focusMode = "system", ...props }, forwardedRef)=>{
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 [hasFocusInside, setHasFocusInside] = useState(false);
74
- const [activeIndex, setActiveIndex] = useState(null);
75
- const [allowHover, setAllowHover] = useState(false);
76
- const elementsRef = useRef([]);
77
- const labelsRef = useRef([]);
78
- const parent = useContext(MenuContext);
79
- const tree = useFloatingTree();
80
- const nodeId = useFloatingNodeId();
81
- const parentId = useFloatingParentNodeId();
82
- const item = useListItem({
83
- label: label !== "Open menu" ? label : null
84
- });
85
- const isNested = parentId != null;
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
- const click = useClick(context, {
129
- event: "mousedown",
130
- toggle: !isNested || !allowHover,
131
- ignoreMouse: isNested && allowHover
132
- });
133
- const role = useRole(context, {
134
- role: "menu"
135
- });
136
- const dismiss = useDismiss(context, {
137
- bubbles: true
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
- // Event emitter allows you to communicate across tree components.
165
- // This effect closes all menus when an item gets clicked anywhere in the tree.
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
- function handleTreeClick() {
171
- setIsOpen(false);
172
- onOpenChange?.(false);
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
- function onSubMenuOpen(event) {
175
- if (event.nodeId !== nodeId && event.parentId === parentId) {
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
- tree.events.on("click", handleTreeClick);
180
- tree.events.on("menuopen", onSubMenuOpen);
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
- tree.events.off("click", handleTreeClick);
183
- tree.events.off("menuopen", onSubMenuOpen);
389
+ clearTimeout(timeoutId);
390
+ document.removeEventListener("mousedown", handleClickOutside);
184
391
  };
185
392
  }, [
186
- tree,
187
- nodeId,
188
- parentId,
393
+ isOpen,
189
394
  onOpenChange
190
395
  ]);
191
- useEffect(()=>{
192
- if (isOpen && tree) {
193
- tree.events.emit("menuopen", {
194
- parentId,
195
- nodeId
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
- nodeId,
202
- parentId
475
+ handleClose,
476
+ handleOpen
203
477
  ]);
204
- // Determine if "hover" logic can run based on input modality.
205
- // This prevents unwanted focus sync as menus open/close with keyboard nav.
206
- useEffect(()=>{
207
- function onPointerMove({ pointerType }) {
208
- if (pointerType !== "touch") {
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
- function onKeyDown() {
213
- setAllowHover(false);
214
- }
215
- window.addEventListener("pointermove", onPointerMove, {
216
- once: true,
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
- // For nested menus (sub-menus), render as a menu item trigger
234
- if (isNested && !trigger) {
235
- const buttonClass = 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", "outline-hidden focus:border focus:border-border-medium focus:bg-surface-lighter focus:underline", "disabled:cursor-not-allowed disabled:text-copy-medium", {
236
- "bg-surface-lighter": isOpen && !hasFocusInside
237
- });
238
- return /*#__PURE__*/ jsxs(FloatingNode, {
239
- id: nodeId,
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
- /*#__PURE__*/ jsxs("button", {
242
- ref: mergedRef,
243
- "data-open": isOpen ? "" : undefined,
244
- ...getReferenceProps(parent.getItemProps({
245
- ...props,
246
- className: buttonClass,
247
- onFocus (event) {
248
- props.onFocus?.(event);
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
- return /*#__PURE__*/ jsxs(FloatingNode, {
314
- id: nodeId,
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
- triggerElement,
317
- /*#__PURE__*/ jsx(MenuContext.Provider, {
318
- value: {
319
- activeIndex,
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
- const Menu = /*#__PURE__*/ forwardRef((props, ref)=>{
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 MenuItem = /*#__PURE__*/ forwardRef(({ label, disabled, icon, raw = false, children, ignoreClick = false, selected, ...props }, forwardedRef)=>{
378
- let buttonSpanClass = "";
379
- const menu = useContext(MenuContext);
380
- const item = useListItem({
381
- label: disabled ? null : label
382
- });
383
- const tree = useFloatingTree();
384
- const mergedRef = useMergeRefs([
385
- item.ref,
386
- forwardedRef
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
- if (raw && children) {
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
- ...menu.getItemProps({
392
- onClick (event) {
393
- if (!ignoreClick) {
394
- props.onClick?.(event);
395
- tree?.events.emit("click");
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 buttonClass = 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", "outline-hidden focus:border focus:border-border-medium focus:bg-surface-lighter focus:underline", "disabled:cursor-not-allowed disabled:text-copy-medium", {
657
+ const itemClass = clsx(ITEM_CLASS, {
406
658
  "bg-none": !disabled && !selected
407
659
  });
408
- return /*#__PURE__*/ jsxs("button", {
409
- ...props,
410
- ref: mergedRef,
660
+ return /*#__PURE__*/ jsxs("div", {
661
+ ref: itemRef,
411
662
  role: "menuitem",
412
- className: buttonClass,
413
- tabIndex: 0,
414
- disabled: disabled,
415
- ...menu.getItemProps({
416
- onClick (event) {
417
- if (!ignoreClick) {
418
- props.onClick?.(event);
419
- tree?.events.emit("click");
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
- const MenuGroupLabel = ({ className, ...props })=>{
453
- const groupLabelClass = clsx(className, "pt-1 mb-2", "text-sm text-copy-dark font-bold", "border-b border-border-medium");
454
- return /*#__PURE__*/ jsx("div", {
455
- className: groupLabelClass,
456
- ...props
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 };