@aurora-ds/components 1.7.16 → 1.7.17

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
@@ -3749,33 +3749,90 @@ const useMenuPanel = ({ open, onClose, anchorEl, minWidth, placement = 'bottom',
3749
3749
  return { panelRef, style, activeDescendant, setChildOpen };
3750
3750
  };
3751
3751
 
3752
+ /** Returns the visible scrollbar width (0 with overlay/inset scrollbars like macOS). */
3753
+ const getScrollbarWidth = () => window.innerWidth - document.documentElement.clientWidth;
3752
3754
  /**
3753
- * Locks scrolling on `document.body` while `active` is true.
3755
+ * Returns every element that is currently scrollable, skipping entire subtrees
3756
+ * rooted at `[data-scroll-lock-ignore]` (overlays: menus, dialogs, drawers…).
3754
3757
  *
3755
- * Preserves the current scroll position and keeps the scrollbar gutter
3756
- * (`overflow-y: scroll`) to avoid horizontal layout shift when the scrollbar
3757
- * disappears. The original styles and scroll position are restored on cleanup.
3758
+ * Uses a TreeWalker so ignored subtrees are never visited at all.
3759
+ * `getComputedStyle` is called only for elements whose scroll dimensions already
3760
+ * confirm overflow avoiding expensive style recalculation for every node.
3758
3761
  *
3759
- * @example useBodyScrollLock(isDialogOpen)
3762
+ * `<html>` is checked separately; its computed `overflow` is often `'visible'`
3763
+ * even when the full page scrolls.
3764
+ */
3765
+ const getScrollableElements = () => {
3766
+ const scrollable = [];
3767
+ // Document-level scroll lives on <html>.
3768
+ if (document.documentElement.scrollHeight > window.innerHeight) {
3769
+ scrollable.push(document.documentElement);
3770
+ }
3771
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, {
3772
+ acceptNode(node) {
3773
+ const el = node;
3774
+ // Reject entire overlay subtrees in one step (menus, dialogs, drawers…).
3775
+ if (el.hasAttribute('data-scroll-lock-ignore')) {
3776
+ return NodeFilter.FILTER_REJECT;
3777
+ }
3778
+ // Cheap dimension pre-check — skips getComputedStyle for most nodes.
3779
+ const mayScrollY = el.scrollHeight > el.clientHeight;
3780
+ const mayScrollX = el.scrollWidth > el.clientWidth;
3781
+ if (!mayScrollY && !mayScrollX) {
3782
+ return NodeFilter.FILTER_SKIP;
3783
+ }
3784
+ // Confirm that overflow is actually set to scroll/auto.
3785
+ const { overflowY, overflowX } = window.getComputedStyle(el);
3786
+ const scrollsY = mayScrollY && (overflowY === 'scroll' || overflowY === 'auto');
3787
+ const scrollsX = mayScrollX && (overflowX === 'scroll' || overflowX === 'auto');
3788
+ return scrollsY || scrollsX ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
3789
+ },
3790
+ });
3791
+ let node;
3792
+ while ((node = walker.nextNode())) {
3793
+ scrollable.push(node);
3794
+ }
3795
+ return scrollable;
3796
+ };
3797
+ /**
3798
+ * Locks **all** currently scrollable elements while `active` is true.
3799
+ *
3800
+ * Instead of assuming the scroll lives on `document.body`, this hook discovers
3801
+ * every element with real overflow and freezes it with `overflow: hidden`.
3802
+ * Elements marked `data-scroll-lock-ignore` — and their entire subtrees — are
3803
+ * skipped so overlays (menus, dialogs, drawers…) remain scrollable internally.
3804
+ *
3805
+ * For `<html>`, `paddingRight` is compensated when a native scrollbar is present
3806
+ * to prevent horizontal layout shift.
3807
+ *
3808
+ * All original styles are restored on cleanup.
3809
+ *
3810
+ * @example useBodyScrollLock(isMenuOpen)
3760
3811
  */
3761
3812
  const useBodyScrollLock = (active) => {
3762
3813
  useEffect(() => {
3763
3814
  if (!active) {
3764
3815
  return;
3765
3816
  }
3766
- const scrollY = window.scrollY;
3767
- const body = document.body;
3768
- body.style.position = 'fixed';
3769
- body.style.top = `-${scrollY}px`;
3770
- body.style.overflowY = 'scroll';
3771
- body.style.width = '100%';
3772
- return () => {
3773
- body.style.position = '';
3774
- body.style.top = '';
3775
- body.style.overflowY = '';
3776
- body.style.width = '';
3777
- window.scrollTo(0, scrollY);
3778
- };
3817
+ const scrollbarWidth = getScrollbarWidth();
3818
+ const elements = getScrollableElements();
3819
+ const restorers = elements.map((el) => {
3820
+ const savedOverflow = el.style.overflow;
3821
+ const savedPaddingRight = el.style.paddingRight;
3822
+ el.style.overflow = 'hidden';
3823
+ // Preserve the scrollbar gutter on the document root.
3824
+ if (el === document.documentElement && scrollbarWidth > 0) {
3825
+ const currentPadding = parseFloat(window.getComputedStyle(el).paddingRight) || 0;
3826
+ el.style.paddingRight = `${currentPadding + scrollbarWidth}px`;
3827
+ }
3828
+ return () => {
3829
+ el.style.overflow = savedOverflow;
3830
+ if (el === document.documentElement && scrollbarWidth > 0) {
3831
+ el.style.paddingRight = savedPaddingRight;
3832
+ }
3833
+ };
3834
+ });
3835
+ return () => restorers.forEach((restore) => restore());
3779
3836
  }, [active]);
3780
3837
  };
3781
3838
 
@@ -3808,7 +3865,7 @@ const MenuPanel = ({ open, onClose, anchorEl, minWidth, maxHeight = '20rem', pla
3808
3865
  if (!open) {
3809
3866
  return null;
3810
3867
  }
3811
- return createPortal(jsxs(MenuContext.Provider, { value: contextValue, children: [!isSubmenu && (jsx("div", { className: MENU_PANEL_STYLES.backdrop, onClick: onClose, "aria-hidden": true })), 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);
3868
+ return createPortal(jsxs(MenuContext.Provider, { value: contextValue, children: [!isSubmenu && (jsx("div", { className: MENU_PANEL_STYLES.backdrop, onClick: onClose, "aria-hidden": true })), 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);
3812
3869
  };
3813
3870
  MenuPanel.displayName = 'MenuPanel';
3814
3871
 
@@ -3955,14 +4012,15 @@ const useMenuItem = ({ ref, role, hasSubmenu, disabled, onClick, closeOnClick, }
3955
4012
  const MENU_ITEM_STYLES = createStyles((theme) => {
3956
4013
  const c = theme.colors;
3957
4014
  return {
3958
- root: {
4015
+ root: ({ size }) => ({
3959
4016
  display: 'flex',
3960
4017
  alignItems: 'center',
3961
4018
  gap: theme.spacing.sm,
3962
- paddingTop: theme.spacing.xs,
3963
- paddingBottom: theme.spacing.xs,
3964
4019
  paddingLeft: theme.spacing.md,
3965
4020
  paddingRight: theme.spacing.md,
4021
+ ...(size === 'default'
4022
+ ? { height: DEFAULT_DRAWER_ITEM_SIZE }
4023
+ : { paddingTop: theme.spacing.xs, paddingBottom: theme.spacing.xs }),
3966
4024
  cursor: 'pointer',
3967
4025
  userSelect: 'none',
3968
4026
  color: c.textPrimary,
@@ -3990,7 +4048,7 @@ const MENU_ITEM_STYLES = createStyles((theme) => {
3990
4048
  ':active:not([data-disabled])': {
3991
4049
  backgroundColor: c.defaultSubtleActive,
3992
4050
  },
3993
- },
4051
+ }),
3994
4052
  /** Fixed-width leading slot keeping labels aligned for checkbox/radio items. */
3995
4053
  indicator: {
3996
4054
  display: 'inline-flex',
@@ -4022,13 +4080,13 @@ const MENU_ITEM_STYLES = createStyles((theme) => {
4022
4080
  };
4023
4081
  }, { id: 'menu-item' });
4024
4082
 
4025
- const MenuItem = ({ ref, label, icon, iconColor, role = 'menuitem', checked, selected, focused, disabled, closeOnClick, submenu, submenuPlacement = 'right', onClick, ...rest }) => {
4083
+ const MenuItem = ({ ref, label, icon, iconColor, role = 'menuitem', checked, selected, focused, disabled, size = 'default', closeOnClick, submenu, submenuPlacement = 'right', onClick, ...rest }) => {
4026
4084
  const hasSubmenu = submenu !== undefined;
4027
4085
  const isCheckable = role === 'menuitemcheckbox' || role === 'menuitemradio';
4028
4086
  const isOption = role === 'option';
4029
4087
  const isHighlighted = isCheckable ? Boolean(checked) : Boolean(selected);
4030
4088
  const { liRef, mergedRef, submenuOpen, handleClick, scheduleOpen, scheduleClose, clearTimers, closeSubmenu, } = useMenuItem({ ref, role, hasSubmenu, disabled, onClick, closeOnClick });
4031
- 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, 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 }))] }));
4089
+ 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 }))] }));
4032
4090
  };
4033
4091
  MenuItem.displayName = 'MenuItem';
4034
4092
 
@@ -4223,7 +4281,7 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled,
4223
4281
  const Select = ({ ref, value, defaultValue, onChange, options, label, helperText, placeholder, size = 'md', status = 'default', disabled, required, width, id, }) => {
4224
4282
  const { fieldId, labelId, helperId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect, } = useSelect({ id, ref, value, defaultValue, onChange, options, disabled });
4225
4283
  return (jsxs(Stack, { flexDirection: 'column', gap: 'xs', style: { width: width ?? '100%' }, children: [label !== undefined && (jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, id: labelId, children: [label, required && (jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), 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 }), 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) => {
4226
- const items = groupOpts.map((opt) => (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)));
4284
+ const items = groupOpts.map((opt) => (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)));
4227
4285
  return groupKey !== undefined ? (jsx(Menu.Group, { label: groupKey, divider: groupIndex > 0, children: items }, groupKey)) : (jsx(Menu.Group, { divider: groupIndex > 0, children: items }, '__ungrouped'));
4228
4286
  }) }), helperText !== undefined && (jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
4229
4287
  };
@@ -5941,7 +5999,7 @@ const DrawerTemporaryPanel = ({ isExpanded, onClose, children, role, ariaLabel,
5941
5999
  if (!isVisible) {
5942
6000
  return null;
5943
6001
  }
5944
- return createPortal(jsxs(Fragment$1, { children: [jsx(Backdrop, { visible: isFadingIn, onClick: onClose }), jsx(DrawerContext.Provider, { value: { isExpanded: true }, children: jsx("nav", { className: cx(DRAWER_STYLES.temporaryPanel, isFadingIn && DRAWER_STYLES.temporaryPanelVisible), role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, children: children }) })] }), document.body);
6002
+ return createPortal(jsxs(Fragment$1, { children: [jsx(Backdrop, { visible: isFadingIn, onClick: onClose }), jsx(DrawerContext.Provider, { value: { isExpanded: true }, children: jsx("nav", { className: 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);
5945
6003
  };
5946
6004
  DrawerTemporaryPanel.displayName = 'DrawerTemporaryPanel';
5947
6005
  // ─── Main Drawer ─────────────────────────────────────────────────────────────
@@ -6850,7 +6908,7 @@ const DialogBase = ({ open, onClose, children, closeOnBackdropClick = false, ful
6850
6908
  if (!isVisible) {
6851
6909
  return null;
6852
6910
  }
6853
- return createPortal(jsxs(Fragment$1, { children: [jsx(Backdrop, { visible: isFadingIn, onClick: handleBackdropClick }), jsx("div", { ref: panelRef, role: 'dialog', "aria-modal": true, "aria-labelledby": labelledBy, "aria-label": ariaLabel, tabIndex: -1, className: cx(DIALOG_STYLES.panel, isFadingIn && DIALOG_STYLES.panelVisible, fullscreen && DIALOG_STYLES.panelFullscreen), style: cssVars, children: jsx(DialogContext.Provider, { value: { titleId, CloseIconComponent: CloseIcon }, children: children }) })] }), document.body);
6911
+ return createPortal(jsxs(Fragment$1, { children: [jsx(Backdrop, { visible: isFadingIn, onClick: handleBackdropClick }), jsx("div", { ref: panelRef, role: 'dialog', "aria-modal": true, "aria-labelledby": labelledBy, "aria-label": ariaLabel, tabIndex: -1, "data-scroll-lock-ignore": true, className: cx(DIALOG_STYLES.panel, isFadingIn && DIALOG_STYLES.panelVisible, fullscreen && DIALOG_STYLES.panelFullscreen), style: cssVars, children: jsx(DialogContext.Provider, { value: { titleId, CloseIconComponent: CloseIcon }, children: children }) })] }), document.body);
6854
6912
  };
6855
6913
  DialogBase.displayName = 'Dialog';
6856
6914
  const Dialog = DialogBase;