@dododog/ui 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/{chunk-L7OYR6U3.js → chunk-2NMA3AXV.js} +37 -8
  2. package/dist/{chunk-ZZ4EWC53.mjs → chunk-52M2KUC4.mjs} +37 -8
  3. package/dist/{chunk-BKM5VCUS.mjs → chunk-CPBDDGRX.mjs} +4 -1
  4. package/dist/{chunk-FLKEMER2.js → chunk-DJTFRMRY.js} +4 -1
  5. package/dist/chunk-DUCRCDLI.mjs +78 -0
  6. package/dist/{chunk-HH4X2JYO.js → chunk-EKOGDJUT.js} +32 -4
  7. package/dist/{chunk-NHB2TI2B.js → chunk-FHAWYXPX.js} +1 -1
  8. package/dist/{chunk-5UNG2O64.js → chunk-FR46MMNP.js} +7 -1
  9. package/dist/{chunk-EEIPCJQ2.mjs → chunk-KWQKC5TG.mjs} +1 -1
  10. package/dist/{chunk-IJIUUBFT.js → chunk-OUN5BU4Z.js} +1 -1
  11. package/dist/{chunk-QWDKSY6Y.js → chunk-OZ4RJDDZ.js} +46 -2
  12. package/dist/{chunk-762LQMWP.js → chunk-RAXPJ7VC.js} +66 -8
  13. package/dist/{chunk-F2BUMJYW.mjs → chunk-RBVTV3LA.mjs} +1 -1
  14. package/dist/{chunk-B47HQHX3.mjs → chunk-RGZHSI2G.mjs} +1 -1
  15. package/dist/{chunk-GXKON34B.js → chunk-RTI25IBR.js} +1 -1
  16. package/dist/{chunk-LJSMPON7.js → chunk-USJDHMKS.js} +1 -1
  17. package/dist/{chunk-N5ZNXNCO.mjs → chunk-UYOPZZBN.mjs} +32 -4
  18. package/dist/{chunk-EAATIFYL.mjs → chunk-WNBZXXFA.mjs} +1 -1
  19. package/dist/{chunk-JWOV5H6G.mjs → chunk-X53C3IQC.mjs} +7 -1
  20. package/dist/chunk-YGAWCP65.mjs +109 -0
  21. package/dist/components/Autocomplete/index.js +3 -3
  22. package/dist/components/Autocomplete/index.mjs +1 -1
  23. package/dist/components/Checkbox/index.js +2 -2
  24. package/dist/components/Checkbox/index.mjs +1 -1
  25. package/dist/components/DateRangePicker/index.js +3 -3
  26. package/dist/components/DateRangePicker/index.mjs +1 -1
  27. package/dist/components/Input/index.js +3 -3
  28. package/dist/components/Input/index.mjs +1 -1
  29. package/dist/components/Modal/index.d.mts +42 -1
  30. package/dist/components/Modal/index.d.ts +42 -1
  31. package/dist/components/Modal/index.js +2 -2
  32. package/dist/components/Modal/index.mjs +1 -1
  33. package/dist/components/RadioGroup/index.js +2 -2
  34. package/dist/components/RadioGroup/index.mjs +1 -1
  35. package/dist/components/SegmentedControl/index.d.mts +16 -0
  36. package/dist/components/SegmentedControl/index.d.ts +16 -0
  37. package/dist/components/SegmentedControl/index.js +2 -2
  38. package/dist/components/SegmentedControl/index.mjs +1 -1
  39. package/dist/components/Select/index.js +3 -3
  40. package/dist/components/Select/index.mjs +1 -1
  41. package/dist/components/Tabs/index.d.mts +21 -0
  42. package/dist/components/Tabs/index.d.ts +21 -0
  43. package/dist/components/Tabs/index.js +2 -2
  44. package/dist/components/Tabs/index.mjs +1 -1
  45. package/dist/components/Textarea/index.js +3 -3
  46. package/dist/components/Textarea/index.mjs +1 -1
  47. package/dist/index.d.mts +16 -3
  48. package/dist/index.d.ts +16 -3
  49. package/dist/index.js +95 -93
  50. package/dist/index.mjs +23 -21
  51. package/package.json +11 -11
  52. package/dist/chunk-36Y7UU32.mjs +0 -34
  53. package/dist/chunk-VOZPGXQD.mjs +0 -51
@@ -46,26 +46,55 @@ var textareaVariants = classVarianceAuthority.cva(
46
46
  }
47
47
  );
48
48
  var Textarea = React__namespace.forwardRef(
49
- ({ className, size, variant, label, error, required, rows = 4, ...props }, ref) => {
49
+ ({
50
+ className,
51
+ size,
52
+ variant,
53
+ label,
54
+ error,
55
+ required,
56
+ rows = 4,
57
+ id,
58
+ "aria-describedby": ariaDescribedBy,
59
+ "aria-invalid": ariaInvalid,
60
+ "aria-required": ariaRequired,
61
+ ...props
62
+ }, ref) => {
63
+ const generatedId = React__namespace.useId();
64
+ const textareaId = id ?? generatedId;
65
+ const errorId = `${textareaId}-error`;
50
66
  const resolvedVariant = error ? "error" : variant ?? "default";
67
+ const describedBy = [ariaDescribedBy, error ? errorId : null].filter(Boolean).join(" ") || void 0;
51
68
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", children: [
52
- label && /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block text-sm font-medium text-primary mb-1.5", children: [
53
- label,
54
- required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-secondary", children: " *" })
55
- ] }),
69
+ label && /* @__PURE__ */ jsxRuntime.jsxs(
70
+ "label",
71
+ {
72
+ htmlFor: textareaId,
73
+ className: "block text-sm font-medium text-primary mb-1.5",
74
+ children: [
75
+ label,
76
+ required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-secondary", "aria-hidden": "true", children: " *" })
77
+ ]
78
+ }
79
+ ),
56
80
  /* @__PURE__ */ jsxRuntime.jsx(
57
81
  "textarea",
58
82
  {
83
+ ...props,
59
84
  ref,
85
+ id: textareaId,
60
86
  rows,
87
+ required,
88
+ "aria-required": ariaRequired ?? (required ? true : void 0),
89
+ "aria-invalid": ariaInvalid ?? (error ? true : void 0),
90
+ "aria-describedby": describedBy,
61
91
  className: chunkADIDI7AJ_js.cn(
62
92
  textareaVariants({ size, variant: resolvedVariant }),
63
93
  className
64
- ),
65
- ...props
94
+ )
66
95
  }
67
96
  ),
68
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-red-500", children: error })
97
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { id: errorId, role: "alert", className: "mt-1 text-sm text-red-600", children: error })
69
98
  ] });
70
99
  }
71
100
  );
@@ -24,26 +24,55 @@ var textareaVariants = cva(
24
24
  }
25
25
  );
26
26
  var Textarea = React.forwardRef(
27
- ({ className, size, variant, label, error, required, rows = 4, ...props }, ref) => {
27
+ ({
28
+ className,
29
+ size,
30
+ variant,
31
+ label,
32
+ error,
33
+ required,
34
+ rows = 4,
35
+ id,
36
+ "aria-describedby": ariaDescribedBy,
37
+ "aria-invalid": ariaInvalid,
38
+ "aria-required": ariaRequired,
39
+ ...props
40
+ }, ref) => {
41
+ const generatedId = React.useId();
42
+ const textareaId = id ?? generatedId;
43
+ const errorId = `${textareaId}-error`;
28
44
  const resolvedVariant = error ? "error" : variant ?? "default";
45
+ const describedBy = [ariaDescribedBy, error ? errorId : null].filter(Boolean).join(" ") || void 0;
29
46
  return /* @__PURE__ */ jsxs("div", { className: "w-full", children: [
30
- label && /* @__PURE__ */ jsxs("label", { className: "block text-sm font-medium text-primary mb-1.5", children: [
31
- label,
32
- required && /* @__PURE__ */ jsx("span", { className: "text-secondary", children: " *" })
33
- ] }),
47
+ label && /* @__PURE__ */ jsxs(
48
+ "label",
49
+ {
50
+ htmlFor: textareaId,
51
+ className: "block text-sm font-medium text-primary mb-1.5",
52
+ children: [
53
+ label,
54
+ required && /* @__PURE__ */ jsx("span", { className: "text-secondary", "aria-hidden": "true", children: " *" })
55
+ ]
56
+ }
57
+ ),
34
58
  /* @__PURE__ */ jsx(
35
59
  "textarea",
36
60
  {
61
+ ...props,
37
62
  ref,
63
+ id: textareaId,
38
64
  rows,
65
+ required,
66
+ "aria-required": ariaRequired ?? (required ? true : void 0),
67
+ "aria-invalid": ariaInvalid ?? (error ? true : void 0),
68
+ "aria-describedby": describedBy,
39
69
  className: cn(
40
70
  textareaVariants({ size, variant: resolvedVariant }),
41
71
  className
42
- ),
43
- ...props
72
+ )
44
73
  }
45
74
  ),
46
- error && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-red-500", children: error })
75
+ error && /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "mt-1 text-sm text-red-600", children: error })
47
76
  ] });
48
77
  }
49
78
  );
@@ -8,8 +8,11 @@ function startOfDay(d) {
8
8
  return r;
9
9
  }
10
10
  function addMonths(d, n) {
11
+ const year = d.getFullYear();
12
+ const month = d.getMonth() + n;
13
+ const lastDayOfTargetMonth = new Date(year, month + 1, 0).getDate();
11
14
  const r = new Date(d);
12
- r.setMonth(r.getMonth() + n);
15
+ r.setFullYear(year, month, Math.min(d.getDate(), lastDayOfTargetMonth));
13
16
  return r;
14
17
  }
15
18
  function addDays(d, n) {
@@ -30,8 +30,11 @@ function startOfDay(d) {
30
30
  return r;
31
31
  }
32
32
  function addMonths(d, n) {
33
+ const year = d.getFullYear();
34
+ const month = d.getMonth() + n;
35
+ const lastDayOfTargetMonth = new Date(year, month + 1, 0).getDate();
33
36
  const r = new Date(d);
34
- r.setMonth(r.getMonth() + n);
37
+ r.setFullYear(year, month, Math.min(d.getDate(), lastDayOfTargetMonth));
35
38
  return r;
36
39
  }
37
40
  function addDays(d, n) {
@@ -0,0 +1,78 @@
1
+ import { cn } from './chunk-IMKLN273.mjs';
2
+ import * as React from 'react';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ var SegmentedControl = React.forwardRef(
6
+ ({
7
+ options,
8
+ value,
9
+ onChange,
10
+ className,
11
+ "aria-label": ariaLabel,
12
+ "aria-labelledby": ariaLabelledBy
13
+ }, ref) => {
14
+ const optionRefs = React.useRef([]);
15
+ const selectedIndex = options.findIndex((option) => option.value === value);
16
+ const rovingIndex = selectedIndex === -1 ? 0 : selectedIndex;
17
+ const handleKeyDown = (event, index) => {
18
+ const lastIndex = options.length - 1;
19
+ let nextIndex;
20
+ switch (event.key) {
21
+ case "ArrowRight":
22
+ case "ArrowDown":
23
+ nextIndex = index === lastIndex ? 0 : index + 1;
24
+ break;
25
+ case "ArrowLeft":
26
+ case "ArrowUp":
27
+ nextIndex = index === 0 ? lastIndex : index - 1;
28
+ break;
29
+ case "Home":
30
+ nextIndex = 0;
31
+ break;
32
+ case "End":
33
+ nextIndex = lastIndex;
34
+ break;
35
+ default:
36
+ return;
37
+ }
38
+ event.preventDefault();
39
+ const nextOption = options[nextIndex];
40
+ if (!nextOption) return;
41
+ onChange(nextOption.value);
42
+ optionRefs.current[nextIndex]?.focus();
43
+ };
44
+ return /* @__PURE__ */ jsx(
45
+ "div",
46
+ {
47
+ ref,
48
+ className: cn("flex gap-1 bg-gray-100 rounded-full p-1", className),
49
+ role: "radiogroup",
50
+ "aria-label": ariaLabelledBy ? void 0 : ariaLabel,
51
+ "aria-labelledby": ariaLabelledBy,
52
+ children: options.map((option, index) => /* @__PURE__ */ jsx(
53
+ "button",
54
+ {
55
+ type: "button",
56
+ role: "radio",
57
+ "aria-checked": value === option.value,
58
+ tabIndex: index === rovingIndex ? 0 : -1,
59
+ ref: (node) => {
60
+ optionRefs.current[index] = node;
61
+ },
62
+ onKeyDown: (event) => handleKeyDown(event, index),
63
+ onClick: () => onChange(option.value),
64
+ className: cn(
65
+ "px-3 py-1.5 text-sm rounded-full transition-all",
66
+ value === option.value ? "bg-white text-gray-900 shadow-sm font-medium" : "text-gray-600 hover:text-gray-900"
67
+ ),
68
+ children: option.label
69
+ },
70
+ option.value
71
+ ))
72
+ }
73
+ );
74
+ }
75
+ );
76
+ SegmentedControl.displayName = "SegmentedControl";
77
+
78
+ export { SegmentedControl };
@@ -37,6 +37,7 @@ var FOCUSABLE_SELECTOR = [
37
37
  "[contenteditable]:not([contenteditable='false'])",
38
38
  '[tabindex]:not([tabindex="-1"])'
39
39
  ].join(", ");
40
+ var DEFAULT_Z_INDEX_CLASS = "z-50";
40
41
  function getFocusableElements(panel) {
41
42
  return Array.from(panel.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
42
43
  (element) => !element.closest("[hidden], [aria-hidden='true'], details:not([open])")
@@ -54,6 +55,11 @@ var Modal = React__namespace.forwardRef(
54
55
  children,
55
56
  title,
56
57
  className,
58
+ variant = "centered",
59
+ overlayClassName,
60
+ zIndexClassName = DEFAULT_Z_INDEX_CLASS,
61
+ initialFocusRef,
62
+ returnFocusTo,
57
63
  "aria-label": ariaLabel,
58
64
  "aria-labelledby": ariaLabelledBy,
59
65
  "aria-describedby": ariaDescribedBy
@@ -62,10 +68,20 @@ var Modal = React__namespace.forwardRef(
62
68
  const generatedTitleId = React__namespace.useId();
63
69
  const titleId = title ? `${generatedTitleId}-title` : void 0;
64
70
  const labelledBy = ariaLabelledBy ?? titleId;
71
+ const isFullBleed = variant === "fullBleed";
72
+ const initialFocusRefLatest = React__namespace.useRef(initialFocusRef);
73
+ initialFocusRefLatest.current = initialFocusRef;
74
+ const returnFocusToLatest = React__namespace.useRef(returnFocusTo);
75
+ returnFocusToLatest.current = returnFocusTo;
65
76
  React__namespace.useEffect(() => {
66
77
  if (!isOpen) return;
67
78
  const trigger = document.activeElement instanceof HTMLElement ? document.activeElement : null;
68
79
  return () => {
80
+ const designated = returnFocusToLatest.current?.current ?? null;
81
+ if (designated && designated.isConnected) {
82
+ designated.focus();
83
+ return;
84
+ }
69
85
  if (trigger && trigger.isConnected) trigger.focus();
70
86
  };
71
87
  }, [isOpen]);
@@ -73,6 +89,11 @@ var Modal = React__namespace.forwardRef(
73
89
  if (!isOpen) return;
74
90
  const panel = panelRef.current;
75
91
  if (!panel) return;
92
+ const preferred = initialFocusRefLatest.current?.current ?? null;
93
+ if (preferred) {
94
+ preferred.focus();
95
+ return;
96
+ }
76
97
  const focusable = getFocusableElements(panel);
77
98
  (focusable[0] ?? panel).focus();
78
99
  }, [isOpen]);
@@ -124,14 +145,21 @@ var Modal = React__namespace.forwardRef(
124
145
  "div",
125
146
  {
126
147
  ref,
127
- className: "fixed inset-0 z-50 flex items-center justify-center p-4",
148
+ className: chunkADIDI7AJ_js.cn(
149
+ "fixed inset-0",
150
+ zIndexClassName,
151
+ // La mise en page centree (et sa marge de 16 px) n'existe que dans la
152
+ // variante `centered` : une surface bord a bord n'en veut pas.
153
+ !isFullBleed && "flex items-center justify-center p-4",
154
+ overlayClassName
155
+ ),
128
156
  role: "dialog",
129
157
  "aria-modal": "true",
130
158
  "aria-labelledby": labelledBy,
131
159
  "aria-label": labelledBy ? void 0 : ariaLabel,
132
160
  "aria-describedby": ariaDescribedBy,
133
161
  children: [
134
- /* @__PURE__ */ jsxRuntime.jsx(
162
+ !isFullBleed && /* @__PURE__ */ jsxRuntime.jsx(
135
163
  "div",
136
164
  {
137
165
  className: "absolute inset-0 bg-black/50",
@@ -145,7 +173,7 @@ var Modal = React__namespace.forwardRef(
145
173
  ref: panelRef,
146
174
  tabIndex: -1,
147
175
  className: chunkADIDI7AJ_js.cn(
148
- "relative bg-white rounded-2xl shadow-xl max-w-md w-full max-h-[90vh] overflow-auto outline-none",
176
+ isFullBleed ? "relative h-full w-full outline-none" : "relative bg-white rounded-2xl shadow-xl max-w-md w-full max-h-[90vh] overflow-auto outline-none",
149
177
  className
150
178
  ),
151
179
  children: [
@@ -186,7 +214,7 @@ var Modal = React__namespace.forwardRef(
186
214
  }
187
215
  )
188
216
  ] }),
189
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: chunkADIDI7AJ_js.cn(title ? "px-6 pb-6" : "p-6"), children })
217
+ isFullBleed ? children : /* @__PURE__ */ jsxRuntime.jsx("div", { className: chunkADIDI7AJ_js.cn(title ? "px-6 pb-6" : "p-6"), children })
190
218
  ]
191
219
  }
192
220
  )
@@ -200,7 +200,7 @@ var Autocomplete = React__namespace.forwardRef(
200
200
  }
201
201
  )
202
202
  ] }),
203
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-red-500", children: error })
203
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 text-sm text-red-600", children: error })
204
204
  ] });
205
205
  }
206
206
  );
@@ -100,7 +100,13 @@ var Input = React__namespace.forwardRef(
100
100
  ),
101
101
  rightIcon && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute right-3 top-1/2 -translate-y-1/2 text-text-muted", children: rightIcon })
102
102
  ] }),
103
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { id: errorId, className: "mt-1 text-sm text-red-500", children: error })
103
+ error && // Pas de `role="alert"` ici, contrairement aux autres champs de la
104
+ // bibliotheque : les trois seuls consommateurs de cette prop
105
+ // (BookingForm, PaymentForm, EscalationForm) sont des formulaires du
106
+ // tunnel, et BookingForm annonce deja ses fautes par un resume en
107
+ // region live (TTR-651). Un second canal produirait des annonces
108
+ // concurrentes sur le chemin de paiement.
109
+ /* @__PURE__ */ jsxRuntime.jsx("p", { id: errorId, className: "mt-1 text-sm text-red-600", children: error })
104
110
  ] });
105
111
  }
106
112
  );
@@ -63,7 +63,7 @@ var Checkbox = React.forwardRef(
63
63
  ]
64
64
  }
65
65
  ),
66
- error && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-red-500", children: error })
66
+ error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 text-sm text-red-600", children: error })
67
67
  ] });
68
68
  }
69
69
  );
@@ -85,7 +85,7 @@ var Checkbox = React__namespace.forwardRef(
85
85
  ]
86
86
  }
87
87
  ),
88
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-red-500", children: error })
88
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 text-sm text-red-600", children: error })
89
89
  ] });
90
90
  }
91
91
  );
@@ -25,19 +25,63 @@ function _interopNamespace(e) {
25
25
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
26
26
 
27
27
  var SegmentedControl = React__namespace.forwardRef(
28
- ({ options, value, onChange, className }, ref) => {
28
+ ({
29
+ options,
30
+ value,
31
+ onChange,
32
+ className,
33
+ "aria-label": ariaLabel,
34
+ "aria-labelledby": ariaLabelledBy
35
+ }, ref) => {
36
+ const optionRefs = React__namespace.useRef([]);
37
+ const selectedIndex = options.findIndex((option) => option.value === value);
38
+ const rovingIndex = selectedIndex === -1 ? 0 : selectedIndex;
39
+ const handleKeyDown = (event, index) => {
40
+ const lastIndex = options.length - 1;
41
+ let nextIndex;
42
+ switch (event.key) {
43
+ case "ArrowRight":
44
+ case "ArrowDown":
45
+ nextIndex = index === lastIndex ? 0 : index + 1;
46
+ break;
47
+ case "ArrowLeft":
48
+ case "ArrowUp":
49
+ nextIndex = index === 0 ? lastIndex : index - 1;
50
+ break;
51
+ case "Home":
52
+ nextIndex = 0;
53
+ break;
54
+ case "End":
55
+ nextIndex = lastIndex;
56
+ break;
57
+ default:
58
+ return;
59
+ }
60
+ event.preventDefault();
61
+ const nextOption = options[nextIndex];
62
+ if (!nextOption) return;
63
+ onChange(nextOption.value);
64
+ optionRefs.current[nextIndex]?.focus();
65
+ };
29
66
  return /* @__PURE__ */ jsxRuntime.jsx(
30
67
  "div",
31
68
  {
32
69
  ref,
33
70
  className: chunkADIDI7AJ_js.cn("flex gap-1 bg-gray-100 rounded-full p-1", className),
34
71
  role: "radiogroup",
35
- children: options.map((option) => /* @__PURE__ */ jsxRuntime.jsx(
72
+ "aria-label": ariaLabelledBy ? void 0 : ariaLabel,
73
+ "aria-labelledby": ariaLabelledBy,
74
+ children: options.map((option, index) => /* @__PURE__ */ jsxRuntime.jsx(
36
75
  "button",
37
76
  {
38
77
  type: "button",
39
78
  role: "radio",
40
79
  "aria-checked": value === option.value,
80
+ tabIndex: index === rovingIndex ? 0 : -1,
81
+ ref: (node) => {
82
+ optionRefs.current[index] = node;
83
+ },
84
+ onKeyDown: (event) => handleKeyDown(event, index),
41
85
  onClick: () => onChange(option.value),
42
86
  className: chunkADIDI7AJ_js.cn(
43
87
  "px-3 py-1.5 text-sm rounded-full transition-all",
@@ -25,17 +25,57 @@ function _interopNamespace(e) {
25
25
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
26
26
 
27
27
  var Tabs = React__namespace.forwardRef(
28
- ({ tabs, defaultTab, value, onChange, className }, ref) => {
28
+ ({
29
+ tabs,
30
+ defaultTab,
31
+ value,
32
+ onChange,
33
+ className,
34
+ "aria-label": ariaLabel,
35
+ "aria-labelledby": ariaLabelledBy
36
+ }, ref) => {
29
37
  const [internalValue, setInternalValue] = React__namespace.useState(
30
38
  defaultTab || tabs[0]?.key || ""
31
39
  );
32
40
  const activeKey = value ?? internalValue;
33
- const handleChange = (key) => {
34
- if (onChange) {
35
- onChange(key);
36
- } else {
37
- setInternalValue(key);
41
+ const idPrefix = React__namespace.useId();
42
+ const tabId = (key) => `${idPrefix}-tab-${key}`;
43
+ const panelId = (key) => `${idPrefix}-panel-${key}`;
44
+ const tabRefs = React__namespace.useRef([]);
45
+ const handleChange = React__namespace.useCallback(
46
+ (key) => {
47
+ if (onChange) {
48
+ onChange(key);
49
+ } else {
50
+ setInternalValue(key);
51
+ }
52
+ },
53
+ [onChange]
54
+ );
55
+ const handleKeyDown = (event, index) => {
56
+ const lastIndex = tabs.length - 1;
57
+ let nextIndex;
58
+ switch (event.key) {
59
+ case "ArrowRight":
60
+ nextIndex = index === lastIndex ? 0 : index + 1;
61
+ break;
62
+ case "ArrowLeft":
63
+ nextIndex = index === 0 ? lastIndex : index - 1;
64
+ break;
65
+ case "Home":
66
+ nextIndex = 0;
67
+ break;
68
+ case "End":
69
+ nextIndex = lastIndex;
70
+ break;
71
+ default:
72
+ return;
38
73
  }
74
+ event.preventDefault();
75
+ const nextTab = tabs[nextIndex];
76
+ if (!nextTab) return;
77
+ handleChange(nextTab.key);
78
+ tabRefs.current[nextIndex]?.focus();
39
79
  };
40
80
  const activeTab = tabs.find((t) => t.key === activeKey);
41
81
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: chunkADIDI7AJ_js.cn("w-full", className), children: [
@@ -43,13 +83,22 @@ var Tabs = React__namespace.forwardRef(
43
83
  "div",
44
84
  {
45
85
  role: "tablist",
86
+ "aria-label": ariaLabelledBy ? void 0 : ariaLabel,
87
+ "aria-labelledby": ariaLabelledBy,
46
88
  className: "flex border-b border-gray-200 gap-1 overflow-x-auto",
47
- children: tabs.map((tab) => /* @__PURE__ */ jsxRuntime.jsxs(
89
+ children: tabs.map((tab, index) => /* @__PURE__ */ jsxRuntime.jsxs(
48
90
  "button",
49
91
  {
92
+ id: tabId(tab.key),
50
93
  role: "tab",
51
94
  type: "button",
52
95
  "aria-selected": tab.key === activeKey,
96
+ "aria-controls": panelId(tab.key),
97
+ tabIndex: tab.key === activeKey ? 0 : -1,
98
+ ref: (node) => {
99
+ tabRefs.current[index] = node;
100
+ },
101
+ onKeyDown: (event) => handleKeyDown(event, index),
53
102
  onClick: () => handleChange(tab.key),
54
103
  className: chunkADIDI7AJ_js.cn(
55
104
  "flex items-center gap-2 px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 transition-colors -mb-px",
@@ -64,7 +113,16 @@ var Tabs = React__namespace.forwardRef(
64
113
  ))
65
114
  }
66
115
  ),
67
- /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tabpanel", className: "py-4", children: activeTab?.content })
116
+ activeTab && /* @__PURE__ */ jsxRuntime.jsx(
117
+ "div",
118
+ {
119
+ role: "tabpanel",
120
+ id: panelId(activeTab.key),
121
+ "aria-labelledby": tabId(activeTab.key),
122
+ className: "py-4",
123
+ children: activeTab.content
124
+ }
125
+ )
68
126
  ] });
69
127
  }
70
128
  );
@@ -55,7 +55,7 @@ var RadioGroup = React.forwardRef(
55
55
  })
56
56
  }
57
57
  ),
58
- error && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-red-500", children: error })
58
+ error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 text-sm text-red-600", children: error })
59
59
  ] });
60
60
  }
61
61
  );
@@ -178,7 +178,7 @@ var Autocomplete = React.forwardRef(
178
178
  }
179
179
  )
180
180
  ] }),
181
- error && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-red-500", children: error })
181
+ error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 text-sm text-red-600", children: error })
182
182
  ] });
183
183
  }
184
184
  );
@@ -77,7 +77,7 @@ var RadioGroup = React__namespace.forwardRef(
77
77
  })
78
78
  }
79
79
  ),
80
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-red-500", children: error })
80
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 text-sm text-red-600", children: error })
81
81
  ] });
82
82
  }
83
83
  );
@@ -239,7 +239,7 @@ var Select = React__namespace.forwardRef(
239
239
  }
240
240
  )
241
241
  ] }),
242
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { id: errorId, className: "mt-1 text-sm text-red-500", children: error })
242
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { id: errorId, role: "alert", className: "mt-1 text-sm text-red-600", children: error })
243
243
  ] });
244
244
  }
245
245
  );