Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created a reusable "Drawer" component #89

Merged
merged 7 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions src/components/ui/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const DialogContent = forwardRef<
{...props}
>
{children}
<DialogClose className={cn('absolute right-2 top-2')}>
<DialogClose className={cn('absolute right-3 top-3')}>
<Icon iconName="close" color="black" size="sm" />
<span className="sr-only">Close</span>
</DialogClose>
Expand All @@ -79,6 +79,16 @@ const DialogHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) =
);
DialogHeader.displayName = 'DialogHeader';

/**
* This is a custom component that we use to wrap the body (between header and footer) of the dialog.
*/
const DialogBody = forwardRef<ElementRef<'div'>, HTMLAttributes<HTMLDivElement>>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you still need this DialogBody?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, Yes, don't care about this comment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆

({ className, ...props }, ref) => (
<div ref={ref} className={cn('px-6 py-4', className)} {...props} />
),
);
DialogBody.displayName = 'DialogBody';

const DialogFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div className={cn('px-4 pb-4 flex justify-end gap-4', className)} {...props} />
);
Expand All @@ -100,16 +110,6 @@ const DialogDescription = forwardRef<
));
DialogDescription.displayName = PrimitiveDescription.displayName;

/**
* This is a custom component that we use to wrap the body (between header and footer) of the dialog.
*/
const DialogBody = forwardRef<ElementRef<'div'>, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('px-6 py-4', className)} {...props} />
),
);
DialogBody.displayName = 'DialogBody';

export {
DialogBody,
DialogClose,
Expand Down
147 changes: 147 additions & 0 deletions src/components/ui/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* This file is based on the Sheet component from shadcn and customized for our needs.
* The Sheet component of shadcn is based on the Dialog component of Radix UI.
*
* See the official docs for more info:
* shadcn/ui: https://ui.shadcn.com/docs/components/sheet
* Radix UI: https://www.radix-ui.com/themes/docs/components/dialog
*/
'use client';

import { Icon } from '@/components/ui';
import { cn } from '@/lib/tailwind/utils';

import {
Close as PrimitiveClose,
Content as PrimitiveContent,
Description as PrimitiveDescription,
Overlay as PrimitiveOverlay,
Portal as PrimitivePortal,
Root as PrimitiveRoot,
Title as PrimitiveTitle,
Trigger as PrimitiveTrigger,
} from '@radix-ui/react-dialog';
import { cva, type VariantProps } from 'class-variance-authority';
import { ComponentPropsWithoutRef, ElementRef, forwardRef, HTMLAttributes } from 'react';

const DrawerRoot = PrimitiveRoot;
const DrawerTrigger = PrimitiveTrigger;
const DrawerClose = PrimitiveClose;
const DrawerPortal = PrimitivePortal;

const DrawerOverlay = forwardRef<
ElementRef<typeof PrimitiveOverlay>,
ComponentPropsWithoutRef<typeof PrimitiveOverlay>
>(({ className, ...props }, ref) => (
<PrimitiveOverlay
className={cn(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked how you classify className by it's categories!
Maybe we can create a note or doc somewhere about how we classify and organize classNames in cn() method

Copy link
Member Author

@nick-y-ito nick-y-ito Dec 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good idea!
I was actually considering creating a STYLEGUIDE.md file to document coding rules for a project, such as using I to name interfaces like IMyProps.

Shall we create a new issue for that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created #91

'fixed inset-0 z-50',
'bg-overlay',
'data-[state=open]:animate-fadeIn data-[state=closed]:animate-fadeOut',
className,
)}
{...props}
ref={ref}
/>
));
DrawerOverlay.displayName = PrimitiveOverlay.displayName;

const DrawerVariants = cva(cn('fixed z-50 bg-white flex flex-col'), {
variants: {
side: {
bottom: cn(
'inset-x-0 bottom-0 rounded-t max-h-[calc(100vh-2rem)]',
'data-[state=open]:animate-slideInFromBottom data-[state=closed]:animate-slideOutToBottom',
),
right: cn(
'inset-y-0 right-0 h-full w-5/6 max-w-sm',
'data-[state=open]:animate-slideInFromRight data-[state=closed]:animate-slideOutToRight',
),
},
},
defaultVariants: {
side: 'bottom',
},
});

interface DrawerContentProps
extends ComponentPropsWithoutRef<typeof PrimitiveContent>,
VariantProps<typeof DrawerVariants> {}

const DrawerContent = forwardRef<ElementRef<typeof PrimitiveContent>, DrawerContentProps>(
({ side, className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<PrimitiveContent ref={ref} className={cn(DrawerVariants({ side }), className)} {...props}>
{children}
<DrawerClose className={cn('absolute right-3 top-3')}>
<Icon iconName="close" color="black" size="sm" />
<span className="sr-only">Close</span>
</DrawerClose>
</PrimitiveContent>
</DrawerPortal>
),
);
DrawerContent.displayName = PrimitiveContent.displayName;

const DrawerHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'h-12 shrink-0 px-4 border-b border-gray-light grow-1',
'flex items-center',
className,
)}
{...props}
/>
);
DrawerHeader.displayName = 'DrawerHeader';

/**
* This is a custom component that we use to wrap the body (between header and footer) of the dialog.
*/
const DrawerBody = forwardRef<ElementRef<'div'>, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('px-4 pt-4 pb-6 overflow-y-auto max-h-full', className)}
{...props}
/>
),
);
DrawerBody.displayName = 'DrawerBody';

const DrawerFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div className={cn('mt-auto px-4 pt-2 pb-12 flex justify-end gap-4', className)} {...props} />
);
DrawerFooter.displayName = 'DrawerFooter';

const DrawerTitle = forwardRef<
ElementRef<typeof PrimitiveTitle>,
ComponentPropsWithoutRef<typeof PrimitiveTitle>
>(({ className, ...props }, ref) => (
<PrimitiveTitle ref={ref} className={cn('text-2xl', className)} {...props} />
));
DrawerTitle.displayName = PrimitiveTitle.displayName;

const DrawerDescription = forwardRef<
ElementRef<typeof PrimitiveDescription>,
ComponentPropsWithoutRef<typeof PrimitiveDescription>
>(({ className, ...props }, ref) => (
<PrimitiveDescription ref={ref} className={cn('', className)} {...props} />
));
DrawerDescription.displayName = PrimitiveDescription.displayName;

export {
DrawerBody,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerOverlay,
DrawerPortal,
DrawerRoot,
DrawerTitle,
DrawerTrigger,
DrawerVariants,
};
75 changes: 75 additions & 0 deletions src/components/ui/DrawerUsageExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* This file is used as an example for the Drawer component.
* Once you're done with the example, you can delete this file.
*/
'use client';

import {
Button,
DrawerBody,
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerRoot,
DrawerTitle,
DrawerTrigger,
} from '@/components/ui';

import { useState } from 'react';

export const DrawerUsageExample = () => {
const [isDrawerOpen, setIsDrawerOpen] = useState(false);

const handleDelete = () => {
alert('Deleted!');
setIsDrawerOpen(false);
};

return (
<DrawerRoot open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
<DrawerTrigger asChild>
<Button>Open</Button>
</DrawerTrigger>
<DrawerContent side="bottom">
<DrawerHeader>
<DrawerTitle>Delete Container</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<ol>
<li>1. Are you sure you want to delete?</li>
<li>2. Are you sure you want to delete?</li>
<li>3. Are you sure you want to delete?</li>
<li>4. Are you sure you want to delete?</li>
<li>5. Are you sure you want to delete?</li>
<li>6. Are you sure you want to delete?</li>
<li>7. Are you sure you want to delete?</li>
<li>8. Are you sure you want to delete?</li>
<li>9. Are you sure you want to delete?</li>
<li>10. Are you sure you want to delete?</li>
<li>11. Are you sure you want to delete?</li>
<li>12. Are you sure you want to delete?</li>
<li>13. Are you sure you want to delete?</li>
<li>14. Are you sure you want to delete?</li>
<li>15. Are you sure you want to delete?</li>
<li>16. Are you sure you want to delete?</li>
<li>17. Are you sure you want to delete?</li>
<li>18. Are you sure you want to delete?</li>
<li>19. Are you sure you want to delete?</li>
<li>20. Are you sure you want to delete?</li>
</ol>
</DrawerBody>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="cancel" size="sm">
Cancel
</Button>
</DrawerClose>
<Button variant="error" size="sm" onClick={handleDelete}>
Delete
</Button>
</DrawerFooter>
</DrawerContent>
</DrawerRoot>
);
};
14 changes: 14 additions & 0 deletions src/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,18 @@ export {
DialogTitle,
DialogTrigger,
} from './Dialog';
export {
DrawerBody,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerOverlay,
DrawerPortal,
DrawerRoot,
DrawerTitle,
DrawerTrigger,
DrawerVariants,
} from './Drawer';
export { Icon } from './Icon';
20 changes: 20 additions & 0 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ module.exports = {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
slideInFromBottom: {
'0%': { transform: 'translateY(100%)' },
'100%': { transform: 'translateY(0)' },
},
slideOutToBottom: {
'0%': { transform: 'translateY(0)' },
'100%': { transform: 'translateY(100%)' },
},
slideInFromRight: {
'0%': { transform: 'translateX(100%)' },
'100%': { transform: 'translateX(0)' },
},
slideOutToRight: {
'0%': { transform: 'translateX(0)' },
'100%': { transform: 'translateX(100%)' },
},
'accordion-down': {
from: { height: 0 },
to: { height: 'var(--radix-accordion-content-height)' },
Expand All @@ -50,6 +66,10 @@ module.exports = {
animation: {
fadeIn: 'fadeIn 0.3s ease-in-out',
fadeOut: 'fadeOut 0.3s ease-in-out',
slideInFromBottom: 'slideInFromBottom 0.3s ease-in-out',
slideOutToBottom: 'slideOutToBottom 0.3s ease-in-out',
slideInFromRight: 'slideInFromRight 0.3s ease-in-out',
slideOutToRight: 'slideOutToRight 0.3s ease-in-out',
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
pulse: 'pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite',
Expand Down