@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,494 @@
1
+ /**
2
+ * @file fab-menu.test.tsx
3
+ *
4
+ * Comprehensive test suite for FABMenu, ToggleFAB, FABMenuItem components.
5
+ * Uses Vitest + @testing-library/react.
6
+ */
7
+
8
+ import { fireEvent, render, screen } from "@testing-library/react";
9
+ import { describe, expect, it, vi } from "vitest";
10
+ import { FABMenu, FABMenuItem, ToggleFAB } from "./fab-menu";
11
+
12
+ // ── Test fixtures ─────────────────────────────────────────────────────────────
13
+
14
+ const AddIcon = () => (
15
+ <svg data-testid="add-icon" aria-hidden="true" viewBox="0 0 24 24" />
16
+ );
17
+ const CloseIcon = () => (
18
+ <svg data-testid="close-icon" aria-hidden="true" viewBox="0 0 24 24" />
19
+ );
20
+ const ShareIcon = () => (
21
+ <svg data-testid="share-icon" aria-hidden="true" viewBox="0 0 24 24" />
22
+ );
23
+ const EditIcon = () => (
24
+ <svg data-testid="edit-icon" aria-hidden="true" viewBox="0 0 24 24" />
25
+ );
26
+
27
+ const defaultItems = [
28
+ {
29
+ id: "share",
30
+ label: "Share",
31
+ icon: <ShareIcon />,
32
+ onClick: vi.fn(),
33
+ },
34
+ {
35
+ id: "edit",
36
+ label: "Edit",
37
+ icon: <EditIcon />,
38
+ onClick: vi.fn(),
39
+ },
40
+ ];
41
+
42
+ const makeItems = (count: number) =>
43
+ Array.from({ length: count }, (_, i) => ({
44
+ id: `item-${i}`,
45
+ label: `Item ${i + 1}`,
46
+ icon: <ShareIcon />,
47
+ onClick: vi.fn(),
48
+ }));
49
+
50
+ // ── FABMenu tests ─────────────────────────────────────────────────────────────
51
+
52
+ describe("FABMenu", () => {
53
+ // ── Rendering ───────────────────────────────────────────────────────────
54
+
55
+ it("renders ToggleFAB in closed state", () => {
56
+ render(
57
+ <FABMenu
58
+ expanded={false}
59
+ onToggle={vi.fn()}
60
+ items={defaultItems}
61
+ aria-label="Test actions"
62
+ />,
63
+ );
64
+ // When aria-label is passed, ToggleFAB inherits it as its accessible name
65
+ const fab = screen.getByRole("button", { name: /test actions/i });
66
+ expect(fab).toBeInTheDocument();
67
+ expect(fab).toHaveAttribute("aria-expanded", "false");
68
+ });
69
+
70
+ it("does not render menu items when closed", () => {
71
+ render(
72
+ <FABMenu expanded={false} onToggle={vi.fn()} items={defaultItems} />,
73
+ );
74
+ expect(screen.queryByRole("menu")).not.toBeInTheDocument();
75
+ expect(screen.queryByRole("menuitem")).not.toBeInTheDocument();
76
+ });
77
+
78
+ it("renders menu items when expanded", () => {
79
+ render(<FABMenu expanded={true} onToggle={vi.fn()} items={defaultItems} />);
80
+ expect(screen.getByRole("menu")).toBeInTheDocument();
81
+ const menuItems = screen.getAllByRole("menuitem");
82
+ expect(menuItems).toHaveLength(2);
83
+ });
84
+
85
+ it("renders 2 items", () => {
86
+ render(<FABMenu expanded={true} onToggle={vi.fn()} items={makeItems(2)} />);
87
+ expect(screen.getAllByRole("menuitem")).toHaveLength(2);
88
+ });
89
+
90
+ it("renders 6 items", () => {
91
+ render(<FABMenu expanded={true} onToggle={vi.fn()} items={makeItems(6)} />);
92
+ expect(screen.getAllByRole("menuitem")).toHaveLength(6);
93
+ });
94
+
95
+ // ── Interaction ─────────────────────────────────────────────────────────
96
+
97
+ it("calls onToggle(true) when FAB is clicked while closed", () => {
98
+ const onToggle = vi.fn();
99
+ render(
100
+ <FABMenu expanded={false} onToggle={onToggle} items={defaultItems} />,
101
+ );
102
+ const fab = screen.getByRole("button");
103
+ fireEvent.click(fab);
104
+ expect(onToggle).toHaveBeenCalledWith(true);
105
+ });
106
+
107
+ it("calls onToggle(false) when FAB is clicked while open", () => {
108
+ const onToggle = vi.fn();
109
+ render(
110
+ <FABMenu expanded={true} onToggle={onToggle} items={defaultItems} />,
111
+ );
112
+ const fab = screen.getByRole("button");
113
+ fireEvent.click(fab);
114
+ expect(onToggle).toHaveBeenCalledWith(false);
115
+ });
116
+
117
+ it("closes menu when Escape key is pressed", () => {
118
+ const onToggle = vi.fn();
119
+ render(
120
+ <FABMenu expanded={true} onToggle={onToggle} items={defaultItems} />,
121
+ );
122
+ const container = screen.getByRole("group");
123
+ fireEvent.keyDown(container, { key: "Escape" });
124
+ expect(onToggle).toHaveBeenCalledWith(false);
125
+ });
126
+
127
+ it("calls item onClick and closes menu when item is clicked", () => {
128
+ const onToggle = vi.fn();
129
+ const handleClick = vi.fn();
130
+ render(
131
+ <FABMenu
132
+ expanded={true}
133
+ onToggle={onToggle}
134
+ items={[
135
+ {
136
+ id: "test",
137
+ label: "Test",
138
+ icon: <ShareIcon />,
139
+ onClick: handleClick,
140
+ },
141
+ ]}
142
+ />,
143
+ );
144
+ const item = screen.getByRole("menuitem");
145
+ fireEvent.click(item);
146
+ expect(handleClick).toHaveBeenCalledTimes(1);
147
+ expect(onToggle).toHaveBeenCalledWith(false);
148
+ });
149
+
150
+ it("does not call onClick on disabled items", () => {
151
+ const onToggle = vi.fn();
152
+ const handleClick = vi.fn();
153
+ render(
154
+ <FABMenu
155
+ expanded={true}
156
+ onToggle={onToggle}
157
+ items={[
158
+ {
159
+ id: "test",
160
+ label: "Disabled",
161
+ icon: <ShareIcon />,
162
+ onClick: handleClick,
163
+ disabled: true,
164
+ },
165
+ ]}
166
+ />,
167
+ );
168
+ const item = screen.getByRole("menuitem");
169
+ fireEvent.click(item);
170
+ expect(handleClick).not.toHaveBeenCalled();
171
+ expect(onToggle).not.toHaveBeenCalled();
172
+ });
173
+
174
+ // ── Accessibility ───────────────────────────────────────────────────────
175
+
176
+ it("has correct aria-expanded on ToggleFAB when closed", () => {
177
+ render(
178
+ <FABMenu expanded={false} onToggle={vi.fn()} items={defaultItems} />,
179
+ );
180
+ expect(screen.getByRole("button")).toHaveAttribute(
181
+ "aria-expanded",
182
+ "false",
183
+ );
184
+ });
185
+
186
+ it("has correct aria-expanded on ToggleFAB when open", () => {
187
+ render(<FABMenu expanded={true} onToggle={vi.fn()} items={defaultItems} />);
188
+ expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
189
+ });
190
+
191
+ it("items have role='menuitem'", () => {
192
+ render(<FABMenu expanded={true} onToggle={vi.fn()} items={defaultItems} />);
193
+ const items = screen.getAllByRole("menuitem");
194
+ expect(items.length).toBeGreaterThan(0);
195
+ for (const item of items) {
196
+ expect(item).toHaveAttribute("role", "menuitem");
197
+ }
198
+ });
199
+
200
+ it("menu container has role='menu'", () => {
201
+ render(<FABMenu expanded={true} onToggle={vi.fn()} items={defaultItems} />);
202
+ expect(screen.getByRole("menu")).toBeInTheDocument();
203
+ });
204
+
205
+ it("group container has role='group'", () => {
206
+ render(
207
+ <FABMenu expanded={false} onToggle={vi.fn()} items={defaultItems} />,
208
+ );
209
+ expect(screen.getByRole("group")).toBeInTheDocument();
210
+ });
211
+
212
+ it("ToggleFAB has aria-haspopup='menu'", () => {
213
+ render(
214
+ <FABMenu expanded={false} onToggle={vi.fn()} items={defaultItems} />,
215
+ );
216
+ expect(screen.getByRole("button")).toHaveAttribute("aria-haspopup", "menu");
217
+ });
218
+
219
+ it("supports keyboard navigation with Arrow Down", () => {
220
+ render(<FABMenu expanded={true} onToggle={vi.fn()} items={makeItems(3)} />);
221
+ const container = screen.getByRole("group");
222
+ fireEvent.keyDown(container, { key: "ArrowDown" });
223
+ // Should not throw
224
+ });
225
+
226
+ it("supports keyboard navigation with Arrow Up", () => {
227
+ render(<FABMenu expanded={true} onToggle={vi.fn()} items={makeItems(3)} />);
228
+ const container = screen.getByRole("group");
229
+ fireEvent.keyDown(container, { key: "ArrowUp" });
230
+ // Should not throw
231
+ });
232
+
233
+ // ── Variant tests ───────────────────────────────────────────────────────
234
+
235
+ it("applies primary color variant by default", () => {
236
+ render(
237
+ <FABMenu expanded={false} onToggle={vi.fn()} items={defaultItems} />,
238
+ );
239
+ const fab = screen.getByRole("button");
240
+ // Primary container background class
241
+ expect(fab.className).toMatch(/m3-primary/);
242
+ });
243
+
244
+ it("applies secondary color variant when specified", () => {
245
+ render(
246
+ <FABMenu
247
+ expanded={false}
248
+ onToggle={vi.fn()}
249
+ items={defaultItems}
250
+ colorVariant="secondary"
251
+ />,
252
+ );
253
+ const fab = screen.getByRole("button");
254
+ expect(fab.className).toMatch(/m3-secondary/);
255
+ });
256
+
257
+ it("applies tertiary color variant when specified", () => {
258
+ render(
259
+ <FABMenu
260
+ expanded={false}
261
+ onToggle={vi.fn()}
262
+ items={defaultItems}
263
+ colorVariant="tertiary"
264
+ />,
265
+ );
266
+ const fab = screen.getByRole("button");
267
+ expect(fab.className).toMatch(/m3-tertiary/);
268
+ });
269
+
270
+ // ── Size tests ──────────────────────────────────────────────────────────
271
+
272
+ it("renders at baseline FAB size", () => {
273
+ render(
274
+ <FABMenu
275
+ expanded={false}
276
+ onToggle={vi.fn()}
277
+ items={defaultItems}
278
+ fabSize="baseline"
279
+ />,
280
+ );
281
+ const fab = screen.getByRole("button");
282
+ // h-14 w-14 for baseline
283
+ expect(fab.className).toContain("h-14");
284
+ expect(fab.className).toContain("w-14");
285
+ });
286
+
287
+ it("renders at medium FAB size", () => {
288
+ render(
289
+ <FABMenu
290
+ expanded={false}
291
+ onToggle={vi.fn()}
292
+ items={defaultItems}
293
+ fabSize="medium"
294
+ />,
295
+ );
296
+ const fab = screen.getByRole("button");
297
+ expect(fab.className).toContain("h-20");
298
+ expect(fab.className).toContain("w-20");
299
+ });
300
+
301
+ it("renders at large FAB size", () => {
302
+ render(
303
+ <FABMenu
304
+ expanded={false}
305
+ onToggle={vi.fn()}
306
+ items={defaultItems}
307
+ fabSize="large"
308
+ />,
309
+ );
310
+ const fab = screen.getByRole("button");
311
+ expect(fab.className).toContain("h-24");
312
+ expect(fab.className).toContain("w-24");
313
+ });
314
+ });
315
+
316
+ // ── ToggleFAB tests ───────────────────────────────────────────────────────────
317
+
318
+ describe("ToggleFAB", () => {
319
+ it("renders with aria-expanded=false initially", () => {
320
+ render(
321
+ <ToggleFAB
322
+ expanded={false}
323
+ onToggle={vi.fn()}
324
+ aria-label="Toggle"
325
+ icon={() => <AddIcon />}
326
+ />,
327
+ );
328
+ expect(screen.getByRole("button")).toHaveAttribute(
329
+ "aria-expanded",
330
+ "false",
331
+ );
332
+ });
333
+
334
+ it("renders with aria-expanded=true when expanded", () => {
335
+ render(
336
+ <ToggleFAB
337
+ expanded={true}
338
+ onToggle={vi.fn()}
339
+ aria-label="Toggle"
340
+ icon={() => <CloseIcon />}
341
+ />,
342
+ );
343
+ expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
344
+ });
345
+
346
+ it("calls onToggle with false when clicked while expanded", () => {
347
+ const onToggle = vi.fn();
348
+ render(
349
+ <ToggleFAB
350
+ expanded={true}
351
+ onToggle={onToggle}
352
+ aria-label="Toggle"
353
+ icon={() => <CloseIcon />}
354
+ />,
355
+ );
356
+ fireEvent.click(screen.getByRole("button"));
357
+ expect(onToggle).toHaveBeenCalledWith(false);
358
+ });
359
+
360
+ it("calls onToggle with true when clicked while collapsed", () => {
361
+ const onToggle = vi.fn();
362
+ render(
363
+ <ToggleFAB
364
+ expanded={false}
365
+ onToggle={onToggle}
366
+ aria-label="Toggle"
367
+ icon={() => <AddIcon />}
368
+ />,
369
+ );
370
+ fireEvent.click(screen.getByRole("button"));
371
+ expect(onToggle).toHaveBeenCalledWith(true);
372
+ });
373
+
374
+ it("accepts custom icon render prop", () => {
375
+ const mockIcon = vi.fn(() => <AddIcon />);
376
+ render(
377
+ <ToggleFAB
378
+ expanded={false}
379
+ onToggle={vi.fn()}
380
+ aria-label="Toggle"
381
+ icon={mockIcon}
382
+ />,
383
+ );
384
+ expect(mockIcon).toHaveBeenCalled();
385
+ // Icon render prop receives progress value (0 when collapsed)
386
+ expect(mockIcon).toHaveBeenCalledWith(expect.any(Number));
387
+ });
388
+
389
+ it("has aria-haspopup='menu'", () => {
390
+ render(
391
+ <ToggleFAB
392
+ expanded={false}
393
+ onToggle={vi.fn()}
394
+ aria-label="Toggle"
395
+ icon={() => <AddIcon />}
396
+ />,
397
+ );
398
+ expect(screen.getByRole("button")).toHaveAttribute("aria-haspopup", "menu");
399
+ });
400
+ });
401
+
402
+ // ── FABMenuItem tests ─────────────────────────────────────────────────────────
403
+
404
+ describe("FABMenuItem", () => {
405
+ it("renders icon and label", () => {
406
+ render(
407
+ <FABMenuItem icon={<ShareIcon />} label="Share" onClick={vi.fn()} />,
408
+ );
409
+ expect(screen.getByText("Share")).toBeInTheDocument();
410
+ expect(screen.getByTestId("share-icon")).toBeInTheDocument();
411
+ });
412
+
413
+ it("renders icon-only when no label provided", () => {
414
+ render(<FABMenuItem icon={<ShareIcon />} onClick={vi.fn()} />);
415
+ expect(screen.getByTestId("share-icon")).toBeInTheDocument();
416
+ // No text content other than icon
417
+ const item = screen.getByRole("menuitem");
418
+ expect(item.textContent).toBe("");
419
+ });
420
+
421
+ it("calls onClick when clicked", () => {
422
+ const onClick = vi.fn();
423
+ render(
424
+ <FABMenuItem icon={<ShareIcon />} label="Share" onClick={onClick} />,
425
+ );
426
+ fireEvent.click(screen.getByRole("menuitem"));
427
+ expect(onClick).toHaveBeenCalledTimes(1);
428
+ });
429
+
430
+ it("has aria-disabled when disabled", () => {
431
+ render(
432
+ <FABMenuItem
433
+ icon={<ShareIcon />}
434
+ label="Disabled"
435
+ onClick={vi.fn()}
436
+ disabled
437
+ />,
438
+ );
439
+ expect(screen.getByRole("menuitem")).toHaveAttribute(
440
+ "aria-disabled",
441
+ "true",
442
+ );
443
+ });
444
+
445
+ it("does not call onClick when disabled", () => {
446
+ const onClick = vi.fn();
447
+ render(
448
+ <FABMenuItem
449
+ icon={<ShareIcon />}
450
+ label="Disabled"
451
+ onClick={onClick}
452
+ disabled
453
+ />,
454
+ );
455
+ fireEvent.click(screen.getByRole("menuitem"));
456
+ expect(onClick).not.toHaveBeenCalled();
457
+ });
458
+
459
+ it("has role='menuitem'", () => {
460
+ render(<FABMenuItem icon={<ShareIcon />} label="Test" onClick={vi.fn()} />);
461
+ expect(screen.getByRole("menuitem")).toBeInTheDocument();
462
+ });
463
+
464
+ it("activates on Enter key", () => {
465
+ const onClick = vi.fn();
466
+ render(<FABMenuItem icon={<ShareIcon />} label="Test" onClick={onClick} />);
467
+ const item = screen.getByRole("menuitem");
468
+ fireEvent.keyDown(item, { key: "Enter" });
469
+ expect(onClick).toHaveBeenCalledTimes(1);
470
+ });
471
+
472
+ it("activates on Space key", () => {
473
+ const onClick = vi.fn();
474
+ render(<FABMenuItem icon={<ShareIcon />} label="Test" onClick={onClick} />);
475
+ const item = screen.getByRole("menuitem");
476
+ fireEvent.keyDown(item, { key: " " });
477
+ expect(onClick).toHaveBeenCalledTimes(1);
478
+ });
479
+
480
+ it("does not activate on Enter when disabled", () => {
481
+ const onClick = vi.fn();
482
+ render(
483
+ <FABMenuItem
484
+ icon={<ShareIcon />}
485
+ label="Disabled"
486
+ onClick={onClick}
487
+ disabled
488
+ />,
489
+ );
490
+ const item = screen.getByRole("menuitem");
491
+ fireEvent.keyDown(item, { key: "Enter" });
492
+ expect(onClick).not.toHaveBeenCalled();
493
+ });
494
+ });