From 804f6b55b6aac9364002ec3fa171e7dff83484b9 Mon Sep 17 00:00:00 2001 From: Mark Rosenberg <38965626+markdoeswork@users.noreply.github.com> Date: Fri, 29 Nov 2024 09:01:47 -0500 Subject: [PATCH] [PLAY-1582] Drawer Menu Behavior (#3932) **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) --- .../pb_kits/playbook/pb_drawer/_drawer.scss | 1 + .../pb_kits/playbook/pb_drawer/_drawer.tsx | 203 ++++++++++++++---- .../playbook/pb_drawer/docs/_drawer_menu.jsx | 31 +++ .../playbook/pb_drawer/docs/_drawer_menu.md | 6 + .../playbook/pb_drawer/docs/example.yml | 2 +- .../pb_kits/playbook/pb_drawer/docs/index.js | 1 + 6 files changed, 197 insertions(+), 47 deletions(-) create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_menu.jsx create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_menu.md diff --git a/playbook/app/pb_kits/playbook/pb_drawer/_drawer.scss b/playbook/app/pb_kits/playbook/pb_drawer/_drawer.scss index 4f66d51894..b9e7d31fb1 100644 --- a/playbook/app/pb_kits/playbook/pb_drawer/_drawer.scss +++ b/playbook/app/pb_kits/playbook/pb_drawer/_drawer.scss @@ -88,6 +88,7 @@ body.PBDrawer__Body--close { } .pb_drawer.pb_drawer_after_open { + pointer-events: auto; transform: translate3d(0, 0, 0); } diff --git a/playbook/app/pb_kits/playbook/pb_drawer/_drawer.tsx b/playbook/app/pb_kits/playbook/pb_drawer/_drawer.tsx index 40909d4bf5..314959b713 100644 --- a/playbook/app/pb_kits/playbook/pb_drawer/_drawer.tsx +++ b/playbook/app/pb_kits/playbook/pb_drawer/_drawer.tsx @@ -15,20 +15,22 @@ 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 => { @@ -36,19 +38,21 @@ const Drawer = (props: DrawerProps): React.ReactElement => { 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); @@ -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", @@ -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 = { + const breakpointWidths: Record = { none: 0, xs: 575, sm: 768, @@ -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 } }; @@ -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(""); @@ -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 = { xl: "365px", lg: "300px", @@ -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;`; @@ -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 ( -
+ {withinElement ? ( + isModalVisible && ( +
e.stopPropagation()} + > + {children} +
+ ) + ) : ( +
{isModalVisible && (
-
e.stopPropagation()} - > - {children} -
+
e.stopPropagation()} + > + {children} +
)} -
+
+ )}
); }; diff --git a/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_menu.jsx b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_menu.jsx new file mode 100644 index 0000000000..6309c09146 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_menu.jsx @@ -0,0 +1,31 @@ +import React from "react"; +import { Button, Drawer, Icon, Title } from "playbook-ui"; + +const DrawerMenu = () => { + + return ( + <> + + + A really neat menu +