@bug-on/md3-react 2.0.3 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/.turbo/turbo-build.log +42 -0
  2. package/CHANGELOG.md +69 -0
  3. package/dist/index.css +178 -0
  4. package/dist/index.css.d.ts +2 -0
  5. package/dist/index.d.mts +6135 -0
  6. package/dist/index.d.ts +6135 -71
  7. package/dist/index.js +1688 -631
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +1600 -564
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/material-symbols-cdn.css.d.ts +2 -0
  12. package/dist/material-symbols-self-hosted.css.d.ts +2 -0
  13. package/dist/plugin.d.mts +1 -0
  14. package/dist/plugin.d.ts +1 -0
  15. package/dist/plugin.js +13 -0
  16. package/dist/plugin.js.map +1 -0
  17. package/dist/plugin.mjs +3 -0
  18. package/dist/plugin.mjs.map +1 -0
  19. package/dist/typography.css.d.ts +2 -0
  20. package/package.json +28 -19
  21. package/scripts/copy-assets.js +115 -0
  22. package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
  23. package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  24. package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  25. package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  26. package/src/assets/loading-indicator.svg +19 -0
  27. package/src/assets/material-symbols-cdn.css +65 -0
  28. package/src/assets/material-symbols-self-hosted.css +90 -0
  29. package/src/css.d.ts +20 -0
  30. package/src/hooks/useClickOutside.ts +37 -0
  31. package/src/hooks/useMediaQuery.ts +28 -0
  32. package/src/hooks/useRipple.ts +88 -0
  33. package/src/index.css +23 -0
  34. package/src/index.ts +349 -0
  35. package/src/lib/material-symbols-preconnect.tsx +82 -0
  36. package/src/lib/theme-utils.ts +195 -0
  37. package/src/lib/utils.ts +6 -0
  38. package/src/plugin.ts +12 -0
  39. package/src/test/button.test.tsx +59 -0
  40. package/src/test/icon.test.tsx +91 -0
  41. package/src/test/loading-indicator.test.tsx +128 -0
  42. package/src/test/progress-indicator.test.tsx +306 -0
  43. package/src/test/setup.ts +80 -0
  44. package/src/test/typography.test.tsx +206 -0
  45. package/src/types/index.ts +7 -0
  46. package/src/types/md3.ts +31 -0
  47. package/src/ui/Text.tsx +60 -0
  48. package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
  49. package/src/ui/app-bar/app-bar-column.tsx +99 -0
  50. package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
  51. package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
  52. package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
  53. package/src/ui/app-bar/app-bar-row.tsx +104 -0
  54. package/src/ui/app-bar/app-bar.test.tsx +87 -0
  55. package/src/ui/app-bar/app-bar.tokens.ts +223 -0
  56. package/src/ui/app-bar/app-bar.types.ts +441 -0
  57. package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
  58. package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
  59. package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
  60. package/src/ui/app-bar/docked-toolbar.tsx +54 -0
  61. package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
  62. package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
  63. package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
  64. package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
  65. package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
  66. package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
  67. package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
  68. package/src/ui/app-bar/search-app-bar.tsx +176 -0
  69. package/src/ui/app-bar/search-view.tsx +227 -0
  70. package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
  71. package/src/ui/app-bar/small-app-bar.tsx +203 -0
  72. package/src/ui/badge.test.tsx +345 -0
  73. package/src/ui/badge.tsx +282 -0
  74. package/src/ui/button-group.test.tsx +71 -0
  75. package/src/ui/button-group.tsx +350 -0
  76. package/src/ui/button.test.tsx +306 -0
  77. package/src/ui/button.tsx +665 -0
  78. package/src/ui/card.test.tsx +187 -0
  79. package/src/ui/card.tsx +259 -0
  80. package/src/ui/checkbox.test.tsx +423 -0
  81. package/src/ui/checkbox.tsx +525 -0
  82. package/src/ui/chip.test.tsx +292 -0
  83. package/src/ui/chip.tsx +548 -0
  84. package/src/ui/code-block.tsx +219 -0
  85. package/src/ui/dialog.test.tsx +300 -0
  86. package/src/ui/dialog.tsx +384 -0
  87. package/src/ui/divider.test.tsx +314 -0
  88. package/src/ui/divider.tsx +412 -0
  89. package/src/ui/drawer.tsx +240 -0
  90. package/src/ui/fab-menu.test.tsx +494 -0
  91. package/src/ui/fab-menu.tsx +739 -0
  92. package/src/ui/fab.test.tsx +232 -0
  93. package/src/ui/fab.tsx +505 -0
  94. package/src/ui/icon-button.test.tsx +515 -0
  95. package/src/ui/icon-button.tsx +525 -0
  96. package/src/ui/icon.test.tsx +197 -0
  97. package/src/ui/icon.tsx +179 -0
  98. package/src/ui/loading-indicator.test.tsx +73 -0
  99. package/src/ui/loading-indicator.tsx +312 -0
  100. package/src/ui/menu/context-menu.tsx +275 -0
  101. package/src/ui/menu/index.ts +77 -0
  102. package/src/ui/menu/menu-animations.ts +102 -0
  103. package/src/ui/menu/menu-context.tsx +99 -0
  104. package/src/ui/menu/menu-divider.tsx +47 -0
  105. package/src/ui/menu/menu-group.tsx +200 -0
  106. package/src/ui/menu/menu-item.tsx +294 -0
  107. package/src/ui/menu/menu-tokens.ts +208 -0
  108. package/src/ui/menu/menu-types.ts +313 -0
  109. package/src/ui/menu/menu.test.tsx +624 -0
  110. package/src/ui/menu/menu.tsx +289 -0
  111. package/src/ui/menu/sub-menu.tsx +223 -0
  112. package/src/ui/menu/vertical-menu.tsx +382 -0
  113. package/src/ui/navigation-rail.test.tsx +404 -0
  114. package/src/ui/navigation-rail.tsx +607 -0
  115. package/src/ui/progress-indicator/circular.tsx +248 -0
  116. package/src/ui/progress-indicator/hooks.ts +51 -0
  117. package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
  118. package/src/ui/progress-indicator/linear-flat.tsx +83 -0
  119. package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
  120. package/src/ui/progress-indicator/linear.tsx +143 -0
  121. package/src/ui/progress-indicator/types.ts +158 -0
  122. package/src/ui/progress-indicator/utils.ts +73 -0
  123. package/src/ui/radio-button.test.tsx +407 -0
  124. package/src/ui/radio-button.tsx +551 -0
  125. package/src/ui/ripple.test.tsx +72 -0
  126. package/src/ui/ripple.tsx +234 -0
  127. package/src/ui/scroll-area.test.tsx +58 -0
  128. package/src/ui/scroll-area.tsx +139 -0
  129. package/src/ui/search/animated-placeholder.tsx +145 -0
  130. package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
  131. package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
  132. package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
  133. package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
  134. package/src/ui/search/index.ts +44 -0
  135. package/src/ui/search/search-bar.tsx +220 -0
  136. package/src/ui/search/search-context.tsx +42 -0
  137. package/src/ui/search/search-view-docked.tsx +194 -0
  138. package/src/ui/search/search-view-fullscreen.tsx +247 -0
  139. package/src/ui/search/search.test.tsx +233 -0
  140. package/src/ui/search/search.tokens.ts +134 -0
  141. package/src/ui/search/search.tsx +131 -0
  142. package/src/ui/search/search.types.ts +154 -0
  143. package/src/ui/search/trailing-action.tsx +49 -0
  144. package/src/ui/shared/constants.ts +135 -0
  145. package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
  146. package/src/ui/slider/hooks/useSliderMath.ts +195 -0
  147. package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
  148. package/src/ui/slider/range-slider.tsx +561 -0
  149. package/src/ui/slider/slider-thumb.tsx +379 -0
  150. package/src/ui/slider/slider-track.tsx +912 -0
  151. package/src/ui/slider/slider.tokens.ts +189 -0
  152. package/src/ui/slider/slider.tsx +259 -0
  153. package/src/ui/slider/slider.types.ts +288 -0
  154. package/src/ui/snackbar/index.ts +20 -0
  155. package/src/ui/snackbar/snackbar.test.tsx +338 -0
  156. package/src/ui/snackbar/snackbar.tsx +476 -0
  157. package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
  158. package/src/ui/switch/switch.stories.tsx +309 -0
  159. package/src/ui/switch/switch.test.tsx +243 -0
  160. package/src/ui/switch/switch.tokens.ts +89 -0
  161. package/src/ui/switch/switch.tsx +504 -0
  162. package/src/ui/switch/switch.types.ts +62 -0
  163. package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
  164. package/src/ui/tabs/tab.tsx +407 -0
  165. package/src/ui/tabs/tabs-content.tsx +89 -0
  166. package/src/ui/tabs/tabs-list.tsx +146 -0
  167. package/src/ui/tabs/tabs.test.tsx +290 -0
  168. package/src/ui/tabs/tabs.tokens.ts +121 -0
  169. package/src/ui/tabs/tabs.tsx +229 -0
  170. package/src/ui/tabs/tabs.types.ts +185 -0
  171. package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
  172. package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
  173. package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
  174. package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
  175. package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
  176. package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
  177. package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
  178. package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
  179. package/src/ui/text-field/text-field.test.tsx +454 -0
  180. package/src/ui/text-field/text-field.tokens.ts +104 -0
  181. package/src/ui/text-field/text-field.tsx +548 -0
  182. package/src/ui/text-field/text-field.types.ts +180 -0
  183. package/src/ui/theme-provider/index.tsx +215 -0
  184. package/src/ui/toc.test.tsx +108 -0
  185. package/src/ui/toc.tsx +172 -0
  186. package/src/ui/tooltip/plain-tooltip.tsx +63 -0
  187. package/src/ui/tooltip/rich-tooltip.tsx +94 -0
  188. package/src/ui/tooltip/tooltip-box.tsx +266 -0
  189. package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
  190. package/src/ui/tooltip/tooltip.tokens.ts +26 -0
  191. package/src/ui/tooltip/tooltip.types.ts +70 -0
  192. package/src/ui/tooltip/use-tooltip-position.ts +208 -0
  193. package/src/ui/tooltip/use-tooltip-state.ts +41 -0
  194. package/src/ui/typography/__tests__/typography.test.tsx +170 -0
  195. package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
  196. package/src/ui/typography/type-scale-tokens.ts +205 -0
  197. package/src/ui/typography/typography-key-tokens.ts +43 -0
  198. package/src/ui/typography/typography-tokens.ts +360 -0
  199. package/src/ui/typography/typography.css +22 -0
  200. package/src/ui/typography/typography.tsx +559 -0
  201. package/test-render.tsx +4 -0
  202. package/test-shadow.html +26 -0
  203. package/test_output.txt +164 -0
  204. package/test_output_v2.txt +5 -0
  205. package/tsconfig.build.json +10 -0
  206. package/tsconfig.json +18 -0
  207. package/tsup.config.ts +20 -0
  208. package/vitest.config.ts +11 -0
  209. package/dist/hooks/useClickOutside.d.ts +0 -8
  210. package/dist/hooks/useMediaQuery.d.ts +0 -11
  211. package/dist/hooks/useRipple.d.ts +0 -26
  212. package/dist/lib/material-symbols-preconnect.d.ts +0 -42
  213. package/dist/lib/theme-utils.d.ts +0 -63
  214. package/dist/lib/utils.d.ts +0 -2
  215. package/dist/types/index.d.ts +0 -1
  216. package/dist/types/md3.d.ts +0 -14
  217. package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
  218. package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
  219. package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
  220. package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
  221. package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
  222. package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
  223. package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
  224. package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
  225. package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
  226. package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
  227. package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
  228. package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
  229. package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
  230. package/dist/ui/app-bar/search-view.d.ts +0 -54
  231. package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
  232. package/dist/ui/badge.d.ts +0 -125
  233. package/dist/ui/button-group.d.ts +0 -59
  234. package/dist/ui/button.d.ts +0 -148
  235. package/dist/ui/card.d.ts +0 -62
  236. package/dist/ui/checkbox.d.ts +0 -82
  237. package/dist/ui/chip.d.ts +0 -110
  238. package/dist/ui/code-block.d.ts +0 -14
  239. package/dist/ui/dialog.d.ts +0 -111
  240. package/dist/ui/divider.d.ts +0 -164
  241. package/dist/ui/drawer.d.ts +0 -39
  242. package/dist/ui/dropdown.d.ts +0 -29
  243. package/dist/ui/fab-menu.d.ts +0 -204
  244. package/dist/ui/fab.d.ts +0 -162
  245. package/dist/ui/icon-button.d.ts +0 -131
  246. package/dist/ui/icon.d.ts +0 -88
  247. package/dist/ui/loading-indicator.d.ts +0 -42
  248. package/dist/ui/navigation-rail.d.ts +0 -29
  249. package/dist/ui/progress-indicator/circular.d.ts +0 -3
  250. package/dist/ui/progress-indicator/hooks.d.ts +0 -3
  251. package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
  252. package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
  253. package/dist/ui/progress-indicator/linear.d.ts +0 -3
  254. package/dist/ui/progress-indicator/types.d.ts +0 -151
  255. package/dist/ui/progress-indicator/utils.d.ts +0 -3
  256. package/dist/ui/radio-button.d.ts +0 -106
  257. package/dist/ui/ripple.d.ts +0 -126
  258. package/dist/ui/scroll-area.d.ts +0 -27
  259. package/dist/ui/search/animated-placeholder.d.ts +0 -54
  260. package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
  261. package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
  262. package/dist/ui/search/index.d.ts +0 -27
  263. package/dist/ui/search/search-bar.d.ts +0 -32
  264. package/dist/ui/search/search-context.d.ts +0 -24
  265. package/dist/ui/search/search-view-docked.d.ts +0 -25
  266. package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
  267. package/dist/ui/search/search.d.ts +0 -50
  268. package/dist/ui/search/search.tokens.d.ts +0 -112
  269. package/dist/ui/search/search.types.d.ts +0 -131
  270. package/dist/ui/search/trailing-action.d.ts +0 -9
  271. package/dist/ui/shared/constants.d.ts +0 -86
  272. package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
  273. package/dist/ui/slider/range-slider.d.ts +0 -47
  274. package/dist/ui/slider/slider-thumb.d.ts +0 -33
  275. package/dist/ui/slider/slider-track.d.ts +0 -25
  276. package/dist/ui/slider/slider.d.ts +0 -60
  277. package/dist/ui/slider/slider.tokens.d.ts +0 -151
  278. package/dist/ui/slider/slider.types.d.ts +0 -259
  279. package/dist/ui/snackbar/index.d.ts +0 -6
  280. package/dist/ui/snackbar/snackbar.d.ts +0 -197
  281. package/dist/ui/switch/switch.d.ts +0 -30
  282. package/dist/ui/switch/switch.stories.d.ts +0 -48
  283. package/dist/ui/switch/switch.tokens.d.ts +0 -67
  284. package/dist/ui/switch/switch.types.d.ts +0 -59
  285. package/dist/ui/tabs/tab.d.ts +0 -43
  286. package/dist/ui/tabs/tabs-content.d.ts +0 -36
  287. package/dist/ui/tabs/tabs-list.d.ts +0 -40
  288. package/dist/ui/tabs/tabs.d.ts +0 -60
  289. package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
  290. package/dist/ui/tabs/tabs.types.d.ts +0 -172
  291. package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
  292. package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
  293. package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
  294. package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
  295. package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
  296. package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
  297. package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
  298. package/dist/ui/text-field/text-field.d.ts +0 -49
  299. package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
  300. package/dist/ui/text-field/text-field.types.d.ts +0 -126
  301. package/dist/ui/theme-provider/index.d.ts +0 -48
  302. package/dist/ui/toc.d.ts +0 -80
  303. package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
  304. package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
  305. package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
  306. package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
  307. package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
  308. package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
  309. package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
  310. package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
  311. package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
  312. package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
  313. package/dist/ui/typography/typography-tokens.d.ts +0 -220
  314. package/dist/ui/typography/typography.d.ts +0 -265
  315. /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
  316. /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
@@ -0,0 +1,404 @@
1
+ /**
2
+ * @file navigation-rail.test.tsx
3
+ *
4
+ * Test suite for the NavigationRail component.
5
+ */
6
+
7
+ import { fireEvent, render, screen } from "@testing-library/react";
8
+ import { describe, expect, it, vi } from "vitest";
9
+
10
+ import { NavigationRail, NavigationRailItem } from "./navigation-rail";
11
+
12
+ describe("NavigationRail & NavigationRailItem", () => {
13
+ it("renders the navigation rail with collapsed variant by default", () => {
14
+ render(
15
+ <NavigationRail>
16
+ <NavigationRailItem
17
+ selected
18
+ icon={<svg data-testid="icon1" />}
19
+ label="Home"
20
+ />
21
+ </NavigationRail>,
22
+ );
23
+
24
+ const nav = screen.getByRole("navigation");
25
+ expect(nav).toBeInTheDocument();
26
+ expect(nav).toHaveClass("w-24"); // default not narrow
27
+ expect(nav).toHaveClass("items-center"); // collapsed layout class
28
+ });
29
+
30
+ it("renders the navigation rail with expanded variant", () => {
31
+ render(
32
+ <NavigationRail variant="expanded">
33
+ <NavigationRailItem selected icon={<svg />} label="Home" />
34
+ </NavigationRail>,
35
+ );
36
+
37
+ const nav = screen.getByRole("navigation");
38
+ expect(nav).toHaveClass("min-w-[13.75rem]");
39
+ });
40
+
41
+ it("renders the modal variant only when open is true", () => {
42
+ const { rerender } = render(
43
+ <NavigationRail variant="modal" open={false}>
44
+ <NavigationRailItem selected icon={<svg />} label="Home" />
45
+ </NavigationRail>,
46
+ );
47
+
48
+ expect(screen.queryByRole("navigation")).not.toBeInTheDocument();
49
+
50
+ rerender(
51
+ <NavigationRail variant="modal" open={true}>
52
+ <NavigationRailItem selected icon={<svg />} label="Home" />
53
+ </NavigationRail>,
54
+ );
55
+
56
+ const nav = screen.getByRole("navigation");
57
+ expect(nav).toBeInTheDocument();
58
+ expect(nav).toHaveClass("fixed");
59
+ });
60
+
61
+ it("calls onClose when modal backdrop is clicked", () => {
62
+ const handleClose = vi.fn();
63
+ render(
64
+ <NavigationRail variant="modal" open={true} onClose={handleClose}>
65
+ <NavigationRailItem selected icon={<svg />} label="Home" />
66
+ </NavigationRail>,
67
+ );
68
+
69
+ // Backdrop is rendered just before the nav
70
+ const nav = screen.getByRole("navigation");
71
+ const backdrop = nav.previousSibling as HTMLElement;
72
+ expect(backdrop).toHaveClass("z-40");
73
+
74
+ fireEvent.click(backdrop);
75
+ expect(handleClose).toHaveBeenCalledTimes(1);
76
+ });
77
+
78
+ it("sets correct attributes for items", () => {
79
+ render(
80
+ <NavigationRail>
81
+ <NavigationRailItem
82
+ selected
83
+ icon={<svg />}
84
+ label="Home"
85
+ aria-label="Home custom"
86
+ />
87
+ <NavigationRailItem selected={false} icon={<svg />} label="Settings" />
88
+ <NavigationRailItem
89
+ selected={false}
90
+ disabled
91
+ icon={<svg />}
92
+ label="Admin"
93
+ />
94
+ </NavigationRail>,
95
+ );
96
+
97
+ const items = screen.getAllByRole("menuitem");
98
+ expect(items).toHaveLength(3);
99
+
100
+ const home = items[0];
101
+ expect(home).toHaveAttribute("aria-current", "page");
102
+ expect(home).toHaveAttribute("aria-label", "Home custom");
103
+
104
+ const settings = items[1];
105
+ expect(settings).not.toHaveAttribute("aria-current");
106
+ expect(settings).toHaveAttribute("aria-label", "Settings");
107
+
108
+ const admin = items[2];
109
+ expect(admin).toHaveAttribute("aria-disabled", "true");
110
+ expect(admin).toHaveClass("opacity-[0.38]");
111
+ });
112
+
113
+ it("calls onClick when an item is clicked", () => {
114
+ const handleClick = vi.fn();
115
+ render(
116
+ <NavigationRail>
117
+ <NavigationRailItem
118
+ selected={false}
119
+ icon={<svg />}
120
+ label="Home"
121
+ onClick={handleClick}
122
+ />
123
+ </NavigationRail>,
124
+ );
125
+
126
+ const item = screen.getByRole("menuitem");
127
+ fireEvent.click(item);
128
+ expect(handleClick).toHaveBeenCalledTimes(1);
129
+ });
130
+
131
+ it("prevents click when an item is disabled", () => {
132
+ const handleClick = vi.fn();
133
+ render(
134
+ <NavigationRail>
135
+ <NavigationRailItem
136
+ selected={false}
137
+ disabled
138
+ icon={<svg />}
139
+ label="Home"
140
+ onClick={handleClick}
141
+ />
142
+ </NavigationRail>,
143
+ );
144
+
145
+ const item = screen.getByRole("menuitem");
146
+ fireEvent.click(item);
147
+ expect(handleClick).not.toHaveBeenCalled();
148
+ });
149
+
150
+ it("initializes roving tabindex with first item", () => {
151
+ render(
152
+ <NavigationRail>
153
+ <NavigationRailItem selected={false} icon={<svg />} label="1" />
154
+ <NavigationRailItem selected={false} icon={<svg />} label="2" />
155
+ </NavigationRail>,
156
+ );
157
+
158
+ const items = screen.getAllByRole("menuitem");
159
+ expect(items[0]).toHaveAttribute("tabindex", "0");
160
+ expect(items[1]).toHaveAttribute("tabindex", "-1");
161
+ });
162
+
163
+ it("initializes roving tabindex with selected item", () => {
164
+ render(
165
+ <NavigationRail>
166
+ <NavigationRailItem selected={false} icon={<svg />} label="1" />
167
+ <NavigationRailItem selected icon={<svg />} label="2" />
168
+ </NavigationRail>,
169
+ );
170
+
171
+ const items = screen.getAllByRole("menuitem");
172
+ expect(items[0]).toHaveAttribute("tabindex", "-1");
173
+ expect(items[1]).toHaveAttribute("tabindex", "0");
174
+ });
175
+
176
+ it("does not include disabled items in roving tabindex on init", () => {
177
+ render(
178
+ <NavigationRail>
179
+ <NavigationRailItem
180
+ selected={false}
181
+ disabled
182
+ icon={<svg />}
183
+ label="1"
184
+ />
185
+ <NavigationRailItem selected={false} icon={<svg />} label="2" />
186
+ </NavigationRail>,
187
+ );
188
+
189
+ const items = screen.getAllByRole("menuitem");
190
+ expect(items[0]).toHaveAttribute("tabindex", "-1"); // disabled item retains its default -1 tabindex
191
+ expect(items[1]).toHaveAttribute("tabindex", "0");
192
+ });
193
+
194
+ it("moves focus down using ArrowDown", () => {
195
+ render(
196
+ <NavigationRail>
197
+ <NavigationRailItem selected icon={<svg />} label="1" />
198
+ <NavigationRailItem selected={false} icon={<svg />} label="2" />
199
+ </NavigationRail>,
200
+ );
201
+
202
+ const nav = screen.getByRole("navigation");
203
+ const items = screen.getAllByRole("menuitem");
204
+
205
+ items[0].focus();
206
+ expect(document.activeElement).toBe(items[0]);
207
+
208
+ fireEvent.keyDown(nav, { key: "ArrowDown" });
209
+ expect(document.activeElement).toBe(items[1]);
210
+ expect(items[1]).toHaveAttribute("tabindex", "0");
211
+ expect(items[0]).toHaveAttribute("tabindex", "-1");
212
+ });
213
+
214
+ it("wraps around focus using ArrowDown", () => {
215
+ render(
216
+ <NavigationRail>
217
+ <NavigationRailItem selected={false} icon={<svg />} label="1" />
218
+ <NavigationRailItem selected icon={<svg />} label="2" />
219
+ </NavigationRail>,
220
+ );
221
+
222
+ const nav = screen.getByRole("navigation");
223
+ const items = screen.getAllByRole("menuitem");
224
+
225
+ items[1].focus();
226
+ fireEvent.keyDown(nav, { key: "ArrowDown" });
227
+ expect(document.activeElement).toBe(items[0]);
228
+ });
229
+
230
+ it("moves focus using Home and End keys", () => {
231
+ render(
232
+ <NavigationRail>
233
+ <NavigationRailItem selected={false} icon={<svg />} label="1" />
234
+ <NavigationRailItem selected icon={<svg />} label="2" />
235
+ <NavigationRailItem selected={false} icon={<svg />} label="3" />
236
+ </NavigationRail>,
237
+ );
238
+
239
+ const nav = screen.getByRole("navigation");
240
+ const items = screen.getAllByRole("menuitem");
241
+
242
+ items[1].focus();
243
+ fireEvent.keyDown(nav, { key: "Home" });
244
+ expect(document.activeElement).toBe(items[0]);
245
+
246
+ fireEvent.keyDown(nav, { key: "End" });
247
+ expect(document.activeElement).toBe(items[2]);
248
+ });
249
+
250
+ it("triggers click on Enter and Space", () => {
251
+ const handleClick = vi.fn();
252
+ render(
253
+ <NavigationRail>
254
+ <NavigationRailItem
255
+ selected={false}
256
+ icon={<svg />}
257
+ label="1"
258
+ onClick={handleClick}
259
+ />
260
+ </NavigationRail>,
261
+ );
262
+
263
+ const nav = screen.getByRole("navigation");
264
+ const item = screen.getByRole("menuitem");
265
+
266
+ item.focus();
267
+ fireEvent.keyDown(nav, { key: "Enter" });
268
+ expect(handleClick).toHaveBeenCalledTimes(1);
269
+
270
+ fireEvent.keyDown(nav, { key: " " });
271
+ expect(handleClick).toHaveBeenCalledTimes(2);
272
+ });
273
+
274
+ it("renders badge when provided", () => {
275
+ render(
276
+ <NavigationRail>
277
+ <NavigationRailItem
278
+ selected={false}
279
+ icon={<svg />}
280
+ label="Notifications"
281
+ badge="3"
282
+ />
283
+ </NavigationRail>,
284
+ );
285
+
286
+ const item = screen.getByRole("menuitem");
287
+ expect(item).toHaveTextContent("3");
288
+ });
289
+
290
+ // ── labelVisibility tests ──────────────────────────────────────────────────
291
+
292
+ describe("labelVisibility", () => {
293
+ it('shows labels for all items when labelVisibility="labeled" (default)', () => {
294
+ render(
295
+ <NavigationRail labelVisibility="labeled">
296
+ <NavigationRailItem selected icon={<svg />} label="Home" />
297
+ <NavigationRailItem selected={false} icon={<svg />} label="Search" />
298
+ </NavigationRail>,
299
+ );
300
+
301
+ expect(screen.getByText("Home")).toBeInTheDocument();
302
+ expect(screen.getByText("Search")).toBeInTheDocument();
303
+ });
304
+
305
+ it('shows label only for active item when labelVisibility="auto"', () => {
306
+ render(
307
+ <NavigationRail labelVisibility="auto">
308
+ <NavigationRailItem selected icon={<svg />} label="Home" />
309
+ <NavigationRailItem selected={false} icon={<svg />} label="Search" />
310
+ </NavigationRail>,
311
+ );
312
+
313
+ expect(screen.getByText("Home")).toBeInTheDocument();
314
+ expect(screen.queryByText("Search")).not.toBeInTheDocument();
315
+ });
316
+
317
+ it('shows no labels when labelVisibility="unlabeled"', () => {
318
+ render(
319
+ <NavigationRail labelVisibility="unlabeled">
320
+ <NavigationRailItem selected icon={<svg />} label="Home" />
321
+ <NavigationRailItem selected={false} icon={<svg />} label="Search" />
322
+ </NavigationRail>,
323
+ );
324
+
325
+ expect(screen.queryByText("Home")).not.toBeInTheDocument();
326
+ expect(screen.queryByText("Search")).not.toBeInTheDocument();
327
+ });
328
+
329
+ it("always shows labels in expanded variant regardless of labelVisibility", () => {
330
+ render(
331
+ <NavigationRail variant="expanded" labelVisibility="unlabeled">
332
+ <NavigationRailItem selected icon={<svg />} label="Home" />
333
+ <NavigationRailItem selected={false} icon={<svg />} label="Search" />
334
+ </NavigationRail>,
335
+ );
336
+
337
+ // In expanded mode, labels are always shown
338
+ expect(screen.getByText("Home")).toBeInTheDocument();
339
+ expect(screen.getByText("Search")).toBeInTheDocument();
340
+ });
341
+ });
342
+
343
+ // ── Additional Props (xr, narrow, header, footer, fab) ─────────────────────
344
+
345
+ describe("additional layout and xr properties", () => {
346
+ it("renders header, footer, and fab elements", () => {
347
+ render(
348
+ <NavigationRail
349
+ header={<div data-testid="rail-header">Header</div>}
350
+ footer={<div data-testid="rail-footer">Footer</div>}
351
+ fab={
352
+ <button type="button" data-testid="rail-fab">
353
+ FAB
354
+ </button>
355
+ }
356
+ >
357
+ <NavigationRailItem selected icon={<svg />} label="Home" />
358
+ </NavigationRail>,
359
+ );
360
+
361
+ expect(screen.getByTestId("rail-header")).toBeInTheDocument();
362
+ expect(screen.getByTestId("rail-footer")).toBeInTheDocument();
363
+ expect(screen.getByTestId("rail-fab")).toBeInTheDocument();
364
+ });
365
+
366
+ it("applies narrow styling when narrow={true}", () => {
367
+ render(
368
+ <NavigationRail narrow>
369
+ <NavigationRailItem selected icon={<svg />} label="Home" />
370
+ </NavigationRail>,
371
+ );
372
+ const nav = screen.getByRole("navigation");
373
+ expect(nav).toHaveClass("w-20"); // narrow width
374
+ });
375
+
376
+ it("applies xr (spatial) styling when xr={true}", () => {
377
+ render(
378
+ <NavigationRail xr>
379
+ <NavigationRailItem selected icon={<svg />} label="Home" />
380
+ </NavigationRail>,
381
+ );
382
+ const nav = screen.getByRole("navigation");
383
+ expect(nav).toHaveClass("py-5", "rounded-[48px]", "bg-m3-surface");
384
+ });
385
+
386
+ it("renders spatial wrapper structurally when xr='spatialized'", () => {
387
+ render(
388
+ <NavigationRail
389
+ xr="spatialized"
390
+ fab={
391
+ <button type="button" data-testid="rail-fab">
392
+ FAB
393
+ </button>
394
+ }
395
+ >
396
+ <NavigationRailItem selected icon={<svg />} label="Home" />
397
+ </NavigationRail>,
398
+ );
399
+ expect(screen.getByTestId("rail-fab")).toBeInTheDocument();
400
+ const nav = screen.getByRole("navigation");
401
+ expect(nav).toBeInTheDocument();
402
+ });
403
+ });
404
+ });