@bug-on/md3-react 2.0.2 → 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 (296) hide show
  1. package/.turbo/turbo-build.log +33 -0
  2. package/CHANGELOG.md +55 -0
  3. package/dist/index.css +23 -0
  4. package/dist/index.css.d.ts +2 -0
  5. package/dist/index.d.mts +6127 -0
  6. package/dist/index.d.ts +6127 -69
  7. package/dist/index.js +2536 -665
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2443 -603
  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/typography.css.d.ts +2 -0
  14. package/package.json +23 -19
  15. package/scripts/copy-assets.js +82 -0
  16. package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
  17. package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  18. package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  19. package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  20. package/src/assets/loading-indicator.svg +19 -0
  21. package/src/assets/material-symbols-cdn.css +65 -0
  22. package/src/assets/material-symbols-self-hosted.css +90 -0
  23. package/src/css.d.ts +20 -0
  24. package/{dist/hooks/index.d.ts → src/hooks/index.ts} +1 -0
  25. package/src/hooks/useClickOutside.ts +37 -0
  26. package/src/hooks/useMediaQuery.ts +28 -0
  27. package/src/hooks/useRipple.ts +88 -0
  28. package/src/index.css +23 -0
  29. package/src/index.ts +349 -0
  30. package/src/lib/material-symbols-preconnect.tsx +82 -0
  31. package/src/lib/theme-utils.ts +180 -0
  32. package/src/lib/utils.ts +6 -0
  33. package/src/test/button.test.tsx +59 -0
  34. package/src/test/icon.test.tsx +91 -0
  35. package/src/test/loading-indicator.test.tsx +128 -0
  36. package/src/test/progress-indicator.test.tsx +306 -0
  37. package/src/test/setup.ts +80 -0
  38. package/src/test/typography.test.tsx +206 -0
  39. package/src/types/index.ts +7 -0
  40. package/src/types/md3.ts +31 -0
  41. package/src/ui/Text.tsx +60 -0
  42. package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
  43. package/src/ui/app-bar/app-bar-column.tsx +99 -0
  44. package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
  45. package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
  46. package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
  47. package/src/ui/app-bar/app-bar-row.tsx +104 -0
  48. package/src/ui/app-bar/app-bar.test.tsx +87 -0
  49. package/src/ui/app-bar/app-bar.tokens.ts +223 -0
  50. package/src/ui/app-bar/app-bar.types.ts +441 -0
  51. package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
  52. package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
  53. package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
  54. package/src/ui/app-bar/docked-toolbar.tsx +54 -0
  55. package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
  56. package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
  57. package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
  58. package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
  59. package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
  60. package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
  61. package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
  62. package/src/ui/app-bar/search-app-bar.tsx +176 -0
  63. package/src/ui/app-bar/search-view.tsx +227 -0
  64. package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
  65. package/src/ui/app-bar/small-app-bar.tsx +203 -0
  66. package/src/ui/badge.test.tsx +345 -0
  67. package/src/ui/badge.tsx +282 -0
  68. package/src/ui/button-group.test.tsx +71 -0
  69. package/src/ui/button-group.tsx +350 -0
  70. package/src/ui/button.test.tsx +297 -0
  71. package/src/ui/button.tsx +669 -0
  72. package/src/ui/card.test.tsx +187 -0
  73. package/src/ui/card.tsx +259 -0
  74. package/src/ui/checkbox.test.tsx +423 -0
  75. package/src/ui/checkbox.tsx +525 -0
  76. package/src/ui/chip.test.tsx +292 -0
  77. package/src/ui/chip.tsx +548 -0
  78. package/src/ui/code-block.tsx +219 -0
  79. package/src/ui/dialog.test.tsx +300 -0
  80. package/src/ui/dialog.tsx +384 -0
  81. package/src/ui/divider.test.tsx +314 -0
  82. package/src/ui/divider.tsx +412 -0
  83. package/src/ui/drawer.tsx +240 -0
  84. package/src/ui/fab-menu.test.tsx +494 -0
  85. package/src/ui/fab-menu.tsx +739 -0
  86. package/src/ui/fab.test.tsx +232 -0
  87. package/src/ui/fab.tsx +505 -0
  88. package/src/ui/icon-button.test.tsx +515 -0
  89. package/src/ui/icon-button.tsx +525 -0
  90. package/src/ui/icon.test.tsx +197 -0
  91. package/src/ui/icon.tsx +179 -0
  92. package/src/ui/loading-indicator.test.tsx +73 -0
  93. package/src/ui/loading-indicator.tsx +312 -0
  94. package/src/ui/menu/context-menu.tsx +275 -0
  95. package/src/ui/menu/index.ts +77 -0
  96. package/src/ui/menu/menu-animations.ts +102 -0
  97. package/src/ui/menu/menu-context.tsx +99 -0
  98. package/src/ui/menu/menu-divider.tsx +47 -0
  99. package/src/ui/menu/menu-group.tsx +200 -0
  100. package/src/ui/menu/menu-item.tsx +294 -0
  101. package/src/ui/menu/menu-tokens.ts +208 -0
  102. package/src/ui/menu/menu-types.ts +313 -0
  103. package/src/ui/menu/menu.test.tsx +624 -0
  104. package/src/ui/menu/menu.tsx +289 -0
  105. package/src/ui/menu/sub-menu.tsx +223 -0
  106. package/src/ui/menu/vertical-menu.tsx +382 -0
  107. package/src/ui/navigation-rail.test.tsx +404 -0
  108. package/src/ui/navigation-rail.tsx +604 -0
  109. package/src/ui/progress-indicator/circular.tsx +248 -0
  110. package/src/ui/progress-indicator/hooks.ts +51 -0
  111. package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
  112. package/src/ui/progress-indicator/linear-flat.tsx +83 -0
  113. package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
  114. package/src/ui/progress-indicator/linear.tsx +143 -0
  115. package/src/ui/progress-indicator/types.ts +158 -0
  116. package/src/ui/progress-indicator/utils.ts +73 -0
  117. package/src/ui/radio-button.test.tsx +407 -0
  118. package/src/ui/radio-button.tsx +551 -0
  119. package/src/ui/ripple.test.tsx +72 -0
  120. package/src/ui/ripple.tsx +234 -0
  121. package/src/ui/scroll-area.test.tsx +58 -0
  122. package/src/ui/scroll-area.tsx +139 -0
  123. package/src/ui/search/animated-placeholder.tsx +145 -0
  124. package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
  125. package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
  126. package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
  127. package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
  128. package/src/ui/search/index.ts +44 -0
  129. package/src/ui/search/search-bar.tsx +220 -0
  130. package/src/ui/search/search-context.tsx +42 -0
  131. package/src/ui/search/search-view-docked.tsx +194 -0
  132. package/src/ui/search/search-view-fullscreen.tsx +247 -0
  133. package/src/ui/search/search.test.tsx +233 -0
  134. package/src/ui/search/search.tokens.ts +134 -0
  135. package/src/ui/search/search.tsx +131 -0
  136. package/src/ui/search/search.types.ts +154 -0
  137. package/src/ui/search/trailing-action.tsx +49 -0
  138. package/src/ui/shared/constants.ts +122 -0
  139. package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
  140. package/src/ui/slider/hooks/useSliderMath.ts +195 -0
  141. package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
  142. package/src/ui/slider/range-slider.tsx +561 -0
  143. package/src/ui/slider/slider-thumb.tsx +379 -0
  144. package/src/ui/slider/slider-track.tsx +912 -0
  145. package/src/ui/slider/slider.tokens.ts +189 -0
  146. package/src/ui/slider/slider.tsx +259 -0
  147. package/src/ui/slider/slider.types.ts +288 -0
  148. package/src/ui/snackbar/index.ts +20 -0
  149. package/src/ui/snackbar/snackbar.test.tsx +338 -0
  150. package/src/ui/snackbar/snackbar.tsx +476 -0
  151. package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
  152. package/src/ui/switch/switch.stories.tsx +309 -0
  153. package/src/ui/switch/switch.test.tsx +243 -0
  154. package/src/ui/switch/switch.tokens.ts +89 -0
  155. package/src/ui/switch/switch.tsx +504 -0
  156. package/src/ui/switch/switch.types.ts +62 -0
  157. package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
  158. package/src/ui/tabs/tab.tsx +407 -0
  159. package/src/ui/tabs/tabs-content.tsx +89 -0
  160. package/src/ui/tabs/tabs-list.tsx +146 -0
  161. package/src/ui/tabs/tabs.test.tsx +290 -0
  162. package/src/ui/tabs/tabs.tokens.ts +121 -0
  163. package/src/ui/tabs/tabs.tsx +229 -0
  164. package/src/ui/tabs/tabs.types.ts +185 -0
  165. package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
  166. package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
  167. package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
  168. package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
  169. package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
  170. package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
  171. package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
  172. package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
  173. package/src/ui/text-field/text-field.test.tsx +454 -0
  174. package/src/ui/text-field/text-field.tokens.ts +104 -0
  175. package/src/ui/text-field/text-field.tsx +548 -0
  176. package/src/ui/text-field/text-field.types.ts +180 -0
  177. package/src/ui/theme-provider/index.tsx +190 -0
  178. package/src/ui/toc.test.tsx +108 -0
  179. package/src/ui/toc.tsx +172 -0
  180. package/src/ui/tooltip/plain-tooltip.tsx +63 -0
  181. package/src/ui/tooltip/rich-tooltip.tsx +94 -0
  182. package/src/ui/tooltip/tooltip-box.tsx +266 -0
  183. package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
  184. package/src/ui/tooltip/tooltip.tokens.ts +26 -0
  185. package/src/ui/tooltip/tooltip.types.ts +70 -0
  186. package/src/ui/tooltip/use-tooltip-position.ts +208 -0
  187. package/src/ui/tooltip/use-tooltip-state.ts +41 -0
  188. package/src/ui/typography/__tests__/typography.test.tsx +170 -0
  189. package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
  190. package/src/ui/typography/type-scale-tokens.ts +205 -0
  191. package/src/ui/typography/typography-key-tokens.ts +43 -0
  192. package/src/ui/typography/typography-tokens.ts +360 -0
  193. package/src/ui/typography/typography.css +22 -0
  194. package/src/ui/typography/typography.tsx +559 -0
  195. package/test-render.tsx +4 -0
  196. package/test-shadow.html +26 -0
  197. package/test_output.txt +164 -0
  198. package/test_output_v2.txt +5 -0
  199. package/tsconfig.build.json +10 -0
  200. package/tsconfig.json +18 -0
  201. package/tsup.config.ts +20 -0
  202. package/vitest.config.ts +11 -0
  203. package/dist/hooks/useMediaQuery.d.ts +0 -11
  204. package/dist/hooks/useRipple.d.ts +0 -26
  205. package/dist/lib/material-symbols-preconnect.d.ts +0 -42
  206. package/dist/lib/theme-utils.d.ts +0 -63
  207. package/dist/lib/utils.d.ts +0 -2
  208. package/dist/types/index.d.ts +0 -1
  209. package/dist/types/md3.d.ts +0 -14
  210. package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
  211. package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
  212. package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
  213. package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
  214. package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
  215. package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
  216. package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
  217. package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
  218. package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
  219. package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
  220. package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
  221. package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
  222. package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
  223. package/dist/ui/app-bar/search-view.d.ts +0 -54
  224. package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
  225. package/dist/ui/badge.d.ts +0 -125
  226. package/dist/ui/button-group.d.ts +0 -59
  227. package/dist/ui/button.d.ts +0 -148
  228. package/dist/ui/card.d.ts +0 -62
  229. package/dist/ui/checkbox.d.ts +0 -82
  230. package/dist/ui/chip.d.ts +0 -110
  231. package/dist/ui/code-block.d.ts +0 -14
  232. package/dist/ui/dialog.d.ts +0 -111
  233. package/dist/ui/divider.d.ts +0 -164
  234. package/dist/ui/drawer.d.ts +0 -39
  235. package/dist/ui/dropdown.d.ts +0 -29
  236. package/dist/ui/fab-menu.d.ts +0 -204
  237. package/dist/ui/fab.d.ts +0 -162
  238. package/dist/ui/icon-button.d.ts +0 -131
  239. package/dist/ui/icon.d.ts +0 -88
  240. package/dist/ui/loading-indicator.d.ts +0 -42
  241. package/dist/ui/navigation-rail.d.ts +0 -29
  242. package/dist/ui/progress-indicator/circular.d.ts +0 -3
  243. package/dist/ui/progress-indicator/hooks.d.ts +0 -3
  244. package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
  245. package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
  246. package/dist/ui/progress-indicator/linear.d.ts +0 -3
  247. package/dist/ui/progress-indicator/types.d.ts +0 -151
  248. package/dist/ui/progress-indicator/utils.d.ts +0 -3
  249. package/dist/ui/radio-button.d.ts +0 -106
  250. package/dist/ui/ripple.d.ts +0 -126
  251. package/dist/ui/scroll-area.d.ts +0 -27
  252. package/dist/ui/shared/constants.d.ts +0 -86
  253. package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
  254. package/dist/ui/slider/range-slider.d.ts +0 -47
  255. package/dist/ui/slider/slider-thumb.d.ts +0 -33
  256. package/dist/ui/slider/slider-track.d.ts +0 -25
  257. package/dist/ui/slider/slider.d.ts +0 -60
  258. package/dist/ui/slider/slider.tokens.d.ts +0 -151
  259. package/dist/ui/slider/slider.types.d.ts +0 -259
  260. package/dist/ui/snackbar/index.d.ts +0 -6
  261. package/dist/ui/snackbar/snackbar.d.ts +0 -197
  262. package/dist/ui/switch/switch.d.ts +0 -30
  263. package/dist/ui/switch/switch.stories.d.ts +0 -48
  264. package/dist/ui/switch/switch.tokens.d.ts +0 -67
  265. package/dist/ui/switch/switch.types.d.ts +0 -59
  266. package/dist/ui/tabs/tab.d.ts +0 -43
  267. package/dist/ui/tabs/tabs-content.d.ts +0 -36
  268. package/dist/ui/tabs/tabs-list.d.ts +0 -40
  269. package/dist/ui/tabs/tabs.d.ts +0 -60
  270. package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
  271. package/dist/ui/tabs/tabs.types.d.ts +0 -172
  272. package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
  273. package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
  274. package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
  275. package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
  276. package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
  277. package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
  278. package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
  279. package/dist/ui/text-field/text-field.d.ts +0 -49
  280. package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
  281. package/dist/ui/text-field/text-field.types.d.ts +0 -126
  282. package/dist/ui/theme-provider/index.d.ts +0 -48
  283. package/dist/ui/toc.d.ts +0 -80
  284. package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
  285. package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
  286. package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
  287. package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
  288. package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
  289. package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
  290. package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
  291. package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
  292. package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
  293. package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
  294. package/dist/ui/typography/typography-tokens.d.ts +0 -220
  295. package/dist/ui/typography/typography.d.ts +0 -265
  296. /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
+ });