@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.
Files changed (77) hide show
  1. package/README.md +35 -7
  2. package/dist/cjs/components/actions/button/Button.d.ts +8 -0
  3. package/dist/cjs/components/actions/button/Button.props.d.ts +10 -0
  4. package/dist/cjs/components/actions/button/utils/getButtonSizeStyles.utils.d.ts +12 -0
  5. package/dist/cjs/components/actions/button/utils/getButtonVariantStyles.utils.d.ts +7 -0
  6. package/dist/cjs/components/actions/icon-button/IconButton.d.ts +13 -0
  7. package/dist/cjs/components/actions/icon-button/IconButton.props.d.ts +11 -1
  8. package/dist/cjs/components/actions/icon-button/utils/getIconButtonSizeStyles.utils.d.ts +9 -0
  9. package/dist/cjs/components/data-display/avatar/Avatar.d.ts +16 -1
  10. package/dist/cjs/components/data-display/avatar/Avatar.props.d.ts +18 -4
  11. package/dist/cjs/components/data-display/avatar/utils/getAvatarSizes.utils.d.ts +11 -0
  12. package/dist/cjs/components/data-display/chip/utils/getChipColorStyles.utils.d.ts +12 -0
  13. package/dist/cjs/components/data-display/chip/utils/getChipContentSize.utils.d.ts +6 -0
  14. package/dist/cjs/components/data-display/chip/utils/getChipSizeStyles.utils.d.ts +10 -0
  15. package/dist/cjs/components/data-display/status/utils/getStatusColorStyles.utils.d.ts +7 -0
  16. package/dist/cjs/components/data-display/status/utils/getStatusContentSize.utils.d.ts +8 -0
  17. package/dist/cjs/components/data-display/status/utils/getStatusSizeStyles.utils.d.ts +12 -0
  18. package/dist/cjs/components/forms/date-picker/calendar/calendar-grid/CalendarGrid.props.d.ts +1 -1
  19. package/dist/cjs/components/forms/date-picker/utils/datePicker.utils.d.ts +13 -0
  20. package/dist/cjs/components/forms/input/Input.props.d.ts +9 -1
  21. package/dist/cjs/components/forms/select/Select.d.ts +18 -0
  22. package/dist/cjs/components/forms/select/Select.props.d.ts +14 -0
  23. package/dist/cjs/components/forms/textarea/TextArea.props.d.ts +9 -1
  24. package/dist/cjs/components/foundation/text/Text.props.d.ts +2 -0
  25. package/dist/cjs/components/foundation/text/utils/getTextVariantStyles.utils.d.ts +7 -0
  26. package/dist/cjs/components/foundation/text/utils/getTruncateTextStyles.utils.d.ts +21 -0
  27. package/dist/cjs/components/foundation/text/utils/parseTextWithBold.utils.d.ts +7 -0
  28. package/dist/cjs/components/navigation/breadcrumb/utils/buildBreadcrumbChildren.utils.d.ts +5 -0
  29. package/dist/cjs/components/navigation/breadcrumb/utils/flattenChildren.utils.d.ts +5 -0
  30. package/dist/cjs/components/navigation/breadcrumb/utils/insertSeparators.utils.d.ts +5 -0
  31. package/dist/cjs/components/navigation/breadcrumb/utils/isSeparator.utils.d.ts +5 -0
  32. package/dist/cjs/components/overlay/alert/Alert.d.ts +5 -0
  33. package/dist/cjs/components/overlay/alert/utils/getAlertIcon.utils.d.ts +8 -0
  34. package/dist/cjs/components/overlay/alert/utils/getAlertPositionStyles.utils.d.ts +8 -0
  35. package/dist/cjs/components/overlay/alert/utils/getAlertVariantColors.utils.d.ts +14 -0
  36. package/dist/cjs/components/overlay/modal/Modal.d.ts +24 -0
  37. package/dist/cjs/index.js +234 -28
  38. package/dist/cjs/index.js.map +1 -1
  39. package/dist/esm/components/actions/button/Button.d.ts +8 -0
  40. package/dist/esm/components/actions/button/Button.props.d.ts +10 -0
  41. package/dist/esm/components/actions/button/utils/getButtonSizeStyles.utils.d.ts +12 -0
  42. package/dist/esm/components/actions/button/utils/getButtonVariantStyles.utils.d.ts +7 -0
  43. package/dist/esm/components/actions/icon-button/IconButton.d.ts +13 -0
  44. package/dist/esm/components/actions/icon-button/IconButton.props.d.ts +11 -1
  45. package/dist/esm/components/actions/icon-button/utils/getIconButtonSizeStyles.utils.d.ts +9 -0
  46. package/dist/esm/components/data-display/avatar/Avatar.d.ts +16 -1
  47. package/dist/esm/components/data-display/avatar/Avatar.props.d.ts +18 -4
  48. package/dist/esm/components/data-display/avatar/utils/getAvatarSizes.utils.d.ts +11 -0
  49. package/dist/esm/components/data-display/chip/utils/getChipColorStyles.utils.d.ts +12 -0
  50. package/dist/esm/components/data-display/chip/utils/getChipContentSize.utils.d.ts +6 -0
  51. package/dist/esm/components/data-display/chip/utils/getChipSizeStyles.utils.d.ts +10 -0
  52. package/dist/esm/components/data-display/status/utils/getStatusColorStyles.utils.d.ts +7 -0
  53. package/dist/esm/components/data-display/status/utils/getStatusContentSize.utils.d.ts +8 -0
  54. package/dist/esm/components/data-display/status/utils/getStatusSizeStyles.utils.d.ts +12 -0
  55. package/dist/esm/components/forms/date-picker/calendar/calendar-grid/CalendarGrid.props.d.ts +1 -1
  56. package/dist/esm/components/forms/date-picker/utils/datePicker.utils.d.ts +13 -0
  57. package/dist/esm/components/forms/input/Input.props.d.ts +9 -1
  58. package/dist/esm/components/forms/select/Select.d.ts +18 -0
  59. package/dist/esm/components/forms/select/Select.props.d.ts +14 -0
  60. package/dist/esm/components/forms/textarea/TextArea.props.d.ts +9 -1
  61. package/dist/esm/components/foundation/text/Text.props.d.ts +2 -0
  62. package/dist/esm/components/foundation/text/utils/getTextVariantStyles.utils.d.ts +7 -0
  63. package/dist/esm/components/foundation/text/utils/getTruncateTextStyles.utils.d.ts +21 -0
  64. package/dist/esm/components/foundation/text/utils/parseTextWithBold.utils.d.ts +7 -0
  65. package/dist/esm/components/navigation/breadcrumb/utils/buildBreadcrumbChildren.utils.d.ts +5 -0
  66. package/dist/esm/components/navigation/breadcrumb/utils/flattenChildren.utils.d.ts +5 -0
  67. package/dist/esm/components/navigation/breadcrumb/utils/insertSeparators.utils.d.ts +5 -0
  68. package/dist/esm/components/navigation/breadcrumb/utils/isSeparator.utils.d.ts +5 -0
  69. package/dist/esm/components/overlay/alert/Alert.d.ts +5 -0
  70. package/dist/esm/components/overlay/alert/utils/getAlertIcon.utils.d.ts +8 -0
  71. package/dist/esm/components/overlay/alert/utils/getAlertPositionStyles.utils.d.ts +8 -0
  72. package/dist/esm/components/overlay/alert/utils/getAlertVariantColors.utils.d.ts +14 -0
  73. package/dist/esm/components/overlay/modal/Modal.d.ts +24 -0
  74. package/dist/esm/index.js +235 -29
  75. package/dist/esm/index.js.map +1 -1
  76. package/dist/index.d.ts +153 -8
  77. 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 text.
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
- return (jsxRuntime.jsx("div", { className: AVATAR_STYLES.root({ hasImage, clickable, size, color, borderColor, backgroundColor }), onClick: (event) => {
545
- if (onClick) {
546
- onClick(event);
547
- }
548
- }, children: hasImage ? (jsxRuntime.jsx("img", { src: image, alt: label || 'Avatar', className: AVATAR_STYLES.image })) : (jsxRuntime.jsx(Text, { variant: 'label', fontSize: AVATAR_SIZES[size].fontSize, children: label || '?' })) }));
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
- 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, }) => {
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
- const IconButton = ({ icon, variant = 'contained', active = false, type = 'button', onClick, disabled, textColor: customTextColor, backgroundColor, hoverBackgroundColor, activeBackgroundColor, size = 'medium', ariaLabel, ariaLabelledBy, ariaDescribedBy, role, tabIndex, }) => {
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: ariaLabel || label, variant: 'text', size: 'small', textColor: 'textSecondary' }) }))] })] }));
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: 'button', tabIndex: disabled ? -1 : 0, "aria-expanded": isOpen, "aria-haspopup": 'listbox', 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, {}) })] }), 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))) }) })] }));
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": 'polite', 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 })] }));
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 (event.key === 'Escape' && isOpen) {
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, children: jsxRuntime.jsx("div", { className: MODAL_STYLES.content(isFadingIn), children: isForm ? (jsxRuntime.jsx(Form$1, { onSubmit: (e) => { e.preventDefault(); safeInvokeAction(); }, children: body })) : (jsxRuntime.jsx(React.Fragment, { children: body })) }) })) : null }), document.body);
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