@aurora-ds/components 1.7.16 → 1.7.18

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 CHANGED
@@ -3665,7 +3665,9 @@ const useMenuPanel = ({ open, onClose, anchorEl, minWidth, placement = 'bottom',
3665
3665
  }
3666
3666
  return Array.from(panelRef.current.querySelectorAll('[role^="menuitem"]:not([data-disabled]), [role="option"]:not([data-disabled])')).filter((el) => el.closest('[data-menu-panel]') === panelRef.current);
3667
3667
  }, []);
3668
- // On open: focus panel, initialise focusedIndex to checked/selected item (or 0)
3668
+ // On open: focus panel, pre-select a checked/selected item if any.
3669
+ // When nothing is pre-selected, leave focusedIndex at -1 so no item appears
3670
+ // highlighted on mouse-open — the first ArrowDown/Up will start navigation.
3669
3671
  React.useEffect(() => {
3670
3672
  if (!open) {
3671
3673
  setFocusedIndex(-1);
@@ -3675,7 +3677,7 @@ const useMenuPanel = ({ open, onClose, anchorEl, minWidth, placement = 'bottom',
3675
3677
  panelRef.current?.focus();
3676
3678
  const options = getOptions();
3677
3679
  const selectedIdx = options.findIndex((el) => el.getAttribute('aria-checked') === 'true' || el.getAttribute('data-selected') !== null);
3678
- setFocusedIndex(selectedIdx >= 0 ? selectedIdx : 0);
3680
+ setFocusedIndex(selectedIdx >= 0 ? selectedIdx : -1);
3679
3681
  });
3680
3682
  return () => cancelAnimationFrame(raf);
3681
3683
  }, [open, getOptions]);
@@ -3720,7 +3722,11 @@ const useMenuPanel = ({ open, onClose, anchorEl, minWidth, placement = 'bottom',
3720
3722
  e.preventDefault();
3721
3723
  setFocusedIndex((prev) => {
3722
3724
  const count = getOptions().length;
3723
- return count === 0 ? prev : (prev - 1 + count) % count;
3725
+ if (count === 0) {
3726
+ return prev;
3727
+ }
3728
+ // prev === -1 means no item focused yet → jump to last item
3729
+ return prev <= 0 ? count - 1 : prev - 1;
3724
3730
  });
3725
3731
  },
3726
3732
  ArrowRight: (e) => {
@@ -3769,33 +3775,90 @@ const useMenuPanel = ({ open, onClose, anchorEl, minWidth, placement = 'bottom',
3769
3775
  return { panelRef, style, activeDescendant, setChildOpen };
3770
3776
  };
3771
3777
 
3778
+ /** Returns the visible scrollbar width (0 with overlay/inset scrollbars like macOS). */
3779
+ const getScrollbarWidth = () => window.innerWidth - document.documentElement.clientWidth;
3772
3780
  /**
3773
- * Locks scrolling on `document.body` while `active` is true.
3781
+ * Returns every element that is currently scrollable, skipping entire subtrees
3782
+ * rooted at `[data-scroll-lock-ignore]` (overlays: menus, dialogs, drawers…).
3783
+ *
3784
+ * Uses a TreeWalker so ignored subtrees are never visited at all.
3785
+ * `getComputedStyle` is called only for elements whose scroll dimensions already
3786
+ * confirm overflow — avoiding expensive style recalculation for every node.
3787
+ *
3788
+ * `<html>` is checked separately; its computed `overflow` is often `'visible'`
3789
+ * even when the full page scrolls.
3790
+ */
3791
+ const getScrollableElements = () => {
3792
+ const scrollable = [];
3793
+ // Document-level scroll lives on <html>.
3794
+ if (document.documentElement.scrollHeight > window.innerHeight) {
3795
+ scrollable.push(document.documentElement);
3796
+ }
3797
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, {
3798
+ acceptNode(node) {
3799
+ const el = node;
3800
+ // Reject entire overlay subtrees in one step (menus, dialogs, drawers…).
3801
+ if (el.hasAttribute('data-scroll-lock-ignore')) {
3802
+ return NodeFilter.FILTER_REJECT;
3803
+ }
3804
+ // Cheap dimension pre-check — skips getComputedStyle for most nodes.
3805
+ const mayScrollY = el.scrollHeight > el.clientHeight;
3806
+ const mayScrollX = el.scrollWidth > el.clientWidth;
3807
+ if (!mayScrollY && !mayScrollX) {
3808
+ return NodeFilter.FILTER_SKIP;
3809
+ }
3810
+ // Confirm that overflow is actually set to scroll/auto.
3811
+ const { overflowY, overflowX } = window.getComputedStyle(el);
3812
+ const scrollsY = mayScrollY && (overflowY === 'scroll' || overflowY === 'auto');
3813
+ const scrollsX = mayScrollX && (overflowX === 'scroll' || overflowX === 'auto');
3814
+ return scrollsY || scrollsX ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
3815
+ },
3816
+ });
3817
+ let node;
3818
+ while ((node = walker.nextNode())) {
3819
+ scrollable.push(node);
3820
+ }
3821
+ return scrollable;
3822
+ };
3823
+ /**
3824
+ * Locks **all** currently scrollable elements while `active` is true.
3825
+ *
3826
+ * Instead of assuming the scroll lives on `document.body`, this hook discovers
3827
+ * every element with real overflow and freezes it with `overflow: hidden`.
3828
+ * Elements marked `data-scroll-lock-ignore` — and their entire subtrees — are
3829
+ * skipped so overlays (menus, dialogs, drawers…) remain scrollable internally.
3774
3830
  *
3775
- * Preserves the current scroll position and keeps the scrollbar gutter
3776
- * (`overflow-y: scroll`) to avoid horizontal layout shift when the scrollbar
3777
- * disappears. The original styles and scroll position are restored on cleanup.
3831
+ * For `<html>`, `paddingRight` is compensated when a native scrollbar is present
3832
+ * to prevent horizontal layout shift.
3778
3833
  *
3779
- * @example useBodyScrollLock(isDialogOpen)
3834
+ * All original styles are restored on cleanup.
3835
+ *
3836
+ * @example useBodyScrollLock(isMenuOpen)
3780
3837
  */
3781
3838
  const useBodyScrollLock = (active) => {
3782
3839
  React.useEffect(() => {
3783
3840
  if (!active) {
3784
3841
  return;
3785
3842
  }
3786
- const scrollY = window.scrollY;
3787
- const body = document.body;
3788
- body.style.position = 'fixed';
3789
- body.style.top = `-${scrollY}px`;
3790
- body.style.overflowY = 'scroll';
3791
- body.style.width = '100%';
3792
- return () => {
3793
- body.style.position = '';
3794
- body.style.top = '';
3795
- body.style.overflowY = '';
3796
- body.style.width = '';
3797
- window.scrollTo(0, scrollY);
3798
- };
3843
+ const scrollbarWidth = getScrollbarWidth();
3844
+ const elements = getScrollableElements();
3845
+ const restorers = elements.map((el) => {
3846
+ const savedOverflow = el.style.overflow;
3847
+ const savedPaddingRight = el.style.paddingRight;
3848
+ el.style.overflow = 'hidden';
3849
+ // Preserve the scrollbar gutter on the document root.
3850
+ if (el === document.documentElement && scrollbarWidth > 0) {
3851
+ const currentPadding = parseFloat(window.getComputedStyle(el).paddingRight) || 0;
3852
+ el.style.paddingRight = `${currentPadding + scrollbarWidth}px`;
3853
+ }
3854
+ return () => {
3855
+ el.style.overflow = savedOverflow;
3856
+ if (el === document.documentElement && scrollbarWidth > 0) {
3857
+ el.style.paddingRight = savedPaddingRight;
3858
+ }
3859
+ };
3860
+ });
3861
+ return () => restorers.forEach((restore) => restore());
3799
3862
  }, [active]);
3800
3863
  };
3801
3864
 
@@ -3828,7 +3891,7 @@ const MenuPanel = ({ open, onClose, anchorEl, minWidth, maxHeight = '20rem', pla
3828
3891
  if (!open) {
3829
3892
  return null;
3830
3893
  }
3831
- return reactDom.createPortal(jsxRuntime.jsxs(MenuContext.Provider, { value: contextValue, children: [!isSubmenu && (jsxRuntime.jsx("div", { className: MENU_PANEL_STYLES.backdrop, onClick: onClose, "aria-hidden": true })), jsxRuntime.jsx("div", { ref: panelRef, id: id, role: role, tabIndex: -1, "data-menu-panel": true, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-activedescendant": activeDescendant, className: MENU_PANEL_STYLES.panel, style: { ...style, maxHeight, outline: 'none' }, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, children: children })] }), document.body);
3894
+ return reactDom.createPortal(jsxRuntime.jsxs(MenuContext.Provider, { value: contextValue, children: [!isSubmenu && (jsxRuntime.jsx("div", { className: MENU_PANEL_STYLES.backdrop, onClick: onClose, "aria-hidden": true })), jsxRuntime.jsx("div", { ref: panelRef, id: id, role: role, tabIndex: -1, "data-menu-panel": true, "data-scroll-lock-ignore": true, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-activedescendant": activeDescendant, className: MENU_PANEL_STYLES.panel, style: { ...style, maxHeight, outline: 'none' }, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, children: children })] }), document.body);
3832
3895
  };
3833
3896
  MenuPanel.displayName = 'MenuPanel';
3834
3897
 
@@ -3975,14 +4038,15 @@ const useMenuItem = ({ ref, role, hasSubmenu, disabled, onClick, closeOnClick, }
3975
4038
  const MENU_ITEM_STYLES = theme.createStyles((theme) => {
3976
4039
  const c = theme.colors;
3977
4040
  return {
3978
- root: {
4041
+ root: ({ size }) => ({
3979
4042
  display: 'flex',
3980
4043
  alignItems: 'center',
3981
4044
  gap: theme.spacing.sm,
3982
- paddingTop: theme.spacing.xs,
3983
- paddingBottom: theme.spacing.xs,
3984
4045
  paddingLeft: theme.spacing.md,
3985
4046
  paddingRight: theme.spacing.md,
4047
+ ...(size === 'default'
4048
+ ? { height: DEFAULT_DRAWER_ITEM_SIZE }
4049
+ : { paddingTop: theme.spacing.xs, paddingBottom: theme.spacing.xs }),
3986
4050
  cursor: 'pointer',
3987
4051
  userSelect: 'none',
3988
4052
  color: c.textPrimary,
@@ -4010,7 +4074,7 @@ const MENU_ITEM_STYLES = theme.createStyles((theme) => {
4010
4074
  ':active:not([data-disabled])': {
4011
4075
  backgroundColor: c.defaultSubtleActive,
4012
4076
  },
4013
- },
4077
+ }),
4014
4078
  /** Fixed-width leading slot keeping labels aligned for checkbox/radio items. */
4015
4079
  indicator: {
4016
4080
  display: 'inline-flex',
@@ -4042,13 +4106,13 @@ const MENU_ITEM_STYLES = theme.createStyles((theme) => {
4042
4106
  };
4043
4107
  }, { id: 'menu-item' });
4044
4108
 
4045
- const MenuItem = ({ ref, label, icon, iconColor, role = 'menuitem', checked, selected, focused, disabled, closeOnClick, submenu, submenuPlacement = 'right', onClick, ...rest }) => {
4109
+ const MenuItem = ({ ref, label, icon, iconColor, role = 'menuitem', checked, selected, focused, disabled, size = 'default', closeOnClick, submenu, submenuPlacement = 'right', onClick, ...rest }) => {
4046
4110
  const hasSubmenu = submenu !== undefined;
4047
4111
  const isCheckable = role === 'menuitemcheckbox' || role === 'menuitemradio';
4048
4112
  const isOption = role === 'option';
4049
4113
  const isHighlighted = isCheckable ? Boolean(checked) : Boolean(selected);
4050
4114
  const { liRef, mergedRef, submenuOpen, handleClick, scheduleOpen, scheduleClose, clearTimers, closeSubmenu, } = useMenuItem({ ref, role, hasSubmenu, disabled, onClick, closeOnClick });
4051
- 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, 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 }))] }));
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 }))] }));
4052
4116
  };
4053
4117
  MenuItem.displayName = 'MenuItem';
4054
4118
 
@@ -4243,7 +4307,7 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled,
4243
4307
  const Select = ({ ref, value, defaultValue, onChange, options, label, helperText, placeholder, size = 'md', status = 'default', disabled, required, width, id, }) => {
4244
4308
  const { fieldId, labelId, helperId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect, } = useSelect({ id, ref, value, defaultValue, onChange, options, disabled });
4245
4309
  return (jsxRuntime.jsxs(Stack, { flexDirection: 'column', gap: 'xs', style: { width: width ?? '100%' }, children: [label !== undefined && (jsxRuntime.jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, id: labelId, children: [label, required && (jsxRuntime.jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsxRuntime.jsx(SelectTrigger, { ref: mergedRef, id: fieldId, size: size, status: status, open: open, hasValue: selectedOption !== undefined, startIcon: selectedOption?.icon, startIconColor: selectedOption?.iconColor, disabled: disabled, "aria-haspopup": 'listbox', "aria-expanded": open, "aria-controls": menuId, "aria-labelledby": label !== undefined ? `${labelId} ${fieldId}` : undefined, "aria-required": required || undefined, "aria-invalid": status === 'error' || undefined, "aria-errormessage": status === 'error' && helperText !== undefined ? helperId : undefined, "aria-describedby": helperText !== undefined ? helperId : undefined, onClick: toggle, children: selectedOption !== undefined ? selectedOption.label : placeholder }), jsxRuntime.jsx(Menu, { open: open, onClose: close, anchorEl: triggerRef.current, id: menuId, role: 'listbox', "aria-labelledby": label !== undefined ? labelId : undefined, "aria-label": label === undefined ? placeholder : undefined, children: Array.from(groupedOptions.entries()).map(([groupKey, groupOpts], groupIndex) => {
4246
- const items = groupOpts.map((opt) => (jsxRuntime.jsx(Menu.Item, { role: 'option', label: opt.label, icon: opt.icon, iconColor: opt.iconColor, selected: opt.value === currentValue, disabled: opt.disabled, onClick: () => handleSelect(opt.value) }, opt.value)));
4310
+ const items = groupOpts.map((opt) => (jsxRuntime.jsx(Menu.Item, { role: 'option', size: 'compact', label: opt.label, icon: opt.icon, iconColor: opt.iconColor, selected: opt.value === currentValue, disabled: opt.disabled, onClick: () => handleSelect(opt.value) }, opt.value)));
4247
4311
  return groupKey !== undefined ? (jsxRuntime.jsx(Menu.Group, { label: groupKey, divider: groupIndex > 0, children: items }, groupKey)) : (jsxRuntime.jsx(Menu.Group, { divider: groupIndex > 0, children: items }, '__ungrouped'));
4248
4312
  }) }), helperText !== undefined && (jsxRuntime.jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
4249
4313
  };
@@ -5961,7 +6025,7 @@ const DrawerTemporaryPanel = ({ isExpanded, onClose, children, role, ariaLabel,
5961
6025
  if (!isVisible) {
5962
6026
  return null;
5963
6027
  }
5964
- return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Backdrop, { visible: isFadingIn, onClick: onClose }), jsxRuntime.jsx(DrawerContext.Provider, { value: { isExpanded: true }, children: jsxRuntime.jsx("nav", { className: theme.cx(DRAWER_STYLES.temporaryPanel, isFadingIn && DRAWER_STYLES.temporaryPanelVisible), role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, children: children }) })] }), document.body);
6028
+ return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Backdrop, { visible: isFadingIn, onClick: onClose }), jsxRuntime.jsx(DrawerContext.Provider, { value: { isExpanded: true }, children: jsxRuntime.jsx("nav", { className: theme.cx(DRAWER_STYLES.temporaryPanel, isFadingIn && DRAWER_STYLES.temporaryPanelVisible), role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "data-scroll-lock-ignore": true, children: children }) })] }), document.body);
5965
6029
  };
5966
6030
  DrawerTemporaryPanel.displayName = 'DrawerTemporaryPanel';
5967
6031
  // ─── Main Drawer ─────────────────────────────────────────────────────────────
@@ -6870,7 +6934,7 @@ const DialogBase = ({ open, onClose, children, closeOnBackdropClick = false, ful
6870
6934
  if (!isVisible) {
6871
6935
  return null;
6872
6936
  }
6873
- return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Backdrop, { visible: isFadingIn, onClick: handleBackdropClick }), jsxRuntime.jsx("div", { ref: panelRef, role: 'dialog', "aria-modal": true, "aria-labelledby": labelledBy, "aria-label": ariaLabel, tabIndex: -1, className: theme.cx(DIALOG_STYLES.panel, isFadingIn && DIALOG_STYLES.panelVisible, fullscreen && DIALOG_STYLES.panelFullscreen), style: cssVars, children: jsxRuntime.jsx(DialogContext.Provider, { value: { titleId, CloseIconComponent: CloseIcon }, children: children }) })] }), document.body);
6937
+ return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Backdrop, { visible: isFadingIn, onClick: handleBackdropClick }), jsxRuntime.jsx("div", { ref: panelRef, role: 'dialog', "aria-modal": true, "aria-labelledby": labelledBy, "aria-label": ariaLabel, tabIndex: -1, "data-scroll-lock-ignore": true, className: theme.cx(DIALOG_STYLES.panel, isFadingIn && DIALOG_STYLES.panelVisible, fullscreen && DIALOG_STYLES.panelFullscreen), style: cssVars, children: jsxRuntime.jsx(DialogContext.Provider, { value: { titleId, CloseIconComponent: CloseIcon }, children: children }) })] }), document.body);
6874
6938
  };
6875
6939
  DialogBase.displayName = 'Dialog';
6876
6940
  const Dialog = DialogBase;