Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add prop sidebarCollapsed and onToggleSidebar #4516

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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: <DashboardIcon />,
},
{
segment: 'orders',
title: 'Orders',
icon: <ShoppingCartIcon />,
},
{
segment: 'reports',
title: 'Reports',
icon: <BarChartIcon />,
},
];

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 (
<Box
sx={{
py: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
}}
>
<Typography>Dashboard content for {pathname}</Typography>
<Button onClick={() => toggleSidebar()}>Toggle Sidebar</Button>
</Box>
);
}

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 (
<AppProvider
navigation={NAVIGATION}
router={router}
theme={demoTheme}
window={demoWindow}
>
<DashboardLayout navigationMenuOpen={navigationMenuOpen}>
<DemoPageContent
pathname={pathname}
toggleSidebar={() => toggleSidebar(!navigationMenuOpen)}
/>
</DashboardLayout>
</AppProvider>
);
}

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;
Original file line number Diff line number Diff line change
@@ -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: <DashboardIcon />,
},
{
segment: 'orders',
title: 'Orders',
icon: <ShoppingCartIcon />,
},
{
segment: 'reports',
title: 'Reports',
icon: <BarChartIcon />,
},
];

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 (
<Box
sx={{
py: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
}}
>
<Typography>Dashboard content for {pathname}</Typography>
<Button onClick={() => toggleSidebar()}>Toggle Sidebar</Button>
</Box>
);
}

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<boolean>(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const [navigationMenuOpen, toggleSidebar] = React.useState<boolean>(true);
const [navigationMenuOpen, setNavigationMenuOpen] = React.useState<boolean>(true);

const router = React.useMemo<Router>(() => {
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 (
<AppProvider
navigation={NAVIGATION}
router={router}
theme={demoTheme}
window={demoWindow}
>
<DashboardLayout navigationMenuOpen={navigationMenuOpen}>
<DemoPageContent
pathname={pathname}
toggleSidebar={() => toggleSidebar(!navigationMenuOpen)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also show the typical usage for the new callbacks in this example, which would change the navigationMenuOpen state accordingly.

/>
</DashboardLayout>
</AppProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<DashboardLayout navigationMenuOpen={navigationMenuOpen}>
<DemoPageContent
pathname={pathname}
toggleSidebar={() => toggleSidebar(!navigationMenuOpen)}
/>
</DashboardLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

@apedroferreira apedroferreira Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section needs to be a bit more complete as in referring to the whole concept of using "controlled state" for the navigation menu, as well as what kind of use case that could serve.

We also need to document the new callbacks, and how they could be used together with navigationMenuOpen.

Anyway, I can take care of the documentation part when the functionality is done unless you really want to tackle it!


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%`.
Expand Down
3 changes: 3 additions & 0 deletions docs/pages/toolpad/core/api/dashboard-layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -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<br>&#124;&nbsp;string" },
"default": "320"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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." },
Expand Down
58 changes: 58 additions & 0 deletions packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for adding tests, they look good!

render(
<DashboardLayout navigationMenuOpen={false}>
<div>Test Content</div>
</DashboardLayout>,
);

// 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(
<DashboardLayout navigationMenuOpen>
<div>Test Content</div>
</DashboardLayout>,
);

expect(screen.getAllByLabelText('Collapse menu')).toBeTruthy();
});

test('calls onNavigationMenuOpen callback when navigationMenuOpen state changes to open', () => {
const mockToggleSidebar = vi.fn();
const { rerender } = render(
<DashboardLayout navigationMenuOpen={false} onNavigationMenuClose={mockToggleSidebar}>
<div>Test Content</div>
</DashboardLayout>,
);

// Trigger sidebar open action
rerender(
<DashboardLayout navigationMenuOpen onNavigationMenuClose={mockToggleSidebar}>
<div>Test Content</div>
</DashboardLayout>,
);

expect(mockToggleSidebar).toHaveBeenCalledOnce();
});

test('calls onNavigationMenuClose callback when navigationMenuOpen state changes to close', () => {
const mockToggleSidebar = vi.fn();
const { rerender } = render(
<DashboardLayout navigationMenuOpen onNavigationMenuClose={mockToggleSidebar}>
<div>Test Content</div>
</DashboardLayout>,
);

// Trigger sidebar close action
rerender(
<DashboardLayout navigationMenuOpen={false} onNavigationMenuClose={mockToggleSidebar}>
<div>Test Content</div>
</DashboardLayout>,
);

expect(mockToggleSidebar).toHaveBeenCalledOnce();
});
});
Loading
Loading