From 02d33e23056d8dd178f71e3cc693743a8f172642 Mon Sep 17 00:00:00 2001 From: Johannes Munker <56400587+jomunker@users.noreply.github.com> Date: Thu, 22 Feb 2024 16:10:15 +0100 Subject: [PATCH] Collapsed menu (#1233) With this PR the Menu will stay open in collapsed variant showing only the icons. **Note:** The chevron arrows which are currently cut of will be fixed in another PR. ## Screen recording https://github.com/vivid-planet/comet/assets/56400587/6034a7e2-3f25-4c6d-b4a8-2b17331574ec --------- Co-authored-by: Ricky James Smith Co-authored-by: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com> Co-authored-by: Thomas Dax --- .changeset/fair-waves-breathe.md | 5 + .gitignore | 1 + packages/admin/admin/src/mui/menu/Item.tsx | 5 +- .../admin/admin/src/mui/menu/ItemGroup.tsx | 78 +++++++++++-- .../admin/admin/src/mui/menu/Menu.styles.ts | 104 ++++++++++-------- packages/admin/admin/src/mui/menu/Menu.tsx | 26 ++--- 6 files changed, 148 insertions(+), 71 deletions(-) create mode 100644 .changeset/fair-waves-breathe.md diff --git a/.changeset/fair-waves-breathe.md b/.changeset/fair-waves-breathe.md new file mode 100644 index 0000000000..7058dcf2b5 --- /dev/null +++ b/.changeset/fair-waves-breathe.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": minor +--- + +Show icons in permanent menu even in closed state. diff --git a/.gitignore b/.gitignore index 1fee62d94c..e2df5b3c2c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ lang/ .pnp.* junit.xml .env.local +/.idea/** diff --git a/packages/admin/admin/src/mui/menu/Item.tsx b/packages/admin/admin/src/mui/menu/Item.tsx index f23cf11ecb..c170a871c1 100644 --- a/packages/admin/admin/src/mui/menu/Item.tsx +++ b/packages/admin/admin/src/mui/menu/Item.tsx @@ -121,6 +121,7 @@ const Item: React.FC & MenuItemProps & MuiListItemProp if (level > 2) throw new Error("Maximum nesting level of 2 exceeded."); const hasIcon = !!icon; + const showText = context.open || level !== 1; const listItemClasses = [classes.root]; if (level === 1) listItemClasses.push(classes.level1); @@ -131,8 +132,8 @@ const Item: React.FC & MenuItemProps & MuiListItemProp return ( - {hasIcon && {icon}} - + {hasIcon && {icon}} + {showText && } {!!secondaryAction && secondaryAction} ); diff --git a/packages/admin/admin/src/mui/menu/ItemGroup.tsx b/packages/admin/admin/src/mui/menu/ItemGroup.tsx index 6c41d587af..b8dee0e17b 100644 --- a/packages/admin/admin/src/mui/menu/ItemGroup.tsx +++ b/packages/admin/admin/src/mui/menu/ItemGroup.tsx @@ -1,31 +1,93 @@ -import { Box, ComponentsOverrides, Theme, Typography } from "@mui/material"; +import { Box, ComponentsOverrides, Theme, Tooltip, Typography } from "@mui/material"; import { createStyles, WithStyles, withStyles } from "@mui/styles"; +import clsx from "clsx"; import * as React from "react"; +import { FormattedMessage, MessageDescriptor, useIntl } from "react-intl"; -export type MenuItemGroupClassKey = "root" | "title"; +import { MenuContext } from "./Context"; + +export type MenuItemGroupClassKey = "root" | "title" | "titleMenuOpen" | "titleContainer" | "titleContainerMenuOpen"; const styles = (theme: Theme) => createStyles({ root: { marginTop: theme.spacing(8) }, title: { fontWeight: theme.typography.fontWeightBold, - fontSize: 14, + fontSize: 12, + border: `2px solid ${theme.palette.grey[100]}`, + borderRadius: 20, + padding: theme.spacing(0.5, 2), lineHeight: "20px", + color: `${theme.palette.grey[300]}`, + }, + titleMenuOpen: { + fontSize: 14, + border: `2px solid ${theme.palette.common.white}`, + borderRadius: "initial", + padding: 0, + color: theme.palette.common.black, + }, + titleContainer: { borderBottom: `1px solid ${theme.palette.grey[50]}`, + display: "flex", + justifyContent: "center", + padding: `${theme.spacing(2)} 0`, + }, + titleContainerMenuOpen: { + justifyContent: "flex-start", padding: theme.spacing(2, 4), }, }); export interface MenuItemGroupProps { - title?: React.ReactNode; + title: React.ReactNode; + shortTitle?: React.ReactNode; } -const ItemGroup: React.FC & MenuItemGroupProps>> = ({ title, children, classes }) => { +const ItemGroup: React.FC & MenuItemGroupProps>> = ({ title, shortTitle, children, classes }) => { + const { open: menuOpen } = React.useContext(MenuContext); + const intl = useIntl(); + let displayedTitle = title; + + function isFormattedMessage(node: React.ReactNode): node is React.ReactElement { + return !!node && React.isValidElement(node) && node.type === FormattedMessage; + } + + function getInitials(title: React.ReactNode) { + let titleAsString: string; + if (typeof title === "string") { + titleAsString = title; + } else if (isFormattedMessage(title)) { + titleAsString = intl.formatMessage(title.props); + } else { + throw new TypeError("Title must be either a string or a FormattedMessage"); + } + const words = titleAsString.split(/\s+/).filter((word) => word.match(/[A-Za-z]/)); + + if (words.length > 3) { + console.warn("Title has more than 3 words, only the first 3 will be used."); + + return words + .slice(0, 3) + .map((word) => word[0].toUpperCase()) + .join(""); + } + return words.map((word) => word[0].toUpperCase()).join(""); + } + + if (!menuOpen) { + displayedTitle = shortTitle || getInitials(title); + } + return ( - - {title} - + + + + {displayedTitle} + + + {children} ); diff --git a/packages/admin/admin/src/mui/menu/Menu.styles.ts b/packages/admin/admin/src/mui/menu/Menu.styles.ts index 52336ac2bb..325eae7730 100644 --- a/packages/admin/admin/src/mui/menu/Menu.styles.ts +++ b/packages/admin/admin/src/mui/menu/Menu.styles.ts @@ -1,58 +1,66 @@ -import { Theme } from "@mui/material"; -import { createStyles } from "@mui/styles"; +import { CSSObject, Drawer as MuiDrawer, DrawerProps, Theme } from "@mui/material"; +import { styled } from "@mui/material/styles"; +import { createStyles, StyledComponent } from "@mui/styles"; +import { MUIStyledCommonProps } from "@mui/system"; +import * as React from "react"; -import { MenuProps } from "./Menu"; +import { MasterLayoutContext } from "../MasterLayoutContext"; +import { DEFAULT_DRAWER_WIDTH, DEFAULT_DRAWER_WIDTH_COLLAPSED, MenuProps } from "./Menu"; -export type MenuClassKey = "drawer" | "permanent" | "temporary" | "open" | "closed"; +const openedMixin = (theme: Theme, drawerWidth?: number): CSSObject => ({ + width: drawerWidth ?? DEFAULT_DRAWER_WIDTH, + transition: theme.transitions.create("width", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + overflowX: "hidden", +}); +const closedMixin = (theme: Theme, drawerVariant: DrawerProps["variant"], drawerWidth?: number, drawerWidthCollapsed?: number): CSSObject => ({ + width: drawerVariant === "temporary" ? drawerWidth ?? DEFAULT_DRAWER_WIDTH : drawerWidthCollapsed ?? DEFAULT_DRAWER_WIDTH_COLLAPSED, + transition: theme.transitions.create("width", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + overflowX: "hidden", +}); +export const Drawer: StyledComponent & Pick> = styled( + MuiDrawer, + { shouldForwardProp: (prop) => prop !== "drawerWidth" && prop !== "drawerWidthCollapsed" }, +) & Pick>( + ({ theme, open, variant, drawerWidth, drawerWidthCollapsed }) => { + const { headerHeight } = React.useContext(MasterLayoutContext); -export const styles = (theme: Theme) => - createStyles({ - drawer: { - "& [class*='MuiDrawer-paper']": { - backgroundColor: "#fff", - }, - "& [class*='MuiPaper-root']": { - flexGrow: 1, - overflowX: "hidden", + return { + ...(variant === "permanent" && { + backgroundColor: theme.palette.common.white, + flexShrink: 0, + whiteSpace: "nowrap", + boxSizing: "border-box", + ...(open ? openedMixin(theme, drawerWidth) : closedMixin(theme, variant, drawerWidth, drawerWidthCollapsed)), + }), + "& .MuiDrawer-paper": { + backgroundColor: theme.palette.common.white, + ...(variant === "permanent" && { + top: headerHeight, + height: `calc(100% - ${headerHeight}px)`, + }), + ...(open ? openedMixin(theme, drawerWidth) : closedMixin(theme, variant, drawerWidth, drawerWidthCollapsed)), }, - "& [class*='MuiDrawer-paperAnchorLeft']": { + "& .MuiDrawer-paperAnchorLeft": { borderRight: "none", }, - "&$permanent": { - "&$open": { - transition: theme.transitions.create("width", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - "& [class*='MuiPaper-root']": { - transition: theme.transitions.create("margin", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - }, - }, - "&$closed": { - transition: theme.transitions.create("width", { - easing: theme.transitions.easing.easeOut, - duration: theme.transitions.duration.leavingScreen, - }), - "& [class*='MuiPaper-root']": { - transition: theme.transitions.create("margin", { - easing: theme.transitions.easing.easeOut, - duration: theme.transitions.duration.enteringScreen, - }), - }, - }, - }, - }, + }; + }, +); + +export type MenuClassKey = "drawer" | "permanent" | "temporary" | "open" | "closed"; + +export const styles = () => { + return createStyles({ + drawer: {}, permanent: {}, temporary: {}, open: {}, - closed: { - "&$permanent": { - "& [class*='MuiPaper']": { - boxShadow: "none", - }, - }, - }, + closed: {}, }); +}; diff --git a/packages/admin/admin/src/mui/menu/Menu.tsx b/packages/admin/admin/src/mui/menu/Menu.tsx index b505dc390f..d1e22d8764 100644 --- a/packages/admin/admin/src/mui/menu/Menu.tsx +++ b/packages/admin/admin/src/mui/menu/Menu.tsx @@ -1,16 +1,19 @@ -import { ComponentsOverrides, Drawer, DrawerProps, PaperProps, Theme } from "@mui/material"; +import { ComponentsOverrides, DrawerProps, PaperProps, Theme } from "@mui/material"; import { WithStyles, withStyles } from "@mui/styles"; import * as React from "react"; import { useHistory } from "react-router"; -import { MasterLayoutContext } from "../MasterLayoutContext"; import { MenuContext } from "./Context"; -import { MenuClassKey, styles } from "./Menu.styles"; +import { Drawer, MenuClassKey, styles } from "./Menu.styles"; + +export const DEFAULT_DRAWER_WIDTH = 300; +export const DEFAULT_DRAWER_WIDTH_COLLAPSED = 60; export interface MenuProps { children: React.ReactNode; variant?: "permanent" | "temporary"; drawerWidth?: number; + drawerWidthCollapsed?: number; temporaryDrawerProps?: DrawerProps; permanentDrawerProps?: DrawerProps; temporaryDrawerPaperProps?: PaperProps; @@ -20,7 +23,8 @@ export interface MenuProps { const MenuDrawer: React.FC & MenuProps> = ({ classes, children, - drawerWidth = 300, + drawerWidth = DEFAULT_DRAWER_WIDTH, + drawerWidthCollapsed = DEFAULT_DRAWER_WIDTH_COLLAPSED, variant = "permanent", temporaryDrawerProps = {}, permanentDrawerProps = {}, @@ -29,7 +33,6 @@ const MenuDrawer: React.FC & MenuProps> = ({ }) => { const history = useHistory(); const { open, toggleOpen } = React.useContext(MenuContext); - const { headerHeight } = React.useContext(MasterLayoutContext); const initialRender = React.useRef(true); // Close the menu on initial render if it is temporary to prevent a page-overlay when initially loading the page. @@ -67,10 +70,11 @@ const MenuDrawer: React.FC & MenuProps> = ({ <> @@ -79,17 +83,13 @@ const MenuDrawer: React.FC & MenuProps> = ({