@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
@@ -62,6 +62,7 @@ export function Drawer({
62
62
  const titleId = `${baseId}-title`;
63
63
  const descriptionId = `${baseId}-description`;
64
64
  const contentRef = useRef<HTMLDivElement | null>(null);
65
+ const triggerRef = useRef<HTMLElement | null>(null);
65
66
 
66
67
  const ctx = useMemo(
67
68
  () => ({
@@ -70,6 +71,7 @@ export function Drawer({
70
71
  titleId,
71
72
  descriptionId,
72
73
  contentRef,
74
+ triggerRef,
73
75
  }),
74
76
  [descriptionId, resolvedOpen, setOpen, titleId],
75
77
  );
@@ -86,13 +88,20 @@ export function DrawerTrigger({
86
88
  children,
87
89
  appearance,
88
90
  onClick,
89
- ref,
91
+ ref: refProp,
90
92
  ...rest
91
93
  }: DrawerTriggerProps) {
92
- const { setOpen } = useDrawerContext("DrawerTrigger");
94
+ const { setOpen, triggerRef } = useDrawerContext("DrawerTrigger");
93
95
  return (
94
96
  <button
95
- ref={ref}
97
+ ref={(node) => {
98
+ triggerRef.current = node;
99
+ if (typeof refProp === "function") {
100
+ refProp(node);
101
+ } else if (refProp) {
102
+ (refProp as RefObject<HTMLButtonElement | null>).current = node;
103
+ }
104
+ }}
96
105
  type="button"
97
106
  data-slot="drawer-trigger"
98
107
  className={cn(drawerTriggerVariants({ appearance }), className)}
@@ -121,7 +130,7 @@ export function DrawerContent({
121
130
  id,
122
131
  style,
123
132
  }: DrawerContentProps) {
124
- const { open, setOpen, titleId, descriptionId, contentRef } =
133
+ const { open, setOpen, titleId, descriptionId, contentRef, triggerRef } =
125
134
  useDrawerContext("DrawerContent");
126
135
  const resolvedSide = side ?? "right";
127
136
 
@@ -129,6 +138,7 @@ export function DrawerContent({
129
138
  open,
130
139
  setOpen,
131
140
  contentRef,
141
+ triggerRef,
132
142
  });
133
143
 
134
144
  const portalTarget = typeof document !== "undefined" ? document.body : null;
@@ -139,10 +149,8 @@ export function DrawerContent({
139
149
  return createPortal(
140
150
  open ? (
141
151
  <div className="fixed inset-0 z-50" data-slot="drawer-portal">
142
- <button
143
- type="button"
144
- aria-hidden
145
- tabIndex={-1}
152
+ <div
153
+ role="presentation"
146
154
  data-slot="drawer-overlay"
147
155
  className={drawerOverlayVariants()}
148
156
  onClick={() => setOpen(false)}
@@ -36,4 +36,5 @@ export type DrawerCtx = {
36
36
  titleId: string;
37
37
  descriptionId: string;
38
38
  contentRef: RefObject<HTMLDivElement | null>;
39
+ triggerRef: RefObject<HTMLElement | null>;
39
40
  };
@@ -104,9 +104,7 @@ describe("Dropdown", () => {
104
104
  </DropdownContent>
105
105
  </Dropdown>,
106
106
  );
107
- const item = screen
108
- .getByText("Alpha")
109
- .closest('div[tabindex="0"]') as HTMLElement;
107
+ const item = screen.getByRole("menuitem", { name: "Alpha" });
110
108
  item.focus();
111
109
  fireEvent.keyDown(item, { key: "Enter" });
112
110
  expect(handleSelect).toHaveBeenCalledTimes(1);
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { createContext, useContext, useState, useRef, useEffect } from "react";
3
+ import { createContext, useContext, useState, useRef, useEffect, useId } from "react";
4
4
  import { FiCheck } from "react-icons/fi";
5
5
  import { cn } from "../../lib/utils";
6
6
  import type {
@@ -34,6 +34,7 @@ export const Dropdown = ({
34
34
  onOpenChange,
35
35
  multiSelect = false,
36
36
  }: DropdownProps) => {
37
+ const menuId = `${useId()}-menu`;
37
38
  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
38
39
  const [selectedValues, setSelectedValues] = useState<string[]>([]);
39
40
 
@@ -70,6 +71,7 @@ export const Dropdown = ({
70
71
  selectedValues,
71
72
  toggleSelect,
72
73
  multiSelect,
74
+ menuId,
73
75
  }}
74
76
  >
75
77
  <div className="relative inline-block">{children}</div>
@@ -85,15 +87,25 @@ export const DropdownTrigger = ({
85
87
  className,
86
88
  variant,
87
89
  size,
90
+ onClick,
88
91
  ...props
89
92
  }: DropdownTriggerProps) => {
90
- const { toggle } = useDropdown();
93
+ const { toggle, open, menuId } = useDropdown();
91
94
 
92
95
  return (
93
96
  <button
94
- onClick={toggle}
97
+ type="button"
98
+ aria-expanded={open}
99
+ aria-haspopup="menu"
100
+ aria-controls={menuId}
95
101
  className={cn(triggerVariants({ variant, size }), className)}
96
102
  {...props}
103
+ onClick={(event) => {
104
+ onClick?.(event);
105
+ if (!event.defaultPrevented) {
106
+ toggle();
107
+ }
108
+ }}
97
109
  >
98
110
  {children}
99
111
  </button>
@@ -111,7 +123,7 @@ export const DropdownContent = ({
111
123
  divider,
112
124
  ...props
113
125
  }: DropdownContentProps) => {
114
- const { open, setOpen } = useDropdown();
126
+ const { open, setOpen, menuId } = useDropdown();
115
127
  const ref = useRef<HTMLDivElement>(null);
116
128
 
117
129
  // click outside
@@ -122,6 +134,8 @@ export const DropdownContent = ({
122
134
  return (
123
135
  <div
124
136
  ref={ref}
137
+ id={menuId}
138
+ role="menu"
125
139
  className={cn(
126
140
  contentVariants({ placement, spacing }),
127
141
  className,
@@ -157,10 +171,14 @@ export const DropdownItem = ({
157
171
 
158
172
  return (
159
173
  <div
174
+ role="menuitem"
160
175
  tabIndex={0}
161
176
  onClick={handleClick}
162
177
  onKeyDown={(e) => {
163
- if (e.key === "Enter") handleClick();
178
+ if (e.key === "Enter" || e.key === " ") {
179
+ e.preventDefault();
180
+ handleClick();
181
+ }
164
182
  }}
165
183
  className={cn(itemVariants({ variant }), className)}
166
184
  {...props}
@@ -7,6 +7,7 @@ export type DropdownContextType = {
7
7
  selectedValues: string[];
8
8
  toggleSelect: (value: string) => void;
9
9
  multiSelect: boolean;
10
+ menuId: string;
10
11
  };
11
12
 
12
13
  type Variant =
@@ -1,7 +1,7 @@
1
1
  import { cva } from "class-variance-authority";
2
2
 
3
3
  export const triggerVariants = cva(
4
- "inline-flex items-center justify-between rounded-md font-medium transition focus:outline-none cursor-pointer",
4
+ "inline-flex items-center justify-between rounded-md font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500/60 cursor-pointer",
5
5
  {
6
6
  variants: {
7
7
  variant: {
@@ -81,7 +81,7 @@ export const contentVariants = cva(
81
81
  );
82
82
 
83
83
  export const itemVariants = cva(
84
- "flex items-center justify-between px-3 py-2 text-sm cursor-pointer rounded-md transition-colors",
84
+ "flex items-center justify-between px-3 py-2 text-sm cursor-pointer rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500/60",
85
85
  {
86
86
  variants: {
87
87
  variant: {
@@ -25,17 +25,25 @@ export function EmptyStateBase(props: EmptyStateProps) {
25
25
  align,
26
26
  children,
27
27
  ref,
28
+ liveRegion = false,
28
29
  as: Wrapper = "section",
29
30
  ...rest
30
31
  } = props;
31
32
  const ctx = useMemo(() => size ?? "md", [size]);
32
33
 
34
+ const liveAria =
35
+ typeof liveRegion === "string"
36
+ ? liveRegion
37
+ : liveRegion === true
38
+ ? "polite"
39
+ : undefined;
40
+
33
41
  return (
34
42
  <EmptyStateSizeContext.Provider value={ctx}>
35
43
  <Wrapper
36
44
  ref={ref}
37
45
  data-slot="empty-state"
38
- aria-live="polite"
46
+ aria-live={liveAria}
39
47
  className={cn(
40
48
  emptyStateVariants({ size, appearance, align }),
41
49
  className,
@@ -8,6 +8,7 @@ type EmptyStateVariantProps = VariantProps<typeof emptyStateVariants>;
8
8
  export type EmptyStateProps = EmptyStateVariantProps &
9
9
  (Omit<ComponentPropsWithRef<"section">, "children"> & {
10
10
  children?: ReactNode;
11
+ liveRegion?: false | true | "polite" | "assertive";
11
12
  as?: ElementType;
12
13
  });
13
14
 
@@ -70,8 +70,6 @@ export function FileUpload({
70
70
  multiple={multiple}
71
71
  disabled={disabled}
72
72
  onChange={handleChange}
73
- aria-hidden
74
- tabIndex={-1}
75
73
  />
76
74
  <label
77
75
  htmlFor={inputId}
@@ -7,6 +7,18 @@ import { cn } from "../../lib/utils";
7
7
  import type { InputProps } from "./types";
8
8
  import { inputVariants } from "./variants";
9
9
 
10
+ function mergeDescribedByIds(
11
+ user: string | undefined,
12
+ ...generated: (string | undefined)[]
13
+ ): string | undefined {
14
+ const ids = [
15
+ ...(user ?? "").split(/\s+/).filter(Boolean),
16
+ ...generated.filter((id): id is string => Boolean(id)),
17
+ ];
18
+ const unique = [...new Set(ids)];
19
+ return unique.length > 0 ? unique.join(" ") : undefined;
20
+ }
21
+
10
22
  export const InputBase = (props: InputProps) => {
11
23
  const generatedId = useId();
12
24
 
@@ -19,13 +31,17 @@ export const InputBase = (props: InputProps) => {
19
31
  ref,
20
32
  "aria-invalid": ariaInvalidProp,
21
33
  errorMessage,
34
+ hint,
35
+ label,
22
36
  id,
23
37
  as,
38
+ "aria-describedby": ariaDescribedByUser,
24
39
  ...rest
25
40
  } = props;
26
41
 
27
42
  const controlId = id ?? generatedId;
28
43
  const errorId = `${controlId}-error`;
44
+ const hintId = `${controlId}-hint`;
29
45
  const ariaInvalid =
30
46
  ariaInvalidProp !== undefined
31
47
  ? ariaInvalidProp
@@ -33,8 +49,27 @@ export const InputBase = (props: InputProps) => {
33
49
  ? true
34
50
  : undefined;
35
51
 
52
+ const describedBy = mergeDescribedByIds(
53
+ ariaDescribedByUser,
54
+ hint !== undefined ? hintId : undefined,
55
+ errorMessage && appearance === "error" ? errorId : undefined,
56
+ );
57
+
36
58
  return (
37
59
  <>
60
+ {label !== undefined && (
61
+ <label
62
+ htmlFor={controlId}
63
+ className="mb-1 block text-sm font-medium text-slate-200"
64
+ >
65
+ {label}
66
+ </label>
67
+ )}
68
+ {hint !== undefined && (
69
+ <p id={hintId} className="mb-1 text-xs text-slate-400">
70
+ {hint}
71
+ </p>
72
+ )}
38
73
  <textarea
39
74
  ref={ref}
40
75
  id={controlId}
@@ -44,9 +79,7 @@ export const InputBase = (props: InputProps) => {
44
79
  className,
45
80
  )}
46
81
  aria-invalid={ariaInvalid}
47
- aria-describedby={
48
- errorMessage && appearance === "error" ? errorId : undefined
49
- }
82
+ aria-describedby={describedBy}
50
83
  {...rest}
51
84
  />
52
85
  {errorMessage && appearance === "error" && (
@@ -69,13 +102,17 @@ export const InputBase = (props: InputProps) => {
69
102
  ref,
70
103
  "aria-invalid": ariaInvalidProp,
71
104
  errorMessage,
105
+ hint,
106
+ label,
72
107
  id,
73
108
  as,
109
+ "aria-describedby": ariaDescribedByUser,
74
110
  ...rest
75
111
  } = props;
76
112
 
77
113
  const controlId = id ?? generatedId;
78
114
  const errorId = `${controlId}-error`;
115
+ const hintId = `${controlId}-hint`;
79
116
  const ariaInvalid =
80
117
  ariaInvalidProp !== undefined
81
118
  ? ariaInvalidProp
@@ -83,8 +120,27 @@ export const InputBase = (props: InputProps) => {
83
120
  ? true
84
121
  : undefined;
85
122
 
123
+ const describedBy = mergeDescribedByIds(
124
+ ariaDescribedByUser,
125
+ hint !== undefined ? hintId : undefined,
126
+ errorMessage && appearance === "error" ? errorId : undefined,
127
+ );
128
+
86
129
  return (
87
130
  <>
131
+ {label !== undefined && (
132
+ <label
133
+ htmlFor={controlId}
134
+ className="mb-1 block text-sm font-medium text-slate-200"
135
+ >
136
+ {label}
137
+ </label>
138
+ )}
139
+ {hint !== undefined && (
140
+ <p id={hintId} className="mb-1 text-xs text-slate-400">
141
+ {hint}
142
+ </p>
143
+ )}
88
144
  <input
89
145
  ref={ref}
90
146
  id={controlId}
@@ -94,9 +150,7 @@ export const InputBase = (props: InputProps) => {
94
150
  className,
95
151
  )}
96
152
  aria-invalid={ariaInvalid}
97
- aria-describedby={
98
- errorMessage && appearance === "error" ? errorId : undefined
99
- }
153
+ aria-describedby={describedBy}
100
154
  {...rest}
101
155
  />
102
156
  {errorMessage && appearance === "error" && (
@@ -1,10 +1,12 @@
1
1
  import type { VariantProps } from "class-variance-authority";
2
- import type { ComponentPropsWithRef } from "react";
2
+ import type { ComponentPropsWithRef, ReactNode } from "react";
3
3
 
4
4
  import type { inputVariants } from "./variants";
5
5
 
6
6
  export type InputSharedProps = Omit<VariantProps<typeof inputVariants>, "as"> & {
7
7
  errorMessage?: string;
8
+ hint?: ReactNode;
9
+ label?: ReactNode;
8
10
  };
9
11
 
10
12
  export type InputProps =
@@ -22,7 +22,7 @@ export function ModalContentAnimated({
22
22
  id,
23
23
  style,
24
24
  }: ModalContentAnimatedProps) {
25
- const { open, setOpen, titleId, descriptionId, contentRef } =
25
+ const { open, setOpen, titleId, descriptionId, contentRef, triggerRef } =
26
26
  useModalContext("ModalContent");
27
27
  const reduceMotion = useReducedMotion();
28
28
  const overlayMotion =
@@ -34,6 +34,7 @@ export function ModalContentAnimated({
34
34
  open,
35
35
  setOpen,
36
36
  contentRef,
37
+ triggerRef,
37
38
  });
38
39
 
39
40
  const portalTarget = typeof document !== "undefined" ? document.body : null;
@@ -46,10 +47,8 @@ export function ModalContentAnimated({
46
47
  <AnimatePresence>
47
48
  {open ? (
48
49
  <div className="fixed inset-0 z-50" data-slot="modal-portal">
49
- <motion.button
50
- type="button"
51
- aria-hidden
52
- tabIndex={-1}
50
+ <motion.div
51
+ role="presentation"
53
52
  data-slot="modal-overlay"
54
53
  className={modalOverlayVariants()}
55
54
  onClick={() => setOpen(false)}
@@ -8,6 +8,7 @@ import {
8
8
  useMemo,
9
9
  useRef,
10
10
  useState,
11
+ type RefObject,
11
12
  } from "react";
12
13
  import { createPortal } from "react-dom";
13
14
 
@@ -31,7 +32,8 @@ type ModalCtx = {
31
32
  setOpen: (next: boolean) => void;
32
33
  titleId: string;
33
34
  descriptionId: string;
34
- contentRef: React.RefObject<HTMLDivElement | null>;
35
+ contentRef: RefObject<HTMLDivElement | null>;
36
+ triggerRef: RefObject<HTMLElement | null>;
35
37
  };
36
38
 
37
39
  const ModalContext = createContext<ModalCtx | null>(null);
@@ -68,6 +70,7 @@ export function Modal({
68
70
  const titleId = `${baseId}-title`;
69
71
  const descriptionId = `${baseId}-description`;
70
72
  const contentRef = useRef<HTMLDivElement | null>(null);
73
+ const triggerRef = useRef<HTMLElement | null>(null);
71
74
 
72
75
  const ctx = useMemo(
73
76
  () => ({
@@ -76,6 +79,7 @@ export function Modal({
76
79
  titleId,
77
80
  descriptionId,
78
81
  contentRef,
82
+ triggerRef,
79
83
  }),
80
84
  [descriptionId, resolvedOpen, setOpen, titleId],
81
85
  );
@@ -90,13 +94,20 @@ export function ModalTrigger({
90
94
  children,
91
95
  appearance,
92
96
  onClick,
93
- ref,
97
+ ref: refProp,
94
98
  ...rest
95
99
  }: ModalTriggerProps) {
96
- const { setOpen } = useModalContext("ModalTrigger");
100
+ const { setOpen, triggerRef } = useModalContext("ModalTrigger");
97
101
  return (
98
102
  <button
99
- ref={ref}
103
+ ref={(node) => {
104
+ triggerRef.current = node;
105
+ if (typeof refProp === "function") {
106
+ refProp(node);
107
+ } else if (refProp) {
108
+ (refProp as RefObject<HTMLButtonElement | null>).current = node;
109
+ }
110
+ }}
100
111
  type="button"
101
112
  data-slot="modal-trigger"
102
113
  className={cn(modalTriggerVariants({ appearance }), className)}
@@ -126,13 +137,14 @@ export function ModalContent({
126
137
  id,
127
138
  style,
128
139
  }: ModalContentProps) {
129
- const { open, setOpen, titleId, descriptionId, contentRef } =
140
+ const { open, setOpen, titleId, descriptionId, contentRef, triggerRef } =
130
141
  useModalContext("ModalContent");
131
142
 
132
143
  useFocusManagement({
133
144
  open,
134
145
  setOpen,
135
146
  contentRef,
147
+ triggerRef,
136
148
  });
137
149
 
138
150
  const portalTarget = typeof document !== "undefined" ? document.body : null;
@@ -144,10 +156,8 @@ export function ModalContent({
144
156
  return createPortal(
145
157
  open ? (
146
158
  <div className="fixed inset-0 z-50" data-slot="modal-portal">
147
- <button
148
- type="button"
149
- aria-hidden
150
- tabIndex={-1}
159
+ <div
160
+ role="presentation"
151
161
  data-slot="modal-overlay"
152
162
  className={modalOverlayVariants()}
153
163
  onClick={() => setOpen(false)}
@@ -77,6 +77,21 @@ describe("Modal", () => {
77
77
  });
78
78
  });
79
79
 
80
+ it("should render overlay as non-focusable presentation surface", async () => {
81
+ render(
82
+ <Modal defaultOpen>
83
+ <ModalContent>
84
+ <ModalTitle>T</ModalTitle>
85
+ </ModalContent>
86
+ </Modal>,
87
+ );
88
+ await waitFor(() => expect(screen.getByRole("dialog")).toBeInTheDocument());
89
+ const overlay = document.querySelector('[data-slot="modal-overlay"]');
90
+ expect(overlay?.tagName.toLowerCase()).toBe("div");
91
+ expect(overlay).toHaveAttribute("role", "presentation");
92
+ expect((overlay as HTMLElement).tabIndex).toBe(-1);
93
+ });
94
+
80
95
  it("should close when Escape is pressed", async () => {
81
96
  const user = userEvent.setup();
82
97
  render(
@@ -126,4 +141,27 @@ describe("Modal", () => {
126
141
  await user.keyboard("{Escape}");
127
142
  await waitFor(() => expect(handleChange).toHaveBeenLastCalledWith(false));
128
143
  });
144
+
145
+ it("should restore focus to the trigger after the dialog closes", async () => {
146
+ const user = userEvent.setup();
147
+ render(
148
+ <Modal>
149
+ <ModalTrigger>Open</ModalTrigger>
150
+ <ModalContent>
151
+ <ModalTitle>T</ModalTitle>
152
+ </ModalContent>
153
+ </Modal>,
154
+ );
155
+ const trigger = screen.getByRole("button", { name: "Open" });
156
+ trigger.focus();
157
+ await user.click(trigger);
158
+ await waitFor(() =>
159
+ expect(screen.getByRole("dialog")).toBeInTheDocument(),
160
+ );
161
+ await user.keyboard("{Escape}");
162
+ await waitFor(() =>
163
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument(),
164
+ );
165
+ expect(trigger).toHaveFocus();
166
+ });
129
167
  });
@@ -192,9 +192,9 @@ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
192
192
  {item.type === "ellipsis" ? (
193
193
  <span
194
194
  data-slot="pagination-ellipsis"
195
- aria-hidden="true"
196
- title={ellipsisLabel}
197
195
  className={paginationEllipsisVariants({ size })}
196
+ role="img"
197
+ aria-label={ellipsisLabel}
198
198
  >
199
199
 
200
200
  </span>
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useMemo } from "react";
3
+ import { useCallback, useId, useMemo, useRef, useState } from "react";
4
4
  import { motion } from "framer-motion";
5
5
 
6
6
  import { cn, clamp } from "../../../lib/utils";
@@ -39,6 +39,32 @@ export function ProgressAnimated({
39
39
  }: ProgressAnimatedProps) {
40
40
  const clamped = clamp(value, min, max);
41
41
  const percent = max === min ? 0 : ((clamped - min) / (max - min)) * 100;
42
+ const labelSlotId = `${useId()}-progress-label`;
43
+ const labelSlotCountRef = useRef(0);
44
+ const [labelSlotMounted, setLabelSlotMounted] = useState(false);
45
+ const registerProgressLabel = useCallback(() => {
46
+ labelSlotCountRef.current += 1;
47
+ if (labelSlotCountRef.current === 1) {
48
+ setLabelSlotMounted(true);
49
+ }
50
+ return () => {
51
+ labelSlotCountRef.current -= 1;
52
+ if (labelSlotCountRef.current === 0) {
53
+ setLabelSlotMounted(false);
54
+ }
55
+ };
56
+ }, []);
57
+ const hasInlineLabelProp = Boolean(label?.trim().length);
58
+
59
+ const labelingProps = useMemo(() => {
60
+ if (hasInlineLabelProp) {
61
+ return { "aria-label": label?.trim() ?? "Progress" };
62
+ }
63
+ if (labelSlotMounted) {
64
+ return { "aria-labelledby": labelSlotId };
65
+ }
66
+ return { "aria-label": "Progress" };
67
+ }, [hasInlineLabelProp, label, labelSlotId, labelSlotMounted]);
42
68
 
43
69
  const ctx = useMemo<ProgressCtx>(
44
70
  () => ({
@@ -50,8 +76,21 @@ export function ProgressAnimated({
50
76
  striped: Boolean(striped),
51
77
  animated: Boolean(animated),
52
78
  appearance: appearance ?? "default",
79
+ labelSlotId,
80
+ registerProgressLabel,
53
81
  }),
54
- [animated, appearance, clamped, max, min, shape, size, striped],
82
+ [
83
+ animated,
84
+ appearance,
85
+ clamped,
86
+ labelSlotId,
87
+ max,
88
+ min,
89
+ registerProgressLabel,
90
+ shape,
91
+ size,
92
+ striped,
93
+ ],
55
94
  );
56
95
 
57
96
  const motionProps = progressAnimationPresets[animation];
@@ -65,8 +104,8 @@ export function ProgressAnimated({
65
104
  aria-valuemin={min}
66
105
  aria-valuemax={max}
67
106
  aria-valuenow={clamped}
68
- aria-label={label}
69
107
  aria-busy={busy ? true : undefined}
108
+ {...labelingProps}
70
109
  className={cn(
71
110
  progressVariants({ appearance, size, shape, striped, animated }),
72
111
  className,