@aurora-ds/components 1.7.18 → 1.7.20
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/cjs/index.js +60 -19
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +60 -19
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.ts +25 -10
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -3524,12 +3524,12 @@ const MENU_MIN_WIDTH_PX = 224;
|
|
|
3524
3524
|
* Computes and continuously updates the `position: fixed` style for a menu panel.
|
|
3525
3525
|
*
|
|
3526
3526
|
* Positioning strategy:
|
|
3527
|
-
* - `placement="bottom"` (default):
|
|
3528
|
-
*
|
|
3529
|
-
*
|
|
3530
|
-
* - `placement="
|
|
3531
|
-
*
|
|
3532
|
-
*
|
|
3527
|
+
* - `placement="bottom"` (default): opens below the anchor; overflows are shifted up.
|
|
3528
|
+
* - `placement="top"`: opens above the anchor; flips to bottom when no room.
|
|
3529
|
+
* - `placement="right"`: opens to the right of the anchor; flips to the left when no room.
|
|
3530
|
+
* - `placement="left"`: opens to the left of the anchor; flips to the right when no room.
|
|
3531
|
+
*
|
|
3532
|
+
* In all cases the vertical position is clamped inside the viewport.
|
|
3533
3533
|
*
|
|
3534
3534
|
* Scroll handling: the page scroll is locked while the menu is open (see
|
|
3535
3535
|
* `MenuPanel`), so the anchor cannot move out from under the menu. As a safety
|
|
@@ -3541,6 +3541,10 @@ const MENU_MIN_WIDTH_PX = 224;
|
|
|
3541
3541
|
*/
|
|
3542
3542
|
const useMenuPosition = ({ anchorEl, open, menuRef, minWidth, gap = 4, placement = 'bottom', }) => {
|
|
3543
3543
|
const [style, setStyle] = useState({});
|
|
3544
|
+
// Stays false until the rAF second pass has run with the real panel dimensions.
|
|
3545
|
+
// Keeps the panel invisible during the initial style={} state and the first
|
|
3546
|
+
// pass (menuHeight = 0) to prevent the position-jump flickering.
|
|
3547
|
+
const [isPositioned, setIsPositioned] = useState(false);
|
|
3544
3548
|
const computePosition = useCallback(() => {
|
|
3545
3549
|
if (!anchorEl) {
|
|
3546
3550
|
return;
|
|
@@ -3551,24 +3555,56 @@ const useMenuPosition = ({ anchorEl, open, menuRef, minWidth, gap = 4, placement
|
|
|
3551
3555
|
const menuWidth = menuEl?.offsetWidth ?? 0;
|
|
3552
3556
|
const viewportRight = window.innerWidth - VIEWPORT_MARGIN_PX;
|
|
3553
3557
|
const viewportBottom = window.innerHeight - VIEWPORT_MARGIN_PX;
|
|
3554
|
-
if (placement === 'right') {
|
|
3558
|
+
if (placement === 'right' || placement === 'left') {
|
|
3555
3559
|
// --- Vertical: align with the anchor top, clamp inside the viewport ---
|
|
3556
3560
|
let top = anchor.top;
|
|
3557
3561
|
if (menuHeight > 0 && top + menuHeight > viewportBottom) {
|
|
3558
3562
|
top = Math.max(VIEWPORT_MARGIN_PX, viewportBottom - menuHeight);
|
|
3559
3563
|
}
|
|
3560
|
-
// --- Horizontal: open to the
|
|
3561
|
-
let left
|
|
3562
|
-
if (
|
|
3563
|
-
|
|
3564
|
-
left
|
|
3565
|
-
|
|
3566
|
-
|
|
3564
|
+
// --- Horizontal: open to the preferred side, flip to the opposite when no room ---
|
|
3565
|
+
let left;
|
|
3566
|
+
if (placement === 'right') {
|
|
3567
|
+
left = anchor.right + gap;
|
|
3568
|
+
if (menuWidth > 0 && left + menuWidth > viewportRight) {
|
|
3569
|
+
const flippedLeft = anchor.left - gap - menuWidth;
|
|
3570
|
+
left = flippedLeft >= VIEWPORT_MARGIN_PX
|
|
3571
|
+
? flippedLeft
|
|
3572
|
+
: Math.max(VIEWPORT_MARGIN_PX, viewportRight - menuWidth);
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
else {
|
|
3576
|
+
// placement === 'left'
|
|
3577
|
+
left = anchor.left - gap - menuWidth;
|
|
3578
|
+
if (menuWidth > 0 && left < VIEWPORT_MARGIN_PX) {
|
|
3579
|
+
const flippedLeft = anchor.right + gap;
|
|
3580
|
+
left = flippedLeft + menuWidth <= viewportRight
|
|
3581
|
+
? flippedLeft
|
|
3582
|
+
: Math.max(VIEWPORT_MARGIN_PX, viewportRight - menuWidth);
|
|
3583
|
+
}
|
|
3567
3584
|
}
|
|
3568
3585
|
setStyle({ top, left, minWidth: minWidth ?? MENU_MIN_WIDTH_PX });
|
|
3569
3586
|
return;
|
|
3570
3587
|
}
|
|
3571
|
-
|
|
3588
|
+
if (placement === 'top') {
|
|
3589
|
+
// --- Vertical: open above, flip to bottom when no room ---
|
|
3590
|
+
let top = anchor.top - gap - menuHeight;
|
|
3591
|
+
if (menuHeight > 0 && top < VIEWPORT_MARGIN_PX) {
|
|
3592
|
+
const flippedTop = anchor.bottom + gap;
|
|
3593
|
+
top = flippedTop + menuHeight <= viewportBottom
|
|
3594
|
+
? flippedTop
|
|
3595
|
+
: Math.max(VIEWPORT_MARGIN_PX, viewportBottom - menuHeight);
|
|
3596
|
+
}
|
|
3597
|
+
// --- Horizontal ---
|
|
3598
|
+
const fallbackMinWidth = Math.max(anchor.width, MENU_MIN_WIDTH_PX);
|
|
3599
|
+
const resolvedMinWidth = typeof minWidth === 'number' ? minWidth : fallbackMinWidth;
|
|
3600
|
+
const effectiveWidth = Math.max(menuWidth, resolvedMinWidth);
|
|
3601
|
+
const maxLeft = window.innerWidth - effectiveWidth - VIEWPORT_MARGIN_PX;
|
|
3602
|
+
const left = Math.max(VIEWPORT_MARGIN_PX, Math.min(anchor.left, maxLeft));
|
|
3603
|
+
setStyle({ top, left, minWidth: minWidth ?? fallbackMinWidth });
|
|
3604
|
+
return;
|
|
3605
|
+
}
|
|
3606
|
+
// --- placement === 'bottom' ---
|
|
3607
|
+
// --- Vertical ---
|
|
3572
3608
|
const preferredTop = anchor.bottom + gap;
|
|
3573
3609
|
let top = preferredTop;
|
|
3574
3610
|
if (menuHeight > 0 && preferredTop + menuHeight > viewportBottom) {
|
|
@@ -3589,14 +3625,19 @@ const useMenuPosition = ({ anchorEl, open, menuRef, minWidth, gap = 4, placement
|
|
|
3589
3625
|
}
|
|
3590
3626
|
else {
|
|
3591
3627
|
setStyle({});
|
|
3628
|
+
setIsPositioned(false);
|
|
3592
3629
|
}
|
|
3593
3630
|
}, [open, computePosition]);
|
|
3594
|
-
// Second pass: recompute after the panel renders to get actual height/width
|
|
3631
|
+
// Second pass: recompute after the panel renders to get actual height/width,
|
|
3632
|
+
// then mark as positioned so the panel becomes visible.
|
|
3595
3633
|
useEffect(() => {
|
|
3596
3634
|
if (!open) {
|
|
3597
3635
|
return;
|
|
3598
3636
|
}
|
|
3599
|
-
const id = requestAnimationFrame(
|
|
3637
|
+
const id = requestAnimationFrame(() => {
|
|
3638
|
+
computePosition();
|
|
3639
|
+
setIsPositioned(true);
|
|
3640
|
+
});
|
|
3600
3641
|
return () => cancelAnimationFrame(id);
|
|
3601
3642
|
}, [open, computePosition]);
|
|
3602
3643
|
// Keep the menu glued to its anchor on scroll (nested containers) and resize.
|
|
@@ -3611,7 +3652,7 @@ const useMenuPosition = ({ anchorEl, open, menuRef, minWidth, gap = 4, placement
|
|
|
3611
3652
|
window.removeEventListener('resize', computePosition);
|
|
3612
3653
|
};
|
|
3613
3654
|
}, [open, computePosition]);
|
|
3614
|
-
return { style };
|
|
3655
|
+
return { style: { ...style, visibility: isPositioned ? 'visible' : 'hidden' } };
|
|
3615
3656
|
};
|
|
3616
3657
|
|
|
3617
3658
|
/** Custom DOM event fired on a submenu trigger item to request it opens. */
|
|
@@ -4092,7 +4133,7 @@ const MenuItem = ({ ref, label, icon, iconColor, role = 'menuitem', checked, sel
|
|
|
4092
4133
|
const isOption = role === 'option';
|
|
4093
4134
|
const isHighlighted = isCheckable ? Boolean(checked) : Boolean(selected);
|
|
4094
4135
|
const { liRef, mergedRef, submenuOpen, handleClick, scheduleOpen, scheduleClose, clearTimers, closeSubmenu, } = useMenuItem({ ref, role, hasSubmenu, disabled, onClick, closeOnClick });
|
|
4095
|
-
return (jsxs(Fragment$1, { children: [jsxs("li", { ref: mergedRef, role: role, "aria-checked": isCheckable ? Boolean(checked) : undefined, "aria-selected": isOption ? Boolean(selected) : undefined, "aria-disabled": disabled, "aria-haspopup": hasSubmenu ? 'menu' : undefined, "aria-expanded": hasSubmenu ? submenuOpen : undefined, "data-selected": isHighlighted || undefined, "data-focused": focused || undefined, "data-disabled": disabled || undefined, className: MENU_ITEM_STYLES.root({ size }), onClick: handleClick, onMouseEnter: hasSubmenu && !disabled ? scheduleOpen : undefined, onMouseLeave: hasSubmenu ? scheduleClose : undefined, ...rest, children: [isCheckable && (jsxs("span", { className: MENU_ITEM_STYLES.indicator, "aria-hidden": true, children: [checked && role === 'menuitemcheckbox' && (jsx(Icon, { icon: CheckIcon, size: 'sm', strokeColor: 'primaryMain' })), checked && role === 'menuitemradio' && (jsx("span", { className: MENU_ITEM_STYLES.radioDot }))] })), icon !== undefined && (jsx(Icon, { icon: icon, size: 'sm', strokeColor: iconColor ?? (isHighlighted ? 'primaryMain' : 'textSecondary') })), jsx(Text, { variant: 'span', fontSize: 'sm', className: MENU_ITEM_STYLES.label, children: label }), hasSubmenu && (jsx("span", { className: MENU_ITEM_STYLES.submenuChevron, "aria-hidden": true, children: jsx(Icon, { icon: ChevronRightIcon, size: 'sm', strokeColor: 'textTertiary' }) }))] }), hasSubmenu && (jsx(MenuPanel, { open: submenuOpen, onClose: () => closeSubmenu(true), onArrowLeft: () => closeSubmenu(true), anchorEl: liRef.current, placement: submenuPlacement, isSubmenu: true, "aria-label": label, onMouseEnter: clearTimers, onMouseLeave: scheduleClose, children: submenu }))] }));
|
|
4136
|
+
return (jsxs(Fragment$1, { children: [jsxs("li", { ref: mergedRef, role: role, "aria-checked": isCheckable ? Boolean(checked) : undefined, "aria-selected": isOption ? Boolean(selected) : undefined, "aria-disabled": disabled, "aria-haspopup": hasSubmenu ? 'menu' : undefined, "aria-expanded": hasSubmenu ? submenuOpen : undefined, "data-selected": isHighlighted || undefined, "data-focused": focused || undefined, "data-disabled": disabled || undefined, className: MENU_ITEM_STYLES.root({ size }), onClick: handleClick, onMouseEnter: hasSubmenu && !disabled ? scheduleOpen : undefined, onMouseLeave: hasSubmenu ? scheduleClose : undefined, ...rest, children: [isCheckable && (jsxs("span", { className: MENU_ITEM_STYLES.indicator, "aria-hidden": true, children: [checked && role === 'menuitemcheckbox' && (jsx(Icon, { icon: CheckIcon, size: size === 'default' ? 'sm' : 'md', strokeColor: 'primaryMain' })), checked && role === 'menuitemradio' && (jsx("span", { className: MENU_ITEM_STYLES.radioDot }))] })), icon !== undefined && (jsx(Icon, { icon: icon, size: 'sm', strokeColor: iconColor ?? (isHighlighted ? 'primaryMain' : 'textSecondary') })), jsx(Text, { variant: 'span', fontSize: 'sm', className: MENU_ITEM_STYLES.label, children: label }), hasSubmenu && (jsx("span", { className: MENU_ITEM_STYLES.submenuChevron, "aria-hidden": true, children: jsx(Icon, { icon: ChevronRightIcon, size: 'sm', strokeColor: 'textTertiary' }) }))] }), hasSubmenu && (jsx(MenuPanel, { open: submenuOpen, onClose: () => closeSubmenu(true), onArrowLeft: () => closeSubmenu(true), anchorEl: liRef.current, placement: submenuPlacement, isSubmenu: true, "aria-label": label, onMouseEnter: clearTimers, onMouseLeave: scheduleClose, children: submenu }))] }));
|
|
4096
4137
|
};
|
|
4097
4138
|
MenuItem.displayName = 'MenuItem';
|
|
4098
4139
|
|