Skip to content

Commit

Permalink
[DEV-1243] Guide mobile menu (#535)
Browse files Browse the repository at this point in the history
* Implementation of mobile guide menu

* refactor top position value

* refactor

* design improvements

* fix duplicated translation key

* Create thirty-eels-notice.md

* udpate changeset

---------

Co-authored-by: marcobottaro <[email protected]>
  • Loading branch information
jeremygordillo and marcobottaro authored Jan 11, 2024
1 parent d0fbfb2 commit 808285d
Show file tree
Hide file tree
Showing 6 changed files with 363 additions and 215 deletions.
5 changes: 5 additions & 0 deletions .changeset/thirty-eels-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nextjs-website": patch
---

[DEV-1243] Guide mobile menu
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const Page = async ({ params }: { params: Params }) => {
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column-reverse', lg: 'row' },
flexDirection: { xs: 'column', lg: 'row' },
margin: '0 auto',
maxWidth: '1900px',
}}
Expand Down
341 changes: 128 additions & 213 deletions apps/nextjs-website/src/components/atoms/GuideMenu/GuideMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,239 +1,154 @@
'use client';
import React, { ReactNode } from 'react';
import NextLink from 'next/link';
import React, { Fragment, useCallback, useEffect, useState } from 'react';
import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { usePathname } from 'next/navigation';
import { TreeItem, TreeView, treeItemClasses } from '@mui/lab';
import { styled } from '@mui/material/styles';
import { parseMenu } from 'gitbook-docs/parseMenu';
import { RenderingComponents, renderMenu } from 'gitbook-docs/renderMenu';
import Dropdown from '@/components/atoms/Dropdown/Dropdown';
import { ExpandLess, ExpandMore } from '@mui/icons-material';
import { Box, useTheme } from '@mui/material';
import { useScrollUp } from '../ProductHeader/useScrollUp';
import ArrowDropDownOutlinedIcon from '@mui/icons-material/ArrowDropDownOutlined';
import CloseIcon from '@mui/icons-material/Close';
import {
Dialog,
DialogContent,
DialogTitle,
IconButton,
Stack,
Theme,
useMediaQuery,
useTheme,
} from '@mui/material';
import { SITE_HEADER_HEIGHT } from '@/components/molecules/SiteHeader/SiteHeader';
import { useScrollUp } from '../ProductHeader/useScrollUp';
import GuideMenuItems, { type GuideMenuItemsProps } from './Menu';
import { useTranslations } from 'next-intl';

type GuideMenuProps = {
linkPrefix: string;
assetsPrefix: string;
menu: string;
guideName: string;
versionName: string;
versions: { name: string; path: string }[];
};

export const PRODUCT_HEADER_HEIGHT = 80;

const StyledTreeItem = styled(TreeItem)(({ theme }) => ({
[`&`]: {
'--x': 32,
},
[`& .${treeItemClasses.content}`]: {
boxSizing: 'border-box',
flexDirection: 'row-reverse',
width: '100%',
paddingTop: 0,
paddingBottom: 0,
paddingLeft: 0,
paddingRight: 32,
alignItems: 'space-between',
},
[`& .${treeItemClasses.content}:has(.${treeItemClasses.iconContainer}:empty)`]:
{
paddingRight: 0,
},
[`& .${treeItemClasses.iconContainer}`]: {
justifyContent: 'flex-end',
marginRight: 0,
paddingRight: 0,
paddingLeft: 0,
},
[`& .${treeItemClasses.iconContainer}:empty`]: {
display: 'none',
},
[`& .${treeItemClasses.content} > .${treeItemClasses.label}`]: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
position: 'relative',
},
[`& .${treeItemClasses.content} > .${treeItemClasses.label} > a`]: {
paddingTop: 16,
paddingBottom: 16,
paddingRight: 32,
},
[`& ul`]: {
paddingLeft: 0,
'--y': 'calc(var(--x) + 0)',
},
[`& li`]: {
'--x': 'calc(var(--y) + 24)',
},
['& a']: {
paddingLeft: 'calc(1px * var(--x))',
},
[`& .${treeItemClasses.group}`]: {
marginLeft: 0,
marginRight: 0,
},
[`& .${treeItemClasses.group} .${treeItemClasses.label}`]: {
paddingLeft: 0,
paddingRight: 0,
},
[`& .${treeItemClasses.label}`]: {
padding: 0,
paddingLeft: 0,
},
[`& .${treeItemClasses.root}`]: {
margin: 0,
paddingLeft: 0,
},
[`& .${treeItemClasses.selected}`]: {
borderRight: `2px solid ${theme.palette.primary.dark}`,
},
[`& .${treeItemClasses.selected} > .${treeItemClasses.label} > *`]: {
color: theme.palette.primary.dark,
},
}));
type GuideMenuProps = GuideMenuItemsProps;

const components: RenderingComponents<ReactNode> = {
Item: ({ href, title, children }) => {
const label = (
<Typography
variant='sidenav'
component={NextLink}
href={href}
style={{ textDecoration: 'none' }}
>
{title}
</Typography>
);
export const PRODUCT_HEADER_HEIGHT = 75;

return (
<StyledTreeItem
key={href}
nodeId={href}
label={label}
disabled={false}
icon={href.startsWith('http') ? <OpenInNewIcon /> : undefined}
>
{children}
</StyledTreeItem>
);
},
Title: ({ children }) => (
<Typography
color='text.secondary'
style={{
paddingLeft: 32,
paddingTop: 24,
paddingBottom: 0,
textDecoration: 'none',
fontSize: 14,
fontWeight: 700,
}}
textTransform='uppercase'
>
{children}
</Typography>
),
};

const GuideMenu = ({
menu,
assetsPrefix,
linkPrefix,
guideName,
versionName,
versions,
}: GuideMenuProps) => {
const GuideMenu = (menuProps: GuideMenuProps) => {
const [open, setOpen] = useState(false);
const { palette } = useTheme();
const t = useTranslations('shared');
const t = useTranslations('productGuidePage');
const isDesktop = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg'));
const scrollUp = useScrollUp();
const currentPath = usePathname();

const segments = currentPath.split('/');
const expanded = segments.map((_, i) => segments.slice(0, i + 1).join('/'));

const top = scrollUp
? SITE_HEADER_HEIGHT + PRODUCT_HEADER_HEIGHT
: PRODUCT_HEADER_HEIGHT;

const height = `calc(100vh - ${top}px)`;

const handleClick = useCallback(() => {
setOpen((prev) => !prev);
}, []);

useEffect(() => {
if (isDesktop) {
setOpen(false);
}
}, [isDesktop]);

const items = (
<GuideMenuItems
{...menuProps}
currentPath={currentPath}
expanded={expanded}
/>
);

return (
<Box
sx={{
backgroundColor: palette.grey[50],
display: 'flex',
flexDirection: 'column',
flexShrink: 0,
position: { lg: 'sticky' },
top: { lg: top },
height: { lg: `calc(100vh - ${top}px)` },
overflowY: 'auto',
transition: 'all 0.5s linear',
scrollbarWidth: 'thin',
width: { lg: '347px' },
}}
>
<Box
<Fragment>
<Stack
sx={{
display: 'flex',
flexDirection: 'column',
padding: '80px 0',
flexGrow: { lg: 0 },
flexShrink: { lg: 0 },
backgroundColor: palette.grey[50],
flexShrink: 0,
position: 'sticky',
top,
height: { lg: height },
overflowY: 'auto',
transition: 'all 0.5s linear',
scrollbarWidth: 'thin',
width: { lg: '347px' },
zIndex: 1,
}}
>
<Typography
variant='h6'
<Stack
sx={{
padding: '16px 32px',
verticalAlign: 'middle',
}}
>
{guideName}
</Typography>
<Dropdown
label={`${t('version')} ${versionName}`}
items={versions.map((version) => ({
href: version.path,
label: version.name,
}))}
icons={{ opened: <ExpandLess />, closed: <ExpandMore /> }}
buttonStyle={{
color: palette.action.active,
display: 'flex',
justifyContent: 'space-between',
padding: '16px 32px',
}}
menuStyle={{
style: {
width: '347px',
maxWidth: '347px',
left: 0,
right: 0,
},
}}
menuAnchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
padding: { lg: '80px 0' },
flexGrow: { lg: 0 },
flexShrink: { lg: 0 },
}}
/>
<TreeView
defaultCollapseIcon={<ExpandLessIcon />}
defaultExpanded={expanded}
selected={currentPath}
defaultExpandIcon={<ExpandMoreIcon />}
>
{renderMenu(
parseMenu(menu, { assetsPrefix, linkPrefix }),
React,
components
)}
</TreeView>
</Box>
</Box>
<Stack
direction='row'
alignItems='center'
justifyContent='flex-start'
onClick={isDesktop ? undefined : handleClick}
sx={{
padding: '12px 24px',
cursor: 'pointer',
display: { lg: 'none' },
}}
>
<Typography
variant='h6'
sx={{
fontSize: '16px!important',
verticalAlign: 'middle',
color: palette.primary.main,
}}
>
{t('tableOfContents')}
</Typography>
<IconButton
size='small'
sx={{ display: { lg: 'none' }, color: palette.primary.main }}
>
<ArrowDropDownOutlinedIcon sx={{ width: 24, height: 24 }} />
</IconButton>
</Stack>
{isDesktop && items}
</Stack>
</Stack>
{!isDesktop && (
<Dialog open={open} onClose={handleClick} fullScreen>
<DialogTitle
component={Stack}
direction='row'
alignItems='center'
justifyContent='flex-start'
sx={{
padding: '12px 24px',
}}
>
<Typography
variant='h6'
sx={{
flexGrow: 1,
flexShrink: 0,
fontSize: '16px!important',
verticalAlign: 'middle',
color: palette.primary.main,
}}
>
{t('tableOfContents')}
</Typography>
<IconButton
aria-label='close'
onClick={handleClick}
sx={{
color: palette.primary.main,
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent sx={{ px: 0 }}>{items}</DialogContent>
</Dialog>
)}
</Fragment>
);
};

Expand Down
Loading

0 comments on commit 808285d

Please sign in to comment.