@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,286 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import PropTypes from "prop-types";
5
+ import { styled, useTheme } from "@mui/material";
6
+ import MuiAppBar from "@mui/material/AppBar";
7
+ import Box from "@mui/material/Box";
8
+ import Drawer from "@mui/material/Drawer";
9
+ import IconButton from "@mui/material/IconButton";
10
+ import Stack from "@mui/material/Stack";
11
+ import Toolbar from "@mui/material/Toolbar";
12
+ import Tooltip from "@mui/material/Tooltip";
13
+ import Typography from "@mui/material/Typography";
14
+ import useMediaQuery from "@mui/material/useMediaQuery";
15
+ import MenuIcon from "@mui/icons-material/Menu";
16
+ import MenuOpenIcon from "@mui/icons-material/MenuOpen";
17
+ import { Link } from "../shared/Link";
18
+ import { BrandingContext, NavigationContext, WindowContext } from "../shared/context";
19
+ import { Account } from "../Account";
20
+ import { useApplicationTitle } from "../shared/branding";
21
+ import { DashboardSidebarSubNavigation } from "./DashboardSidebarSubNavigation";
22
+ import { ToolbarActions } from "./ToolbarActions";
23
+ import { ThemeSwitcher } from "./ThemeSwitcher";
24
+ import { getDrawerSxTransitionMixin, getDrawerWidthTransitionMixin } from "./utils";
25
+ const AppBar = styled(MuiAppBar)(({ theme }) => ({
26
+ borderWidth: 0,
27
+ borderBottomWidth: 1,
28
+ borderStyle: "solid",
29
+ borderColor: (theme.vars ?? theme).palette.divider,
30
+ boxShadow: "none",
31
+ zIndex: theme.zIndex.drawer + 1
32
+ }));
33
+ const LogoContainer = styled("div")({
34
+ position: "relative",
35
+ height: 40,
36
+ "& img": {
37
+ maxHeight: 40
38
+ }
39
+ });
40
+ /**
41
+ *
42
+ * Demos:
43
+ *
44
+ * - [Dashboard Layout](https://mui.com/toolpad/core/react-dashboard-layout/)
45
+ *
46
+ * API:
47
+ *
48
+ * - [DashboardLayout API](https://mui.com/toolpad/core/api/dashboard-layout)
49
+ */
50
+ function DashboardLayout(props) {
51
+ const { children, disableCollapsibleSidebar = false, defaultSidebarCollapsed = false, hideNavigation = false, sidebarExpandedWidth = 320, slots, slotProps, sx } = props;
52
+ const theme = useTheme();
53
+ const branding = React.useContext(BrandingContext);
54
+ const navigation = React.useContext(NavigationContext);
55
+ const appWindow = React.useContext(WindowContext);
56
+ const applicationTitle = useApplicationTitle();
57
+ const [isDesktopNavigationExpanded, setIsDesktopNavigationExpanded] = React.useState(!defaultSidebarCollapsed);
58
+ const [isMobileNavigationExpanded, setIsMobileNavigationExpanded] = React.useState(false);
59
+ const isUnderMdViewport = useMediaQuery(theme.breakpoints.down("md"), appWindow && {
60
+ matchMedia: appWindow.matchMedia
61
+ });
62
+ const isOverSmViewport = useMediaQuery(theme.breakpoints.up("sm"), appWindow && {
63
+ matchMedia: appWindow.matchMedia
64
+ });
65
+ const isNavigationExpanded = isUnderMdViewport
66
+ ? isMobileNavigationExpanded
67
+ : isDesktopNavigationExpanded;
68
+ const setIsNavigationExpanded = React.useCallback((newExpanded) => {
69
+ if (isUnderMdViewport) {
70
+ setIsMobileNavigationExpanded(newExpanded);
71
+ }
72
+ else {
73
+ setIsDesktopNavigationExpanded(newExpanded);
74
+ }
75
+ }, [isUnderMdViewport]);
76
+ const [isNavigationFullyExpanded, setIsNavigationFullyExpanded] = React.useState(isNavigationExpanded);
77
+ React.useEffect(() => {
78
+ if (isNavigationExpanded) {
79
+ const drawerWidthTransitionTimeout = setTimeout(() => {
80
+ setIsNavigationFullyExpanded(true);
81
+ }, theme.transitions.duration.enteringScreen);
82
+ return () => clearTimeout(drawerWidthTransitionTimeout);
83
+ }
84
+ setIsNavigationFullyExpanded(false);
85
+ return () => { };
86
+ }, [isNavigationExpanded, theme]);
87
+ const selectedItemIdRef = React.useRef("");
88
+ const handleSetNavigationExpanded = React.useCallback((newExpanded) => () => {
89
+ setIsNavigationExpanded(newExpanded);
90
+ }, [setIsNavigationExpanded]);
91
+ const toggleNavigationExpanded = React.useCallback(() => {
92
+ setIsNavigationExpanded(!isNavigationExpanded);
93
+ }, [isNavigationExpanded, setIsNavigationExpanded]);
94
+ const handleNavigationLinkClick = React.useCallback(() => {
95
+ selectedItemIdRef.current = "";
96
+ setIsMobileNavigationExpanded(false);
97
+ }, [setIsMobileNavigationExpanded]);
98
+ // If useEffect was used, the reset would also happen on the client render after SSR which we don't need
99
+ React.useMemo(() => {
100
+ if (navigation) {
101
+ selectedItemIdRef.current = "";
102
+ }
103
+ }, [navigation]);
104
+ const isDesktopMini = !disableCollapsibleSidebar && !isDesktopNavigationExpanded;
105
+ const isMobileMini = !disableCollapsibleSidebar && !isMobileNavigationExpanded;
106
+ const getMenuIcon = React.useCallback((isExpanded) => {
107
+ const expandMenuActionText = "Expand";
108
+ const collapseMenuActionText = "Collapse";
109
+ return (_jsx(Tooltip, { title: `${isExpanded ? collapseMenuActionText : expandMenuActionText} menu`, enterDelay: 1000, children: _jsx("div", { children: _jsx(IconButton, { "aria-label": `${isExpanded ? collapseMenuActionText : expandMenuActionText} navigation menu`, onClick: toggleNavigationExpanded, children: isExpanded ? _jsx(MenuOpenIcon, {}) : _jsx(MenuIcon, {}) }) }) }));
110
+ }, [toggleNavigationExpanded]);
111
+ const hasDrawerTransitions = isOverSmViewport && (disableCollapsibleSidebar || !isUnderMdViewport);
112
+ const ToolbarActionsSlot = slots?.toolbarActions ?? ToolbarActions;
113
+ const ToolbarAccountSlot = slots?.toolbarAccount ?? Account;
114
+ const SidebarFooterSlot = slots?.sidebarFooter ?? null;
115
+ const getDrawerContent = React.useCallback((isMini, viewport) => (_jsxs(React.Fragment, { children: [_jsx(Toolbar, {}), _jsxs(Box, { component: "nav", "aria-label": `${viewport.charAt(0).toUpperCase()}${viewport.slice(1)}`, sx: {
116
+ height: "100%",
117
+ display: "flex",
118
+ flexDirection: "column",
119
+ justifyContent: "space-between",
120
+ overflow: "auto",
121
+ pt: navigation[0]?.kind === "header" && !isMini ? 0 : 2,
122
+ ...(hasDrawerTransitions
123
+ ? getDrawerSxTransitionMixin(isNavigationFullyExpanded, "padding")
124
+ : {})
125
+ }, children: [_jsx(DashboardSidebarSubNavigation, { subNavigation: navigation, onLinkClick: handleNavigationLinkClick, isMini: isMini, isFullyExpanded: isNavigationFullyExpanded, hasDrawerTransitions: hasDrawerTransitions, selectedItemId: selectedItemIdRef.current }), SidebarFooterSlot ? (_jsx(SidebarFooterSlot, { mini: isMini, ...slotProps?.sidebarFooter })) : null] })] })), [
126
+ SidebarFooterSlot,
127
+ handleNavigationLinkClick,
128
+ hasDrawerTransitions,
129
+ isNavigationFullyExpanded,
130
+ navigation,
131
+ slotProps?.sidebarFooter
132
+ ]);
133
+ const getDrawerSharedSx = React.useCallback((isMini, isTemporary) => {
134
+ const drawerWidth = isMini ? 64 : sidebarExpandedWidth;
135
+ return {
136
+ width: drawerWidth,
137
+ flexShrink: 0,
138
+ ...getDrawerWidthTransitionMixin(isNavigationExpanded),
139
+ ...(isTemporary ? { position: "absolute" } : {}),
140
+ [`& .MuiDrawer-paper`]: {
141
+ position: "absolute",
142
+ width: drawerWidth,
143
+ boxSizing: "border-box",
144
+ backgroundImage: "none",
145
+ ...getDrawerWidthTransitionMixin(isNavigationExpanded)
146
+ }
147
+ };
148
+ }, [isNavigationExpanded, sidebarExpandedWidth]);
149
+ const layoutRef = React.useRef(null);
150
+ return (_jsxs(Box, { ref: layoutRef, sx: {
151
+ position: "relative",
152
+ display: "flex",
153
+ overflow: "hidden",
154
+ height: "100vh",
155
+ width: "100vw",
156
+ ...sx
157
+ }, children: [_jsx(AppBar, { color: "inherit", position: "absolute", children: _jsxs(Toolbar, { sx: { backgroundColor: "inherit", mx: { xs: -0.75, sm: -1.5 } }, children: [!hideNavigation ? (_jsxs(React.Fragment, { children: [_jsx(Box, { sx: {
158
+ mr: { sm: disableCollapsibleSidebar ? 0 : 1 },
159
+ display: { md: "none" }
160
+ }, children: getMenuIcon(isMobileNavigationExpanded) }), _jsx(Box, { sx: {
161
+ display: {
162
+ xs: "none",
163
+ md: disableCollapsibleSidebar ? "none" : "block"
164
+ },
165
+ mr: disableCollapsibleSidebar ? 0 : 1
166
+ }, children: getMenuIcon(isDesktopNavigationExpanded) })] })) : null, _jsx(Box, { sx: {
167
+ position: { xs: "absolute", md: "static" },
168
+ left: { xs: "50%", md: "auto" },
169
+ transform: { xs: "translateX(-50%)", md: "none" }
170
+ }, children: _jsx(Link, { href: "/", style: { color: "inherit", textDecoration: "none" }, children: _jsxs(Stack, { direction: "row", alignItems: "center", children: [branding?.logo && (_jsx(LogoContainer, { children: branding.logo })), _jsx(Typography, { variant: "h6", sx: {
171
+ color: (theme.vars ?? theme).palette.primary.main,
172
+ fontWeight: "700",
173
+ ml: 0.5,
174
+ whiteSpace: "nowrap"
175
+ }, children: applicationTitle })] }) }) }), _jsx(Box, { sx: { flexGrow: 1 } }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(ToolbarActionsSlot, { ...slotProps?.toolbarActions }), _jsx(ThemeSwitcher, {}), _jsx(ToolbarAccountSlot, { ...slotProps?.toolbarAccount })] })] }) }), !hideNavigation ? (_jsxs(React.Fragment, { children: [_jsx(Drawer, { container: layoutRef.current, variant: "temporary", open: isMobileNavigationExpanded, onClose: handleSetNavigationExpanded(false), ModalProps: {
176
+ keepMounted: true // Better open performance on mobile.
177
+ }, sx: {
178
+ display: {
179
+ xs: "block",
180
+ sm: disableCollapsibleSidebar ? "block" : "none",
181
+ md: "none"
182
+ },
183
+ ...getDrawerSharedSx(false, true)
184
+ }, children: getDrawerContent(false, "phone") }), _jsx(Drawer, { variant: "permanent", sx: {
185
+ display: {
186
+ xs: "none",
187
+ sm: disableCollapsibleSidebar ? "none" : "block",
188
+ md: "none"
189
+ },
190
+ ...getDrawerSharedSx(isMobileMini, false)
191
+ }, children: getDrawerContent(isMobileMini, "tablet") }), _jsx(Drawer, { variant: "permanent", sx: {
192
+ display: { xs: "none", md: "block" },
193
+ ...getDrawerSharedSx(isDesktopMini, false)
194
+ }, children: getDrawerContent(isDesktopMini, "desktop") })] })) : null, _jsxs(Box, { sx: {
195
+ display: "flex",
196
+ flexDirection: "column",
197
+ flex: 1
198
+ }, children: [_jsx(Toolbar, {}), _jsx(Box, { component: "main", sx: {
199
+ display: "flex",
200
+ flexDirection: "column",
201
+ flex: 1,
202
+ overflow: "auto"
203
+ }, children: children })] })] }));
204
+ }
205
+ DashboardLayout.propTypes /* remove-proptypes */ = {
206
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
207
+ // │ These PropTypes are generated from the TypeScript type definitions. │
208
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
209
+ // └─────────────────────────────────────────────────────────────────────┘
210
+ /**
211
+ * The content of the dashboard.
212
+ */
213
+ children: PropTypes.node,
214
+ /**
215
+ * Whether the sidebar should start collapsed in desktop size screens.
216
+ * @default false
217
+ */
218
+ defaultSidebarCollapsed: PropTypes.bool,
219
+ /**
220
+ * Whether the sidebar should not be collapsible to a mini variant in desktop and tablet viewports.
221
+ * @default false
222
+ */
223
+ disableCollapsibleSidebar: PropTypes.bool,
224
+ /**
225
+ * Whether the navigation bar and menu icon should be hidden
226
+ * @default false
227
+ */
228
+ hideNavigation: PropTypes.bool,
229
+ /**
230
+ * Width of the sidebar when expanded.
231
+ * @default 320
232
+ */
233
+ sidebarExpandedWidth: PropTypes.oneOfType([
234
+ PropTypes.number,
235
+ PropTypes.string
236
+ ]),
237
+ /**
238
+ * The props used for each slot inside.
239
+ * @default {}
240
+ */
241
+ slotProps: PropTypes.shape({
242
+ sidebarFooter: PropTypes.shape({
243
+ mini: PropTypes.bool.isRequired
244
+ }),
245
+ toolbarAccount: PropTypes.shape({
246
+ localeText: PropTypes.shape({
247
+ iconButtonAriaLabel: PropTypes.string,
248
+ signInLabel: PropTypes.string,
249
+ signOutLabel: PropTypes.string
250
+ }),
251
+ slotProps: PropTypes.shape({
252
+ popover: PropTypes.object,
253
+ popoverContent: PropTypes.object,
254
+ preview: PropTypes.object,
255
+ signInButton: PropTypes.object,
256
+ signOutButton: PropTypes.object
257
+ }),
258
+ slots: PropTypes.shape({
259
+ popover: PropTypes.elementType,
260
+ popoverContent: PropTypes.elementType,
261
+ preview: PropTypes.elementType,
262
+ signInButton: PropTypes.elementType,
263
+ signOutButton: PropTypes.elementType
264
+ })
265
+ }),
266
+ toolbarActions: PropTypes.object
267
+ }),
268
+ /**
269
+ * The components used for each slot inside.
270
+ * @default {}
271
+ */
272
+ slots: PropTypes.shape({
273
+ sidebarFooter: PropTypes.elementType,
274
+ toolbarAccount: PropTypes.elementType,
275
+ toolbarActions: PropTypes.elementType
276
+ }),
277
+ /**
278
+ * The system prop that allows defining system overrides as well as additional CSS styles.
279
+ */
280
+ sx: PropTypes.oneOfType([
281
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
282
+ PropTypes.func,
283
+ PropTypes.object
284
+ ])
285
+ };
286
+ export { DashboardLayout };
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom/vitest";
@@ -0,0 +1,291 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, test, expect, vi } from "vitest";
3
+ import { render, within, screen } from "@testing-library/react";
4
+ import DashboardIcon from "@mui/icons-material/Dashboard";
5
+ import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
6
+ import BarChartIcon from "@mui/icons-material/BarChart";
7
+ import DescriptionIcon from "@mui/icons-material/Description";
8
+ import LayersIcon from "@mui/icons-material/Layers";
9
+ import userEvent from "@testing-library/user-event";
10
+ import "@testing-library/jest-dom/vitest";
11
+ import { DashboardLayout } from "./DashboardLayout";
12
+ import { AppProvider } from "../AppProvider/AppProviderComponent";
13
+ describe("DashboardLayout", () => {
14
+ test("renders content correctly", async () => {
15
+ render(_jsx(DashboardLayout, { children: "Hello world" }));
16
+ expect(screen.getByText("Hello world")).toBeTruthy();
17
+ });
18
+ test("renders branding correctly in header", async () => {
19
+ const BRANDING = {
20
+ title: "My Company",
21
+ logo: _jsx("img", { src: "https://placehold.co/600x400", alt: "Placeholder Logo" })
22
+ };
23
+ render(_jsx(AppProvider, { branding: BRANDING, children: _jsx(DashboardLayout, { children: "Hello world" }) }));
24
+ const header = screen.getByRole("banner");
25
+ expect(within(header).getByText("My Company")).toBeTruthy();
26
+ expect(within(header).getByAltText("Placeholder Logo")).toBeTruthy();
27
+ });
28
+ test("can switch theme", async () => {
29
+ const user = userEvent.setup();
30
+ render(_jsx(AppProvider, { children: _jsx(DashboardLayout, { children: "Hello world" }) }));
31
+ const getBackgroundColorCSSVariable = () => getComputedStyle(document.documentElement).getPropertyValue("--mui-palette-common-background");
32
+ const header = screen.getByRole("banner");
33
+ const themeSwitcherButton = within(header).getByLabelText("Switch to dark mode");
34
+ expect(getBackgroundColorCSSVariable()).toBe("#fff");
35
+ await user.click(themeSwitcherButton);
36
+ expect(getBackgroundColorCSSVariable()).toBe("#000");
37
+ await user.click(themeSwitcherButton);
38
+ expect(getBackgroundColorCSSVariable()).toBe("#fff");
39
+ });
40
+ test("navigation works correctly", async () => {
41
+ const NAVIGATION = [
42
+ {
43
+ kind: "header",
44
+ title: "Main items"
45
+ },
46
+ {
47
+ title: "Dashboard",
48
+ segment: "dashboard",
49
+ icon: _jsx(DashboardIcon, {})
50
+ },
51
+ {
52
+ title: "Orders",
53
+ segment: "orders",
54
+ icon: _jsx(ShoppingCartIcon, {})
55
+ },
56
+ {
57
+ kind: "divider"
58
+ },
59
+ {
60
+ kind: "header",
61
+ title: "Analytics"
62
+ },
63
+ {
64
+ segment: "reports",
65
+ title: "Reports",
66
+ icon: _jsx(BarChartIcon, {}),
67
+ children: [
68
+ {
69
+ segment: "sales",
70
+ title: "Sales",
71
+ icon: _jsx(DescriptionIcon, {})
72
+ },
73
+ {
74
+ segment: "traffic",
75
+ title: "Traffic",
76
+ icon: _jsx(DescriptionIcon, {})
77
+ }
78
+ ]
79
+ },
80
+ {
81
+ segment: "integrations",
82
+ title: "Integrations",
83
+ icon: _jsx(LayersIcon, {})
84
+ }
85
+ ];
86
+ const user = userEvent.setup();
87
+ render(_jsx(AppProvider, { navigation: NAVIGATION, children: _jsx(DashboardLayout, { children: "Hello world" }) }));
88
+ const desktopNavigation = screen.getByRole("navigation", {
89
+ name: "Desktop"
90
+ });
91
+ // List subheaders are present
92
+ expect(within(desktopNavigation).getByText("Main items")).toBeTruthy();
93
+ expect(within(desktopNavigation).getByText("Analytics")).toBeTruthy();
94
+ // List items and their links are present
95
+ const dashboardLink = within(desktopNavigation).getByRole("link", {
96
+ name: "Dashboard"
97
+ });
98
+ const ordersLink = within(desktopNavigation).getByRole("link", {
99
+ name: "Orders"
100
+ });
101
+ expect(dashboardLink.getAttribute("href")).toBe("/dashboard");
102
+ expect(ordersLink.getAttribute("href")).toBe("/orders");
103
+ const reportsItem = within(desktopNavigation).getByText("Reports");
104
+ expect(reportsItem).toBeTruthy();
105
+ expect(within(desktopNavigation).getByText("Integrations")).toBeTruthy();
106
+ // Nested list items show when parent item is clicked
107
+ expect(within(desktopNavigation).queryByText("Sales")).toBeNull();
108
+ expect(within(desktopNavigation).queryByText("Traffic")).toBeNull();
109
+ await user.click(reportsItem);
110
+ expect(within(desktopNavigation).getByText("Sales")).toBeTruthy();
111
+ expect(within(desktopNavigation).getByText("Traffic")).toBeTruthy();
112
+ });
113
+ test("starts with parent items expanded if any of their children is the current page", () => {
114
+ const NAVIGATION = [
115
+ {
116
+ segment: "reports",
117
+ title: "Reports",
118
+ icon: _jsx(BarChartIcon, {}),
119
+ children: [
120
+ {
121
+ segment: "sales",
122
+ title: "Sales",
123
+ icon: _jsx(DescriptionIcon, {})
124
+ },
125
+ {
126
+ segment: "traffic",
127
+ title: "Traffic",
128
+ icon: _jsx(DescriptionIcon, {})
129
+ }
130
+ ]
131
+ }
132
+ ];
133
+ const mockRouter = {
134
+ pathname: "/reports/sales",
135
+ searchParams: new URLSearchParams(),
136
+ navigate: vi.fn()
137
+ };
138
+ render(_jsx(AppProvider, { navigation: NAVIGATION, router: mockRouter, children: _jsx(DashboardLayout, { children: "Hello world" }) }));
139
+ const desktopNavigation = screen.getByRole("navigation", {
140
+ name: "Desktop"
141
+ });
142
+ expect(within(desktopNavigation).getByText("Sales")).toBeTruthy();
143
+ expect(within(desktopNavigation).getByText("Traffic")).toBeTruthy();
144
+ });
145
+ test("shows correct selected page item", () => {
146
+ const NAVIGATION = [
147
+ {
148
+ title: "Dashboard",
149
+ segment: "dashboard",
150
+ icon: _jsx(DashboardIcon, {})
151
+ },
152
+ {
153
+ title: "Orders",
154
+ segment: "orders",
155
+ icon: _jsx(ShoppingCartIcon, {})
156
+ },
157
+ {
158
+ segment: "dynamic",
159
+ title: "Dynamic",
160
+ icon: _jsx(BarChartIcon, {}),
161
+ pattern: "dynamic/:dynamicId"
162
+ },
163
+ {
164
+ segment: "optional",
165
+ title: "Optional",
166
+ pattern: "optional{/:optionalId}?"
167
+ },
168
+ {
169
+ segment: "oneOrMore",
170
+ title: "One or more",
171
+ pattern: "oneormore{/:oneormoreId}+"
172
+ },
173
+ {
174
+ segment: "zeroOrMore",
175
+ title: "Zero or more",
176
+ pattern: "zeroormore{/:zeroormoreId}*"
177
+ }
178
+ ];
179
+ function AppWithPathname({ pathname }) {
180
+ const mockRouter = {
181
+ pathname,
182
+ searchParams: new URLSearchParams(),
183
+ navigate: vi.fn()
184
+ };
185
+ return (_jsx(AppProvider, { navigation: NAVIGATION, router: mockRouter, children: _jsx(DashboardLayout, { children: "Hello world" }) }));
186
+ }
187
+ const { rerender } = render(_jsx(AppWithPathname, { pathname: "/dashboard" }));
188
+ const desktopNavigation = screen.getByRole("navigation", {
189
+ name: "Desktop"
190
+ });
191
+ expect(within(desktopNavigation).getByRole("link", { name: "Dashboard" })).toHaveClass("Mui-selected");
192
+ rerender(_jsx(AppWithPathname, { pathname: "/orders" }));
193
+ expect(within(desktopNavigation).getByRole("link", { name: "Dashboard" })).not.toHaveClass("Mui-selected");
194
+ expect(within(desktopNavigation).getByRole("link", { name: "Orders" })).toHaveClass("Mui-selected");
195
+ rerender(_jsx(AppWithPathname, { pathname: "/dynamic" }));
196
+ expect(within(desktopNavigation).getByRole("link", { name: "Dynamic" })).not.toHaveClass("Mui-selected");
197
+ rerender(_jsx(AppWithPathname, { pathname: "/dynamic/123" }));
198
+ expect(within(desktopNavigation).getByRole("link", { name: "Dynamic" })).toHaveClass("Mui-selected");
199
+ rerender(_jsx(AppWithPathname, { pathname: "/dynamic/123/456" }));
200
+ expect(within(desktopNavigation).getByRole("link", { name: "Dynamic" })).not.toHaveClass("Mui-selected");
201
+ rerender(_jsx(AppWithPathname, { pathname: "/optional" }));
202
+ expect(within(desktopNavigation).getByRole("link", { name: "Optional" })).toHaveClass("Mui-selected");
203
+ rerender(_jsx(AppWithPathname, { pathname: "/optional/123" }));
204
+ expect(within(desktopNavigation).getByRole("link", { name: "Optional" })).toHaveClass("Mui-selected");
205
+ rerender(_jsx(AppWithPathname, { pathname: "/optional/123/456" }));
206
+ expect(within(desktopNavigation).getByRole("link", { name: "Optional" })).not.toHaveClass("Mui-selected");
207
+ rerender(_jsx(AppWithPathname, { pathname: "/oneormore" }));
208
+ expect(within(desktopNavigation).getByRole("link", { name: "One or more" })).not.toHaveClass("Mui-selected");
209
+ rerender(_jsx(AppWithPathname, { pathname: "/oneormore/123" }));
210
+ expect(within(desktopNavigation).getByRole("link", { name: "One or more" })).toHaveClass("Mui-selected");
211
+ rerender(_jsx(AppWithPathname, { pathname: "/oneormore/123/456" }));
212
+ expect(within(desktopNavigation).getByRole("link", { name: "One or more" })).toHaveClass("Mui-selected");
213
+ rerender(_jsx(AppWithPathname, { pathname: "/zeroormore" }));
214
+ expect(within(desktopNavigation).getByRole("link", { name: "Zero or more" })).toHaveClass("Mui-selected");
215
+ rerender(_jsx(AppWithPathname, { pathname: "/zeroormore/123" }));
216
+ expect(within(desktopNavigation).getByRole("link", { name: "Zero or more" })).toHaveClass("Mui-selected");
217
+ rerender(_jsx(AppWithPathname, { pathname: "/zeroormore/123/456" }));
218
+ expect(within(desktopNavigation).getByRole("link", { name: "Zero or more" })).toHaveClass("Mui-selected");
219
+ });
220
+ test("renders navigation actions", async () => {
221
+ const NAVIGATION = [
222
+ {
223
+ title: "Item 1",
224
+ segment: "item1",
225
+ icon: _jsx(DescriptionIcon, {}),
226
+ action: _jsx("div", { children: "Action 1" })
227
+ },
228
+ {
229
+ title: "Item",
230
+ segment: "item2",
231
+ icon: _jsx(DescriptionIcon, {}),
232
+ action: _jsx("div", { children: "Action 2" })
233
+ }
234
+ ];
235
+ render(_jsx(AppProvider, { navigation: NAVIGATION, children: _jsx(DashboardLayout, { children: "Hello world" }) }));
236
+ const desktopNavigation = screen.getByRole("navigation", {
237
+ name: "Desktop"
238
+ });
239
+ expect(within(desktopNavigation).getByText("Action 1")).toBeTruthy();
240
+ expect(within(desktopNavigation).getByText("Action 2")).toBeTruthy();
241
+ });
242
+ test("renders sidebar footer slot content", async () => {
243
+ function SidebarFooter() {
244
+ return _jsx("div", { children: "I am footer" });
245
+ }
246
+ render(_jsx(AppProvider, { children: _jsx(DashboardLayout, { slots: { sidebarFooter: SidebarFooter }, children: "Hello world" }) }));
247
+ const desktopNavigation = screen.getByRole("navigation", {
248
+ name: "Desktop"
249
+ });
250
+ expect(within(desktopNavigation).getByText("I am footer")).toBeTruthy();
251
+ });
252
+ test("renders without the navigation and toggle button", async () => {
253
+ const NAVIGATION = [
254
+ {
255
+ title: "Dashboard",
256
+ segment: "dashboard",
257
+ icon: _jsx(DashboardIcon, {})
258
+ },
259
+ {
260
+ title: "Orders",
261
+ segment: "orders",
262
+ icon: _jsx(ShoppingCartIcon, {})
263
+ }
264
+ ];
265
+ render(_jsx(AppProvider, { navigation: NAVIGATION, children: _jsx(DashboardLayout, { hideNavigation: true, children: "Hello world" }) }));
266
+ const desktopNavigation = screen.queryByRole("navigation", {
267
+ name: "Desktop"
268
+ });
269
+ const navigationToggle = screen.queryByLabelText("Collapse menu");
270
+ // Expect that navigation and menu button are not rendered
271
+ expect(desktopNavigation).toBeNull();
272
+ expect(navigationToggle).toBeNull();
273
+ // Ensure that main content is still rendered
274
+ expect(screen.getByText("Hello world")).toBeTruthy();
275
+ });
276
+ test("renders without default collapsed navigation on desktop", async () => {
277
+ const NAVIGATION = [
278
+ {
279
+ title: "Dashboard",
280
+ segment: "dashboard",
281
+ icon: _jsx(DashboardIcon, {})
282
+ }
283
+ ];
284
+ render(_jsx(AppProvider, { navigation: NAVIGATION, children: _jsx(DashboardLayout, { defaultSidebarCollapsed: true, children: "Hello world" }) }));
285
+ // Expect that menu button has expand action
286
+ expect(screen.getAllByLabelText("Expand menu")).toBeTruthy();
287
+ expect(screen.queryByLabelText("Collapse menu")).toBeNull();
288
+ // Ensure that main content is still rendered
289
+ expect(screen.getByText("Hello world")).toBeTruthy();
290
+ });
291
+ });
@@ -0,0 +1,16 @@
1
+ import type { Navigation } from "../AppProvider";
2
+ interface DashboardSidebarSubNavigationProps {
3
+ subNavigation: Navigation;
4
+ basePath?: string;
5
+ depth?: number;
6
+ onLinkClick: () => void;
7
+ isMini?: boolean;
8
+ isFullyExpanded?: boolean;
9
+ hasDrawerTransitions?: boolean;
10
+ selectedItemId: string;
11
+ }
12
+ /**
13
+ * @ignore - internal component.
14
+ */
15
+ declare function DashboardSidebarSubNavigation({ subNavigation, basePath, depth, onLinkClick, isMini, isFullyExpanded, hasDrawerTransitions, selectedItemId }: DashboardSidebarSubNavigationProps): import("react/jsx-runtime").JSX.Element;
16
+ export { DashboardSidebarSubNavigation };