From 89fee1208e8cbca2e4ded423bfdb3333f281f790 Mon Sep 17 00:00:00 2001 From: Pedro Ladaria Date: Tue, 17 Dec 2024 18:30:31 +0100 Subject: [PATCH] WEB-2113 drawer layout --- src/__stories__/drawer-story.tsx | 54 +++++++++++++ src/drawer.css.ts | 69 +++++++++++++++++ src/drawer.tsx | 128 +++++++++++++++++++++++++------ 3 files changed, 228 insertions(+), 23 deletions(-) create mode 100644 src/__stories__/drawer-story.tsx create mode 100644 src/drawer.css.ts diff --git a/src/__stories__/drawer-story.tsx b/src/__stories__/drawer-story.tsx new file mode 100644 index 000000000..7d9019955 --- /dev/null +++ b/src/__stories__/drawer-story.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import Drawer from '../drawer'; +import {Placeholder} from '../placeholder'; +import Stack from '../stack'; +import {ButtonPrimary} from '../button'; + +export default { + title: 'Components/Modals/Drawer', +}; + +type Args = { + title: string; + subtitle: string; + description: string; + contentLength: number; +}; + +export const Default = ({title, subtitle, description, contentLength}: Args): JSX.Element => { + const [isOpen, setIsOpen] = React.useState(false); + const content = ( + + {Array.from({length: contentLength}).map((_, index) => ( + + ))} + + ); + + return ( + <> + setIsOpen(true)}>Open Drawer + {isOpen && ( + { + setIsOpen(false); + }} + > + {content} + + )} + + ); +}; + +Default.storyName = 'Drawer'; + +Default.args = { + title: 'Title', + subtitle: 'Subtitle', + description: 'Description', + contentLength: 2, +}; diff --git a/src/drawer.css.ts b/src/drawer.css.ts new file mode 100644 index 000000000..0cf21b169 --- /dev/null +++ b/src/drawer.css.ts @@ -0,0 +1,69 @@ +import {style} from '@vanilla-extract/css'; +import * as mq from './media-queries.css'; +import {vars} from './skins/skin-contract.css'; +import {sprinkles} from './sprinkles.css'; + +export const ANIMATION_DURATION_MS = 400; // review + +export const container = style({ + background: vars.colors.background, + position: 'fixed', + top: 0, + right: 0, + bottom: 0, + '@media': { + [mq.tabletOrSmaller]: { + left: 0, + transition: `transform ${ANIMATION_DURATION_MS}ms cubic-bezier(0.32, 0.72, 0, 1)`, + }, + [mq.desktopOrBigger]: { + borderTopLeftRadius: vars.borderRadii.container, + borderBottomLeftRadius: vars.borderRadii.container, + transition: `transform ${ANIMATION_DURATION_MS}ms cubic-bezier(0.65, 0, 0.35, 1)`, + }, + }, +}); + +export const open = style({ + transform: '', +}); + +export const closed = style({ + '@media': { + [mq.desktopOrBigger]: { + transform: 'translateX(100%)', + }, + [mq.tabletOrSmaller]: { + transform: 'translateY(100%)', + }, + }, +}); + +export const overlay = style([ + sprinkles({ + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: vars.colors.backgroundOverlay, + }), + { + transition: `opacity ${ANIMATION_DURATION_MS}ms`, + touchAction: 'none', + }, +]); + +export const overlayClosed = style({ + opacity: 0, +}); + +export const overlayOpen = style({ + opacity: 1, +}); + +export const closeButton = style({ + position: 'absolute', + top: 8, + right: 8, +}); diff --git a/src/drawer.tsx b/src/drawer.tsx index f68f59a84..eba7fbe41 100644 --- a/src/drawer.tsx +++ b/src/drawer.tsx @@ -1,44 +1,126 @@ // https://www.figma.com/design/NfM16IJ4ffPVEdiFbtU0Lu/%F0%9F%94%B8-Drawer-Specs?node-id=10-5397&t=58YG59t526tkk7dP-1 +'use client'; import * as React from 'react'; import Stack from './stack'; import {Text3, Text4, Text5} from './text'; import {vars} from './skins/skin-contract.css'; import {IconButton} from './icon-button'; import IconCloseRegular from './generated/mistica-icons/icon-close-regular'; +import Box from './box'; +import * as styles from './drawer.css'; +import classnames from 'classnames'; +import {Portal} from './portal'; +import {useScreenSize} from './hooks'; +import FocusTrap from './focus-trap'; + +const PADDING_X_DESKTOP = 40; +const PADDING_X_MOBILE = 16; +const CONTENT_WIDTH_DESKTOP = 388; + +type DrawerLayoutProps = { + width?: number; + children: React.ReactNode; + onClose?: () => void; +}; + +type DrawerPropsRef = { + close: () => void; +}; + +const DrawerLayout = React.forwardRef( + ({width, children, onClose}, ref) => { + const defaultWidth = CONTENT_WIDTH_DESKTOP + PADDING_X_DESKTOP * 2; + + const {isDesktopOrBigger} = useScreenSize(); + + const [isOpen, setIsOpen] = React.useState(false); + + const open = React.useCallback((node: HTMLDivElement) => { + if (node) { + // small delay to allow the Portal to be mounted + setTimeout(() => setIsOpen(true), 50); + } + }, []); + + const close = React.useCallback(() => { + setIsOpen(false); + setTimeout(() => { + onClose?.(); + }, styles.ANIMATION_DURATION_MS); + }, [onClose]); + + React.useImperativeHandle(ref, () => ({ + close, + })); + + return ( + + + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
+
+ {children} +
+ + + ); + } +); type DrawerProps = { title?: string; subtitle?: string; description?: string; + onClose?: () => void; + children?: React.ReactNode; // actions?: React.ReactNode; - onDismiss?: () => void; - // overlay?: boolean; + /** Ignored in mobile viewport */ + width?: number; }; -const Drawer = ({title, subtitle, description, onDismiss}: DrawerProps): JSX.Element => { - const handleDismiss = () => { - onDismiss?.(); - }; +const Drawer = ({title, subtitle, description, onClose, width, children}: DrawerProps): JSX.Element => { + const layoutRef = React.useRef(null); return ( -
- - - {title && {title}} - {subtitle && {subtitle}} - {description && ( - - {description} - + + + {onClose && ( +
+ layoutRef.current?.close()} + Icon={IconCloseRegular} + aria-label="Close drawer" + type="neutral" + backgroundType="transparent" + > +
)} -
-
+ + {title && {title}} + {subtitle && {subtitle}} + {description && ( + + {description} + + )} + {children} + + + ); };