Skip to content

Commit

Permalink
WEB-2113 drawer layout
Browse files Browse the repository at this point in the history
  • Loading branch information
Pedro Ladaria committed Dec 17, 2024
1 parent 46da8a7 commit 89fee12
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 23 deletions.
54 changes: 54 additions & 0 deletions src/__stories__/drawer-story.tsx
Original file line number Diff line number Diff line change
@@ -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 = (
<Stack space={16}>
{Array.from({length: contentLength}).map((_, index) => (
<Placeholder key={index} height={200} />
))}
</Stack>
);

return (
<>
<ButtonPrimary onPress={() => setIsOpen(true)}>Open Drawer</ButtonPrimary>
{isOpen && (
<Drawer
title={title}
subtitle={subtitle}
description={description}
onClose={() => {
setIsOpen(false);
}}
>
{content}
</Drawer>
)}
</>
);
};

Default.storyName = 'Drawer';

Default.args = {
title: 'Title',
subtitle: 'Subtitle',
description: 'Description',
contentLength: 2,
};
69 changes: 69 additions & 0 deletions src/drawer.css.ts
Original file line number Diff line number Diff line change
@@ -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,
});
128 changes: 105 additions & 23 deletions src/drawer.tsx
Original file line number Diff line number Diff line change
@@ -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<DrawerPropsRef, DrawerLayoutProps>(
({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 (
<Portal>
<FocusTrap>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div
onClick={close}
className={classnames(
styles.overlay,
isOpen ? styles.overlayOpen : styles.overlayClosed
)}
/>
<div
ref={open}
style={{width: isDesktopOrBigger ? width || defaultWidth : 'unset'}}
className={classnames(styles.container, isOpen ? styles.open : styles.closed)}
>
{children}
</div>
</FocusTrap>
</Portal>
);
}
);

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<DrawerPropsRef>(null);

return (
<div>
<IconButton
onPress={handleDismiss}
Icon={IconCloseRegular}
aria-label="Close drawer"
type="neutral"
backgroundType="transparent"
></IconButton>
<Stack space={16}>
{title && <Text5>{title}</Text5>}
{subtitle && <Text4 regular>{subtitle}</Text4>}
{description && (
<Text3 regular color={vars.colors.textSecondary}>
{description}
</Text3>
<DrawerLayout width={width} ref={layoutRef} onClose={onClose}>
<Box
paddingX={{desktop: PADDING_X_DESKTOP, mobile: PADDING_X_MOBILE}}
paddingTop={40}
paddingBottom={24}
>
{onClose && (
<div className={styles.closeButton}>
<IconButton
onPress={() => layoutRef.current?.close()}
Icon={IconCloseRegular}
aria-label="Close drawer"
type="neutral"
backgroundType="transparent"
></IconButton>
</div>
)}
</Stack>
</div>
<Stack space={16}>
{title && <Text5>{title}</Text5>}
{subtitle && <Text4 regular>{subtitle}</Text4>}
{description && (
<Text3 regular color={vars.colors.textSecondary}>
{description}
</Text3>
)}
{children}
</Stack>
</Box>
</DrawerLayout>
);
};

Expand Down

0 comments on commit 89fee12

Please sign in to comment.