@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/esm/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
|
|
2
2
|
import { keyframes, createStyles, useTheme, cx, createVariants, createTheme } from '@aurora-ds/theme';
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import { createElement, Fragment, useRef, useState, useCallback, useEffect, useId, isValidElement, cloneElement, useLayoutEffect, useMemo, createContext, useContext } from 'react';
|
|
4
|
+
import { createElement, Fragment, useRef, useState, useCallback, useEffect, useId, isValidElement, cloneElement, useLayoutEffect, useMemo, createContext, useContext, Children } from 'react';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
6
|
|
|
7
7
|
function _extends$8() { return _extends$8 = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends$8.apply(null, arguments); }
|
|
@@ -574,7 +574,7 @@ const ICON_SIZE$2 = {
|
|
|
574
574
|
* @example <Button label='Delete' variant='outlined' color='error' startIcon={IconRegistry.CloseIcon} />
|
|
575
575
|
* @example <Button label='Submitting…' color='success' isLoading width='100%' />
|
|
576
576
|
*/
|
|
577
|
-
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 }) => {
|
|
577
|
+
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 }) => {
|
|
578
578
|
const isDisabled = disabled || isLoading;
|
|
579
579
|
const iconSize = ICON_SIZE$2[size];
|
|
580
580
|
const rootClassName = cx(BUTTON_STYLES.root({ variant, color, size }), className);
|
|
@@ -584,7 +584,7 @@ const Button = ({ ref, variant = 'contained', color = 'primary', size = 'md', wi
|
|
|
584
584
|
...(flexGrow !== undefined ? { flexGrow } : {}),
|
|
585
585
|
...(flexShrink !== undefined ? { flexShrink } : {}),
|
|
586
586
|
};
|
|
587
|
-
return (jsxs("button", { ref: ref, type: type, className: rootClassName, disabled: isDisabled, "aria-busy": isLoading || undefined, style: mergedStyle, ...rest, children: [isLoading && (jsx("span", { className: BUTTON_STYLES.spinnerWrap, children: jsx(Icon, { icon: SpinnerIcon, size: iconSize, className: BUTTON_STYLES.spinnerIcon }) })), jsxs("span", { className: cx(BUTTON_STYLES.content, isLoading && BUTTON_STYLES.contentHidden), children: [StartIcon && (jsx(Icon, { icon: StartIcon, size: iconSize })), label && (jsx(Text, { variant: 'span', fontSize: LABEL_FONT_SIZE$1[size], fontWeight: 'medium', lineHeight: 'none', children: label })), EndIcon && (jsx(Icon, { icon: EndIcon, size: iconSize }))] })] }));
|
|
587
|
+
return (jsxs("button", { ref: ref, type: type, className: rootClassName, disabled: isDisabled, "aria-busy": isLoading || undefined, style: mergedStyle, ...rest, children: [isLoading && (jsx("span", { className: BUTTON_STYLES.spinnerWrap, children: jsx(Icon, { icon: SpinnerIcon, size: iconSize, className: BUTTON_STYLES.spinnerIcon }) })), jsxs("span", { className: cx(BUTTON_STYLES.content, isLoading && BUTTON_STYLES.contentHidden), children: [StartIcon && (jsx(Icon, { icon: StartIcon, size: iconSize })), (label !== undefined || children !== undefined) && (jsx(Text, { variant: 'span', fontSize: LABEL_FONT_SIZE$1[size], fontWeight: 'medium', lineHeight: 'none', children: label ?? children })), EndIcon && (jsx(Icon, { icon: EndIcon, size: iconSize }))] })] }));
|
|
588
588
|
};
|
|
589
589
|
Button.displayName = 'Button';
|
|
590
590
|
|
|
@@ -641,36 +641,42 @@ const IconButton = ({ ref, icon: IconComponent, ariaLabel, variant = 'contained'
|
|
|
641
641
|
IconButton.displayName = 'IconButton';
|
|
642
642
|
|
|
643
643
|
const LINK_STYLES = createStyles((theme) => ({
|
|
644
|
-
root: ({ underline = 'hover' }) =>
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
color: theme.colors.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
644
|
+
root: ({ underline = 'hover', color = 'default' }) => {
|
|
645
|
+
const mainColor = color === 'secondary' ? theme.colors.textSecondary : theme.colors.linkMain;
|
|
646
|
+
const hoverColor = color === 'secondary' ? theme.colors.textTertiary : theme.colors.linkHover;
|
|
647
|
+
const activeColor = color === 'secondary' ? theme.colors.textPrimary : theme.colors.linkActive;
|
|
648
|
+
const disabledColor = color === 'secondary' ? theme.colors.textDisabled : theme.colors.linkDisabled;
|
|
649
|
+
return {
|
|
650
|
+
display: 'inline-flex',
|
|
651
|
+
alignItems: 'center',
|
|
652
|
+
gap: '0.25em',
|
|
653
|
+
color: mainColor,
|
|
654
|
+
fontFamily: 'inherit',
|
|
655
|
+
fontSize: 'inherit',
|
|
656
|
+
lineHeight: 'inherit',
|
|
657
|
+
fontWeight: 'inherit',
|
|
658
|
+
textDecoration: underline === 'always' ? 'underline' : 'none',
|
|
659
|
+
cursor: 'pointer',
|
|
660
|
+
borderRadius: theme.radius.xs,
|
|
661
|
+
transition: `color ${theme.transition.fast}`,
|
|
662
|
+
':hover:not([aria-disabled="true"])': {
|
|
663
|
+
color: hoverColor,
|
|
664
|
+
textDecoration: underline !== 'none' ? 'underline' : 'none',
|
|
665
|
+
},
|
|
666
|
+
':active:not([aria-disabled="true"])': {
|
|
667
|
+
color: activeColor,
|
|
668
|
+
},
|
|
669
|
+
':focus-visible': {
|
|
670
|
+
outline: `2px solid ${mainColor}`,
|
|
671
|
+
outlineOffset: '2px',
|
|
672
|
+
},
|
|
673
|
+
'&[aria-disabled="true"]': {
|
|
674
|
+
color: disabledColor,
|
|
675
|
+
cursor: 'not-allowed',
|
|
676
|
+
textDecoration: 'none',
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
},
|
|
674
680
|
icon: {
|
|
675
681
|
display: 'inline-flex',
|
|
676
682
|
alignItems: 'center',
|
|
@@ -683,12 +689,20 @@ const LINK_STYLES = createStyles((theme) => ({
|
|
|
683
689
|
/**
|
|
684
690
|
* Theme-aware anchor element with optional icons and underline control.
|
|
685
691
|
*
|
|
692
|
+
* Supports SPA navigation (e.g. React Router) via `onClick` without `href`.
|
|
693
|
+
* In that case the component stays accessible: it gets `role="link"`,
|
|
694
|
+
* `tabIndex={0}` and keyboard Enter support automatically.
|
|
695
|
+
*
|
|
686
696
|
* @example <Link href='/about'>About</Link>
|
|
687
697
|
* @example <Link href='https://example.com' external>External site</Link>
|
|
688
698
|
* @example <Link href='/profile' underline='always' startIcon={UserIcon}>Profile</Link>
|
|
689
699
|
* @example <Link href='/terms' underline='none'>Terms</Link>
|
|
700
|
+
* @example <Link onClick={() => navigate('/about')}>About (SPA)</Link>
|
|
690
701
|
*/
|
|
691
|
-
const Link = ({ ref, underline = 'hover', external = false, disabled = false, startIcon: StartIcon, endIcon: EndIcon, children, className, onClick, onKeyDown, ...rest }) => {
|
|
702
|
+
const Link = ({ ref, underline = 'hover', color = 'default', external = false, disabled = false, startIcon: StartIcon, endIcon: EndIcon, children, className, href, onClick, onKeyDown, ...rest }) => {
|
|
703
|
+
// An <a> without href has no implicit ARIA role and is not focusable.
|
|
704
|
+
// When used for SPA navigation (onClick only), we restore both behaviours.
|
|
705
|
+
const hasHref = !!href;
|
|
692
706
|
const handleClick = (e) => {
|
|
693
707
|
if (disabled) {
|
|
694
708
|
e.preventDefault();
|
|
@@ -696,14 +710,22 @@ const Link = ({ ref, underline = 'hover', external = false, disabled = false, st
|
|
|
696
710
|
}
|
|
697
711
|
onClick?.(e);
|
|
698
712
|
};
|
|
699
|
-
// Prevents Enter navigation when disabled; satisfies jsx-a11y/click-events-have-key-events.
|
|
700
713
|
const handleKeyDown = (e) => {
|
|
701
714
|
if (disabled && e.key === 'Enter') {
|
|
702
715
|
e.preventDefault();
|
|
703
716
|
}
|
|
717
|
+
// Without href, the browser does not fire a click on Enter natively.
|
|
718
|
+
if (!hasHref && !disabled && e.key === 'Enter') {
|
|
719
|
+
e.currentTarget.click();
|
|
720
|
+
}
|
|
704
721
|
onKeyDown?.(e);
|
|
705
722
|
};
|
|
706
|
-
return (jsxs("a", { ref: ref, className: cx(LINK_STYLES.root({ underline }), className), "aria-disabled": disabled || undefined,
|
|
723
|
+
return (jsxs("a", { ref: ref, href: href, className: cx(LINK_STYLES.root({ underline, color }), className), "aria-disabled": disabled || undefined,
|
|
724
|
+
// Without href: must be explicitly put in the tab order.
|
|
725
|
+
// With href: the browser handles focusability natively (no tabIndex needed).
|
|
726
|
+
tabIndex: disabled ? -1 : (!hasHref ? 0 : undefined),
|
|
727
|
+
// Without href: <a> has no implicit ARIA role — add role="link" explicitly.
|
|
728
|
+
role: !hasHref ? 'link' : undefined, target: external ? '_blank' : undefined, rel: external ? 'noopener noreferrer' : undefined, onClick: handleClick, onKeyDown: handleKeyDown, ...rest, children: [StartIcon && (jsx("span", { className: LINK_STYLES.icon, "aria-hidden": true, children: jsx(StartIcon, { width: '1em', height: '1em' }) })), children, EndIcon && (jsx("span", { className: LINK_STYLES.icon, "aria-hidden": true, children: jsx(EndIcon, { width: '1em', height: '1em' }) }))] }));
|
|
707
729
|
};
|
|
708
730
|
Link.displayName = 'Link';
|
|
709
731
|
|
|
@@ -1759,7 +1781,7 @@ const useTextField = ({ id, ref, type, size, endAction, }) => {
|
|
|
1759
1781
|
*/
|
|
1760
1782
|
const TextField = ({ ref, label, helperText, size = 'md', status = 'default', startIcon: StartIcon, endAction, type, id, disabled, required, ...rest }) => {
|
|
1761
1783
|
const { fieldId, helperId, mergedRef, isPassword, showPassword, togglePassword, resolvedType, iconSize, iconButtonSize, hasEndSection, focusInput, } = useTextField({ id, ref, type, size, endAction });
|
|
1762
|
-
return (jsxs(Stack, { flexDirection: 'column', gap: 'xs', children: [label !== undefined && (jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, children: [label, required && (jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsxs("div", { className: TEXTFIELD_WRAPPER_VARIANTS({ size, status }), "data-disabled": disabled || undefined, children: [StartIcon && (jsx("span", { className: TEXTFIELD_STYLES.startIconWrap, onClick: focusInput, children: jsx(Icon, { icon: StartIcon, size: iconSize, strokeColor: 'textSecondary' }) })), 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 && (jsxs("span", { className: TEXTFIELD_STYLES.endActionWrap, children: [endAction, isPassword && (jsx(IconButton, { icon: showPassword ? EyeSlashIcon : EyeIcon, ariaLabel: showPassword ? 'Hide password' : 'Show password', variant: 'text', color: 'neutral', size: iconButtonSize, type: 'button',
|
|
1784
|
+
return (jsxs(Stack, { flexDirection: 'column', gap: 'xs', children: [label !== undefined && (jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, children: [label, required && (jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsxs("div", { className: TEXTFIELD_WRAPPER_VARIANTS({ size, status }), "data-disabled": disabled || undefined, children: [StartIcon && (jsx("span", { className: TEXTFIELD_STYLES.startIconWrap, onClick: focusInput, "aria-hidden": true, children: jsx(Icon, { icon: StartIcon, size: iconSize, strokeColor: 'textSecondary' }) })), 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 && (jsxs("span", { className: TEXTFIELD_STYLES.endActionWrap, children: [endAction, isPassword && (jsx(IconButton, { icon: showPassword ? EyeSlashIcon : EyeIcon, ariaLabel: showPassword ? 'Hide password' : 'Show password', variant: 'text', color: 'neutral', size: iconButtonSize, type: 'button', onClick: togglePassword }))] }))] }), helperText !== undefined && (jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
|
|
1763
1785
|
};
|
|
1764
1786
|
TextField.displayName = 'TextField';
|
|
1765
1787
|
|
|
@@ -1894,7 +1916,9 @@ const useMenuPosition = ({ anchorEl, open, menuRef, minWidth, gap = 4, }) => {
|
|
|
1894
1916
|
*/
|
|
1895
1917
|
const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
|
|
1896
1918
|
const panelRef = useRef(null);
|
|
1919
|
+
const baseId = useId();
|
|
1897
1920
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
1921
|
+
const [activeDescendant, setActiveDescendant] = useState(undefined);
|
|
1898
1922
|
const { style } = useMenuPosition({ anchorEl, open, menuRef: panelRef, minWidth });
|
|
1899
1923
|
/** Returns all non-disabled option elements inside the panel. */
|
|
1900
1924
|
const getOptions = useCallback(() => {
|
|
@@ -1917,22 +1941,31 @@ const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
|
|
|
1917
1941
|
});
|
|
1918
1942
|
return () => cancelAnimationFrame(raf);
|
|
1919
1943
|
}, [open, getOptions]);
|
|
1920
|
-
// Keep data-focused
|
|
1944
|
+
// Keep data-focused (visual highlight) and aria-activedescendant (screen reader
|
|
1945
|
+
// announcement) in sync with focusedIndex. Each option is assigned a stable id
|
|
1946
|
+
// so the listbox can reference the active one via aria-activedescendant.
|
|
1921
1947
|
useEffect(() => {
|
|
1922
1948
|
if (!open) {
|
|
1949
|
+
setActiveDescendant(undefined);
|
|
1923
1950
|
return;
|
|
1924
1951
|
}
|
|
1925
1952
|
const options = getOptions();
|
|
1953
|
+
let activeId;
|
|
1926
1954
|
options.forEach((el, idx) => {
|
|
1955
|
+
if (!el.id) {
|
|
1956
|
+
el.id = `${baseId}-option-${idx}`;
|
|
1957
|
+
}
|
|
1927
1958
|
if (idx === focusedIndex) {
|
|
1928
1959
|
el.setAttribute('data-focused', 'true');
|
|
1929
1960
|
el.scrollIntoView({ block: 'nearest' });
|
|
1961
|
+
activeId = el.id;
|
|
1930
1962
|
}
|
|
1931
1963
|
else {
|
|
1932
1964
|
el.removeAttribute('data-focused');
|
|
1933
1965
|
}
|
|
1934
1966
|
});
|
|
1935
|
-
|
|
1967
|
+
setActiveDescendant(activeId);
|
|
1968
|
+
}, [focusedIndex, open, getOptions, baseId]);
|
|
1936
1969
|
useKeyPress({
|
|
1937
1970
|
Escape: onClose,
|
|
1938
1971
|
ArrowDown: (e) => {
|
|
@@ -1970,7 +2003,7 @@ const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
|
|
|
1970
2003
|
}
|
|
1971
2004
|
},
|
|
1972
2005
|
}, { enabled: open });
|
|
1973
|
-
return { panelRef, style };
|
|
2006
|
+
return { panelRef, style, activeDescendant };
|
|
1974
2007
|
};
|
|
1975
2008
|
|
|
1976
2009
|
const MENU_GROUP_STYLES = createStyles((theme) => {
|
|
@@ -2011,7 +2044,8 @@ const MENU_GROUP_STYLES = createStyles((theme) => {
|
|
|
2011
2044
|
}, { id: 'menu-group' });
|
|
2012
2045
|
|
|
2013
2046
|
const MenuGroup = ({ label, divider, children, }) => {
|
|
2014
|
-
|
|
2047
|
+
const labelId = useId();
|
|
2048
|
+
return (jsxs("div", { className: MENU_GROUP_STYLES.root, children: [divider && (jsx("div", { className: MENU_GROUP_STYLES.divider, role: 'separator', "aria-hidden": true })), label !== undefined && (jsx("span", { id: labelId, className: MENU_GROUP_STYLES.label, "aria-hidden": true, children: label })), jsx("ul", { className: MENU_GROUP_STYLES.list, role: 'group', "aria-labelledby": label !== undefined ? labelId : undefined, children: children })] }));
|
|
2015
2049
|
};
|
|
2016
2050
|
MenuGroup.displayName = 'MenuGroup';
|
|
2017
2051
|
|
|
@@ -2062,12 +2096,12 @@ const MenuItem = ({ ref, label, icon, selected, focused, disabled, onClick, ...r
|
|
|
2062
2096
|
};
|
|
2063
2097
|
MenuItem.displayName = 'MenuItem';
|
|
2064
2098
|
|
|
2065
|
-
const MenuBase = ({ open, onClose, anchorEl, minWidth, maxHeight = '20rem', id, children, }) => {
|
|
2066
|
-
const { panelRef, style } = useMenu({ open, onClose, anchorEl, minWidth });
|
|
2099
|
+
const MenuBase = ({ open, onClose, anchorEl, minWidth, maxHeight = '20rem', id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, children, }) => {
|
|
2100
|
+
const { panelRef, style, activeDescendant } = useMenu({ open, onClose, anchorEl, minWidth });
|
|
2067
2101
|
if (!open) {
|
|
2068
2102
|
return null;
|
|
2069
2103
|
}
|
|
2070
|
-
return createPortal(jsxs(Fragment$1, { children: [jsx("div", { className: MENU_STYLES.backdrop, onClick: onClose, "aria-hidden": true }), jsx("div", { ref: panelRef, id: id, role: 'listbox', tabIndex: -1, className: MENU_STYLES.panel, style: { ...style, maxHeight, outline: 'none' }, children: children })] }), document.body);
|
|
2104
|
+
return createPortal(jsxs(Fragment$1, { children: [jsx("div", { className: MENU_STYLES.backdrop, onClick: onClose, "aria-hidden": true }), 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);
|
|
2071
2105
|
};
|
|
2072
2106
|
MenuBase.displayName = 'Menu';
|
|
2073
2107
|
const Menu = MenuBase;
|
|
@@ -2189,9 +2223,9 @@ const ICON_SIZE_MAP = {
|
|
|
2189
2223
|
md: 'md',
|
|
2190
2224
|
lg: 'lg',
|
|
2191
2225
|
};
|
|
2192
|
-
const SelectTrigger = ({ ref, size = 'md', status = 'default', open, hasValue, startIcon, disabled, children, ...rest }) => {
|
|
2226
|
+
const SelectTrigger = ({ ref, size = 'md', status = 'default', open, hasValue, startIcon, disabled, children, 'aria-expanded': ariaExpanded, 'aria-controls': ariaControls, ...rest }) => {
|
|
2193
2227
|
const iconSize = ICON_SIZE_MAP[size];
|
|
2194
|
-
return (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 && (jsx(Icon, { icon: startIcon, size: iconSize, strokeColor: 'textSecondary' })), jsx("span", { className: hasValue ? SELECT_TRIGGER_STYLES.value : SELECT_TRIGGER_STYLES.placeholder, children: children }), jsx(Icon, { icon: ChevronDownIcon, size: iconSize, className: cx(SELECT_TRIGGER_STYLES.chevron, open ? SELECT_TRIGGER_STYLES.chevronOpen : undefined), strokeColor: 'textSecondary' })] }));
|
|
2228
|
+
return (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 && (jsx(Icon, { icon: startIcon, size: iconSize, strokeColor: 'textSecondary' })), jsx("span", { className: hasValue ? SELECT_TRIGGER_STYLES.value : SELECT_TRIGGER_STYLES.placeholder, children: children }), jsx(Icon, { icon: ChevronDownIcon, size: iconSize, className: cx(SELECT_TRIGGER_STYLES.chevron, open ? SELECT_TRIGGER_STYLES.chevronOpen : undefined), strokeColor: 'textSecondary' })] }));
|
|
2195
2229
|
};
|
|
2196
2230
|
SelectTrigger.displayName = 'SelectTrigger';
|
|
2197
2231
|
|
|
@@ -2205,6 +2239,7 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled,
|
|
|
2205
2239
|
const fieldId = id ?? generatedId;
|
|
2206
2240
|
const helperId = `${fieldId}-helper`;
|
|
2207
2241
|
const menuId = `${fieldId}-menu`;
|
|
2242
|
+
const labelId = `${fieldId}-label`;
|
|
2208
2243
|
const triggerRef = useRef(null);
|
|
2209
2244
|
const mergedRef = useMergedRefs(ref, triggerRef);
|
|
2210
2245
|
const [open, setOpen] = useState(false);
|
|
@@ -2248,6 +2283,7 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled,
|
|
|
2248
2283
|
const close = useCallback(() => setOpen(false), []);
|
|
2249
2284
|
return {
|
|
2250
2285
|
fieldId,
|
|
2286
|
+
labelId,
|
|
2251
2287
|
helperId,
|
|
2252
2288
|
menuId,
|
|
2253
2289
|
triggerRef,
|
|
@@ -2263,8 +2299,8 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled,
|
|
|
2263
2299
|
};
|
|
2264
2300
|
|
|
2265
2301
|
const Select = ({ ref, value, defaultValue, onChange, options, label, helperText, placeholder, size = 'md', status = 'default', disabled, required, width, id, }) => {
|
|
2266
|
-
const { fieldId, helperId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect, } = useSelect({ id, ref, value, defaultValue, onChange, options, disabled });
|
|
2267
|
-
return (jsxs(Stack, { flexDirection: 'column', gap: 'xs', style: { width: width ?? '100%' }, children: [label !== undefined && (jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, children: [label, required && (jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsx(SelectTrigger, { ref: mergedRef, id: fieldId, size: size, status: status, open: open, hasValue: selectedOption !== undefined, disabled: disabled,
|
|
2302
|
+
const { fieldId, labelId, helperId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect, } = useSelect({ id, ref, value, defaultValue, onChange, options, disabled });
|
|
2303
|
+
return (jsxs(Stack, { flexDirection: 'column', gap: 'xs', style: { width: width ?? '100%' }, children: [label !== undefined && (jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, id: labelId, children: [label, required && (jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsx(SelectTrigger, { ref: mergedRef, id: fieldId, size: size, status: status, open: open, hasValue: selectedOption !== undefined, disabled: disabled, "aria-haspopup": 'listbox', "aria-expanded": open, "aria-controls": menuId, "aria-labelledby": label !== undefined ? `${labelId} ${fieldId}` : undefined, "aria-required": required || undefined, "aria-invalid": status === 'error' || undefined, "aria-errormessage": status === 'error' && helperText !== undefined ? helperId : undefined, "aria-describedby": helperText !== undefined ? helperId : undefined, onClick: toggle, children: selectedOption !== undefined ? selectedOption.label : placeholder }), jsx(Menu, { open: open, onClose: close, anchorEl: triggerRef.current, id: menuId, "aria-labelledby": label !== undefined ? labelId : undefined, "aria-label": label === undefined ? placeholder : undefined, children: Array.from(groupedOptions.entries()).map(([groupKey, groupOpts], groupIndex) => {
|
|
2268
2304
|
const items = groupOpts.map((opt) => (jsx(Menu.Item, { label: opt.label, icon: opt.icon, selected: opt.value === currentValue, disabled: opt.disabled, onClick: () => handleSelect(opt.value) }, opt.value)));
|
|
2269
2305
|
return groupKey !== undefined ? (jsx(Menu.Group, { label: groupKey, divider: groupIndex > 0, children: items }, groupKey)) : (jsx(Menu.Group, { divider: groupIndex > 0, children: items }, '__ungrouped'));
|
|
2270
2306
|
}) }), helperText !== undefined && (jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
|
|
@@ -2432,7 +2468,7 @@ const useCheckbox = ({ id, ref, indeterminate = false }) => {
|
|
|
2432
2468
|
const Checkbox = ({ ref, label, helperText, size = 'md', status = 'default', indeterminate = false, error, id, disabled, required, ...rest }) => {
|
|
2433
2469
|
const resolvedStatus = error ? 'error' : status;
|
|
2434
2470
|
const { checkboxId, helperId, mergedRef } = useCheckbox({ id, ref, indeterminate });
|
|
2435
|
-
return (jsxs("div", { className: CHECKBOX_STYLES.wrapper, children: [jsxs("label", { htmlFor: checkboxId, className: CHECKBOX_ROOT_VARIANTS({ disabled: disabled ? 'true' : 'false' }), children: [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({
|
|
2471
|
+
return (jsxs("div", { className: CHECKBOX_STYLES.wrapper, children: [jsxs("label", { htmlFor: checkboxId, className: CHECKBOX_ROOT_VARIANTS({ disabled: disabled ? 'true' : 'false' }), children: [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({
|
|
2436
2472
|
size,
|
|
2437
2473
|
status: resolvedStatus,
|
|
2438
2474
|
disabled: disabled ? 'true' : 'false',
|
|
@@ -2523,6 +2559,78 @@ const Grid = ({ display = 'grid', columns, rows, autoFlow, autoColumns, autoRows
|
|
|
2523
2559
|
};
|
|
2524
2560
|
Grid.displayName = 'Grid';
|
|
2525
2561
|
|
|
2562
|
+
const BREADCRUMB_STYLES = createStyles((_theme) => ({
|
|
2563
|
+
nav: {
|
|
2564
|
+
display: 'block',
|
|
2565
|
+
},
|
|
2566
|
+
list: {
|
|
2567
|
+
display: 'flex',
|
|
2568
|
+
flexWrap: 'wrap',
|
|
2569
|
+
alignItems: 'center',
|
|
2570
|
+
listStyle: 'none',
|
|
2571
|
+
margin: 0,
|
|
2572
|
+
padding: 0,
|
|
2573
|
+
},
|
|
2574
|
+
}), { id: 'breadcrumb' });
|
|
2575
|
+
|
|
2576
|
+
const BreadcrumbContext = createContext({
|
|
2577
|
+
separator: '/',
|
|
2578
|
+
});
|
|
2579
|
+
const useBreadcrumbContext = () => useContext(BreadcrumbContext);
|
|
2580
|
+
|
|
2581
|
+
const BREADCRUMB_ITEM_STYLES = createStyles((theme) => ({
|
|
2582
|
+
item: {
|
|
2583
|
+
display: 'inline-flex',
|
|
2584
|
+
alignItems: 'center',
|
|
2585
|
+
fontSize: theme.fontSize.sm,
|
|
2586
|
+
lineHeight: theme.lineHeight.normal,
|
|
2587
|
+
},
|
|
2588
|
+
separator: {
|
|
2589
|
+
display: 'inline-flex',
|
|
2590
|
+
alignItems: 'center',
|
|
2591
|
+
marginLeft: theme.spacing.sm,
|
|
2592
|
+
marginRight: theme.spacing.sm,
|
|
2593
|
+
color: theme.colors.textTertiary,
|
|
2594
|
+
userSelect: 'none',
|
|
2595
|
+
},
|
|
2596
|
+
}), { id: 'breadcrumb-item' });
|
|
2597
|
+
|
|
2598
|
+
const BreadcrumbItem = ({ label, href, onClick, current = false, isFirst = false, }) => {
|
|
2599
|
+
const { separator } = useBreadcrumbContext();
|
|
2600
|
+
return (jsxs("li", { className: BREADCRUMB_ITEM_STYLES.item, "aria-current": current ? 'page' : undefined, children: [!isFirst && (jsx("span", { "aria-hidden": true, className: BREADCRUMB_ITEM_STYLES.separator, children: separator })), current ? (jsx(Text, { variant: 'span', fontWeight: 'semibold', color: 'textPrimary', children: label })) : (jsx(Link, { href: href, onClick: onClick, color: 'secondary', underline: 'hover', children: label }))] }));
|
|
2601
|
+
};
|
|
2602
|
+
BreadcrumbItem.displayName = 'Breadcrumb.Item';
|
|
2603
|
+
|
|
2604
|
+
/**
|
|
2605
|
+
* WAI-ARIA compliant breadcrumb navigation using a compound component API.
|
|
2606
|
+
*
|
|
2607
|
+
* ```tsx
|
|
2608
|
+
* <Breadcrumb separator='/'>
|
|
2609
|
+
* <Breadcrumb.Item label='Home' href='/' />
|
|
2610
|
+
* <Breadcrumb.Item label='Products' href='/products' />
|
|
2611
|
+
* <Breadcrumb.Item label='Laptop Pro X' current />
|
|
2612
|
+
* </Breadcrumb>
|
|
2613
|
+
* ```
|
|
2614
|
+
*
|
|
2615
|
+
* The last item is typically marked as `current` — it renders as bold text (non-clickable)
|
|
2616
|
+
* and exposes `aria-current="page"` for assistive technologies.
|
|
2617
|
+
*/
|
|
2618
|
+
const BreadcrumbBase = ({ children, separator = '/', ariaLabel = 'Breadcrumb', }) => {
|
|
2619
|
+
const contextValue = useMemo(() => ({ separator }), [separator]);
|
|
2620
|
+
const items = Children.map(children, (child, index) => {
|
|
2621
|
+
if (!isValidElement(child)) {
|
|
2622
|
+
return child;
|
|
2623
|
+
}
|
|
2624
|
+
return cloneElement(child, {
|
|
2625
|
+
isFirst: index === 0,
|
|
2626
|
+
});
|
|
2627
|
+
});
|
|
2628
|
+
return (jsx(BreadcrumbContext.Provider, { value: contextValue, children: jsx("nav", { "aria-label": ariaLabel, className: BREADCRUMB_STYLES.nav, children: jsx("ol", { className: BREADCRUMB_STYLES.list, children: items }) }) }));
|
|
2629
|
+
};
|
|
2630
|
+
BreadcrumbBase.displayName = 'Breadcrumb';
|
|
2631
|
+
const Breadcrumb = BreadcrumbBase;
|
|
2632
|
+
Breadcrumb.Item = BreadcrumbItem;
|
|
2633
|
+
|
|
2526
2634
|
const DrawerContext = createContext({
|
|
2527
2635
|
isExpanded: true,
|
|
2528
2636
|
});
|
|
@@ -2878,6 +2986,234 @@ Drawer.Body = DrawerBody;
|
|
|
2878
2986
|
Drawer.Footer = DrawerFooter;
|
|
2879
2987
|
Drawer.Item = DrawerItem;
|
|
2880
2988
|
|
|
2989
|
+
const TABS_LIST_STYLES = createStyles(() => ({
|
|
2990
|
+
root: {
|
|
2991
|
+
display: 'flex',
|
|
2992
|
+
flexDirection: 'row',
|
|
2993
|
+
alignItems: 'center',
|
|
2994
|
+
overflowX: 'auto',
|
|
2995
|
+
scrollbarWidth: 'none',
|
|
2996
|
+
width: 'fit-content',
|
|
2997
|
+
'::-webkit-scrollbar': { display: 'none' },
|
|
2998
|
+
},
|
|
2999
|
+
}));
|
|
3000
|
+
|
|
3001
|
+
const TabsContext = createContext(null);
|
|
3002
|
+
|
|
3003
|
+
/**
|
|
3004
|
+
* Internal hook — consumes the Tabs context and throws if used outside a `<Tabs>` provider.
|
|
3005
|
+
* @internal
|
|
3006
|
+
*/
|
|
3007
|
+
const useTabsContext = () => {
|
|
3008
|
+
const ctx = useContext(TabsContext);
|
|
3009
|
+
if (!ctx) {
|
|
3010
|
+
throw new Error('This component must be used inside a <Tabs> provider.');
|
|
3011
|
+
}
|
|
3012
|
+
return ctx;
|
|
3013
|
+
};
|
|
3014
|
+
|
|
3015
|
+
const TabsList = ({ children, ariaLabel, ariaLabelledBy }) => {
|
|
3016
|
+
const { value, setValue, baseId, getTabValues } = useTabsContext();
|
|
3017
|
+
/**
|
|
3018
|
+
* Returns true if the tab at the given index is aria-disabled.
|
|
3019
|
+
* We check the DOM attribute because disabled state lives in TabItem,
|
|
3020
|
+
* not in the shared context, and we want to avoid adding it to the context.
|
|
3021
|
+
*/
|
|
3022
|
+
const isTabDisabled = (tabValue) => {
|
|
3023
|
+
const el = document.getElementById(`${baseId}-tab-${tabValue}`);
|
|
3024
|
+
return el?.getAttribute('aria-disabled') === 'true';
|
|
3025
|
+
};
|
|
3026
|
+
/**
|
|
3027
|
+
* Moves focus to the nearest non-disabled tab starting from `startIndex`
|
|
3028
|
+
* and stepping in the given `direction` (+1 = right, -1 = left).
|
|
3029
|
+
* Stops after a full loop if all tabs are disabled.
|
|
3030
|
+
*/
|
|
3031
|
+
const focusNextEnabled = (startIndex, direction) => {
|
|
3032
|
+
const values = getTabValues();
|
|
3033
|
+
for (let i = 1; i <= values.length; i++) {
|
|
3034
|
+
const index = (startIndex + direction * i + values.length) % values.length;
|
|
3035
|
+
const nextValue = values[index];
|
|
3036
|
+
if (!isTabDisabled(nextValue)) {
|
|
3037
|
+
document.getElementById(`${baseId}-tab-${nextValue}`)?.focus();
|
|
3038
|
+
setValue(nextValue);
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
};
|
|
3043
|
+
const handleKeyDown = (event) => {
|
|
3044
|
+
const values = getTabValues();
|
|
3045
|
+
const currentIndex = values.indexOf(value);
|
|
3046
|
+
if (currentIndex === -1) {
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
3049
|
+
switch (event.key) {
|
|
3050
|
+
case 'ArrowRight':
|
|
3051
|
+
event.preventDefault();
|
|
3052
|
+
focusNextEnabled(currentIndex, 1);
|
|
3053
|
+
break;
|
|
3054
|
+
case 'ArrowLeft':
|
|
3055
|
+
event.preventDefault();
|
|
3056
|
+
focusNextEnabled(currentIndex, -1);
|
|
3057
|
+
break;
|
|
3058
|
+
case 'Home': {
|
|
3059
|
+
event.preventDefault();
|
|
3060
|
+
// Focus the first non-disabled tab
|
|
3061
|
+
const firstIdx = values.findIndex((v) => !isTabDisabled(v));
|
|
3062
|
+
if (firstIdx !== -1) {
|
|
3063
|
+
document.getElementById(`${baseId}-tab-${values[firstIdx]}`)?.focus();
|
|
3064
|
+
setValue(values[firstIdx]);
|
|
3065
|
+
}
|
|
3066
|
+
break;
|
|
3067
|
+
}
|
|
3068
|
+
case 'End': {
|
|
3069
|
+
event.preventDefault();
|
|
3070
|
+
// Focus the last non-disabled tab
|
|
3071
|
+
const lastIdx = [...values].reverse().findIndex((v) => !isTabDisabled(v));
|
|
3072
|
+
if (lastIdx !== -1) {
|
|
3073
|
+
const realIndex = values.length - 1 - lastIdx;
|
|
3074
|
+
document.getElementById(`${baseId}-tab-${values[realIndex]}`)?.focus();
|
|
3075
|
+
setValue(values[realIndex]);
|
|
3076
|
+
}
|
|
3077
|
+
break;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
};
|
|
3081
|
+
return (jsx("div", { role: 'tablist', "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className: TABS_LIST_STYLES.root, onKeyDown: handleKeyDown, children: children }));
|
|
3082
|
+
};
|
|
3083
|
+
TabsList.displayName = 'Tabs.List';
|
|
3084
|
+
|
|
3085
|
+
const TABS_PANEL_STYLES = createStyles(() => ({
|
|
3086
|
+
root: {
|
|
3087
|
+
width: '100%',
|
|
3088
|
+
},
|
|
3089
|
+
}));
|
|
3090
|
+
|
|
3091
|
+
const TabsPanel = ({ value, children, keepMounted = false }) => {
|
|
3092
|
+
const { value: activeValue, baseId } = useTabsContext();
|
|
3093
|
+
const isActive = activeValue === value;
|
|
3094
|
+
if (!isActive && !keepMounted) {
|
|
3095
|
+
return null;
|
|
3096
|
+
}
|
|
3097
|
+
return (jsx("div", { role: 'tabpanel', id: `${baseId}-panel-${value}`, "aria-labelledby": `${baseId}-tab-${value}`,
|
|
3098
|
+
// WAI-ARIA APG: tabIndex={0} allows keyboard scrolling when the panel has no focusable child.
|
|
3099
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
3100
|
+
tabIndex: 0, hidden: !isActive, className: TABS_PANEL_STYLES.root, children: children }));
|
|
3101
|
+
};
|
|
3102
|
+
TabsPanel.displayName = 'Tabs.Panel';
|
|
3103
|
+
|
|
3104
|
+
const TAB_ITEM_STYLES = createStyles((theme) => ({
|
|
3105
|
+
root: ({ isActive, disabled }) => ({
|
|
3106
|
+
display: 'inline-flex',
|
|
3107
|
+
alignItems: 'center',
|
|
3108
|
+
justifyContent: 'center',
|
|
3109
|
+
boxSizing: 'border-box',
|
|
3110
|
+
paddingTop: theme.spacing.xs,
|
|
3111
|
+
paddingBottom: theme.spacing.xs,
|
|
3112
|
+
paddingLeft: theme.spacing.md,
|
|
3113
|
+
paddingRight: theme.spacing.md,
|
|
3114
|
+
border: 'none',
|
|
3115
|
+
borderBottom: `2px solid ${isActive ? theme.colors.primaryMain : 'transparent'}`,
|
|
3116
|
+
background: 'transparent',
|
|
3117
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
3118
|
+
fontFamily: 'inherit',
|
|
3119
|
+
whiteSpace: 'nowrap',
|
|
3120
|
+
transition: `border-color ${theme.transition.fast}`,
|
|
3121
|
+
...(disabled ? {} : {
|
|
3122
|
+
':hover': {
|
|
3123
|
+
borderBottomColor: isActive ? theme.colors.primaryMain : theme.colors.textTertiary,
|
|
3124
|
+
},
|
|
3125
|
+
}),
|
|
3126
|
+
':focus-visible': {
|
|
3127
|
+
outline: `2px solid ${theme.colors.primaryMain}`,
|
|
3128
|
+
outlineOffset: '2px',
|
|
3129
|
+
},
|
|
3130
|
+
}),
|
|
3131
|
+
}));
|
|
3132
|
+
|
|
3133
|
+
/**
|
|
3134
|
+
* Handles tab registration, active state derivation and interaction props
|
|
3135
|
+
* for a single `Tabs.Tab` (TabItem) button.
|
|
3136
|
+
*
|
|
3137
|
+
* Registers the tab value into the shared ordered registry on mount so that
|
|
3138
|
+
* `TabsList` can perform correct ArrowLeft/Right/Home/End keyboard navigation.
|
|
3139
|
+
*/
|
|
3140
|
+
const useTabItem = ({ value, disabled }) => {
|
|
3141
|
+
const { value: activeValue, setValue, baseId, registerTab } = useTabsContext();
|
|
3142
|
+
// Maintains the ordered tab registry used by TabsList for keyboard navigation.
|
|
3143
|
+
useEffect(() => registerTab(value), [registerTab, value]);
|
|
3144
|
+
const isActive = activeValue === value;
|
|
3145
|
+
return {
|
|
3146
|
+
isActive,
|
|
3147
|
+
buttonProps: {
|
|
3148
|
+
id: `${baseId}-tab-${value}`,
|
|
3149
|
+
'aria-selected': isActive,
|
|
3150
|
+
'aria-controls': `${baseId}-panel-${value}`,
|
|
3151
|
+
'aria-disabled': disabled || undefined,
|
|
3152
|
+
tabIndex: isActive ? 0 : -1,
|
|
3153
|
+
onClick: () => {
|
|
3154
|
+
if (!disabled) {
|
|
3155
|
+
setValue(value);
|
|
3156
|
+
}
|
|
3157
|
+
},
|
|
3158
|
+
},
|
|
3159
|
+
};
|
|
3160
|
+
};
|
|
3161
|
+
|
|
3162
|
+
const TabItem = ({ value, label, disabled = false }) => {
|
|
3163
|
+
const { isActive, buttonProps } = useTabItem({ value, disabled });
|
|
3164
|
+
return (jsx("button", { type: 'button', role: 'tab', className: TAB_ITEM_STYLES.root({ isActive, disabled }), ...buttonProps, children: jsx(Text, { variant: 'span', fontSize: 'sm', fontWeight: isActive ? 'semibold' : 'medium', color: disabled ? 'textDisabled' : isActive ? 'textPrimary' : 'textSecondary', children: label }) }));
|
|
3165
|
+
};
|
|
3166
|
+
TabItem.displayName = 'Tabs.Tab';
|
|
3167
|
+
|
|
3168
|
+
/**
|
|
3169
|
+
* WAI-ARIA compliant tab interface using a compound component API.
|
|
3170
|
+
*
|
|
3171
|
+
* ```tsx
|
|
3172
|
+
* <Tabs defaultValue={'active'} onChange={handleChange}>
|
|
3173
|
+
* <Tabs.List ariaLabel={'Subscriptions'}>
|
|
3174
|
+
* <Tabs.Tab value={'active'} label={'Active'} />
|
|
3175
|
+
* <Tabs.Tab value={'cancelled'} label={'Cancelled'} />
|
|
3176
|
+
* </Tabs.List>
|
|
3177
|
+
* <Tabs.Panel value={'active'}>...</Tabs.Panel>
|
|
3178
|
+
* <Tabs.Panel value={'cancelled'}>...</Tabs.Panel>
|
|
3179
|
+
* </Tabs>
|
|
3180
|
+
* ```
|
|
3181
|
+
*
|
|
3182
|
+
* Supports controlled (`value` + `onChange`) and uncontrolled (`defaultValue`) modes.
|
|
3183
|
+
* Keyboard navigation: `ArrowLeft/Right`, `Home`, `End`.
|
|
3184
|
+
* Only the active panel is mounted — use `keepMounted` on `Tabs.Panel` to preserve state across switches.
|
|
3185
|
+
*/
|
|
3186
|
+
const TabsBase = ({ children, value: controlledValue, defaultValue, onChange, id, }) => {
|
|
3187
|
+
const reactId = useId();
|
|
3188
|
+
const baseId = id ?? `tabs-${reactId}`;
|
|
3189
|
+
const [internalValue, setInternalValue] = useState(defaultValue ?? '');
|
|
3190
|
+
const isControlled = controlledValue !== undefined;
|
|
3191
|
+
const value = isControlled ? controlledValue : internalValue;
|
|
3192
|
+
const tabsRef = useRef([]);
|
|
3193
|
+
const registerTab = useCallback((tabValue) => {
|
|
3194
|
+
if (!tabsRef.current.includes(tabValue)) {
|
|
3195
|
+
tabsRef.current.push(tabValue);
|
|
3196
|
+
}
|
|
3197
|
+
return () => {
|
|
3198
|
+
tabsRef.current = tabsRef.current.filter((v) => v !== tabValue);
|
|
3199
|
+
};
|
|
3200
|
+
}, []);
|
|
3201
|
+
const getTabValues = useCallback(() => tabsRef.current, []);
|
|
3202
|
+
const setValue = useCallback((next) => {
|
|
3203
|
+
if (!isControlled) {
|
|
3204
|
+
setInternalValue(next);
|
|
3205
|
+
}
|
|
3206
|
+
onChange?.(next);
|
|
3207
|
+
}, [isControlled, onChange]);
|
|
3208
|
+
const contextValue = useMemo(() => ({ value, setValue, baseId, registerTab, getTabValues }), [value, setValue, baseId, registerTab, getTabValues]);
|
|
3209
|
+
return (jsx(TabsContext.Provider, { value: contextValue, children: children }));
|
|
3210
|
+
};
|
|
3211
|
+
TabsBase.displayName = 'Tabs';
|
|
3212
|
+
const Tabs = TabsBase;
|
|
3213
|
+
Tabs.List = TabsList;
|
|
3214
|
+
Tabs.Tab = TabItem;
|
|
3215
|
+
Tabs.Panel = TabsPanel;
|
|
3216
|
+
|
|
2881
3217
|
const AlertContext = createContext({
|
|
2882
3218
|
variant: 'default',
|
|
2883
3219
|
accentColor: 'defaultActive',
|
|
@@ -2930,6 +3266,15 @@ const VARIANT_ARIA_LIVE = {
|
|
|
2930
3266
|
warning: 'polite',
|
|
2931
3267
|
info: 'polite',
|
|
2932
3268
|
};
|
|
3269
|
+
/** role="alert" implies aria-live="assertive" — reserve it for errors.
|
|
3270
|
+
* Other variants use role="status" (implies aria-live="polite"). */
|
|
3271
|
+
const VARIANT_ROLE = {
|
|
3272
|
+
default: 'status',
|
|
3273
|
+
success: 'status',
|
|
3274
|
+
error: 'alert',
|
|
3275
|
+
warning: 'status',
|
|
3276
|
+
info: 'status',
|
|
3277
|
+
};
|
|
2933
3278
|
/**
|
|
2934
3279
|
* Inline alert banner with 5 visual variants: default, success, error, warning, info.
|
|
2935
3280
|
* Use `Alert.Title` (icon + heading) and `Alert.Body` (message) as children.
|
|
@@ -2942,7 +3287,7 @@ const VARIANT_ARIA_LIVE = {
|
|
|
2942
3287
|
*/
|
|
2943
3288
|
const AlertBase = ({ variant = 'default', children, width = '100%', outline = false, shadow = 'none', }) => {
|
|
2944
3289
|
const { backgroundColor, borderColor, accentColor } = VARIANT_TOKENS[variant];
|
|
2945
|
-
return (jsx(AlertContext.Provider, { value: { variant, accentColor }, children: jsx(Stack, { role:
|
|
3290
|
+
return (jsx(AlertContext.Provider, { value: { variant, accentColor }, children: 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 }) }));
|
|
2946
3291
|
};
|
|
2947
3292
|
AlertBase.displayName = 'Alert';
|
|
2948
3293
|
const Alert = AlertBase;
|
|
@@ -3109,7 +3454,7 @@ const useDialog = ({ open, onClose, closeOnBackdropClick, maxWidth, maxHeight, m
|
|
|
3109
3454
|
*/
|
|
3110
3455
|
const DialogHeader = ({ title, onClose }) => {
|
|
3111
3456
|
const { titleId, CloseIconComponent } = useContext(DialogContext);
|
|
3112
|
-
return (jsxs(Stack, { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 'md', paddingTop: 'md', paddingBottom: 'md', paddingLeft: 'lg', paddingRight: 'md', flexShrink: 0, children: [jsx(Text, { id: titleId, variant: 'span', fontSize: 'md', fontWeight: 'semibold', color: 'textPrimary', children: title }), jsx(IconButton, { icon: CloseIconComponent, ariaLabel: 'Close dialog', variant: 'text', color: 'neutral', size: 'sm', type: 'button', onClick: onClose })] }));
|
|
3457
|
+
return (jsxs(Stack, { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 'md', paddingTop: 'md', paddingBottom: 'md', paddingLeft: 'lg', paddingRight: 'md', flexShrink: 0, children: [jsx(Text, { id: titleId, variant: 'span', as: 'h2', fontSize: 'md', fontWeight: 'semibold', color: 'textPrimary', children: title }), jsx(IconButton, { icon: CloseIconComponent, ariaLabel: 'Close dialog', variant: 'text', color: 'neutral', size: 'sm', type: 'button', onClick: onClose })] }));
|
|
3113
3458
|
};
|
|
3114
3459
|
DialogHeader.displayName = 'Dialog.Header';
|
|
3115
3460
|
|
|
@@ -3537,5 +3882,5 @@ const darkTheme = createTheme({
|
|
|
3537
3882
|
breakpoints: themeBreakpoints,
|
|
3538
3883
|
});
|
|
3539
3884
|
|
|
3540
|
-
export { Alert, Backdrop, Badge, Box, Button, Card, Checkbox, Dialog, Drawer, Form, Grid, Icon, IconButton, InfoBubble, Link, Menu, Select, Skeleton, Stack, Switch, Text, TextField, Tooltip, darkTheme, lightTheme, useDrawerContext };
|
|
3885
|
+
export { Alert, Backdrop, Badge, Box, Breadcrumb, Button, Card, Checkbox, Dialog, Drawer, Form, Grid, Icon, IconButton, InfoBubble, Link, Menu, Select, Skeleton, Stack, Switch, Tabs, Text, TextField, Tooltip, darkTheme, lightTheme, useDrawerContext };
|
|
3541
3886
|
//# sourceMappingURL=index.js.map
|