diff --git a/src/features/navigation-bar/MaterialYouNavigation.ts b/src/features/navigation-bar/MaterialYouNavigation.ts new file mode 100644 index 00000000..7168b15e --- /dev/null +++ b/src/features/navigation-bar/MaterialYouNavigation.ts @@ -0,0 +1,16 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +export const MATERIAL_YOU_NAVIGATION = { + railWidth: 88, + expandedWidth: 280, + bottomBarMinHeight: 80, + touchTarget: 48, + activeIndicatorRadius: 999, + landmarkLabel: 'Primary navigation', +} as const; diff --git a/src/features/navigation-bar/components/DefaultNavBar.tsx b/src/features/navigation-bar/components/DefaultNavBar.tsx index 81ef7f7c..e37b7e1b 100644 --- a/src/features/navigation-bar/components/DefaultNavBar.tsx +++ b/src/features/navigation-bar/components/DefaultNavBar.tsx @@ -117,9 +117,11 @@ export function DefaultNavBar() { pt: 'env(safe-area-inset-top)', width: `calc(100% - ${actualNavBarWidth}px)`, zIndex: theme.zIndex.drawer, + borderBottom: '1px solid', + borderColor: 'divider', }} > - + {!isMobileWidth && ( - setIsCollapsed(false)} color="inherit"> + setIsCollapsed(false)} + color="inherit" + > @@ -156,12 +162,13 @@ export function DefaultNavBar() { )} {title} diff --git a/src/features/navigation-bar/components/DesktopSideBar.tsx b/src/features/navigation-bar/components/DesktopSideBar.tsx index 8c83e6e3..a3dacd84 100644 --- a/src/features/navigation-bar/components/DesktopSideBar.tsx +++ b/src/features/navigation-bar/components/DesktopSideBar.tsx @@ -11,7 +11,6 @@ import List from '@mui/material/List'; import IconButton from '@mui/material/IconButton'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import Divider from '@mui/material/Divider'; import { styled } from '@mui/material/styles'; import { useCallback, useRef } from 'react'; import Box from '@mui/material/Box'; @@ -20,21 +19,18 @@ import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx'; import { useResizeObserver } from '@/base/hooks/useResizeObserver.tsx'; import type { NavbarItem } from '@/features/navigation-bar/NavigationBar.types.ts'; import { NavigationBarItem } from '@/features/navigation-bar/components/NavigationBarItem.tsx'; +import { MATERIAL_YOU_NAVIGATION } from '@/features/navigation-bar/MaterialYouNavigation.ts'; const DrawerHeader = styled('div')(({ theme }) => ({ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', - padding: theme.spacing(0, 1), + minHeight: 72, + padding: theme.spacing(1), // necessary for content to be below app bar ...theme.mixins.toolbar, })); -const MIN_WIDTH_COLLAPSED = undefined; -const MAX_WIDTH_COLLAPSED = 120; -const MIN_WIDTH_EXTENDED = 240; -const MAX_WIDTH_EXTENDED = 400; - export const DesktopSideBar = ({ navBarItems }: { navBarItems: NavbarItem[] }) => { const { isCollapsed, setIsCollapsed, navBarWidth, setNavBarWidth } = useNavBarContext(); const getOptionForDirection = useGetOptionForDirection(); @@ -52,11 +48,15 @@ export const DesktopSideBar = ({ navBarItems }: { navBarItems: NavbarItem[] }) = width: navBarWidth, '& .MuiDrawer-paper': { zIndex: (theme) => theme.zIndex.drawer - 1, + overflowX: 'hidden', + transition: (theme) => theme.transitions.create('width'), }, }} slotProps={{ paper: { ref, + component: 'nav', + 'aria-label': MATERIAL_YOU_NAVIGATION.landmarkLabel, }, }} > @@ -64,17 +64,19 @@ export const DesktopSideBar = ({ navBarItems }: { navBarItems: NavbarItem[] }) = sx={{ pt: 'env(safe-area-inset-top)', pl: 'env(safe-area-inset-left)', - minWidth: isCollapsed ? MIN_WIDTH_COLLAPSED : MIN_WIDTH_EXTENDED, - maxWidth: isCollapsed ? MAX_WIDTH_COLLAPSED : MAX_WIDTH_EXTENDED, + width: isCollapsed + ? MATERIAL_YOU_NAVIGATION.railWidth + : MATERIAL_YOU_NAVIGATION.expandedWidth, }} > - - setIsCollapsed(true)}> - {getOptionForDirection(, )} - + + {!isCollapsed && ( + setIsCollapsed(true)}> + {getOptionForDirection(, )} + + )} - - + {navBarItems.map((navBarItem) => ( ))} diff --git a/src/features/navigation-bar/components/MobileBottomBar.tsx b/src/features/navigation-bar/components/MobileBottomBar.tsx index 8f068c97..c8dda55f 100644 --- a/src/features/navigation-bar/components/MobileBottomBar.tsx +++ b/src/features/navigation-bar/components/MobileBottomBar.tsx @@ -16,6 +16,7 @@ import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx'; import type { NavbarItem } from '@/features/navigation-bar/NavigationBar.types.ts'; import { NavigationBarItem } from '@/features/navigation-bar/components/NavigationBarItem.tsx'; import Stack from '@mui/material/Stack'; +import { MATERIAL_YOU_NAVIGATION } from '@/features/navigation-bar/MaterialYouNavigation.ts'; export const MobileBottomBar = ({ navBarItems }: { navBarItems: NavbarItem[] }) => { const theme = useTheme(); @@ -30,6 +31,8 @@ export const MobileBottomBar = ({ navBarItems }: { navBarItems: NavbarItem[] }) return ( - + {navBarItems.map((item) => ( ({ - listItemProps: isCollapsed ? { p: 0.5, display: 'flex', flexDirection: 'column' } : {}, - listItemIconProps: isCollapsed ? { justifyContent: 'center' } : {}, + listItemProps: isCollapsed + ? { px: 0.5, py: 0.25, display: 'flex', flexDirection: 'column', gap: 0.25 } + : { px: 1.5, py: 1 }, + listItemIconProps: isCollapsed + ? { + width: 64, + minWidth: 64, + height: 32, + borderRadius: MATERIAL_YOU_NAVIGATION.activeIndicatorRadius, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: isActive ? 'primary.main' : 'transparent', + } + : { minWidth: MATERIAL_YOU_NAVIGATION.touchTarget }, }), [isCollapsed], ); @@ -59,7 +72,15 @@ export const NavigationBarItem = ({ @@ -92,9 +116,13 @@ export const NavigationBarItem = ({ lines={1} variant={isCollapsed ? 'caption' : undefined} sx={{ - color: isActive ? 'primary.dark' : undefined, - ...theme.applyStyles('dark', { - color: isActive ? 'primary.light' : undefined, + color: isCollapsed && isActive ? 'text.primary' : undefined, + fontWeight: isActive ? 700 : 500, + ...(!isCollapsed && { + color: isActive ? 'primary.dark' : undefined, + ...theme.applyStyles('dark', { + color: isActive ? 'primary.light' : undefined, + }), }), }} > diff --git a/src/features/theme/services/MaterialYouNavigation.test.ts b/src/features/theme/services/MaterialYouNavigation.test.ts new file mode 100644 index 00000000..a8d016a4 --- /dev/null +++ b/src/features/theme/services/MaterialYouNavigation.test.ts @@ -0,0 +1,25 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { MATERIAL_YOU_NAVIGATION } from '@/features/navigation-bar/MaterialYouNavigation.ts'; + +describe('Material You navigation tokens', () => { + it('uses accessible rail and bottom-bar dimensions', () => { + assert.equal(MATERIAL_YOU_NAVIGATION.railWidth, 88); + assert.equal(MATERIAL_YOU_NAVIGATION.expandedWidth, 280); + assert.equal(MATERIAL_YOU_NAVIGATION.bottomBarMinHeight, 80); + assert.ok(MATERIAL_YOU_NAVIGATION.touchTarget >= 44); + }); + + it('uses pill indicators and an accessible landmark label', () => { + assert.equal(MATERIAL_YOU_NAVIGATION.activeIndicatorRadius, 999); + assert.equal(MATERIAL_YOU_NAVIGATION.landmarkLabel, 'Primary navigation'); + }); +});