@aurora-ds/components 0.24.9 → 0.25.1
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/README.md +35 -7
- package/dist/cjs/components/actions/button/Button.d.ts +8 -0
- package/dist/cjs/components/actions/button/Button.props.d.ts +10 -0
- package/dist/cjs/components/actions/button/utils/getButtonSizeStyles.utils.d.ts +12 -0
- package/dist/cjs/components/actions/button/utils/getButtonVariantStyles.utils.d.ts +7 -0
- package/dist/cjs/components/actions/icon-button/IconButton.d.ts +13 -0
- package/dist/cjs/components/actions/icon-button/IconButton.props.d.ts +11 -1
- package/dist/cjs/components/actions/icon-button/utils/getIconButtonSizeStyles.utils.d.ts +9 -0
- package/dist/cjs/components/data-display/avatar/Avatar.d.ts +16 -1
- package/dist/cjs/components/data-display/avatar/Avatar.props.d.ts +18 -4
- package/dist/cjs/components/data-display/avatar/utils/getAvatarSizes.utils.d.ts +11 -0
- package/dist/cjs/components/data-display/chip/utils/getChipColorStyles.utils.d.ts +12 -0
- package/dist/cjs/components/data-display/chip/utils/getChipContentSize.utils.d.ts +6 -0
- package/dist/cjs/components/data-display/chip/utils/getChipSizeStyles.utils.d.ts +10 -0
- package/dist/cjs/components/data-display/status/utils/getStatusColorStyles.utils.d.ts +7 -0
- package/dist/cjs/components/data-display/status/utils/getStatusContentSize.utils.d.ts +8 -0
- package/dist/cjs/components/data-display/status/utils/getStatusSizeStyles.utils.d.ts +12 -0
- package/dist/cjs/components/forms/date-picker/calendar/calendar-grid/CalendarGrid.props.d.ts +1 -1
- package/dist/cjs/components/forms/date-picker/utils/datePicker.utils.d.ts +13 -0
- package/dist/cjs/components/forms/input/Input.props.d.ts +9 -1
- package/dist/cjs/components/forms/select/Select.d.ts +18 -0
- package/dist/cjs/components/forms/select/Select.props.d.ts +14 -0
- package/dist/cjs/components/forms/textarea/TextArea.props.d.ts +9 -1
- package/dist/cjs/components/foundation/text/Text.props.d.ts +2 -0
- package/dist/cjs/components/foundation/text/utils/getTextVariantStyles.utils.d.ts +7 -0
- package/dist/cjs/components/foundation/text/utils/getTruncateTextStyles.utils.d.ts +21 -0
- package/dist/cjs/components/foundation/text/utils/parseTextWithBold.utils.d.ts +7 -0
- package/dist/cjs/components/navigation/breadcrumb/utils/buildBreadcrumbChildren.utils.d.ts +5 -0
- package/dist/cjs/components/navigation/breadcrumb/utils/flattenChildren.utils.d.ts +5 -0
- package/dist/cjs/components/navigation/breadcrumb/utils/insertSeparators.utils.d.ts +5 -0
- package/dist/cjs/components/navigation/breadcrumb/utils/isSeparator.utils.d.ts +5 -0
- package/dist/cjs/components/overlay/alert/Alert.d.ts +5 -0
- package/dist/cjs/components/overlay/alert/utils/getAlertIcon.utils.d.ts +8 -0
- package/dist/cjs/components/overlay/alert/utils/getAlertPositionStyles.utils.d.ts +8 -0
- package/dist/cjs/components/overlay/alert/utils/getAlertVariantColors.utils.d.ts +14 -0
- package/dist/cjs/components/overlay/modal/Modal.d.ts +24 -0
- package/dist/cjs/index.js +234 -28
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/components/actions/button/Button.d.ts +8 -0
- package/dist/esm/components/actions/button/Button.props.d.ts +10 -0
- package/dist/esm/components/actions/button/utils/getButtonSizeStyles.utils.d.ts +12 -0
- package/dist/esm/components/actions/button/utils/getButtonVariantStyles.utils.d.ts +7 -0
- package/dist/esm/components/actions/icon-button/IconButton.d.ts +13 -0
- package/dist/esm/components/actions/icon-button/IconButton.props.d.ts +11 -1
- package/dist/esm/components/actions/icon-button/utils/getIconButtonSizeStyles.utils.d.ts +9 -0
- package/dist/esm/components/data-display/avatar/Avatar.d.ts +16 -1
- package/dist/esm/components/data-display/avatar/Avatar.props.d.ts +18 -4
- package/dist/esm/components/data-display/avatar/utils/getAvatarSizes.utils.d.ts +11 -0
- package/dist/esm/components/data-display/chip/utils/getChipColorStyles.utils.d.ts +12 -0
- package/dist/esm/components/data-display/chip/utils/getChipContentSize.utils.d.ts +6 -0
- package/dist/esm/components/data-display/chip/utils/getChipSizeStyles.utils.d.ts +10 -0
- package/dist/esm/components/data-display/status/utils/getStatusColorStyles.utils.d.ts +7 -0
- package/dist/esm/components/data-display/status/utils/getStatusContentSize.utils.d.ts +8 -0
- package/dist/esm/components/data-display/status/utils/getStatusSizeStyles.utils.d.ts +12 -0
- package/dist/esm/components/forms/date-picker/calendar/calendar-grid/CalendarGrid.props.d.ts +1 -1
- package/dist/esm/components/forms/date-picker/utils/datePicker.utils.d.ts +13 -0
- package/dist/esm/components/forms/input/Input.props.d.ts +9 -1
- package/dist/esm/components/forms/select/Select.d.ts +18 -0
- package/dist/esm/components/forms/select/Select.props.d.ts +14 -0
- package/dist/esm/components/forms/textarea/TextArea.props.d.ts +9 -1
- package/dist/esm/components/foundation/text/Text.props.d.ts +2 -0
- package/dist/esm/components/foundation/text/utils/getTextVariantStyles.utils.d.ts +7 -0
- package/dist/esm/components/foundation/text/utils/getTruncateTextStyles.utils.d.ts +21 -0
- package/dist/esm/components/foundation/text/utils/parseTextWithBold.utils.d.ts +7 -0
- package/dist/esm/components/navigation/breadcrumb/utils/buildBreadcrumbChildren.utils.d.ts +5 -0
- package/dist/esm/components/navigation/breadcrumb/utils/flattenChildren.utils.d.ts +5 -0
- package/dist/esm/components/navigation/breadcrumb/utils/insertSeparators.utils.d.ts +5 -0
- package/dist/esm/components/navigation/breadcrumb/utils/isSeparator.utils.d.ts +5 -0
- package/dist/esm/components/overlay/alert/Alert.d.ts +5 -0
- package/dist/esm/components/overlay/alert/utils/getAlertIcon.utils.d.ts +8 -0
- package/dist/esm/components/overlay/alert/utils/getAlertPositionStyles.utils.d.ts +8 -0
- package/dist/esm/components/overlay/alert/utils/getAlertVariantColors.utils.d.ts +14 -0
- package/dist/esm/components/overlay/modal/Modal.d.ts +24 -0
- package/dist/esm/index.js +235 -29
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.ts +153 -8
- package/package.json +2 -2
package/dist/cjs/index.js
CHANGED
|
@@ -228,7 +228,7 @@ const parseTextWithBold = (children) => {
|
|
|
228
228
|
* - Preserve whitespace with `preserveWhitespace` prop
|
|
229
229
|
* - Bold text with **double asterisks** syntax
|
|
230
230
|
*/
|
|
231
|
-
const Text = ({ children, variant = 'span', color, fontSize, fontFamily, maxLines, underline, preserveWhitespace, ariaLabel, ariaLabelledBy, ariaDescribedBy, role, tabIndex, }) => {
|
|
231
|
+
const Text = ({ children, variant = 'span', color, fontSize, fontFamily, maxLines, underline, preserveWhitespace, ariaLabel, ariaLabelledBy, ariaDescribedBy, role, tabIndex, id, }) => {
|
|
232
232
|
const theme$1 = theme.useTheme();
|
|
233
233
|
const variantStyles = React.useMemo(() => getTextVariantStyles(theme$1), [theme$1]);
|
|
234
234
|
const tag = variantStyles[variant].tag;
|
|
@@ -236,6 +236,7 @@ const Text = ({ children, variant = 'span', color, fontSize, fontFamily, maxLine
|
|
|
236
236
|
// Force inline truncate styles when needed (fix for multi-line clamp not applied in some envs)
|
|
237
237
|
const truncateStyles = maxLines ? getTruncateTextStyles(maxLines) : undefined;
|
|
238
238
|
return React.createElement(tag, {
|
|
239
|
+
id,
|
|
239
240
|
className: TEXT_STYLES.root({ variant, color, fontSize, fontFamily, maxLines, underline, preserveWhitespace }),
|
|
240
241
|
style: truncateStyles,
|
|
241
242
|
'aria-label': ariaLabel,
|
|
@@ -532,20 +533,37 @@ const AVATAR_STYLES = theme.createStyles((theme) => {
|
|
|
532
533
|
/**
|
|
533
534
|
* Avatar component
|
|
534
535
|
*
|
|
535
|
-
* Displays a user's avatar with optional image or fallback
|
|
536
|
+
* Displays a user's avatar with an optional image or fallback initials.
|
|
537
|
+
*
|
|
538
|
+
* **Accessibility:**
|
|
539
|
+
* - When `onClick` is provided, the avatar becomes a focusable button
|
|
540
|
+
* with keyboard support (Enter/Space activates the click)
|
|
541
|
+
* - Provide `label` or `ariaLabel` for screen readers
|
|
542
|
+
* - For decorative avatars, both can be omitted
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* ```tsx
|
|
546
|
+
* // Static avatar with image
|
|
547
|
+
* <Avatar image="/user.jpg" label="Jane Doe" />
|
|
548
|
+
*
|
|
549
|
+
* // Interactive avatar (clickable)
|
|
550
|
+
* <Avatar label="JD" onClick={() => openProfile()} ariaLabel="Open Jane's profile" />
|
|
551
|
+
* ```
|
|
536
552
|
*/
|
|
537
|
-
const Avatar = ({ image, label, onClick, size = 'medium', color, borderColor, backgroundColor, }) => {
|
|
553
|
+
const Avatar = ({ image, label, onClick, size = 'medium', color, borderColor, backgroundColor, ariaLabel, tabIndex, }) => {
|
|
538
554
|
// hooks
|
|
539
555
|
const theme$1 = theme.useTheme();
|
|
540
556
|
// variables
|
|
541
557
|
const AVATAR_SIZES = getAvatarSizes(theme$1);
|
|
542
558
|
const hasImage = !!image;
|
|
543
559
|
const clickable = !!onClick;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
560
|
+
const handleKeyDown = (event) => {
|
|
561
|
+
if (onClick && (event.key === 'Enter' || event.key === ' ')) {
|
|
562
|
+
event.preventDefault();
|
|
563
|
+
onClick(event);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
return (jsxRuntime.jsx("div", { className: AVATAR_STYLES.root({ hasImage, clickable, size, color, borderColor, backgroundColor }), onClick: onClick, onKeyDown: clickable ? handleKeyDown : undefined, role: clickable ? 'button' : undefined, tabIndex: clickable ? (tabIndex ?? 0) : tabIndex, "aria-label": ariaLabel || label, children: hasImage ? (jsxRuntime.jsx("img", { src: image, alt: label || ariaLabel || '', className: AVATAR_STYLES.image })) : (jsxRuntime.jsx(Text, { variant: 'label', fontSize: AVATAR_SIZES[size].fontSize, children: label || '?' })) }));
|
|
549
567
|
};
|
|
550
568
|
Avatar.displayName = 'Avatar';
|
|
551
569
|
|
|
@@ -787,13 +805,21 @@ const BUTTON_STYLES = theme.createStyles((theme) => {
|
|
|
787
805
|
* - `contained`: Solid background button (default)
|
|
788
806
|
* - `outlined`: Border only button
|
|
789
807
|
* - `text`: Text only button without background
|
|
790
|
-
|
|
791
|
-
|
|
808
|
+
* - `destructive`: Danger/delete action button
|
|
809
|
+
*
|
|
810
|
+
* **Accessibility:**
|
|
811
|
+
* - Use `ariaLabel` when the button has no visible text or the label is unclear
|
|
812
|
+
* - Use `ariaPressed` for toggle buttons to indicate their state
|
|
813
|
+
* - Use `ariaBusy` when the button triggers an async action
|
|
814
|
+
* - Use `ariaExpanded` when the button controls a collapsible section
|
|
815
|
+
* - Use `ariaControls` to reference the controlled element
|
|
816
|
+
*/
|
|
817
|
+
const Button = ({ label, startIcon, endIcon, variant = 'contained', active = false, onClick, disabled, type = 'button', textColor: customTextColor, backgroundColor, hoverBackgroundColor, activeBackgroundColor, size = 'medium', ariaLabel, ariaLabelledBy, ariaDescribedBy, role, tabIndex, ariaPressed, ariaBusy, ariaExpanded, ariaHasPopup, ariaControls, }) => {
|
|
792
818
|
const theme$1 = theme.useTheme();
|
|
793
819
|
const variantStyles = getButtonVariantStyles(theme$1);
|
|
794
820
|
const sizeStyles = getButtonSizeStyles();
|
|
795
821
|
const textColor = disabled ? 'disabledText' : (customTextColor ?? variantStyles[variant].textColor);
|
|
796
|
-
return (jsxRuntime.jsxs("button", { onClick: onClick, disabled: disabled, type: type, className: BUTTON_STYLES.root({ variant, active, textColor: customTextColor, backgroundColor, hoverBackgroundColor, activeBackgroundColor, size }), "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, role: role, tabIndex: tabIndex, children: [startIcon && (jsxRuntime.jsx(Icon, { color: textColor, children: startIcon })), jsxRuntime.jsx(Text, { variant: 'label', color: textColor, fontSize: sizeStyles[size].fontSize, children: label }), endIcon && (jsxRuntime.jsx(Icon, { color: textColor, children: endIcon }))] }));
|
|
822
|
+
return (jsxRuntime.jsxs("button", { onClick: onClick, disabled: disabled, type: type, className: BUTTON_STYLES.root({ variant, active, textColor: customTextColor, backgroundColor, hoverBackgroundColor, activeBackgroundColor, size }), "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-pressed": ariaPressed, "aria-busy": ariaBusy, "aria-expanded": ariaExpanded, "aria-haspopup": ariaHasPopup, "aria-controls": ariaControls, role: role, tabIndex: tabIndex, children: [startIcon && (jsxRuntime.jsx(Icon, { color: textColor, children: startIcon })), jsxRuntime.jsx(Text, { variant: 'label', color: textColor, fontSize: sizeStyles[size].fontSize, children: label }), endIcon && (jsxRuntime.jsx(Icon, { color: textColor, children: endIcon }))] }));
|
|
797
823
|
};
|
|
798
824
|
Button.displayName = 'Button';
|
|
799
825
|
|
|
@@ -861,12 +887,25 @@ const ICON_BUTTON_STYLES = theme.createStyles((theme) => {
|
|
|
861
887
|
};
|
|
862
888
|
});
|
|
863
889
|
|
|
864
|
-
|
|
890
|
+
/**
|
|
891
|
+
* IconButton component
|
|
892
|
+
*
|
|
893
|
+
* A button that displays only an icon without text.
|
|
894
|
+
*
|
|
895
|
+
* **⚠️ Accessibility:** Always provide an `ariaLabel` for icon-only buttons
|
|
896
|
+
* so screen readers can describe the action.
|
|
897
|
+
*
|
|
898
|
+
* @example
|
|
899
|
+
* ```tsx
|
|
900
|
+
* <IconButton icon={<CloseIcon />} ariaLabel="Close dialog" onClick={onClose} />
|
|
901
|
+
* ```
|
|
902
|
+
*/
|
|
903
|
+
const IconButton = ({ icon, variant = 'contained', active = false, type = 'button', onClick, disabled, textColor: customTextColor, backgroundColor, hoverBackgroundColor, activeBackgroundColor, size = 'medium', ariaLabel, ariaLabelledBy, ariaDescribedBy, role, tabIndex, ariaPressed, ariaBusy, ariaExpanded, ariaHasPopup, ariaControls, }) => {
|
|
865
904
|
const theme$1 = theme.useTheme();
|
|
866
905
|
const variantStyles = getButtonVariantStyles(theme$1);
|
|
867
906
|
const textColor = disabled ? 'disabledText' : (customTextColor ?? variantStyles[variant].textColor);
|
|
868
907
|
const iconSize = getIconButtonSizeStyles()[size].iconSize;
|
|
869
|
-
return (jsxRuntime.jsx("button", { onClick: onClick, disabled: disabled, type: type, className: ICON_BUTTON_STYLES.root({ variant, active, size, textColor: customTextColor, backgroundColor, hoverBackgroundColor, activeBackgroundColor }), "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, role: role, tabIndex: tabIndex, children: jsxRuntime.jsx(Icon, { color: textColor, size: iconSize, children: icon }) }));
|
|
908
|
+
return (jsxRuntime.jsx("button", { onClick: onClick, disabled: disabled, type: type, className: ICON_BUTTON_STYLES.root({ variant, active, size, textColor: customTextColor, backgroundColor, hoverBackgroundColor, activeBackgroundColor }), "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-pressed": ariaPressed, "aria-busy": ariaBusy, "aria-expanded": ariaExpanded, "aria-haspopup": ariaHasPopup, "aria-controls": ariaControls, role: role, tabIndex: tabIndex, children: jsxRuntime.jsx(Icon, { color: textColor, size: iconSize, children: icon }) }));
|
|
870
909
|
};
|
|
871
910
|
IconButton.displayName = 'IconButton';
|
|
872
911
|
|
|
@@ -1217,15 +1256,38 @@ const UploadIcon = () => (jsxRuntime.jsxs("svg", { xmlns: 'http://www.w3.org/200
|
|
|
1217
1256
|
|
|
1218
1257
|
/**
|
|
1219
1258
|
* Input component
|
|
1259
|
+
*
|
|
1260
|
+
* A text input field with optional label, icons, and error state.
|
|
1261
|
+
*
|
|
1262
|
+
* **Accessibility:**
|
|
1263
|
+
* - `aria-label` defaults to the `label` prop for screen readers
|
|
1264
|
+
* - When `error` is provided, `aria-invalid="true"` and `aria-errormessage`
|
|
1265
|
+
* are automatically set to link the input to its error message
|
|
1266
|
+
* - `mandatory` adds `aria-required="true"` to the input element
|
|
1267
|
+
*
|
|
1268
|
+
* @example
|
|
1269
|
+
* ```tsx
|
|
1270
|
+
* <Input
|
|
1271
|
+
* value={email}
|
|
1272
|
+
* onChange={setEmail}
|
|
1273
|
+
* label="Email address"
|
|
1274
|
+
* mandatory
|
|
1275
|
+
* error={emailError}
|
|
1276
|
+
* type="email"
|
|
1277
|
+
* />
|
|
1278
|
+
* ```
|
|
1220
1279
|
*/
|
|
1221
|
-
const Input = React.forwardRef(({ value, onChange, onFocus, onBlur, onClick, label, mandatory = false, placeholder, disabled = false, type = 'text', ariaLabel, startIcon, endIcon, width, }, ref) => {
|
|
1280
|
+
const Input = React.forwardRef(({ value, onChange, onFocus, onBlur, onClick, label, mandatory = false, placeholder, disabled = false, type = 'text', ariaLabel, startIcon, endIcon, width, error, id, }, ref) => {
|
|
1281
|
+
const generatedId = React.useId();
|
|
1282
|
+
const inputId = id ?? generatedId;
|
|
1283
|
+
const errorId = error ? `${inputId}-error` : undefined;
|
|
1222
1284
|
const [showPassword, setShowPassword] = React.useState(false);
|
|
1223
1285
|
const handleChange = (event) => {
|
|
1224
1286
|
onChange(event.target.value);
|
|
1225
1287
|
};
|
|
1226
1288
|
const inputType = type === 'password' ? (showPassword ? 'text' : 'password') : type;
|
|
1227
1289
|
const hasPasswordToggle = type === 'password';
|
|
1228
|
-
return (jsxRuntime.jsxs(Stack, { direction: 'column', gap: 'xs', align: 'stretch', width: width ?? '100%', children: [label && (jsxRuntime.jsxs(Stack, { direction: 'row', gap: 'xs', align: 'center', children: [jsxRuntime.jsx(Text, { variant: 'label', fontSize: 'sm', children: label }), mandatory && (jsxRuntime.jsx(Text, { variant: 'label', fontSize: 'sm', color: 'error', children: "*" }))] })), jsxRuntime.jsxs("div", { className: `${INPUT_STYLES.container({ width })} ${disabled ? 'disabled' : ''}`, children: [jsxRuntime.jsx("input", { ref: ref, type: inputType, value: value, onChange: handleChange, onFocus: onFocus, onBlur: onBlur, onClick: onClick, placeholder: placeholder, disabled: disabled, className: INPUT_STYLES.root({ disabled, hasStartIcon: !!startIcon, hasEndIcon: !!endIcon, hasPasswordToggle }), "aria-label": ariaLabel || label }), startIcon && (jsxRuntime.jsx("div", { className: INPUT_STYLES.startIcon, children: jsxRuntime.jsx(Icon, { color: 'textTertiary', children: startIcon }) })), endIcon && (jsxRuntime.jsx("div", { className: hasPasswordToggle ? INPUT_STYLES.endIconShifted : INPUT_STYLES.endIcon, children: jsxRuntime.jsx(Icon, { color: 'textTertiary', children: endIcon }) })), hasPasswordToggle && (jsxRuntime.jsx("div", { className: INPUT_STYLES.passwordToggle, children: jsxRuntime.jsx(IconButton, { icon: showPassword ? jsxRuntime.jsx(EyeOffIcon, {}) : jsxRuntime.jsx(EyeIcon, {}), onClick: () => setShowPassword(!showPassword), disabled: disabled, ariaLabel:
|
|
1290
|
+
return (jsxRuntime.jsxs(Stack, { direction: 'column', gap: 'xs', align: 'stretch', width: width ?? '100%', children: [label && (jsxRuntime.jsxs(Stack, { direction: 'row', gap: 'xs', align: 'center', children: [jsxRuntime.jsx(Text, { variant: 'label', fontSize: 'sm', id: `${inputId}-label`, children: label }), mandatory && (jsxRuntime.jsx(Text, { variant: 'label', fontSize: 'sm', color: 'error', ariaLabel: 'required', children: "*" }))] })), jsxRuntime.jsxs("div", { className: `${INPUT_STYLES.container({ width })} ${disabled ? 'disabled' : ''}`, children: [jsxRuntime.jsx("input", { id: inputId, ref: ref, type: inputType, value: value, onChange: handleChange, onFocus: onFocus, onBlur: onBlur, onClick: onClick, placeholder: placeholder, disabled: disabled, className: INPUT_STYLES.root({ disabled, hasStartIcon: !!startIcon, hasEndIcon: !!endIcon, hasPasswordToggle, hasError: !!error }), "aria-label": ariaLabel || label, "aria-required": mandatory || undefined, "aria-invalid": error ? true : undefined, "aria-errormessage": errorId }), startIcon && (jsxRuntime.jsx("div", { className: INPUT_STYLES.startIcon, children: jsxRuntime.jsx(Icon, { color: 'textTertiary', children: startIcon }) })), endIcon && (jsxRuntime.jsx("div", { className: hasPasswordToggle ? INPUT_STYLES.endIconShifted : INPUT_STYLES.endIcon, children: jsxRuntime.jsx(Icon, { color: 'textTertiary', children: endIcon }) })), hasPasswordToggle && (jsxRuntime.jsx("div", { className: INPUT_STYLES.passwordToggle, children: jsxRuntime.jsx(IconButton, { icon: showPassword ? jsxRuntime.jsx(EyeOffIcon, {}) : jsxRuntime.jsx(EyeIcon, {}), onClick: () => setShowPassword(!showPassword), disabled: disabled, ariaLabel: showPassword ? 'Hide password' : 'Show password', variant: 'text', size: 'small', textColor: 'textSecondary' }) }))] }), error && (jsxRuntime.jsx(Text, { variant: 'span', fontSize: 'sm', color: 'error', id: errorId, role: 'alert', children: error }))] }));
|
|
1229
1291
|
});
|
|
1230
1292
|
Input.displayName = 'Input';
|
|
1231
1293
|
var Input_default = React.memo(Input);
|
|
@@ -1277,23 +1339,40 @@ const TEXTAREA_STYLES = theme.createStyles((theme) => ({
|
|
|
1277
1339
|
|
|
1278
1340
|
/**
|
|
1279
1341
|
* TextArea component with auto-expanding height based on content
|
|
1342
|
+
*
|
|
1343
|
+
* **Accessibility:**
|
|
1344
|
+
* - `aria-label` defaults to the `label` prop for screen readers
|
|
1345
|
+
* - When `error` is provided, `aria-invalid="true"` and `aria-errormessage`
|
|
1346
|
+
* are automatically set to link the textarea to its error message
|
|
1347
|
+
* - `mandatory` adds `aria-required="true"` to the textarea element
|
|
1348
|
+
*
|
|
1349
|
+
* @example
|
|
1350
|
+
* ```tsx
|
|
1351
|
+
* <TextArea
|
|
1352
|
+
* value={message}
|
|
1353
|
+
* onChange={setMessage}
|
|
1354
|
+
* label="Message"
|
|
1355
|
+
* mandatory
|
|
1356
|
+
* error={messageError}
|
|
1357
|
+
* minRows={4}
|
|
1358
|
+
* />
|
|
1359
|
+
* ```
|
|
1280
1360
|
*/
|
|
1281
|
-
const TextArea = React.forwardRef(({ value, onChange, onFocus, onBlur, label, mandatory = false, placeholder, disabled = false, ariaLabel, width, minRows = 3, maxRows, }, ref) => {
|
|
1361
|
+
const TextArea = React.forwardRef(({ value, onChange, onFocus, onBlur, label, mandatory = false, placeholder, disabled = false, ariaLabel, width, minRows = 3, maxRows, error, id, }, ref) => {
|
|
1362
|
+
const generatedId = React.useId();
|
|
1363
|
+
const textareaId = id ?? generatedId;
|
|
1364
|
+
const errorId = error ? `${textareaId}-error` : undefined;
|
|
1282
1365
|
const internalRef = React.useRef(null);
|
|
1283
1366
|
const textareaRef = ref || internalRef;
|
|
1284
1367
|
const adjustHeight = React.useCallback(() => {
|
|
1285
1368
|
const textarea = textareaRef.current;
|
|
1286
1369
|
if (textarea) {
|
|
1287
|
-
// Reset height to calculate the correct scrollHeight
|
|
1288
1370
|
textarea.style.height = 'auto';
|
|
1289
|
-
// Set minHeight based on minRows
|
|
1290
1371
|
const lineHeight = parseInt(getComputedStyle(textarea).lineHeight) || 20;
|
|
1291
1372
|
const minHeight = lineHeight * minRows;
|
|
1292
1373
|
const maxHeight = maxRows ? lineHeight * maxRows : Infinity;
|
|
1293
|
-
// Set the height to the greater of minHeight or scrollHeight, but capped at maxHeight
|
|
1294
1374
|
const newHeight = Math.min(Math.max(minHeight, textarea.scrollHeight), maxHeight);
|
|
1295
1375
|
textarea.style.height = `${newHeight}px`;
|
|
1296
|
-
// Enable scrolling if content exceeds maxHeight
|
|
1297
1376
|
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden';
|
|
1298
1377
|
}
|
|
1299
1378
|
}, [minRows, maxRows, textareaRef]);
|
|
@@ -1303,7 +1382,7 @@ const TextArea = React.forwardRef(({ value, onChange, onFocus, onBlur, label, ma
|
|
|
1303
1382
|
const handleChange = (event) => {
|
|
1304
1383
|
onChange(event.target.value);
|
|
1305
1384
|
};
|
|
1306
|
-
return (jsxRuntime.jsxs(Stack, { direction: 'column', gap: 'xs', align: 'stretch', width: width ?? '100%', children: [label && (jsxRuntime.jsxs(Stack, { direction: 'row', gap: 'xs', align: 'center', children: [jsxRuntime.jsx(Text, { variant: 'label', fontSize: 'sm', children: label }), mandatory && (jsxRuntime.jsx(Text, { variant: 'label', fontSize: 'sm', color: 'error', children: "*" }))] })), jsxRuntime.jsx("div", { className: `${TEXTAREA_STYLES.container({ width })} ${disabled ? 'disabled' : ''}`, children: jsxRuntime.jsx("textarea", { ref: textareaRef, value: value, onChange: handleChange, onFocus: onFocus, onBlur: onBlur, placeholder: placeholder, disabled: disabled, className: TEXTAREA_STYLES.root({ disabled }), "aria-label": ariaLabel || label, rows: minRows }) })] }));
|
|
1385
|
+
return (jsxRuntime.jsxs(Stack, { direction: 'column', gap: 'xs', align: 'stretch', width: width ?? '100%', children: [label && (jsxRuntime.jsxs(Stack, { direction: 'row', gap: 'xs', align: 'center', children: [jsxRuntime.jsx(Text, { variant: 'label', fontSize: 'sm', id: `${textareaId}-label`, children: label }), mandatory && (jsxRuntime.jsx(Text, { variant: 'label', fontSize: 'sm', color: 'error', ariaLabel: 'required', children: "*" }))] })), jsxRuntime.jsx("div", { className: `${TEXTAREA_STYLES.container({ width })} ${disabled ? 'disabled' : ''}`, children: jsxRuntime.jsx("textarea", { id: textareaId, ref: textareaRef, value: value, onChange: handleChange, onFocus: onFocus, onBlur: onBlur, placeholder: placeholder, disabled: disabled, className: TEXTAREA_STYLES.root({ disabled, hasError: !!error }), "aria-label": ariaLabel || label, "aria-required": mandatory || undefined, "aria-invalid": error ? true : undefined, "aria-errormessage": errorId, rows: minRows }) }), error && (jsxRuntime.jsx(Text, { variant: 'span', fontSize: 'sm', color: 'error', id: errorId, role: 'alert', children: error }))] }));
|
|
1307
1386
|
});
|
|
1308
1387
|
TextArea.displayName = 'TextArea';
|
|
1309
1388
|
var TextArea_default = React.memo(TextArea);
|
|
@@ -1626,10 +1705,31 @@ MenuItem.displayName = 'MenuItem';
|
|
|
1626
1705
|
|
|
1627
1706
|
/**
|
|
1628
1707
|
* Select component that uses Menu for dropdown
|
|
1708
|
+
*
|
|
1709
|
+
* **Accessibility:**
|
|
1710
|
+
* - Uses `role="combobox"` with `aria-expanded` and `aria-haspopup="listbox"`
|
|
1711
|
+
* - Keyboard support: Enter/Space opens, Escape closes
|
|
1712
|
+
* - When `error` is provided, `aria-invalid="true"` and `aria-errormessage` are set
|
|
1713
|
+
* - `mandatory` adds `aria-required="true"`
|
|
1714
|
+
*
|
|
1715
|
+
* @example
|
|
1716
|
+
* ```tsx
|
|
1717
|
+
* <Select
|
|
1718
|
+
* options={[{ value: 'fr', label: 'France' }]}
|
|
1719
|
+
* value={country}
|
|
1720
|
+
* onChange={setCountry}
|
|
1721
|
+
* label="Country"
|
|
1722
|
+
* mandatory
|
|
1723
|
+
* error={countryError}
|
|
1724
|
+
* />
|
|
1725
|
+
* ```
|
|
1629
1726
|
*/
|
|
1630
|
-
const Select = ({ options, value, onChange, label, mandatory = false, placeholder = 'Select an option', disabled = false, width }) => {
|
|
1727
|
+
const Select = ({ options, value, onChange, label, mandatory = false, placeholder = 'Select an option', disabled = false, width, error, id, ariaLabel, }) => {
|
|
1631
1728
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
1632
1729
|
const triggerRef = React.useRef(null);
|
|
1730
|
+
const generatedId = React.useId();
|
|
1731
|
+
const selectId = id ?? generatedId;
|
|
1732
|
+
const errorId = error ? `${selectId}-error` : undefined;
|
|
1633
1733
|
const selectedOption = options.find(option => option.value === value);
|
|
1634
1734
|
const menuWidth = triggerRef.current?.offsetWidth ?? width;
|
|
1635
1735
|
const handleTriggerClick = () => {
|
|
@@ -1637,6 +1737,18 @@ const Select = ({ options, value, onChange, label, mandatory = false, placeholde
|
|
|
1637
1737
|
setIsOpen(!isOpen);
|
|
1638
1738
|
}
|
|
1639
1739
|
};
|
|
1740
|
+
const handleTriggerKeyDown = (event) => {
|
|
1741
|
+
if (disabled) {
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1745
|
+
event.preventDefault();
|
|
1746
|
+
setIsOpen(prev => !prev);
|
|
1747
|
+
}
|
|
1748
|
+
if (event.key === 'Escape' && isOpen) {
|
|
1749
|
+
setIsOpen(false);
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1640
1752
|
const handleClose = () => {
|
|
1641
1753
|
setIsOpen(false);
|
|
1642
1754
|
};
|
|
@@ -1644,7 +1756,7 @@ const Select = ({ options, value, onChange, label, mandatory = false, placeholde
|
|
|
1644
1756
|
onChange(selectedValue);
|
|
1645
1757
|
setIsOpen(false);
|
|
1646
1758
|
};
|
|
1647
|
-
return (jsxRuntime.jsxs(Stack, { direction: 'column', gap: 'xs', align: 'stretch', width: width ?? '100%', children: [label && (jsxRuntime.jsxs(Stack, { direction: 'row', gap: 'xs', align: 'center', children: [jsxRuntime.jsx(Text, { variant: 'label', fontSize: 'sm', children: label }), mandatory && (jsxRuntime.jsx(Text, { variant: 'label', fontSize: 'sm', color: 'error', children: "*" }))] })), jsxRuntime.jsxs("div", { ref: triggerRef, className: SELECT_STYLES.root({ disabled, width, isOpen }), onClick: handleTriggerClick, role: '
|
|
1759
|
+
return (jsxRuntime.jsxs(Stack, { direction: 'column', gap: 'xs', align: 'stretch', width: width ?? '100%', children: [label && (jsxRuntime.jsxs(Stack, { direction: 'row', gap: 'xs', align: 'center', children: [jsxRuntime.jsx(Text, { variant: 'label', fontSize: 'sm', id: `${selectId}-label`, children: label }), mandatory && (jsxRuntime.jsx(Text, { variant: 'label', fontSize: 'sm', color: 'error', ariaLabel: 'required', children: "*" }))] })), jsxRuntime.jsxs("div", { ref: triggerRef, className: SELECT_STYLES.root({ disabled, width, isOpen, hasError: !!error }), onClick: handleTriggerClick, onKeyDown: handleTriggerKeyDown, role: 'combobox', tabIndex: disabled ? -1 : 0, "aria-expanded": isOpen, "aria-haspopup": 'listbox', "aria-label": ariaLabel || label, "aria-required": mandatory || undefined, "aria-invalid": error ? true : undefined, "aria-errormessage": errorId, children: [jsxRuntime.jsx("div", { className: SELECT_STYLES.trigger, children: jsxRuntime.jsx(Text, { variant: 'p', maxLines: 1, color: selectedOption ? 'text' : 'textSecondary', children: selectedOption ? selectedOption.label : placeholder }) }), jsxRuntime.jsx(Icon, { children: jsxRuntime.jsx(ChevronDownIcon, {}) })] }), error && (jsxRuntime.jsx(Text, { variant: 'span', fontSize: 'sm', color: 'error', id: errorId, role: 'alert', children: error })), jsxRuntime.jsx(Menu, { anchor: isOpen ? triggerRef.current : null, onClose: handleClose, width: menuWidth, children: jsxRuntime.jsx(MenuGroup, { children: options.map(option => (jsxRuntime.jsx(SelectItem, { option: option, isSelected: option.value === value, onSelect: handleSelect }, option.value))) }) })] }));
|
|
1648
1760
|
};
|
|
1649
1761
|
Select.displayName = 'Select';
|
|
1650
1762
|
|
|
@@ -2810,6 +2922,11 @@ const getAlertIcon = (variant) => {
|
|
|
2810
2922
|
* - Smooth animations
|
|
2811
2923
|
* - Dynamic height calculation for proper stacking
|
|
2812
2924
|
*
|
|
2925
|
+
* **Accessibility:**
|
|
2926
|
+
* - Uses `role="alert"` for screen reader announcements
|
|
2927
|
+
* - Error alerts use `aria-live="assertive"` for urgent announcements
|
|
2928
|
+
* - Other variants use `aria-live="polite"` to avoid interrupting users
|
|
2929
|
+
*
|
|
2813
2930
|
* @example
|
|
2814
2931
|
* ```tsx
|
|
2815
2932
|
* <Alert
|
|
@@ -2826,6 +2943,8 @@ const Alert = React.memo(({ text, variant = 'default', position = 'top-right', i
|
|
|
2826
2943
|
const lastHeightRef = React.useRef(0);
|
|
2827
2944
|
const icon = React.useMemo(() => getAlertIcon(variant), [variant]);
|
|
2828
2945
|
const colors = React.useMemo(() => getAlertVariantColors(theme$1, variant), [theme$1, variant]);
|
|
2946
|
+
// Error alerts are urgent → assertive; others are polite
|
|
2947
|
+
const ariaLive = variant === 'error' ? 'assertive' : 'polite';
|
|
2829
2948
|
// Report height changes to parent (only when height actually changes)
|
|
2830
2949
|
React.useEffect(() => {
|
|
2831
2950
|
if (alertRef.current && onHeightChange && isVisible && alertId) {
|
|
@@ -2837,7 +2956,7 @@ const Alert = React.memo(({ text, variant = 'default', position = 'top-right', i
|
|
|
2837
2956
|
}
|
|
2838
2957
|
}
|
|
2839
2958
|
}, [isVisible, text, maxWidth, alertId, onHeightChange]);
|
|
2840
|
-
return (jsxRuntime.jsxs("div", { ref: alertRef, className: ALERT_STYLES.root({ variant, position, isVisible, offsetY, maxWidth }), role: 'alert', "aria-live": '
|
|
2959
|
+
return (jsxRuntime.jsxs("div", { ref: alertRef, className: ALERT_STYLES.root({ variant, position, isVisible, offsetY, maxWidth }), role: 'alert', "aria-live": ariaLive, "aria-atomic": 'true', children: [jsxRuntime.jsx(Icon, { size: 'sm', color: colors.iconColor, children: icon }), jsxRuntime.jsx(Text, { variant: 'span', fontSize: 'sm', color: colors.iconColor, maxLines: 4, children: text })] }));
|
|
2841
2960
|
});
|
|
2842
2961
|
Alert.displayName = 'Alert';
|
|
2843
2962
|
|
|
@@ -2877,15 +2996,98 @@ const MODAL_STYLES = theme.createStyles((theme) => ({
|
|
|
2877
2996
|
}),
|
|
2878
2997
|
}));
|
|
2879
2998
|
|
|
2999
|
+
const FOCUSABLE_SELECTORS = [
|
|
3000
|
+
'a[href]',
|
|
3001
|
+
'button:not([disabled])',
|
|
3002
|
+
'input:not([disabled])',
|
|
3003
|
+
'select:not([disabled])',
|
|
3004
|
+
'textarea:not([disabled])',
|
|
3005
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
3006
|
+
].join(', ');
|
|
3007
|
+
/**
|
|
3008
|
+
* Modal component
|
|
3009
|
+
*
|
|
3010
|
+
* An overlay dialog for focused interactions.
|
|
3011
|
+
*
|
|
3012
|
+
* **Accessibility:**
|
|
3013
|
+
* - Uses `role="dialog"` and `aria-modal="true"` for screen readers
|
|
3014
|
+
* - `aria-labelledby` links to the modal title
|
|
3015
|
+
* - Focus is trapped inside the modal while it is open
|
|
3016
|
+
* - Focus returns to the triggering element when closed
|
|
3017
|
+
* - Escape key closes the modal
|
|
3018
|
+
*
|
|
3019
|
+
* @example
|
|
3020
|
+
* ```tsx
|
|
3021
|
+
* <Modal
|
|
3022
|
+
* isOpen={isOpen}
|
|
3023
|
+
* onClose={() => setIsOpen(false)}
|
|
3024
|
+
* label="Confirm deletion"
|
|
3025
|
+
* action={{ label: 'Delete', onClick: handleDelete }}
|
|
3026
|
+
* >
|
|
3027
|
+
* <Text>Are you sure you want to delete this item?</Text>
|
|
3028
|
+
* </Modal>
|
|
3029
|
+
* ```
|
|
3030
|
+
*/
|
|
2880
3031
|
const Modal = ({ isOpen, onClose, label, children, isForm, action }) => {
|
|
2881
3032
|
// refs
|
|
2882
3033
|
const modalRef = React.useRef(null);
|
|
3034
|
+
const contentRef = React.useRef(null);
|
|
3035
|
+
const previousFocusRef = React.useRef(null);
|
|
3036
|
+
// unique id for aria-labelledby
|
|
3037
|
+
const titleId = React.useId();
|
|
2883
3038
|
// hooks
|
|
2884
3039
|
const { isVisible, isFadingIn } = useTransitionRender(isOpen);
|
|
3040
|
+
// Save focus and trap it inside modal
|
|
3041
|
+
React.useEffect(() => {
|
|
3042
|
+
if (isOpen) {
|
|
3043
|
+
previousFocusRef.current = document.activeElement;
|
|
3044
|
+
// Move focus to first focusable element in modal
|
|
3045
|
+
setTimeout(() => {
|
|
3046
|
+
const content = contentRef.current;
|
|
3047
|
+
if (content) {
|
|
3048
|
+
const focusable = content.querySelectorAll(FOCUSABLE_SELECTORS);
|
|
3049
|
+
focusable[0]?.focus();
|
|
3050
|
+
}
|
|
3051
|
+
}, 50);
|
|
3052
|
+
}
|
|
3053
|
+
else {
|
|
3054
|
+
// Restore focus to the element that triggered the modal
|
|
3055
|
+
previousFocusRef.current?.focus();
|
|
3056
|
+
}
|
|
3057
|
+
}, [isOpen]);
|
|
3058
|
+
// Keyboard: Escape to close + focus trap (Tab/Shift+Tab)
|
|
2885
3059
|
React.useEffect(() => {
|
|
2886
3060
|
const handleKeyDown = (event) => {
|
|
2887
|
-
if (
|
|
3061
|
+
if (!isOpen) {
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
if (event.key === 'Escape') {
|
|
2888
3065
|
onClose();
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
if (event.key === 'Tab') {
|
|
3069
|
+
const content = contentRef.current;
|
|
3070
|
+
if (!content) {
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
const focusable = Array.from(content.querySelectorAll(FOCUSABLE_SELECTORS));
|
|
3074
|
+
if (focusable.length === 0) {
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
const first = focusable[0];
|
|
3078
|
+
const last = focusable[focusable.length - 1];
|
|
3079
|
+
if (event.shiftKey) {
|
|
3080
|
+
if (document.activeElement === first) {
|
|
3081
|
+
event.preventDefault();
|
|
3082
|
+
last.focus();
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
else {
|
|
3086
|
+
if (document.activeElement === last) {
|
|
3087
|
+
event.preventDefault();
|
|
3088
|
+
first.focus();
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
2889
3091
|
}
|
|
2890
3092
|
};
|
|
2891
3093
|
document.addEventListener('keydown', handleKeyDown);
|
|
@@ -2894,13 +3096,17 @@ const Modal = ({ isOpen, onClose, label, children, isForm, action }) => {
|
|
|
2894
3096
|
// actions
|
|
2895
3097
|
const safeInvokeAction = (e) => {
|
|
2896
3098
|
if (action && typeof action.onClick === 'function') {
|
|
2897
|
-
// Cast e to required MouseEvent type (or undefined) and call
|
|
2898
3099
|
action.onClick(e);
|
|
2899
3100
|
}
|
|
2900
3101
|
};
|
|
2901
3102
|
// body of the modal
|
|
2902
|
-
const body = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs(Stack, { justify: 'space-between', height: BUTTON_SIZE, width: '100%', children: [jsxRuntime.jsx(Text, { variant: 'h3', children: label }), !action && (jsxRuntime.jsx(IconButton, { icon: jsxRuntime.jsx(CloseIcon, {}), onClick: onClose, size: 'small', variant: 'text', textColor: 'text' }))] }), children, action && (jsxRuntime.jsxs(Stack, { justify: 'flex-end', width: '100%', children: [jsxRuntime.jsx(Button, { label: 'Cancel', onClick: onClose, variant: 'outlined' }), jsxRuntime.jsx(Button, { ...action, type: isForm ? 'submit' : 'button', onClick: isForm ? undefined : action.onClick })] }))] }));
|
|
2903
|
-
return reactDom.createPortal(jsxRuntime.jsx(React.Fragment, { children: isVisible ? (jsxRuntime.jsx("div", { className: MODAL_STYLES.background(isFadingIn), ref: modalRef,
|
|
3103
|
+
const body = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs(Stack, { justify: 'space-between', height: BUTTON_SIZE, width: '100%', children: [jsxRuntime.jsx(Text, { variant: 'h3', id: titleId, children: label }), !action && (jsxRuntime.jsx(IconButton, { icon: jsxRuntime.jsx(CloseIcon, {}), onClick: onClose, size: 'small', variant: 'text', textColor: 'text', ariaLabel: 'Close dialog' }))] }), children, action && (jsxRuntime.jsxs(Stack, { justify: 'flex-end', width: '100%', children: [jsxRuntime.jsx(Button, { label: 'Cancel', onClick: onClose, variant: 'outlined' }), jsxRuntime.jsx(Button, { ...action, type: isForm ? 'submit' : 'button', onClick: isForm ? undefined : action.onClick })] }))] }));
|
|
3104
|
+
return reactDom.createPortal(jsxRuntime.jsx(React.Fragment, { children: isVisible ? (jsxRuntime.jsx("div", { className: MODAL_STYLES.background(isFadingIn), ref: modalRef, onClick: (e) => {
|
|
3105
|
+
// Close when clicking the backdrop
|
|
3106
|
+
if (e.target === e.currentTarget) {
|
|
3107
|
+
onClose();
|
|
3108
|
+
}
|
|
3109
|
+
}, children: jsxRuntime.jsx("div", { ref: contentRef, className: MODAL_STYLES.content(isFadingIn), role: 'dialog', "aria-modal": 'true', "aria-labelledby": titleId, children: isForm ? (jsxRuntime.jsx(Form$1, { onSubmit: (e) => { e.preventDefault(); safeInvokeAction(); }, children: body })) : (jsxRuntime.jsx(React.Fragment, { children: body })) }) })) : null }), document.body);
|
|
2904
3110
|
};
|
|
2905
3111
|
Modal.displayName = 'Modal';
|
|
2906
3112
|
|