Skip to content

Commit

Permalink
feat: add sheet animation
Browse files Browse the repository at this point in the history
  • Loading branch information
alter-eggo committed Nov 15, 2024
1 parent 7361358 commit c49a809
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 71 deletions.
18 changes: 18 additions & 0 deletions packages/tokens/src/keyframes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
export const keyframes = {
slideUpSheet: {
from: { transform: 'translate(-50%, -48%)', opacity: 0 },
to: { transform: 'translate(-50%, -50%)', opacity: 1 },
},
slideDownSheet: {
from: { transform: 'translate(-50%, -50%)', opacity: 1 },
to: { transform: 'translate(-50%, -48%)', opacity: 0 },
},
contentShow: {
from: {
opacity: 0,
Expand All @@ -9,6 +17,16 @@ export const keyframes = {
transform: 'translate(-50%, -50%) scale(1)',
},
},
contentHide: {
from: {
opacity: 1,
transform: 'translate(-50%, -50%) scale(1)',
},
to: {
opacity: 0,
transform: 'translate(-50%, -48%) scale(0.96)',
},
},
fadein: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
Expand Down
43 changes: 26 additions & 17 deletions packages/ui/src/components/sheet/sheet.web.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useRef } from 'react';

import type { Meta } from '@storybook/react';
import { Box, Flex } from 'leather-styles/jsx';
Expand All @@ -23,14 +23,17 @@ const meta: Meta<typeof Component> = {
export default meta;

export function Sheet() {
const [isShowing, setIsShowing] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const closeRef = useRef<HTMLButtonElement>(null);

return (
<>
<Button onClick={() => setIsShowing(!isShowing)}>Open</Button>
<Button onClick={() => triggerRef.current?.click()}>Open</Button>
<Component
header={<SheetHeader title="Leather" />}
isShowing={isShowing}
onClose={() => setIsShowing(false)}
onClose={() => closeRef.current?.click()}
header={<SheetHeader title="Leather" onClose={() => closeRef.current?.click()} />}
triggerRef={triggerRef}
closeRef={closeRef}
>
<Box textAlign="center" height="60vh">
Let's start talk sheet.
Expand All @@ -41,16 +44,19 @@ export function Sheet() {
}

export function SheetWithFooter() {
const [isShowing, setIsShowing] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const closeRef = useRef<HTMLButtonElement>(null);

return (
<>
<Button onClick={() => setIsShowing(!isShowing)}>Open</Button>
<Button onClick={() => triggerRef.current?.click()}>Open</Button>
<Component
header={<SheetHeader title="Send" />}
isShowing={isShowing}
onClose={() => setIsShowing(false)}
onClose={() => closeRef.current?.click()}
triggerRef={triggerRef}
closeRef={closeRef}
footer={
<Button fullWidth onClick={() => setIsShowing(false)}>
<Button fullWidth onClick={() => closeRef.current?.click()}>
Close
</Button>
}
Expand All @@ -64,20 +70,23 @@ export function SheetWithFooter() {
}

export function SheetWithButtonsFooter() {
const [isShowing, setIsShowing] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const closeRef = useRef<HTMLButtonElement>(null);

return (
<>
<Button onClick={() => setIsShowing(!isShowing)}>Open</Button>
<Button onClick={() => triggerRef.current?.click()}>Open</Button>
<Component
header={<SheetHeader title="Send" />}
isShowing={isShowing}
onClose={() => setIsShowing(false)}
onClose={() => closeRef.current?.click()}
triggerRef={triggerRef}
closeRef={closeRef}
footer={
<Flex flexDirection="row" gap="space.04" width="100%">
<Button flexGrow={1} variant="outline" onClick={() => setIsShowing(false)}>
<Button flexGrow={1} variant="outline" onClick={() => closeRef.current?.click()}>
Cancel
</Button>
<Button flexGrow={1} onClick={() => setIsShowing(false)}>
<Button flexGrow={1} onClick={() => closeRef.current?.click()}>
Send
</Button>
</Flex>
Expand Down
135 changes: 81 additions & 54 deletions packages/ui/src/components/sheet/sheet.web.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { JSXElementConstructor, ReactElement, ReactNode, cloneElement } from 'react';
import { JSXElementConstructor, ReactElement, ReactNode, RefObject, cloneElement } from 'react';

import * as RadixDialog from '@radix-ui/react-dialog';
import { css } from 'leather-styles/css';
Expand All @@ -10,8 +10,10 @@ import { pxStringToNumber } from '@leather.io/utils';
import { SheetFooter } from './components/sheet-footer.web';

export interface SheetProps {
isShowing: boolean;
onClose?(): void;
triggerRef?: RefObject<HTMLButtonElement>;
closeRef?: RefObject<HTMLButtonElement>;
isDefaultOpen?: boolean;
}
interface RadixDialogProps extends SheetProps {
children: ReactNode;
Expand All @@ -38,65 +40,90 @@ export function Sheet({
footer,
header,
onClose,
isShowing,
wrapChildren = true,
triggerRef,
closeRef,
isDefaultOpen = false,
}: RadixDialogProps) {
const maxHeightOffset = getHeightOffset(header, footer);
const contentMaxHeight = getContentMaxHeight(maxHeightOffset);

return (
<RadixDialog.Root open={isShowing}>
<RadixDialog.Portal>
<RadixDialog.Overlay
className={css({
bg: 'overlay',
position: 'fixed',
inset: 0,
animation: 'overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1)',
zIndex: 999,
})}
>
<RadixDialog.Content
onPointerDownOutside={onClose}
className={css({
bg: 'ink.background-primary',
// remove borderRadius on small to give impression of full page
borderRadius: { base: '0', md: 'md' },
boxShadow:
'hsl(206 22% 7% / 35%) 0 10px 38px -10px, hsl(206 22% 7% / 20%) 0 10px 20px -15px',
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { base: '100vw', md: '90vw' },
height: { base: '100%', md: 'auto' },
maxWidth: { base: '100vw', md: 'pageWidth' },
maxHeight: { base: '100vh', md: '90vh' },
'&[data-state=open]': {
animation: { base: '', md: 'contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)' },
},
})}
>
{header && cloneElement(header, { onClose })}
<RadixDialog.Root defaultOpen={isDefaultOpen}>
<RadixDialog.Trigger ref={triggerRef} />
<RadixDialog.Close ref={closeRef} />
<RadixDialog.Overlay
className={css({
display: { base: 'none', md: 'block' },
bg: 'overlay',
position: 'fixed',
inset: 0,
zIndex: 999,
'&[data-state=open]': {
animation: {
base: '',
md: 'fadein 50ms',
},
},
'&[data-state=closed]': {
animation: {
base: '',
md: 'fadeout 50ms',
},
},
})}
/>
<RadixDialog.Content
onPointerDownOutside={onClose}
onEscapeKeyDown={onClose}
className={css({
bg: 'ink.background-primary',
// remove borderRadius on small to give impression of full page
borderRadius: { base: '0', md: 'md' },
boxShadow:
'hsl(206 22% 7% / 35%) 0 10px 38px -10px, hsl(206 22% 7% / 20%) 0 10px 20px -15px',
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { base: '100vw', md: '90vw' },
height: { base: '100%', md: 'auto' },
maxWidth: { base: '100vw', md: 'pageWidth' },
maxHeight: { base: '100vh', md: '90vh' },
zIndex: 999,

'&[data-state=open]': {
animation: {
base: 'slideUpSheet 70ms',
md: 'contentShow 70ms',
},
},
'&[data-state=closed]': {
animation: {
base: 'slideDownSheet 70ms',
md: 'contentHide 70ms',
},
},
})}
>
{header && cloneElement(header, { onClose })}

{wrapChildren ? (
<Box
style={{
height: '100%',
maxHeight: contentMaxHeight,
marginBottom: footer ? token('sizes.footerHeight') : token('spacing.space.04'),
overflowY: 'auto',
}}
>
{children}
</Box>
) : (
children
)}
{footer && <SheetFooter>{footer}</SheetFooter>}
</RadixDialog.Content>
</RadixDialog.Overlay>
</RadixDialog.Portal>
{wrapChildren ? (
<Box
style={{
height: '100%',
maxHeight: contentMaxHeight,
marginBottom: footer ? token('sizes.footerHeight') : token('spacing.space.04'),
overflowY: 'auto',
}}
>
{children}
</Box>
) : (
children
)}
{footer && <SheetFooter>{footer}</SheetFooter>}
</RadixDialog.Content>
</RadixDialog.Root>
);
}

0 comments on commit c49a809

Please sign in to comment.