Skip to content

Commit

Permalink
Fix scrolling/overflow issue of MasterLayout (#2948)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesricky authored Dec 19, 2024
1 parent b6c17f5 commit 20f6341
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 10 deletions.
7 changes: 7 additions & 0 deletions .changeset/fuzzy-dogs-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@comet/admin": patch
---

Prevent the page content from overflowing the window, causing a horizontal scrollbar

This happened when using elements like `Tabs` that are intended to be horizontally scrollable and could, therefore, be wider than the window.
40 changes: 36 additions & 4 deletions packages/admin/admin/src/mui/MasterLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ComponentsOverrides, CssBaseline } from "@mui/material";
import { css, Theme, useThemeProps } from "@mui/material/styles";
import { ComponentType, CSSProperties, ReactNode, useState } from "react";
import { ComponentType, CSSProperties, ReactNode, useEffect, useRef, useState } from "react";

import { AppHeader } from "../appHeader/AppHeader";
import { AppHeaderMenuButton } from "../appHeader/menuButton/AppHeaderMenuButton";
Expand All @@ -9,7 +9,7 @@ import { ThemedComponentBaseProps } from "../helpers/ThemedComponentBaseProps";
import { MasterLayoutContext } from "./MasterLayoutContext";
import { MenuContext } from "./menu/Context";

export type MasterLayoutClassKey = "root" | "header" | "contentWrapper";
export type MasterLayoutClassKey = "root" | "header" | "menuWrapper" | "contentWrapper";

const Root = createComponentSlot("div")<MasterLayoutClassKey>({
componentName: "MasterLayout",
Expand All @@ -28,18 +28,25 @@ const Header = createComponentSlot("div")<MasterLayoutClassKey>({
`,
);

const MenuWrapper = createComponentSlot("div")<MasterLayoutClassKey>({
componentName: "MasterLayout",
slotName: "menuWrapper",
})();

const ContentWrapper = createComponentSlot("div")<MasterLayoutClassKey>({
componentName: "MasterLayout",
slotName: "contentWrapper",
})(css`
flex-grow: 1;
padding-top: var(--comet-admin-master-layout-content-top-spacing);
width: calc(100% - var(--comet-admin-master-layout-menu-width));
`);

export interface MasterLayoutProps
extends ThemedComponentBaseProps<{
root: "div";
header: "div";
menuWrapper: "div";
contentWrapper: "div";
}> {
children: ReactNode;
Expand All @@ -65,6 +72,24 @@ export function MasterLayout(inProps: MasterLayoutProps) {

const [open, setOpen] = useState(openMenuByDefault);
const [drawerVariant, setDrawerVariant] = useState<"permanent" | "temporary">("permanent");
const [menuWidth, setMenuWidth] = useState(0);
const menuRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!menuRef.current) return;

const resizeObserver = new ResizeObserver(([entry]) => {
if (entry) {
setMenuWidth(entry.contentRect.width);
}
});

resizeObserver.observe(menuRef.current);

return () => {
resizeObserver.disconnect();
};
}, []);

const toggleOpen = () => {
setOpen(!open);
Expand All @@ -84,10 +109,17 @@ export function MasterLayout(inProps: MasterLayoutProps) {
</AppHeader>
)}
</Header>
<Menu />
<MenuWrapper {...slotProps?.menuWrapper} ref={menuRef}>
<Menu />
</MenuWrapper>
<ContentWrapper
{...slotProps?.contentWrapper}
style={{ "--comet-admin-master-layout-content-top-spacing": `${headerHeight}px` } as CSSProperties}
style={
{
"--comet-admin-master-layout-content-top-spacing": `${headerHeight}px`,
"--comet-admin-master-layout-menu-width": `${menuWidth}px`,
} as CSSProperties
}
>
{children}
</ContentWrapper>
Expand Down
189 changes: 189 additions & 0 deletions storybook/src/admin/tabs/OverflowIssues.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { MainContent, RouterTab, RouterTabs, Toolbar, ToolbarAutomaticTitleItem } from "@comet/admin";
import { Box, Tab, Tabs, Typography } from "@mui/material";
import { Meta } from "@storybook/react/*";
import { ComponentType, useState } from "react";

import { masterLayoutDecorator, stackRouteDecorator } from "../../helpers/storyDecorators";
import { storyRouterDecorator } from "../../story-router.decorator";

const storyConfig: Meta = {
title: "@comet/admin/MasterLayout/OverflowIssues",
decorators: [maxHeightDecorator()],
parameters: {
docs: {
description: {
component:
"This story is for debugging an issue where using certain content inside MasterLayout causes the content to overflow the page.",
},
},
},
};

function maxHeightDecorator() {
// Make sure the story is not too tall and can be scrolled - setting the story-height in the config only affects the min-height and does not make the content scrollable
return (Story: ComponentType) => {
return (
<Box maxHeight="400px">
<Story />
</Box>
);
};
}

export default storyConfig;

const numberOfTabs = 20;

const ExampleContentBlock = () => {
return (
<Box
sx={(theme) => ({
width: 200,
height: "150vh",
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.grey[100],
})}
/>
);
};

export const RouterTabsInMasterContent = {
decorators: [masterLayoutDecorator(), stackRouteDecorator(), storyRouterDecorator()],
parameters: {
layout: "none",
},
render: () => {
return (
<MainContent>
<RouterTabs>
{Array.from({ length: numberOfTabs }, (_, index) => (
<RouterTab key={index} path={index === 0 ? "" : `/lorem-ipsum-${index + 1}`} label={`Lorem ipsum ${index + 1}`}>
<Typography variant="h2" gutterBottom>
Lorem ipsum {index + 1}
</Typography>
<ExampleContentBlock />
</RouterTab>
))}
</RouterTabs>
</MainContent>
);
},
};

export const RouterTabsInMasterContentWithToolbar = {
decorators: [masterLayoutDecorator(), stackRouteDecorator(), storyRouterDecorator()],
parameters: {
layout: "none",
},
render: () => {
return (
<>
<Toolbar>
<ToolbarAutomaticTitleItem />
</Toolbar>
<MainContent>
<RouterTabs>
{Array.from({ length: numberOfTabs }, (_, index) => (
<RouterTab key={index} path={index === 0 ? "" : `/lorem-ipsum-${index + 1}`} label={`Lorem ipsum ${index + 1}`}>
<Typography variant="h2" gutterBottom>
Lorem ipsum {index + 1}
</Typography>
<ExampleContentBlock />
</RouterTab>
))}
</RouterTabs>
</MainContent>
</>
);
},
};

export const MuiTabsInMasterContent = {
decorators: [masterLayoutDecorator(), stackRouteDecorator(), storyRouterDecorator()],
parameters: {
layout: "none",
},
render: () => {
const [activeTabIndex, setActiveTabIndex] = useState(0);

return (
<MainContent>
<Tabs variant="scrollable" value={activeTabIndex} onChange={(_, index) => setActiveTabIndex(index)}>
{Array.from({ length: numberOfTabs }, (_, index) => (
<Tab key={index} label={`Lorem ipsum ${index + 1}`} />
))}
</Tabs>
<ExampleContentBlock />
</MainContent>
);
},
};

export const MuiTabsInMasterContentWithToolbar = {
decorators: [masterLayoutDecorator(), stackRouteDecorator(), storyRouterDecorator()],
parameters: {
layout: "none",
},
render: () => {
const [activeTabIndex, setActiveTabIndex] = useState(0);

return (
<>
<Toolbar>
<ToolbarAutomaticTitleItem />
</Toolbar>
<MainContent>
<Tabs variant="scrollable" value={activeTabIndex} onChange={(_, index) => setActiveTabIndex(index)}>
{Array.from({ length: numberOfTabs }, (_, index) => (
<Tab key={index} label={`Lorem ipsum ${index + 1}`} />
))}
</Tabs>
<ExampleContentBlock />
</MainContent>
</>
);
},
};

export const RouterTabsInMainContent = {
decorators: [stackRouteDecorator(), storyRouterDecorator()],
parameters: {
layout: "none",
},
render: () => {
return (
<MainContent>
<RouterTabs>
{Array.from({ length: numberOfTabs }, (_, index) => (
<RouterTab key={index} path={index === 0 ? "" : `/lorem-ipsum-${index + 1}`} label={`Lorem ipsum ${index + 1}`}>
<Typography variant="h2" gutterBottom>
Lorem ipsum {index + 1}
</Typography>
<ExampleContentBlock />
</RouterTab>
))}
</RouterTabs>
</MainContent>
);
},
};

export const MuiTabsInMainContent = {
parameters: {
layout: "none",
},
render: () => {
const [activeTabIndex, setActiveTabIndex] = useState(0);

return (
<MainContent>
<Tabs variant="scrollable" value={activeTabIndex} onChange={(_, index) => setActiveTabIndex(index)}>
{Array.from({ length: numberOfTabs }, (_, index) => (
<Tab key={index} label={`Lorem ipsum ${index + 1}`} />
))}
</Tabs>
<ExampleContentBlock />
</MainContent>
);
},
};
19 changes: 13 additions & 6 deletions storybook/src/helpers/storyDecorators.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AppHeader, AppHeaderMenuButton, MasterLayout, Menu, MenuItemRouterLink, Stack } from "@comet/admin";
import { AppHeader, AppHeaderMenuButton, MasterLayout, Menu, MenuItemRouterLink, Stack, useWindowSize } from "@comet/admin";
import { Dashboard } from "@comet/admin-icons";
import { useTheme } from "@mui/material";
import { ComponentType } from "react";
import { Route } from "react-router";

Expand All @@ -10,11 +11,17 @@ export function masterLayoutDecorator() {
</AppHeader>
);

const MasterMenu = () => (
<Menu>
<MenuItemRouterLink primary="Example Page" to="/" icon={<Dashboard />} />
</Menu>
);
const MasterMenu = () => {
const windowSize = useWindowSize();
const { breakpoints } = useTheme();
const useTemporaryMenu: boolean = windowSize.width < breakpoints.values.md;

return (
<Menu variant={useTemporaryMenu ? "temporary" : "permanent"}>
<MenuItemRouterLink primary="Example Page" to="/" icon={<Dashboard />} />
</Menu>
);
};

return (Story: ComponentType) => {
return (
Expand Down

0 comments on commit 20f6341

Please sign in to comment.