@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/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): the menu opens below the anchor. If it would
3528
- * overflow the bottom of the viewport, `top` is shifted up so the menu bottom
3529
- * lands at `viewportHeight - margin` (never going off the top of the screen).
3530
- * - `placement="right"` (submenu): the menu opens to the right of the anchor and
3531
- * flips to the left when there is no horizontal room. Its `top` aligns with the
3532
- * anchor top and is clamped to the viewport.
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 right, flip to the left when no room ---
3561
- let left = anchor.right + gap;
3562
- if (menuWidth > 0 && left + menuWidth > viewportRight) {
3563
- const flippedLeft = anchor.left - gap - menuWidth;
3564
- left = flippedLeft >= VIEWPORT_MARGIN_PX
3565
- ? flippedLeft
3566
- : Math.max(VIEWPORT_MARGIN_PX, viewportRight - menuWidth);
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
- // --- Vertical (bottom) ---
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(computePosition);
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