Skip to content

Commit

Permalink
feat: add Drawer component
Browse files Browse the repository at this point in the history
  • Loading branch information
bobbychan committed Jan 8, 2024
1 parent 561c140 commit a704e7e
Show file tree
Hide file tree
Showing 14 changed files with 518 additions and 119 deletions.
6 changes: 6 additions & 0 deletions .changeset/fuzzy-seals-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@alice-ui/react": patch
"@alice-ui/theme": patch
---

Add Drawer component
45 changes: 45 additions & 0 deletions packages/react/src/drawer/drawer-transition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export const slideHorizontal = {
enter: {
x: 'var(--slide-enter)',
opacity: 1,
transition: {
opacity: {
duration: 0.4,
ease: [0.36, 0.66, 0.4, 1],
},
x: {
type: 'spring',
bounce: 0,
duration: 0.6,
},
},
},
exit: {
x: 'var(--slide-exit)',
opacity: 0,
transition: {
duration: 0.4,
ease: [0.36, 0.66, 0.4, 1],
},
},
};

export const slideVertical = {
enter: {
y: 'var(--slide-enter)',
transition: {
y: {
type: 'spring',
bounce: 0,
duration: 0.6,
},
},
},
exit: {
y: 'var(--slide-exit)',
transition: {
duration: 0.4,
ease: [0.36, 0.66, 0.4, 1],
},
},
};
67 changes: 67 additions & 0 deletions packages/react/src/drawer/drawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client';

import { clsx } from '@alice-ui/shared-utils';
import type { DrawerSlots, DrawerVariantProps, SlotsToClasses } from '@alice-ui/theme';
import { drawer } from '@alice-ui/theme';
import type { HTMLMotionProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { useMemo } from 'react';
import type { ModalOverlayProps } from 'react-aria-components';
import { Modal as AriaModal, ModalOverlay } from 'react-aria-components';
import { InternalModalContext } from '../modal/modal';
import { slideHorizontal, slideVertical } from './drawer-transition';

export interface DrawerProps extends ModalOverlayProps, DrawerVariantProps {
/**
* The props to modify the framer motion animation. Use the `variants` API to create your own animation.
*/
motionProps?: HTMLMotionProps<'section'>;
/**
* Classes object to style the modal and its children.
*/
classNames?: SlotsToClasses<DrawerSlots>;
}

function Drawer(props: DrawerProps) {
const {
children,
classNames,
className,
placement,
backdrop = 'opaque',
motionProps,
...otherProps
} = props;

const slots = useMemo(() => drawer({ placement, backdrop }), [backdrop, placement]);

const baseStyles = clsx(classNames?.base, className);

return (
<ModalOverlay {...otherProps} className={slots.backdrop({ class: classNames?.backdrop })}>
{({ state }) => (
<InternalModalContext.Provider value={{ slots, classNames, state }}>
<AnimatePresence>
{state.isOpen && (
<motion.div
className={slots.base({ class: baseStyles })}
data-placement={placement}
animate="enter"
exit="exit"
initial="exit"
variants={
placement === 'top' || placement === 'bottom' ? slideVertical : slideHorizontal
}
{...motionProps}
>
<AriaModal style={{ height: '100%' }}>{children}</AriaModal>
</motion.div>
)}
</AnimatePresence>
</InternalModalContext.Provider>
)}
</ModalOverlay>
);
}

export { Drawer };
22 changes: 22 additions & 0 deletions packages/react/src/drawer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client';

import { ModalBody } from '../modal/modal-body';
import { ModalCloseButton } from '../modal/modal-close-button';
import { ModalContent } from '../modal/modal-content';
import { ModalFooter } from '../modal/modal-footer';
import { ModalHeader } from '../modal/modal-header';
import { Drawer } from './drawer';

// export types
export type { DrawerProps } from './drawer';

// export component

export {
Drawer,
ModalBody as DrawerBody,
ModalCloseButton as DrawerCloseButton,
ModalContent as DrawerContent,
ModalFooter as DrawerFooter,
ModalHeader as DrawerHeader,
};
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './card';
export * from './checkbox';
export * from './chip';
export * from './divider';
export * from './drawer';
export * from './image';
export * from './input';
export * from './link';
Expand Down
6 changes: 3 additions & 3 deletions packages/react/src/modal/modal-transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export const scaleInOut = {
},
y: {
type: 'spring',
stiffness: 200,
damping: 22,
bounce: 0,
duration: 0.6,
},
},
},
Expand All @@ -24,7 +24,7 @@ export const scaleInOut = {
y: 'var(--slide-exit)',
opacity: 0,
transition: {
duration: 0.6,
duration: 0.4,
ease: [0.36, 0.66, 0.4, 1],
},
},
Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { clsx } from '@alice-ui/shared-utils';
import type {
DrawerReturnType,
ModalReturnType,
ModalSlots,
ModalVariantProps,
Expand All @@ -27,7 +28,7 @@ export interface ModalProps extends ModalOverlayProps, ModalVariantProps {
}

interface InternalModalContextValue {
slots: ModalReturnType;
slots: ModalReturnType | DrawerReturnType;
classNames?: SlotsToClasses<ModalSlots>;
state: ModalRenderProps['state'];
}
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/text-field/text-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ function TextField(props: TextFieldProps, ref: ForwardedRef<HTMLDivElement>) {
let { labelProps, inputProps, descriptionProps, errorMessageProps } = useTextField<any>(
{
...removeDataAttributes(props),
value: inputValue,
inputElementType,
label,
onChange: setInputValue,
Expand Down
145 changes: 145 additions & 0 deletions packages/react/stories/drawer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { drawer } from '@alice-ui/theme';
import { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import { Button } from '../src/button';
import {
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerProps,
} from '../src/drawer';

const TemplateContent = () => (
<>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus
hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam.
</p>
</>
);

const meta: Meta<typeof Drawer> = {
title: 'Components/Drawer',
component: Drawer,
argTypes: {
placement: {
control: {
type: 'select',
},
options: ['top', 'right', 'bottom', 'left'],
},
backdrop: {
control: {
type: 'select',
},
options: ['transparent', 'blur', 'opaque'],
},
isDismissable: {
control: {
type: 'boolean',
},
},
isKeyboardDismissDisabled: {
control: {
type: 'boolean',
},
},
children: {
control: {
disable: true,
},
},
},
decorators: [
(Story) => (
<div className="flex h-screen w-screen items-center justify-center">
<Story />
</div>
),
],
};

export default meta;
type Story = StoryObj<typeof Drawer>;

const defaultProps: DrawerProps = {
...drawer.defaultVariants,
};

const Template = (args: DrawerProps) => {
const [isOpen, setOpen] = React.useState(false);

return (
<>
<Button onPress={() => setOpen(true)}>Open drawer</Button>
<Drawer {...args} isDismissable isOpen={isOpen} onOpenChange={setOpen}>
<DrawerContent>
{({ close }) => (
<>
<DrawerCloseButton />
<DrawerHeader>Drawer Title</DrawerHeader>
<DrawerBody>
<TemplateContent />
</DrawerBody>
<DrawerFooter>
<Button color="danger" variant="light" onPress={close}>
Close
</Button>
<Button color="primary" onPress={close}>
Action
</Button>
</DrawerFooter>
</>
)}
</DrawerContent>
</Drawer>
</>
);
};

export const Left: Story = {
render: Template,

args: {
...defaultProps,
backdrop: 'blur',
placement: 'left',
className: 'w-[360px]',
},
};

export const Right: Story = {
render: Template,

args: {
...defaultProps,
backdrop: 'opaque',
placement: 'right',
className: 'w-[360px]',
},
};

export const Top: Story = {
render: Template,

args: {
...defaultProps,
backdrop: 'blur',
placement: 'top',
className: 'h-[300px]',
},
};

export const Bottom: Story = {
render: Template,

args: {
...defaultProps,
backdrop: 'blur',
placement: 'bottom',
className: 'h-[300px]',
},
};
Loading

0 comments on commit a704e7e

Please sign in to comment.