diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.js b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.js new file mode 100644 index 00000000000..6ef9969bee5 --- /dev/null +++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.js @@ -0,0 +1,110 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { createTheme } from '@mui/material/styles'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import BarChartIcon from '@mui/icons-material/BarChart'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { DashboardLayout } from '@toolpad/core/DashboardLayout'; +import { Button } from '@mui/material'; + +const NAVIGATION = [ + { + segment: 'dashboard', + title: 'Dashboard', + icon: , + }, + { + segment: 'orders', + title: 'Orders', + icon: , + }, + { + segment: 'reports', + title: 'Reports', + icon: , + }, +]; + +const demoTheme = createTheme({ + cssVariables: { + colorSchemeSelector: 'data-toolpad-color-scheme', + }, + colorSchemes: { light: true, dark: true }, + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 600, + lg: 1200, + xl: 1536, + }, + }, +}); + +function DemoPageContent({ pathname, toggleSidebar }) { + return ( + + Dashboard content for {pathname} + + + ); +} + +DemoPageContent.propTypes = { + pathname: PropTypes.string.isRequired, + toggleSidebar: PropTypes.func.isRequired, +}; + +function DashboardLayoutSidebarCollapsedProp(props) { + const { window } = props; + + const [pathname, setPathname] = React.useState('/dashboard'); + const [navigationMenuOpen, toggleSidebar] = React.useState(true); + const router = React.useMemo(() => { + return { + pathname, + searchParams: new URLSearchParams(), + navigate: (path) => setPathname(String(path)), + }; + }, [pathname]); + + // Remove this const when copying and pasting into your project. + const demoWindow = window !== undefined ? window() : undefined; + + return ( + + + toggleSidebar(!navigationMenuOpen)} + /> + + + ); +} + +DashboardLayoutSidebarCollapsedProp.propTypes = { + /** + * Injected by the documentation to work in an iframe. + * Remove this when copying and pasting into your project. + */ + window: PropTypes.func, +}; + +export default DashboardLayoutSidebarCollapsedProp; diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.tsx b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.tsx new file mode 100644 index 00000000000..f00eadeb905 --- /dev/null +++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.tsx @@ -0,0 +1,112 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { createTheme } from '@mui/material/styles'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import BarChartIcon from '@mui/icons-material/BarChart'; +import { + AppProvider, + type Router, + type Navigation, +} from '@toolpad/core/AppProvider'; +import { DashboardLayout } from '@toolpad/core/DashboardLayout'; +import { Button } from '@mui/material'; + +const NAVIGATION: Navigation = [ + { + segment: 'dashboard', + title: 'Dashboard', + icon: , + }, + { + segment: 'orders', + title: 'Orders', + icon: , + }, + { + segment: 'reports', + title: 'Reports', + icon: , + }, +]; + +const demoTheme = createTheme({ + cssVariables: { + colorSchemeSelector: 'data-toolpad-color-scheme', + }, + colorSchemes: { light: true, dark: true }, + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 600, + lg: 1200, + xl: 1536, + }, + }, +}); + +function DemoPageContent({ + pathname, + toggleSidebar, +}: { + pathname: string; + toggleSidebar: () => void; +}) { + return ( + + Dashboard content for {pathname} + + + ); +} + +interface DemoProps { + /** + * Injected by the documentation to work in an iframe. + * Remove this when copying and pasting into your project. + */ + window?: () => Window; +} + +export default function DashboardLayoutSidebarCollapsedProp(props: DemoProps) { + const { window } = props; + + const [pathname, setPathname] = React.useState('/dashboard'); + const [navigationMenuOpen, toggleSidebar] = React.useState(true); + const router = React.useMemo(() => { + return { + pathname, + searchParams: new URLSearchParams(), + navigate: (path) => setPathname(String(path)), + }; + }, [pathname]); + + // Remove this const when copying and pasting into your project. + const demoWindow = window !== undefined ? window() : undefined; + + return ( + + + toggleSidebar(!navigationMenuOpen)} + /> + + + ); +} diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.tsx.preview b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.tsx.preview new file mode 100644 index 00000000000..3e5869c9ede --- /dev/null +++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.tsx.preview @@ -0,0 +1,6 @@ + + toggleSidebar(!navigationMenuOpen)} + /> + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md b/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md index dc46918b677..b27e92c67dd 100644 --- a/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md +++ b/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md @@ -126,6 +126,12 @@ The layout sidebar can be hidden if needed with the `hideNavigation` prop. {{"demo": "DashboardLayoutSidebarHidden.js", "height": 400, "iframe": true}} +### Toggle sidebar + +The sidebar can be toggled if needed with the `navigationMenuOpen` prop. + +{{"demo": "DashboardLayoutSidebarCollapsedProp.js", "height": 400, "iframe": true}} + ## Full-size content The layout content can take up the full available area with styles such as `flex: 1` or `height: 100%`. diff --git a/docs/pages/toolpad/core/api/dashboard-layout.json b/docs/pages/toolpad/core/api/dashboard-layout.json index fb900ae907a..217260c8f13 100644 --- a/docs/pages/toolpad/core/api/dashboard-layout.json +++ b/docs/pages/toolpad/core/api/dashboard-layout.json @@ -11,6 +11,9 @@ "defaultSidebarCollapsed": { "type": { "name": "bool" }, "default": "false" }, "disableCollapsibleSidebar": { "type": { "name": "bool" }, "default": "false" }, "hideNavigation": { "type": { "name": "bool" }, "default": "false" }, + "navigationMenuOpen": { "type": { "name": "bool" }, "default": "false" }, + "onNavigationMenuClose": { "type": { "name": "func" } }, + "onNavigationMenuOpen": { "type": { "name": "func" } }, "sidebarExpandedWidth": { "type": { "name": "union", "description": "number
| string" }, "default": "320" diff --git a/docs/translations/api-docs/dashboard-layout/dashboard-layout.json b/docs/translations/api-docs/dashboard-layout/dashboard-layout.json index 3cd8c58bad6..c95c460fb05 100644 --- a/docs/translations/api-docs/dashboard-layout/dashboard-layout.json +++ b/docs/translations/api-docs/dashboard-layout/dashboard-layout.json @@ -12,6 +12,15 @@ "hideNavigation": { "description": "Whether the navigation bar and menu icon should be hidden" }, + "navigationMenuOpen": { + "description": "A prop that controls the collapsed state of the sidebar." + }, + "onNavigationMenuClose": { + "description": "Callback function to be executed on navigation menu state changes to closed" + }, + "onNavigationMenuOpen": { + "description": "Callback function to be executed on navigation menu state changes to open" + }, "sidebarExpandedWidth": { "description": "Width of the sidebar when expanded." }, "slotProps": { "description": "The props used for each slot inside." }, "slots": { "description": "The components used for each slot inside." }, diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx index 8881519cc84..2d7b6f4f411 100644 --- a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx +++ b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx @@ -410,4 +410,62 @@ describe('DashboardLayout', () => { // Ensure that main content is still rendered expect(screen.getByText('Hello world')).toBeTruthy(); }); + + test('renders the sidebar in collapsed state when navigationMenuOpen is false', () => { + render( + +
Test Content
+
, + ); + + // Expect that menu button has expand action + expect(screen.getAllByLabelText('Expand menu')).toBeTruthy(); + expect(screen.queryByLabelText('Collapse menu')).toBeNull(); + }); + + test('renders the sidebar in expanded state when navigationMenuOpen is true', () => { + render( + +
Test Content
+
, + ); + + expect(screen.getAllByLabelText('Collapse menu')).toBeTruthy(); + }); + + test('calls onNavigationMenuOpen callback when navigationMenuOpen state changes to open', () => { + const mockToggleSidebar = vi.fn(); + const { rerender } = render( + +
Test Content
+
, + ); + + // Trigger sidebar open action + rerender( + +
Test Content
+
, + ); + + expect(mockToggleSidebar).toHaveBeenCalledOnce(); + }); + + test('calls onNavigationMenuClose callback when navigationMenuOpen state changes to close', () => { + const mockToggleSidebar = vi.fn(); + const { rerender } = render( + +
Test Content
+
, + ); + + // Trigger sidebar close action + rerender( + +
Test Content
+
, + ); + + expect(mockToggleSidebar).toHaveBeenCalledOnce(); + }); }); diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx index da545108989..14ef364bd8d 100644 --- a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx +++ b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx @@ -89,6 +89,18 @@ export interface DashboardLayoutProps { * @default false */ hideNavigation?: boolean; + /** A prop that controls the collapsed state of the sidebar. + * @default false + */ + navigationMenuOpen?: boolean; + /** + * Callback function to be executed on navigation menu state changes to open + */ + onNavigationMenuOpen?: () => void; + /** + * Callback function to be executed on navigation menu state changes to closed + */ + onNavigationMenuClose?: () => void; /** * Width of the sidebar when expanded. * @default 320 @@ -131,6 +143,9 @@ function DashboardLayout(props: DashboardLayoutProps) { slots, slotProps, sx, + navigationMenuOpen, + onNavigationMenuOpen, + onNavigationMenuClose, } = props; const theme = useTheme(); @@ -174,6 +189,17 @@ function DashboardLayout(props: DashboardLayoutProps) { const [isNavigationFullyExpanded, setIsNavigationFullyExpanded] = React.useState(isNavigationExpanded); + React.useEffect(() => { + if (typeof navigationMenuOpen === 'boolean') { + setIsNavigationExpanded(navigationMenuOpen); + if (navigationMenuOpen) { + onNavigationMenuOpen?.(); + } else { + onNavigationMenuClose?.(); + } + } + }, [navigationMenuOpen, setIsNavigationExpanded, onNavigationMenuOpen, onNavigationMenuClose]); + React.useEffect(() => { if (isNavigationExpanded) { const drawerWidthTransitionTimeout = setTimeout(() => { @@ -483,6 +509,19 @@ DashboardLayout.propTypes /* remove-proptypes */ = { * @default false */ hideNavigation: PropTypes.bool, + /** + * A prop that controls the collapsed state of the sidebar. + * @default false + */ + navigationMenuOpen: PropTypes.bool, + /** + * Callback function to be executed on navigation menu state changes to closed + */ + onNavigationMenuClose: PropTypes.func, + /** + * Callback function to be executed on navigation menu state changes to open + */ + onNavigationMenuOpen: PropTypes.func, /** * Width of the sidebar when expanded. * @default 320