diff --git a/src/__stories__/drawer-story.tsx b/src/__stories__/drawer-story.tsx index 7d9019955..9ee6c5262 100644 --- a/src/__stories__/drawer-story.tsx +++ b/src/__stories__/drawer-story.tsx @@ -3,6 +3,7 @@ import Drawer from '../drawer'; import {Placeholder} from '../placeholder'; import Stack from '../stack'; import {ButtonPrimary} from '../button'; +import {Text3} from '../text'; export default { title: 'Components/Modals/Drawer', @@ -13,10 +14,21 @@ type Args = { subtitle: string; description: string; contentLength: number; + onDismissHandler: boolean; + showButton: boolean; + showSecondaryButton: boolean; + showButtonLink: boolean; }; -export const Default = ({title, subtitle, description, contentLength}: Args): JSX.Element => { +export const Default = ({ + title, + subtitle, + description, + contentLength, + onDismissHandler, +}: Args): JSX.Element => { const [isOpen, setIsOpen] = React.useState(false); + const [result, setResult] = React.useState(''); const content = ( {Array.from({length: contentLength}).map((_, index) => ( @@ -27,15 +39,20 @@ export const Default = ({title, subtitle, description, contentLength}: Args): JS return ( <> - setIsOpen(true)}>Open Drawer + + setIsOpen(true)}>Open Drawer + Result: {result} + {isOpen && ( { - setIsOpen(false); - }} + onClose={() => setIsOpen(false)} + onDismiss={onDismissHandler ? () => setResult('dismiss') : undefined} + button={{text: 'Primary', onPress: () => setResult('primary')}} + secondaryButton={{text: 'Secondary', onPress: () => setResult('secondary')}} + buttonLink={{text: 'Link', onPress: () => setResult('link')}} > {content} @@ -51,4 +68,8 @@ Default.args = { subtitle: 'Subtitle', description: 'Description', contentLength: 2, + onDismissHandler: true, + showButton: true, + showSecondaryButton: true, + showButtonLink: true, }; diff --git a/src/drawer.css.ts b/src/drawer.css.ts index 0cf21b169..6e8a96172 100644 --- a/src/drawer.css.ts +++ b/src/drawer.css.ts @@ -6,17 +6,20 @@ import {sprinkles} from './sprinkles.css'; export const ANIMATION_DURATION_MS = 400; // review export const container = style({ - background: vars.colors.background, position: 'fixed', + display: 'flex', + paddingBottom: 'env(safe-area-inset-bottom)', + background: vars.colors.background, top: 0, right: 0, bottom: 0, + overflow: 'hidden', '@media': { - [mq.tabletOrSmaller]: { + [mq.mobile]: { left: 0, transition: `transform ${ANIMATION_DURATION_MS}ms cubic-bezier(0.32, 0.72, 0, 1)`, }, - [mq.desktopOrBigger]: { + [mq.tabletOrBigger]: { borderTopLeftRadius: vars.borderRadii.container, borderBottomLeftRadius: vars.borderRadii.container, transition: `transform ${ANIMATION_DURATION_MS}ms cubic-bezier(0.65, 0, 0.35, 1)`, @@ -24,18 +27,45 @@ export const container = style({ }, }); +export const drawer = style([ + sprinkles({ + paddingTop: 40, + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + }), + { + border: '1px dotted red', + '@media': { + [mq.tabletOrSmaller]: { + paddingBottom: 16 - 8, + }, + [mq.desktopOrBigger]: { + paddingBottom: 24 - 8, + }, + }, + }, +]); + +export const scrollableSection = style([ + sprinkles({ + flexGrow: 1, + overflowY: 'auto', + }), +]); + export const open = style({ transform: '', }); export const closed = style({ '@media': { - [mq.desktopOrBigger]: { - transform: 'translateX(100%)', - }, - [mq.tabletOrSmaller]: { + [mq.mobile]: { transform: 'translateY(100%)', }, + [mq.tabletOrBigger]: { + transform: 'translateX(100%)', + }, }, }); @@ -62,7 +92,7 @@ export const overlayOpen = style({ opacity: 1, }); -export const closeButton = style({ +export const closeButtonContainer = style({ position: 'absolute', top: 8, right: 8, diff --git a/src/drawer.tsx b/src/drawer.tsx index 70931e37b..670af94d2 100644 --- a/src/drawer.tsx +++ b/src/drawer.tsx @@ -13,26 +13,34 @@ import {Portal} from './portal'; import {useScreenSize} from './hooks'; import FocusTrap from './focus-trap'; import {useSetModalStateEffect} from './modal-context-provider'; +import ButtonLayout from './button-layout'; +import {ButtonLink, ButtonPrimary, ButtonSecondary} from './button'; + +import type {DataAttributes, TrackingEvent} from './utils/types'; const PADDING_X_DESKTOP = 40; const PADDING_X_MOBILE = 16; -const WIDTH_CONTENT_DESKTOP = 388; -const WIDTH_DESKTOP = WIDTH_CONTENT_DESKTOP + PADDING_X_DESKTOP * 2; +const PADDING_X_TABLET = 16; +const WIDTH_CONTENT = 388; +const WIDTH_DESKTOP = WIDTH_CONTENT + PADDING_X_DESKTOP * 2; +const WIDTH_TABLET = WIDTH_CONTENT + PADDING_X_TABLET * 2; type DrawerLayoutProps = { width?: number; children: React.ReactNode; - onClose?: () => void; + onClose: () => void; + onDismiss?: () => void; }; type DrawerPropsRef = { - close: () => void; + close: () => Promise; + dismiss: () => Promise; }; const DrawerLayout = React.forwardRef( - ({width, children, onClose}, ref) => { + ({width, children, onClose, onDismiss}, ref) => { useSetModalStateEffect(); - const {isDesktopOrBigger} = useScreenSize(); + const {isMobile, isTablet} = useScreenSize(); const [isOpen, setIsOpen] = React.useState(false); const open = React.useCallback((node: HTMLDivElement) => { @@ -44,33 +52,35 @@ const DrawerLayout = React.forwardRef( const close = React.useCallback(() => { setIsOpen(false); - setTimeout(() => { - onClose?.(); - }, styles.ANIMATION_DURATION_MS); + return new Promise((resolve) => { + setTimeout(resolve, styles.ANIMATION_DURATION_MS); + }).then(onClose); }, [onClose]); - React.useImperativeHandle(ref, () => ({ - close, - })); + const dismiss = React.useCallback(() => { + return close().then(() => onDismiss?.()); + }, [onDismiss, close]); + + React.useImperativeHandle(ref, () => ({close, dismiss})); React.useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { - close(); + dismiss(); } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; - }, [close]); + }, [dismiss]); return ( {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
( />
{children} @@ -89,49 +101,134 @@ const DrawerLayout = React.forwardRef( } ); +type ButtonProps = { + text: string; + trackingEvent?: TrackingEvent | ReadonlyArray; + trackEvent?: boolean; + onPress: () => unknown; +}; + type DrawerProps = { title?: string; subtitle?: string; description?: string; - onClose?: () => void; + /** + * set this handler to enable dismiss: + * - touching "X" + * - touching overlay + * - pressing ESC + */ + onDismiss?: () => void; + onClose: () => void; children?: React.ReactNode; - // actions?: React.ReactNode; - /** Ignored in mobile viewport */ + /** + * width is ignored in mobile viewport + */ width?: number; + button?: ButtonProps; + secondaryButton?: ButtonProps; + buttonLink?: ButtonProps; }; -const Drawer = ({title, subtitle, description, onClose, width, children}: DrawerProps): JSX.Element => { +const Drawer = ({ + title, + subtitle, + description, + width, + onClose, + onDismiss, + children, + button, + secondaryButton, + buttonLink, +}: DrawerProps): JSX.Element => { const layoutRef = React.useRef(null); + const hasButtons = !!(button || secondaryButton || buttonLink); + const paddingX = { + mobile: PADDING_X_MOBILE, + tablet: PADDING_X_TABLET, + desktop: PADDING_X_DESKTOP, + } as const; + + const handleButtonPress = (pressHandlerFromProps: () => unknown) => { + layoutRef.current?.close().then(pressHandlerFromProps); + }; return ( - - - {onClose && ( -
+ +
+ {onDismiss && ( +
layoutRef.current?.close()} + onPress={() => layoutRef.current?.dismiss()} Icon={IconCloseRegular} aria-label="Close drawer" type="neutral" backgroundType="transparent" - > + /> +
+ )} + {title && ( +
+ + {title} +
)} - - {title && {title}} - {subtitle && {subtitle}} - {description && ( - - {description} - - )} - {children} - - +
+ + + {subtitle && {subtitle}} + {description && ( + + {description} + + )} + {children} + + +
+ + {hasButtons && ( + + handleButtonPress(button.onPress)} + > + {button.text} + + ) : undefined + } + secondaryButton={ + secondaryButton ? ( + handleButtonPress(secondaryButton.onPress)} + > + {secondaryButton.text} + + ) : undefined + } + link={ + buttonLink ? ( + handleButtonPress(buttonLink.onPress)} + > + {buttonLink.text} + + ) : undefined + } + /> + + )} +
); };