@bug-on/md3-react 2.0.3 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/.turbo/turbo-build.log +42 -0
  2. package/CHANGELOG.md +69 -0
  3. package/dist/index.css +178 -0
  4. package/dist/index.css.d.ts +2 -0
  5. package/dist/index.d.mts +6135 -0
  6. package/dist/index.d.ts +6135 -71
  7. package/dist/index.js +1688 -631
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +1600 -564
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/material-symbols-cdn.css.d.ts +2 -0
  12. package/dist/material-symbols-self-hosted.css.d.ts +2 -0
  13. package/dist/plugin.d.mts +1 -0
  14. package/dist/plugin.d.ts +1 -0
  15. package/dist/plugin.js +13 -0
  16. package/dist/plugin.js.map +1 -0
  17. package/dist/plugin.mjs +3 -0
  18. package/dist/plugin.mjs.map +1 -0
  19. package/dist/typography.css.d.ts +2 -0
  20. package/package.json +28 -19
  21. package/scripts/copy-assets.js +115 -0
  22. package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
  23. package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  24. package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  25. package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  26. package/src/assets/loading-indicator.svg +19 -0
  27. package/src/assets/material-symbols-cdn.css +65 -0
  28. package/src/assets/material-symbols-self-hosted.css +90 -0
  29. package/src/css.d.ts +20 -0
  30. package/src/hooks/useClickOutside.ts +37 -0
  31. package/src/hooks/useMediaQuery.ts +28 -0
  32. package/src/hooks/useRipple.ts +88 -0
  33. package/src/index.css +23 -0
  34. package/src/index.ts +349 -0
  35. package/src/lib/material-symbols-preconnect.tsx +82 -0
  36. package/src/lib/theme-utils.ts +195 -0
  37. package/src/lib/utils.ts +6 -0
  38. package/src/plugin.ts +12 -0
  39. package/src/test/button.test.tsx +59 -0
  40. package/src/test/icon.test.tsx +91 -0
  41. package/src/test/loading-indicator.test.tsx +128 -0
  42. package/src/test/progress-indicator.test.tsx +306 -0
  43. package/src/test/setup.ts +80 -0
  44. package/src/test/typography.test.tsx +206 -0
  45. package/src/types/index.ts +7 -0
  46. package/src/types/md3.ts +31 -0
  47. package/src/ui/Text.tsx +60 -0
  48. package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
  49. package/src/ui/app-bar/app-bar-column.tsx +99 -0
  50. package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
  51. package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
  52. package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
  53. package/src/ui/app-bar/app-bar-row.tsx +104 -0
  54. package/src/ui/app-bar/app-bar.test.tsx +87 -0
  55. package/src/ui/app-bar/app-bar.tokens.ts +223 -0
  56. package/src/ui/app-bar/app-bar.types.ts +441 -0
  57. package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
  58. package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
  59. package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
  60. package/src/ui/app-bar/docked-toolbar.tsx +54 -0
  61. package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
  62. package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
  63. package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
  64. package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
  65. package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
  66. package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
  67. package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
  68. package/src/ui/app-bar/search-app-bar.tsx +176 -0
  69. package/src/ui/app-bar/search-view.tsx +227 -0
  70. package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
  71. package/src/ui/app-bar/small-app-bar.tsx +203 -0
  72. package/src/ui/badge.test.tsx +345 -0
  73. package/src/ui/badge.tsx +282 -0
  74. package/src/ui/button-group.test.tsx +71 -0
  75. package/src/ui/button-group.tsx +350 -0
  76. package/src/ui/button.test.tsx +306 -0
  77. package/src/ui/button.tsx +665 -0
  78. package/src/ui/card.test.tsx +187 -0
  79. package/src/ui/card.tsx +259 -0
  80. package/src/ui/checkbox.test.tsx +423 -0
  81. package/src/ui/checkbox.tsx +525 -0
  82. package/src/ui/chip.test.tsx +292 -0
  83. package/src/ui/chip.tsx +548 -0
  84. package/src/ui/code-block.tsx +219 -0
  85. package/src/ui/dialog.test.tsx +300 -0
  86. package/src/ui/dialog.tsx +384 -0
  87. package/src/ui/divider.test.tsx +314 -0
  88. package/src/ui/divider.tsx +412 -0
  89. package/src/ui/drawer.tsx +240 -0
  90. package/src/ui/fab-menu.test.tsx +494 -0
  91. package/src/ui/fab-menu.tsx +739 -0
  92. package/src/ui/fab.test.tsx +232 -0
  93. package/src/ui/fab.tsx +505 -0
  94. package/src/ui/icon-button.test.tsx +515 -0
  95. package/src/ui/icon-button.tsx +525 -0
  96. package/src/ui/icon.test.tsx +197 -0
  97. package/src/ui/icon.tsx +179 -0
  98. package/src/ui/loading-indicator.test.tsx +73 -0
  99. package/src/ui/loading-indicator.tsx +312 -0
  100. package/src/ui/menu/context-menu.tsx +275 -0
  101. package/src/ui/menu/index.ts +77 -0
  102. package/src/ui/menu/menu-animations.ts +102 -0
  103. package/src/ui/menu/menu-context.tsx +99 -0
  104. package/src/ui/menu/menu-divider.tsx +47 -0
  105. package/src/ui/menu/menu-group.tsx +200 -0
  106. package/src/ui/menu/menu-item.tsx +294 -0
  107. package/src/ui/menu/menu-tokens.ts +208 -0
  108. package/src/ui/menu/menu-types.ts +313 -0
  109. package/src/ui/menu/menu.test.tsx +624 -0
  110. package/src/ui/menu/menu.tsx +289 -0
  111. package/src/ui/menu/sub-menu.tsx +223 -0
  112. package/src/ui/menu/vertical-menu.tsx +382 -0
  113. package/src/ui/navigation-rail.test.tsx +404 -0
  114. package/src/ui/navigation-rail.tsx +607 -0
  115. package/src/ui/progress-indicator/circular.tsx +248 -0
  116. package/src/ui/progress-indicator/hooks.ts +51 -0
  117. package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
  118. package/src/ui/progress-indicator/linear-flat.tsx +83 -0
  119. package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
  120. package/src/ui/progress-indicator/linear.tsx +143 -0
  121. package/src/ui/progress-indicator/types.ts +158 -0
  122. package/src/ui/progress-indicator/utils.ts +73 -0
  123. package/src/ui/radio-button.test.tsx +407 -0
  124. package/src/ui/radio-button.tsx +551 -0
  125. package/src/ui/ripple.test.tsx +72 -0
  126. package/src/ui/ripple.tsx +234 -0
  127. package/src/ui/scroll-area.test.tsx +58 -0
  128. package/src/ui/scroll-area.tsx +139 -0
  129. package/src/ui/search/animated-placeholder.tsx +145 -0
  130. package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
  131. package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
  132. package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
  133. package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
  134. package/src/ui/search/index.ts +44 -0
  135. package/src/ui/search/search-bar.tsx +220 -0
  136. package/src/ui/search/search-context.tsx +42 -0
  137. package/src/ui/search/search-view-docked.tsx +194 -0
  138. package/src/ui/search/search-view-fullscreen.tsx +247 -0
  139. package/src/ui/search/search.test.tsx +233 -0
  140. package/src/ui/search/search.tokens.ts +134 -0
  141. package/src/ui/search/search.tsx +131 -0
  142. package/src/ui/search/search.types.ts +154 -0
  143. package/src/ui/search/trailing-action.tsx +49 -0
  144. package/src/ui/shared/constants.ts +135 -0
  145. package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
  146. package/src/ui/slider/hooks/useSliderMath.ts +195 -0
  147. package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
  148. package/src/ui/slider/range-slider.tsx +561 -0
  149. package/src/ui/slider/slider-thumb.tsx +379 -0
  150. package/src/ui/slider/slider-track.tsx +912 -0
  151. package/src/ui/slider/slider.tokens.ts +189 -0
  152. package/src/ui/slider/slider.tsx +259 -0
  153. package/src/ui/slider/slider.types.ts +288 -0
  154. package/src/ui/snackbar/index.ts +20 -0
  155. package/src/ui/snackbar/snackbar.test.tsx +338 -0
  156. package/src/ui/snackbar/snackbar.tsx +476 -0
  157. package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
  158. package/src/ui/switch/switch.stories.tsx +309 -0
  159. package/src/ui/switch/switch.test.tsx +243 -0
  160. package/src/ui/switch/switch.tokens.ts +89 -0
  161. package/src/ui/switch/switch.tsx +504 -0
  162. package/src/ui/switch/switch.types.ts +62 -0
  163. package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
  164. package/src/ui/tabs/tab.tsx +407 -0
  165. package/src/ui/tabs/tabs-content.tsx +89 -0
  166. package/src/ui/tabs/tabs-list.tsx +146 -0
  167. package/src/ui/tabs/tabs.test.tsx +290 -0
  168. package/src/ui/tabs/tabs.tokens.ts +121 -0
  169. package/src/ui/tabs/tabs.tsx +229 -0
  170. package/src/ui/tabs/tabs.types.ts +185 -0
  171. package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
  172. package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
  173. package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
  174. package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
  175. package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
  176. package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
  177. package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
  178. package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
  179. package/src/ui/text-field/text-field.test.tsx +454 -0
  180. package/src/ui/text-field/text-field.tokens.ts +104 -0
  181. package/src/ui/text-field/text-field.tsx +548 -0
  182. package/src/ui/text-field/text-field.types.ts +180 -0
  183. package/src/ui/theme-provider/index.tsx +215 -0
  184. package/src/ui/toc.test.tsx +108 -0
  185. package/src/ui/toc.tsx +172 -0
  186. package/src/ui/tooltip/plain-tooltip.tsx +63 -0
  187. package/src/ui/tooltip/rich-tooltip.tsx +94 -0
  188. package/src/ui/tooltip/tooltip-box.tsx +266 -0
  189. package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
  190. package/src/ui/tooltip/tooltip.tokens.ts +26 -0
  191. package/src/ui/tooltip/tooltip.types.ts +70 -0
  192. package/src/ui/tooltip/use-tooltip-position.ts +208 -0
  193. package/src/ui/tooltip/use-tooltip-state.ts +41 -0
  194. package/src/ui/typography/__tests__/typography.test.tsx +170 -0
  195. package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
  196. package/src/ui/typography/type-scale-tokens.ts +205 -0
  197. package/src/ui/typography/typography-key-tokens.ts +43 -0
  198. package/src/ui/typography/typography-tokens.ts +360 -0
  199. package/src/ui/typography/typography.css +22 -0
  200. package/src/ui/typography/typography.tsx +559 -0
  201. package/test-render.tsx +4 -0
  202. package/test-shadow.html +26 -0
  203. package/test_output.txt +164 -0
  204. package/test_output_v2.txt +5 -0
  205. package/tsconfig.build.json +10 -0
  206. package/tsconfig.json +18 -0
  207. package/tsup.config.ts +20 -0
  208. package/vitest.config.ts +11 -0
  209. package/dist/hooks/useClickOutside.d.ts +0 -8
  210. package/dist/hooks/useMediaQuery.d.ts +0 -11
  211. package/dist/hooks/useRipple.d.ts +0 -26
  212. package/dist/lib/material-symbols-preconnect.d.ts +0 -42
  213. package/dist/lib/theme-utils.d.ts +0 -63
  214. package/dist/lib/utils.d.ts +0 -2
  215. package/dist/types/index.d.ts +0 -1
  216. package/dist/types/md3.d.ts +0 -14
  217. package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
  218. package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
  219. package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
  220. package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
  221. package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
  222. package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
  223. package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
  224. package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
  225. package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
  226. package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
  227. package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
  228. package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
  229. package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
  230. package/dist/ui/app-bar/search-view.d.ts +0 -54
  231. package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
  232. package/dist/ui/badge.d.ts +0 -125
  233. package/dist/ui/button-group.d.ts +0 -59
  234. package/dist/ui/button.d.ts +0 -148
  235. package/dist/ui/card.d.ts +0 -62
  236. package/dist/ui/checkbox.d.ts +0 -82
  237. package/dist/ui/chip.d.ts +0 -110
  238. package/dist/ui/code-block.d.ts +0 -14
  239. package/dist/ui/dialog.d.ts +0 -111
  240. package/dist/ui/divider.d.ts +0 -164
  241. package/dist/ui/drawer.d.ts +0 -39
  242. package/dist/ui/dropdown.d.ts +0 -29
  243. package/dist/ui/fab-menu.d.ts +0 -204
  244. package/dist/ui/fab.d.ts +0 -162
  245. package/dist/ui/icon-button.d.ts +0 -131
  246. package/dist/ui/icon.d.ts +0 -88
  247. package/dist/ui/loading-indicator.d.ts +0 -42
  248. package/dist/ui/navigation-rail.d.ts +0 -29
  249. package/dist/ui/progress-indicator/circular.d.ts +0 -3
  250. package/dist/ui/progress-indicator/hooks.d.ts +0 -3
  251. package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
  252. package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
  253. package/dist/ui/progress-indicator/linear.d.ts +0 -3
  254. package/dist/ui/progress-indicator/types.d.ts +0 -151
  255. package/dist/ui/progress-indicator/utils.d.ts +0 -3
  256. package/dist/ui/radio-button.d.ts +0 -106
  257. package/dist/ui/ripple.d.ts +0 -126
  258. package/dist/ui/scroll-area.d.ts +0 -27
  259. package/dist/ui/search/animated-placeholder.d.ts +0 -54
  260. package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
  261. package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
  262. package/dist/ui/search/index.d.ts +0 -27
  263. package/dist/ui/search/search-bar.d.ts +0 -32
  264. package/dist/ui/search/search-context.d.ts +0 -24
  265. package/dist/ui/search/search-view-docked.d.ts +0 -25
  266. package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
  267. package/dist/ui/search/search.d.ts +0 -50
  268. package/dist/ui/search/search.tokens.d.ts +0 -112
  269. package/dist/ui/search/search.types.d.ts +0 -131
  270. package/dist/ui/search/trailing-action.d.ts +0 -9
  271. package/dist/ui/shared/constants.d.ts +0 -86
  272. package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
  273. package/dist/ui/slider/range-slider.d.ts +0 -47
  274. package/dist/ui/slider/slider-thumb.d.ts +0 -33
  275. package/dist/ui/slider/slider-track.d.ts +0 -25
  276. package/dist/ui/slider/slider.d.ts +0 -60
  277. package/dist/ui/slider/slider.tokens.d.ts +0 -151
  278. package/dist/ui/slider/slider.types.d.ts +0 -259
  279. package/dist/ui/snackbar/index.d.ts +0 -6
  280. package/dist/ui/snackbar/snackbar.d.ts +0 -197
  281. package/dist/ui/switch/switch.d.ts +0 -30
  282. package/dist/ui/switch/switch.stories.d.ts +0 -48
  283. package/dist/ui/switch/switch.tokens.d.ts +0 -67
  284. package/dist/ui/switch/switch.types.d.ts +0 -59
  285. package/dist/ui/tabs/tab.d.ts +0 -43
  286. package/dist/ui/tabs/tabs-content.d.ts +0 -36
  287. package/dist/ui/tabs/tabs-list.d.ts +0 -40
  288. package/dist/ui/tabs/tabs.d.ts +0 -60
  289. package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
  290. package/dist/ui/tabs/tabs.types.d.ts +0 -172
  291. package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
  292. package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
  293. package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
  294. package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
  295. package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
  296. package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
  297. package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
  298. package/dist/ui/text-field/text-field.d.ts +0 -49
  299. package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
  300. package/dist/ui/text-field/text-field.types.d.ts +0 -126
  301. package/dist/ui/theme-provider/index.d.ts +0 -48
  302. package/dist/ui/toc.d.ts +0 -80
  303. package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
  304. package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
  305. package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
  306. package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
  307. package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
  308. package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
  309. package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
  310. package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
  311. package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
  312. package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
  313. package/dist/ui/typography/typography-tokens.d.ts +0 -220
  314. package/dist/ui/typography/typography.d.ts +0 -265
  315. /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
  316. /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
@@ -0,0 +1,407 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import * as React from "react";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { RadioButton, RadioGroup } from "./radio-button";
5
+
6
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
7
+
8
+ /** Renders a basic RadioGroup with 3 options. */
9
+ function renderGroup(props: {
10
+ value?: string;
11
+ defaultValue?: string;
12
+ onValueChange?: (val: string) => void;
13
+ disabled?: boolean;
14
+ }) {
15
+ return render(
16
+ <RadioGroup name="test-group" {...props} label="Test Group">
17
+ <RadioButton value="a" label="Option A" />
18
+ <RadioButton value="b" label="Option B" />
19
+ <RadioButton value="c" label="Option C" />
20
+ </RadioGroup>,
21
+ );
22
+ }
23
+
24
+ // ─── Tests ────────────────────────────────────────────────────────────────────
25
+
26
+ describe("RadioButton Component", () => {
27
+ // ── 1. Rendering ──────────────────────────────────────────────────────────
28
+
29
+ describe("rendering", () => {
30
+ it("renders without label", () => {
31
+ render(<RadioButton value="x" aria-label="Option X" />);
32
+ const input = screen.getByRole("radio");
33
+ expect(input).toBeInTheDocument();
34
+ expect(input).toHaveAttribute("type", "radio");
35
+ });
36
+
37
+ it("renders with label", () => {
38
+ render(<RadioButton value="x" label="My Option" />);
39
+ expect(screen.getByRole("radio")).toBeInTheDocument();
40
+ expect(screen.getByText("My Option")).toBeInTheDocument();
41
+ });
42
+
43
+ it("is unselected by default", () => {
44
+ render(<RadioButton value="x" aria-label="Option X" />);
45
+ expect(screen.getByRole("radio")).not.toBeChecked();
46
+ });
47
+
48
+ it("renders as selected when selected prop is true", () => {
49
+ render(
50
+ <RadioButton
51
+ value="x"
52
+ selected
53
+ aria-label="Option X"
54
+ onClick={vi.fn()}
55
+ />,
56
+ );
57
+ expect(screen.getByRole("radio")).toBeChecked();
58
+ });
59
+
60
+ it("renders as unselected when selected prop is false", () => {
61
+ render(
62
+ <RadioButton
63
+ value="x"
64
+ selected={false}
65
+ aria-label="Option X"
66
+ onClick={vi.fn()}
67
+ />,
68
+ );
69
+ expect(screen.getByRole("radio")).not.toBeChecked();
70
+ });
71
+ });
72
+
73
+ // ── 2. Selection (Controlled) ─────────────────────────────────────────────
74
+
75
+ describe("controlled selection", () => {
76
+ it("fires onClick when clicked", () => {
77
+ const handleClick = vi.fn();
78
+ render(
79
+ <RadioButton value="x" aria-label="Option X" onClick={handleClick} />,
80
+ );
81
+ fireEvent.click(screen.getByRole("radio"));
82
+ expect(handleClick).toHaveBeenCalledTimes(1);
83
+ });
84
+
85
+ it("does not fire onClick when onClick is null", () => {
86
+ const handleClick = vi.fn();
87
+ render(<RadioButton value="x" aria-label="Option X" onClick={null} />);
88
+ fireEvent.click(screen.getByRole("radio"));
89
+ expect(handleClick).not.toHaveBeenCalled();
90
+ });
91
+
92
+ it("reflects controlled selected state", () => {
93
+ const { rerender } = render(
94
+ <RadioButton
95
+ value="x"
96
+ selected={false}
97
+ aria-label="Option X"
98
+ onClick={vi.fn()}
99
+ />,
100
+ );
101
+ expect(screen.getByRole("radio")).not.toBeChecked();
102
+
103
+ rerender(
104
+ <RadioButton
105
+ value="x"
106
+ selected={true}
107
+ aria-label="Option X"
108
+ onClick={vi.fn()}
109
+ />,
110
+ );
111
+ expect(screen.getByRole("radio")).toBeChecked();
112
+ });
113
+ });
114
+
115
+ // ── 3. Selection (Uncontrolled) ───────────────────────────────────────────
116
+
117
+ describe("uncontrolled selection", () => {
118
+ it("starts unselected with no defaultSelected", () => {
119
+ render(<RadioButton value="x" aria-label="Option X" />);
120
+ expect(screen.getByRole("radio")).not.toBeChecked();
121
+ });
122
+
123
+ it("starts selected when defaultSelected is true", () => {
124
+ render(<RadioButton value="x" defaultSelected aria-label="Option X" />);
125
+ expect(screen.getByRole("radio")).toBeChecked();
126
+ });
127
+
128
+ it("updates internal state on click", () => {
129
+ render(<RadioButton value="x" aria-label="Option X" />);
130
+ const input = screen.getByRole("radio");
131
+ expect(input).not.toBeChecked();
132
+
133
+ fireEvent.click(input);
134
+ expect(input).toBeChecked();
135
+ });
136
+ });
137
+
138
+ // ── 4. Disabled state ─────────────────────────────────────────────────────
139
+
140
+ describe("disabled state", () => {
141
+ it("has disabled attribute when disabled", () => {
142
+ render(<RadioButton value="x" disabled aria-label="Option X" />);
143
+ expect(screen.getByRole("radio")).toBeDisabled();
144
+ });
145
+
146
+ it("sets aria-disabled on the input", () => {
147
+ render(<RadioButton value="x" disabled aria-label="Option X" />);
148
+ expect(screen.getByRole("radio")).toHaveAttribute(
149
+ "aria-disabled",
150
+ "true",
151
+ );
152
+ });
153
+
154
+ it("does not fire onClick when disabled", () => {
155
+ const handleClick = vi.fn();
156
+ render(
157
+ <RadioButton
158
+ value="x"
159
+ disabled
160
+ aria-label="Option X"
161
+ onClick={handleClick}
162
+ />,
163
+ );
164
+ fireEvent.click(screen.getByRole("radio"));
165
+ expect(handleClick).not.toHaveBeenCalled();
166
+ });
167
+ });
168
+
169
+ // ── 5. RadioGroup behavior ────────────────────────────────────────────────
170
+
171
+ describe("RadioGroup", () => {
172
+ it("only one radio selected at a time (controlled)", () => {
173
+ const handleChange = vi.fn();
174
+ renderGroup({ value: "a", onValueChange: handleChange });
175
+
176
+ const radios = screen.getAllByRole("radio");
177
+ expect(radios[0]).toBeChecked();
178
+ expect(radios[1]).not.toBeChecked();
179
+ expect(radios[2]).not.toBeChecked();
180
+ });
181
+
182
+ it("fires onValueChange with correct value when selecting", () => {
183
+ const handleChange = vi.fn();
184
+ renderGroup({ value: "a", onValueChange: handleChange });
185
+
186
+ fireEvent.click(screen.getAllByRole("radio")[1]);
187
+ expect(handleChange).toHaveBeenCalledWith("b");
188
+ });
189
+
190
+ it("works in uncontrolled mode with defaultValue", () => {
191
+ renderGroup({ defaultValue: "b" });
192
+ const radios = screen.getAllByRole("radio");
193
+ expect(radios[1]).toBeChecked();
194
+ });
195
+
196
+ it("disables all radios when group is disabled", () => {
197
+ renderGroup({ disabled: true });
198
+ const radios = screen.getAllByRole("radio");
199
+ for (const radio of radios) {
200
+ expect(radio).toBeDisabled();
201
+ }
202
+ });
203
+
204
+ it("shares the same name across all radios", () => {
205
+ renderGroup({});
206
+ const radios = screen.getAllByRole("radio");
207
+ for (const radio of radios) {
208
+ expect(radio).toHaveAttribute("name", "test-group");
209
+ }
210
+ });
211
+
212
+ it("updates selection in uncontrolled mode on click", () => {
213
+ renderGroup({ defaultValue: "a" });
214
+ const radios = screen.getAllByRole("radio");
215
+
216
+ expect(radios[0]).toBeChecked();
217
+ fireEvent.click(radios[2]);
218
+ expect(radios[2]).toBeChecked();
219
+ });
220
+ });
221
+
222
+ // ── 6. Keyboard navigation ────────────────────────────────────────────────
223
+
224
+ describe("keyboard navigation", () => {
225
+ it("ArrowDown moves to next radio and selects it", () => {
226
+ const handleChange = vi.fn();
227
+ const { container } = renderGroup({
228
+ value: "a",
229
+ onValueChange: handleChange,
230
+ });
231
+
232
+ const group = container.querySelector("[role='radiogroup']");
233
+ if (!group) throw new Error("radiogroup not found");
234
+ const radios = screen.getAllByRole("radio");
235
+ radios[0].focus();
236
+
237
+ fireEvent.keyDown(group, { key: "ArrowDown" });
238
+ expect(handleChange).toHaveBeenCalledWith("b");
239
+ });
240
+
241
+ it("ArrowRight moves to next radio", () => {
242
+ const handleChange = vi.fn();
243
+ const { container } = renderGroup({
244
+ value: "a",
245
+ onValueChange: handleChange,
246
+ });
247
+
248
+ const group = container.querySelector("[role='radiogroup']");
249
+ if (!group) throw new Error("radiogroup not found");
250
+ const radios = screen.getAllByRole("radio");
251
+ radios[0].focus();
252
+
253
+ fireEvent.keyDown(group, { key: "ArrowRight" });
254
+ expect(handleChange).toHaveBeenCalledWith("b");
255
+ });
256
+
257
+ it("ArrowUp moves to previous radio", () => {
258
+ const handleChange = vi.fn();
259
+ const { container } = renderGroup({
260
+ value: "b",
261
+ onValueChange: handleChange,
262
+ });
263
+
264
+ const group = container.querySelector("[role='radiogroup']");
265
+ if (!group) throw new Error("radiogroup not found");
266
+ const radios = screen.getAllByRole("radio");
267
+ radios[1].focus();
268
+
269
+ fireEvent.keyDown(group, { key: "ArrowUp" });
270
+ expect(handleChange).toHaveBeenCalledWith("a");
271
+ });
272
+
273
+ it("ArrowLeft moves to previous radio", () => {
274
+ const handleChange = vi.fn();
275
+ const { container } = renderGroup({
276
+ value: "b",
277
+ onValueChange: handleChange,
278
+ });
279
+
280
+ const group = container.querySelector("[role='radiogroup']");
281
+ if (!group) throw new Error("radiogroup not found");
282
+ const radios = screen.getAllByRole("radio");
283
+ radios[1].focus();
284
+
285
+ fireEvent.keyDown(group, { key: "ArrowLeft" });
286
+ expect(handleChange).toHaveBeenCalledWith("a");
287
+ });
288
+
289
+ it("wraps from last to first with ArrowDown", () => {
290
+ const handleChange = vi.fn();
291
+ const { container } = renderGroup({
292
+ value: "c",
293
+ onValueChange: handleChange,
294
+ });
295
+
296
+ const group = container.querySelector("[role='radiogroup']");
297
+ if (!group) throw new Error("radiogroup not found");
298
+ const radios = screen.getAllByRole("radio");
299
+ radios[2].focus();
300
+
301
+ fireEvent.keyDown(group, { key: "ArrowDown" });
302
+ expect(handleChange).toHaveBeenCalledWith("a");
303
+ });
304
+
305
+ it("wraps from first to last with ArrowUp", () => {
306
+ const handleChange = vi.fn();
307
+ const { container } = renderGroup({
308
+ value: "a",
309
+ onValueChange: handleChange,
310
+ });
311
+
312
+ const group = container.querySelector("[role='radiogroup']");
313
+ if (!group) throw new Error("radiogroup not found");
314
+ const radios = screen.getAllByRole("radio");
315
+ radios[0].focus();
316
+
317
+ fireEvent.keyDown(group, { key: "ArrowUp" });
318
+ expect(handleChange).toHaveBeenCalledWith("c");
319
+ });
320
+
321
+ it("does not navigate when group is disabled", () => {
322
+ const handleChange = vi.fn();
323
+ const { container } = renderGroup({
324
+ value: "a",
325
+ onValueChange: handleChange,
326
+ disabled: true,
327
+ });
328
+
329
+ const group = container.querySelector("[role='radiogroup']");
330
+ if (!group) throw new Error("radiogroup not found");
331
+ fireEvent.keyDown(group, { key: "ArrowDown" });
332
+ expect(handleChange).not.toHaveBeenCalled();
333
+ });
334
+ });
335
+
336
+ // ── 7. Accessibility ──────────────────────────────────────────────────────
337
+
338
+ describe("accessibility", () => {
339
+ it("input has role radio", () => {
340
+ render(<RadioButton value="x" aria-label="Option X" />);
341
+ expect(screen.getByRole("radio")).toBeInTheDocument();
342
+ });
343
+
344
+ it("has aria-label when provided", () => {
345
+ render(<RadioButton value="x" aria-label="My Radio Label" />);
346
+ expect(screen.getByRole("radio")).toHaveAttribute(
347
+ "aria-label",
348
+ "My Radio Label",
349
+ );
350
+ });
351
+
352
+ it("RadioGroup has role radiogroup", () => {
353
+ renderGroup({});
354
+ expect(screen.getByRole("radiogroup")).toBeInTheDocument();
355
+ });
356
+
357
+ it("RadioGroup sets aria-disabled when disabled", () => {
358
+ renderGroup({ disabled: true });
359
+ expect(screen.getByRole("radiogroup")).toHaveAttribute(
360
+ "aria-disabled",
361
+ "true",
362
+ );
363
+ });
364
+
365
+ it("RadioGroup label is accessible via aria-label", () => {
366
+ renderGroup({});
367
+ const group = screen.getByRole("radiogroup");
368
+ expect(group).toHaveAttribute("aria-label", "Test Group");
369
+ });
370
+
371
+ it("RadioButton label associates with input via htmlFor", () => {
372
+ render(<RadioButton value="x" label="Labeled Option" />);
373
+ const input = screen.getByRole("radio");
374
+ const label = screen.getByText("Labeled Option").closest("label");
375
+ expect(label).toHaveAttribute("for", input.id);
376
+ });
377
+
378
+ it("input is visually hidden (sr-only)", () => {
379
+ render(<RadioButton value="x" aria-label="Option X" />);
380
+ const input = screen.getByRole("radio");
381
+ expect(input.className).toContain("sr-only");
382
+ });
383
+ });
384
+
385
+ // ── 8. Ref forwarding ─────────────────────────────────────────────────────
386
+
387
+ describe("ref forwarding", () => {
388
+ it("ref points to the hidden input element", () => {
389
+ const ref = React.createRef<HTMLInputElement>();
390
+ render(<RadioButton value="x" aria-label="Option X" ref={ref} />);
391
+ expect(ref.current).toBeTruthy();
392
+ if (!ref.current) throw new Error("ref.current is null");
393
+ expect(ref.current.tagName.toLowerCase()).toBe("input");
394
+ expect(ref.current.type).toBe("radio");
395
+ });
396
+ });
397
+
398
+ // ── 9. Reduced motion ─────────────────────────────────────────────────────
399
+
400
+ describe("reduced motion", () => {
401
+ it("renders correctly when reduced motion is preferred", () => {
402
+ // RadioVisual internal renders; just check component doesn't crash
403
+ render(<RadioButton value="x" aria-label="Option X" selected />);
404
+ expect(screen.getByRole("radio")).toBeChecked();
405
+ });
406
+ });
407
+ });