@versini/ui-menu 7.1.1 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +16 -7
- package/dist/index.js +103 -42
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { JSX } from 'react
|
|
1
|
+
import { JSX } from 'react';
|
|
2
|
+
import * as React_2 from 'react';
|
|
2
3
|
|
|
3
4
|
export declare const Menu: {
|
|
4
|
-
({ trigger, children, label, defaultPlacement, onOpenChange, mode, sideOffset, noBackdrop, }: MenuProps): JSX.Element;
|
|
5
|
+
({ trigger, children, label, defaultPlacement, onOpenChange, mode, sideOffset, noBackdrop, }: MenuProps): React_2.JSX.Element;
|
|
5
6
|
displayName: string;
|
|
6
7
|
};
|
|
7
8
|
|
|
@@ -37,8 +38,8 @@ export declare const MenuItem: {
|
|
|
37
38
|
declare type MenuItemProps = {
|
|
38
39
|
/**
|
|
39
40
|
* The label to use for the menu item. Accepts any React node so you can
|
|
40
|
-
* compose richer labels (e.g. text plus a `<Badge>`, an icon, or styled
|
|
41
|
-
*
|
|
41
|
+
* compose richer labels (e.g. text plus a `<Badge>`, an icon, or styled inline
|
|
42
|
+
* elements). For a plain text label, pass a string.
|
|
42
43
|
*/
|
|
43
44
|
label?: React.ReactNode;
|
|
44
45
|
/**
|
|
@@ -146,15 +147,15 @@ export declare const MenuSeparator: {
|
|
|
146
147
|
declare type MenuSeparatorProps = React.HTMLAttributes<HTMLDivElement>;
|
|
147
148
|
|
|
148
149
|
export declare const MenuSub: {
|
|
149
|
-
({ label, icon, children, disabled, sideOffset, }: MenuSubProps): JSX.Element;
|
|
150
|
+
({ label, icon, children, disabled, sideOffset, selected, }: MenuSubProps): JSX.Element;
|
|
150
151
|
displayName: string;
|
|
151
152
|
};
|
|
152
153
|
|
|
153
154
|
declare type MenuSubProps = {
|
|
154
155
|
/**
|
|
155
156
|
* The label for the sub-menu trigger. Accepts any React node so you can
|
|
156
|
-
* compose richer labels (e.g. text plus a `<Badge>`). For a plain text
|
|
157
|
-
*
|
|
157
|
+
* compose richer labels (e.g. text plus a `<Badge>`). For a plain text label,
|
|
158
|
+
* pass a string.
|
|
158
159
|
*/
|
|
159
160
|
label: React.ReactNode;
|
|
160
161
|
/**
|
|
@@ -175,6 +176,14 @@ declare type MenuSubProps = {
|
|
|
175
176
|
* @default 20
|
|
176
177
|
*/
|
|
177
178
|
sideOffset?: number;
|
|
179
|
+
/**
|
|
180
|
+
* Forces the "contains a selection" indicator on (true) or off (false),
|
|
181
|
+
* overriding automatic detection. Leave undefined to auto-detect from
|
|
182
|
+
* descendant MenuItems. Use this when MenuItem children are wrapped in a
|
|
183
|
+
* custom component that hides the `selected` prop from auto-detection.
|
|
184
|
+
* @default undefined (auto-detected)
|
|
185
|
+
*/
|
|
186
|
+
selected?: boolean;
|
|
178
187
|
};
|
|
179
188
|
|
|
180
189
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-menu v7.
|
|
2
|
+
@versini/ui-menu v7.2.0
|
|
3
3
|
© 2026 gizmette.com
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -53,18 +53,20 @@ const TYPEAHEAD_TIMEOUT_MS = 500;
|
|
|
53
53
|
function getTypeaheadMatchIndex(items, searchString, startIndex) {
|
|
54
54
|
const count = items.length;
|
|
55
55
|
const normalized = searchString.toLowerCase();
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
/**
|
|
57
|
+
* When the same single character is repeated (e.g. "aaa"), cycle through items
|
|
58
|
+
* starting with that character instead of trying to match the full repeated
|
|
59
|
+
* string.
|
|
60
|
+
*/ const isRepeatedChar = normalized.length > 1 && [
|
|
60
61
|
...normalized
|
|
61
62
|
].every((c)=>c === normalized[0]);
|
|
62
63
|
const matchStr = isRepeatedChar ? normalized[0] : normalized;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
/**
|
|
65
|
+
* For a single character (including repeated single-char), start searching
|
|
66
|
+
* from the item *after* the current one so we always advance to the next
|
|
67
|
+
* match. For multi-character strings, start from the current item so the user
|
|
68
|
+
* can refine the match in-place.
|
|
69
|
+
*/ const offset = normalized.length === 1 || isRepeatedChar ? 1 : 0;
|
|
68
70
|
for(let i = 0; i < count; i++){
|
|
69
71
|
const index = (startIndex + offset + i) % count;
|
|
70
72
|
if (items[index].disabled) {
|
|
@@ -216,9 +218,10 @@ function getLastEnabledIndex(items) {
|
|
|
216
218
|
}
|
|
217
219
|
/* v8 ignore stop */ default:
|
|
218
220
|
{
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Typeahead: printable single characters (ignore modifier-only or
|
|
223
|
+
* functional keys such as Shift, Control, Alt, Meta, etc.).
|
|
224
|
+
*/ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
|
|
222
225
|
event.preventDefault();
|
|
223
226
|
event.stopPropagation();
|
|
224
227
|
// Reset the debounce timer.
|
|
@@ -264,6 +267,13 @@ function getLastEnabledIndex(items) {
|
|
|
264
267
|
|
|
265
268
|
|
|
266
269
|
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Shared classes for the small green "selected" dot, reused by MenuItem (inner
|
|
274
|
+
* dot) and MenuSub (nested-selection indicator) so the success token lives in
|
|
275
|
+
* one place.
|
|
276
|
+
*/ const SELECTED_DOT_CLASS = "size-1.5 rounded-full bg-copy-success-light";
|
|
267
277
|
const getDisplayName = (element)=>{
|
|
268
278
|
if (typeof element === "string") {
|
|
269
279
|
return element;
|
|
@@ -279,12 +289,41 @@ const getDisplayName = (element)=>{
|
|
|
279
289
|
}
|
|
280
290
|
return "Element";
|
|
281
291
|
};
|
|
292
|
+
/**
|
|
293
|
+
* Recursively walks a React children tree looking for a selected menu entry.
|
|
294
|
+
* Only `MenuItem`/`MenuSub` elements with `selected === true` count, so an
|
|
295
|
+
* unrelated `selected` prop on other nested content (e.g. a native `<option
|
|
296
|
+
* selected>`) does not produce a false indicator. Traversal still descends
|
|
297
|
+
* through any container (MenuGroup, fragments) to reach those entries.
|
|
298
|
+
*
|
|
299
|
+
* Used by MenuSub to surface a nested selection on its trigger row even while
|
|
300
|
+
* the sub-menu is closed (sub-menu children are conditionally mounted, so they
|
|
301
|
+
* cannot report selection upward via context).
|
|
302
|
+
*
|
|
303
|
+
*/ const hasSelectedDescendant = (node)=>{
|
|
304
|
+
let found = false;
|
|
305
|
+
__rspack_external_react.Children.forEach(node, (child)=>{
|
|
306
|
+
if (found || !__rspack_external_react.isValidElement(child)) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const childProps = child.props;
|
|
310
|
+
const name = getDisplayName(child);
|
|
311
|
+
if ((name === "MenuItem" || name === "MenuSub") && childProps.selected === true) {
|
|
312
|
+
found = true;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (childProps.children) {
|
|
316
|
+
found = hasSelectedDescendant(childProps.children);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
return found;
|
|
320
|
+
};
|
|
282
321
|
const calculatePosition = (triggerRect, menuRect, placement, sideOffset, viewportWidth, viewportHeight)=>{
|
|
283
322
|
/* v8 ignore start - split always returns non-empty for valid placements */ const side = placement.split("-")[0] || "bottom";
|
|
284
323
|
/* v8 ignore stop */ const alignment = placement.includes("-") ? placement.split("-")[1] : "center";
|
|
285
324
|
let top = 0;
|
|
286
325
|
let left = 0;
|
|
287
|
-
// Calculate base position based on side
|
|
326
|
+
// Calculate base position based on side.
|
|
288
327
|
switch(side){
|
|
289
328
|
case "bottom":
|
|
290
329
|
top = triggerRect.bottom + sideOffset;
|
|
@@ -299,7 +338,7 @@ const calculatePosition = (triggerRect, menuRect, placement, sideOffset, viewpor
|
|
|
299
338
|
left = triggerRect.left - menuRect.width - sideOffset;
|
|
300
339
|
break;
|
|
301
340
|
}
|
|
302
|
-
// Calculate alignment
|
|
341
|
+
// Calculate alignment.
|
|
303
342
|
if (side === "bottom" || side === "top") {
|
|
304
343
|
switch(alignment){
|
|
305
344
|
case "start":
|
|
@@ -325,7 +364,7 @@ const calculatePosition = (triggerRect, menuRect, placement, sideOffset, viewpor
|
|
|
325
364
|
break;
|
|
326
365
|
}
|
|
327
366
|
}
|
|
328
|
-
// Auto-flip if overflowing viewport
|
|
367
|
+
// Auto-flip if overflowing viewport.
|
|
329
368
|
/* v8 ignore start - auto-flip with fallback when flipped position also overflows */ if (side === "bottom" && top + menuRect.height > viewportHeight) {
|
|
330
369
|
const flippedTop = triggerRect.top - menuRect.height - sideOffset;
|
|
331
370
|
if (flippedTop >= 0) {
|
|
@@ -347,7 +386,7 @@ const calculatePosition = (triggerRect, menuRect, placement, sideOffset, viewpor
|
|
|
347
386
|
left = flippedLeft;
|
|
348
387
|
}
|
|
349
388
|
}
|
|
350
|
-
/* v8 ignore stop */ // Clamp to viewport bounds
|
|
389
|
+
/* v8 ignore stop */ // Clamp to viewport bounds.
|
|
351
390
|
left = Math.max(0, Math.min(left, viewportWidth - menuRect.width));
|
|
352
391
|
top = Math.max(0, Math.min(top, viewportHeight - menuRect.height));
|
|
353
392
|
return {
|
|
@@ -440,13 +479,13 @@ const Menu = ({ trigger, children, label = "Open menu", defaultPlacement = "bott
|
|
|
440
479
|
/* v8 ignore stop */ sideOffset,
|
|
441
480
|
isOpen
|
|
442
481
|
});
|
|
443
|
-
// Show/hide popover after the menu element is mounted/unmounted
|
|
482
|
+
// Show/hide popover after the menu element is mounted/unmounted.
|
|
444
483
|
/* v8 ignore start - popover API may not be available in test env */ (0,__rspack_external_react.useEffect)(()=>{
|
|
445
484
|
if (isOpen && menuRef.current && typeof menuRef.current.showPopover === "function") {
|
|
446
485
|
try {
|
|
447
486
|
menuRef.current.showPopover();
|
|
448
487
|
} catch {
|
|
449
|
-
// Popover might already be shown
|
|
488
|
+
// Popover might already be shown.
|
|
450
489
|
}
|
|
451
490
|
}
|
|
452
491
|
}, [
|
|
@@ -464,7 +503,7 @@ const Menu = ({ trigger, children, label = "Open menu", defaultPlacement = "bott
|
|
|
464
503
|
try {
|
|
465
504
|
menuRef.current.hidePopover();
|
|
466
505
|
} catch {
|
|
467
|
-
// Popover might already be hidden
|
|
506
|
+
// Popover might already be hidden.
|
|
468
507
|
}
|
|
469
508
|
}
|
|
470
509
|
/* v8 ignore stop */ triggerRef.current?.focus();
|
|
@@ -472,7 +511,7 @@ const Menu = ({ trigger, children, label = "Open menu", defaultPlacement = "bott
|
|
|
472
511
|
isOpen,
|
|
473
512
|
onOpenChange
|
|
474
513
|
]);
|
|
475
|
-
// Click-outside detection: only active when menu is open
|
|
514
|
+
// Click-outside detection: only active when menu is open.
|
|
476
515
|
/* v8 ignore start - click-outside detection requires full browser env */ (0,__rspack_external_react.useEffect)(()=>{
|
|
477
516
|
if (!isOpen) {
|
|
478
517
|
return;
|
|
@@ -487,7 +526,7 @@ const Menu = ({ trigger, children, label = "Open menu", defaultPlacement = "bott
|
|
|
487
526
|
triggerRef.current?.focus();
|
|
488
527
|
}
|
|
489
528
|
};
|
|
490
|
-
// Use a timeout to avoid catching the opening click itself
|
|
529
|
+
// Use a timeout to avoid catching the opening click itself.
|
|
491
530
|
const timeoutId = setTimeout(()=>{
|
|
492
531
|
document.addEventListener("mousedown", handleClickOutside);
|
|
493
532
|
}, 0);
|
|
@@ -501,7 +540,7 @@ const Menu = ({ trigger, children, label = "Open menu", defaultPlacement = "bott
|
|
|
501
540
|
]);
|
|
502
541
|
/* v8 ignore stop */ const getItems = (0,__rspack_external_react.useCallback)(()=>itemsRef.current, []);
|
|
503
542
|
const registerItem = (0,__rspack_external_react.useCallback)((element, disabled)=>{
|
|
504
|
-
// Maintain DOM order by using compareDocumentPosition
|
|
543
|
+
// Maintain DOM order by using compareDocumentPosition.
|
|
505
544
|
const existing = itemsRef.current.findIndex((item)=>item.element === element);
|
|
506
545
|
/* v8 ignore start - re-registration on re-render */ if (existing >= 0) {
|
|
507
546
|
itemsRef.current[existing].disabled = disabled;
|
|
@@ -511,7 +550,7 @@ const Menu = ({ trigger, children, label = "Open menu", defaultPlacement = "bott
|
|
|
511
550
|
element,
|
|
512
551
|
disabled
|
|
513
552
|
};
|
|
514
|
-
// Insert in DOM order
|
|
553
|
+
// Insert in DOM order.
|
|
515
554
|
const insertIndex = itemsRef.current.findIndex((item)=>item.element.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING);
|
|
516
555
|
/* v8 ignore start */ if (insertIndex === -1) {
|
|
517
556
|
itemsRef.current.push(newItem);
|
|
@@ -537,7 +576,7 @@ const Menu = ({ trigger, children, label = "Open menu", defaultPlacement = "bott
|
|
|
537
576
|
const handleOpen = (0,__rspack_external_react.useCallback)(()=>{
|
|
538
577
|
setIsOpen(true);
|
|
539
578
|
onOpenChange?.(true);
|
|
540
|
-
// Focus first enabled item after menu renders
|
|
579
|
+
// Focus first enabled item after menu renders.
|
|
541
580
|
/* v8 ignore start - rAF does not execute in test env */ requestAnimationFrame(()=>{
|
|
542
581
|
const items = getItems();
|
|
543
582
|
for(let i = 0; i < items.length; i++){
|
|
@@ -563,9 +602,9 @@ const Menu = ({ trigger, children, label = "Open menu", defaultPlacement = "bott
|
|
|
563
602
|
} else {
|
|
564
603
|
/* v8 ignore stop */ handleOpen();
|
|
565
604
|
/**
|
|
566
|
-
* Dispatch a click event to parent elements.
|
|
567
|
-
*
|
|
568
|
-
*
|
|
605
|
+
* Dispatch a click event to parent elements. This ensures that parent
|
|
606
|
+
* components like Tooltip can detect the interaction and respond
|
|
607
|
+
* appropriately (e.g., disable tooltip display).
|
|
569
608
|
*/ /* v8 ignore start */ if (triggerRef.current) {
|
|
570
609
|
const clickEvent = new MouseEvent("click", {
|
|
571
610
|
bubbles: true,
|
|
@@ -592,7 +631,7 @@ const Menu = ({ trigger, children, label = "Open menu", defaultPlacement = "bott
|
|
|
592
631
|
isOpen,
|
|
593
632
|
handleOpen
|
|
594
633
|
]);
|
|
595
|
-
/* v8 ignore stop */ // Build trigger element with props
|
|
634
|
+
/* v8 ignore stop */ // Build trigger element with props.
|
|
596
635
|
const noInternalClick = getDisplayName(trigger) === "Button" || getDisplayName(trigger) === "ButtonIcon";
|
|
597
636
|
const uiButtonsExtraProps = noInternalClick ? {
|
|
598
637
|
noInternalClick,
|
|
@@ -739,7 +778,7 @@ const MenuItem = ({ label, disabled, icon, raw = false, children, ignoreClick =
|
|
|
739
778
|
setActiveIndex(myIndex);
|
|
740
779
|
itemRef.current?.focus();
|
|
741
780
|
}
|
|
742
|
-
/* v8 ignore stop */ // Close any open sub-menu when hovering a regular item
|
|
781
|
+
/* v8 ignore stop */ // Close any open sub-menu when hovering a regular item.
|
|
743
782
|
setOpenSubMenuId(null);
|
|
744
783
|
onMouseEnter?.(event);
|
|
745
784
|
};
|
|
@@ -797,7 +836,7 @@ const MenuItem = ({ label, disabled, icon, raw = false, children, ignoreClick =
|
|
|
797
836
|
selected === true && /*#__PURE__*/ jsx("span", {
|
|
798
837
|
className: "mr-2 flex size-4 shrink-0 items-center justify-center rounded-full border border-copy-medium",
|
|
799
838
|
children: /*#__PURE__*/ jsx("span", {
|
|
800
|
-
className:
|
|
839
|
+
className: SELECTED_DOT_CLASS
|
|
801
840
|
})
|
|
802
841
|
}),
|
|
803
842
|
selected === false && /*#__PURE__*/ jsx("span", {
|
|
@@ -868,7 +907,7 @@ const SUB_CONTENT_CLASS = getMenuClasses({
|
|
|
868
907
|
const SUB_TRIGGER_CLASS = getMenuItemClasses({
|
|
869
908
|
isSub: true
|
|
870
909
|
});
|
|
871
|
-
const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 20 })=>{
|
|
910
|
+
const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 20, selected })=>{
|
|
872
911
|
const subMenuId = useUniqueId("av-menu-sub-");
|
|
873
912
|
const [isSubOpen, setIsSubOpen] = useState(false);
|
|
874
913
|
const [subActiveIndex, setSubActiveIndex] = useState(-1);
|
|
@@ -878,7 +917,7 @@ const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 20 })=>
|
|
|
878
917
|
const subItemsRef = useRef([]);
|
|
879
918
|
const parentContext = useContext(MenuContentContext);
|
|
880
919
|
const rootContext = useContext(MenuRootContext);
|
|
881
|
-
// Register as an item in the parent menu
|
|
920
|
+
// Register as an item in the parent menu.
|
|
882
921
|
useEffect(()=>{
|
|
883
922
|
const element = triggerItemRef.current;
|
|
884
923
|
/* v8 ignore start - ref is always set after mount */ if (element) {
|
|
@@ -892,8 +931,10 @@ const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 20 })=>
|
|
|
892
931
|
parentContext.registerItem,
|
|
893
932
|
parentContext.unregisterItem
|
|
894
933
|
]);
|
|
895
|
-
|
|
896
|
-
|
|
934
|
+
/**
|
|
935
|
+
* Close if a sibling sub-menu opens or parent signals close (openSubMenuId set
|
|
936
|
+
* to null).
|
|
937
|
+
*/ useEffect(()=>{
|
|
897
938
|
if (isSubOpen && parentContext.openSubMenuId !== subMenuId) {
|
|
898
939
|
setIsSubOpen(false);
|
|
899
940
|
setSubActiveIndex(-1);
|
|
@@ -901,7 +942,7 @@ const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 20 })=>
|
|
|
901
942
|
try {
|
|
902
943
|
subMenuRef.current.hidePopover();
|
|
903
944
|
} catch {
|
|
904
|
-
// Already hidden
|
|
945
|
+
// Already hidden.
|
|
905
946
|
}
|
|
906
947
|
}
|
|
907
948
|
/* v8 ignore stop */ }
|
|
@@ -917,13 +958,13 @@ const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 20 })=>
|
|
|
917
958
|
sideOffset,
|
|
918
959
|
isOpen: isSubOpen
|
|
919
960
|
});
|
|
920
|
-
// Show popover after the sub-menu element is mounted
|
|
961
|
+
// Show popover after the sub-menu element is mounted.
|
|
921
962
|
/* v8 ignore start - popover API may not be available in test env */ useEffect(()=>{
|
|
922
963
|
if (isSubOpen && subMenuRef.current && typeof subMenuRef.current.showPopover === "function") {
|
|
923
964
|
try {
|
|
924
965
|
subMenuRef.current.showPopover();
|
|
925
966
|
} catch {
|
|
926
|
-
// Popover might already be shown
|
|
967
|
+
// Popover might already be shown.
|
|
927
968
|
}
|
|
928
969
|
}
|
|
929
970
|
}, [
|
|
@@ -958,7 +999,7 @@ const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 20 })=>
|
|
|
958
999
|
try {
|
|
959
1000
|
subMenuRef.current.hidePopover();
|
|
960
1001
|
} catch {
|
|
961
|
-
// Already hidden
|
|
1002
|
+
// Already hidden.
|
|
962
1003
|
}
|
|
963
1004
|
}
|
|
964
1005
|
triggerItemRef.current?.focus();
|
|
@@ -969,7 +1010,7 @@ const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 20 })=>
|
|
|
969
1010
|
}
|
|
970
1011
|
/* v8 ignore stop */ setIsSubOpen(true);
|
|
971
1012
|
parentContext.setOpenSubMenuId(subMenuId);
|
|
972
|
-
// Focus first enabled item after render
|
|
1013
|
+
// Focus first enabled item after render.
|
|
973
1014
|
/* v8 ignore start - rAF does not execute in test env */ requestAnimationFrame(()=>{
|
|
974
1015
|
const items = getSubItems();
|
|
975
1016
|
for(let i = 0; i < items.length; i++){
|
|
@@ -1017,14 +1058,24 @@ const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 20 })=>
|
|
|
1017
1058
|
parentContext.setActiveIndex(myIndex);
|
|
1018
1059
|
triggerItemRef.current?.focus();
|
|
1019
1060
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1061
|
+
/**
|
|
1062
|
+
* Open sub-menu on hover (WAI-ARIA: hover opens sub-menus when parent menu is
|
|
1063
|
+
* already open).
|
|
1064
|
+
*/ if (!isSubOpen && !disabled) {
|
|
1022
1065
|
openSubMenu();
|
|
1023
1066
|
}
|
|
1024
1067
|
};
|
|
1025
1068
|
/* v8 ignore stop */ const myIndex = parentContext.getItems().findIndex((item)=>item.element === triggerItemRef.current);
|
|
1026
1069
|
const isHighlighted = myIndex >= 0 && parentContext.activeIndex === myIndex;
|
|
1027
1070
|
const labelSpanClass = icon ? "pl-2" : "";
|
|
1071
|
+
/**
|
|
1072
|
+
* Surface a nested selection on the trigger row. An explicit `selected` prop
|
|
1073
|
+
* wins; otherwise auto-detect from the descendant children tree.
|
|
1074
|
+
*/ const showSelectedIndicator = selected ?? hasSelectedDescendant(children);
|
|
1075
|
+
/**
|
|
1076
|
+
* Screen-reader description id. Kept OUTSIDE the menuitem's text content so it
|
|
1077
|
+
* never pollutes typeahead navigation (which reads element.textContent).
|
|
1078
|
+
*/ const selectionDescriptionId = `${subMenuId}-selection`;
|
|
1028
1079
|
const subContentContextValue = {
|
|
1029
1080
|
registerItem: registerSubItem,
|
|
1030
1081
|
unregisterItem: unregisterSubItem,
|
|
@@ -1044,6 +1095,7 @@ const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 20 })=>
|
|
|
1044
1095
|
"aria-haspopup": "menu",
|
|
1045
1096
|
"aria-expanded": isSubOpen,
|
|
1046
1097
|
"aria-controls": subMenuId,
|
|
1098
|
+
"aria-describedby": showSelectedIndicator ? selectionDescriptionId : undefined,
|
|
1047
1099
|
tabIndex: isHighlighted ? 0 : -1,
|
|
1048
1100
|
className: SUB_TRIGGER_CLASS,
|
|
1049
1101
|
"data-state": isSubOpen ? "open" : "closed",
|
|
@@ -1056,6 +1108,10 @@ const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 20 })=>
|
|
|
1056
1108
|
/*#__PURE__*/ jsxs("span", {
|
|
1057
1109
|
className: "flex items-center",
|
|
1058
1110
|
children: [
|
|
1111
|
+
showSelectedIndicator && /*#__PURE__*/ jsx("span", {
|
|
1112
|
+
className: `mr-2 shrink-0 ${SELECTED_DOT_CLASS}`,
|
|
1113
|
+
"aria-hidden": "true"
|
|
1114
|
+
}),
|
|
1059
1115
|
icon,
|
|
1060
1116
|
/*#__PURE__*/ jsx("span", {
|
|
1061
1117
|
className: labelSpanClass,
|
|
@@ -1070,6 +1126,11 @@ const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 20 })=>
|
|
|
1070
1126
|
})
|
|
1071
1127
|
]
|
|
1072
1128
|
}),
|
|
1129
|
+
showSelectedIndicator && /*#__PURE__*/ jsx("span", {
|
|
1130
|
+
id: selectionDescriptionId,
|
|
1131
|
+
className: "sr-only",
|
|
1132
|
+
children: "(contains a selection)"
|
|
1133
|
+
}),
|
|
1073
1134
|
isSubOpen && /*#__PURE__*/ jsx("div", {
|
|
1074
1135
|
ref: subMenuRef,
|
|
1075
1136
|
id: subMenuId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versini/ui-menu",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Arno Versini",
|
|
6
6
|
"publishConfig": {
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@versini/ui-hooks": "6.1.1",
|
|
45
|
-
"@versini/ui-icons": "4.
|
|
45
|
+
"@versini/ui-icons": "4.29.0",
|
|
46
46
|
"clsx": "2.1.1",
|
|
47
|
-
"tailwindcss": "4.3.
|
|
47
|
+
"tailwindcss": "4.3.1"
|
|
48
48
|
},
|
|
49
49
|
"sideEffects": [
|
|
50
50
|
"**/*.css"
|
|
51
51
|
],
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "42b47804f1144c240ee9488e268c95f44cb8a796"
|
|
53
53
|
}
|