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