@bug-on/md3-react 2.0.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/.turbo/turbo-build.log +33 -0
  2. package/CHANGELOG.md +55 -0
  3. package/dist/index.css.d.ts +2 -0
  4. package/dist/index.d.mts +6127 -0
  5. package/dist/index.d.ts +6127 -71
  6. package/dist/index.js +1653 -614
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1566 -547
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/material-symbols-cdn.css.d.ts +2 -0
  11. package/dist/material-symbols-self-hosted.css.d.ts +2 -0
  12. package/dist/typography.css.d.ts +2 -0
  13. package/package.json +22 -19
  14. package/scripts/copy-assets.js +82 -0
  15. package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
  16. package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  17. package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  18. package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  19. package/src/assets/loading-indicator.svg +19 -0
  20. package/src/assets/material-symbols-cdn.css +65 -0
  21. package/src/assets/material-symbols-self-hosted.css +90 -0
  22. package/src/css.d.ts +20 -0
  23. package/src/hooks/useClickOutside.ts +37 -0
  24. package/src/hooks/useMediaQuery.ts +28 -0
  25. package/src/hooks/useRipple.ts +88 -0
  26. package/src/index.css +23 -0
  27. package/src/index.ts +349 -0
  28. package/src/lib/material-symbols-preconnect.tsx +82 -0
  29. package/src/lib/theme-utils.ts +180 -0
  30. package/src/lib/utils.ts +6 -0
  31. package/src/test/button.test.tsx +59 -0
  32. package/src/test/icon.test.tsx +91 -0
  33. package/src/test/loading-indicator.test.tsx +128 -0
  34. package/src/test/progress-indicator.test.tsx +306 -0
  35. package/src/test/setup.ts +80 -0
  36. package/src/test/typography.test.tsx +206 -0
  37. package/src/types/index.ts +7 -0
  38. package/src/types/md3.ts +31 -0
  39. package/src/ui/Text.tsx +60 -0
  40. package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
  41. package/src/ui/app-bar/app-bar-column.tsx +99 -0
  42. package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
  43. package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
  44. package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
  45. package/src/ui/app-bar/app-bar-row.tsx +104 -0
  46. package/src/ui/app-bar/app-bar.test.tsx +87 -0
  47. package/src/ui/app-bar/app-bar.tokens.ts +223 -0
  48. package/src/ui/app-bar/app-bar.types.ts +441 -0
  49. package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
  50. package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
  51. package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
  52. package/src/ui/app-bar/docked-toolbar.tsx +54 -0
  53. package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
  54. package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
  55. package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
  56. package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
  57. package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
  58. package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
  59. package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
  60. package/src/ui/app-bar/search-app-bar.tsx +176 -0
  61. package/src/ui/app-bar/search-view.tsx +227 -0
  62. package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
  63. package/src/ui/app-bar/small-app-bar.tsx +203 -0
  64. package/src/ui/badge.test.tsx +345 -0
  65. package/src/ui/badge.tsx +282 -0
  66. package/src/ui/button-group.test.tsx +71 -0
  67. package/src/ui/button-group.tsx +350 -0
  68. package/src/ui/button.test.tsx +297 -0
  69. package/src/ui/button.tsx +669 -0
  70. package/src/ui/card.test.tsx +187 -0
  71. package/src/ui/card.tsx +259 -0
  72. package/src/ui/checkbox.test.tsx +423 -0
  73. package/src/ui/checkbox.tsx +525 -0
  74. package/src/ui/chip.test.tsx +292 -0
  75. package/src/ui/chip.tsx +548 -0
  76. package/src/ui/code-block.tsx +219 -0
  77. package/src/ui/dialog.test.tsx +300 -0
  78. package/src/ui/dialog.tsx +384 -0
  79. package/src/ui/divider.test.tsx +314 -0
  80. package/src/ui/divider.tsx +412 -0
  81. package/src/ui/drawer.tsx +240 -0
  82. package/src/ui/fab-menu.test.tsx +494 -0
  83. package/src/ui/fab-menu.tsx +739 -0
  84. package/src/ui/fab.test.tsx +232 -0
  85. package/src/ui/fab.tsx +505 -0
  86. package/src/ui/icon-button.test.tsx +515 -0
  87. package/src/ui/icon-button.tsx +525 -0
  88. package/src/ui/icon.test.tsx +197 -0
  89. package/src/ui/icon.tsx +179 -0
  90. package/src/ui/loading-indicator.test.tsx +73 -0
  91. package/src/ui/loading-indicator.tsx +312 -0
  92. package/src/ui/menu/context-menu.tsx +275 -0
  93. package/src/ui/menu/index.ts +77 -0
  94. package/src/ui/menu/menu-animations.ts +102 -0
  95. package/src/ui/menu/menu-context.tsx +99 -0
  96. package/src/ui/menu/menu-divider.tsx +47 -0
  97. package/src/ui/menu/menu-group.tsx +200 -0
  98. package/src/ui/menu/menu-item.tsx +294 -0
  99. package/src/ui/menu/menu-tokens.ts +208 -0
  100. package/src/ui/menu/menu-types.ts +313 -0
  101. package/src/ui/menu/menu.test.tsx +624 -0
  102. package/src/ui/menu/menu.tsx +289 -0
  103. package/src/ui/menu/sub-menu.tsx +223 -0
  104. package/src/ui/menu/vertical-menu.tsx +382 -0
  105. package/src/ui/navigation-rail.test.tsx +404 -0
  106. package/src/ui/navigation-rail.tsx +604 -0
  107. package/src/ui/progress-indicator/circular.tsx +248 -0
  108. package/src/ui/progress-indicator/hooks.ts +51 -0
  109. package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
  110. package/src/ui/progress-indicator/linear-flat.tsx +83 -0
  111. package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
  112. package/src/ui/progress-indicator/linear.tsx +143 -0
  113. package/src/ui/progress-indicator/types.ts +158 -0
  114. package/src/ui/progress-indicator/utils.ts +73 -0
  115. package/src/ui/radio-button.test.tsx +407 -0
  116. package/src/ui/radio-button.tsx +551 -0
  117. package/src/ui/ripple.test.tsx +72 -0
  118. package/src/ui/ripple.tsx +234 -0
  119. package/src/ui/scroll-area.test.tsx +58 -0
  120. package/src/ui/scroll-area.tsx +139 -0
  121. package/src/ui/search/animated-placeholder.tsx +145 -0
  122. package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
  123. package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
  124. package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
  125. package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
  126. package/src/ui/search/index.ts +44 -0
  127. package/src/ui/search/search-bar.tsx +220 -0
  128. package/src/ui/search/search-context.tsx +42 -0
  129. package/src/ui/search/search-view-docked.tsx +194 -0
  130. package/src/ui/search/search-view-fullscreen.tsx +247 -0
  131. package/src/ui/search/search.test.tsx +233 -0
  132. package/src/ui/search/search.tokens.ts +134 -0
  133. package/src/ui/search/search.tsx +131 -0
  134. package/src/ui/search/search.types.ts +154 -0
  135. package/src/ui/search/trailing-action.tsx +49 -0
  136. package/src/ui/shared/constants.ts +122 -0
  137. package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
  138. package/src/ui/slider/hooks/useSliderMath.ts +195 -0
  139. package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
  140. package/src/ui/slider/range-slider.tsx +561 -0
  141. package/src/ui/slider/slider-thumb.tsx +379 -0
  142. package/src/ui/slider/slider-track.tsx +912 -0
  143. package/src/ui/slider/slider.tokens.ts +189 -0
  144. package/src/ui/slider/slider.tsx +259 -0
  145. package/src/ui/slider/slider.types.ts +288 -0
  146. package/src/ui/snackbar/index.ts +20 -0
  147. package/src/ui/snackbar/snackbar.test.tsx +338 -0
  148. package/src/ui/snackbar/snackbar.tsx +476 -0
  149. package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
  150. package/src/ui/switch/switch.stories.tsx +309 -0
  151. package/src/ui/switch/switch.test.tsx +243 -0
  152. package/src/ui/switch/switch.tokens.ts +89 -0
  153. package/src/ui/switch/switch.tsx +504 -0
  154. package/src/ui/switch/switch.types.ts +62 -0
  155. package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
  156. package/src/ui/tabs/tab.tsx +407 -0
  157. package/src/ui/tabs/tabs-content.tsx +89 -0
  158. package/src/ui/tabs/tabs-list.tsx +146 -0
  159. package/src/ui/tabs/tabs.test.tsx +290 -0
  160. package/src/ui/tabs/tabs.tokens.ts +121 -0
  161. package/src/ui/tabs/tabs.tsx +229 -0
  162. package/src/ui/tabs/tabs.types.ts +185 -0
  163. package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
  164. package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
  165. package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
  166. package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
  167. package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
  168. package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
  169. package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
  170. package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
  171. package/src/ui/text-field/text-field.test.tsx +454 -0
  172. package/src/ui/text-field/text-field.tokens.ts +104 -0
  173. package/src/ui/text-field/text-field.tsx +548 -0
  174. package/src/ui/text-field/text-field.types.ts +180 -0
  175. package/src/ui/theme-provider/index.tsx +190 -0
  176. package/src/ui/toc.test.tsx +108 -0
  177. package/src/ui/toc.tsx +172 -0
  178. package/src/ui/tooltip/plain-tooltip.tsx +63 -0
  179. package/src/ui/tooltip/rich-tooltip.tsx +94 -0
  180. package/src/ui/tooltip/tooltip-box.tsx +266 -0
  181. package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
  182. package/src/ui/tooltip/tooltip.tokens.ts +26 -0
  183. package/src/ui/tooltip/tooltip.types.ts +70 -0
  184. package/src/ui/tooltip/use-tooltip-position.ts +208 -0
  185. package/src/ui/tooltip/use-tooltip-state.ts +41 -0
  186. package/src/ui/typography/__tests__/typography.test.tsx +170 -0
  187. package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
  188. package/src/ui/typography/type-scale-tokens.ts +205 -0
  189. package/src/ui/typography/typography-key-tokens.ts +43 -0
  190. package/src/ui/typography/typography-tokens.ts +360 -0
  191. package/src/ui/typography/typography.css +22 -0
  192. package/src/ui/typography/typography.tsx +559 -0
  193. package/test-render.tsx +4 -0
  194. package/test-shadow.html +26 -0
  195. package/test_output.txt +164 -0
  196. package/test_output_v2.txt +5 -0
  197. package/tsconfig.build.json +10 -0
  198. package/tsconfig.json +18 -0
  199. package/tsup.config.ts +20 -0
  200. package/vitest.config.ts +11 -0
  201. package/dist/hooks/useClickOutside.d.ts +0 -8
  202. package/dist/hooks/useMediaQuery.d.ts +0 -11
  203. package/dist/hooks/useRipple.d.ts +0 -26
  204. package/dist/lib/material-symbols-preconnect.d.ts +0 -42
  205. package/dist/lib/theme-utils.d.ts +0 -63
  206. package/dist/lib/utils.d.ts +0 -2
  207. package/dist/types/index.d.ts +0 -1
  208. package/dist/types/md3.d.ts +0 -14
  209. package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
  210. package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
  211. package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
  212. package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
  213. package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
  214. package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
  215. package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
  216. package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
  217. package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
  218. package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
  219. package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
  220. package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
  221. package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
  222. package/dist/ui/app-bar/search-view.d.ts +0 -54
  223. package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
  224. package/dist/ui/badge.d.ts +0 -125
  225. package/dist/ui/button-group.d.ts +0 -59
  226. package/dist/ui/button.d.ts +0 -148
  227. package/dist/ui/card.d.ts +0 -62
  228. package/dist/ui/checkbox.d.ts +0 -82
  229. package/dist/ui/chip.d.ts +0 -110
  230. package/dist/ui/code-block.d.ts +0 -14
  231. package/dist/ui/dialog.d.ts +0 -111
  232. package/dist/ui/divider.d.ts +0 -164
  233. package/dist/ui/drawer.d.ts +0 -39
  234. package/dist/ui/dropdown.d.ts +0 -29
  235. package/dist/ui/fab-menu.d.ts +0 -204
  236. package/dist/ui/fab.d.ts +0 -162
  237. package/dist/ui/icon-button.d.ts +0 -131
  238. package/dist/ui/icon.d.ts +0 -88
  239. package/dist/ui/loading-indicator.d.ts +0 -42
  240. package/dist/ui/navigation-rail.d.ts +0 -29
  241. package/dist/ui/progress-indicator/circular.d.ts +0 -3
  242. package/dist/ui/progress-indicator/hooks.d.ts +0 -3
  243. package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
  244. package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
  245. package/dist/ui/progress-indicator/linear.d.ts +0 -3
  246. package/dist/ui/progress-indicator/types.d.ts +0 -151
  247. package/dist/ui/progress-indicator/utils.d.ts +0 -3
  248. package/dist/ui/radio-button.d.ts +0 -106
  249. package/dist/ui/ripple.d.ts +0 -126
  250. package/dist/ui/scroll-area.d.ts +0 -27
  251. package/dist/ui/search/animated-placeholder.d.ts +0 -54
  252. package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
  253. package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
  254. package/dist/ui/search/index.d.ts +0 -27
  255. package/dist/ui/search/search-bar.d.ts +0 -32
  256. package/dist/ui/search/search-context.d.ts +0 -24
  257. package/dist/ui/search/search-view-docked.d.ts +0 -25
  258. package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
  259. package/dist/ui/search/search.d.ts +0 -50
  260. package/dist/ui/search/search.tokens.d.ts +0 -112
  261. package/dist/ui/search/search.types.d.ts +0 -131
  262. package/dist/ui/search/trailing-action.d.ts +0 -9
  263. package/dist/ui/shared/constants.d.ts +0 -86
  264. package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
  265. package/dist/ui/slider/range-slider.d.ts +0 -47
  266. package/dist/ui/slider/slider-thumb.d.ts +0 -33
  267. package/dist/ui/slider/slider-track.d.ts +0 -25
  268. package/dist/ui/slider/slider.d.ts +0 -60
  269. package/dist/ui/slider/slider.tokens.d.ts +0 -151
  270. package/dist/ui/slider/slider.types.d.ts +0 -259
  271. package/dist/ui/snackbar/index.d.ts +0 -6
  272. package/dist/ui/snackbar/snackbar.d.ts +0 -197
  273. package/dist/ui/switch/switch.d.ts +0 -30
  274. package/dist/ui/switch/switch.stories.d.ts +0 -48
  275. package/dist/ui/switch/switch.tokens.d.ts +0 -67
  276. package/dist/ui/switch/switch.types.d.ts +0 -59
  277. package/dist/ui/tabs/tab.d.ts +0 -43
  278. package/dist/ui/tabs/tabs-content.d.ts +0 -36
  279. package/dist/ui/tabs/tabs-list.d.ts +0 -40
  280. package/dist/ui/tabs/tabs.d.ts +0 -60
  281. package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
  282. package/dist/ui/tabs/tabs.types.d.ts +0 -172
  283. package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
  284. package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
  285. package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
  286. package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
  287. package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
  288. package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
  289. package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
  290. package/dist/ui/text-field/text-field.d.ts +0 -49
  291. package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
  292. package/dist/ui/text-field/text-field.types.d.ts +0 -126
  293. package/dist/ui/theme-provider/index.d.ts +0 -48
  294. package/dist/ui/toc.d.ts +0 -80
  295. package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
  296. package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
  297. package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
  298. package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
  299. package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
  300. package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
  301. package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
  302. package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
  303. package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
  304. package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
  305. package/dist/ui/typography/typography-tokens.d.ts +0 -220
  306. package/dist/ui/typography/typography.d.ts +0 -265
  307. /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
  308. /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
@@ -0,0 +1,423 @@
1
+ /**
2
+ * @file checkbox.test.tsx
3
+ *
4
+ * Comprehensive test suite for the MD3 Expressive Checkbox component.
5
+ * Tests cover: rendering, controlled state, tri-state, error, disabled,
6
+ * accessibility, form integration, and keyboard interaction.
7
+ */
8
+
9
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
10
+ import { afterEach, describe, expect, it, vi } from "vitest";
11
+ import { Checkbox, TriStateCheckbox } from "./checkbox";
12
+
13
+ afterEach(cleanup);
14
+
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+ // Rendering
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+
19
+ describe("Checkbox — Rendering", () => {
20
+ it("renders unchecked by default", () => {
21
+ render(<Checkbox aria-label="Test checkbox" />);
22
+ const input = screen.getByRole("checkbox", { name: "Test checkbox" });
23
+ expect(input).not.toBeChecked();
24
+ expect(input).toHaveAttribute("aria-checked", "false");
25
+ });
26
+
27
+ it("renders checked when checked=true", () => {
28
+ render(<Checkbox checked aria-label="Test" onCheckedChange={() => {}} />);
29
+ const input = screen.getByRole("checkbox", { name: "Test" });
30
+ expect(input).toBeChecked();
31
+ expect(input).toHaveAttribute("aria-checked", "true");
32
+ });
33
+
34
+ it("renders indeterminate when indeterminate=true", () => {
35
+ render(<Checkbox indeterminate aria-label="Test" />);
36
+ const input = screen.getByRole("checkbox", { name: "Test" });
37
+ expect(input).toHaveAttribute("aria-checked", "mixed");
38
+ });
39
+
40
+ it("renders with label text", () => {
41
+ render(<Checkbox label="Accept terms" />);
42
+ expect(screen.getByText("Accept terms")).toBeInTheDocument();
43
+ // Label should be associated with the checkbox
44
+ const input = screen.getByRole("checkbox");
45
+ expect(input).toBeInTheDocument();
46
+ });
47
+
48
+ it("renders without label when label prop is omitted", () => {
49
+ render(<Checkbox aria-label="Standalone" />);
50
+ expect(screen.queryByText("Accept terms")).not.toBeInTheDocument();
51
+ expect(
52
+ screen.getByRole("checkbox", { name: "Standalone" }),
53
+ ).toBeInTheDocument();
54
+ });
55
+
56
+ it("applies custom className to the wrapper", () => {
57
+ render(<Checkbox aria-label="Test" className="custom-class" />);
58
+ // The wrapper div should have the custom class
59
+ const wrapper = screen
60
+ .getByRole("checkbox", { name: "Test" })
61
+ .closest(".w-12");
62
+ expect(wrapper).toHaveClass("custom-class");
63
+ });
64
+ });
65
+
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+ // Controlled State
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+
70
+ describe("Checkbox — Controlled State", () => {
71
+ it("calls onCheckedChange with true when unchecked checkbox is clicked", () => {
72
+ const onCheckedChange = vi.fn();
73
+ render(
74
+ <Checkbox
75
+ checked={false}
76
+ onCheckedChange={onCheckedChange}
77
+ aria-label="Test"
78
+ />,
79
+ );
80
+ const input = screen.getByRole("checkbox");
81
+ fireEvent.click(input);
82
+ expect(onCheckedChange).toHaveBeenCalledOnce();
83
+ expect(onCheckedChange).toHaveBeenCalledWith(true);
84
+ });
85
+
86
+ it("calls onCheckedChange with false when checked checkbox is clicked", () => {
87
+ const onCheckedChange = vi.fn();
88
+ render(
89
+ <Checkbox checked onCheckedChange={onCheckedChange} aria-label="Test" />,
90
+ );
91
+ const input = screen.getByRole("checkbox");
92
+ fireEvent.click(input);
93
+ expect(onCheckedChange).toHaveBeenCalledOnce();
94
+ expect(onCheckedChange).toHaveBeenCalledWith(false);
95
+ });
96
+
97
+ it("does not call onCheckedChange when disabled", () => {
98
+ const onCheckedChange = vi.fn();
99
+ render(
100
+ <Checkbox
101
+ checked={false}
102
+ onCheckedChange={onCheckedChange}
103
+ disabled
104
+ aria-label="Test"
105
+ />,
106
+ );
107
+ const input = screen.getByRole("checkbox");
108
+ fireEvent.click(input);
109
+ expect(onCheckedChange).not.toHaveBeenCalled();
110
+ });
111
+ });
112
+
113
+ // ─────────────────────────────────────────────────────────────────────────────
114
+ // Tri-State Mode
115
+ // ─────────────────────────────────────────────────────────────────────────────
116
+
117
+ describe("Checkbox — Tri-State", () => {
118
+ it("renders unchecked when state='unchecked'", () => {
119
+ render(
120
+ <Checkbox
121
+ state="unchecked"
122
+ onStateChange={() => {}}
123
+ aria-label="Parent"
124
+ />,
125
+ );
126
+ const input = screen.getByRole("checkbox");
127
+ expect(input).toHaveAttribute("aria-checked", "false");
128
+ });
129
+
130
+ it("renders checked when state='checked'", () => {
131
+ render(
132
+ <Checkbox state="checked" onStateChange={() => {}} aria-label="Parent" />,
133
+ );
134
+ const input = screen.getByRole("checkbox");
135
+ expect(input).toHaveAttribute("aria-checked", "true");
136
+ });
137
+
138
+ it("renders indeterminate when state='indeterminate'", () => {
139
+ render(
140
+ <Checkbox
141
+ state="indeterminate"
142
+ onStateChange={() => {}}
143
+ aria-label="Parent"
144
+ />,
145
+ );
146
+ const input = screen.getByRole("checkbox");
147
+ expect(input).toHaveAttribute("aria-checked", "mixed");
148
+ });
149
+
150
+ it("calls onStateChange with next state on click (unchecked → checked)", () => {
151
+ const onStateChange = vi.fn();
152
+ render(
153
+ <Checkbox
154
+ state="unchecked"
155
+ onStateChange={onStateChange}
156
+ aria-label="Parent"
157
+ />,
158
+ );
159
+ fireEvent.click(screen.getByRole("checkbox"));
160
+ expect(onStateChange).toHaveBeenCalledWith("checked");
161
+ });
162
+
163
+ it("calls onStateChange with next state on click (checked → indeterminate)", () => {
164
+ const onStateChange = vi.fn();
165
+ render(
166
+ <Checkbox
167
+ state="checked"
168
+ onStateChange={onStateChange}
169
+ aria-label="Parent"
170
+ />,
171
+ );
172
+ fireEvent.click(screen.getByRole("checkbox"));
173
+ expect(onStateChange).toHaveBeenCalledWith("indeterminate");
174
+ });
175
+
176
+ it("calls onStateChange with next state on click (indeterminate → unchecked)", () => {
177
+ const onStateChange = vi.fn();
178
+ render(
179
+ <Checkbox
180
+ state="indeterminate"
181
+ onStateChange={onStateChange}
182
+ aria-label="Parent"
183
+ />,
184
+ );
185
+ fireEvent.click(screen.getByRole("checkbox"));
186
+ expect(onStateChange).toHaveBeenCalledWith("unchecked");
187
+ });
188
+ });
189
+
190
+ // ─────────────────────────────────────────────────────────────────────────────
191
+ // TriStateCheckbox component
192
+ // ─────────────────────────────────────────────────────────────────────────────
193
+
194
+ describe("TriStateCheckbox", () => {
195
+ it("renders with correct state", () => {
196
+ render(
197
+ <TriStateCheckbox
198
+ state="indeterminate"
199
+ onStateChange={() => {}}
200
+ aria-label="Select all"
201
+ />,
202
+ );
203
+ expect(screen.getByRole("checkbox")).toHaveAttribute(
204
+ "aria-checked",
205
+ "mixed",
206
+ );
207
+ });
208
+
209
+ it("calls onStateChange when clicked", () => {
210
+ const onStateChange = vi.fn();
211
+ render(
212
+ <TriStateCheckbox
213
+ state="unchecked"
214
+ onStateChange={onStateChange}
215
+ aria-label="Select all"
216
+ />,
217
+ );
218
+ fireEvent.click(screen.getByRole("checkbox"));
219
+ expect(onStateChange).toHaveBeenCalledWith("checked");
220
+ });
221
+ });
222
+
223
+ // ─────────────────────────────────────────────────────────────────────────────
224
+ // Error State
225
+ // ─────────────────────────────────────────────────────────────────────────────
226
+
227
+ describe("Checkbox — Error State", () => {
228
+ it("sets aria-invalid=true in error state", () => {
229
+ render(<Checkbox error aria-label="Required" />);
230
+ const input = screen.getByRole("checkbox");
231
+ expect(input).toHaveAttribute("aria-invalid", "true");
232
+ });
233
+
234
+ it("does not set aria-invalid when error is false", () => {
235
+ render(<Checkbox aria-label="Test" />);
236
+ const input = screen.getByRole("checkbox");
237
+ expect(input).not.toHaveAttribute("aria-invalid");
238
+ });
239
+ });
240
+
241
+ // ─────────────────────────────────────────────────────────────────────────────
242
+ // Disabled State
243
+ // ─────────────────────────────────────────────────────────────────────────────
244
+
245
+ describe("Checkbox — Disabled State", () => {
246
+ it("has disabled attribute when disabled=true", () => {
247
+ render(<Checkbox disabled aria-label="Disabled checkbox" />);
248
+ expect(screen.getByRole("checkbox")).toBeDisabled();
249
+ });
250
+
251
+ it("has aria-disabled=true when disabled=true", () => {
252
+ render(<Checkbox disabled aria-label="Disabled checkbox" />);
253
+ expect(screen.getByRole("checkbox")).toHaveAttribute(
254
+ "aria-disabled",
255
+ "true",
256
+ );
257
+ });
258
+
259
+ it("does not call onCheckedChange when disabled and clicked", () => {
260
+ const onCheckedChange = vi.fn();
261
+ render(
262
+ <Checkbox
263
+ disabled
264
+ onCheckedChange={onCheckedChange}
265
+ aria-label="Disabled"
266
+ />,
267
+ );
268
+ fireEvent.click(screen.getByRole("checkbox"));
269
+ expect(onCheckedChange).not.toHaveBeenCalled();
270
+ });
271
+
272
+ it("does not call onStateChange when disabled (tri-state)", () => {
273
+ const onStateChange = vi.fn();
274
+ render(
275
+ <Checkbox
276
+ disabled
277
+ state="unchecked"
278
+ onStateChange={onStateChange}
279
+ aria-label="Disabled"
280
+ />,
281
+ );
282
+ fireEvent.click(screen.getByRole("checkbox"));
283
+ expect(onStateChange).not.toHaveBeenCalled();
284
+ });
285
+ });
286
+
287
+ // ─────────────────────────────────────────────────────────────────────────────
288
+ // Accessibility
289
+ // ─────────────────────────────────────────────────────────────────────────────
290
+
291
+ describe("Checkbox — Accessibility", () => {
292
+ it("has correct aria-checked='false' when unchecked", () => {
293
+ render(<Checkbox aria-label="Test" />);
294
+ expect(screen.getByRole("checkbox")).toHaveAttribute(
295
+ "aria-checked",
296
+ "false",
297
+ );
298
+ });
299
+
300
+ it("has correct aria-checked='true' when checked", () => {
301
+ render(<Checkbox checked aria-label="Test" onCheckedChange={() => {}} />);
302
+ expect(screen.getByRole("checkbox")).toHaveAttribute(
303
+ "aria-checked",
304
+ "true",
305
+ );
306
+ });
307
+
308
+ it("has correct aria-checked='mixed' when indeterminate", () => {
309
+ render(<Checkbox indeterminate aria-label="Test" />);
310
+ expect(screen.getByRole("checkbox")).toHaveAttribute(
311
+ "aria-checked",
312
+ "mixed",
313
+ );
314
+ });
315
+
316
+ it("associates label with input via htmlFor when label prop provided", () => {
317
+ render(<Checkbox label="My label" id="my-checkbox" />);
318
+ const label = screen.getByText("My label").closest("label");
319
+ const input = screen.getByRole("checkbox");
320
+ expect(label).toHaveAttribute("for", "my-checkbox");
321
+ expect(input).toHaveAttribute("id", "my-checkbox");
322
+ });
323
+
324
+ it("forwards ref to the hidden input element", () => {
325
+ const ref = { current: null } as React.RefObject<HTMLInputElement | null>;
326
+ render(<Checkbox ref={ref} aria-label="Test" />);
327
+ expect(ref.current).not.toBeNull();
328
+ expect(ref.current?.tagName).toBe("INPUT");
329
+ expect(ref.current?.type).toBe("checkbox");
330
+ });
331
+
332
+ it("supports aria-label prop", () => {
333
+ render(<Checkbox aria-label="Custom accessible label" />);
334
+ expect(
335
+ screen.getByRole("checkbox", { name: "Custom accessible label" }),
336
+ ).toBeInTheDocument();
337
+ });
338
+
339
+ it("supports aria-labelledby prop", () => {
340
+ render(
341
+ <>
342
+ <span id="label-text">External label</span>
343
+ <Checkbox aria-labelledby="label-text" />
344
+ </>,
345
+ );
346
+ expect(screen.getByRole("checkbox")).toHaveAttribute(
347
+ "aria-labelledby",
348
+ "label-text",
349
+ );
350
+ });
351
+
352
+ it("supports aria-describedby prop", () => {
353
+ render(
354
+ <>
355
+ <span id="desc">Description text</span>
356
+ <Checkbox aria-label="Test" aria-describedby="desc" />
357
+ </>,
358
+ );
359
+ expect(screen.getByRole("checkbox")).toHaveAttribute(
360
+ "aria-describedby",
361
+ "desc",
362
+ );
363
+ });
364
+
365
+ it("supports aria-required prop", () => {
366
+ render(<Checkbox aria-label="Required field" aria-required />);
367
+ expect(screen.getByRole("checkbox")).toHaveAttribute(
368
+ "aria-required",
369
+ "true",
370
+ );
371
+ });
372
+ });
373
+
374
+ // ─────────────────────────────────────────────────────────────────────────────
375
+ // Form Integration
376
+ // ─────────────────────────────────────────────────────────────────────────────
377
+
378
+ describe("Checkbox — Form Integration", () => {
379
+ it("renders with name prop on the input", () => {
380
+ render(<Checkbox name="newsletter" aria-label="Subscribe" />);
381
+ expect(screen.getByRole("checkbox")).toHaveAttribute("name", "newsletter");
382
+ });
383
+
384
+ it("renders with value prop on the input", () => {
385
+ render(<Checkbox value="yes" aria-label="Subscribe" />);
386
+ expect(screen.getByRole("checkbox")).toHaveAttribute("value", "yes");
387
+ });
388
+
389
+ it("works as uncontrolled with defaultChecked=true", () => {
390
+ render(<Checkbox defaultChecked aria-label="Uncontrolled" />);
391
+ expect(screen.getByRole("checkbox")).toBeChecked();
392
+ });
393
+
394
+ it("toggles state when uncontrolled", () => {
395
+ render(<Checkbox aria-label="Uncontrolled toggle" />);
396
+ const input = screen.getByRole("checkbox");
397
+ expect(input).toHaveAttribute("aria-checked", "false");
398
+ fireEvent.click(input);
399
+ expect(input).toHaveAttribute("aria-checked", "true");
400
+ });
401
+ });
402
+
403
+ // ─────────────────────────────────────────────────────────────────────────────
404
+ // Keyboard Interaction
405
+ // ─────────────────────────────────────────────────────────────────────────────
406
+
407
+ describe("Checkbox — Keyboard Interaction", () => {
408
+ it("toggles on Space key press", () => {
409
+ const onCheckedChange = vi.fn();
410
+ render(
411
+ <Checkbox
412
+ checked={false}
413
+ onCheckedChange={onCheckedChange}
414
+ aria-label="Space test"
415
+ />,
416
+ );
417
+ const input = screen.getByRole("checkbox");
418
+ input.focus();
419
+ fireEvent.keyDown(input, { key: " ", code: "Space" });
420
+ fireEvent.click(input);
421
+ expect(onCheckedChange).toHaveBeenCalledWith(true);
422
+ });
423
+ });