@aurora-ds/components 1.1.7 → 1.2.0

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
@@ -594,7 +594,7 @@ const ICON_SIZE$2 = {
594
594
  * @example <Button label='Delete' variant='outlined' color='error' startIcon={IconRegistry.CloseIcon} />
595
595
  * @example <Button label='Submitting…' color='success' isLoading width='100%' />
596
596
  */
597
- const Button = ({ ref, variant = 'contained', color = 'primary', size = 'md', width, flexGrow, flexShrink, isLoading = false, startIcon: StartIcon, endIcon: EndIcon, label, className, type = 'button', disabled, style, ...rest }) => {
597
+ const Button = ({ ref, variant = 'contained', color = 'primary', size = 'md', width, flexGrow, flexShrink, isLoading = false, startIcon: StartIcon, endIcon: EndIcon, label, children, className, type = 'button', disabled, style, ...rest }) => {
598
598
  const isDisabled = disabled || isLoading;
599
599
  const iconSize = ICON_SIZE$2[size];
600
600
  const rootClassName = theme.cx(BUTTON_STYLES.root({ variant, color, size }), className);
@@ -604,7 +604,7 @@ const Button = ({ ref, variant = 'contained', color = 'primary', size = 'md', wi
604
604
  ...(flexGrow !== undefined ? { flexGrow } : {}),
605
605
  ...(flexShrink !== undefined ? { flexShrink } : {}),
606
606
  };
607
- return (jsxRuntime.jsxs("button", { ref: ref, type: type, className: rootClassName, disabled: isDisabled, "aria-busy": isLoading || undefined, style: mergedStyle, ...rest, children: [isLoading && (jsxRuntime.jsx("span", { className: BUTTON_STYLES.spinnerWrap, children: jsxRuntime.jsx(Icon, { icon: SpinnerIcon, size: iconSize, className: BUTTON_STYLES.spinnerIcon }) })), jsxRuntime.jsxs("span", { className: theme.cx(BUTTON_STYLES.content, isLoading && BUTTON_STYLES.contentHidden), children: [StartIcon && (jsxRuntime.jsx(Icon, { icon: StartIcon, size: iconSize })), label && (jsxRuntime.jsx(Text, { variant: 'span', fontSize: LABEL_FONT_SIZE$1[size], fontWeight: 'medium', lineHeight: 'none', children: label })), EndIcon && (jsxRuntime.jsx(Icon, { icon: EndIcon, size: iconSize }))] })] }));
607
+ return (jsxRuntime.jsxs("button", { ref: ref, type: type, className: rootClassName, disabled: isDisabled, "aria-busy": isLoading || undefined, style: mergedStyle, ...rest, children: [isLoading && (jsxRuntime.jsx("span", { className: BUTTON_STYLES.spinnerWrap, children: jsxRuntime.jsx(Icon, { icon: SpinnerIcon, size: iconSize, className: BUTTON_STYLES.spinnerIcon }) })), jsxRuntime.jsxs("span", { className: theme.cx(BUTTON_STYLES.content, isLoading && BUTTON_STYLES.contentHidden), children: [StartIcon && (jsxRuntime.jsx(Icon, { icon: StartIcon, size: iconSize })), (label !== undefined || children !== undefined) && (jsxRuntime.jsx(Text, { variant: 'span', fontSize: LABEL_FONT_SIZE$1[size], fontWeight: 'medium', lineHeight: 'none', children: label ?? children })), EndIcon && (jsxRuntime.jsx(Icon, { icon: EndIcon, size: iconSize }))] })] }));
608
608
  };
609
609
  Button.displayName = 'Button';
610
610
 
@@ -703,12 +703,20 @@ const LINK_STYLES = theme.createStyles((theme) => ({
703
703
  /**
704
704
  * Theme-aware anchor element with optional icons and underline control.
705
705
  *
706
+ * Supports SPA navigation (e.g. React Router) via `onClick` without `href`.
707
+ * In that case the component stays accessible: it gets `role="link"`,
708
+ * `tabIndex={0}` and keyboard Enter support automatically.
709
+ *
706
710
  * @example <Link href='/about'>About</Link>
707
711
  * @example <Link href='https://example.com' external>External site</Link>
708
712
  * @example <Link href='/profile' underline='always' startIcon={UserIcon}>Profile</Link>
709
713
  * @example <Link href='/terms' underline='none'>Terms</Link>
714
+ * @example <Link onClick={() => navigate('/about')}>About (SPA)</Link>
710
715
  */
711
- const Link = ({ ref, underline = 'hover', external = false, disabled = false, startIcon: StartIcon, endIcon: EndIcon, children, className, onClick, onKeyDown, ...rest }) => {
716
+ const Link = ({ ref, underline = 'hover', external = false, disabled = false, startIcon: StartIcon, endIcon: EndIcon, children, className, href, onClick, onKeyDown, ...rest }) => {
717
+ // An <a> without href has no implicit ARIA role and is not focusable.
718
+ // When used for SPA navigation (onClick only), we restore both behaviours.
719
+ const hasHref = !!href;
712
720
  const handleClick = (e) => {
713
721
  if (disabled) {
714
722
  e.preventDefault();
@@ -716,14 +724,22 @@ const Link = ({ ref, underline = 'hover', external = false, disabled = false, st
716
724
  }
717
725
  onClick?.(e);
718
726
  };
719
- // Prevents Enter navigation when disabled; satisfies jsx-a11y/click-events-have-key-events.
720
727
  const handleKeyDown = (e) => {
721
728
  if (disabled && e.key === 'Enter') {
722
729
  e.preventDefault();
723
730
  }
731
+ // Without href, the browser does not fire a click on Enter natively.
732
+ if (!hasHref && !disabled && e.key === 'Enter') {
733
+ e.currentTarget.click();
734
+ }
724
735
  onKeyDown?.(e);
725
736
  };
726
- return (jsxRuntime.jsxs("a", { ref: ref, className: theme.cx(LINK_STYLES.root({ underline }), className), "aria-disabled": disabled || undefined, tabIndex: disabled ? -1 : undefined, target: external ? '_blank' : undefined, rel: external ? 'noopener noreferrer' : undefined, onClick: handleClick, onKeyDown: handleKeyDown, ...rest, children: [StartIcon && (jsxRuntime.jsx("span", { className: LINK_STYLES.icon, "aria-hidden": true, children: jsxRuntime.jsx(StartIcon, { width: '1em', height: '1em' }) })), children, EndIcon && (jsxRuntime.jsx("span", { className: LINK_STYLES.icon, "aria-hidden": true, children: jsxRuntime.jsx(EndIcon, { width: '1em', height: '1em' }) }))] }));
737
+ return (jsxRuntime.jsxs("a", { ref: ref, href: href, className: theme.cx(LINK_STYLES.root({ underline }), className), "aria-disabled": disabled || undefined,
738
+ // Without href: must be explicitly put in the tab order.
739
+ // With href: the browser handles focusability natively (no tabIndex needed).
740
+ tabIndex: disabled ? -1 : (!hasHref ? 0 : undefined),
741
+ // Without href: <a> has no implicit ARIA role — add role="link" explicitly.
742
+ role: !hasHref ? 'link' : undefined, target: external ? '_blank' : undefined, rel: external ? 'noopener noreferrer' : undefined, onClick: handleClick, onKeyDown: handleKeyDown, ...rest, children: [StartIcon && (jsxRuntime.jsx("span", { className: LINK_STYLES.icon, "aria-hidden": true, children: jsxRuntime.jsx(StartIcon, { width: '1em', height: '1em' }) })), children, EndIcon && (jsxRuntime.jsx("span", { className: LINK_STYLES.icon, "aria-hidden": true, children: jsxRuntime.jsx(EndIcon, { width: '1em', height: '1em' }) }))] }));
727
743
  };
728
744
  Link.displayName = 'Link';
729
745
 
@@ -1779,7 +1795,7 @@ const useTextField = ({ id, ref, type, size, endAction, }) => {
1779
1795
  */
1780
1796
  const TextField = ({ ref, label, helperText, size = 'md', status = 'default', startIcon: StartIcon, endAction, type, id, disabled, required, ...rest }) => {
1781
1797
  const { fieldId, helperId, mergedRef, isPassword, showPassword, togglePassword, resolvedType, iconSize, iconButtonSize, hasEndSection, focusInput, } = useTextField({ id, ref, type, size, endAction });
1782
- return (jsxRuntime.jsxs(Stack, { flexDirection: 'column', gap: 'xs', children: [label !== undefined && (jsxRuntime.jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, children: [label, required && (jsxRuntime.jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsxRuntime.jsxs("div", { className: TEXTFIELD_WRAPPER_VARIANTS({ size, status }), "data-disabled": disabled || undefined, children: [StartIcon && (jsxRuntime.jsx("span", { className: TEXTFIELD_STYLES.startIconWrap, onClick: focusInput, children: jsxRuntime.jsx(Icon, { icon: StartIcon, size: iconSize, strokeColor: 'textSecondary' }) })), jsxRuntime.jsx("input", { ref: mergedRef, id: fieldId, type: resolvedType, disabled: disabled, required: required, "aria-required": required || undefined, "aria-invalid": status === 'error' || undefined, "aria-describedby": helperText !== undefined ? helperId : undefined, className: TEXTFIELD_STYLES.input, ...rest }), hasEndSection && (jsxRuntime.jsxs("span", { className: TEXTFIELD_STYLES.endActionWrap, children: [endAction, isPassword && (jsxRuntime.jsx(IconButton, { icon: showPassword ? EyeSlashIcon : EyeIcon, ariaLabel: showPassword ? 'Hide password' : 'Show password', variant: 'text', color: 'neutral', size: iconButtonSize, type: 'button', tabIndex: -1, onClick: togglePassword }))] }))] }), helperText !== undefined && (jsxRuntime.jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
1798
+ return (jsxRuntime.jsxs(Stack, { flexDirection: 'column', gap: 'xs', children: [label !== undefined && (jsxRuntime.jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, children: [label, required && (jsxRuntime.jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsxRuntime.jsxs("div", { className: TEXTFIELD_WRAPPER_VARIANTS({ size, status }), "data-disabled": disabled || undefined, children: [StartIcon && (jsxRuntime.jsx("span", { className: TEXTFIELD_STYLES.startIconWrap, onClick: focusInput, "aria-hidden": true, children: jsxRuntime.jsx(Icon, { icon: StartIcon, size: iconSize, strokeColor: 'textSecondary' }) })), jsxRuntime.jsx("input", { ref: mergedRef, id: fieldId, type: resolvedType, disabled: disabled, required: required, "aria-required": required || undefined, "aria-invalid": status === 'error' || undefined, "aria-describedby": helperText !== undefined ? helperId : undefined, "aria-errormessage": status === 'error' && helperText !== undefined ? helperId : undefined, className: TEXTFIELD_STYLES.input, ...rest }), hasEndSection && (jsxRuntime.jsxs("span", { className: TEXTFIELD_STYLES.endActionWrap, children: [endAction, isPassword && (jsxRuntime.jsx(IconButton, { icon: showPassword ? EyeSlashIcon : EyeIcon, ariaLabel: showPassword ? 'Hide password' : 'Show password', variant: 'text', color: 'neutral', size: iconButtonSize, type: 'button', onClick: togglePassword }))] }))] }), helperText !== undefined && (jsxRuntime.jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
1783
1799
  };
1784
1800
  TextField.displayName = 'TextField';
1785
1801
 
@@ -1914,7 +1930,9 @@ const useMenuPosition = ({ anchorEl, open, menuRef, minWidth, gap = 4, }) => {
1914
1930
  */
1915
1931
  const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
1916
1932
  const panelRef = React.useRef(null);
1933
+ const baseId = React.useId();
1917
1934
  const [focusedIndex, setFocusedIndex] = React.useState(-1);
1935
+ const [activeDescendant, setActiveDescendant] = React.useState(undefined);
1918
1936
  const { style } = useMenuPosition({ anchorEl, open, menuRef: panelRef, minWidth });
1919
1937
  /** Returns all non-disabled option elements inside the panel. */
1920
1938
  const getOptions = React.useCallback(() => {
@@ -1937,22 +1955,31 @@ const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
1937
1955
  });
1938
1956
  return () => cancelAnimationFrame(raf);
1939
1957
  }, [open, getOptions]);
1940
- // Keep data-focused in sync with focusedIndex
1958
+ // Keep data-focused (visual highlight) and aria-activedescendant (screen reader
1959
+ // announcement) in sync with focusedIndex. Each option is assigned a stable id
1960
+ // so the listbox can reference the active one via aria-activedescendant.
1941
1961
  React.useEffect(() => {
1942
1962
  if (!open) {
1963
+ setActiveDescendant(undefined);
1943
1964
  return;
1944
1965
  }
1945
1966
  const options = getOptions();
1967
+ let activeId;
1946
1968
  options.forEach((el, idx) => {
1969
+ if (!el.id) {
1970
+ el.id = `${baseId}-option-${idx}`;
1971
+ }
1947
1972
  if (idx === focusedIndex) {
1948
1973
  el.setAttribute('data-focused', 'true');
1949
1974
  el.scrollIntoView({ block: 'nearest' });
1975
+ activeId = el.id;
1950
1976
  }
1951
1977
  else {
1952
1978
  el.removeAttribute('data-focused');
1953
1979
  }
1954
1980
  });
1955
- }, [focusedIndex, open, getOptions]);
1981
+ setActiveDescendant(activeId);
1982
+ }, [focusedIndex, open, getOptions, baseId]);
1956
1983
  useKeyPress({
1957
1984
  Escape: onClose,
1958
1985
  ArrowDown: (e) => {
@@ -1990,7 +2017,7 @@ const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
1990
2017
  }
1991
2018
  },
1992
2019
  }, { enabled: open });
1993
- return { panelRef, style };
2020
+ return { panelRef, style, activeDescendant };
1994
2021
  };
1995
2022
 
1996
2023
  const MENU_GROUP_STYLES = theme.createStyles((theme) => {
@@ -2031,7 +2058,8 @@ const MENU_GROUP_STYLES = theme.createStyles((theme) => {
2031
2058
  }, { id: 'menu-group' });
2032
2059
 
2033
2060
  const MenuGroup = ({ label, divider, children, }) => {
2034
- return (jsxRuntime.jsxs("div", { className: MENU_GROUP_STYLES.root, children: [divider && (jsxRuntime.jsx("div", { className: MENU_GROUP_STYLES.divider, role: 'separator', "aria-hidden": true })), label !== undefined && (jsxRuntime.jsx("span", { className: MENU_GROUP_STYLES.label, children: label })), jsxRuntime.jsx("ul", { className: MENU_GROUP_STYLES.list, role: 'group', "aria-label": label, children: children })] }));
2061
+ const labelId = React.useId();
2062
+ return (jsxRuntime.jsxs("div", { className: MENU_GROUP_STYLES.root, children: [divider && (jsxRuntime.jsx("div", { className: MENU_GROUP_STYLES.divider, role: 'separator', "aria-hidden": true })), label !== undefined && (jsxRuntime.jsx("span", { id: labelId, className: MENU_GROUP_STYLES.label, "aria-hidden": true, children: label })), jsxRuntime.jsx("ul", { className: MENU_GROUP_STYLES.list, role: 'group', "aria-labelledby": label !== undefined ? labelId : undefined, children: children })] }));
2035
2063
  };
2036
2064
  MenuGroup.displayName = 'MenuGroup';
2037
2065
 
@@ -2082,12 +2110,12 @@ const MenuItem = ({ ref, label, icon, selected, focused, disabled, onClick, ...r
2082
2110
  };
2083
2111
  MenuItem.displayName = 'MenuItem';
2084
2112
 
2085
- const MenuBase = ({ open, onClose, anchorEl, minWidth, maxHeight = '20rem', id, children, }) => {
2086
- const { panelRef, style } = useMenu({ open, onClose, anchorEl, minWidth });
2113
+ const MenuBase = ({ open, onClose, anchorEl, minWidth, maxHeight = '20rem', id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, children, }) => {
2114
+ const { panelRef, style, activeDescendant } = useMenu({ open, onClose, anchorEl, minWidth });
2087
2115
  if (!open) {
2088
2116
  return null;
2089
2117
  }
2090
- return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: MENU_STYLES.backdrop, onClick: onClose, "aria-hidden": true }), jsxRuntime.jsx("div", { ref: panelRef, id: id, role: 'listbox', tabIndex: -1, className: MENU_STYLES.panel, style: { ...style, maxHeight, outline: 'none' }, children: children })] }), document.body);
2118
+ return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: MENU_STYLES.backdrop, onClick: onClose, "aria-hidden": true }), jsxRuntime.jsx("div", { ref: panelRef, id: id, role: 'listbox', tabIndex: -1, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-activedescendant": activeDescendant, className: MENU_STYLES.panel, style: { ...style, maxHeight, outline: 'none' }, children: children })] }), document.body);
2091
2119
  };
2092
2120
  MenuBase.displayName = 'Menu';
2093
2121
  const Menu = MenuBase;
@@ -2209,9 +2237,9 @@ const ICON_SIZE_MAP = {
2209
2237
  md: 'md',
2210
2238
  lg: 'lg',
2211
2239
  };
2212
- const SelectTrigger = ({ ref, size = 'md', status = 'default', open, hasValue, startIcon, disabled, children, ...rest }) => {
2240
+ const SelectTrigger = ({ ref, size = 'md', status = 'default', open, hasValue, startIcon, disabled, children, 'aria-expanded': ariaExpanded, 'aria-controls': ariaControls, ...rest }) => {
2213
2241
  const iconSize = ICON_SIZE_MAP[size];
2214
- return (jsxRuntime.jsxs("button", { type: 'button', ref: ref, className: SELECT_TRIGGER_VARIANTS({ size, status }), "data-open": open || undefined, "data-disabled": disabled || undefined, disabled: disabled, ...rest, children: [startIcon !== undefined && (jsxRuntime.jsx(Icon, { icon: startIcon, size: iconSize, strokeColor: 'textSecondary' })), jsxRuntime.jsx("span", { className: hasValue ? SELECT_TRIGGER_STYLES.value : SELECT_TRIGGER_STYLES.placeholder, children: children }), jsxRuntime.jsx(Icon, { icon: ChevronDownIcon, size: iconSize, className: theme.cx(SELECT_TRIGGER_STYLES.chevron, open ? SELECT_TRIGGER_STYLES.chevronOpen : undefined), strokeColor: 'textSecondary' })] }));
2242
+ return (jsxRuntime.jsxs("button", { type: 'button', role: 'combobox', ref: ref, className: SELECT_TRIGGER_VARIANTS({ size, status }), "data-open": open || undefined, "data-disabled": disabled || undefined, disabled: disabled, "aria-expanded": ariaExpanded, "aria-controls": ariaControls, ...rest, children: [startIcon !== undefined && (jsxRuntime.jsx(Icon, { icon: startIcon, size: iconSize, strokeColor: 'textSecondary' })), jsxRuntime.jsx("span", { className: hasValue ? SELECT_TRIGGER_STYLES.value : SELECT_TRIGGER_STYLES.placeholder, children: children }), jsxRuntime.jsx(Icon, { icon: ChevronDownIcon, size: iconSize, className: theme.cx(SELECT_TRIGGER_STYLES.chevron, open ? SELECT_TRIGGER_STYLES.chevronOpen : undefined), strokeColor: 'textSecondary' })] }));
2215
2243
  };
2216
2244
  SelectTrigger.displayName = 'SelectTrigger';
2217
2245
 
@@ -2225,6 +2253,7 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled,
2225
2253
  const fieldId = id ?? generatedId;
2226
2254
  const helperId = `${fieldId}-helper`;
2227
2255
  const menuId = `${fieldId}-menu`;
2256
+ const labelId = `${fieldId}-label`;
2228
2257
  const triggerRef = React.useRef(null);
2229
2258
  const mergedRef = useMergedRefs(ref, triggerRef);
2230
2259
  const [open, setOpen] = React.useState(false);
@@ -2268,6 +2297,7 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled,
2268
2297
  const close = React.useCallback(() => setOpen(false), []);
2269
2298
  return {
2270
2299
  fieldId,
2300
+ labelId,
2271
2301
  helperId,
2272
2302
  menuId,
2273
2303
  triggerRef,
@@ -2283,8 +2313,8 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled,
2283
2313
  };
2284
2314
 
2285
2315
  const Select = ({ ref, value, defaultValue, onChange, options, label, helperText, placeholder, size = 'md', status = 'default', disabled, required, width, id, }) => {
2286
- const { fieldId, helperId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect, } = useSelect({ id, ref, value, defaultValue, onChange, options, disabled });
2287
- 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, 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, disabled: disabled, required: required, "aria-haspopup": 'listbox', "aria-expanded": open, "aria-controls": menuId, "aria-invalid": status === 'error' || 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, children: Array.from(groupedOptions.entries()).map(([groupKey, groupOpts], groupIndex) => {
2316
+ const { fieldId, labelId, helperId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect, } = useSelect({ id, ref, value, defaultValue, onChange, options, disabled });
2317
+ 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, 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, "aria-labelledby": label !== undefined ? labelId : undefined, "aria-label": label === undefined ? placeholder : undefined, children: Array.from(groupedOptions.entries()).map(([groupKey, groupOpts], groupIndex) => {
2288
2318
  const items = groupOpts.map((opt) => (jsxRuntime.jsx(Menu.Item, { label: opt.label, icon: opt.icon, selected: opt.value === currentValue, disabled: opt.disabled, onClick: () => handleSelect(opt.value) }, opt.value)));
2289
2319
  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'));
2290
2320
  }) }), helperText !== undefined && (jsxRuntime.jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
@@ -2452,7 +2482,7 @@ const useCheckbox = ({ id, ref, indeterminate = false }) => {
2452
2482
  const Checkbox = ({ ref, label, helperText, size = 'md', status = 'default', indeterminate = false, error, id, disabled, required, ...rest }) => {
2453
2483
  const resolvedStatus = error ? 'error' : status;
2454
2484
  const { checkboxId, helperId, mergedRef } = useCheckbox({ id, ref, indeterminate });
2455
- return (jsxRuntime.jsxs("div", { className: CHECKBOX_STYLES.wrapper, children: [jsxRuntime.jsxs("label", { htmlFor: checkboxId, className: CHECKBOX_ROOT_VARIANTS({ disabled: disabled ? 'true' : 'false' }), children: [jsxRuntime.jsx("input", { ref: mergedRef, id: checkboxId, type: 'checkbox', disabled: disabled, required: required, "aria-required": required || undefined, "aria-invalid": resolvedStatus === 'error' || undefined, "aria-describedby": helperText !== undefined ? helperId : undefined, className: CHECKBOX_INPUT_VARIANTS({
2485
+ return (jsxRuntime.jsxs("div", { className: CHECKBOX_STYLES.wrapper, children: [jsxRuntime.jsxs("label", { htmlFor: checkboxId, className: CHECKBOX_ROOT_VARIANTS({ disabled: disabled ? 'true' : 'false' }), children: [jsxRuntime.jsx("input", { ref: mergedRef, id: checkboxId, type: 'checkbox', disabled: disabled, required: required, "aria-required": required || undefined, "aria-invalid": resolvedStatus === 'error' || undefined, "aria-describedby": helperText !== undefined ? helperId : undefined, "aria-errormessage": resolvedStatus === 'error' && helperText !== undefined ? helperId : undefined, className: CHECKBOX_INPUT_VARIANTS({
2456
2486
  size,
2457
2487
  status: resolvedStatus,
2458
2488
  disabled: disabled ? 'true' : 'false',
@@ -2950,6 +2980,15 @@ const VARIANT_ARIA_LIVE = {
2950
2980
  warning: 'polite',
2951
2981
  info: 'polite',
2952
2982
  };
2983
+ /** role="alert" implies aria-live="assertive" — reserve it for errors.
2984
+ * Other variants use role="status" (implies aria-live="polite"). */
2985
+ const VARIANT_ROLE = {
2986
+ default: 'status',
2987
+ success: 'status',
2988
+ error: 'alert',
2989
+ warning: 'status',
2990
+ info: 'status',
2991
+ };
2953
2992
  /**
2954
2993
  * Inline alert banner with 5 visual variants: default, success, error, warning, info.
2955
2994
  * Use `Alert.Title` (icon + heading) and `Alert.Body` (message) as children.
@@ -2962,7 +3001,7 @@ const VARIANT_ARIA_LIVE = {
2962
3001
  */
2963
3002
  const AlertBase = ({ variant = 'default', children, width = '100%', outline = false, shadow = 'none', }) => {
2964
3003
  const { backgroundColor, borderColor, accentColor } = VARIANT_TOKENS[variant];
2965
- return (jsxRuntime.jsx(AlertContext.Provider, { value: { variant, accentColor }, children: jsxRuntime.jsx(Stack, { role: 'alert', "aria-live": VARIANT_ARIA_LIVE[variant], flexDirection: 'column', gap: 'xs', padding: 'md', borderRadius: 'lg', backgroundColor: backgroundColor, borderColor: outline ? borderColor : undefined, borderWidth: outline ? '1px' : undefined, borderStyle: outline ? 'solid' : undefined, boxShadow: shadow, width: width, children: children }) }));
3004
+ return (jsxRuntime.jsx(AlertContext.Provider, { value: { variant, accentColor }, children: jsxRuntime.jsx(Stack, { role: VARIANT_ROLE[variant], "aria-live": VARIANT_ARIA_LIVE[variant], flexDirection: 'column', gap: 'xs', padding: 'md', borderRadius: 'lg', backgroundColor: backgroundColor, borderColor: outline ? borderColor : undefined, borderWidth: outline ? '1px' : undefined, borderStyle: outline ? 'solid' : undefined, boxShadow: shadow, width: width, children: children }) }));
2966
3005
  };
2967
3006
  AlertBase.displayName = 'Alert';
2968
3007
  const Alert = AlertBase;
@@ -3129,7 +3168,7 @@ const useDialog = ({ open, onClose, closeOnBackdropClick, maxWidth, maxHeight, m
3129
3168
  */
3130
3169
  const DialogHeader = ({ title, onClose }) => {
3131
3170
  const { titleId, CloseIconComponent } = React.useContext(DialogContext);
3132
- return (jsxRuntime.jsxs(Stack, { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 'md', paddingTop: 'md', paddingBottom: 'md', paddingLeft: 'lg', paddingRight: 'md', flexShrink: 0, children: [jsxRuntime.jsx(Text, { id: titleId, variant: 'span', fontSize: 'md', fontWeight: 'semibold', color: 'textPrimary', children: title }), jsxRuntime.jsx(IconButton, { icon: CloseIconComponent, ariaLabel: 'Close dialog', variant: 'text', color: 'neutral', size: 'sm', type: 'button', onClick: onClose })] }));
3171
+ return (jsxRuntime.jsxs(Stack, { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 'md', paddingTop: 'md', paddingBottom: 'md', paddingLeft: 'lg', paddingRight: 'md', flexShrink: 0, children: [jsxRuntime.jsx(Text, { id: titleId, variant: 'span', as: 'h2', fontSize: 'md', fontWeight: 'semibold', color: 'textPrimary', children: title }), jsxRuntime.jsx(IconButton, { icon: CloseIconComponent, ariaLabel: 'Close dialog', variant: 'text', color: 'neutral', size: 'sm', type: 'button', onClick: onClose })] }));
3133
3172
  };
3134
3173
  DialogHeader.displayName = 'Dialog.Header';
3135
3174