@zentauri-ui/zentauri-components 1.4.61 → 1.4.62

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 (260) hide show
  1. package/dist/{chunk-UXGHUBNJ.mjs → chunk-2PJF7DLJ.mjs} +3 -3
  2. package/dist/{chunk-UXGHUBNJ.mjs.map → chunk-2PJF7DLJ.mjs.map} +1 -1
  3. package/dist/{chunk-WDCIZHXY.mjs → chunk-45FCOQ63.mjs} +5 -3
  4. package/dist/chunk-45FCOQ63.mjs.map +1 -0
  5. package/dist/{chunk-RDSPHBHK.mjs → chunk-4ANBTJ5G.mjs} +49 -6
  6. package/dist/chunk-4ANBTJ5G.mjs.map +1 -0
  7. package/dist/chunk-4E66ICIR.mjs +158 -0
  8. package/dist/chunk-4E66ICIR.mjs.map +1 -0
  9. package/dist/{chunk-XLAFQ24R.js → chunk-4U6FOCFK.js} +22 -14
  10. package/dist/chunk-4U6FOCFK.js.map +1 -0
  11. package/dist/{chunk-XWM2S6VV.mjs → chunk-EQSSYK27.mjs} +12 -10
  12. package/dist/chunk-EQSSYK27.mjs.map +1 -0
  13. package/dist/{chunk-5QB2KNZQ.js → chunk-FGGYDAX3.js} +5 -3
  14. package/dist/chunk-FGGYDAX3.js.map +1 -0
  15. package/dist/{chunk-7HL3A4YF.mjs → chunk-IK75NHRX.mjs} +63 -14
  16. package/dist/chunk-IK75NHRX.mjs.map +1 -0
  17. package/dist/{chunk-BORK3BJO.mjs → chunk-J56L4ZQ3.mjs} +10 -10
  18. package/dist/{chunk-BORK3BJO.mjs.map → chunk-J56L4ZQ3.mjs.map} +1 -1
  19. package/dist/{chunk-PGH27VTL.mjs → chunk-JF3FKUUP.mjs} +21 -13
  20. package/dist/chunk-JF3FKUUP.mjs.map +1 -0
  21. package/dist/{chunk-WZKGRU3U.js → chunk-MQZB5EPD.js} +92 -27
  22. package/dist/chunk-MQZB5EPD.js.map +1 -0
  23. package/dist/{chunk-N4NO3SYL.js → chunk-NX3IHMT7.js} +22 -14
  24. package/dist/chunk-NX3IHMT7.js.map +1 -0
  25. package/dist/{chunk-BVXTOEBI.mjs → chunk-OG2WM5YK.mjs} +45 -17
  26. package/dist/chunk-OG2WM5YK.mjs.map +1 -0
  27. package/dist/{chunk-IXDJ3IPG.mjs → chunk-OXS6UJUG.mjs} +21 -13
  28. package/dist/chunk-OXS6UJUG.mjs.map +1 -0
  29. package/dist/{chunk-PCK6LX3K.js → chunk-PFOV3U7W.js} +3 -3
  30. package/dist/{chunk-PCK6LX3K.js.map → chunk-PFOV3U7W.js.map} +1 -1
  31. package/dist/{chunk-2PQEXQVR.js → chunk-THCNTPPL.js} +62 -13
  32. package/dist/chunk-THCNTPPL.js.map +1 -0
  33. package/dist/chunk-UP6S75V5.js +160 -0
  34. package/dist/chunk-UP6S75V5.js.map +1 -0
  35. package/dist/{chunk-P5HUBXUX.js → chunk-V2IWLR4O.js} +48 -5
  36. package/dist/chunk-V2IWLR4O.js.map +1 -0
  37. package/dist/{chunk-3OR47XMY.js → chunk-VSKL5LOB.js} +45 -17
  38. package/dist/chunk-VSKL5LOB.js.map +1 -0
  39. package/dist/{chunk-E3DZNJAD.js → chunk-Y4EDWZKH.js} +12 -10
  40. package/dist/chunk-Y4EDWZKH.js.map +1 -0
  41. package/dist/{chunk-YNCD6TKE.mjs → chunk-Y4IFVO46.mjs} +93 -28
  42. package/dist/chunk-Y4IFVO46.mjs.map +1 -0
  43. package/dist/{chunk-BITDSQMR.js → chunk-ZNDHS5OK.js} +10 -10
  44. package/dist/{chunk-BITDSQMR.js.map → chunk-ZNDHS5OK.js.map} +1 -1
  45. package/dist/hooks/useFocusManagement/useFocusManagement.d.ts +5 -14
  46. package/dist/hooks/useFocusManagement/useFocusManagement.d.ts.map +1 -1
  47. package/dist/hooks/useFocusManagement.js +2 -2
  48. package/dist/hooks/useFocusManagement.mjs +1 -1
  49. package/dist/ui/badge/animated.js +2 -2
  50. package/dist/ui/badge/animated.mjs +1 -1
  51. package/dist/ui/badge/badge-base.d.ts +1 -1
  52. package/dist/ui/badge/badge-base.d.ts.map +1 -1
  53. package/dist/ui/badge/types.d.ts +1 -0
  54. package/dist/ui/badge/types.d.ts.map +1 -1
  55. package/dist/ui/badge/variants.d.ts +7 -7
  56. package/dist/ui/badge.js +4 -4
  57. package/dist/ui/badge.mjs +2 -2
  58. package/dist/ui/buttons/animated.js +3 -3
  59. package/dist/ui/buttons/animated.mjs +1 -1
  60. package/dist/ui/buttons.js +4 -4
  61. package/dist/ui/buttons.mjs +2 -2
  62. package/dist/ui/drawer/animated/drawer-content-animated.d.ts.map +1 -1
  63. package/dist/ui/drawer/animated.js +17 -18
  64. package/dist/ui/drawer/animated.js.map +1 -1
  65. package/dist/ui/drawer/animated.mjs +8 -9
  66. package/dist/ui/drawer/animated.mjs.map +1 -1
  67. package/dist/ui/drawer/drawer-base.d.ts +1 -1
  68. package/dist/ui/drawer/drawer-base.d.ts.map +1 -1
  69. package/dist/ui/drawer/types.d.ts +1 -0
  70. package/dist/ui/drawer/types.d.ts.map +1 -1
  71. package/dist/ui/drawer.js +12 -12
  72. package/dist/ui/drawer.mjs +2 -2
  73. package/dist/ui/dropdown/dropdown.d.ts +1 -1
  74. package/dist/ui/dropdown/dropdown.d.ts.map +1 -1
  75. package/dist/ui/dropdown/types.d.ts +1 -0
  76. package/dist/ui/dropdown/types.d.ts.map +1 -1
  77. package/dist/ui/dropdown/variants.d.ts +1 -1
  78. package/dist/ui/dropdown.js +25 -7
  79. package/dist/ui/dropdown.js.map +1 -1
  80. package/dist/ui/dropdown.mjs +26 -8
  81. package/dist/ui/dropdown.mjs.map +1 -1
  82. package/dist/ui/empty-state/animated.js +2 -2
  83. package/dist/ui/empty-state/animated.mjs +1 -1
  84. package/dist/ui/empty-state/empty-state-base.d.ts.map +1 -1
  85. package/dist/ui/empty-state/types.d.ts +1 -0
  86. package/dist/ui/empty-state/types.d.ts.map +1 -1
  87. package/dist/ui/empty-state.js +10 -10
  88. package/dist/ui/empty-state.mjs +2 -2
  89. package/dist/ui/file-upload/file-upload.d.ts.map +1 -1
  90. package/dist/ui/file-upload.js +1 -3
  91. package/dist/ui/file-upload.js.map +1 -1
  92. package/dist/ui/file-upload.mjs +1 -3
  93. package/dist/ui/file-upload.mjs.map +1 -1
  94. package/dist/ui/inputs/input-base.d.ts.map +1 -1
  95. package/dist/ui/inputs/types.d.ts +3 -1
  96. package/dist/ui/inputs/types.d.ts.map +1 -1
  97. package/dist/ui/inputs.js +46 -2
  98. package/dist/ui/inputs.js.map +1 -1
  99. package/dist/ui/inputs.mjs +46 -2
  100. package/dist/ui/inputs.mjs.map +1 -1
  101. package/dist/ui/modal/animated/modal-content-animated.d.ts.map +1 -1
  102. package/dist/ui/modal/animated.js +10 -11
  103. package/dist/ui/modal/animated.js.map +1 -1
  104. package/dist/ui/modal/animated.mjs +7 -8
  105. package/dist/ui/modal/animated.mjs.map +1 -1
  106. package/dist/ui/modal/modal-base.d.ts +4 -2
  107. package/dist/ui/modal/modal-base.d.ts.map +1 -1
  108. package/dist/ui/modal.js +13 -13
  109. package/dist/ui/modal.mjs +3 -3
  110. package/dist/ui/pagination.js +6 -6
  111. package/dist/ui/pagination.js.map +1 -1
  112. package/dist/ui/pagination.mjs +3 -3
  113. package/dist/ui/pagination.mjs.map +1 -1
  114. package/dist/ui/progress/animated/progress-animated.d.ts.map +1 -1
  115. package/dist/ui/progress/animated.js +49 -11
  116. package/dist/ui/progress/animated.js.map +1 -1
  117. package/dist/ui/progress/animated.mjs +44 -6
  118. package/dist/ui/progress/animated.mjs.map +1 -1
  119. package/dist/ui/progress/progress-base.d.ts.map +1 -1
  120. package/dist/ui/progress/types.d.ts +3 -0
  121. package/dist/ui/progress/types.d.ts.map +1 -1
  122. package/dist/ui/progress.js +9 -9
  123. package/dist/ui/progress.mjs +2 -2
  124. package/dist/ui/search/search-bar.d.ts +1 -1
  125. package/dist/ui/search/search-bar.d.ts.map +1 -1
  126. package/dist/ui/search.js +2 -0
  127. package/dist/ui/search.js.map +1 -1
  128. package/dist/ui/search.mjs +2 -0
  129. package/dist/ui/search.mjs.map +1 -1
  130. package/dist/ui/select/select.d.ts +1 -1
  131. package/dist/ui/select/select.d.ts.map +1 -1
  132. package/dist/ui/select/types.d.ts +1 -0
  133. package/dist/ui/select/types.d.ts.map +1 -1
  134. package/dist/ui/select/variants.d.ts +1 -1
  135. package/dist/ui/select/variants.d.ts.map +1 -1
  136. package/dist/ui/select.js +121 -39
  137. package/dist/ui/select.js.map +1 -1
  138. package/dist/ui/select.mjs +122 -40
  139. package/dist/ui/select.mjs.map +1 -1
  140. package/dist/ui/skeleton/variants.d.ts +1 -1
  141. package/dist/ui/slider/slider.d.ts.map +1 -1
  142. package/dist/ui/slider/types.d.ts +8 -2
  143. package/dist/ui/slider/types.d.ts.map +1 -1
  144. package/dist/ui/slider.js +43 -7
  145. package/dist/ui/slider.js.map +1 -1
  146. package/dist/ui/slider.mjs +43 -7
  147. package/dist/ui/slider.mjs.map +1 -1
  148. package/dist/ui/spinner/animated/spinner.d.ts.map +1 -1
  149. package/dist/ui/spinner/animated.js +62 -50
  150. package/dist/ui/spinner/animated.js.map +1 -1
  151. package/dist/ui/spinner/animated.mjs +63 -51
  152. package/dist/ui/spinner/animated.mjs.map +1 -1
  153. package/dist/ui/stepper/stepper.d.ts +2 -7
  154. package/dist/ui/stepper/stepper.d.ts.map +1 -1
  155. package/dist/ui/stepper/types.d.ts +3 -3
  156. package/dist/ui/stepper/types.d.ts.map +1 -1
  157. package/dist/ui/stepper/variants.d.ts +1 -1
  158. package/dist/ui/stepper.js +7 -5
  159. package/dist/ui/stepper.js.map +1 -1
  160. package/dist/ui/stepper.mjs +7 -5
  161. package/dist/ui/stepper.mjs.map +1 -1
  162. package/dist/ui/table/animated.js +8 -8
  163. package/dist/ui/table/animated.mjs +2 -2
  164. package/dist/ui/table/table-base.d.ts +1 -1
  165. package/dist/ui/table/table-base.d.ts.map +1 -1
  166. package/dist/ui/table/types.d.ts +5 -1
  167. package/dist/ui/table/types.d.ts.map +1 -1
  168. package/dist/ui/table.js +14 -14
  169. package/dist/ui/table.mjs +1 -1
  170. package/dist/ui/tabs/animated.js +2 -2
  171. package/dist/ui/tabs/animated.mjs +1 -1
  172. package/dist/ui/tabs/tabs-base.d.ts.map +1 -1
  173. package/dist/ui/tabs/types.d.ts +2 -1
  174. package/dist/ui/tabs/types.d.ts.map +1 -1
  175. package/dist/ui/tabs.js +9 -9
  176. package/dist/ui/tabs.mjs +1 -1
  177. package/dist/ui/toast/animated.js +7 -7
  178. package/dist/ui/toast/animated.mjs +1 -1
  179. package/dist/ui/toast.js +12 -12
  180. package/dist/ui/toast.mjs +1 -1
  181. package/dist/ui/toggle/toggle-base.d.ts.map +1 -1
  182. package/dist/ui/toggle.js +28 -3
  183. package/dist/ui/toggle.js.map +1 -1
  184. package/dist/ui/toggle.mjs +29 -4
  185. package/dist/ui/toggle.mjs.map +1 -1
  186. package/dist/ui/tooltip/animated.js +3 -3
  187. package/dist/ui/tooltip/animated.mjs +1 -1
  188. package/dist/ui/tooltip/tooltip-base.d.ts.map +1 -1
  189. package/dist/ui/tooltip/types.d.ts +1 -0
  190. package/dist/ui/tooltip/types.d.ts.map +1 -1
  191. package/dist/ui/tooltip.js +7 -7
  192. package/dist/ui/tooltip.mjs +1 -1
  193. package/package.json +1 -1
  194. package/src/hooks/useFocusManagement/useFocusManagement.test.tsx +8 -0
  195. package/src/hooks/useFocusManagement/useFocusManagement.ts +162 -33
  196. package/src/ui/badge/badge-base.tsx +4 -1
  197. package/src/ui/badge/types.ts +1 -0
  198. package/src/ui/badge/variants.ts +7 -7
  199. package/src/ui/buttons/button.test.tsx +1 -1
  200. package/src/ui/buttons/variants.ts +8 -8
  201. package/src/ui/drawer/animated/drawer-content-animated.tsx +4 -5
  202. package/src/ui/drawer/drawer-base.tsx +16 -8
  203. package/src/ui/drawer/types.ts +1 -0
  204. package/src/ui/dropdown/dropdown.test.tsx +1 -3
  205. package/src/ui/dropdown/dropdown.tsx +23 -5
  206. package/src/ui/dropdown/types.ts +1 -0
  207. package/src/ui/dropdown/variants.ts +2 -2
  208. package/src/ui/empty-state/empty-state-base.tsx +9 -1
  209. package/src/ui/empty-state/types.ts +1 -0
  210. package/src/ui/file-upload/file-upload.tsx +0 -2
  211. package/src/ui/inputs/input-base.tsx +60 -6
  212. package/src/ui/inputs/types.ts +3 -1
  213. package/src/ui/modal/animated/modal-content-animated.tsx +4 -5
  214. package/src/ui/modal/modal-base.tsx +19 -9
  215. package/src/ui/modal/modal.test.tsx +38 -0
  216. package/src/ui/pagination/pagination.tsx +2 -2
  217. package/src/ui/progress/animated/progress-animated.tsx +42 -3
  218. package/src/ui/progress/progress-base.tsx +59 -3
  219. package/src/ui/progress/types.ts +3 -0
  220. package/src/ui/search/search-bar.tsx +5 -0
  221. package/src/ui/select/select.tsx +97 -6
  222. package/src/ui/select/types.ts +1 -0
  223. package/src/ui/select/variants.ts +5 -3
  224. package/src/ui/slider/slider.test.tsx +25 -1
  225. package/src/ui/slider/slider.tsx +45 -4
  226. package/src/ui/slider/types.ts +8 -2
  227. package/src/ui/spinner/animated/spinner.tsx +4 -0
  228. package/src/ui/stepper/stepper.test.tsx +6 -7
  229. package/src/ui/stepper/stepper.tsx +11 -10
  230. package/src/ui/stepper/types.ts +7 -3
  231. package/src/ui/table/table-base.tsx +32 -6
  232. package/src/ui/table/types.ts +8 -1
  233. package/src/ui/tabs/tabs-base.tsx +71 -10
  234. package/src/ui/tabs/types.ts +2 -1
  235. package/src/ui/tabs/variants.ts +1 -1
  236. package/src/ui/toast/toast-base.tsx +1 -1
  237. package/src/ui/toggle/toggle-base.tsx +37 -4
  238. package/src/ui/tooltip/tooltip-base.tsx +119 -22
  239. package/src/ui/tooltip/types.ts +1 -0
  240. package/src/ui/tooltip/variants.ts +2 -2
  241. package/dist/chunk-2PQEXQVR.js.map +0 -1
  242. package/dist/chunk-3OR47XMY.js.map +0 -1
  243. package/dist/chunk-5QB2KNZQ.js.map +0 -1
  244. package/dist/chunk-7HL3A4YF.mjs.map +0 -1
  245. package/dist/chunk-BVXTOEBI.mjs.map +0 -1
  246. package/dist/chunk-E3DZNJAD.js.map +0 -1
  247. package/dist/chunk-IXDJ3IPG.mjs.map +0 -1
  248. package/dist/chunk-N4NO3SYL.js.map +0 -1
  249. package/dist/chunk-P5HUBXUX.js.map +0 -1
  250. package/dist/chunk-PGH27VTL.mjs.map +0 -1
  251. package/dist/chunk-RDSPHBHK.mjs.map +0 -1
  252. package/dist/chunk-WDCIZHXY.mjs.map +0 -1
  253. package/dist/chunk-WL5I7RVS.mjs +0 -54
  254. package/dist/chunk-WL5I7RVS.mjs.map +0 -1
  255. package/dist/chunk-WZKGRU3U.js.map +0 -1
  256. package/dist/chunk-XLAFQ24R.js.map +0 -1
  257. package/dist/chunk-XWM2S6VV.mjs.map +0 -1
  258. package/dist/chunk-YNCD6TKE.mjs.map +0 -1
  259. package/dist/chunk-YPLVTUYL.js +0 -56
  260. package/dist/chunk-YPLVTUYL.js.map +0 -1
@@ -1,6 +1,15 @@
1
1
  "use client";
2
2
 
3
- import { createContext, useContext, useMemo } from "react";
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useId,
10
+ useRef,
11
+ useState,
12
+ } from "react";
4
13
 
5
14
  import { cn, clamp } from "../../lib/utils";
6
15
 
@@ -40,6 +49,32 @@ export function ProgressBase(props: ProgressProps) {
40
49
  } = props;
41
50
  const clamped = clamp(value, min, max);
42
51
  const percent = max === min ? 0 : ((clamped - min) / (max - min)) * 100;
52
+ const labelSlotId = `${useId()}-progress-label`;
53
+ const labelSlotCountRef = useRef(0);
54
+ const [labelSlotMounted, setLabelSlotMounted] = useState(false);
55
+ const registerProgressLabel = useCallback(() => {
56
+ labelSlotCountRef.current += 1;
57
+ if (labelSlotCountRef.current === 1) {
58
+ setLabelSlotMounted(true);
59
+ }
60
+ return () => {
61
+ labelSlotCountRef.current -= 1;
62
+ if (labelSlotCountRef.current === 0) {
63
+ setLabelSlotMounted(false);
64
+ }
65
+ };
66
+ }, []);
67
+ const hasInlineLabelProp = Boolean(label?.trim().length);
68
+
69
+ const labelingProps = useMemo(() => {
70
+ if (hasInlineLabelProp) {
71
+ return { "aria-label": label?.trim() ?? "Progress" };
72
+ }
73
+ if (labelSlotMounted) {
74
+ return { "aria-labelledby": labelSlotId };
75
+ }
76
+ return { "aria-label": "Progress" };
77
+ }, [hasInlineLabelProp, label, labelSlotId, labelSlotMounted]);
43
78
 
44
79
  const ctx = useMemo(
45
80
  () => ({
@@ -51,8 +86,21 @@ export function ProgressBase(props: ProgressProps) {
51
86
  striped: Boolean(striped),
52
87
  animated: Boolean(animated),
53
88
  appearance: appearance ?? "default",
89
+ labelSlotId,
90
+ registerProgressLabel,
54
91
  }),
55
- [animated, appearance, clamped, max, min, shape, size, striped],
92
+ [
93
+ animated,
94
+ appearance,
95
+ clamped,
96
+ labelSlotId,
97
+ max,
98
+ min,
99
+ registerProgressLabel,
100
+ shape,
101
+ size,
102
+ striped,
103
+ ],
56
104
  );
57
105
 
58
106
  return (
@@ -64,7 +112,7 @@ export function ProgressBase(props: ProgressProps) {
64
112
  aria-valuemin={min}
65
113
  aria-valuemax={max}
66
114
  aria-valuenow={clamped}
67
- aria-label={label}
115
+ {...labelingProps}
68
116
  className={cn(
69
117
  progressVariants({ appearance, size, shape, striped, animated }),
70
118
  className,
@@ -121,8 +169,16 @@ export function ProgressBar({
121
169
  ProgressBar.displayName = "ProgressBar";
122
170
 
123
171
  export function ProgressLabel({ className, children }: ProgressSectionProps) {
172
+ const { labelSlotId, registerProgressLabel } =
173
+ useProgressContext("ProgressLabel");
174
+
175
+ useEffect(() => {
176
+ return registerProgressLabel();
177
+ }, [registerProgressLabel]);
178
+
124
179
  return (
125
180
  <div
181
+ id={labelSlotId}
126
182
  data-slot="progress-label"
127
183
  className={cn("mb-2 font-medium text-slate-200", className)}
128
184
  >
@@ -30,4 +30,7 @@ export type ProgressCtx = {
30
30
  striped: boolean;
31
31
  animated: boolean;
32
32
  appearance: NonNullable<ProgressProps["appearance"]>;
33
+ labelSlotId: string;
34
+ /** Increments the label mount count; returned cleanup decrements. Multiple labels are supported. */
35
+ registerProgressLabel: () => () => void;
33
36
  };
@@ -24,6 +24,7 @@ export const SearchBar = function SearchBar(
24
24
  comboboxListboxId,
25
25
  comboboxActiveOptionId,
26
26
  comboboxExpanded,
27
+ "aria-label": ariaLabel,
27
28
  ref,
28
29
  ...rest
29
30
  }: SearchBarProps,
@@ -53,6 +54,10 @@ export const SearchBar = function SearchBar(
53
54
  spellCheck={false}
54
55
  disabled={disabled}
55
56
  value={value}
57
+ aria-label={
58
+ ariaLabel ??
59
+ (combobox ? undefined : "Search")
60
+ }
56
61
  data-slot="search-bar-input"
57
62
  className={cn(
58
63
  inputVariants({ appearance, size: inputSize, ring, as: "input" }),
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { useState, useEffect, useCallback, useRef } from "react";
2
+ import { useState, useEffect, useCallback, useRef, useId } from "react";
3
3
  import {
4
4
  SelectProps,
5
5
  SelectOption,
@@ -32,6 +32,7 @@ export const Select = ({
32
32
  onChange,
33
33
  multiple = true,
34
34
  }: SelectProps) => {
35
+ const listboxId = `${useId()}-listbox`;
35
36
  const [internal, setInternal] = useState<string[]>(defaultValue);
36
37
  const [open, setOpen] = useState(false);
37
38
  const [options, setOptions] = useState<SelectOption[]>([]);
@@ -97,6 +98,7 @@ export const Select = ({
97
98
  registerOption,
98
99
  options,
99
100
  multiple,
101
+ listboxId,
100
102
  }}
101
103
  >
102
104
  <div ref={rootRef} className="relative w-full">
@@ -110,16 +112,25 @@ export const SelectTrigger = ({
110
112
  className,
111
113
  variant,
112
114
  size,
115
+ onClick,
113
116
  ...props
114
117
  }: SelectTriggerProps) => {
115
- const { open, setOpen } = useSelect();
118
+ const { open, setOpen, listboxId } = useSelect();
116
119
 
117
120
  return (
118
121
  <button
119
122
  type="button"
120
- onClick={() => setOpen(!open)}
123
+ aria-expanded={open}
124
+ aria-haspopup="listbox"
125
+ aria-controls={listboxId}
121
126
  className={cn(selectTriggerVariants({ variant, size }), className)}
122
127
  {...props}
128
+ onClick={(event) => {
129
+ onClick?.(event);
130
+ if (!event.defaultPrevented) {
131
+ setOpen(!open);
132
+ }
133
+ }}
123
134
  />
124
135
  );
125
136
  };
@@ -161,12 +172,85 @@ export const SelectContent = ({
161
172
  spacing = "default",
162
173
  ...props
163
174
  }: SelectContentProps) => {
164
- const { open } = useSelect();
175
+ const { open, listboxId, multiple } = useSelect();
176
+ const panelRef = useRef<HTMLDivElement>(null);
177
+
178
+ useEffect(() => {
179
+ if (!open) {
180
+ return;
181
+ }
182
+ const panel = panelRef.current;
183
+ if (!panel) {
184
+ return;
185
+ }
186
+ const opts = Array.from(
187
+ panel.querySelectorAll<HTMLElement>('[role="option"]'),
188
+ ).filter((el) => el.getAttribute("aria-disabled") !== "true");
189
+ requestAnimationFrame(() => opts[0]?.focus());
190
+ }, [open]);
191
+
192
+ useEffect(() => {
193
+ if (!open) {
194
+ return;
195
+ }
196
+ const panel = panelRef.current;
197
+ if (!panel) {
198
+ return;
199
+ }
200
+
201
+ const enabledOptions = () =>
202
+ Array.from(panel.querySelectorAll<HTMLElement>('[role="option"]')).filter(
203
+ (el) => el.getAttribute("aria-disabled") !== "true",
204
+ );
205
+
206
+ const handleKeyDown = (event: KeyboardEvent) => {
207
+ const options = enabledOptions();
208
+ if (options.length === 0) {
209
+ return;
210
+ }
211
+
212
+ const idx = options.findIndex((el) => el === document.activeElement);
213
+
214
+ if (event.key === "ArrowDown") {
215
+ event.preventDefault();
216
+ const next = idx < 0 ? 0 : Math.min(idx + 1, options.length - 1);
217
+ options[next]?.focus();
218
+ return;
219
+ }
220
+
221
+ if (event.key === "ArrowUp") {
222
+ event.preventDefault();
223
+ const prev =
224
+ idx <= 0 ? options.length - 1 : Math.max(idx - 1, 0);
225
+ options[prev]?.focus();
226
+ return;
227
+ }
228
+
229
+ if (event.key === "Home") {
230
+ event.preventDefault();
231
+ options[0]?.focus();
232
+ return;
233
+ }
234
+
235
+ if (event.key === "End") {
236
+ event.preventDefault();
237
+ options[options.length - 1]?.focus();
238
+ }
239
+ };
240
+
241
+ panel.addEventListener("keydown", handleKeyDown);
242
+ return () => panel.removeEventListener("keydown", handleKeyDown);
243
+ }, [open]);
165
244
 
166
245
  if (!open) return null;
167
246
 
168
247
  return (
169
248
  <div
249
+ ref={panelRef}
250
+ id={listboxId}
251
+ role="listbox"
252
+ aria-multiselectable={multiple}
253
+ tabIndex={-1}
170
254
  className={cn(
171
255
  selectContentVariants({ appearance, size, spacing }),
172
256
  className,
@@ -198,10 +282,17 @@ export const SelectItem = ({
198
282
  <div
199
283
  role="option"
200
284
  aria-selected={isActive}
201
- tabIndex={0}
285
+ aria-disabled={disabled ? true : undefined}
286
+ tabIndex={-1}
202
287
  onClick={() => !disabled && toggleValue(value)}
203
288
  onKeyDown={(e) => {
204
- if (e.key === "Enter") toggleValue(value);
289
+ if (disabled) {
290
+ return;
291
+ }
292
+ if (e.key === "Enter" || e.key === " ") {
293
+ e.preventDefault();
294
+ toggleValue(value);
295
+ }
205
296
  }}
206
297
  data-selected={isActive ? "true" : "false"}
207
298
  className={cn(
@@ -40,6 +40,7 @@ export type SelectContextType = {
40
40
  registerOption: (option: SelectOption) => void;
41
41
  options: SelectOption[];
42
42
  multiple: boolean;
43
+ listboxId: string;
43
44
  };
44
45
 
45
46
  export type SelectProps = {
@@ -1,7 +1,7 @@
1
1
  import { cva } from "class-variance-authority";
2
2
 
3
3
  export const selectTriggerVariants = cva(
4
- "flex items-center justify-between rounded-md border transition-all focus:outline-none",
4
+ "flex items-center justify-between rounded-md border transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2",
5
5
  {
6
6
  variants: {
7
7
  variant: {
@@ -50,8 +50,10 @@ export const selectTriggerVariants = cva(
50
50
  },
51
51
  );
52
52
 
53
- export const selectItemVariants = cva("cursor-pointer px-3 py-2 rounded-md", {
54
- variants: {
53
+ export const selectItemVariants = cva(
54
+ "cursor-pointer px-3 py-2 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-inset",
55
+ {
56
+ variants: {
55
57
  appearance: {
56
58
  default:
57
59
  "bg-white text-gray-900 data-[selected=true]:bg-gray-200 data-[selected=true]:text-gray-900",
@@ -41,7 +41,9 @@ describe("Slider", () => {
41
41
  </SliderTrack>
42
42
  </Slider>,
43
43
  );
44
- expect(screen.getByRole("slider")).toHaveAttribute("aria-valuenow", "25");
44
+ const thumb = screen.getByRole("slider");
45
+ expect(thumb).toHaveAttribute("aria-valuenow", "25");
46
+ expect(thumb).toHaveAttribute("aria-label", "Level");
45
47
  });
46
48
 
47
49
  it("should adjust value with keyboard", async () => {
@@ -91,4 +93,26 @@ describe("RangeSlider", () => {
91
93
  );
92
94
  expect(screen.getAllByRole("slider")).toHaveLength(2);
93
95
  });
96
+
97
+ it("should give each thumb an accessible name when aria-label is set", () => {
98
+ render(<RangeSlider defaultValue={[10, 90]} aria-label="Budget" />);
99
+ const thumbs = screen.getAllByRole("slider");
100
+ expect(thumbs[0]).toHaveAttribute("aria-label", "Budget, minimum");
101
+ expect(thumbs[1]).toHaveAttribute("aria-label", "Budget, maximum");
102
+ });
103
+
104
+ it("should mirror aria-labelledby onto thumbs when aria-label is omitted", () => {
105
+ render(
106
+ <>
107
+ <span id="range-lbl">Volume band</span>
108
+ <RangeSlider
109
+ defaultValue={[10, 90]}
110
+ aria-labelledby="range-lbl"
111
+ />
112
+ </>,
113
+ );
114
+ const thumbs = screen.getAllByRole("slider");
115
+ expect(thumbs[0]).toHaveAttribute("aria-labelledby", "range-lbl");
116
+ expect(thumbs[1]).toHaveAttribute("aria-labelledby", "range-lbl");
117
+ });
94
118
  });
@@ -208,8 +208,21 @@ export function Slider({
208
208
  size: size ?? "md",
209
209
  appearance: appearance ?? "default",
210
210
  trackRef,
211
+ "aria-label": ariaLabel,
212
+ "aria-labelledby": ariaLabelledBy,
211
213
  }),
212
- [appearance, disabled, max, min, setValue, size, step, value],
214
+ [
215
+ appearance,
216
+ ariaLabel,
217
+ ariaLabelledBy,
218
+ disabled,
219
+ max,
220
+ min,
221
+ setValue,
222
+ size,
223
+ step,
224
+ value,
225
+ ],
213
226
  );
214
227
 
215
228
  return (
@@ -303,8 +316,18 @@ export function SliderThumb({
303
316
  ref: refProp,
304
317
  ...rest
305
318
  }: SliderThumbProps & { ref?: Ref<HTMLDivElement> }) {
306
- const { min, max, value, step, setValue, disabled, size, trackRef } =
307
- useSliderContext("SliderThumb");
319
+ const {
320
+ min,
321
+ max,
322
+ value,
323
+ step,
324
+ setValue,
325
+ disabled,
326
+ size,
327
+ trackRef,
328
+ "aria-label": sliderAriaLabel,
329
+ "aria-labelledby": sliderAriaLabelledBy,
330
+ } = useSliderContext("SliderThumb");
308
331
  /** Horizontal thumb position; same mapping as `SliderRange` width uses. */
309
332
  const pct = max === min ? 0 : ((value - min) / (max - min)) * 100;
310
333
 
@@ -414,6 +437,8 @@ export function SliderThumb({
414
437
  role="slider"
415
438
  tabIndex={disabled ? -1 : 0}
416
439
  data-slot="slider-thumb"
440
+ aria-label={sliderAriaLabel}
441
+ aria-labelledby={sliderAriaLabelledBy}
417
442
  aria-valuemin={min}
418
443
  aria-valuemax={max}
419
444
  aria-valuenow={value}
@@ -561,6 +586,13 @@ export function RangeSlider({
561
586
 
562
587
  const resolvedSize = size ?? "md";
563
588
 
589
+ const lowThumbAriaLabel =
590
+ ariaLabel !== undefined ? `${ariaLabel}, minimum` : undefined;
591
+ const highThumbAriaLabel =
592
+ ariaLabel !== undefined ? `${ariaLabel}, maximum` : undefined;
593
+ const thumbAriaLabelledBy =
594
+ ariaLabel === undefined ? ariaLabelledBy : undefined;
595
+
564
596
  return (
565
597
  <div
566
598
  ref={ref}
@@ -568,7 +600,6 @@ export function RangeSlider({
568
600
  role="group"
569
601
  aria-label={ariaLabel}
570
602
  aria-labelledby={ariaLabelledBy}
571
- aria-valuetext={`${lo} – ${hi}`}
572
603
  className={cn(sliderRootVariants({ size: resolvedSize }), className)}
573
604
  {...rest}
574
605
  >
@@ -596,6 +627,8 @@ export function RangeSlider({
596
627
  trackRef={trackRef}
597
628
  onMoveClientX={(x) => moveThumb(0, x)}
598
629
  onNudge={(delta) => setPair([lo + delta, hi])}
630
+ aria-label={lowThumbAriaLabel}
631
+ aria-labelledby={thumbAriaLabelledBy}
599
632
  />
600
633
  <RangeThumb
601
634
  disabled={disabled}
@@ -608,6 +641,8 @@ export function RangeSlider({
608
641
  trackRef={trackRef}
609
642
  onMoveClientX={(x) => moveThumb(1, x)}
610
643
  onNudge={(delta) => setPair([lo, hi + delta])}
644
+ aria-label={highThumbAriaLabel}
645
+ aria-labelledby={thumbAriaLabelledBy}
611
646
  />
612
647
  </div>
613
648
  </div>
@@ -629,6 +664,8 @@ type RangeThumbProps = {
629
664
  onMoveClientX: (clientX: number) => void;
630
665
  /** Relative keyboard adjustment in value units; parent merges into the pair. */
631
666
  onNudge: (delta: number) => void;
667
+ "aria-label"?: string;
668
+ "aria-labelledby"?: string;
632
669
  };
633
670
 
634
671
  /** Private thumb implementation shared by the low and high endpoints. */
@@ -643,6 +680,8 @@ function RangeThumb({
643
680
  trackRef,
644
681
  onMoveClientX,
645
682
  onNudge,
683
+ "aria-label": thumbAriaLabel,
684
+ "aria-labelledby": thumbAriaLabelledBy,
646
685
  }: RangeThumbProps) {
647
686
  const onPointerDown = useCallback(
648
687
  (event: ReactPointerEvent<HTMLDivElement>) => {
@@ -712,6 +751,8 @@ function RangeThumb({
712
751
  role="slider"
713
752
  tabIndex={disabled ? -1 : 0}
714
753
  data-slot="range-slider-thumb"
754
+ aria-label={thumbAriaLabel}
755
+ aria-labelledby={thumbAriaLabelledBy}
715
756
  aria-valuemin={min}
716
757
  aria-valuemax={max}
717
758
  aria-valuenow={value}
@@ -20,9 +20,9 @@ export type SliderProps = SliderRootVariantProps & {
20
20
  onValueChange?: (value: number) => void;
21
21
  disabled?: boolean;
22
22
  appearance?: VariantProps<typeof sliderRangeVariants>["appearance"];
23
- /** Label for the slider group (accessibility). */
23
+ /** Accessible name for the slider (applied to the focusable thumb). */
24
24
  "aria-label"?: string;
25
- /** Optional visible label id */
25
+ /** Visible label element id (`aria-labelledby` on the focusable thumb). */
26
26
  "aria-labelledby"?: string;
27
27
  children?: ReactNode;
28
28
  } & Omit<ComponentPropsWithoutRef<"div">, "children" | "defaultValue">;
@@ -45,7 +45,9 @@ export type RangeSliderProps = SliderRootVariantProps & {
45
45
  onValueChange?: (value: [number, number]) => void;
46
46
  disabled?: boolean;
47
47
  appearance?: VariantProps<typeof sliderRangeVariants>["appearance"];
48
+ /** Accessible name for the range control (each thumb gets a distinct suffix). */
48
49
  "aria-label"?: string;
50
+ /** Visible label element id for each thumb when `aria-label` is not used. */
49
51
  "aria-labelledby"?: string;
50
52
  } & Omit<ComponentPropsWithoutRef<"div">, "children" | "defaultValue">;
51
53
 
@@ -63,4 +65,8 @@ export type SliderCtx = {
63
65
  size: NonNullable<SliderProps["size"]>;
64
66
  appearance: SliderAppearance;
65
67
  trackRef: RefObject<HTMLDivElement | null>;
68
+ /** Mirrored from `<Slider>` so the focusable thumb exposes an accessible name. */
69
+ "aria-label"?: string;
70
+ /** Mirrored from `<Slider>` so the focusable thumb exposes an accessible name. */
71
+ "aria-labelledby"?: string;
66
72
  };
@@ -33,6 +33,7 @@ export function Spinner(props: SpinnerProps) {
33
33
  initial={false}
34
34
  {...rest}
35
35
  >
36
+ <span className="sr-only">{ariaLabel}</span>
36
37
  <motion.span
37
38
  className="block size-full rounded-full border-2 border-current border-t-transparent"
38
39
  animate={{ rotate: 360 }}
@@ -62,6 +63,7 @@ export function Spinner(props: SpinnerProps) {
62
63
  initial={false}
63
64
  {...rest}
64
65
  >
66
+ <span className="sr-only">{ariaLabel}</span>
65
67
  {[0, 1, 2].map((index) => (
66
68
  <motion.span
67
69
  key={index}
@@ -91,6 +93,7 @@ export function Spinner(props: SpinnerProps) {
91
93
  initial={false}
92
94
  {...rest}
93
95
  >
96
+ <span className="sr-only">{ariaLabel}</span>
94
97
  <motion.span
95
98
  className="block size-full rounded-full bg-current"
96
99
  animate={{ scale: [0.75, 1, 0.75], opacity: [0.45, 1, 0.45] }}
@@ -122,6 +125,7 @@ export function Spinner(props: SpinnerProps) {
122
125
  initial={false}
123
126
  {...rest}
124
127
  >
128
+ <span className="sr-only">{ariaLabel}</span>
125
129
  {[0, 1, 2, 3].map((index) => (
126
130
  <motion.span
127
131
  key={index}
@@ -19,7 +19,7 @@ describe("Stepper", () => {
19
19
  expect(StepperDescription.displayName).toBe("StepperDescription");
20
20
  });
21
21
 
22
- it("should stamp data-slot on stepper root and use list semantics", () => {
22
+ it("should stamp data-slot on stepper root and use ordered list markup", () => {
23
23
  render(
24
24
  <Stepper>
25
25
  <StepperItem>
@@ -30,11 +30,10 @@ describe("Stepper", () => {
30
30
  );
31
31
  const root = document.querySelector('[data-slot="stepper"]');
32
32
  expect(root).toBeTruthy();
33
- expect(root).toHaveAttribute("role", "list");
34
- expect(document.querySelector('[data-slot="stepper-item"]')).toHaveAttribute(
35
- "role",
36
- "listitem",
37
- );
33
+ expect(root?.tagName).toBe("OL");
34
+ expect(
35
+ document.querySelector('[data-slot="stepper-item"]')?.tagName,
36
+ ).toBe("LI");
38
37
  });
39
38
 
40
39
  it("should apply default upcoming appearance to indicators", () => {
@@ -153,7 +152,7 @@ describe("Stepper", () => {
153
152
  });
154
153
 
155
154
  it("should forward ref on Stepper", () => {
156
- const ref = createRef<HTMLDivElement>();
155
+ const ref = createRef<HTMLOListElement>();
157
156
  render(
158
157
  <Stepper ref={ref}>
159
158
  <StepperItem>
@@ -6,7 +6,6 @@ import {
6
6
  isValidElement,
7
7
  useContext,
8
8
  useMemo,
9
- type Ref,
10
9
  } from "react";
11
10
 
12
11
  import { cn } from "../../lib/utils";
@@ -54,7 +53,7 @@ export function Stepper({
54
53
  children,
55
54
  ref,
56
55
  ...rest
57
- }: StepperProps & { ref?: Ref<HTMLDivElement> }) {
56
+ }: StepperProps) {
58
57
  const items = useMemo(
59
58
  () => Children.toArray(children).filter(isValidElement),
60
59
  [children],
@@ -70,11 +69,14 @@ export function Stepper({
70
69
 
71
70
  return (
72
71
  <StepperContext.Provider value={ctx}>
73
- <div
72
+ <ol
74
73
  ref={ref}
75
74
  data-slot="stepper"
76
- role="list"
77
- className={cn(stepperVariants({ orientation }), className)}
75
+ className={cn(
76
+ stepperVariants({ orientation }),
77
+ "m-0 list-none p-0",
78
+ className,
79
+ )}
78
80
  {...rest}
79
81
  >
80
82
  {items.map((child, index) => (
@@ -82,7 +84,7 @@ export function Stepper({
82
84
  {child}
83
85
  </StepperIndexContext.Provider>
84
86
  ))}
85
- </div>
87
+ </ol>
86
88
  </StepperContext.Provider>
87
89
  );
88
90
  }
@@ -94,18 +96,17 @@ export function StepperItem({
94
96
  ref,
95
97
  children,
96
98
  ...rest
97
- }: StepperItemProps & { ref?: React.Ref<HTMLDivElement> }) {
99
+ }: StepperItemProps) {
98
100
  const { orientation } = useStepper("StepperItem");
99
101
  return (
100
- <div
102
+ <li
101
103
  ref={ref}
102
104
  data-slot="stepper-item"
103
- role="listitem"
104
105
  className={cn(stepperItemVariants({ orientation }), className)}
105
106
  {...rest}
106
107
  >
107
108
  {children}
108
- </div>
109
+ </li>
109
110
  );
110
111
  }
111
112
 
@@ -1,5 +1,9 @@
1
1
  import type { VariantProps } from "class-variance-authority";
2
- import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import type {
3
+ ComponentPropsWithoutRef,
4
+ ComponentPropsWithRef,
5
+ ReactNode,
6
+ } from "react";
3
7
 
4
8
  import type { stepperIndicatorVariants, stepperVariants } from "./variants";
5
9
 
@@ -8,9 +12,9 @@ type StepperVariantProps = VariantProps<typeof stepperVariants>;
8
12
  export type StepperProps = StepperVariantProps & {
9
13
  size?: VariantProps<typeof stepperIndicatorVariants>["size"];
10
14
  children?: ReactNode;
11
- } & Omit<ComponentPropsWithoutRef<"div">, "children">;
15
+ } & Omit<ComponentPropsWithRef<"ol">, "children">;
12
16
 
13
- export type StepperItemProps = ComponentPropsWithoutRef<"div"> & {
17
+ export type StepperItemProps = ComponentPropsWithRef<"li"> & {
14
18
  children?: ReactNode;
15
19
  };
16
20