@etsoo/toolpad 1.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 (202) hide show
  1. package/.github/workflows/main.yml +49 -0
  2. package/.vscode/settings.json +3 -0
  3. package/LICENSE +21 -0
  4. package/README.md +11 -0
  5. package/build/src/Account/Account.d.ts +71 -0
  6. package/build/src/Account/Account.js +117 -0
  7. package/build/src/Account/Account.test.d.ts +1 -0
  8. package/build/src/Account/Account.test.js +35 -0
  9. package/build/src/Account/AccountPopoverFooter.d.ts +6 -0
  10. package/build/src/Account/AccountPopoverFooter.js +21 -0
  11. package/build/src/Account/AccountPopoverHeader.d.ts +6 -0
  12. package/build/src/Account/AccountPopoverHeader.js +15 -0
  13. package/build/src/Account/AccountPreview.d.ts +67 -0
  14. package/build/src/Account/AccountPreview.js +76 -0
  15. package/build/src/Account/AccountPreview.test.d.ts +1 -0
  16. package/build/src/Account/AccountPreview.test.js +47 -0
  17. package/build/src/Account/SignInButton.d.ts +2 -0
  18. package/build/src/Account/SignInButton.js +26 -0
  19. package/build/src/Account/SignOutButton.d.ts +3 -0
  20. package/build/src/Account/SignOutButton.js +28 -0
  21. package/build/src/Account/index.d.ts +6 -0
  22. package/build/src/Account/index.js +6 -0
  23. package/build/src/AppProvider/AppProvider.d.ts +98 -0
  24. package/build/src/AppProvider/AppProvider.js +4 -0
  25. package/build/src/AppProvider/AppProvider.test.d.ts +4 -0
  26. package/build/src/AppProvider/AppProvider.test.js +16 -0
  27. package/build/src/AppProvider/AppProviderComponent.d.ts +17 -0
  28. package/build/src/AppProvider/AppProviderComponent.js +120 -0
  29. package/build/src/AppProvider/AppThemeProvider.d.ts +12 -0
  30. package/build/src/AppProvider/AppThemeProvider.js +73 -0
  31. package/build/src/AppProvider/index.d.ts +1 -0
  32. package/build/src/AppProvider/index.js +1 -0
  33. package/build/src/DashboardLayout/DashboardLayout.d.ts +83 -0
  34. package/build/src/DashboardLayout/DashboardLayout.js +286 -0
  35. package/build/src/DashboardLayout/DashboardLayout.test.d.ts +1 -0
  36. package/build/src/DashboardLayout/DashboardLayout.test.js +291 -0
  37. package/build/src/DashboardLayout/DashboardSidebarSubNavigation.d.ts +16 -0
  38. package/build/src/DashboardLayout/DashboardSidebarSubNavigation.js +149 -0
  39. package/build/src/DashboardLayout/ThemeSwitcher.d.ts +5 -0
  40. package/build/src/DashboardLayout/ThemeSwitcher.js +40 -0
  41. package/build/src/DashboardLayout/ToolbarActions.d.ts +5 -0
  42. package/build/src/DashboardLayout/ToolbarActions.js +9 -0
  43. package/build/src/DashboardLayout/index.d.ts +2 -0
  44. package/build/src/DashboardLayout/index.js +3 -0
  45. package/build/src/DashboardLayout/utils.d.ts +8 -0
  46. package/build/src/DashboardLayout/utils.js +16 -0
  47. package/build/src/PageContainer/PageContainer.d.ts +66 -0
  48. package/build/src/PageContainer/PageContainer.js +97 -0
  49. package/build/src/PageContainer/PageContainer.test.d.ts +1 -0
  50. package/build/src/PageContainer/PageContainer.test.js +127 -0
  51. package/build/src/PageContainer/PageContainerToolbar.d.ts +16 -0
  52. package/build/src/PageContainer/PageContainerToolbar.js +24 -0
  53. package/build/src/PageContainer/PageContainerToolbar.test.d.ts +1 -0
  54. package/build/src/PageContainer/PageContainerToolbar.test.js +10 -0
  55. package/build/src/PageContainer/index.d.ts +2 -0
  56. package/build/src/PageContainer/index.js +2 -0
  57. package/build/src/index.d.ts +8 -0
  58. package/build/src/index.js +8 -0
  59. package/build/src/nextjs/AppProvider.d.ts +6 -0
  60. package/build/src/nextjs/AppProvider.js +16 -0
  61. package/build/src/nextjs/AppProvider.test.d.ts +1 -0
  62. package/build/src/nextjs/AppProvider.test.js +36 -0
  63. package/build/src/nextjs/AppProviderNextApp.d.ts +5 -0
  64. package/build/src/nextjs/AppProviderNextApp.js +27 -0
  65. package/build/src/nextjs/AppProviderNextPages.d.ts +5 -0
  66. package/build/src/nextjs/AppProviderNextPages.js +37 -0
  67. package/build/src/nextjs/index.d.ts +1 -0
  68. package/build/src/nextjs/index.js +1 -0
  69. package/build/src/persistence/codec.d.ts +47 -0
  70. package/build/src/persistence/codec.js +57 -0
  71. package/build/src/persistence/index.d.ts +1 -0
  72. package/build/src/persistence/index.js +1 -0
  73. package/build/src/persistence/useStorageState.d.ts +50 -0
  74. package/build/src/persistence/useStorageState.js +116 -0
  75. package/build/src/persistence/useStorageState.test.d.ts +1 -0
  76. package/build/src/persistence/useStorageState.test.js +61 -0
  77. package/build/src/react-router-dom/AppProvider.d.ts +6 -0
  78. package/build/src/react-router-dom/AppProvider.js +29 -0
  79. package/build/src/react-router-dom/AppProvider.test.d.ts +1 -0
  80. package/build/src/react-router-dom/AppProvider.test.js +12 -0
  81. package/build/src/react-router-dom/index.d.ts +1 -0
  82. package/build/src/react-router-dom/index.js +1 -0
  83. package/build/src/shared/Link.d.ts +8 -0
  84. package/build/src/shared/Link.js +19 -0
  85. package/build/src/shared/branding.d.ts +1 -0
  86. package/build/src/shared/branding.js +6 -0
  87. package/build/src/shared/components.d.ts +5 -0
  88. package/build/src/shared/components.js +23 -0
  89. package/build/src/shared/context.d.ts +12 -0
  90. package/build/src/shared/context.js +10 -0
  91. package/build/src/shared/locales/LocaleContext.d.ts +19 -0
  92. package/build/src/shared/locales/LocaleContext.js +18 -0
  93. package/build/src/shared/locales/en.d.ts +6 -0
  94. package/build/src/shared/locales/en.js +8 -0
  95. package/build/src/shared/navigation.d.ts +16 -0
  96. package/build/src/shared/navigation.js +129 -0
  97. package/build/src/useActivePage/index.d.ts +1 -0
  98. package/build/src/useActivePage/index.js +1 -0
  99. package/build/src/useActivePage/useActivePage.d.ts +11 -0
  100. package/build/src/useActivePage/useActivePage.js +48 -0
  101. package/build/src/useLocalStorageState/index.d.ts +1 -0
  102. package/build/src/useLocalStorageState/index.js +1 -0
  103. package/build/src/useLocalStorageState/useLocalStorageState.d.ts +2 -0
  104. package/build/src/useLocalStorageState/useLocalStorageState.js +14 -0
  105. package/build/src/useSessionStorageState/index.d.ts +1 -0
  106. package/build/src/useSessionStorageState/index.js +1 -0
  107. package/build/src/useSessionStorageState/useSessionStorageState.d.ts +2 -0
  108. package/build/src/useSessionStorageState/useSessionStorageState.js +14 -0
  109. package/build/src/utils/collections.d.ts +44 -0
  110. package/build/src/utils/collections.js +43 -0
  111. package/build/src/utils/describeConformance.d.ts +7 -0
  112. package/build/src/utils/describeConformance.js +3 -0
  113. package/build/src/utils/events.d.ts +27 -0
  114. package/build/src/utils/events.js +54 -0
  115. package/build/src/utils/hooks/index.d.ts +2 -0
  116. package/build/src/utils/hooks/index.js +2 -0
  117. package/build/src/utils/hooks/useBoolean.d.ts +11 -0
  118. package/build/src/utils/hooks/useBoolean.js +11 -0
  119. package/build/src/utils/hooks/useDebounced.d.ts +8 -0
  120. package/build/src/utils/hooks/useDebounced.js +28 -0
  121. package/build/src/utils/hooks/useDebouncedHandler.d.ts +12 -0
  122. package/build/src/utils/hooks/useDebouncedHandler.js +41 -0
  123. package/build/src/utils/hooks/useLatest.d.ts +6 -0
  124. package/build/src/utils/hooks/useLatest.js +9 -0
  125. package/build/src/utils/hooks/usePageTitle.d.ts +4 -0
  126. package/build/src/utils/hooks/usePageTitle.js +13 -0
  127. package/build/src/utils/hooks/useSsr.d.ts +4 -0
  128. package/build/src/utils/hooks/useSsr.js +16 -0
  129. package/build/src/utils/hooks/useStorageState.d.ts +10 -0
  130. package/build/src/utils/hooks/useStorageState.js +51 -0
  131. package/build/src/utils/warnOnce.d.ts +5 -0
  132. package/build/src/utils/warnOnce.js +11 -0
  133. package/build/vitest.setup.d.ts +1 -0
  134. package/build/vitest.setup.js +19 -0
  135. package/package.json +60 -0
  136. package/src/Account/Account.test.tsx +56 -0
  137. package/src/Account/Account.tsx +226 -0
  138. package/src/Account/AccountPopoverFooter.tsx +33 -0
  139. package/src/Account/AccountPopoverHeader.tsx +20 -0
  140. package/src/Account/AccountPreview.test.tsx +79 -0
  141. package/src/Account/AccountPreview.tsx +189 -0
  142. package/src/Account/SignInButton.tsx +38 -0
  143. package/src/Account/SignOutButton.tsx +44 -0
  144. package/src/Account/index.ts +6 -0
  145. package/src/AppProvider/AppProvider.test.tsx +25 -0
  146. package/src/AppProvider/AppProvider.tsx +116 -0
  147. package/src/AppProvider/AppProviderComponent.tsx +162 -0
  148. package/src/AppProvider/AppThemeProvider.tsx +177 -0
  149. package/src/AppProvider/index.ts +1 -0
  150. package/src/DashboardLayout/DashboardLayout.test.tsx +429 -0
  151. package/src/DashboardLayout/DashboardLayout.tsx +558 -0
  152. package/src/DashboardLayout/DashboardSidebarSubNavigation.tsx +301 -0
  153. package/src/DashboardLayout/ThemeSwitcher.tsx +81 -0
  154. package/src/DashboardLayout/ToolbarActions.tsx +11 -0
  155. package/src/DashboardLayout/index.ts +4 -0
  156. package/src/DashboardLayout/utils.ts +23 -0
  157. package/src/PageContainer/PageContainer.test.tsx +194 -0
  158. package/src/PageContainer/PageContainer.tsx +201 -0
  159. package/src/PageContainer/PageContainerToolbar.test.tsx +11 -0
  160. package/src/PageContainer/PageContainerToolbar.tsx +31 -0
  161. package/src/PageContainer/index.ts +2 -0
  162. package/src/index.ts +15 -0
  163. package/src/nextjs/AppProvider.test.tsx +48 -0
  164. package/src/nextjs/AppProvider.tsx +18 -0
  165. package/src/nextjs/AppProviderNextApp.tsx +37 -0
  166. package/src/nextjs/AppProviderNextPages.tsx +52 -0
  167. package/src/nextjs/index.tsx +1 -0
  168. package/src/persistence/codec.tsx +81 -0
  169. package/src/persistence/index.ts +1 -0
  170. package/src/persistence/useStorageState.test.tsx +101 -0
  171. package/src/persistence/useStorageState.tsx +244 -0
  172. package/src/react-router-dom/AppProvider.test.tsx +17 -0
  173. package/src/react-router-dom/AppProvider.tsx +40 -0
  174. package/src/react-router-dom/index.tsx +1 -0
  175. package/src/shared/Link.tsx +37 -0
  176. package/src/shared/branding.ts +7 -0
  177. package/src/shared/components.tsx +46 -0
  178. package/src/shared/context.ts +21 -0
  179. package/src/shared/locales/LocaleContext.tsx +44 -0
  180. package/src/shared/locales/en.tsx +9 -0
  181. package/src/shared/navigation.tsx +197 -0
  182. package/src/useActivePage/index.ts +1 -0
  183. package/src/useActivePage/useActivePage.ts +66 -0
  184. package/src/useLocalStorageState/index.ts +1 -0
  185. package/src/useLocalStorageState/useLocalStorageState.tsx +23 -0
  186. package/src/useSessionStorageState/index.tsx +1 -0
  187. package/src/useSessionStorageState/useSessionStorageState.tsx +23 -0
  188. package/src/utils/collections.ts +125 -0
  189. package/src/utils/describeConformance.ts +14 -0
  190. package/src/utils/events.ts +81 -0
  191. package/src/utils/hooks/index.ts +2 -0
  192. package/src/utils/hooks/useBoolean.ts +12 -0
  193. package/src/utils/hooks/useDebounced.ts +39 -0
  194. package/src/utils/hooks/useDebouncedHandler.ts +71 -0
  195. package/src/utils/hooks/useLatest.ts +16 -0
  196. package/src/utils/hooks/usePageTitle.ts +14 -0
  197. package/src/utils/hooks/useSsr.ts +24 -0
  198. package/src/utils/hooks/useStorageState.ts +130 -0
  199. package/src/utils/warnOnce.ts +12 -0
  200. package/tsconfig.json +19 -0
  201. package/vite.config.ts +27 -0
  202. package/vitest.setup.ts +22 -0
@@ -0,0 +1,177 @@
1
+ import * as React from "react";
2
+ import { PaletteMode, Theme, useMediaQuery } from "@mui/material";
3
+ import { ThemeProvider, useColorScheme } from "@mui/material/styles";
4
+ import InitColorSchemeScript from "@mui/material/InitColorSchemeScript";
5
+ import CssBaseline from "@mui/material/CssBaseline";
6
+ import invariant from "invariant";
7
+ import { useLocalStorageState } from "../useLocalStorageState";
8
+ import { PaletteModeContext } from "../shared/context";
9
+ import type { AppTheme } from "./AppProvider";
10
+
11
+ const COLOR_SCHEME_ATTRIBUTE = "data-toolpad-color-scheme";
12
+ const COLOR_SCHEME_STORAGE_KEY = "toolpad-color-scheme";
13
+ const MODE_STORAGE_KEY = "toolpad-mode";
14
+
15
+ function usePreferredMode(window?: Window) {
16
+ const prefersDarkMode = useMediaQuery(
17
+ "(prefers-color-scheme: dark)",
18
+ window && {
19
+ matchMedia: window.matchMedia
20
+ }
21
+ );
22
+ return prefersDarkMode ? "dark" : "light";
23
+ }
24
+
25
+ type ThemeMode = PaletteMode | "system";
26
+
27
+ type CssVarsTheme = Theme & { vars: Record<string, string> };
28
+
29
+ function isCssVarsTheme(theme: AppTheme): theme is CssVarsTheme {
30
+ return "vars" in theme;
31
+ }
32
+
33
+ interface LegacyThemeProviderProps {
34
+ children: React.ReactNode;
35
+ theme: AppTheme;
36
+ window?: Window;
37
+ }
38
+
39
+ /**
40
+ * Compatibility layer for classic v5 themes. It will handle state management for the theme switcher.
41
+ * In the v6 theme, this state management is handled by `useColorScheme`. But this hook will crash if
42
+ * not run under context with a css vars theme.
43
+ */
44
+ function LegacyThemeProvider(props: LegacyThemeProviderProps) {
45
+ const { children, theme, window: appWindow } = props;
46
+ invariant(
47
+ !isCssVarsTheme(theme),
48
+ "This provider only accepts legacy themes."
49
+ );
50
+
51
+ const isDualTheme = "light" in theme || "dark" in theme;
52
+
53
+ const preferredMode = usePreferredMode(appWindow);
54
+ const [userMode, setUserMode] = useLocalStorageState<ThemeMode>(
55
+ MODE_STORAGE_KEY,
56
+ "system"
57
+ );
58
+
59
+ const paletteMode =
60
+ !userMode || userMode === "system" ? preferredMode : userMode;
61
+ const dualAwareTheme = React.useMemo(
62
+ () =>
63
+ isDualTheme
64
+ ? theme[paletteMode === "dark" ? "dark" : "light"] ??
65
+ theme[paletteMode === "dark" ? "light" : "dark"]
66
+ : theme,
67
+ [isDualTheme, paletteMode, theme]
68
+ );
69
+
70
+ // The v5 shim, based on local state
71
+ const paletteModeContextValue = React.useMemo(
72
+ () => ({
73
+ paletteMode,
74
+ setPaletteMode: setUserMode,
75
+ isDualTheme
76
+ }),
77
+ [isDualTheme, paletteMode, setUserMode]
78
+ );
79
+
80
+ return (
81
+ <ThemeProvider theme={dualAwareTheme}>
82
+ <PaletteModeContext.Provider value={paletteModeContextValue}>
83
+ <CssBaseline enableColorScheme />
84
+ {children}
85
+ </PaletteModeContext.Provider>
86
+ </ThemeProvider>
87
+ );
88
+ }
89
+
90
+ interface CssVarsPaletteModeProviderProps {
91
+ children: React.ReactNode;
92
+ window?: Window;
93
+ }
94
+
95
+ function CssVarsPaletteModeProvider(props: CssVarsPaletteModeProviderProps) {
96
+ const { children, window: appWindow } = props;
97
+
98
+ const preferredMode = usePreferredMode(appWindow);
99
+ const { mode, setMode, allColorSchemes } = useColorScheme();
100
+
101
+ // The v6 API, based on `useColorScheme`
102
+ const paletteModeContextValue = React.useMemo(() => {
103
+ return {
104
+ paletteMode: !mode || mode === "system" ? preferredMode : mode,
105
+ setPaletteMode: setMode,
106
+ isDualTheme: allColorSchemes.length > 1
107
+ };
108
+ }, [allColorSchemes, mode, preferredMode, setMode]);
109
+
110
+ return (
111
+ <PaletteModeContext.Provider value={paletteModeContextValue}>
112
+ {children}
113
+ </PaletteModeContext.Provider>
114
+ );
115
+ }
116
+
117
+ interface CssVarsThemeProviderProps {
118
+ children: React.ReactNode;
119
+ theme: Theme;
120
+ window?: Window;
121
+ }
122
+
123
+ function CssVarsThemeProvider(props: CssVarsThemeProviderProps) {
124
+ const { children, theme, window: appWindow } = props;
125
+ invariant(
126
+ isCssVarsTheme(theme),
127
+ "This provider only accepts CSS vars themes."
128
+ );
129
+
130
+ return (
131
+ <ThemeProvider
132
+ theme={theme}
133
+ documentNode={appWindow?.document}
134
+ colorSchemeNode={appWindow?.document.documentElement}
135
+ disableNestedContext
136
+ colorSchemeStorageKey={COLOR_SCHEME_STORAGE_KEY}
137
+ modeStorageKey={MODE_STORAGE_KEY}
138
+ >
139
+ <InitColorSchemeScript
140
+ attribute={COLOR_SCHEME_ATTRIBUTE}
141
+ colorSchemeStorageKey={COLOR_SCHEME_STORAGE_KEY}
142
+ modeStorageKey={MODE_STORAGE_KEY}
143
+ />
144
+ <CssVarsPaletteModeProvider window={appWindow}>
145
+ <CssBaseline enableColorScheme />
146
+ {children}
147
+ </CssVarsPaletteModeProvider>
148
+ </ThemeProvider>
149
+ );
150
+ }
151
+
152
+ interface AppThemeProviderProps {
153
+ children: React.ReactNode;
154
+ theme: AppTheme;
155
+ window?: Window;
156
+ }
157
+
158
+ /**
159
+ * @ignore - internal component.
160
+ */
161
+ function AppThemeProvider(props: AppThemeProviderProps) {
162
+ const { children, theme, ...rest } = props;
163
+
164
+ const useCssVarsProvider = isCssVarsTheme(theme);
165
+
166
+ return useCssVarsProvider ? (
167
+ <CssVarsThemeProvider theme={theme} {...rest}>
168
+ {children}
169
+ </CssVarsThemeProvider>
170
+ ) : (
171
+ <LegacyThemeProvider theme={theme} {...rest}>
172
+ {children}
173
+ </LegacyThemeProvider>
174
+ );
175
+ }
176
+
177
+ export { AppThemeProvider };
@@ -0,0 +1 @@
1
+ export * from "./AppProvider";
@@ -0,0 +1,429 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { render, within, screen } from "@testing-library/react";
3
+ import DashboardIcon from "@mui/icons-material/Dashboard";
4
+ import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
5
+ import BarChartIcon from "@mui/icons-material/BarChart";
6
+ import DescriptionIcon from "@mui/icons-material/Description";
7
+ import LayersIcon from "@mui/icons-material/Layers";
8
+ import userEvent from "@testing-library/user-event";
9
+ import "@testing-library/jest-dom/vitest";
10
+ import { Navigation } from "../AppProvider";
11
+ import { DashboardLayout } from "./DashboardLayout";
12
+ import { AppProvider } from "../AppProvider/AppProviderComponent";
13
+
14
+ describe("DashboardLayout", () => {
15
+ test("renders content correctly", async () => {
16
+ render(<DashboardLayout>Hello world</DashboardLayout>);
17
+
18
+ expect(screen.getByText("Hello world")).toBeTruthy();
19
+ });
20
+
21
+ test("renders branding correctly in header", async () => {
22
+ const BRANDING = {
23
+ title: "My Company",
24
+ logo: <img src="https://placehold.co/600x400" alt="Placeholder Logo" />
25
+ };
26
+
27
+ render(
28
+ <AppProvider branding={BRANDING}>
29
+ <DashboardLayout>Hello world</DashboardLayout>
30
+ </AppProvider>
31
+ );
32
+
33
+ const header = screen.getByRole("banner");
34
+
35
+ expect(within(header).getByText("My Company")).toBeTruthy();
36
+ expect(within(header).getByAltText("Placeholder Logo")).toBeTruthy();
37
+ });
38
+
39
+ test("can switch theme", async () => {
40
+ const user = userEvent.setup();
41
+
42
+ render(
43
+ <AppProvider>
44
+ <DashboardLayout>Hello world</DashboardLayout>
45
+ </AppProvider>
46
+ );
47
+
48
+ const getBackgroundColorCSSVariable = () =>
49
+ getComputedStyle(document.documentElement).getPropertyValue(
50
+ "--mui-palette-common-background"
51
+ );
52
+
53
+ const header = screen.getByRole("banner");
54
+
55
+ const themeSwitcherButton = within(header).getByLabelText(
56
+ "Switch to dark mode"
57
+ );
58
+
59
+ expect(getBackgroundColorCSSVariable()).toBe("#fff");
60
+
61
+ await user.click(themeSwitcherButton);
62
+
63
+ expect(getBackgroundColorCSSVariable()).toBe("#000");
64
+
65
+ await user.click(themeSwitcherButton);
66
+
67
+ expect(getBackgroundColorCSSVariable()).toBe("#fff");
68
+ });
69
+
70
+ test("navigation works correctly", async () => {
71
+ const NAVIGATION: Navigation = [
72
+ {
73
+ kind: "header",
74
+ title: "Main items"
75
+ },
76
+ {
77
+ title: "Dashboard",
78
+ segment: "dashboard",
79
+ icon: <DashboardIcon />
80
+ },
81
+ {
82
+ title: "Orders",
83
+ segment: "orders",
84
+ icon: <ShoppingCartIcon />
85
+ },
86
+ {
87
+ kind: "divider"
88
+ },
89
+ {
90
+ kind: "header",
91
+ title: "Analytics"
92
+ },
93
+ {
94
+ segment: "reports",
95
+ title: "Reports",
96
+ icon: <BarChartIcon />,
97
+ children: [
98
+ {
99
+ segment: "sales",
100
+ title: "Sales",
101
+ icon: <DescriptionIcon />
102
+ },
103
+ {
104
+ segment: "traffic",
105
+ title: "Traffic",
106
+ icon: <DescriptionIcon />
107
+ }
108
+ ]
109
+ },
110
+ {
111
+ segment: "integrations",
112
+ title: "Integrations",
113
+ icon: <LayersIcon />
114
+ }
115
+ ];
116
+
117
+ const user = userEvent.setup();
118
+
119
+ render(
120
+ <AppProvider navigation={NAVIGATION}>
121
+ <DashboardLayout>Hello world</DashboardLayout>
122
+ </AppProvider>
123
+ );
124
+
125
+ const desktopNavigation = screen.getByRole("navigation", {
126
+ name: "Desktop"
127
+ });
128
+
129
+ // List subheaders are present
130
+
131
+ expect(within(desktopNavigation).getByText("Main items")).toBeTruthy();
132
+ expect(within(desktopNavigation).getByText("Analytics")).toBeTruthy();
133
+
134
+ // List items and their links are present
135
+
136
+ const dashboardLink = within(desktopNavigation).getByRole("link", {
137
+ name: "Dashboard"
138
+ });
139
+ const ordersLink = within(desktopNavigation).getByRole("link", {
140
+ name: "Orders"
141
+ });
142
+
143
+ expect(dashboardLink.getAttribute("href")).toBe("/dashboard");
144
+ expect(ordersLink.getAttribute("href")).toBe("/orders");
145
+
146
+ const reportsItem = within(desktopNavigation).getByText("Reports");
147
+
148
+ expect(reportsItem).toBeTruthy();
149
+ expect(within(desktopNavigation).getByText("Integrations")).toBeTruthy();
150
+
151
+ // Nested list items show when parent item is clicked
152
+
153
+ expect(within(desktopNavigation).queryByText("Sales")).toBeNull();
154
+ expect(within(desktopNavigation).queryByText("Traffic")).toBeNull();
155
+
156
+ await user.click(reportsItem);
157
+
158
+ expect(within(desktopNavigation).getByText("Sales")).toBeTruthy();
159
+ expect(within(desktopNavigation).getByText("Traffic")).toBeTruthy();
160
+ });
161
+
162
+ test("starts with parent items expanded if any of their children is the current page", () => {
163
+ const NAVIGATION: Navigation = [
164
+ {
165
+ segment: "reports",
166
+ title: "Reports",
167
+ icon: <BarChartIcon />,
168
+ children: [
169
+ {
170
+ segment: "sales",
171
+ title: "Sales",
172
+ icon: <DescriptionIcon />
173
+ },
174
+ {
175
+ segment: "traffic",
176
+ title: "Traffic",
177
+ icon: <DescriptionIcon />
178
+ }
179
+ ]
180
+ }
181
+ ];
182
+
183
+ const mockRouter = {
184
+ pathname: "/reports/sales",
185
+ searchParams: new URLSearchParams(),
186
+ navigate: vi.fn()
187
+ };
188
+
189
+ render(
190
+ <AppProvider navigation={NAVIGATION} router={mockRouter}>
191
+ <DashboardLayout>Hello world</DashboardLayout>
192
+ </AppProvider>
193
+ );
194
+
195
+ const desktopNavigation = screen.getByRole("navigation", {
196
+ name: "Desktop"
197
+ });
198
+
199
+ expect(within(desktopNavigation).getByText("Sales")).toBeTruthy();
200
+ expect(within(desktopNavigation).getByText("Traffic")).toBeTruthy();
201
+ });
202
+
203
+ test("shows correct selected page item", () => {
204
+ const NAVIGATION: Navigation = [
205
+ {
206
+ title: "Dashboard",
207
+ segment: "dashboard",
208
+ icon: <DashboardIcon />
209
+ },
210
+ {
211
+ title: "Orders",
212
+ segment: "orders",
213
+ icon: <ShoppingCartIcon />
214
+ },
215
+ {
216
+ segment: "dynamic",
217
+ title: "Dynamic",
218
+ icon: <BarChartIcon />,
219
+ pattern: "dynamic/:dynamicId"
220
+ },
221
+ {
222
+ segment: "optional",
223
+ title: "Optional",
224
+ pattern: "optional{/:optionalId}?"
225
+ },
226
+ {
227
+ segment: "oneOrMore",
228
+ title: "One or more",
229
+ pattern: "oneormore{/:oneormoreId}+"
230
+ },
231
+ {
232
+ segment: "zeroOrMore",
233
+ title: "Zero or more",
234
+ pattern: "zeroormore{/:zeroormoreId}*"
235
+ }
236
+ ];
237
+
238
+ function AppWithPathname({ pathname }: { pathname: string }) {
239
+ const mockRouter = {
240
+ pathname,
241
+ searchParams: new URLSearchParams(),
242
+ navigate: vi.fn()
243
+ };
244
+
245
+ return (
246
+ <AppProvider navigation={NAVIGATION} router={mockRouter}>
247
+ <DashboardLayout>Hello world</DashboardLayout>
248
+ </AppProvider>
249
+ );
250
+ }
251
+
252
+ const { rerender } = render(<AppWithPathname pathname="/dashboard" />);
253
+
254
+ const desktopNavigation = screen.getByRole("navigation", {
255
+ name: "Desktop"
256
+ });
257
+
258
+ expect(
259
+ within(desktopNavigation).getByRole("link", { name: "Dashboard" })
260
+ ).toHaveClass("Mui-selected");
261
+
262
+ rerender(<AppWithPathname pathname="/orders" />);
263
+
264
+ expect(
265
+ within(desktopNavigation).getByRole("link", { name: "Dashboard" })
266
+ ).not.toHaveClass("Mui-selected");
267
+ expect(
268
+ within(desktopNavigation).getByRole("link", { name: "Orders" })
269
+ ).toHaveClass("Mui-selected");
270
+
271
+ rerender(<AppWithPathname pathname="/dynamic" />);
272
+ expect(
273
+ within(desktopNavigation).getByRole("link", { name: "Dynamic" })
274
+ ).not.toHaveClass("Mui-selected");
275
+ rerender(<AppWithPathname pathname="/dynamic/123" />);
276
+ expect(
277
+ within(desktopNavigation).getByRole("link", { name: "Dynamic" })
278
+ ).toHaveClass("Mui-selected");
279
+ rerender(<AppWithPathname pathname="/dynamic/123/456" />);
280
+ expect(
281
+ within(desktopNavigation).getByRole("link", { name: "Dynamic" })
282
+ ).not.toHaveClass("Mui-selected");
283
+
284
+ rerender(<AppWithPathname pathname="/optional" />);
285
+ expect(
286
+ within(desktopNavigation).getByRole("link", { name: "Optional" })
287
+ ).toHaveClass("Mui-selected");
288
+ rerender(<AppWithPathname pathname="/optional/123" />);
289
+ expect(
290
+ within(desktopNavigation).getByRole("link", { name: "Optional" })
291
+ ).toHaveClass("Mui-selected");
292
+ rerender(<AppWithPathname pathname="/optional/123/456" />);
293
+ expect(
294
+ within(desktopNavigation).getByRole("link", { name: "Optional" })
295
+ ).not.toHaveClass("Mui-selected");
296
+
297
+ rerender(<AppWithPathname pathname="/oneormore" />);
298
+ expect(
299
+ within(desktopNavigation).getByRole("link", { name: "One or more" })
300
+ ).not.toHaveClass("Mui-selected");
301
+ rerender(<AppWithPathname pathname="/oneormore/123" />);
302
+ expect(
303
+ within(desktopNavigation).getByRole("link", { name: "One or more" })
304
+ ).toHaveClass("Mui-selected");
305
+ rerender(<AppWithPathname pathname="/oneormore/123/456" />);
306
+ expect(
307
+ within(desktopNavigation).getByRole("link", { name: "One or more" })
308
+ ).toHaveClass("Mui-selected");
309
+
310
+ rerender(<AppWithPathname pathname="/zeroormore" />);
311
+ expect(
312
+ within(desktopNavigation).getByRole("link", { name: "Zero or more" })
313
+ ).toHaveClass("Mui-selected");
314
+ rerender(<AppWithPathname pathname="/zeroormore/123" />);
315
+ expect(
316
+ within(desktopNavigation).getByRole("link", { name: "Zero or more" })
317
+ ).toHaveClass("Mui-selected");
318
+ rerender(<AppWithPathname pathname="/zeroormore/123/456" />);
319
+ expect(
320
+ within(desktopNavigation).getByRole("link", { name: "Zero or more" })
321
+ ).toHaveClass("Mui-selected");
322
+ });
323
+
324
+ test("renders navigation actions", async () => {
325
+ const NAVIGATION: Navigation = [
326
+ {
327
+ title: "Item 1",
328
+ segment: "item1",
329
+ icon: <DescriptionIcon />,
330
+ action: <div>Action 1</div>
331
+ },
332
+ {
333
+ title: "Item",
334
+ segment: "item2",
335
+ icon: <DescriptionIcon />,
336
+ action: <div>Action 2</div>
337
+ }
338
+ ];
339
+
340
+ render(
341
+ <AppProvider navigation={NAVIGATION}>
342
+ <DashboardLayout>Hello world</DashboardLayout>
343
+ </AppProvider>
344
+ );
345
+
346
+ const desktopNavigation = screen.getByRole("navigation", {
347
+ name: "Desktop"
348
+ });
349
+
350
+ expect(within(desktopNavigation).getByText("Action 1")).toBeTruthy();
351
+ expect(within(desktopNavigation).getByText("Action 2")).toBeTruthy();
352
+ });
353
+
354
+ test("renders sidebar footer slot content", async () => {
355
+ function SidebarFooter() {
356
+ return <div>I am footer</div>;
357
+ }
358
+
359
+ render(
360
+ <AppProvider>
361
+ <DashboardLayout slots={{ sidebarFooter: SidebarFooter }}>
362
+ Hello world
363
+ </DashboardLayout>
364
+ </AppProvider>
365
+ );
366
+
367
+ const desktopNavigation = screen.getByRole("navigation", {
368
+ name: "Desktop"
369
+ });
370
+
371
+ expect(within(desktopNavigation).getByText("I am footer")).toBeTruthy();
372
+ });
373
+
374
+ test("renders without the navigation and toggle button", async () => {
375
+ const NAVIGATION: Navigation = [
376
+ {
377
+ title: "Dashboard",
378
+ segment: "dashboard",
379
+ icon: <DashboardIcon />
380
+ },
381
+ {
382
+ title: "Orders",
383
+ segment: "orders",
384
+ icon: <ShoppingCartIcon />
385
+ }
386
+ ];
387
+
388
+ render(
389
+ <AppProvider navigation={NAVIGATION}>
390
+ <DashboardLayout hideNavigation>Hello world</DashboardLayout>
391
+ </AppProvider>
392
+ );
393
+
394
+ const desktopNavigation = screen.queryByRole("navigation", {
395
+ name: "Desktop"
396
+ });
397
+ const navigationToggle = screen.queryByLabelText("Collapse menu");
398
+
399
+ // Expect that navigation and menu button are not rendered
400
+ expect(desktopNavigation).toBeNull();
401
+ expect(navigationToggle).toBeNull();
402
+
403
+ // Ensure that main content is still rendered
404
+ expect(screen.getByText("Hello world")).toBeTruthy();
405
+ });
406
+
407
+ test("renders without default collapsed navigation on desktop", async () => {
408
+ const NAVIGATION: Navigation = [
409
+ {
410
+ title: "Dashboard",
411
+ segment: "dashboard",
412
+ icon: <DashboardIcon />
413
+ }
414
+ ];
415
+
416
+ render(
417
+ <AppProvider navigation={NAVIGATION}>
418
+ <DashboardLayout defaultSidebarCollapsed>Hello world</DashboardLayout>
419
+ </AppProvider>
420
+ );
421
+
422
+ // Expect that menu button has expand action
423
+ expect(screen.getAllByLabelText("Expand menu")).toBeTruthy();
424
+ expect(screen.queryByLabelText("Collapse menu")).toBeNull();
425
+
426
+ // Ensure that main content is still rendered
427
+ expect(screen.getByText("Hello world")).toBeTruthy();
428
+ });
429
+ });