import React, { useContext, useState } from "react"; import { useSpring, animated } from "react-spring"; import classNames from "classnames"; import { easeCubicInOut } from "d3-ease"; import { bemHOF } from "../../utilities/bem"; import { DrawerContext } from "../../contexts/drawer"; import { Box, BoxProps } from "../Box"; import { TopBar } from "../TopBar"; import { CloseButton } from "../CloseButton"; import { Flex } from "../Flex"; const cn = bemHOF("Drawer"); export interface WithDrawerProps { /** * The content of the drawer */ drawerSlot: React.ReactNode; /** * Called when the close button is clicked */ onClose: () => void; /** * Determines whether the `Drawer` is open */ isOpen?: boolean; /** * Content that will shrink as the Drawer animates in */ fixedSlotTop?: React.ReactNode; /** * Content that renders below the `scrollableSlot` and will shrink as the Drawer animates in */ fixedSlotBottom?: React.ReactNode; /** * Scrollable content that will be pushed off screen as the Drawer animates in */ scrollableSlot?: React.ReactNode; /** * Puts the drawer on the left or right of the content */ drawerSide?: "left" | "right"; /** * Removes the animation */ noAnimation?: boolean; /** * Overrides the default drawer width */ drawerWidth?: number; } const DEFAULT_DRAWER_WIDTH = 264; const calculateTransformValue = ( isOpen: boolean, drawerOnRight: boolean, drawerWidth: number, ) => { const translateZero = "translateX(0px)"; const translateNegWidth = `translateX(${-drawerWidth}px)`; if (isOpen) { return drawerOnRight ? translateNegWidth : translateZero; } return drawerOnRight ? translateZero : translateNegWidth; }; export const WithDrawer = ({ drawerSlot, onClose, isOpen = false, fixedSlotTop, fixedSlotBottom, scrollableSlot, drawerSide = "left", noAnimation = false, drawerWidth = DEFAULT_DRAWER_WIDTH, }: WithDrawerProps) => { const drawerOnRight = drawerSide === "right"; const [isClosedAtRest, setIsClosedAtRest] = useState(false); const { transform, paddingLeft, paddingRight } = useSpring({ transform: calculateTransformValue(isOpen, drawerOnRight, drawerWidth), paddingLeft: isOpen && !drawerOnRight ? drawerWidth : 0, paddingRight: isOpen && drawerOnRight ? drawerWidth : 0, onFrame: ({ paddingLeft: pl, paddingRight: pr, }: { paddingLeft: number; paddingRight: number; }) => { const padding = drawerOnRight ? pr : pl; if (padding === 0 && !isClosedAtRest) { setIsClosedAtRest(true); } else if (padding !== 0 && isClosedAtRest) { setIsClosedAtRest(false); } }, config: { duration: noAnimation ? 0 : 300, easing: easeCubicInOut, }, }); const { transform: transformReverse } = useSpring({ transform: isOpen && !drawerOnRight ? `translateX(${drawerWidth}px)` : "translateX(0px)", }); // Setting the width in js so we don't have to keep css/js in sync const wrapperCombinedStyle = { width: drawerWidth, transform, }; const scrollableSlotPaddingToInterpolate = drawerOnRight ? paddingRight : paddingLeft; return ( {drawerSlot} {fixedSlotTop && ( {fixedSlotTop} )} {scrollableSlot && ( `calc(100% - ${w}px)`, ), }} className={cn({ e: "scrollableSlot" })} > {scrollableSlot} )} {fixedSlotBottom && ( {fixedSlotBottom} )} ); }; export interface DrawerProps extends BoxProps { /** * The content on the left side of `Drawer`'s `TopBar` */ headingSlot: React.ReactNode; /** * Optional footer content in the `Drawer` */ footerSlot?: React.ReactNode; /** * Changes styles based on whether the `Drawer` is on the left or ride side */ drawerSide?: "left" | "right"; } export const Drawer = ({ children, headingSlot, footerSlot, className, drawerSide, ...rest }: DrawerProps) => { const { onClose } = useContext(DrawerContext); return ( } /> {children} {!!footerSlot && {footerSlot}} ); };