@aurora-ds/components 1.7.15 → 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/cjs/index.js CHANGED
@@ -1524,7 +1524,7 @@ const getAvatarInitials = (name) => {
1524
1524
  * <Avatar src="/me.jpg" name="Me" onClick={() => openProfile()} ariaLabel="Open profile" />
1525
1525
  * ```
1526
1526
  */
1527
- const Avatar = ({ src, name, initials, size = 'md', shape = 'circle', color = 'primary', onClick, ariaLabel, }) => {
1527
+ const Avatar = ({ src, name, initials, size = 'md', shape = 'circle', color = 'primary', onClick, ariaLabel, ref, id, 'aria-haspopup': ariaHasPopup, 'aria-expanded': ariaExpanded, 'aria-controls': ariaControls, }) => {
1528
1528
  const [imageError, setImageError] = React.useState(false);
1529
1529
  const isInteractive = Boolean(onClick);
1530
1530
  const resolvedInitials = initials ?? (name ? getAvatarInitials(name) : undefined);
@@ -1543,9 +1543,9 @@ const Avatar = ({ src, name, initials, size = 'md', shape = 'circle', color = 'p
1543
1543
  ? jsxRuntime.jsx("span", { "aria-hidden": true, children: resolvedInitials })
1544
1544
  : null);
1545
1545
  if (isInteractive) {
1546
- return (jsxRuntime.jsx("div", { role: 'button', tabIndex: 0, "aria-label": resolvedAriaLabel, title: resolvedAriaLabel, className: AVATAR_STYLES.root({ size, shape, color, isInteractive: true }), onClick: onClick, onKeyDown: handleKeyDown, children: avatarContent }));
1546
+ return (jsxRuntime.jsx("div", { ref: ref, id: id, role: 'button', tabIndex: 0, "aria-label": resolvedAriaLabel, "aria-haspopup": ariaHasPopup, "aria-expanded": ariaExpanded, "aria-controls": ariaControls, title: resolvedAriaLabel, className: AVATAR_STYLES.root({ size, shape, color, isInteractive: true }), onClick: onClick, onKeyDown: handleKeyDown, children: avatarContent }));
1547
1547
  }
1548
- return (jsxRuntime.jsx("div", { role: 'img', "aria-label": resolvedAriaLabel, title: resolvedAriaLabel, className: AVATAR_STYLES.root({ size, shape, color, isInteractive: false }), children: avatarContent }));
1548
+ return (jsxRuntime.jsx("div", { ref: ref, id: id, role: 'img', "aria-label": resolvedAriaLabel, title: resolvedAriaLabel, className: AVATAR_STYLES.root({ size, shape, color, isInteractive: false }), children: avatarContent }));
1549
1549
  };
1550
1550
  Avatar.displayName = 'Avatar';
1551
1551
 
@@ -3769,33 +3769,90 @@ const useMenuPanel = ({ open, onClose, anchorEl, minWidth, placement = 'bottom',
3769
3769
  return { panelRef, style, activeDescendant, setChildOpen };
3770
3770
  };
3771
3771
 
3772
+ /** Returns the visible scrollbar width (0 with overlay/inset scrollbars like macOS). */
3773
+ const getScrollbarWidth = () => window.innerWidth - document.documentElement.clientWidth;
3772
3774
  /**
3773
- * Locks scrolling on `document.body` while `active` is true.
3775
+ * Returns every element that is currently scrollable, skipping entire subtrees
3776
+ * rooted at `[data-scroll-lock-ignore]` (overlays: menus, dialogs, drawers…).
3774
3777
  *
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.
3778
+ * Uses a TreeWalker so ignored subtrees are never visited at all.
3779
+ * `getComputedStyle` is called only for elements whose scroll dimensions already
3780
+ * confirm overflow avoiding expensive style recalculation for every node.
3778
3781
  *
3779
- * @example useBodyScrollLock(isDialogOpen)
3782
+ * `<html>` is checked separately; its computed `overflow` is often `'visible'`
3783
+ * even when the full page scrolls.
3784
+ */
3785
+ const getScrollableElements = () => {
3786
+ const scrollable = [];
3787
+ // Document-level scroll lives on <html>.
3788
+ if (document.documentElement.scrollHeight > window.innerHeight) {
3789
+ scrollable.push(document.documentElement);
3790
+ }
3791
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, {
3792
+ acceptNode(node) {
3793
+ const el = node;
3794
+ // Reject entire overlay subtrees in one step (menus, dialogs, drawers…).
3795
+ if (el.hasAttribute('data-scroll-lock-ignore')) {
3796
+ return NodeFilter.FILTER_REJECT;
3797
+ }
3798
+ // Cheap dimension pre-check — skips getComputedStyle for most nodes.
3799
+ const mayScrollY = el.scrollHeight > el.clientHeight;
3800
+ const mayScrollX = el.scrollWidth > el.clientWidth;
3801
+ if (!mayScrollY && !mayScrollX) {
3802
+ return NodeFilter.FILTER_SKIP;
3803
+ }
3804
+ // Confirm that overflow is actually set to scroll/auto.
3805
+ const { overflowY, overflowX } = window.getComputedStyle(el);
3806
+ const scrollsY = mayScrollY && (overflowY === 'scroll' || overflowY === 'auto');
3807
+ const scrollsX = mayScrollX && (overflowX === 'scroll' || overflowX === 'auto');
3808
+ return scrollsY || scrollsX ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
3809
+ },
3810
+ });
3811
+ let node;
3812
+ while ((node = walker.nextNode())) {
3813
+ scrollable.push(node);
3814
+ }
3815
+ return scrollable;
3816
+ };
3817
+ /**
3818
+ * Locks **all** currently scrollable elements while `active` is true.
3819
+ *
3820
+ * Instead of assuming the scroll lives on `document.body`, this hook discovers
3821
+ * every element with real overflow and freezes it with `overflow: hidden`.
3822
+ * Elements marked `data-scroll-lock-ignore` — and their entire subtrees — are
3823
+ * skipped so overlays (menus, dialogs, drawers…) remain scrollable internally.
3824
+ *
3825
+ * For `<html>`, `paddingRight` is compensated when a native scrollbar is present
3826
+ * to prevent horizontal layout shift.
3827
+ *
3828
+ * All original styles are restored on cleanup.
3829
+ *
3830
+ * @example useBodyScrollLock(isMenuOpen)
3780
3831
  */
3781
3832
  const useBodyScrollLock = (active) => {
3782
3833
  React.useEffect(() => {
3783
3834
  if (!active) {
3784
3835
  return;
3785
3836
  }
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
- };
3837
+ const scrollbarWidth = getScrollbarWidth();
3838
+ const elements = getScrollableElements();
3839
+ const restorers = elements.map((el) => {
3840
+ const savedOverflow = el.style.overflow;
3841
+ const savedPaddingRight = el.style.paddingRight;
3842
+ el.style.overflow = 'hidden';
3843
+ // Preserve the scrollbar gutter on the document root.
3844
+ if (el === document.documentElement && scrollbarWidth > 0) {
3845
+ const currentPadding = parseFloat(window.getComputedStyle(el).paddingRight) || 0;
3846
+ el.style.paddingRight = `${currentPadding + scrollbarWidth}px`;
3847
+ }
3848
+ return () => {
3849
+ el.style.overflow = savedOverflow;
3850
+ if (el === document.documentElement && scrollbarWidth > 0) {
3851
+ el.style.paddingRight = savedPaddingRight;
3852
+ }
3853
+ };
3854
+ });
3855
+ return () => restorers.forEach((restore) => restore());
3799
3856
  }, [active]);
3800
3857
  };
3801
3858
 
@@ -3828,7 +3885,7 @@ const MenuPanel = ({ open, onClose, anchorEl, minWidth, maxHeight = '20rem', pla
3828
3885
  if (!open) {
3829
3886
  return null;
3830
3887
  }
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);
3888
+ 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
3889
  };
3833
3890
  MenuPanel.displayName = 'MenuPanel';
3834
3891
 
@@ -3975,14 +4032,15 @@ const useMenuItem = ({ ref, role, hasSubmenu, disabled, onClick, closeOnClick, }
3975
4032
  const MENU_ITEM_STYLES = theme.createStyles((theme) => {
3976
4033
  const c = theme.colors;
3977
4034
  return {
3978
- root: {
4035
+ root: ({ size }) => ({
3979
4036
  display: 'flex',
3980
4037
  alignItems: 'center',
3981
4038
  gap: theme.spacing.sm,
3982
- paddingTop: theme.spacing.xs,
3983
- paddingBottom: theme.spacing.xs,
3984
4039
  paddingLeft: theme.spacing.md,
3985
4040
  paddingRight: theme.spacing.md,
4041
+ ...(size === 'default'
4042
+ ? { height: DEFAULT_DRAWER_ITEM_SIZE }
4043
+ : { paddingTop: theme.spacing.xs, paddingBottom: theme.spacing.xs }),
3986
4044
  cursor: 'pointer',
3987
4045
  userSelect: 'none',
3988
4046
  color: c.textPrimary,
@@ -4010,7 +4068,7 @@ const MENU_ITEM_STYLES = theme.createStyles((theme) => {
4010
4068
  ':active:not([data-disabled])': {
4011
4069
  backgroundColor: c.defaultSubtleActive,
4012
4070
  },
4013
- },
4071
+ }),
4014
4072
  /** Fixed-width leading slot keeping labels aligned for checkbox/radio items. */
4015
4073
  indicator: {
4016
4074
  display: 'inline-flex',
@@ -4042,13 +4100,13 @@ const MENU_ITEM_STYLES = theme.createStyles((theme) => {
4042
4100
  };
4043
4101
  }, { id: 'menu-item' });
4044
4102
 
4045
- const MenuItem = ({ ref, label, icon, iconColor, role = 'menuitem', checked, selected, focused, disabled, closeOnClick, submenu, submenuPlacement = 'right', onClick, ...rest }) => {
4103
+ const MenuItem = ({ ref, label, icon, iconColor, role = 'menuitem', checked, selected, focused, disabled, size = 'default', closeOnClick, submenu, submenuPlacement = 'right', onClick, ...rest }) => {
4046
4104
  const hasSubmenu = submenu !== undefined;
4047
4105
  const isCheckable = role === 'menuitemcheckbox' || role === 'menuitemradio';
4048
4106
  const isOption = role === 'option';
4049
4107
  const isHighlighted = isCheckable ? Boolean(checked) : Boolean(selected);
4050
4108
  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 }))] }));
4109
+ 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
4110
  };
4053
4111
  MenuItem.displayName = 'MenuItem';
4054
4112
 
@@ -4243,7 +4301,7 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled,
4243
4301
  const Select = ({ ref, value, defaultValue, onChange, options, label, helperText, placeholder, size = 'md', status = 'default', disabled, required, width, id, }) => {
4244
4302
  const { fieldId, labelId, helperId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect, } = useSelect({ id, ref, value, defaultValue, onChange, options, disabled });
4245
4303
  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)));
4304
+ 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
4305
  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
4306
  }) }), helperText !== undefined && (jsxRuntime.jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
4249
4307
  };
@@ -5961,7 +6019,7 @@ const DrawerTemporaryPanel = ({ isExpanded, onClose, children, role, ariaLabel,
5961
6019
  if (!isVisible) {
5962
6020
  return null;
5963
6021
  }
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);
6022
+ 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
6023
  };
5966
6024
  DrawerTemporaryPanel.displayName = 'DrawerTemporaryPanel';
5967
6025
  // ─── Main Drawer ─────────────────────────────────────────────────────────────
@@ -6870,7 +6928,7 @@ const DialogBase = ({ open, onClose, children, closeOnBackdropClick = false, ful
6870
6928
  if (!isVisible) {
6871
6929
  return null;
6872
6930
  }
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);
6931
+ 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
6932
  };
6875
6933
  DialogBase.displayName = 'Dialog';
6876
6934
  const Dialog = DialogBase;