@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,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
+ });