Add mobile drawer menu
This commit is contained in:
parent
989f66f7de
commit
bf752d1ee7
7 changed files with 290 additions and 9 deletions
|
@ -16,6 +16,7 @@ import ErrorPage from "../ErrorPage";
|
||||||
import Navigator from "../Navigator";
|
import Navigator from "../Navigator";
|
||||||
import NavigatorButton from "../NavigatorButton/NavigatorButton";
|
import NavigatorButton from "../NavigatorButton/NavigatorButton";
|
||||||
import SideBar from "../SideBar";
|
import SideBar from "../SideBar";
|
||||||
|
import SideBarDrawer from "../SideBarDrawer/SideBarDrawer";
|
||||||
import UserChip from "../UserChip";
|
import UserChip from "../UserChip";
|
||||||
import AppActionContext from "./AppActionContext";
|
import AppActionContext from "./AppActionContext";
|
||||||
import AppHeaderContext from "./AppHeaderContext";
|
import AppHeaderContext from "./AppHeaderContext";
|
||||||
|
@ -162,7 +163,15 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
|
||||||
<Container>
|
<Container>
|
||||||
<div className={classes.header}>
|
<div className={classes.header}>
|
||||||
{isMdUp && <div ref={appHeaderAnchor} />}
|
{isMdUp && <div ref={appHeaderAnchor} />}
|
||||||
{!isMdUp && <>=</>}
|
{!isMdUp && (
|
||||||
|
<SideBarDrawer
|
||||||
|
menuItems={menuStructure}
|
||||||
|
location={location.pathname}
|
||||||
|
user={user}
|
||||||
|
renderConfigure={renderConfigure}
|
||||||
|
onMenuItemClick={navigate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className={classes.spacer} />
|
<div className={classes.spacer} />
|
||||||
<div className={classes.userBar}>
|
<div className={classes.userBar}>
|
||||||
<ThemeSwitch
|
<ThemeSwitch
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { sectionNames } from "@saleor/intl";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import SVG from "react-inlinesvg";
|
import SVG from "react-inlinesvg";
|
||||||
import { useIntl } from "react-intl";
|
import { IntlShape, useIntl } from "react-intl";
|
||||||
|
|
||||||
import { IMenuItem } from "../AppLayout/menuStructure";
|
import { IMenuItem } from "../AppLayout/menuStructure";
|
||||||
import ExpandButton from "./ExpandButton";
|
import ExpandButton from "./ExpandButton";
|
||||||
|
@ -55,6 +55,14 @@ export interface IActiveSubMenu {
|
||||||
label: string | null;
|
label: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getConfigureMenuItem = (intl: IntlShape): IMenuItem => ({
|
||||||
|
ariaLabel: "configure",
|
||||||
|
icon: configurationIcon,
|
||||||
|
label: intl.formatMessage(sectionNames.configuration),
|
||||||
|
testingContextId: "configure",
|
||||||
|
url: configurationMenuUrl
|
||||||
|
});
|
||||||
|
|
||||||
const SideBar: React.FC<SideBarProps> = ({
|
const SideBar: React.FC<SideBarProps> = ({
|
||||||
location,
|
location,
|
||||||
menuItems,
|
menuItems,
|
||||||
|
@ -65,6 +73,7 @@ const SideBar: React.FC<SideBarProps> = ({
|
||||||
const classes = useStyles({});
|
const classes = useStyles({});
|
||||||
const [isShrunk, setShrink] = useLocalStorage("isMenuSmall", false);
|
const [isShrunk, setShrink] = useLocalStorage("isMenuSmall", false);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const configureMenuItem = getConfigureMenuItem(intl);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -107,13 +116,7 @@ const SideBar: React.FC<SideBarProps> = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
isMenuShrunk={isShrunk}
|
isMenuShrunk={isShrunk}
|
||||||
menuItem={{
|
menuItem={configureMenuItem}
|
||||||
ariaLabel: "configure",
|
|
||||||
icon: configurationIcon,
|
|
||||||
label: intl.formatMessage(sectionNames.configuration),
|
|
||||||
testingContextId: "configure",
|
|
||||||
url: configurationMenuUrl
|
|
||||||
}}
|
|
||||||
onClick={onMenuItemClick}
|
onClick={onMenuItemClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
32
src/components/SideBarDrawer/MenuItemBtn.tsx
Normal file
32
src/components/SideBarDrawer/MenuItemBtn.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import { UseNavigatorResult } from "@saleor/hooks/useNavigator";
|
||||||
|
import React from "react";
|
||||||
|
import SVG from "react-inlinesvg";
|
||||||
|
|
||||||
|
import { IMenuItem } from "../AppLayout/menuStructure";
|
||||||
|
import useStyles from "./styles";
|
||||||
|
|
||||||
|
export interface MenuItemBtnProps {
|
||||||
|
menuItem: IMenuItem;
|
||||||
|
onClick: UseNavigatorResult;
|
||||||
|
}
|
||||||
|
const MenuItemBtn: React.FC<MenuItemBtnProps> = ({ menuItem, onClick }) => {
|
||||||
|
const classes = useStyles({});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={classes.menuItemBtn}
|
||||||
|
data-test="menu-item-label"
|
||||||
|
data-test-id={menuItem.testingContextId}
|
||||||
|
onClick={() => onClick(menuItem.url)}
|
||||||
|
>
|
||||||
|
{menuItem.icon && <SVG className={classes.icon} src={menuItem.icon} />}
|
||||||
|
<Typography aria-label={menuItem.ariaLabel} className={classes.label}>
|
||||||
|
{menuItem.label}
|
||||||
|
</Typography>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
MenuItemBtn.displayName = "MenuItemBtn";
|
||||||
|
export default MenuItemBtn;
|
37
src/components/SideBarDrawer/SideBarDrawer.stories.tsx
Normal file
37
src/components/SideBarDrawer/SideBarDrawer.stories.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { staffMember } from "@saleor/staff/fixtures";
|
||||||
|
import Decorator from "@saleor/storybook/Decorator";
|
||||||
|
import { storiesOf } from "@storybook/react";
|
||||||
|
import { config } from "@test/intl";
|
||||||
|
import React from "react";
|
||||||
|
import { createIntl } from "react-intl";
|
||||||
|
|
||||||
|
import createMenuStructure from "../AppLayout/menuStructure";
|
||||||
|
import SideBarDrawer from "./SideBarDrawer";
|
||||||
|
|
||||||
|
const intl = createIntl(config);
|
||||||
|
const user = {
|
||||||
|
__typename: staffMember.__typename,
|
||||||
|
avatar: {
|
||||||
|
__typename: staffMember.avatar.__typename,
|
||||||
|
url: staffMember.avatar.url
|
||||||
|
},
|
||||||
|
email: staffMember.email,
|
||||||
|
firstName: "Adam Evan",
|
||||||
|
id: staffMember.id,
|
||||||
|
isStaff: true,
|
||||||
|
lastName: "Newton",
|
||||||
|
note: null,
|
||||||
|
userPermissions: staffMember.userPermissions
|
||||||
|
};
|
||||||
|
|
||||||
|
storiesOf("Generics / Mobile Side Menu", module)
|
||||||
|
.addDecorator(Decorator)
|
||||||
|
.add("default", () => (
|
||||||
|
<SideBarDrawer
|
||||||
|
location="/"
|
||||||
|
menuItems={createMenuStructure(intl)}
|
||||||
|
onMenuItemClick={() => undefined}
|
||||||
|
renderConfigure={true}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
));
|
132
src/components/SideBarDrawer/SideBarDrawer.tsx
Normal file
132
src/components/SideBarDrawer/SideBarDrawer.tsx
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import logoLight from "@assets/images/logo-sidebar-light.svg";
|
||||||
|
import { Typography } from "@material-ui/core";
|
||||||
|
import Drawer from "@material-ui/core/Drawer";
|
||||||
|
import ArrowLeftIcon from "@material-ui/icons/ArrowLeft";
|
||||||
|
import MenuIcon from "@material-ui/icons/Menu";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import React from "react";
|
||||||
|
import SVG from "react-inlinesvg";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import { IMenuItem } from "../AppLayout/menuStructure";
|
||||||
|
import { getConfigureMenuItem, SideBarProps } from "../SideBar/SideBar";
|
||||||
|
import SquareButton from "../SquareButton";
|
||||||
|
import MenuItemBtn from "./MenuItemBtn";
|
||||||
|
import useStyles from "./styles";
|
||||||
|
|
||||||
|
export type SideBarDrawerProps = SideBarProps;
|
||||||
|
|
||||||
|
const SideBarDrawer: React.FC<SideBarDrawerProps> = ({
|
||||||
|
menuItems,
|
||||||
|
onMenuItemClick,
|
||||||
|
renderConfigure,
|
||||||
|
user
|
||||||
|
}) => {
|
||||||
|
const [isOpened, setOpened] = React.useState(false);
|
||||||
|
const classes = useStyles({});
|
||||||
|
const intl = useIntl();
|
||||||
|
const [activeMenu, setActiveMenu] = React.useState<IMenuItem>(null);
|
||||||
|
const [showSubmenu, setShowSubmenu] = React.useState(false);
|
||||||
|
const container = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const configureMenuItem = getConfigureMenuItem(intl);
|
||||||
|
|
||||||
|
const handleMenuItemClick = (url: string) => {
|
||||||
|
setOpened(false);
|
||||||
|
setShowSubmenu(false);
|
||||||
|
onMenuItemClick(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuItemWithChildrenClick = (menuItem: IMenuItem) => {
|
||||||
|
setActiveMenu(menuItem);
|
||||||
|
setShowSubmenu(true);
|
||||||
|
container.current.scrollTo({
|
||||||
|
top: 0
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SquareButton onClick={() => setOpened(true)}>
|
||||||
|
<MenuIcon />
|
||||||
|
</SquareButton>
|
||||||
|
<Drawer
|
||||||
|
classes={{
|
||||||
|
paper: classes.root
|
||||||
|
}}
|
||||||
|
open={isOpened}
|
||||||
|
onClose={() => setOpened(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(classes.container, {
|
||||||
|
[classes.containerSubMenu]: showSubmenu
|
||||||
|
})}
|
||||||
|
ref={container}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(classes.innerContainer, {
|
||||||
|
[classes.secondaryContentActive]: showSubmenu
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={classes.content}>
|
||||||
|
<SVG className={classes.logo} src={logoLight} />
|
||||||
|
{menuItems.map(menuItem => {
|
||||||
|
if (
|
||||||
|
menuItem.permission &&
|
||||||
|
!user.userPermissions
|
||||||
|
.map(perm => perm.code)
|
||||||
|
.includes(menuItem.permission)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItemBtn
|
||||||
|
menuItem={menuItem}
|
||||||
|
onClick={
|
||||||
|
menuItem.children
|
||||||
|
? () => handleMenuItemWithChildrenClick(menuItem)
|
||||||
|
: handleMenuItemClick
|
||||||
|
}
|
||||||
|
key={menuItem.ariaLabel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{renderConfigure && (
|
||||||
|
<MenuItemBtn
|
||||||
|
menuItem={configureMenuItem}
|
||||||
|
onClick={handleMenuItemClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{activeMenu && (
|
||||||
|
<div className={classes.content}>
|
||||||
|
<div className={classes.subMenuTopBar}>
|
||||||
|
<div className={classes.activeMenuLabel}>
|
||||||
|
<SVG className={classes.icon} src={activeMenu.icon} />
|
||||||
|
<Typography className={classes.label}>
|
||||||
|
{activeMenu.label}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<SquareButton onClick={() => setShowSubmenu(false)}>
|
||||||
|
<ArrowLeftIcon />
|
||||||
|
</SquareButton>
|
||||||
|
</div>
|
||||||
|
{activeMenu.children.map(subMenuItem => (
|
||||||
|
<MenuItemBtn
|
||||||
|
menuItem={subMenuItem}
|
||||||
|
onClick={handleMenuItemClick}
|
||||||
|
key={subMenuItem.ariaLabel}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SideBarDrawer.displayName = "SideBarDrawer";
|
||||||
|
export default SideBarDrawer;
|
0
src/components/SideBarDrawer/index.ts
Normal file
0
src/components/SideBarDrawer/index.ts
Normal file
68
src/components/SideBarDrawer/styles.ts
Normal file
68
src/components/SideBarDrawer/styles.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
activeMenuLabel: {
|
||||||
|
display: "flex"
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
overflowX: "hidden",
|
||||||
|
width: "100%"
|
||||||
|
},
|
||||||
|
containerSubMenu: {
|
||||||
|
"&$container": {
|
||||||
|
overflow: "hidden"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
width: "50%"
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
marginRight: theme.spacing(2)
|
||||||
|
},
|
||||||
|
innerContainer: {
|
||||||
|
display: "flex",
|
||||||
|
position: "relative",
|
||||||
|
right: 0,
|
||||||
|
transition: theme.transitions.duration.short + "ms",
|
||||||
|
width: "200%"
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontWeight: "bold"
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
display: "block",
|
||||||
|
marginBottom: theme.spacing(4)
|
||||||
|
},
|
||||||
|
menuItemBtn: {
|
||||||
|
alignItems: "center",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
display: "flex",
|
||||||
|
marginBottom: theme.spacing(3),
|
||||||
|
padding: 0
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
borderBottomRightRadius: 32,
|
||||||
|
borderTopRightRadius: 32,
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
width: 260
|
||||||
|
},
|
||||||
|
secondaryContentActive: {
|
||||||
|
right: "100%"
|
||||||
|
},
|
||||||
|
subMenuTopBar: {
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: theme.spacing(3)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "SideBarDrawer"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default useStyles;
|
Loading…
Reference in a new issue