Dropdown menu
Playground
TSX
'use client';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,
Icon,
Avatar,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
Button,
type DropdownMenuItemSize,
type DropdownMenuItemVariant,
} from '@mbsw/ui';
import { Check, Ellipsis } from 'lucide-react';
export default function Demo() {
const settings = {
"leadingIcon": false,
"mode": "items",
"variant": "default",
"size": "md",
"disabled": false,
"showValue": true
};
const [open, setOpen] = useState(true);
const [subOpen, setSubOpen] = useState(false);
const windowBlur = useRef(false);
useEffect(() => {
const onBlur = () => {
windowBlur.current = true;
queueMicrotask(() => {
windowBlur.current = false;
});
};
// Only suppress dismissal during the iframe's blur event, not the next
// pointer interaction that focuses the preview again.
window.addEventListener('blur', onBlur, true);
return () => window.removeEventListener('blur', onBlur, true);
}, []);
const [checked, setChecked] = useState<number[]>([0]);
const [menu, setMenu] = useState<HTMLDivElement | null>(null);
const [menuHeight, setMenuHeight] = useState(120);
useLayoutEffect(() => {
if (!menu) return;
const measure = () => setMenuHeight(menu.offsetHeight);
measure();
const observer = new ResizeObserver(measure);
observer.observe(menu);
return () => observer.disconnect();
}, [menu]);
const menuWidth =
settings.mode === 'submenu' ? 'min(180px, calc((100vw - 64px) / 2))' : 180;
const items = ['Option A', 'Option B', 'Option C'];
const leadingAvatar = (label: string) =>
settings.leadingIcon ? (
<span data-slot="leading" aria-hidden="true" style={{ display: 'flex' }}>
<Avatar size="sm" label={label} initials={label.slice(-1)} />
</span>
) : null;
const common = {
size: settings.size as DropdownMenuItemSize,
variant: settings.variant as DropdownMenuItemVariant,
disabled: settings.disabled,
};
return (
<div style={{ position: 'relative', width: menuWidth, height: menuHeight }}>
<DropdownMenu
open={open}
modal={false}
onOpenChange={(next) => {
// Keep the preview open when focus moves to the surrounding controls.
// Selection, Escape and outside clicks inside this document still close it.
if (next || !windowBlur.current) setOpen(next);
}}
>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
tone="secondary"
iconOnly
icon={Ellipsis}
aria-label={"메뉴 열기"}
style={{
position: 'absolute',
bottom: 'calc(100% + 8px)',
left: '50%',
transform: 'translateX(-50%)',
}}
/>
</DropdownMenuTrigger>
<DropdownMenuContent
ref={setMenu}
side="bottom"
align="center"
sideOffset={8}
style={{ width: menuWidth }}
>
{settings.mode === 'submenu' ? (
<DropdownMenuSub
open={subOpen}
onOpenChange={(next) => {
if (next || !windowBlur.current) setSubOpen(next);
}}
>
<DropdownMenuSubTrigger
size={settings.size as DropdownMenuItemSize}
disabled={settings.disabled}
textValue="Options"
>
{leadingAvatar('Options')}
Options
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
sideOffset={13}
alignOffset={-5}
collisionPadding={8}
>
{items.map((label, index) => (
<DropdownMenuItem {...common} key={index}>
{leadingAvatar(label)}
{label}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
) : (
items.map((label, index) =>
settings.mode === 'checkbox' ? (
<DropdownMenuCheckboxItem
{...common}
key={index}
textValue={label}
checked={checked.includes(index)}
onSelect={(event) => event.preventDefault()}
onCheckedChange={(value) =>
setChecked((current) =>
value
? [...current, index]
: current.filter((i) => i !== index),
)
}
>
{leadingAvatar(label)}
{label}
<DropdownMenuItemIndicator>
<Icon icon={Check} size="sm" decorative />
</DropdownMenuItemIndicator>
</DropdownMenuCheckboxItem>
) : (
<DropdownMenuItem
{...common}
key={index}
description={settings.showValue ? 'Description' : undefined}
>
{leadingAvatar(label)}
{label}
</DropdownMenuItem>
),
)
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
Properties
Size
Leading icon
Items
Item variant
Description
Disabled
Usage
동작을 실행하는 메뉴에 사용합니다. 폼의 값을 선택할 때는 Select를 사용합니다. Items를 checkbox로 바꾸면 여러 항목을 선택하거나 해제할 수 있으며, 선택한 항목 오른쪽에 체크가 표시됩니다. 이 예제는 Option A가 기본 선택되어 있고 선택 후에도 메뉴가 열려 있습니다. 선택 표시는 DropdownMenuItemIndicator로 직접 구성합니다. DropdownMenuContent는 포털에 표시됩니다.
TSX
'use client';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
Button,
} from '@mbsw/ui';
export default function Example() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>{"메뉴 열기"}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem description="Description">Option A</DropdownMenuItem>
<DropdownMenuItem description="Description">Option B</DropdownMenuItem>
<DropdownMenuItem description="Description">Option C</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
Playground의 Code 탭에서 현재 설정과 동작이 포함된 예제 전체를 복사할 수 있습니다. 공통 CSS 설치는 설치하기를 확인하세요.