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;