Skip to content

Commit

Permalink
[PLAY-1582] Drawer Menu Behavior (#3932)
Browse files Browse the repository at this point in the history
**What does this PR do?** A clear and concise description with your
runway ticket url.
Runway https://runway.powerhrg.com/backlog_items/PLAY-1582

I add/rename props so the drawer kits has 
`closeBreakpoint`
`openBreakpoint`
`withinElement`

So that we can have the drawer behave like a responsive menu. You can
set what element is the drawer's menu button that will open and close
the menu

You can see it in action here and here's a screen cast (the animation on
the drawer is kinda whack)


[screencast-localhost_3000-2024_11_22-13_13_38.webm](https://github.com/user-attachments/assets/ea77424c-5550-46c2-b486-8439a4cff72f)
  • Loading branch information
markdoeswork authored Nov 29, 2024
1 parent 172debf commit 804f6b5
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 47 deletions.
1 change: 1 addition & 0 deletions playbook/app/pb_kits/playbook/pb_drawer/_drawer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ body.PBDrawer__Body--close {
}

.pb_drawer.pb_drawer_after_open {
pointer-events: auto;
transform: translate3d(0, 0, 0);
}

Expand Down
203 changes: 157 additions & 46 deletions playbook/app/pb_kits/playbook/pb_drawer/_drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,44 @@ type DrawerProps = {
aria?: { [key: string]: string };
behavior?: "floating" | "push";
border?: "full" | "none" | "right" | "left";
breakpoint?: "none" | "xs" | "sm" | "md" | "lg" | "xl";
openBreakpoint?: "none" | "xs" | "sm" | "md" | "lg" | "xl";
closeBreakpoint?: "none" | "xs" | "sm" | "md" | "lg" | "xl";
children: React.ReactNode | React.ReactNode[] | string;
className?: string;
data?: { [key: string]: string };
htmlOptions?: { [key: string]: string | number | boolean | (() => void) };
id?: string;
fullHeight?: boolean;
menuButtonID?: string;
onClose?: () => void;
opened: boolean;
overlay: boolean;
placement?: "left" | "right";
size?: "xs" | "sm" | "md" | "lg" | "xl";
text?: string;
trigger?: string;
withinElement?: boolean;
};

const Drawer = (props: DrawerProps): React.ReactElement => {
const {
aria = {},
behavior = "floating",
border = "none",
breakpoint = "none",
openBreakpoint = "none",
closeBreakpoint = "none",
className,
data = {},
htmlOptions = {},
id,
size = "md",
children,
fullHeight = false,
fullHeight = true,
menuButtonID,
opened,
onClose,
overlay = true,
placement = "left",
trigger,
withinElement = false,
} = props;
const ariaProps = buildAriaProps(aria);
const dataProps = buildDataProps(data);
Expand Down Expand Up @@ -80,6 +84,7 @@ const Drawer = (props: DrawerProps): React.ReactElement => {
drawer_border_full: border === "full",
drawer_border_right: border === "right",
drawer_border_left: border === "left",
pb_drawer_within_element: withinElement,
}
)} ${globalPropsString}`,
afterOpen: "pb_drawer_after_open",
Expand All @@ -100,12 +105,11 @@ const Drawer = (props: DrawerProps): React.ReactElement => {
};

const classes = classnames(buildCss("pb_drawer_wrapper"), className);

const dynamicInlineProps = globalInlineProps(props)

const [menuButtonOpened, setMenuButtonOpened] = useState(false);
const [triggerOpened, setTriggerOpened] = useState(false);

const breakpointWidths: Record<DrawerProps["breakpoint"], number> = {
const breakpointWidths: Record<DrawerProps["openBreakpoint"], number> = {
none: 0,
xs: 575,
sm: 768,
Expand All @@ -114,20 +118,30 @@ const Drawer = (props: DrawerProps): React.ReactElement => {
xl: 1400,
};

// State to manage opening the drawer based on breakpoint
const [isBreakpointOpen, setIsBreakpointOpen] = useState(false);
const breakpointValues = {
none: 0,
xs: 575,
sm: 768,
md: 992,
lg: 1200,
xl: 1400,
}

const [isOpenBreakpointOpen, setIsOpenBreakpointOpen] = useState(false);
const [isUserClosed, setIsUserClosed] = useState(false);

useEffect(() => {
if (breakpoint === "none") return;
if (openBreakpoint === "none") return;

const handleResize = () => {
const width = window.innerWidth;
const breakpointWidth = breakpointWidths[breakpoint];
const openBreakpointWidth = breakpointWidths[openBreakpoint];

if (width <= breakpointWidth) {
setIsBreakpointOpen(true);
if (width <= openBreakpointWidth) {
setIsOpenBreakpointOpen(true);
} else {
setIsBreakpointOpen(false);
setIsOpenBreakpointOpen(false);
setIsUserClosed(false); // Reset when the breakpoint condition changes
}
};

Expand All @@ -139,9 +153,53 @@ const Drawer = (props: DrawerProps): React.ReactElement => {
return () => {
window.removeEventListener("resize", handleResize);
};
}, [breakpoint]);
}, [openBreakpoint]);

useEffect(() => {
if (closeBreakpoint === "none") return;

const handleResize = () => {
const width = window.innerWidth;
if (width >= breakpointValues[closeBreakpoint]) {
setIsOpenBreakpointOpen(true);
} else {
setIsOpenBreakpointOpen(false);
}
}

window.addEventListener("resize", handleResize);

handleResize();

return () => {
window.removeEventListener("resize", handleResize);
};

}, [closeBreakpoint]);

//hide menu button if breakpoint opens the drawer
useEffect(() => {
if (menuButtonID) {
const menuButton = document.getElementById(menuButtonID);
if (menuButton) {
if (isOpenBreakpointOpen) {
menuButton.style.display = 'none';
} else {
menuButton.style.display = '';
}
}
}
}, [menuButtonID, isOpenBreakpointOpen]);

const modalIsOpened = trigger ? triggerOpened : opened || isBreakpointOpen;
// Reset isUserClosed when isBreakpointOpen changes
useEffect(() => {
if (isOpenBreakpointOpen) {
setIsUserClosed(false);
}
}, [isOpenBreakpointOpen]);

const modalIsOpened =
(isOpenBreakpointOpen && !isUserClosed) || menuButtonOpened || opened;

const [animationState, setAnimationState] = useState("");

Expand All @@ -152,13 +210,15 @@ const Drawer = (props: DrawerProps): React.ReactElement => {
setAnimationState("beforeClose");
setTimeout(() => {
setAnimationState("");
}, 200); // closeTimeoutMS
}, 200);
}
}, [modalIsOpened]);

const isModalVisible = modalIsOpened || animationState === "beforeClose";

useEffect(() => {
if (withinElement) return;

const sizeMap: Record<DrawerProps["size"], string> = {
xl: "365px",
lg: "300px",
Expand All @@ -167,7 +227,6 @@ const Drawer = (props: DrawerProps): React.ReactElement => {
xs: "64px",
};
const body = document.querySelector("body");

if (modalIsOpened && behavior === "push" && body) {
if (placement === "left") {
body.style.cssText = `margin-left: ${sizeMap[size]} !important; margin-right: '' !important;`;
Expand All @@ -183,46 +242,98 @@ const Drawer = (props: DrawerProps): React.ReactElement => {
body.style.cssText = ""; // Clear the styles when modal is closed or behavior is not 'push'
body.classList.remove("PBDrawer__Body--open");
}
}, [modalIsOpened, behavior, placement, size]);
}, [modalIsOpened, behavior, placement, size, withinElement]);

const api = {
onClose: trigger
? function () {
setTriggerOpened(false);
}
: onClose,
onClose: () => {
if (menuButtonID) {
setMenuButtonOpened(false);
}
setIsUserClosed(true);
if (onClose) {
onClose();
}
},
};

useEffect(() => {
if (menuButtonID) {
const menuButton = document.getElementById(menuButtonID);
if (menuButton) {
const handleMenuButtonClick = () => {
if (modalIsOpened) {
// Drawer is open, close it
setMenuButtonOpened(false);
setIsUserClosed(true);
} else {
// Drawer is closed, open it
setMenuButtonOpened(true);
setIsUserClosed(false);
}
};
menuButton.addEventListener("click", handleMenuButtonClick);
return () => {
menuButton.removeEventListener("click", handleMenuButtonClick);
};
}
}
}, [menuButtonID, modalIsOpened]);

return (
<DialogContext.Provider value={api}>
<div
{...ariaProps}
{...dataProps}
{...htmlProps}
className={classes}
style={dynamicInlineProps}
>
{withinElement ? (
isModalVisible && (
<div
{...ariaProps}
{...dataProps}
{...htmlProps}
style={dynamicInlineProps}
className={classnames(drawerClassNames.base, {
[drawerClassNames.afterOpen]:
animationState === "afterOpen",
[drawerClassNames.beforeClose]:
animationState === "beforeClose",
})}
id={id}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
)
) : (
<div
{...ariaProps}
{...dataProps}
{...htmlProps}
className={classes}
style={dynamicInlineProps}
>
{isModalVisible && (
<div
className={classnames(overlayClassNames.base, {
[overlayClassNames.afterOpen]: animationState === "afterOpen",
[overlayClassNames.beforeClose]: animationState === "beforeClose",
})}
[overlayClassNames.afterOpen]:
animationState === "afterOpen",
[overlayClassNames.beforeClose]:
animationState === "beforeClose",
})}
id={id}
onClick={overlay ? onClose : undefined}
onClick={overlay ? api.onClose : undefined}
>
<div
className={classnames(drawerClassNames.base, {
[drawerClassNames.afterOpen]: animationState === "afterOpen",
[drawerClassNames.beforeClose]: animationState === "beforeClose",
})}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
<div
className={classnames(drawerClassNames.base, {
[drawerClassNames.afterOpen]:
animationState === "afterOpen",
[drawerClassNames.beforeClose]:
animationState === "beforeClose",
})}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
)}
</div>
</div>
)}
</DialogContext.Provider>
);
};
Expand Down
31 changes: 31 additions & 0 deletions playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_menu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react";
import { Button, Drawer, Icon, Title } from "playbook-ui";

const DrawerMenu = () => {

return (
<>
<Button id="menuButton"
padding="sm"
>
<Icon icon="bars"
size="3x"
/>
</Button>
<Drawer
behavior="push"
closeBreakpoint="md"
menuButtonID="menuButton"
overlay={false}
placement="left"
size="lg"
withinElement
>
<Title paddingBottom="md">A really neat menu</Title>
<Button text="This Button does nothing" />
</Drawer>
</>
);
};

export default DrawerMenu;
6 changes: 6 additions & 0 deletions playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Our drawer kit can fulfill your responsive menu needs! Using the `closeBreakpoint` prop you can have the menu close on smaller screens like phones/tablets.

Set a menu button with the `menuButtonID` props. When the Drawer is open, the menu button will be hidden. But when your Brakpoint closes the drawer, you can toggle the Drawer open/close with your menu butotn.

Also use the `withinElement` props to have the Drawer open within a specific element, instead of the default behavior of it taking up the entire screen size.

2 changes: 1 addition & 1 deletion playbook/app/pb_kits/playbook/pb_drawer/docs/example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ examples:

react:
- drawer_default: Default
- drawer_menu: Menu Behavior
- drawer_sizes: Sizes
- drawer_overlay: Overlay
- drawer_borders: Borders
- drawer_breakpoints: Open on Breakpoints
1 change: 1 addition & 0 deletions playbook/app/pb_kits/playbook/pb_drawer/docs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { default as DrawerSizes } from './_drawer_sizes.jsx'
export { default as DrawerOverlay } from './_drawer_overlay.jsx'
export { default as DrawerBorders } from './_drawer_borders.jsx'
export { default as DrawerBreakpoints } from './_drawer_breakpoints.jsx'
export { default as DrawerMenu } from './_drawer_menu.jsx'

0 comments on commit 804f6b5

Please sign in to comment.