@aurora-ds/components 1.1.7 → 1.4.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 +397 -50
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +397 -52
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.ts +363 -57
- package/package.json +1 -1
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
|
|
|
@@ -661,36 +661,42 @@ const IconButton = ({ ref, icon: IconComponent, ariaLabel, variant = 'contained'
|
|
|
661
661
|
IconButton.displayName = 'IconButton';
|
|
662
662
|
|
|
663
663
|
const LINK_STYLES = theme.createStyles((theme) => ({
|
|
664
|
-
root: ({ underline = 'hover' }) =>
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
color: theme.colors.
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
664
|
+
root: ({ underline = 'hover', color = 'default' }) => {
|
|
665
|
+
const mainColor = color === 'secondary' ? theme.colors.textSecondary : theme.colors.linkMain;
|
|
666
|
+
const hoverColor = color === 'secondary' ? theme.colors.textTertiary : theme.colors.linkHover;
|
|
667
|
+
const activeColor = color === 'secondary' ? theme.colors.textPrimary : theme.colors.linkActive;
|
|
668
|
+
const disabledColor = color === 'secondary' ? theme.colors.textDisabled : theme.colors.linkDisabled;
|
|
669
|
+
return {
|
|
670
|
+
display: 'inline-flex',
|
|
671
|
+
alignItems: 'center',
|
|
672
|
+
gap: '0.25em',
|
|
673
|
+
color: mainColor,
|
|
674
|
+
fontFamily: 'inherit',
|
|
675
|
+
fontSize: 'inherit',
|
|
676
|
+
lineHeight: 'inherit',
|
|
677
|
+
fontWeight: 'inherit',
|
|
678
|
+
textDecoration: underline === 'always' ? 'underline' : 'none',
|
|
679
|
+
cursor: 'pointer',
|
|
680
|
+
borderRadius: theme.radius.xs,
|
|
681
|
+
transition: `color ${theme.transition.fast}`,
|
|
682
|
+
':hover:not([aria-disabled="true"])': {
|
|
683
|
+
color: hoverColor,
|
|
684
|
+
textDecoration: underline !== 'none' ? 'underline' : 'none',
|
|
685
|
+
},
|
|
686
|
+
':active:not([aria-disabled="true"])': {
|
|
687
|
+
color: activeColor,
|
|
688
|
+
},
|
|
689
|
+
':focus-visible': {
|
|
690
|
+
outline: `2px solid ${mainColor}`,
|
|
691
|
+
outlineOffset: '2px',
|
|
692
|
+
},
|
|
693
|
+
'&[aria-disabled="true"]': {
|
|
694
|
+
color: disabledColor,
|
|
695
|
+
cursor: 'not-allowed',
|
|
696
|
+
textDecoration: 'none',
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
},
|
|
694
700
|
icon: {
|
|
695
701
|
display: 'inline-flex',
|
|
696
702
|
alignItems: 'center',
|
|
@@ -703,12 +709,20 @@ const LINK_STYLES = theme.createStyles((theme) => ({
|
|
|
703
709
|
/**
|
|
704
710
|
* Theme-aware anchor element with optional icons and underline control.
|
|
705
711
|
*
|
|
712
|
+
* Supports SPA navigation (e.g. React Router) via `onClick` without `href`.
|
|
713
|
+
* In that case the component stays accessible: it gets `role="link"`,
|
|
714
|
+
* `tabIndex={0}` and keyboard Enter support automatically.
|
|
715
|
+
*
|
|
706
716
|
* @example <Link href='/about'>About</Link>
|
|
707
717
|
* @example <Link href='https://example.com' external>External site</Link>
|
|
708
718
|
* @example <Link href='/profile' underline='always' startIcon={UserIcon}>Profile</Link>
|
|
709
719
|
* @example <Link href='/terms' underline='none'>Terms</Link>
|
|
720
|
+
* @example <Link onClick={() => navigate('/about')}>About (SPA)</Link>
|
|
710
721
|
*/
|
|
711
|
-
const Link = ({ ref, underline = 'hover', external = false, disabled = false, startIcon: StartIcon, endIcon: EndIcon, children, className, onClick, onKeyDown, ...rest }) => {
|
|
722
|
+
const Link = ({ ref, underline = 'hover', color = 'default', external = false, disabled = false, startIcon: StartIcon, endIcon: EndIcon, children, className, href, onClick, onKeyDown, ...rest }) => {
|
|
723
|
+
// An <a> without href has no implicit ARIA role and is not focusable.
|
|
724
|
+
// When used for SPA navigation (onClick only), we restore both behaviours.
|
|
725
|
+
const hasHref = !!href;
|
|
712
726
|
const handleClick = (e) => {
|
|
713
727
|
if (disabled) {
|
|
714
728
|
e.preventDefault();
|
|
@@ -716,14 +730,22 @@ const Link = ({ ref, underline = 'hover', external = false, disabled = false, st
|
|
|
716
730
|
}
|
|
717
731
|
onClick?.(e);
|
|
718
732
|
};
|
|
719
|
-
// Prevents Enter navigation when disabled; satisfies jsx-a11y/click-events-have-key-events.
|
|
720
733
|
const handleKeyDown = (e) => {
|
|
721
734
|
if (disabled && e.key === 'Enter') {
|
|
722
735
|
e.preventDefault();
|
|
723
736
|
}
|
|
737
|
+
// Without href, the browser does not fire a click on Enter natively.
|
|
738
|
+
if (!hasHref && !disabled && e.key === 'Enter') {
|
|
739
|
+
e.currentTarget.click();
|
|
740
|
+
}
|
|
724
741
|
onKeyDown?.(e);
|
|
725
742
|
};
|
|
726
|
-
return (jsxRuntime.jsxs("a", { ref: ref, className: theme.cx(LINK_STYLES.root({ underline }), className), "aria-disabled": disabled || undefined,
|
|
743
|
+
return (jsxRuntime.jsxs("a", { ref: ref, href: href, className: theme.cx(LINK_STYLES.root({ underline, color }), className), "aria-disabled": disabled || undefined,
|
|
744
|
+
// Without href: must be explicitly put in the tab order.
|
|
745
|
+
// With href: the browser handles focusability natively (no tabIndex needed).
|
|
746
|
+
tabIndex: disabled ? -1 : (!hasHref ? 0 : undefined),
|
|
747
|
+
// Without href: <a> has no implicit ARIA role — add role="link" explicitly.
|
|
748
|
+
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
749
|
};
|
|
728
750
|
Link.displayName = 'Link';
|
|
729
751
|
|
|
@@ -1779,7 +1801,7 @@ const useTextField = ({ id, ref, type, size, endAction, }) => {
|
|
|
1779
1801
|
*/
|
|
1780
1802
|
const TextField = ({ ref, label, helperText, size = 'md', status = 'default', startIcon: StartIcon, endAction, type, id, disabled, required, ...rest }) => {
|
|
1781
1803
|
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',
|
|
1804
|
+
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
1805
|
};
|
|
1784
1806
|
TextField.displayName = 'TextField';
|
|
1785
1807
|
|
|
@@ -1914,7 +1936,9 @@ const useMenuPosition = ({ anchorEl, open, menuRef, minWidth, gap = 4, }) => {
|
|
|
1914
1936
|
*/
|
|
1915
1937
|
const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
|
|
1916
1938
|
const panelRef = React.useRef(null);
|
|
1939
|
+
const baseId = React.useId();
|
|
1917
1940
|
const [focusedIndex, setFocusedIndex] = React.useState(-1);
|
|
1941
|
+
const [activeDescendant, setActiveDescendant] = React.useState(undefined);
|
|
1918
1942
|
const { style } = useMenuPosition({ anchorEl, open, menuRef: panelRef, minWidth });
|
|
1919
1943
|
/** Returns all non-disabled option elements inside the panel. */
|
|
1920
1944
|
const getOptions = React.useCallback(() => {
|
|
@@ -1937,22 +1961,31 @@ const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
|
|
|
1937
1961
|
});
|
|
1938
1962
|
return () => cancelAnimationFrame(raf);
|
|
1939
1963
|
}, [open, getOptions]);
|
|
1940
|
-
// Keep data-focused
|
|
1964
|
+
// Keep data-focused (visual highlight) and aria-activedescendant (screen reader
|
|
1965
|
+
// announcement) in sync with focusedIndex. Each option is assigned a stable id
|
|
1966
|
+
// so the listbox can reference the active one via aria-activedescendant.
|
|
1941
1967
|
React.useEffect(() => {
|
|
1942
1968
|
if (!open) {
|
|
1969
|
+
setActiveDescendant(undefined);
|
|
1943
1970
|
return;
|
|
1944
1971
|
}
|
|
1945
1972
|
const options = getOptions();
|
|
1973
|
+
let activeId;
|
|
1946
1974
|
options.forEach((el, idx) => {
|
|
1975
|
+
if (!el.id) {
|
|
1976
|
+
el.id = `${baseId}-option-${idx}`;
|
|
1977
|
+
}
|
|
1947
1978
|
if (idx === focusedIndex) {
|
|
1948
1979
|
el.setAttribute('data-focused', 'true');
|
|
1949
1980
|
el.scrollIntoView({ block: 'nearest' });
|
|
1981
|
+
activeId = el.id;
|
|
1950
1982
|
}
|
|
1951
1983
|
else {
|
|
1952
1984
|
el.removeAttribute('data-focused');
|
|
1953
1985
|
}
|
|
1954
1986
|
});
|
|
1955
|
-
|
|
1987
|
+
setActiveDescendant(activeId);
|
|
1988
|
+
}, [focusedIndex, open, getOptions, baseId]);
|
|
1956
1989
|
useKeyPress({
|
|
1957
1990
|
Escape: onClose,
|
|
1958
1991
|
ArrowDown: (e) => {
|
|
@@ -1990,7 +2023,7 @@ const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
|
|
|
1990
2023
|
}
|
|
1991
2024
|
},
|
|
1992
2025
|
}, { enabled: open });
|
|
1993
|
-
return { panelRef, style };
|
|
2026
|
+
return { panelRef, style, activeDescendant };
|
|
1994
2027
|
};
|
|
1995
2028
|
|
|
1996
2029
|
const MENU_GROUP_STYLES = theme.createStyles((theme) => {
|
|
@@ -2031,7 +2064,8 @@ const MENU_GROUP_STYLES = theme.createStyles((theme) => {
|
|
|
2031
2064
|
}, { id: 'menu-group' });
|
|
2032
2065
|
|
|
2033
2066
|
const MenuGroup = ({ label, divider, children, }) => {
|
|
2034
|
-
|
|
2067
|
+
const labelId = React.useId();
|
|
2068
|
+
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
2069
|
};
|
|
2036
2070
|
MenuGroup.displayName = 'MenuGroup';
|
|
2037
2071
|
|
|
@@ -2082,12 +2116,12 @@ const MenuItem = ({ ref, label, icon, selected, focused, disabled, onClick, ...r
|
|
|
2082
2116
|
};
|
|
2083
2117
|
MenuItem.displayName = 'MenuItem';
|
|
2084
2118
|
|
|
2085
|
-
const MenuBase = ({ open, onClose, anchorEl, minWidth, maxHeight = '20rem', id, children, }) => {
|
|
2086
|
-
const { panelRef, style } = useMenu({ open, onClose, anchorEl, minWidth });
|
|
2119
|
+
const MenuBase = ({ open, onClose, anchorEl, minWidth, maxHeight = '20rem', id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, children, }) => {
|
|
2120
|
+
const { panelRef, style, activeDescendant } = useMenu({ open, onClose, anchorEl, minWidth });
|
|
2087
2121
|
if (!open) {
|
|
2088
2122
|
return null;
|
|
2089
2123
|
}
|
|
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);
|
|
2124
|
+
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
2125
|
};
|
|
2092
2126
|
MenuBase.displayName = 'Menu';
|
|
2093
2127
|
const Menu = MenuBase;
|
|
@@ -2209,9 +2243,9 @@ const ICON_SIZE_MAP = {
|
|
|
2209
2243
|
md: 'md',
|
|
2210
2244
|
lg: 'lg',
|
|
2211
2245
|
};
|
|
2212
|
-
const SelectTrigger = ({ ref, size = 'md', status = 'default', open, hasValue, startIcon, disabled, children, ...rest }) => {
|
|
2246
|
+
const SelectTrigger = ({ ref, size = 'md', status = 'default', open, hasValue, startIcon, disabled, children, 'aria-expanded': ariaExpanded, 'aria-controls': ariaControls, ...rest }) => {
|
|
2213
2247
|
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' })] }));
|
|
2248
|
+
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
2249
|
};
|
|
2216
2250
|
SelectTrigger.displayName = 'SelectTrigger';
|
|
2217
2251
|
|
|
@@ -2225,6 +2259,7 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled,
|
|
|
2225
2259
|
const fieldId = id ?? generatedId;
|
|
2226
2260
|
const helperId = `${fieldId}-helper`;
|
|
2227
2261
|
const menuId = `${fieldId}-menu`;
|
|
2262
|
+
const labelId = `${fieldId}-label`;
|
|
2228
2263
|
const triggerRef = React.useRef(null);
|
|
2229
2264
|
const mergedRef = useMergedRefs(ref, triggerRef);
|
|
2230
2265
|
const [open, setOpen] = React.useState(false);
|
|
@@ -2268,6 +2303,7 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled,
|
|
|
2268
2303
|
const close = React.useCallback(() => setOpen(false), []);
|
|
2269
2304
|
return {
|
|
2270
2305
|
fieldId,
|
|
2306
|
+
labelId,
|
|
2271
2307
|
helperId,
|
|
2272
2308
|
menuId,
|
|
2273
2309
|
triggerRef,
|
|
@@ -2283,8 +2319,8 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled,
|
|
|
2283
2319
|
};
|
|
2284
2320
|
|
|
2285
2321
|
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,
|
|
2322
|
+
const { fieldId, labelId, helperId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect, } = useSelect({ id, ref, value, defaultValue, onChange, options, disabled });
|
|
2323
|
+
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
2324
|
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
2325
|
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
2326
|
}) }), helperText !== undefined && (jsxRuntime.jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
|
|
@@ -2452,7 +2488,7 @@ const useCheckbox = ({ id, ref, indeterminate = false }) => {
|
|
|
2452
2488
|
const Checkbox = ({ ref, label, helperText, size = 'md', status = 'default', indeterminate = false, error, id, disabled, required, ...rest }) => {
|
|
2453
2489
|
const resolvedStatus = error ? 'error' : status;
|
|
2454
2490
|
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({
|
|
2491
|
+
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
2492
|
size,
|
|
2457
2493
|
status: resolvedStatus,
|
|
2458
2494
|
disabled: disabled ? 'true' : 'false',
|
|
@@ -2543,6 +2579,78 @@ const Grid = ({ display = 'grid', columns, rows, autoFlow, autoColumns, autoRows
|
|
|
2543
2579
|
};
|
|
2544
2580
|
Grid.displayName = 'Grid';
|
|
2545
2581
|
|
|
2582
|
+
const BREADCRUMB_STYLES = theme.createStyles((_theme) => ({
|
|
2583
|
+
nav: {
|
|
2584
|
+
display: 'block',
|
|
2585
|
+
},
|
|
2586
|
+
list: {
|
|
2587
|
+
display: 'flex',
|
|
2588
|
+
flexWrap: 'wrap',
|
|
2589
|
+
alignItems: 'center',
|
|
2590
|
+
listStyle: 'none',
|
|
2591
|
+
margin: 0,
|
|
2592
|
+
padding: 0,
|
|
2593
|
+
},
|
|
2594
|
+
}), { id: 'breadcrumb' });
|
|
2595
|
+
|
|
2596
|
+
const BreadcrumbContext = React.createContext({
|
|
2597
|
+
separator: '/',
|
|
2598
|
+
});
|
|
2599
|
+
const useBreadcrumbContext = () => React.useContext(BreadcrumbContext);
|
|
2600
|
+
|
|
2601
|
+
const BREADCRUMB_ITEM_STYLES = theme.createStyles((theme) => ({
|
|
2602
|
+
item: {
|
|
2603
|
+
display: 'inline-flex',
|
|
2604
|
+
alignItems: 'center',
|
|
2605
|
+
fontSize: theme.fontSize.sm,
|
|
2606
|
+
lineHeight: theme.lineHeight.normal,
|
|
2607
|
+
},
|
|
2608
|
+
separator: {
|
|
2609
|
+
display: 'inline-flex',
|
|
2610
|
+
alignItems: 'center',
|
|
2611
|
+
marginLeft: theme.spacing.sm,
|
|
2612
|
+
marginRight: theme.spacing.sm,
|
|
2613
|
+
color: theme.colors.textTertiary,
|
|
2614
|
+
userSelect: 'none',
|
|
2615
|
+
},
|
|
2616
|
+
}), { id: 'breadcrumb-item' });
|
|
2617
|
+
|
|
2618
|
+
const BreadcrumbItem = ({ label, href, onClick, current = false, isFirst = false, }) => {
|
|
2619
|
+
const { separator } = useBreadcrumbContext();
|
|
2620
|
+
return (jsxRuntime.jsxs("li", { className: BREADCRUMB_ITEM_STYLES.item, "aria-current": current ? 'page' : undefined, children: [!isFirst && (jsxRuntime.jsx("span", { "aria-hidden": true, className: BREADCRUMB_ITEM_STYLES.separator, children: separator })), current ? (jsxRuntime.jsx(Text, { variant: 'span', fontWeight: 'semibold', color: 'textPrimary', children: label })) : (jsxRuntime.jsx(Link, { href: href, onClick: onClick, color: 'secondary', underline: 'hover', children: label }))] }));
|
|
2621
|
+
};
|
|
2622
|
+
BreadcrumbItem.displayName = 'Breadcrumb.Item';
|
|
2623
|
+
|
|
2624
|
+
/**
|
|
2625
|
+
* WAI-ARIA compliant breadcrumb navigation using a compound component API.
|
|
2626
|
+
*
|
|
2627
|
+
* ```tsx
|
|
2628
|
+
* <Breadcrumb separator='/'>
|
|
2629
|
+
* <Breadcrumb.Item label='Home' href='/' />
|
|
2630
|
+
* <Breadcrumb.Item label='Products' href='/products' />
|
|
2631
|
+
* <Breadcrumb.Item label='Laptop Pro X' current />
|
|
2632
|
+
* </Breadcrumb>
|
|
2633
|
+
* ```
|
|
2634
|
+
*
|
|
2635
|
+
* The last item is typically marked as `current` — it renders as bold text (non-clickable)
|
|
2636
|
+
* and exposes `aria-current="page"` for assistive technologies.
|
|
2637
|
+
*/
|
|
2638
|
+
const BreadcrumbBase = ({ children, separator = '/', ariaLabel = 'Breadcrumb', }) => {
|
|
2639
|
+
const contextValue = React.useMemo(() => ({ separator }), [separator]);
|
|
2640
|
+
const items = React.Children.map(children, (child, index) => {
|
|
2641
|
+
if (!React.isValidElement(child)) {
|
|
2642
|
+
return child;
|
|
2643
|
+
}
|
|
2644
|
+
return React.cloneElement(child, {
|
|
2645
|
+
isFirst: index === 0,
|
|
2646
|
+
});
|
|
2647
|
+
});
|
|
2648
|
+
return (jsxRuntime.jsx(BreadcrumbContext.Provider, { value: contextValue, children: jsxRuntime.jsx("nav", { "aria-label": ariaLabel, className: BREADCRUMB_STYLES.nav, children: jsxRuntime.jsx("ol", { className: BREADCRUMB_STYLES.list, children: items }) }) }));
|
|
2649
|
+
};
|
|
2650
|
+
BreadcrumbBase.displayName = 'Breadcrumb';
|
|
2651
|
+
const Breadcrumb = BreadcrumbBase;
|
|
2652
|
+
Breadcrumb.Item = BreadcrumbItem;
|
|
2653
|
+
|
|
2546
2654
|
const DrawerContext = React.createContext({
|
|
2547
2655
|
isExpanded: true,
|
|
2548
2656
|
});
|
|
@@ -2898,6 +3006,234 @@ Drawer.Body = DrawerBody;
|
|
|
2898
3006
|
Drawer.Footer = DrawerFooter;
|
|
2899
3007
|
Drawer.Item = DrawerItem;
|
|
2900
3008
|
|
|
3009
|
+
const TABS_LIST_STYLES = theme.createStyles(() => ({
|
|
3010
|
+
root: {
|
|
3011
|
+
display: 'flex',
|
|
3012
|
+
flexDirection: 'row',
|
|
3013
|
+
alignItems: 'center',
|
|
3014
|
+
overflowX: 'auto',
|
|
3015
|
+
scrollbarWidth: 'none',
|
|
3016
|
+
width: 'fit-content',
|
|
3017
|
+
'::-webkit-scrollbar': { display: 'none' },
|
|
3018
|
+
},
|
|
3019
|
+
}));
|
|
3020
|
+
|
|
3021
|
+
const TabsContext = React.createContext(null);
|
|
3022
|
+
|
|
3023
|
+
/**
|
|
3024
|
+
* Internal hook — consumes the Tabs context and throws if used outside a `<Tabs>` provider.
|
|
3025
|
+
* @internal
|
|
3026
|
+
*/
|
|
3027
|
+
const useTabsContext = () => {
|
|
3028
|
+
const ctx = React.useContext(TabsContext);
|
|
3029
|
+
if (!ctx) {
|
|
3030
|
+
throw new Error('This component must be used inside a <Tabs> provider.');
|
|
3031
|
+
}
|
|
3032
|
+
return ctx;
|
|
3033
|
+
};
|
|
3034
|
+
|
|
3035
|
+
const TabsList = ({ children, ariaLabel, ariaLabelledBy }) => {
|
|
3036
|
+
const { value, setValue, baseId, getTabValues } = useTabsContext();
|
|
3037
|
+
/**
|
|
3038
|
+
* Returns true if the tab at the given index is aria-disabled.
|
|
3039
|
+
* We check the DOM attribute because disabled state lives in TabItem,
|
|
3040
|
+
* not in the shared context, and we want to avoid adding it to the context.
|
|
3041
|
+
*/
|
|
3042
|
+
const isTabDisabled = (tabValue) => {
|
|
3043
|
+
const el = document.getElementById(`${baseId}-tab-${tabValue}`);
|
|
3044
|
+
return el?.getAttribute('aria-disabled') === 'true';
|
|
3045
|
+
};
|
|
3046
|
+
/**
|
|
3047
|
+
* Moves focus to the nearest non-disabled tab starting from `startIndex`
|
|
3048
|
+
* and stepping in the given `direction` (+1 = right, -1 = left).
|
|
3049
|
+
* Stops after a full loop if all tabs are disabled.
|
|
3050
|
+
*/
|
|
3051
|
+
const focusNextEnabled = (startIndex, direction) => {
|
|
3052
|
+
const values = getTabValues();
|
|
3053
|
+
for (let i = 1; i <= values.length; i++) {
|
|
3054
|
+
const index = (startIndex + direction * i + values.length) % values.length;
|
|
3055
|
+
const nextValue = values[index];
|
|
3056
|
+
if (!isTabDisabled(nextValue)) {
|
|
3057
|
+
document.getElementById(`${baseId}-tab-${nextValue}`)?.focus();
|
|
3058
|
+
setValue(nextValue);
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
};
|
|
3063
|
+
const handleKeyDown = (event) => {
|
|
3064
|
+
const values = getTabValues();
|
|
3065
|
+
const currentIndex = values.indexOf(value);
|
|
3066
|
+
if (currentIndex === -1) {
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
switch (event.key) {
|
|
3070
|
+
case 'ArrowRight':
|
|
3071
|
+
event.preventDefault();
|
|
3072
|
+
focusNextEnabled(currentIndex, 1);
|
|
3073
|
+
break;
|
|
3074
|
+
case 'ArrowLeft':
|
|
3075
|
+
event.preventDefault();
|
|
3076
|
+
focusNextEnabled(currentIndex, -1);
|
|
3077
|
+
break;
|
|
3078
|
+
case 'Home': {
|
|
3079
|
+
event.preventDefault();
|
|
3080
|
+
// Focus the first non-disabled tab
|
|
3081
|
+
const firstIdx = values.findIndex((v) => !isTabDisabled(v));
|
|
3082
|
+
if (firstIdx !== -1) {
|
|
3083
|
+
document.getElementById(`${baseId}-tab-${values[firstIdx]}`)?.focus();
|
|
3084
|
+
setValue(values[firstIdx]);
|
|
3085
|
+
}
|
|
3086
|
+
break;
|
|
3087
|
+
}
|
|
3088
|
+
case 'End': {
|
|
3089
|
+
event.preventDefault();
|
|
3090
|
+
// Focus the last non-disabled tab
|
|
3091
|
+
const lastIdx = [...values].reverse().findIndex((v) => !isTabDisabled(v));
|
|
3092
|
+
if (lastIdx !== -1) {
|
|
3093
|
+
const realIndex = values.length - 1 - lastIdx;
|
|
3094
|
+
document.getElementById(`${baseId}-tab-${values[realIndex]}`)?.focus();
|
|
3095
|
+
setValue(values[realIndex]);
|
|
3096
|
+
}
|
|
3097
|
+
break;
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
};
|
|
3101
|
+
return (jsxRuntime.jsx("div", { role: 'tablist', "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className: TABS_LIST_STYLES.root, onKeyDown: handleKeyDown, children: children }));
|
|
3102
|
+
};
|
|
3103
|
+
TabsList.displayName = 'Tabs.List';
|
|
3104
|
+
|
|
3105
|
+
const TABS_PANEL_STYLES = theme.createStyles(() => ({
|
|
3106
|
+
root: {
|
|
3107
|
+
width: '100%',
|
|
3108
|
+
},
|
|
3109
|
+
}));
|
|
3110
|
+
|
|
3111
|
+
const TabsPanel = ({ value, children, keepMounted = false }) => {
|
|
3112
|
+
const { value: activeValue, baseId } = useTabsContext();
|
|
3113
|
+
const isActive = activeValue === value;
|
|
3114
|
+
if (!isActive && !keepMounted) {
|
|
3115
|
+
return null;
|
|
3116
|
+
}
|
|
3117
|
+
return (jsxRuntime.jsx("div", { role: 'tabpanel', id: `${baseId}-panel-${value}`, "aria-labelledby": `${baseId}-tab-${value}`,
|
|
3118
|
+
// WAI-ARIA APG: tabIndex={0} allows keyboard scrolling when the panel has no focusable child.
|
|
3119
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
3120
|
+
tabIndex: 0, hidden: !isActive, className: TABS_PANEL_STYLES.root, children: children }));
|
|
3121
|
+
};
|
|
3122
|
+
TabsPanel.displayName = 'Tabs.Panel';
|
|
3123
|
+
|
|
3124
|
+
const TAB_ITEM_STYLES = theme.createStyles((theme) => ({
|
|
3125
|
+
root: ({ isActive, disabled }) => ({
|
|
3126
|
+
display: 'inline-flex',
|
|
3127
|
+
alignItems: 'center',
|
|
3128
|
+
justifyContent: 'center',
|
|
3129
|
+
boxSizing: 'border-box',
|
|
3130
|
+
paddingTop: theme.spacing.xs,
|
|
3131
|
+
paddingBottom: theme.spacing.xs,
|
|
3132
|
+
paddingLeft: theme.spacing.md,
|
|
3133
|
+
paddingRight: theme.spacing.md,
|
|
3134
|
+
border: 'none',
|
|
3135
|
+
borderBottom: `2px solid ${isActive ? theme.colors.primaryMain : 'transparent'}`,
|
|
3136
|
+
background: 'transparent',
|
|
3137
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
3138
|
+
fontFamily: 'inherit',
|
|
3139
|
+
whiteSpace: 'nowrap',
|
|
3140
|
+
transition: `border-color ${theme.transition.fast}`,
|
|
3141
|
+
...(disabled ? {} : {
|
|
3142
|
+
':hover': {
|
|
3143
|
+
borderBottomColor: isActive ? theme.colors.primaryMain : theme.colors.textTertiary,
|
|
3144
|
+
},
|
|
3145
|
+
}),
|
|
3146
|
+
':focus-visible': {
|
|
3147
|
+
outline: `2px solid ${theme.colors.primaryMain}`,
|
|
3148
|
+
outlineOffset: '2px',
|
|
3149
|
+
},
|
|
3150
|
+
}),
|
|
3151
|
+
}));
|
|
3152
|
+
|
|
3153
|
+
/**
|
|
3154
|
+
* Handles tab registration, active state derivation and interaction props
|
|
3155
|
+
* for a single `Tabs.Tab` (TabItem) button.
|
|
3156
|
+
*
|
|
3157
|
+
* Registers the tab value into the shared ordered registry on mount so that
|
|
3158
|
+
* `TabsList` can perform correct ArrowLeft/Right/Home/End keyboard navigation.
|
|
3159
|
+
*/
|
|
3160
|
+
const useTabItem = ({ value, disabled }) => {
|
|
3161
|
+
const { value: activeValue, setValue, baseId, registerTab } = useTabsContext();
|
|
3162
|
+
// Maintains the ordered tab registry used by TabsList for keyboard navigation.
|
|
3163
|
+
React.useEffect(() => registerTab(value), [registerTab, value]);
|
|
3164
|
+
const isActive = activeValue === value;
|
|
3165
|
+
return {
|
|
3166
|
+
isActive,
|
|
3167
|
+
buttonProps: {
|
|
3168
|
+
id: `${baseId}-tab-${value}`,
|
|
3169
|
+
'aria-selected': isActive,
|
|
3170
|
+
'aria-controls': `${baseId}-panel-${value}`,
|
|
3171
|
+
'aria-disabled': disabled || undefined,
|
|
3172
|
+
tabIndex: isActive ? 0 : -1,
|
|
3173
|
+
onClick: () => {
|
|
3174
|
+
if (!disabled) {
|
|
3175
|
+
setValue(value);
|
|
3176
|
+
}
|
|
3177
|
+
},
|
|
3178
|
+
},
|
|
3179
|
+
};
|
|
3180
|
+
};
|
|
3181
|
+
|
|
3182
|
+
const TabItem = ({ value, label, disabled = false }) => {
|
|
3183
|
+
const { isActive, buttonProps } = useTabItem({ value, disabled });
|
|
3184
|
+
return (jsxRuntime.jsx("button", { type: 'button', role: 'tab', className: TAB_ITEM_STYLES.root({ isActive, disabled }), ...buttonProps, children: jsxRuntime.jsx(Text, { variant: 'span', fontSize: 'sm', fontWeight: isActive ? 'semibold' : 'medium', color: disabled ? 'textDisabled' : isActive ? 'textPrimary' : 'textSecondary', children: label }) }));
|
|
3185
|
+
};
|
|
3186
|
+
TabItem.displayName = 'Tabs.Tab';
|
|
3187
|
+
|
|
3188
|
+
/**
|
|
3189
|
+
* WAI-ARIA compliant tab interface using a compound component API.
|
|
3190
|
+
*
|
|
3191
|
+
* ```tsx
|
|
3192
|
+
* <Tabs defaultValue={'active'} onChange={handleChange}>
|
|
3193
|
+
* <Tabs.List ariaLabel={'Subscriptions'}>
|
|
3194
|
+
* <Tabs.Tab value={'active'} label={'Active'} />
|
|
3195
|
+
* <Tabs.Tab value={'cancelled'} label={'Cancelled'} />
|
|
3196
|
+
* </Tabs.List>
|
|
3197
|
+
* <Tabs.Panel value={'active'}>...</Tabs.Panel>
|
|
3198
|
+
* <Tabs.Panel value={'cancelled'}>...</Tabs.Panel>
|
|
3199
|
+
* </Tabs>
|
|
3200
|
+
* ```
|
|
3201
|
+
*
|
|
3202
|
+
* Supports controlled (`value` + `onChange`) and uncontrolled (`defaultValue`) modes.
|
|
3203
|
+
* Keyboard navigation: `ArrowLeft/Right`, `Home`, `End`.
|
|
3204
|
+
* Only the active panel is mounted — use `keepMounted` on `Tabs.Panel` to preserve state across switches.
|
|
3205
|
+
*/
|
|
3206
|
+
const TabsBase = ({ children, value: controlledValue, defaultValue, onChange, id, }) => {
|
|
3207
|
+
const reactId = React.useId();
|
|
3208
|
+
const baseId = id ?? `tabs-${reactId}`;
|
|
3209
|
+
const [internalValue, setInternalValue] = React.useState(defaultValue ?? '');
|
|
3210
|
+
const isControlled = controlledValue !== undefined;
|
|
3211
|
+
const value = isControlled ? controlledValue : internalValue;
|
|
3212
|
+
const tabsRef = React.useRef([]);
|
|
3213
|
+
const registerTab = React.useCallback((tabValue) => {
|
|
3214
|
+
if (!tabsRef.current.includes(tabValue)) {
|
|
3215
|
+
tabsRef.current.push(tabValue);
|
|
3216
|
+
}
|
|
3217
|
+
return () => {
|
|
3218
|
+
tabsRef.current = tabsRef.current.filter((v) => v !== tabValue);
|
|
3219
|
+
};
|
|
3220
|
+
}, []);
|
|
3221
|
+
const getTabValues = React.useCallback(() => tabsRef.current, []);
|
|
3222
|
+
const setValue = React.useCallback((next) => {
|
|
3223
|
+
if (!isControlled) {
|
|
3224
|
+
setInternalValue(next);
|
|
3225
|
+
}
|
|
3226
|
+
onChange?.(next);
|
|
3227
|
+
}, [isControlled, onChange]);
|
|
3228
|
+
const contextValue = React.useMemo(() => ({ value, setValue, baseId, registerTab, getTabValues }), [value, setValue, baseId, registerTab, getTabValues]);
|
|
3229
|
+
return (jsxRuntime.jsx(TabsContext.Provider, { value: contextValue, children: children }));
|
|
3230
|
+
};
|
|
3231
|
+
TabsBase.displayName = 'Tabs';
|
|
3232
|
+
const Tabs = TabsBase;
|
|
3233
|
+
Tabs.List = TabsList;
|
|
3234
|
+
Tabs.Tab = TabItem;
|
|
3235
|
+
Tabs.Panel = TabsPanel;
|
|
3236
|
+
|
|
2901
3237
|
const AlertContext = React.createContext({
|
|
2902
3238
|
variant: 'default',
|
|
2903
3239
|
accentColor: 'defaultActive',
|
|
@@ -2950,6 +3286,15 @@ const VARIANT_ARIA_LIVE = {
|
|
|
2950
3286
|
warning: 'polite',
|
|
2951
3287
|
info: 'polite',
|
|
2952
3288
|
};
|
|
3289
|
+
/** role="alert" implies aria-live="assertive" — reserve it for errors.
|
|
3290
|
+
* Other variants use role="status" (implies aria-live="polite"). */
|
|
3291
|
+
const VARIANT_ROLE = {
|
|
3292
|
+
default: 'status',
|
|
3293
|
+
success: 'status',
|
|
3294
|
+
error: 'alert',
|
|
3295
|
+
warning: 'status',
|
|
3296
|
+
info: 'status',
|
|
3297
|
+
};
|
|
2953
3298
|
/**
|
|
2954
3299
|
* Inline alert banner with 5 visual variants: default, success, error, warning, info.
|
|
2955
3300
|
* Use `Alert.Title` (icon + heading) and `Alert.Body` (message) as children.
|
|
@@ -2962,7 +3307,7 @@ const VARIANT_ARIA_LIVE = {
|
|
|
2962
3307
|
*/
|
|
2963
3308
|
const AlertBase = ({ variant = 'default', children, width = '100%', outline = false, shadow = 'none', }) => {
|
|
2964
3309
|
const { backgroundColor, borderColor, accentColor } = VARIANT_TOKENS[variant];
|
|
2965
|
-
return (jsxRuntime.jsx(AlertContext.Provider, { value: { variant, accentColor }, children: jsxRuntime.jsx(Stack, { role:
|
|
3310
|
+
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
3311
|
};
|
|
2967
3312
|
AlertBase.displayName = 'Alert';
|
|
2968
3313
|
const Alert = AlertBase;
|
|
@@ -3129,7 +3474,7 @@ const useDialog = ({ open, onClose, closeOnBackdropClick, maxWidth, maxHeight, m
|
|
|
3129
3474
|
*/
|
|
3130
3475
|
const DialogHeader = ({ title, onClose }) => {
|
|
3131
3476
|
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 })] }));
|
|
3477
|
+
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
3478
|
};
|
|
3134
3479
|
DialogHeader.displayName = 'Dialog.Header';
|
|
3135
3480
|
|
|
@@ -3561,6 +3906,7 @@ exports.Alert = Alert;
|
|
|
3561
3906
|
exports.Backdrop = Backdrop;
|
|
3562
3907
|
exports.Badge = Badge;
|
|
3563
3908
|
exports.Box = Box;
|
|
3909
|
+
exports.Breadcrumb = Breadcrumb;
|
|
3564
3910
|
exports.Button = Button;
|
|
3565
3911
|
exports.Card = Card;
|
|
3566
3912
|
exports.Checkbox = Checkbox;
|
|
@@ -3577,6 +3923,7 @@ exports.Select = Select;
|
|
|
3577
3923
|
exports.Skeleton = Skeleton;
|
|
3578
3924
|
exports.Stack = Stack;
|
|
3579
3925
|
exports.Switch = Switch;
|
|
3926
|
+
exports.Tabs = Tabs;
|
|
3580
3927
|
exports.Text = Text;
|
|
3581
3928
|
exports.TextField = TextField;
|
|
3582
3929
|
exports.Tooltip = Tooltip;
|