diff --git a/docs/translations/api-docs/dashboard-layout/dashboard-layout.json b/docs/translations/api-docs/dashboard-layout/dashboard-layout.json index 0a9ddf79fb6..1dd92dd0858 100644 --- a/docs/translations/api-docs/dashboard-layout/dashboard-layout.json +++ b/docs/translations/api-docs/dashboard-layout/dashboard-layout.json @@ -10,7 +10,7 @@ "description": "Whether the sidebar should not be collapsible to a mini variant in desktop and tablet viewports." }, "hideNavigation": { - "description": "Whether the navigation bar and menu icon should be hidden" + "description": "Whether the navigation bar and menu icon should be hidden." }, "navigation": { "description": "Navigation definition for the dashboard. Find out more." diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx index dde3582feed..8b89572c504 100644 --- a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx +++ b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx @@ -207,13 +207,18 @@ describe('DashboardLayout', () => { icon: , }, { - segment: 'dynamic', + segment: 'dynamic/override', + title: 'Dynamic Override', + icon: , + }, + { + segment: 'dynamicMoreOnly', title: 'Dynamic', icon: , pattern: 'dynamic/:dynamicId', }, { - segment: 'optional', + segment: 'optionalMoreOnly', title: 'Optional', pattern: 'optional{/:optionalId}?', }, @@ -268,11 +273,23 @@ describe('DashboardLayout', () => { expect(within(desktopNavigation).getByRole('link', { name: 'Dynamic' })).toHaveClass( 'Mui-selected', ); + expect( + within(desktopNavigation).getByRole('link', { name: 'Dynamic Override' }), + ).not.toHaveClass('Mui-selected'); rerender(); expect(within(desktopNavigation).getByRole('link', { name: 'Dynamic' })).not.toHaveClass( 'Mui-selected', ); + // Does not show multiple selected items if a dynamic segment is overridden by a more specific segment + rerender(); + expect(within(desktopNavigation).getByRole('link', { name: 'Dynamic Override' })).toHaveClass( + 'Mui-selected', + ); + expect(within(desktopNavigation).getByRole('link', { name: 'Dynamic' })).not.toHaveClass( + 'Mui-selected', + ); + rerender(); expect(within(desktopNavigation).getByRole('link', { name: 'Optional' })).toHaveClass( 'Mui-selected', diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx index 8145f927215..4a3c29f6fb1 100644 --- a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx +++ b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx @@ -19,6 +19,7 @@ import { DashboardSidebarSubNavigation } from './DashboardSidebarSubNavigation'; import { ToolbarActions } from './ToolbarActions'; import { AppTitle, AppTitleProps } from './AppTitle'; import { getDrawerSxTransitionMixin, getDrawerWidthTransitionMixin } from './utils'; +import { MINI_DRAWER_WIDTH } from './shared'; import type { Branding, Navigation } from '../AppProvider'; const AppBar = styled(MuiAppBar)(({ theme }) => ({ @@ -80,17 +81,17 @@ export interface DashboardLayoutProps { */ navigation?: Navigation; /** - * Whether the sidebar should not be collapsible to a mini variant in desktop and tablet viewports. + * Whether the sidebar should start collapsed in desktop size screens. * @default false */ - disableCollapsibleSidebar?: boolean; + defaultSidebarCollapsed?: boolean; /** - * Whether the sidebar should start collapsed in desktop size screens. + * Whether the sidebar should not be collapsible to a mini variant in desktop and tablet viewports. * @default false */ - defaultSidebarCollapsed?: boolean; + disableCollapsibleSidebar?: boolean; /** - * Whether the navigation bar and menu icon should be hidden + * Whether the navigation bar and menu icon should be hidden. * @default false */ hideNavigation?: boolean; @@ -130,8 +131,8 @@ function DashboardLayout(props: DashboardLayoutProps) { children, branding: brandingProp, navigation: navigationProp, - disableCollapsibleSidebar = false, defaultSidebarCollapsed = false, + disableCollapsibleSidebar = false, hideNavigation = false, sidebarExpandedWidth = 320, slots, @@ -182,6 +183,8 @@ function DashboardLayout(props: DashboardLayoutProps) { const [isNavigationFullyExpanded, setIsNavigationFullyExpanded] = React.useState(isNavigationExpanded); + const [isNavigationFullyCollapsed, setIsNavigationFullyCollapsed] = + React.useState(!isNavigationExpanded); React.useEffect(() => { if (isNavigationExpanded) { @@ -197,7 +200,19 @@ function DashboardLayout(props: DashboardLayoutProps) { return () => {}; }, [isNavigationExpanded, theme]); - const selectedItemIdRef = React.useRef(''); + React.useEffect(() => { + if (!isNavigationExpanded) { + const drawerWidthTransitionTimeout = setTimeout(() => { + setIsNavigationFullyCollapsed(true); + }, theme.transitions.duration.leavingScreen); + + return () => clearTimeout(drawerWidthTransitionTimeout); + } + + setIsNavigationFullyCollapsed(false); + + return () => {}; + }, [isNavigationExpanded, theme]); const handleSetNavigationExpanded = React.useCallback( (newExpanded: boolean) => () => { @@ -211,17 +226,9 @@ function DashboardLayout(props: DashboardLayoutProps) { }, [isNavigationExpanded, setIsNavigationExpanded]); const handleNavigationLinkClick = React.useCallback(() => { - selectedItemIdRef.current = ''; setIsMobileNavigationExpanded(false); }, [setIsMobileNavigationExpanded]); - // If useEffect was used, the reset would also happen on the client render after SSR which we don't need - React.useMemo(() => { - if (navigation) { - selectedItemIdRef.current = ''; - } - }, [navigation]); - const isDesktopMini = !disableCollapsibleSidebar && !isDesktopNavigationExpanded; const isMobileMini = !disableCollapsibleSidebar && !isMobileNavigationExpanded; @@ -279,8 +286,8 @@ function DashboardLayout(props: DashboardLayoutProps) { onLinkClick={handleNavigationLinkClick} isMini={isMini} isFullyExpanded={isNavigationFullyExpanded} + isFullyCollapsed={isNavigationFullyCollapsed} hasDrawerTransitions={hasDrawerTransitions} - selectedItemId={selectedItemIdRef.current} /> {SidebarFooterSlot ? ( @@ -292,6 +299,7 @@ function DashboardLayout(props: DashboardLayoutProps) { SidebarFooterSlot, handleNavigationLinkClick, hasDrawerTransitions, + isNavigationFullyCollapsed, isNavigationFullyExpanded, navigation, slotProps?.sidebarFooter, @@ -300,7 +308,7 @@ function DashboardLayout(props: DashboardLayoutProps) { const getDrawerSharedSx = React.useCallback( (isMini: boolean, isTemporary: boolean) => { - const drawerWidth = isMini ? 64 : sidebarExpandedWidth; + const drawerWidth = isMini ? MINI_DRAWER_WIDTH : sidebarExpandedWidth; return { displayPrint: 'none', @@ -335,7 +343,7 @@ function DashboardLayout(props: DashboardLayoutProps) { }} > - +