Context
shadcn/ui์ Sidebar ์ปดํฌ๋ํธ๋ Mobile์ผ ํ๊ฒฝ์ผ ๋ Sheet ์ปดํฌ๋ํธ๋ฅผ ํ์ฉํ์ฌ ์ฌ์ด๋๋ฐ๋ฅผ ๋ ๋๋งํ๋ค. Sheet ์ปดํฌ๋ํธ๊ฐ ์ด๋ป๊ฒ ์ฐ์ด๋์ง, ์ ํ์ํ์ง์ ๋ํด์ ๋ถ์ํด๋ณด๊ณ ์ ํ๋ค.
Agenda
โข
Sheet ๊ด๋ จ ์ปดํฌ๋ํธ์ ๋ํด ๋ถ์ํ๊ณ ์ดํดํ๋ ๊ฒ์ ๋ชฉํ๋ก ํจ.
Contents
Extends the Dialog component to display content that complements the main content of the screen.
Plain Text
๋ณต์ฌ
shadcn/ui์ Sheet docs ์ฒซ ๋ถ๋ถ์ ์๋ ์ค๋ช
์ด๋ค. Sheet ์ปดํฌ๋ํธ๋ Dialog๋ฅผ ํ์ฅํ์ฌ ํ๋ฉด์ ๋ถ๊ฐ์ ์ธ ์์๋ฅผ ํํํ ์ ์๋๋ก ๋๋ ์ญํ ์ ์ํํ๋ค.
<Sheet>
<SheetTrigger>Open</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Are you absolutely sure?</SheetTitle>
<SheetDescription>
This action cannot be undone. This will permanently delete your account
and remove your data from our servers.
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
JavaScript
๋ณต์ฌ
๊ณต์ ๋ฌธ์์ ๋์์๋ Usage์ด๋ค. Sheet๋ถํฐ SheetDescription๊น์ง์ ๊ณ์ธต์ผ๋ก ๊ตฌ์ฑ ๋์ด์๋ ๊ฒ์ ์ ์ ์๋ค. Sidebar์ ๋ง์ฐฌ๊ฐ์ง๋ก SheetTrigger๊ฐ ์กด์ฌํ๋ฉฐ ์ด๋ Sheet์ ํ์ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ์ปดํฌ๋ํธ์ธ ๊ฒ ๊ฐ๋ค.
Sheet
import * as SheetPrimitive from '@radix-ui/react-dialog';
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot='sheet' {...props} />;
}
TypeScript
๋ณต์ฌ
shadcn/ui์ sheet ํ์ผ์ ์ฒ์์ผ๋ก ์๋ ์ปดํฌ๋ํธ์ด๋ค. radix-ui์ react-dialog์์ ๋์จ ๋ชจ๋์ ์ฌ์ฉํ๋ ๊ฒ์ ์ ์ ์๋ค. ์ด์ฒ๋ผ shadcn/ui๋ radix-ui์ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ ธ์์ ์ฌ์ฉํ๋ wrapper ์ฒ๋ผ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌ์ฑํ๋ค. ์ด Sheet ์ปดํฌ๋ํธ๋ Root์ props๋ฅผ ๊ทธ๋๋ก ๋ฐ์๋ผ ์ ์๊ณ ์ฌ์ค์ Radix Dialog์ Root๋ฅผ ๊ทธ๋๋ก ๋
ธ์ถํ๋ค๊ณ ๋ณผ ์ ์๋ค. ์๋๋ Sheet๊ฐ ๋ฐ์ ์ ์๋ ํ๋ผ๋ฏธํฐ์ ์ข
๋ฅ.
(parameter) props: {
children?: React.ReactNode;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?(open: boolean): void;
modal?: boolean;
}
JavaScript
๋ณต์ฌ
SheetTrigger
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot='sheet-trigger' {...props} />;
}
JavaScript
๋ณต์ฌ
SheetTrigger๋ Sheet๋ฅผ ์ด ์ ์๋ ์ปดํฌ๋ํธ์ด๋ค. ๋ง์ฐฌ๊ฐ์ง๋ก radix-ui์ Dialog์ Trigger๋ฅผ ๊ฐ์ธ ๋ ๋๋ง ํ๋ค.
SheetClose
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot='sheet-close' {...props} />;
}
TypeScript
๋ณต์ฌ
SheetClose๋ Sheet๋ฅผ ๋ซ์ ์ ์๋ ๋ฒํผ ์ปดํฌ๋ํธ์ด๋ค. radix-ui์ Dialog์ Close๋ฅผ ๊ฐ์ธ์ ๋ ๋๋งํ๋ค.
SheetPortal
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot='sheet-portal' {...props} />;
}
TypeScript
๋ณต์ฌ
SheetPortal์ ์ฌ์ฉ ์ overlay์ ์ฝํ
์ธ ๋ฅผ body์ ๋ ๋๋ง ํ๋ค. ๋ง์ด ์ด์ง ์ด์ํ๊ณ ๋น์ฐํ body์ ๋ ๋๋ง ํ๋๋ฐ, ์ด Portal ๊ธฐ๋ฅ์ z-index๋ overflow ๊ฐ์ ๋ ๋๋ง ๊ณ์ธต ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์ปดํฌ๋ํธ๋ฅผ ๋
ผ๋ฆฌ์ ์ผ๋ก๋ ๋ฐฐ์นํ ๊ณณ์ ์์ ์ํค์ง๋ง ์ค์ ๋ ๋๋ง์ body ํ๊ทธ์ ๊ฐ๊น์ด ๊ณณ์ ๋ ๋๋งํ๋ค.
SheetOverlay
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot='sheet-overlay'
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className,
)}
{...props}
/>
);
}
TypeScript
๋ณต์ฌ
SheetOverlay ์ปดํฌ๋ํธ๋ Sheet๊ฐ ์ด๋ ธ์ ๋ ๋ค ๋ฐฐ๊ฒฝ์ ์ด๋ก๊ฒ ํด์ฃผ๋ ๋ฐํฌ๋ช
๋ ์ด์ด์ด๋ค. Backdrop ํจ๊ณผ๋ฅผ ๋ด๋นํ๋ ์ปดํฌ๋ํธ์ด๋ฉฐ ์ ๊ทผ์ฑ, ํฌ์ปค์ค, ์คํฌ๋กค๋ฝ๊ณผ ๊ฐ์ด ๋ชจ๋ฌ ๋ฐฐ๊ฒฝ์ ๊ตฌ์ฑํ๋ ์์์ด๋ค. ๋ง์ฐฌ๊ฐ์ง๋ก radix-ui Dialog์ Overlay๋ฅผ ์ฌ์ฉํ๋ค.
SheetContent
function SheetContent({
className,
children,
side = 'right',
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: 'top' | 'right' | 'bottom' | 'left';
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot='sheet-content'
className={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
side === 'right' &&
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
side === 'left' &&
'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
side === 'top' &&
'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
side === 'bottom' &&
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
className,
)}
{...props}
>
{children}
<SheetPrimitive.Close className='ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none'>
<XIcon className='size-4' />
<span className='sr-only'>Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
);
}
TypeScript
๋ณต์ฌ
Sheet์ ๋ด์ฉ์ ๋ณด์ฌ์ฃผ๋ ์ปดํฌ๋ํธ์ด๋ค. side ํ๋ผ๋ฏธํฐ๋ฅผ ํ์ฉํด Sheet๊ฐ ํผ์ณ์ง๊ณ ์ ํ๋ ๋ฐฉํฅ์ ์ค์ ํ ์ ์๋ค. ์ง๊ธ๊น์ง Overlay์ children์ Content๋ฅผ ๋ ๋๋งํ๋ ๋ฐฉ์์ผ๋ก ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํ์๋๋ฐ, shadcn/ui์์๋ Overlay์ Content๋ฅผ sibling depth์ ๋์ด ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ์๋ค. ์๋ง ํด๋ฆญ๊ณผ ๊ฐ์ ์ด๋ฒคํธ ์ฒ๋ฆฌ์ ์ปดํฌ๋ํธ ๊ธฐ๋ฅ์ ๋ช
ํํ๊ฒ ๊ตฌ๋ถํ๊ธฐ ์ํ ๊ตฌ์กฐ๊ฐ ์๋๊น ์๊ฐํ๋ค.
const SheetHeader = ({ className, ...props }: React.ComponentProps<'div'>) => {
return (
<div
data-slot='sheet-header'
className={cn('flex flex-col gap-1.5 p-4', className)}
{...props}
/>
);
};
const SheetFooter = ({ className, ...props }: React.ComponentProps<'div'>) => {
return (
<div
data-slot='sheet-footer'
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props}
/>
);
};
TypeScript
๋ณต์ฌ
Sheet์ Header์ Footer ์ปดํฌ๋ํธ์ด๋ค. radix-ui์ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๊ตฌ์ฑ๋์ด์๋ค.
Sidebar์์ ์ฌ์ฉ๋ ์์๋ฅผ ๋ณด๋ฉด ์๋์ ๊ฐ์ด sr-only๋ฅผ ํ์ฉํ์ฌ ํ๋ฉด์๋ ์๋ณด์ด๊ฒ ํ๊ณ ์คํฌ๋ฆฐ ๋ฆฌ๋ ์ฌ์ฉ์์๊ฒ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ค.
<SheetHeader className='sr-only'>
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
TypeScript
๋ณต์ฌ
const SheetTitle = ({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) => {
return (
<SheetPrimitive.Title
data-slot='sheet-title'
className={cn('text-foreground font-semibold', className)}
{...props}
/>
);
};
const SheetDescription = ({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) => {
return (
<SheetPrimitive.Description
data-slot='sheet-description'
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
};
TypeScript
๋ณต์ฌ
Sheet์ Title๊ณผ Description์ด๋ค. ์ผ๋ฐ์ ์ผ๋ก h2 ํ๊ทธ์ p ํ๊ทธ๋ฅผ ํ์ฉํด์ ์ ๋ชฉ๊ณผ ๊ทธ ์ ๋ณด๋ฅผ ๋งํฌ์
ํ ์ ์์ผ๋ ์ ๊ทผ์ฑ ์ฐจ์์์ Title๊ณผ Description์ ์ฌ์ฉํ๋ ๊ฒ ๊ฐ๋ค. ๊ณต์ ๋ฌธ์์ ๋ณด๋ฉด Title๊ณผ Description์ด announced ๋๋ค๊ณ ๋์์๋๋ฐ ์๋ง ์ด๋ฐ ๋งฅ๋ฝ์์์ ์ค๋ช
์ด๋ผ๊ณ ๋ฐ์๋ค์ฌ์ง๋ค.