import React, { useContext, useState } from "react"; import { useSpring, animated } from "@react-spring/web"; 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, TopBarProps } from "../TopBar"; import { CloseButton, CloseButtonProps } from "../CloseButton"; import { Flex } from "../Flex"; const cn = bemHOF("Drawer"); type ScrollableSlotRenderFunction = (props: { isOpen: boolean; isAnimationComplete: boolean; }) => React.ReactNode; export interface WithDrawerProps { /** * The content of the drawer */ drawerSlot: React.ReactNode; /** * Optional classname for the drawer slot div */ drawerSlotClassName?: string; /** * 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; /** * Optional classname for the fixed top slot div */ fixedSlotTopClassName?: string; /** * Content that renders below the `scrollableSlot` and will shrink as the Drawer animates in */ fixedSlotBottom?: React.ReactNode; /** * Optional classname for the fixed bottom slot div */ fixedSlotBottomClassName?: string; /** * Scrollable content that will be pushed off screen as the Drawer animates in */ scrollableSlot?: ScrollableSlotRenderFunction | React.ReactNode; /** * Optional classname for the scrollable slot div */ scrollableSlotClassName?: string; /** * 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; /** * Optional classname for the outermost container div */ className?: string; /** * Optional classname for the on-screen content div */ contentClassName?: string; } 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, drawerSlotClassName, onClose, isOpen = false, fixedSlotTop, fixedSlotTopClassName, fixedSlotBottom, fixedSlotBottomClassName, scrollableSlot, scrollableSlotClassName, drawerSide = "left", noAnimation = false, drawerWidth = DEFAULT_DRAWER_WIDTH, className, contentClassName, }: WithDrawerProps) => { const drawerOnRight = drawerSide === "right"; const [isClosedAtRest, setIsClosedAtRest] = useState(false); const [isAnimationComplete, setIsAnimationComplete] = 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); } }, onStart: () => { setIsAnimationComplete(false); }, onRest: () => { setIsAnimationComplete(true); }, 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={classNames( cn({ e: "scrollableSlot" }), scrollableSlotClassName, )} > {typeof scrollableSlot === "function" ? scrollableSlot({ isOpen, isAnimationComplete, }) : 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 DrawerContent = ({ children, className, ...rest }: BoxProps) => { return ( {children} ); }; export const DrawerHeader = ({ className, ...rest }: TopBarProps) => { return ( ); }; export const DrawerCloseButton = ({ onClick, className, ...rest }: CloseButtonProps) => { const { onClose } = useContext(DrawerContext); return ( { if (onClick) { onClick(event); } onClose(); }} className={classNames("text-icon", className)} {...rest} /> ); }; export const DrawerBody = ({ children, className, ...rest }: BoxProps) => { return ( {children} ); }; export const DrawerFooter = ({ children, className, ...rest }: BoxProps) => { return ( {children} ); }; export const Drawer = ({ children, headingSlot, footerSlot, className, drawerSide, ...rest }: DrawerProps) => { return ( } /> {children} {!!footerSlot && {footerSlot}} ); }; Drawer.Content = DrawerContent; Drawer.Header = DrawerHeader; Drawer.CloseButton = DrawerCloseButton; Drawer.Body = DrawerBody; Drawer.Footer = DrawerFooter;