@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/cjs/index.js
CHANGED
|
@@ -3544,12 +3544,12 @@ const MENU_MIN_WIDTH_PX = 224;
|
|
|
3544
3544
|
* Computes and continuously updates the `position: fixed` style for a menu panel.
|
|
3545
3545
|
*
|
|
3546
3546
|
* Positioning strategy:
|
|
3547
|
-
* - `placement="bottom"` (default):
|
|
3548
|
-
*
|
|
3549
|
-
*
|
|
3550
|
-
* - `placement="
|
|
3551
|
-
*
|
|
3552
|
-
*
|
|
3547
|
+
* - `placement="bottom"` (default): opens below the anchor; overflows are shifted up.
|
|
3548
|
+
* - `placement="top"`: opens above the anchor; flips to bottom when no room.
|
|
3549
|
+
* - `placement="right"`: opens to the right of the anchor; flips to the left when no room.
|
|
3550
|
+
* - `placement="left"`: opens to the left of the anchor; flips to the right when no room.
|
|
3551
|
+
*
|
|
3552
|
+
* In all cases the vertical position is clamped inside the viewport.
|
|
3553
3553
|
*
|
|
3554
3554
|
* Scroll handling: the page scroll is locked while the menu is open (see
|
|
3555
3555
|
* `MenuPanel`), so the anchor cannot move out from under the menu. As a safety
|
|
@@ -3561,6 +3561,10 @@ const MENU_MIN_WIDTH_PX = 224;
|
|
|
3561
3561
|
*/
|
|
3562
3562
|
const useMenuPosition = ({ anchorEl, open, menuRef, minWidth, gap = 4, placement = 'bottom', }) => {
|
|
3563
3563
|
const [style, setStyle] = React.useState({});
|
|
3564
|
+
// Stays false until the rAF second pass has run with the real panel dimensions.
|
|
3565
|
+
// Keeps the panel invisible during the initial style={} state and the first
|
|
3566
|
+
// pass (menuHeight = 0) to prevent the position-jump flickering.
|
|
3567
|
+
const [isPositioned, setIsPositioned] = React.useState(false);
|
|
3564
3568
|
const computePosition = React.useCallback(() => {
|
|
3565
3569
|
if (!anchorEl) {
|
|
3566
3570
|
return;
|
|
@@ -3571,24 +3575,56 @@ const useMenuPosition = ({ anchorEl, open, menuRef, minWidth, gap = 4, placement
|
|
|
3571
3575
|
const menuWidth = menuEl?.offsetWidth ?? 0;
|
|
3572
3576
|
const viewportRight = window.innerWidth - VIEWPORT_MARGIN_PX;
|
|
3573
3577
|
const viewportBottom = window.innerHeight - VIEWPORT_MARGIN_PX;
|
|
3574
|
-
if (placement === 'right') {
|
|
3578
|
+
if (placement === 'right' || placement === 'left') {
|
|
3575
3579
|
// --- Vertical: align with the anchor top, clamp inside the viewport ---
|
|
3576
3580
|
let top = anchor.top;
|
|
3577
3581
|
if (menuHeight > 0 && top + menuHeight > viewportBottom) {
|
|
3578
3582
|
top = Math.max(VIEWPORT_MARGIN_PX, viewportBottom - menuHeight);
|
|
3579
3583
|
}
|
|
3580
|
-
// --- Horizontal: open to the
|
|
3581
|
-
let left
|
|
3582
|
-
if (
|
|
3583
|
-
|
|
3584
|
-
left
|
|
3585
|
-
|
|
3586
|
-
|
|
3584
|
+
// --- Horizontal: open to the preferred side, flip to the opposite when no room ---
|
|
3585
|
+
let left;
|
|
3586
|
+
if (placement === 'right') {
|
|
3587
|
+
left = anchor.right + gap;
|
|
3588
|
+
if (menuWidth > 0 && left + menuWidth > viewportRight) {
|
|
3589
|
+
const flippedLeft = anchor.left - gap - menuWidth;
|
|
3590
|
+
left = flippedLeft >= VIEWPORT_MARGIN_PX
|
|
3591
|
+
? flippedLeft
|
|
3592
|
+
: Math.max(VIEWPORT_MARGIN_PX, viewportRight - menuWidth);
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
else {
|
|
3596
|
+
// placement === 'left'
|
|
3597
|
+
left = anchor.left - gap - menuWidth;
|
|
3598
|
+
if (menuWidth > 0 && left < VIEWPORT_MARGIN_PX) {
|
|
3599
|
+
const flippedLeft = anchor.right + gap;
|
|
3600
|
+
left = flippedLeft + menuWidth <= viewportRight
|
|
3601
|
+
? flippedLeft
|
|
3602
|
+
: Math.max(VIEWPORT_MARGIN_PX, viewportRight - menuWidth);
|
|
3603
|
+
}
|
|
3587
3604
|
}
|
|
3588
3605
|
setStyle({ top, left, minWidth: minWidth ?? MENU_MIN_WIDTH_PX });
|
|
3589
3606
|
return;
|
|
3590
3607
|
}
|
|
3591
|
-
|
|
3608
|
+
if (placement === 'top') {
|
|
3609
|
+
// --- Vertical: open above, flip to bottom when no room ---
|
|
3610
|
+
let top = anchor.top - gap - menuHeight;
|
|
3611
|
+
if (menuHeight > 0 && top < VIEWPORT_MARGIN_PX) {
|
|
3612
|
+
const flippedTop = anchor.bottom + gap;
|
|
3613
|
+
top = flippedTop + menuHeight <= viewportBottom
|
|
3614
|
+
? flippedTop
|
|
3615
|
+
: Math.max(VIEWPORT_MARGIN_PX, viewportBottom - menuHeight);
|
|
3616
|
+
}
|
|
3617
|
+
// --- Horizontal ---
|
|
3618
|
+
const fallbackMinWidth = Math.max(anchor.width, MENU_MIN_WIDTH_PX);
|
|
3619
|
+
const resolvedMinWidth = typeof minWidth === 'number' ? minWidth : fallbackMinWidth;
|
|
3620
|
+
const effectiveWidth = Math.max(menuWidth, resolvedMinWidth);
|
|
3621
|
+
const maxLeft = window.innerWidth - effectiveWidth - VIEWPORT_MARGIN_PX;
|
|
3622
|
+
const left = Math.max(VIEWPORT_MARGIN_PX, Math.min(anchor.left, maxLeft));
|
|
3623
|
+
setStyle({ top, left, minWidth: minWidth ?? fallbackMinWidth });
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
// --- placement === 'bottom' ---
|
|
3627
|
+
// --- Vertical ---
|
|
3592
3628
|
const preferredTop = anchor.bottom + gap;
|
|
3593
3629
|
let top = preferredTop;
|
|
3594
3630
|
if (menuHeight > 0 && preferredTop + menuHeight > viewportBottom) {
|
|
@@ -3609,14 +3645,19 @@ const useMenuPosition = ({ anchorEl, open, menuRef, minWidth, gap = 4, placement
|
|
|
3609
3645
|
}
|
|
3610
3646
|
else {
|
|
3611
3647
|
setStyle({});
|
|
3648
|
+
setIsPositioned(false);
|
|
3612
3649
|
}
|
|
3613
3650
|
}, [open, computePosition]);
|
|
3614
|
-
// Second pass: recompute after the panel renders to get actual height/width
|
|
3651
|
+
// Second pass: recompute after the panel renders to get actual height/width,
|
|
3652
|
+
// then mark as positioned so the panel becomes visible.
|
|
3615
3653
|
React.useEffect(() => {
|
|
3616
3654
|
if (!open) {
|
|
3617
3655
|
return;
|
|
3618
3656
|
}
|
|
3619
|
-
const id = requestAnimationFrame(
|
|
3657
|
+
const id = requestAnimationFrame(() => {
|
|
3658
|
+
computePosition();
|
|
3659
|
+
setIsPositioned(true);
|
|
3660
|
+
});
|
|
3620
3661
|
return () => cancelAnimationFrame(id);
|
|
3621
3662
|
}, [open, computePosition]);
|
|
3622
3663
|
// Keep the menu glued to its anchor on scroll (nested containers) and resize.
|
|
@@ -3631,7 +3672,7 @@ const useMenuPosition = ({ anchorEl, open, menuRef, minWidth, gap = 4, placement
|
|
|
3631
3672
|
window.removeEventListener('resize', computePosition);
|
|
3632
3673
|
};
|
|
3633
3674
|
}, [open, computePosition]);
|
|
3634
|
-
return { style };
|
|
3675
|
+
return { style: { ...style, visibility: isPositioned ? 'visible' : 'hidden' } };
|
|
3635
3676
|
};
|
|
3636
3677
|
|
|
3637
3678
|
/** Custom DOM event fired on a submenu trigger item to request it opens. */
|
|
@@ -4112,7 +4153,7 @@ const MenuItem = ({ ref, label, icon, iconColor, role = 'menuitem', checked, sel
|
|
|
4112
4153
|
const isOption = role === 'option';
|
|
4113
4154
|
const isHighlighted = isCheckable ? Boolean(checked) : Boolean(selected);
|
|
4114
4155
|
const { liRef, mergedRef, submenuOpen, handleClick, scheduleOpen, scheduleClose, clearTimers, closeSubmenu, } = useMenuItem({ ref, role, hasSubmenu, disabled, onClick, closeOnClick });
|
|
4115
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.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 && (jsxRuntime.jsxs("span", { className: MENU_ITEM_STYLES.indicator, "aria-hidden": true, children: [checked && role === 'menuitemcheckbox' && (jsxRuntime.jsx(Icon, { icon: CheckIcon, size: 'sm', strokeColor: 'primaryMain' })), checked && role === 'menuitemradio' && (jsxRuntime.jsx("span", { className: MENU_ITEM_STYLES.radioDot }))] })), icon !== undefined && (jsxRuntime.jsx(Icon, { icon: icon, size: 'sm', strokeColor: iconColor ?? (isHighlighted ? 'primaryMain' : 'textSecondary') })), jsxRuntime.jsx(Text, { variant: 'span', fontSize: 'sm', className: MENU_ITEM_STYLES.label, children: label }), hasSubmenu && (jsxRuntime.jsx("span", { className: MENU_ITEM_STYLES.submenuChevron, "aria-hidden": true, children: jsxRuntime.jsx(Icon, { icon: ChevronRightIcon, size: 'sm', strokeColor: 'textTertiary' }) }))] }), hasSubmenu && (jsxRuntime.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 }))] }));
|
|
4156
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.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 && (jsxRuntime.jsxs("span", { className: MENU_ITEM_STYLES.indicator, "aria-hidden": true, children: [checked && role === 'menuitemcheckbox' && (jsxRuntime.jsx(Icon, { icon: CheckIcon, size: size === 'default' ? 'sm' : 'md', strokeColor: 'primaryMain' })), checked && role === 'menuitemradio' && (jsxRuntime.jsx("span", { className: MENU_ITEM_STYLES.radioDot }))] })), icon !== undefined && (jsxRuntime.jsx(Icon, { icon: icon, size: 'sm', strokeColor: iconColor ?? (isHighlighted ? 'primaryMain' : 'textSecondary') })), jsxRuntime.jsx(Text, { variant: 'span', fontSize: 'sm', className: MENU_ITEM_STYLES.label, children: label }), hasSubmenu && (jsxRuntime.jsx("span", { className: MENU_ITEM_STYLES.submenuChevron, "aria-hidden": true, children: jsxRuntime.jsx(Icon, { icon: ChevronRightIcon, size: 'sm', strokeColor: 'textTertiary' }) }))] }), hasSubmenu && (jsxRuntime.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 }))] }));
|
|
4116
4157
|
};
|
|
4117
4158
|
MenuItem.displayName = 'MenuItem';
|
|
4118
4159
|
|