@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,345 @@
1
+ "use client";
2
+
3
+ import { render, screen } from "@testing-library/react";
4
+ import * as MotionReact from "motion/react";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { Badge, BadgedBox } from "./badge";
7
+
8
+ // Mock motion/react – same pattern as chip.test.tsx
9
+ vi.mock("motion/react", async (importOriginal) => {
10
+ const actual = await importOriginal<typeof import("motion/react")>();
11
+ return {
12
+ ...actual,
13
+ useReducedMotion: () => false,
14
+ };
15
+ });
16
+
17
+ // ── Helpers ──────────────────────────────────────────────────────────────────
18
+
19
+ const TestIcon = () => (
20
+ <svg data-testid="test-icon" aria-hidden="true" viewBox="0 0 24 24" />
21
+ );
22
+
23
+ // ── Test Suites ───────────────────────────────────────────────────────────────
24
+
25
+ describe("Badge", () => {
26
+ afterEach(() => {
27
+ vi.restoreAllMocks();
28
+ });
29
+
30
+ // ── Rendering ──────────────────────────────────────────────────────────────
31
+
32
+ describe("rendering", () => {
33
+ it("renders as small dot when no children provided", () => {
34
+ const { container } = render(<Badge />);
35
+ const badge = container.firstChild as HTMLElement;
36
+ expect(badge).toBeInTheDocument();
37
+ // Small dot: aria-hidden (decorative)
38
+ expect(badge).toHaveAttribute("aria-hidden", "true");
39
+ });
40
+
41
+ it("renders with text content when children provided", () => {
42
+ render(<Badge>3</Badge>);
43
+ expect(screen.getByRole("status")).toHaveTextContent("3");
44
+ });
45
+
46
+ it("applies correct size classes for small variant (6x6px)", () => {
47
+ const { container } = render(<Badge />);
48
+ const badge = container.firstChild as HTMLElement;
49
+ // Tailwind shorthand: w-1.5 = 6px, h-1.5 = 6px
50
+ expect(badge.className).toContain("w-1.5");
51
+ expect(badge.className).toContain("h-1.5");
52
+ });
53
+
54
+ it("applies correct size classes for large variant (min 16px)", () => {
55
+ const { container } = render(<Badge>3</Badge>);
56
+ const badge = container.firstChild as HTMLElement;
57
+ // Tailwind shorthand: min-w-4 = 16px, h-4 = 16px
58
+ expect(badge.className).toContain("min-w-4");
59
+ expect(badge.className).toContain("h-4");
60
+ });
61
+ });
62
+
63
+ // ── Content truncation ─────────────────────────────────────────────────────
64
+
65
+ describe("content truncation", () => {
66
+ it("displays exact number when below max", () => {
67
+ render(<Badge max={99}>42</Badge>);
68
+ expect(screen.getByRole("status")).toHaveTextContent("42");
69
+ });
70
+
71
+ it("displays '{max}+' when number exceeds max prop", () => {
72
+ render(<Badge max={99}>150</Badge>);
73
+ expect(screen.getByRole("status")).toHaveTextContent("99+");
74
+ });
75
+
76
+ it("defaults to showing full number when max not set", () => {
77
+ render(<Badge>9999</Badge>);
78
+ expect(screen.getByRole("status")).toHaveTextContent("9999");
79
+ });
80
+
81
+ it("truncates string to 4 characters maximum including +", () => {
82
+ render(<Badge>HELLO</Badge>);
83
+ // "HELLO" → 5 chars → truncated to "HELL"
84
+ expect(screen.getByRole("status")).toHaveTextContent("HELL");
85
+ });
86
+
87
+ it("does not truncate strings of 4 chars or fewer", () => {
88
+ render(<Badge>NEW</Badge>);
89
+ expect(screen.getByRole("status")).toHaveTextContent("NEW");
90
+ });
91
+
92
+ it("displays '99+' when value is exactly max+1", () => {
93
+ render(<Badge max={99}>100</Badge>);
94
+ expect(screen.getByRole("status")).toHaveTextContent("99+");
95
+ });
96
+ });
97
+
98
+ // ── Styling ────────────────────────────────────────────────────────────────
99
+
100
+ describe("styling", () => {
101
+ it("applies MD3 error color as default container color for small badge", () => {
102
+ const { container } = render(<Badge />);
103
+ const badge = container.firstChild as HTMLElement;
104
+ expect(badge.className).toContain("bg-m3-error");
105
+ });
106
+
107
+ it("applies MD3 error color as default container color for large badge", () => {
108
+ const { container } = render(<Badge>3</Badge>);
109
+ const badge = container.firstChild as HTMLElement;
110
+ expect(badge.className).toContain("bg-m3-error");
111
+ });
112
+
113
+ it("applies MD3 onError color as default content color", () => {
114
+ const { container } = render(<Badge>3</Badge>);
115
+ const badge = container.firstChild as HTMLElement;
116
+ expect(badge.className).toContain("text-m3-on-error");
117
+ });
118
+
119
+ it("accepts custom containerColor prop via inline style", () => {
120
+ const { container } = render(<Badge containerColor="blue">3</Badge>);
121
+ const badge = container.firstChild as HTMLElement;
122
+ expect(badge.style.backgroundColor).toBe("blue");
123
+ // Should NOT apply default bg class when override is provided
124
+ expect(badge.className).not.toContain("bg-m3-error");
125
+ });
126
+
127
+ it("accepts custom contentColor prop via inline style", () => {
128
+ const { container } = render(<Badge contentColor="white">3</Badge>);
129
+ const badge = container.firstChild as HTMLElement;
130
+ expect(badge.style.color).toBe("white");
131
+ });
132
+
133
+ it("applies rounded-full shape (MD3 CornerFull = 9999px)", () => {
134
+ const { container } = render(<Badge>3</Badge>);
135
+ const badge = container.firstChild as HTMLElement;
136
+ expect(badge.className).toContain("rounded-full");
137
+ });
138
+ });
139
+
140
+ // ── Accessibility ──────────────────────────────────────────────────────────
141
+
142
+ describe("accessibility", () => {
143
+ it("has role='status' for screen reader announcements when content present", () => {
144
+ render(<Badge>3</Badge>);
145
+ expect(screen.getByRole("status")).toBeInTheDocument();
146
+ });
147
+
148
+ it("renders aria-hidden='true' for small dot badge (decorative)", () => {
149
+ const { container } = render(<Badge />);
150
+ const badge = container.firstChild as HTMLElement;
151
+ expect(badge).toHaveAttribute("aria-hidden", "true");
152
+ });
153
+
154
+ it("renders aria-label with content value for large badge", () => {
155
+ render(<Badge>3</Badge>);
156
+ expect(screen.getByRole("status")).toHaveAttribute("aria-label", "3");
157
+ });
158
+
159
+ it("renders aria-label with max+ when number exceeds max", () => {
160
+ render(<Badge max={99}>150</Badge>);
161
+ expect(screen.getByRole("status")).toHaveAttribute("aria-label", "99+");
162
+ });
163
+
164
+ it("accepts explicit aria-label prop (overrides default)", () => {
165
+ render(<Badge aria-label="3 notifications">3</Badge>);
166
+ expect(screen.getByRole("status")).toHaveAttribute(
167
+ "aria-label",
168
+ "3 notifications",
169
+ );
170
+ });
171
+
172
+ it("small badge with explicit aria-label gets role='status'", () => {
173
+ render(<Badge aria-label="New notification" />);
174
+ expect(screen.getByRole("status")).toBeInTheDocument();
175
+ expect(screen.getByRole("status")).toHaveAttribute(
176
+ "aria-label",
177
+ "New notification",
178
+ );
179
+ });
180
+ });
181
+
182
+ // ── className merging ──────────────────────────────────────────────────────
183
+
184
+ describe("className merging", () => {
185
+ it("merges additional className with base classes", () => {
186
+ const { container } = render(
187
+ <Badge className="my-custom-class">3</Badge>,
188
+ );
189
+ const badge = container.firstChild as HTMLElement;
190
+ expect(badge).toHaveClass("my-custom-class");
191
+ // Still has base classes
192
+ expect(badge.className).toContain("rounded-full");
193
+ });
194
+
195
+ it("merges additional className for small dot badge", () => {
196
+ const { container } = render(<Badge className="dot-extra" />);
197
+ const badge = container.firstChild as HTMLElement;
198
+ expect(badge).toHaveClass("dot-extra");
199
+ expect(badge.className).toContain("w-1.5");
200
+ });
201
+ });
202
+
203
+ // ── forwardRef ─────────────────────────────────────────────────────────────
204
+
205
+ describe("forwardRef", () => {
206
+ it("forwards ref to underlying span element", () => {
207
+ const ref = { current: null };
208
+ render(<Badge ref={ref}>3</Badge>);
209
+ expect(ref.current).not.toBeNull();
210
+ expect((ref.current as unknown as HTMLElement).tagName).toBe("SPAN");
211
+ });
212
+
213
+ it("forwards ref to small dot badge span element", () => {
214
+ const ref = { current: null };
215
+ render(<Badge ref={ref} />);
216
+ expect(ref.current).not.toBeNull();
217
+ });
218
+ });
219
+
220
+ // ── Reduced Motion ─────────────────────────────────────────────────────────
221
+
222
+ describe("prefers-reduced-motion", () => {
223
+ it("renders correctly when prefers-reduced-motion is active", () => {
224
+ vi.spyOn(MotionReact, "useReducedMotion").mockReturnValue(true);
225
+ render(<Badge>3</Badge>);
226
+ expect(screen.getByRole("status")).toBeInTheDocument();
227
+ });
228
+ });
229
+ });
230
+
231
+ // ── BadgedBox Tests ───────────────────────────────────────────────────────────
232
+
233
+ describe("BadgedBox", () => {
234
+ it("renders anchor content", () => {
235
+ render(
236
+ <BadgedBox badge={<Badge />}>
237
+ <TestIcon />
238
+ </BadgedBox>,
239
+ );
240
+ expect(screen.getByTestId("test-icon")).toBeInTheDocument();
241
+ });
242
+
243
+ it("renders badge overlaying the anchor", () => {
244
+ render(
245
+ <BadgedBox badge={<Badge aria-label="3 new" />}>
246
+ <TestIcon />
247
+ </BadgedBox>,
248
+ );
249
+ // The small badge is decorated, but the aria-label makes it visible to a11y
250
+ expect(screen.getByLabelText("3 new")).toBeInTheDocument();
251
+ });
252
+
253
+ it("applies relative positioning to container", () => {
254
+ const { container } = render(
255
+ <BadgedBox badge={<Badge />}>
256
+ <TestIcon />
257
+ </BadgedBox>,
258
+ );
259
+ // The outer wrapper span has relative positioning class
260
+ const wrapper = container.firstChild as HTMLElement;
261
+ expect(wrapper.className).toContain("relative");
262
+ expect(wrapper.className).toContain("inline-flex");
263
+ });
264
+
265
+ it("applies absolute positioning to badge wrapper", () => {
266
+ const { container } = render(
267
+ <BadgedBox badge={<Badge />}>
268
+ <TestIcon />
269
+ </BadgedBox>,
270
+ );
271
+ // Find the badge positioner span (second child of outer wrapper)
272
+ const wrapper = container.firstChild as HTMLElement;
273
+ const badgeSlot = wrapper.children[1] as HTMLElement;
274
+ expect(badgeSlot.className).toContain("absolute");
275
+ });
276
+
277
+ it("positions small badge at top-trailing corner with 50% offset", () => {
278
+ const { container } = render(
279
+ <BadgedBox badge={<Badge />}>
280
+ <TestIcon />
281
+ </BadgedBox>,
282
+ );
283
+ const wrapper = container.firstChild as HTMLElement;
284
+ const badgeSlot = wrapper.children[1] as HTMLElement;
285
+ expect(badgeSlot.className).toContain("translate-x-[50%]");
286
+ expect(badgeSlot.className).toContain("-translate-y-[50%]");
287
+ });
288
+
289
+ it("positions large badge at top-trailing corner with 35% offset", () => {
290
+ const { container } = render(
291
+ <BadgedBox badge={<Badge>3</Badge>}>
292
+ <TestIcon />
293
+ </BadgedBox>,
294
+ );
295
+ const wrapper = container.firstChild as HTMLElement;
296
+ const badgeSlot = wrapper.children[1] as HTMLElement;
297
+ expect(badgeSlot.className).toContain("translate-x-[35%]");
298
+ expect(badgeSlot.className).toContain("-translate-y-[35%]");
299
+ });
300
+
301
+ it("respects explicit badgeSize='small' prop", () => {
302
+ const { container } = render(
303
+ // Even though badge has content, explicitly set small
304
+ <BadgedBox badge={<Badge>3</Badge>} badgeSize="small">
305
+ <TestIcon />
306
+ </BadgedBox>,
307
+ );
308
+ const wrapper = container.firstChild as HTMLElement;
309
+ const badgeSlot = wrapper.children[1] as HTMLElement;
310
+ expect(badgeSlot.className).toContain("translate-x-[50%]");
311
+ });
312
+
313
+ it("respects explicit badgeSize='large' prop", () => {
314
+ const { container } = render(
315
+ // Even though badge has no content, explicitly set large
316
+ <BadgedBox badge={<Badge />} badgeSize="large">
317
+ <TestIcon />
318
+ </BadgedBox>,
319
+ );
320
+ const wrapper = container.firstChild as HTMLElement;
321
+ const badgeSlot = wrapper.children[1] as HTMLElement;
322
+ expect(badgeSlot.className).toContain("translate-x-[35%]");
323
+ });
324
+
325
+ it("accepts and applies className to container", () => {
326
+ const { container } = render(
327
+ <BadgedBox badge={<Badge />} className="my-box-class">
328
+ <TestIcon />
329
+ </BadgedBox>,
330
+ );
331
+ const wrapper = container.firstChild as HTMLElement;
332
+ expect(wrapper).toHaveClass("my-box-class");
333
+ });
334
+
335
+ it("badge slot wrapper has aria-hidden to avoid double-announcement", () => {
336
+ const { container } = render(
337
+ <BadgedBox badge={<Badge>3</Badge>}>
338
+ <TestIcon />
339
+ </BadgedBox>,
340
+ );
341
+ const wrapper = container.firstChild as HTMLElement;
342
+ const badgeSlot = wrapper.children[1] as HTMLElement;
343
+ expect(badgeSlot).toHaveAttribute("aria-hidden", "true");
344
+ });
345
+ });
@@ -0,0 +1,282 @@
1
+ /**
2
+ * @file badge.tsx
3
+ *
4
+ * MD3 Expressive Badge component.
5
+ *
6
+ * - `Badge` → A small status indicator. Can be a dot (no content) or labeled (with content).
7
+ * - `BadgedBox` → Positions a Badge at the top-trailing corner of an anchor element.
8
+ *
9
+ * @remarks
10
+ * Token references (Kotlin source):
11
+ * BadgeTokens — Size=6dp (→ 6px), LargeSize=16dp (→ 16px), Shape=CornerFull, Color=Error,
12
+ * LargeLabelTextFont=LabelSmall, LargeLabelTextColor=OnError
13
+ *
14
+ * BadgedBox offsets:
15
+ * - Small (dot): BadgeOffset = 6dp → translate(50%, -50%)
16
+ * - Large (text): HOffset=12dp, VOffset=14dp → translate(35%, -35%)
17
+ *
18
+ * Architecture:
19
+ * - Styling: `cn` (clsx/tailwind-merge) + static Tailwind classes
20
+ * - Animation: Framer Motion (`LazyMotion` + `domMax`) spring mount/unmount
21
+ * - A11y: `role="status"` with `aria-label`, decorative dots use `aria-hidden="true"`
22
+ *
23
+ * @see https://m3.material.io/components/badge/overview
24
+ */
25
+
26
+ import {
27
+ AnimatePresence,
28
+ domMax,
29
+ LazyMotion,
30
+ m,
31
+ useReducedMotion,
32
+ } from "motion/react";
33
+ import * as React from "react";
34
+ import { cn } from "../lib/utils";
35
+
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+ // Types
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+
40
+ // Exclude onDrag-family event handlers that conflict between React's HTMLAttributes
41
+ // and Framer Motion's MotionProps (different DragEvent signatures).
42
+ type SafeHTMLSpanAttrs = Omit<
43
+ React.HTMLAttributes<HTMLSpanElement>,
44
+ | "onDrag"
45
+ | "onDragStart"
46
+ | "onDragEnd"
47
+ | "onDragEnter"
48
+ | "onDragLeave"
49
+ | "onDragOver"
50
+ | "onDrop"
51
+ >;
52
+
53
+ export interface BadgeProps extends SafeHTMLSpanAttrs {
54
+ /**
55
+ * The content to display inside the badge.
56
+ * - Omitted / undefined → renders as a small 6×6px dot (decorative).
57
+ * - string | number → renders as a large badge (min 16px height) with label.
58
+ *
59
+ * Numbers exceeding `max` are displayed as `{max}+`.
60
+ * Strings longer than 4 characters are truncated to 4.
61
+ */
62
+ children?: React.ReactNode;
63
+
64
+ /**
65
+ * Maximum numeric value to display before appending "+".
66
+ * Only applies when `children` is a number.
67
+ * @example max={99} + children={150} → "99+"
68
+ */
69
+ max?: number;
70
+
71
+ /**
72
+ * Override the background (container) color.
73
+ * Accepts any valid CSS color value.
74
+ * Defaults to MD3 `error` token — `bg-m3-error`.
75
+ */
76
+ containerColor?: string;
77
+
78
+ /**
79
+ * Override the text/content color.
80
+ * Accepts any valid CSS color value.
81
+ * Defaults to MD3 `on-error` token — `text-m3-on-error`.
82
+ */
83
+ contentColor?: string;
84
+ }
85
+
86
+ export interface BadgedBoxProps {
87
+ /**
88
+ * The badge element to overlay on the anchor.
89
+ * Typically a `<Badge />`.
90
+ */
91
+ badge: React.ReactNode;
92
+
93
+ /**
94
+ * The anchor content that the badge is attached to.
95
+ */
96
+ children: React.ReactNode;
97
+
98
+ /**
99
+ * Additional className applied to the outer wrapper `span`.
100
+ */
101
+ className?: string;
102
+
103
+ /**
104
+ * Explicitly override size detection for badge positioning.
105
+ * - `'small'` → BadgeOffset = 6dp → translate(50%, -50%)
106
+ * - `'large'` → HOffset=12dp/VOffset=14dp → translate(35%, -35%)
107
+ * When omitted, BadgedBox auto-detects by inspecting `badge` children prop.
108
+ */
109
+ badgeSize?: "small" | "large";
110
+ }
111
+
112
+ // ─────────────────────────────────────────────────────────────────────────────
113
+ // Helpers
114
+ // ─────────────────────────────────────────────────────────────────────────────
115
+
116
+ function formatBadgeLabel(children: React.ReactNode, max?: number): string {
117
+ if (typeof children === "number") {
118
+ return max !== undefined && children > max ? `${max}+` : String(children);
119
+ }
120
+ if (typeof children === "string") {
121
+ if (max !== undefined) {
122
+ const asNum = Number(children);
123
+ if (!Number.isNaN(asNum) && asNum > max) return `${max}+`;
124
+ }
125
+ return children.length > 4 ? children.slice(0, 4) : children;
126
+ }
127
+ return "";
128
+ }
129
+
130
+ function detectBadgeHasContent(badge: React.ReactNode): boolean {
131
+ return (
132
+ React.isValidElement<{ children?: React.ReactNode }>(badge) &&
133
+ badge.props.children != null
134
+ );
135
+ }
136
+
137
+ // ─────────────────────────────────────────────────────────────────────────────
138
+ // Badge
139
+ // ─────────────────────────────────────────────────────────────────────────────
140
+
141
+ const BadgeImpl = React.forwardRef<HTMLSpanElement, BadgeProps>(
142
+ (
143
+ {
144
+ children,
145
+ max,
146
+ containerColor,
147
+ contentColor,
148
+ className,
149
+ style,
150
+ "aria-label": ariaLabel,
151
+ ...props
152
+ },
153
+ ref,
154
+ ) => {
155
+ const hasContent = children != null;
156
+ const label = hasContent ? formatBadgeLabel(children, max) : "";
157
+ const reducedMotion = useReducedMotion();
158
+ const isDecorative = !hasContent && !ariaLabel;
159
+
160
+ const springTransition = reducedMotion
161
+ ? { duration: 0 }
162
+ : { type: "spring" as const, stiffness: 500, damping: 30, mass: 0.8 };
163
+
164
+ const colorStyle: React.CSSProperties = {
165
+ ...(containerColor && { backgroundColor: containerColor }),
166
+ ...(contentColor && { color: contentColor }),
167
+ ...style,
168
+ };
169
+
170
+ return (
171
+ <LazyMotion features={domMax} strict>
172
+ <m.span
173
+ ref={ref}
174
+ role={isDecorative ? undefined : "status"}
175
+ aria-hidden={isDecorative ? "true" : undefined}
176
+ aria-label={hasContent ? (ariaLabel ?? label) : ariaLabel}
177
+ initial={{ scale: 0, opacity: 0 }}
178
+ animate={{ scale: 1, opacity: 1 }}
179
+ exit={{ scale: 0, opacity: 0 }}
180
+ transition={springTransition}
181
+ className={cn(
182
+ "rounded-full ring-[1.5px] ring-m3-surface",
183
+ !containerColor && "bg-m3-error",
184
+ hasContent
185
+ ? cn(
186
+ "inline-flex items-center justify-center",
187
+ "min-w-4 h-4 px-1 text-[11px] font-medium leading-none",
188
+ !contentColor && "text-m3-on-error",
189
+ )
190
+ : "inline-block w-1.5 h-1.5",
191
+ className,
192
+ )}
193
+ style={colorStyle}
194
+ // biome-ignore lint/suspicious/noExplicitAny: spread safe subset of HTML attrs
195
+ {...(props as any)}
196
+ >
197
+ {hasContent && label}
198
+ </m.span>
199
+ </LazyMotion>
200
+ );
201
+ },
202
+ );
203
+
204
+ BadgeImpl.displayName = "Badge";
205
+
206
+ /**
207
+ * MD3 Expressive Badge — dynamic status indicator.
208
+ *
209
+ * @example
210
+ * ```tsx
211
+ * // Small dot badge (no content) — decorative
212
+ * <Badge />
213
+ *
214
+ * // Large badge with number (truncated at max)
215
+ * <Badge max={99}>150</Badge>
216
+ * // → displays "99+"
217
+ *
218
+ * // Large badge with text label
219
+ * <Badge>NEW</Badge>
220
+ *
221
+ * // Custom colors
222
+ * <Badge containerColor="#6750A4" contentColor="#FFFFFF">3</Badge>
223
+ * ```
224
+ *
225
+ * @see https://m3.material.io/components/badge/overview
226
+ */
227
+ export const Badge = React.memo(BadgeImpl);
228
+
229
+ // ─────────────────────────────────────────────────────────────────────────────
230
+ // BadgedBox
231
+ // ─────────────────────────────────────────────────────────────────────────────
232
+
233
+ /**
234
+ * MD3 BadgedBox — positions a Badge at the top-trailing corner of an anchor.
235
+ *
236
+ * Implements MD3 offset specs from Badge.kt:
237
+ * - Small badge (dot): `BadgeOffset = 6dp` → translate(50%, -50%)
238
+ * - Large badge (text): `BadgeWithContentHorizontalOffset = 12dp` / `VerticalOffset = 14dp`
239
+ * → translate(35%, -35%)
240
+ *
241
+ * Auto-detects badge size by inspecting the badge element's children prop,
242
+ * or accepts an explicit `badgeSize` override.
243
+ *
244
+ * @example
245
+ * ```tsx
246
+ * // Small dot on mail icon
247
+ * <BadgedBox badge={<Badge />}>
248
+ * <Icon name="mail" />
249
+ * </BadgedBox>
250
+ *
251
+ * // Count badge on notification icon
252
+ * <BadgedBox badge={<Badge max={99}>{count}</Badge>}>
253
+ * <Icon name="notifications" />
254
+ * </BadgedBox>
255
+ * ```
256
+ */
257
+ export function BadgedBox({
258
+ badge,
259
+ children,
260
+ className,
261
+ badgeSize,
262
+ }: BadgedBoxProps) {
263
+ const isLarge = badgeSize
264
+ ? badgeSize === "large"
265
+ : detectBadgeHasContent(badge);
266
+
267
+ const badgePositionClass = isLarge
268
+ ? "translate-x-[35%] -translate-y-[35%]"
269
+ : "translate-x-[50%] -translate-y-[50%]";
270
+
271
+ return (
272
+ <span className={cn("relative inline-flex", className)}>
273
+ {children}
274
+ <span
275
+ className={cn("absolute right-0 top-0", badgePositionClass)}
276
+ aria-hidden="true"
277
+ >
278
+ <AnimatePresence mode="wait">{badge}</AnimatePresence>
279
+ </span>
280
+ </span>
281
+ );
282
+ }
@@ -0,0 +1,71 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { Button } from "./button";
4
+ import { ButtonGroup } from "./button-group";
5
+
6
+ describe("ButtonGroup Component", () => {
7
+ it("renders children correctly", () => {
8
+ render(
9
+ <ButtonGroup>
10
+ <Button>One</Button>
11
+ <Button>Two</Button>
12
+ </ButtonGroup>,
13
+ );
14
+ expect(screen.getByText("One")).toBeInTheDocument();
15
+ expect(screen.getByText("Two")).toBeInTheDocument();
16
+ });
17
+
18
+ it("applies gap-2 for standard variant by default", () => {
19
+ const { container } = render(
20
+ <ButtonGroup>
21
+ <Button>One</Button>
22
+ <Button>Two</Button>
23
+ </ButtonGroup>,
24
+ );
25
+ const fieldset = container.querySelector("fieldset");
26
+ expect(fieldset).toHaveClass("gap-2");
27
+ });
28
+
29
+ it("applies gap-0.5 for connected variant", () => {
30
+ const { container } = render(
31
+ <ButtonGroup variant="connected">
32
+ <Button>One</Button>
33
+ <Button>Two</Button>
34
+ </ButtonGroup>,
35
+ );
36
+ const fieldset = container.querySelector("fieldset");
37
+ expect(fieldset).toHaveClass("gap-0.5");
38
+ });
39
+
40
+ it("passes size prop to children to enforce uniformity", () => {
41
+ const { container } = render(
42
+ <ButtonGroup size="lg">
43
+ {/* Even if child specifies sm, the group's lg should override or inject */}
44
+ <Button size="sm">One</Button>
45
+ <Button>Two</Button>
46
+ </ButtonGroup>,
47
+ );
48
+
49
+ const buttons = container.querySelectorAll("button");
50
+ // SIZE_PADDING_MAP["lg"] is "3rem" in button.tsx (roughly corresponding to h-14/px-6 class)
51
+ // We can check if the inner-radius variable is correctly injected (lg -> 16px)
52
+ expect(buttons[0]).toHaveStyle({ "--m3-inner-rad": "16px" });
53
+ expect(buttons[1]).toHaveStyle({ "--m3-inner-rad": "16px" });
54
+ });
55
+
56
+ it("applies morphing transition style to connected buttons by default", () => {
57
+ const { container } = render(
58
+ <ButtonGroup variant="connected">
59
+ <Button>One</Button>
60
+ <Button>Two</Button>
61
+ </ButtonGroup>,
62
+ );
63
+
64
+ const buttons = container.querySelectorAll("button");
65
+ // Check that the custom transition is injected via style
66
+ const expectedTransition =
67
+ "border-top-left-radius 0.25s cubic-bezier(0.2, 0, 0, 1), border-top-right-radius 0.25s cubic-bezier(0.2, 0, 0, 1), border-bottom-right-radius 0.25s cubic-bezier(0.2, 0, 0, 1), border-bottom-left-radius 0.25s cubic-bezier(0.2, 0, 0, 1), padding 0.2s cubic-bezier(0.2, 0, 0, 1), flex 0.2s cubic-bezier(0.2, 0, 0, 1)";
68
+ expect(buttons[0]).toHaveStyle({ transition: expectedTransition });
69
+ expect(buttons[1]).toHaveStyle({ transition: expectedTransition });
70
+ });
71
+ });