@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,91 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import * as React from "react";
3
+ import { describe, expect, it } from "vitest";
4
+ import { Icon } from "../ui/icon";
5
+
6
+ describe("Icon Component", () => {
7
+ it("renders successfully with a name", () => {
8
+ render(<Icon name="home" />);
9
+ const icon = screen.getByText("home");
10
+ expect(icon).toBeInTheDocument();
11
+ expect(icon.tagName).toBe("SPAN");
12
+ });
13
+
14
+ it("applies default styles and attributes", () => {
15
+ render(<Icon name="home" />);
16
+ const icon = screen.getByText("home");
17
+
18
+ expect(icon).toHaveClass("md-icon", "select-none");
19
+ expect(icon).toHaveAttribute("aria-hidden", "true");
20
+
21
+ expect(icon).toHaveStyle({
22
+ fontFamily: "'Material Symbols Outlined'",
23
+ fontSize: "24px",
24
+ fontVariationSettings: "'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24",
25
+ });
26
+ });
27
+
28
+ it("renders different variants correctly", () => {
29
+ const { rerender } = render(<Icon name="home" variant="rounded" />);
30
+ let icon = screen.getByText("home");
31
+ expect(icon).toHaveStyle({ fontFamily: "'Material Symbols Rounded'" });
32
+
33
+ rerender(<Icon name="home" variant="sharp" />);
34
+ icon = screen.getByText("home");
35
+ expect(icon).toHaveStyle({ fontFamily: "'Material Symbols Sharp'" });
36
+
37
+ rerender(<Icon name="home" variant="outlined" />);
38
+ icon = screen.getByText("home");
39
+ expect(icon).toHaveStyle({ fontFamily: "'Material Symbols Outlined'" });
40
+ });
41
+
42
+ it("updates font-variation-settings based on axes props", () => {
43
+ render(
44
+ <Icon
45
+ name="settings"
46
+ fill={1}
47
+ weight={700}
48
+ grade={200}
49
+ opticalSize={48}
50
+ />,
51
+ );
52
+ const icon = screen.getByText("settings");
53
+ expect(icon).toHaveStyle({
54
+ fontVariationSettings: "'FILL' 1, 'wght' 700, 'GRAD' 200, 'opsz' 48",
55
+ });
56
+ });
57
+
58
+ it("overrides font-size when size prop is provided", () => {
59
+ render(<Icon name="home" size={32} opticalSize={40} />);
60
+ const icon = screen.getByText("home");
61
+ expect(icon).toHaveStyle({ fontSize: "32px" });
62
+ // Ensure opsz axis still uses opticalSize
63
+ expect(icon.style.fontVariationSettings).toContain("'opsz' 40");
64
+ });
65
+
66
+ it("forwards ref correctly", () => {
67
+ const ref = React.createRef<HTMLSpanElement>();
68
+ render(<Icon name="home" ref={ref} />);
69
+ expect(ref.current).not.toBeNull();
70
+ expect(ref.current?.tagName).toBe("SPAN");
71
+ expect(ref.current?.textContent).toBe("home");
72
+ });
73
+
74
+ it("renders with animateFill without crashing", () => {
75
+ render(<Icon name="favorite" fill={1} animateFill />);
76
+ const icon = screen.getByText("favorite");
77
+ expect(icon).toBeInTheDocument();
78
+ // In a jsdom environment, complex motion logic might not fully 'run'
79
+ // but we verify the component renders and has the correct styles.
80
+ const style = window.getComputedStyle(icon);
81
+ expect(style.fontVariationSettings).toBe(
82
+ "'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24",
83
+ );
84
+ });
85
+
86
+ it("passes through extra HTML attributes", () => {
87
+ render(<Icon name="home" data-testid="custom-icon" id="my-icon" />);
88
+ const icon = screen.getByTestId("custom-icon");
89
+ expect(icon).toHaveAttribute("id", "my-icon");
90
+ });
91
+ });
@@ -0,0 +1,128 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { LoadingIndicator } from "../ui/loading-indicator";
4
+
5
+ describe("LoadingIndicator", () => {
6
+ // ─── A11y: ARIA Attributes ───────────────────────────────────────────────
7
+ it("renders with role='progressbar'", () => {
8
+ render(<LoadingIndicator aria-label="Loading content" />);
9
+ expect(screen.getByRole("progressbar")).toBeInTheDocument();
10
+ });
11
+
12
+ it("applies required aria-label", () => {
13
+ render(<LoadingIndicator aria-label="Loading news article" />);
14
+ const el = screen.getByRole("progressbar");
15
+ expect(el).toHaveAttribute("aria-label", "Loading news article");
16
+ });
17
+
18
+ it("sets aria-valuemin and aria-valuemax", () => {
19
+ render(<LoadingIndicator aria-label="Loading" />);
20
+ const el = screen.getByRole("progressbar");
21
+ expect(el).toHaveAttribute("aria-valuemin", "0");
22
+ expect(el).toHaveAttribute("aria-valuemax", "100");
23
+ });
24
+
25
+ it("does not have aria-valuenow in indeterminate mode", () => {
26
+ render(<LoadingIndicator aria-label="Loading" />);
27
+ expect(screen.getByRole("progressbar")).not.toHaveAttribute(
28
+ "aria-valuenow",
29
+ );
30
+ });
31
+
32
+ // ─── Determinate Mode ────────────────────────────────────────────────────
33
+ it("sets aria-valuenow when progress is provided", () => {
34
+ render(<LoadingIndicator aria-label="Loading" progress={0.5} />);
35
+ expect(screen.getByRole("progressbar")).toHaveAttribute(
36
+ "aria-valuenow",
37
+ "50",
38
+ );
39
+ });
40
+
41
+ it("clamps aria-valuenow at 0 for negative progress", () => {
42
+ render(<LoadingIndicator aria-label="Loading" progress={-0.5} />);
43
+ expect(screen.getByRole("progressbar")).toHaveAttribute(
44
+ "aria-valuenow",
45
+ "0",
46
+ );
47
+ });
48
+
49
+ it("clamps aria-valuenow at 100 for progress > 1", () => {
50
+ render(<LoadingIndicator aria-label="Loading" progress={1.5} />);
51
+ expect(screen.getByRole("progressbar")).toHaveAttribute(
52
+ "aria-valuenow",
53
+ "100",
54
+ );
55
+ });
56
+
57
+ it("renders static path (no SMIL) in determinate mode", () => {
58
+ const { container } = render(
59
+ <LoadingIndicator aria-label="Loading" progress={0.5} />,
60
+ );
61
+ const path = container.querySelector("path");
62
+ expect(path).toBeInTheDocument();
63
+ // Determinate mode has no SMIL animate child
64
+ const animateEl = path?.querySelector("animate");
65
+ expect(animateEl).toBeNull();
66
+ });
67
+
68
+ // ─── Indeterminate Mode (SMIL) ───────────────────────────────────────────
69
+ it("renders SVG path with SMIL shape morphing animation in indeterminate mode", async () => {
70
+ const { container } = render(<LoadingIndicator aria-label="Loading" />);
71
+ const path = container.querySelector("path");
72
+ expect(path).toBeInTheDocument();
73
+ await waitFor(() => {
74
+ const animateEl = path?.querySelector("animate[attributeName='d']");
75
+ expect(animateEl).toBeInTheDocument();
76
+ });
77
+ });
78
+
79
+ // ─── Variants ────────────────────────────────────────────────────────────
80
+ it("renders default uncontained variant without container background", () => {
81
+ const { container } = render(<LoadingIndicator aria-label="Loading" />);
82
+ const inner = container.querySelector(".rounded-full");
83
+ expect(inner).toBeNull();
84
+ });
85
+
86
+ it("renders contained variant with a circular container", () => {
87
+ const { container } = render(
88
+ <LoadingIndicator variant="contained" aria-label="Loading" />,
89
+ );
90
+ const inner = container.querySelector(".rounded-full");
91
+ expect(inner).toBeInTheDocument();
92
+ });
93
+
94
+ // ─── Responsive Sizing ───────────────────────────────────────────────────
95
+ it("applies custom size via style", () => {
96
+ render(<LoadingIndicator size={96} aria-label="Loading" />);
97
+ const el = screen.getByRole("progressbar");
98
+ expect(el.style.width).toBe("96px");
99
+ expect(el.style.height).toBe("96px");
100
+ });
101
+
102
+ it("clamps size to 24dp minimum", () => {
103
+ render(<LoadingIndicator size={8} aria-label="Loading" />);
104
+ const el = screen.getByRole("progressbar");
105
+ expect(el.style.width).toBe("24px");
106
+ expect(el.style.height).toBe("24px");
107
+ });
108
+
109
+ it("clamps size to 240dp maximum", () => {
110
+ render(<LoadingIndicator size={999} aria-label="Loading" />);
111
+ const el = screen.getByRole("progressbar");
112
+ expect(el.style.width).toBe("240px");
113
+ expect(el.style.height).toBe("240px");
114
+ });
115
+
116
+ // ─── General ─────────────────────────────────────────────────────────────
117
+ it("renders an SVG element inside", () => {
118
+ const { container } = render(<LoadingIndicator aria-label="Loading" />);
119
+ const svg = container.querySelector("svg");
120
+ expect(svg).toBeInTheDocument();
121
+ expect(svg).toHaveAttribute("aria-hidden", "true");
122
+ });
123
+
124
+ it("merges custom className", () => {
125
+ render(<LoadingIndicator aria-label="Loading" className="my-custom" />);
126
+ expect(screen.getByRole("progressbar").className).toContain("my-custom");
127
+ });
128
+ });
@@ -0,0 +1,306 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { ProgressIndicator } from "../ui/progress-indicator";
4
+
5
+ describe("ProgressIndicator", () => {
6
+ // ── Linear Flat ──────────────────────────────────────────────────────────
7
+ describe("Linear - Flat", () => {
8
+ it("renders with role='progressbar'", () => {
9
+ render(
10
+ <ProgressIndicator variant="linear" aria-label="Uploading file" />,
11
+ );
12
+ expect(screen.getByRole("progressbar")).toBeInTheDocument();
13
+ });
14
+
15
+ it("applies required aria-label", () => {
16
+ render(
17
+ <ProgressIndicator variant="linear" aria-label="Uploading file" />,
18
+ );
19
+ expect(screen.getByRole("progressbar")).toHaveAttribute(
20
+ "aria-label",
21
+ "Uploading file",
22
+ );
23
+ });
24
+
25
+ it("shows aria-valuenow for determinate state", () => {
26
+ render(
27
+ <ProgressIndicator
28
+ variant="linear"
29
+ value={42}
30
+ aria-label="Downloading"
31
+ />,
32
+ );
33
+ const el = screen.getByRole("progressbar");
34
+ expect(el).toHaveAttribute("aria-valuenow", "42");
35
+ expect(el).toHaveAttribute("aria-valuemin", "0");
36
+ expect(el).toHaveAttribute("aria-valuemax", "100");
37
+ });
38
+
39
+ it("does not show aria-valuenow for indeterminate state", () => {
40
+ render(<ProgressIndicator variant="linear" aria-label="Loading" />);
41
+ expect(screen.getByRole("progressbar")).not.toHaveAttribute(
42
+ "aria-valuenow",
43
+ );
44
+ });
45
+
46
+ it("clamps value between 0 and 100", () => {
47
+ render(
48
+ <ProgressIndicator
49
+ variant="linear"
50
+ value={150}
51
+ aria-label="Overloaded"
52
+ />,
53
+ );
54
+ expect(screen.getByRole("progressbar")).toHaveAttribute(
55
+ "aria-valuenow",
56
+ "100",
57
+ );
58
+ });
59
+
60
+ it("shows stop indicator dot in determinate mode by default", () => {
61
+ const { container } = render(
62
+ <ProgressIndicator variant="linear" value={50} aria-label="Loading" />,
63
+ );
64
+ const dot = container.querySelector(".rounded-full[aria-hidden='true']");
65
+ expect(dot).toBeInTheDocument();
66
+ });
67
+
68
+ it("stop indicator is 4px (MD3 spec) regardless of trackHeight", () => {
69
+ const { container } = render(
70
+ <ProgressIndicator
71
+ variant="linear"
72
+ value={50}
73
+ trackHeight={8}
74
+ aria-label="Thick track"
75
+ />,
76
+ );
77
+ const dot = container.querySelector(
78
+ ".rounded-full[aria-hidden='true']",
79
+ ) as HTMLElement | null;
80
+ expect(dot).toBeInTheDocument();
81
+ expect(dot?.style.width).toBe("4px");
82
+ expect(dot?.style.height).toBe("4px");
83
+ });
84
+
85
+ it("stop indicator is NOT shown for indeterminate state", () => {
86
+ const { container } = render(
87
+ <ProgressIndicator variant="linear" aria-label="Loading" />,
88
+ );
89
+ const dot = container.querySelector(".rounded-full[aria-hidden='true']");
90
+ expect(dot).toBeNull();
91
+ });
92
+
93
+ it("accepts space prop", () => {
94
+ render(
95
+ <ProgressIndicator
96
+ variant="linear"
97
+ value={50}
98
+ gapSize={8}
99
+ aria-label="Test space"
100
+ />,
101
+ );
102
+ expect(screen.getByRole("progressbar")).toBeInTheDocument();
103
+ });
104
+
105
+ it("hides stop indicator when showStopIndicator=false", () => {
106
+ const { container } = render(
107
+ <ProgressIndicator
108
+ variant="linear"
109
+ value={50}
110
+ showStopIndicator={false}
111
+ aria-label="Loading"
112
+ />,
113
+ );
114
+ const dot = container.querySelector(".rounded-full[aria-hidden='true']");
115
+ expect(dot).toBeNull();
116
+ });
117
+
118
+ it("merges custom className", () => {
119
+ render(
120
+ <ProgressIndicator
121
+ variant="linear"
122
+ aria-label="Loading"
123
+ className="test-class"
124
+ />,
125
+ );
126
+ expect(screen.getByRole("progressbar").className).toContain("test-class");
127
+ });
128
+
129
+ it("active indicator has minWidth (dot) when value is 0 — MD3 spec", () => {
130
+ // MD3 spec: "When progress first begins, the active indicator appears as a dot."
131
+ const { container } = render(
132
+ <ProgressIndicator variant="linear" value={0} aria-label="Starting" />,
133
+ );
134
+ // The animated div inside the track should have minWidth set (not zero)
135
+ // Track background is the first absolute div, active is the second absolute div.
136
+ const track = container.querySelectorAll(".relative.w-full .absolute")[1];
137
+ expect(track).toBeInTheDocument();
138
+ const style = (track as HTMLElement)?.style;
139
+ // minWidth should be the trackHeight default (4px)
140
+ expect(style?.minWidth).toBe("4px");
141
+ });
142
+ });
143
+
144
+ // ── Linear Wavy ──────────────────────────────────────────────────────────
145
+ describe("Linear - Wavy", () => {
146
+ it("renders SVG for wavy shape", () => {
147
+ const { container } = render(
148
+ <ProgressIndicator
149
+ variant="linear"
150
+ shape="wavy"
151
+ aria-label="Loading wavy"
152
+ />,
153
+ );
154
+ const svg = container.querySelector("svg");
155
+ expect(svg).toBeInTheDocument();
156
+ });
157
+
158
+ it("accepts custom amplitude and wavelength", () => {
159
+ const { container } = render(
160
+ <ProgressIndicator
161
+ variant="linear"
162
+ shape="wavy"
163
+ amplitude={10}
164
+ wavelength={40}
165
+ aria-label="Loading wavy custom"
166
+ />,
167
+ );
168
+ expect(container.querySelector("svg")).toBeInTheDocument();
169
+ });
170
+
171
+ it("SVG has overflow=visible to prevent wave clip (MD3)", () => {
172
+ const { container } = render(
173
+ <ProgressIndicator
174
+ variant="linear"
175
+ shape="wavy"
176
+ aria-label="Loading wavy overflow"
177
+ />,
178
+ );
179
+ const svg = container.querySelector("svg") as SVGElement | null;
180
+ expect(svg?.style.overflow).toBe("visible");
181
+ });
182
+
183
+ it("accepts indeterminateWavelength prop", () => {
184
+ const { container } = render(
185
+ <ProgressIndicator
186
+ variant="linear"
187
+ shape="wavy"
188
+ indeterminateWavelength={30}
189
+ aria-label="Custom indeterminate wavelength"
190
+ />,
191
+ );
192
+ expect(container.querySelector("svg")).toBeInTheDocument();
193
+ });
194
+
195
+ it("does NOT show stop indicator for wavy indeterminate — MD3 spec", () => {
196
+ const { container } = render(
197
+ <ProgressIndicator
198
+ variant="linear"
199
+ shape="wavy"
200
+ aria-label="Loading wavy no stop"
201
+ />,
202
+ );
203
+ const dot = container.querySelector(".rounded-full[aria-hidden='true']");
204
+ expect(dot).toBeNull();
205
+ });
206
+ });
207
+
208
+ // ── Circular ─────────────────────────────────────────────────────────────
209
+ describe("Circular", () => {
210
+ it("renders with role='progressbar'", () => {
211
+ render(
212
+ <ProgressIndicator variant="circular" aria-label="Loading circular" />,
213
+ );
214
+ expect(screen.getByRole("progressbar")).toBeInTheDocument();
215
+ });
216
+
217
+ it("renders SVG with circle elements", () => {
218
+ const { container } = render(
219
+ <ProgressIndicator variant="circular" aria-label="Loading circular" />,
220
+ );
221
+ const circles = container.querySelectorAll("circle");
222
+ expect(circles.length).toBeGreaterThanOrEqual(2);
223
+ });
224
+
225
+ it("shows aria-valuenow for determinate state", () => {
226
+ render(
227
+ <ProgressIndicator
228
+ variant="circular"
229
+ value={75}
230
+ aria-label="Loading 75%"
231
+ />,
232
+ );
233
+ expect(screen.getByRole("progressbar")).toHaveAttribute(
234
+ "aria-valuenow",
235
+ "75",
236
+ );
237
+ });
238
+
239
+ it("applies custom size via inline style", () => {
240
+ render(
241
+ <ProgressIndicator variant="circular" size={64} aria-label="Loading" />,
242
+ );
243
+ const el = screen.getByRole("progressbar");
244
+ expect(el.style.width).toBe("64px");
245
+ expect(el.style.height).toBe("64px");
246
+ });
247
+
248
+ it("does not show aria-valuenow for indeterminate", () => {
249
+ render(<ProgressIndicator variant="circular" aria-label="Loading" />);
250
+ expect(screen.getByRole("progressbar")).not.toHaveAttribute(
251
+ "aria-valuenow",
252
+ );
253
+ });
254
+ });
255
+
256
+ // ── Circular Wavy ────────────────────────────────────────────────────────
257
+ describe("Circular - Wavy", () => {
258
+ it("renders SVG with path element for wavy shape", () => {
259
+ const { container } = render(
260
+ <ProgressIndicator
261
+ variant="circular"
262
+ shape="wavy"
263
+ aria-label="Loading circular wavy"
264
+ />,
265
+ );
266
+ const svg = container.querySelector("svg");
267
+ expect(svg).toBeInTheDocument();
268
+ const path = container.querySelector("path");
269
+ expect(path).toBeInTheDocument();
270
+ });
271
+
272
+ it("accepts custom amplitude and wavelength", () => {
273
+ const { container } = render(
274
+ <ProgressIndicator
275
+ variant="circular"
276
+ shape="wavy"
277
+ amplitude={8}
278
+ wavelength={40}
279
+ aria-label="Loading circular wavy custom"
280
+ />,
281
+ );
282
+ const path = container.querySelector("path");
283
+ expect(path).toBeInTheDocument();
284
+ // verify that path starts with M (move layout command generated mathematically)
285
+ expect(path?.getAttribute("d")?.startsWith("M ")).toBe(true);
286
+ });
287
+ });
288
+
289
+ // ── RTL Support ──────────────────────────────────────────────────────────
290
+ describe("RTL support", () => {
291
+ it("renders linear inside RTL container without crashing", () => {
292
+ const { container } = render(
293
+ <div dir="rtl">
294
+ <ProgressIndicator
295
+ variant="linear"
296
+ value={30}
297
+ aria-label="RTL progress"
298
+ />
299
+ </div>,
300
+ );
301
+ expect(
302
+ container.querySelector("[role='progressbar']"),
303
+ ).toBeInTheDocument();
304
+ });
305
+ });
306
+ });
@@ -0,0 +1,80 @@
1
+ import "@testing-library/jest-dom";
2
+ import { vi } from "vitest";
3
+
4
+ // Mock ResizeObserver for Radix UI (must be a constructor and trigger callback)
5
+ class ResizeObserverMock implements ResizeObserver {
6
+ private callback: ResizeObserverCallback;
7
+
8
+ constructor(callback: ResizeObserverCallback) {
9
+ this.callback = callback;
10
+ }
11
+
12
+ observe(element: Element, _options?: ResizeObserverOptions) {
13
+ // Immediately trigger callback with some dimensions to trick Radix
14
+ this.callback(
15
+ [
16
+ {
17
+ target: element,
18
+ contentRect: {
19
+ height: 1000,
20
+ width: 1000,
21
+ x: 0,
22
+ y: 0,
23
+ top: 0,
24
+ right: 1000,
25
+ bottom: 1000,
26
+ left: 0,
27
+ toJSON: () => ({}),
28
+ },
29
+ borderBoxSize: [],
30
+ contentBoxSize: [],
31
+ devicePixelContentBoxSize: [],
32
+ } as ResizeObserverEntry,
33
+ ],
34
+ this,
35
+ );
36
+ }
37
+ unobserve = vi.fn((_element: Element) => {});
38
+ disconnect = vi.fn(() => {});
39
+ }
40
+
41
+ vi.stubGlobal("ResizeObserver", ResizeObserverMock);
42
+
43
+ // Mock PointerEvent which is missing in JSDOM but used by Radix
44
+ if (!globalThis.PointerEvent) {
45
+ class PointerEventMock extends MouseEvent {
46
+ constructor(type: string, params: PointerEventInit = {}) {
47
+ super(type, params);
48
+ }
49
+ }
50
+ // Use any here for global registration as it's a known environment issue
51
+ // but we cast it as unknown first to satisfy some strict linting
52
+ vi.stubGlobal(
53
+ "PointerEvent",
54
+ PointerEventMock as unknown as typeof PointerEvent,
55
+ );
56
+ }
57
+
58
+ // Mock HTMLElement prototype to return dimensions
59
+ Object.defineProperty(HTMLElement.prototype, "offsetParent", {
60
+ get() {
61
+ return this.parentNode;
62
+ },
63
+ });
64
+
65
+ // Mock IntersectionObserver
66
+ class IntersectionObserverMock implements IntersectionObserver {
67
+ readonly root: Element | null = null;
68
+ readonly rootMargin: string = "";
69
+ readonly thresholds: ReadonlyArray<number> = [];
70
+ constructor(
71
+ public callback: IntersectionObserverCallback,
72
+ _options?: IntersectionObserverInit,
73
+ ) {}
74
+ observe = vi.fn((_element: Element) => {});
75
+ unobserve = vi.fn((_element: Element) => {});
76
+ disconnect = vi.fn(() => {});
77
+ takeRecords = vi.fn(() => []);
78
+ }
79
+
80
+ vi.stubGlobal("IntersectionObserver", IntersectionObserverMock);