Context
Auto-complete ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค๊ธฐ ์ํด ๊ฒ์ ๊ธฐ๋ฅ์ ํ์ฌํ Popover๊ฐ ํ์ํ๋ค. shadcn์์๋ Command๋ผ๋ ์ปดํฌ๋ํธ๋ฅผ ํ์ฉํ์ฌ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค. ์ด ์ปดํฌ๋ํธ๋ฅผ ๋ถ์ํ๊ณ ์ดํดํ๊ธฐ ์ํด ์ด ๊ธ์ ์์ฑํ๋ค.
Agenda
โข
Command์ปดํฌ๋ํธ๋ฅผ ๋ถ์ํ๊ณ ์ดํดํ๋ ๊ฒ์ ๋ชฉํ๋ก ํจ
Contents
Auto-complete ์ปดํฌ๋ํธ๋ ์ฌ์ฉ์๊ฐ ์
๋ ฅํ๋ ๊ฒ์ ๊ธฐ๋ฐ์ผ๋ก ์ต์
์์ ์ฐพ๊ณ ์ ํ ์ ๊ทธ ๊ฐ์ ํ ๋นํ๋ค. ์ผ๋ฐ์ ์ผ๋ก Auto-complete๋ Input ํํ๋ก ์ฌ์ฉ์๊ฐ ์ง์ ์
๋ ฅํ ์ ์์ด์ผํ๋๋ฐ, ์ด ๊ฒฝ์ฐ ๋ช๋ช ๋ฌธ์ ๊ฐ ์๋ค. ์ฌ์ฉ์์ ์๊ตฌ์ฌํญ์ด ๋ฐ์ํ์ ์ด ์์๋๋ฐ, ์ด๋ ๊ฐ์ ์
๋ ฅํ ๋ค ์ ํ ํ ๋ค์ ์ฌ์
๋ ฅ ํ ๋ ์ด ๋ ์๋ก์ด ๊ฐ์ ์ ํํ์ง ์์ผ๋ฉด ์ด์ ๊ฐ์ผ๋ก ๋จ์์๊ฒ ํด๋ฌ๋ผ๋ ๊ฒ์ด์๋ค. ์ํ ๊ฐ์ ์ ์ฅํด๋๋ ๊ฒ์ผ๋ก ํฐ ์ด๋ ค์์ด ์์ง๋ ์์ง๋ง ํต์ผ์ฑ์ด ๊นจ์ง๋ ๋๋์ด๋ค. shadcn์์ ๋ ํผ๋ฐ์ค๋ฅผ ์ฐพ๋ ์ค Command๋ผ๋ ์ปดํฌ๋ํธ๊ฐ ์์ด ์ด ๊ฒ์ ๋ํด ๋ถ์ํด๋ณธ๋ค.
Command
const Command = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) => {
return (
<CommandPrimitive
data-slot='command'
className={cn(
'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
className,
)}
{...props}
/>
);
};
TypeScript
๋ณต์ฌ
Command ์ปดํฌ๋ํธ์ด๋ค. ๋ค๋ฅธ shadcn/ui ์ปดํฌ๋ํธ์ ๊ฐ๊ฒ ์ด๋ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ธ๋ Wrapper ํ์์ผ๋ก ๊ฐ๋จํ๊ฒ ๊ตฌํ๋์ด ์๋ค. ๊ทธ๋ฐ๋ฐ, ์ด Command๋ radix-ui๊ฐ ์๋ cmdk๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ปดํฌ๋ํธ์ด๋ค. ๊ธฐ๋ณธ ์คํ์ผ๋ง์ผ๋ก flex display์ ๊ทธ ์์ ํญ๋ชฉ๋ค์ ์ธ๋ก๋ก ๋ฐฐ์นํ๋ค. ์ด Command ์ปดํฌ๋ํธ๋ ์๋์ ๊ธฐ์ ํ ์ฌ๋ฌ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ธ๋ ๋๋ค๋ฅธ Wrapper์ด๋ค.
CommandDialog
const CommandDialog = ({
title = 'Command Palette',
description = 'Search for a command to run...',
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string;
description?: string;
className?: string;
showCloseButton?: boolean;
}) => {
return (
<Dialog {...props}>
<DialogHeader className='sr-only'>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn('overflow-hidden p-0', className)}
showCloseButton={showCloseButton}
>
<Command className='**:[[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5'>
{children}
</Command>
</DialogContent>
</Dialog>
);
};
TypeScript
๋ณต์ฌ
Command์ ๋ ๋๋ง ๋ ํญ๋ชฉ์ ๊ฐ์ธ๋ ์ปดํฌ๋ํธ์ด๋ค. shadcn/ui์ Dialog ์ปดํฌ๋ํธ๋ฅผ ํ์ฉํ์ฌ ํ
ํ๋ฆฟ์ ๊ตฌ์ฑํ๋ค.
CommandInput
const CommandInput = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) => {
return (
<div
data-slot='command-input-wrapper'
className='flex h-10 items-center gap-2 border-b px-3'
>
<SearchIcon className='size-4 shrink-0 opacity-50' />
<CommandPrimitive.Input
data-slot='command-input'
className={cn(
'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
/>
</div>
);
};
TypeScript
๋ณต์ฌ
CommandInput ์ปดํฌ๋ํธ๋ Command ์์์ ์ฌ์ฉ๋๋ ๊ฒ์ ์
๋ ฅ ์ปดํฌ๋ํธ์ด๋ค. ๋ณดํต ์ด ์๋์ CommandList๊ฐ ๋ฐฐ์น ๋๋ฉฐ ์
๋ ฅํ ๊ฒ์ ๊ธฐ๋ฐ์ผ๋ก CommandList๊ฐ ํํฐ๋ง ๋์ด ํ์ ๋๋ค.
์๋ CommandInput ๋ด๋ถ์ wrapper์ธ div์ className์ ๋ค์๊ณผ ๊ฐ์๋ค.
className='flex h-9 items-center gap-2 border-b px-3'
JavaScript
๋ณต์ฌ
๊ทธ๋ฐ๋ฐ CommandPrimitive.Input์ height์ด h-10์ผ๋ก ๋์ด์์ด์ h-10์ผ๋ก ์์ ํ์๋ค.
CommandList
const CommandList = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) => {
return (
<CommandPrimitive.List
data-slot='command-list'
className={cn(
'max-h-75 scroll-py-1 overflow-x-hidden overflow-y-auto',
className,
)}
{...props}
/>
);
};
TypeScript
๋ณต์ฌ
Command ํญ๋ชฉ์ด ํ์ ๋๋ ์์ญ์ด๋ค. CommandItem์ด ํ์๋๋ ๊ณณ์ด๋ค.
CommandEmpty
const CommandEmpty = ({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) => {
return (
<CommandPrimitive.Empty
data-slot='command-empty'
className='py-6 text-center text-sm'
{...props}
/>
);
};
TypeScript
๋ณต์ฌ
CommandList์ ๋์์ด ์์ ๋ ๋
ธ์ถ ๋๋ ํญ๋ชฉ์ด๋ค.
CommandGroup
const CommandGroup = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) => {
return (
<CommandPrimitive.Group
data-slot='command-group'
className={cn(
'text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium',
className,
)}
{...props}
/>
);
};
TypeScript
๋ณต์ฌ
Command์ ํ์ ๋๋ List๋ Group์ ์ง์ด์ ํ์ ๋ ์ ์๋ค. ์ด Group ๋จ์์ Wrapper๋ฅผ ๋ด๋นํ๋ CommandGroup ์ปดํฌ๋ํธ์ด๋ค. CommandGroup์ heading ํ๋ผ๋ฏธํฐ์ ๊ฐ์ ๋ถ์ฌํด์ ํด๋น ๊ทธ๋ฃน์ ํ์ดํ์ ํ์ํ ์ ์๋ค.
CommandSeparator
const CommandSeparator = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) => {
return (
<CommandPrimitive.Separator
data-slot='command-separator'
className={cn('bg-border -mx-1 h-px', className)}
{...props}
/>
);
};
TypeScript
๋ณต์ฌ
๊ทธ๋ฃน๊ณผ ๊ทธ๋ฃน๊ฐ ๋ถ๋ฆฌ์ ์ ํ์ ํด์ฃผ๋ ์ปดํฌ๋ํธ์ด๋ค. -mx-1์ ์์ชฝ์ผ๋ก ์ด ๋ถ๋ฆฌ์ ์ ๋๋ ค์ฃผ๋ ์ญํ ์ ์ํํ๋๋ฐ ์๋ํ๋ฉด ๋ง์ฝ ๋ถ๋ชจ ์ปดํฌ๋ํธ์ ํจ๋ฉ์ด ์์ผ๋ฉด ๋ถ๋ฆฌ์ ์ด ๋๊น์ง ๊ฐ์ง ์๊ธฐ ๋๋ฌธ์ด๋ค.
CommandItem
const CommandItem = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) => {
return (
<CommandPrimitive.Item
data-slot='command-item'
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
};
TypeScript
๋ณต์ฌ
CommandList์ ํ์ ๋๋ Item ์ปดํฌ๋ํธ์ด๋ค.
CommandShortcut
const CommandShortcut = ({
className,
...props
}: React.ComponentProps<'span'>) => {
return (
<span
data-slot='command-shortcut'
className={cn(
'text-muted-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
);
};
TypeScript
๋ณต์ฌ
Command ๋ฉ๋ด์์ ๋จ์ถํค ํ์์ฉ ์ปดํฌ๋ํธ์ด๋ค. ml-auto๋ฅผ ํตํด ์ค๋ฅธ์ชฝ ๋์ ๋ฐฐ์นํ๋ฉฐ tracking-widest๋ก ๋์ ์๊ฐ์ ์ ์ฉํ๋ค.