MenuButton

Action

트리거와 명령 목록을 하나로 묶는 메뉴 버튼.

Usage

한 버튼 아래에 서로 관련된 여러 행동을 제공할 때 도구 모음에서 보조 행동을 압축할 때

import

import

import { MenuButton } from '@mildang/design-system/MenuButton';

예제를 복사해 쓸 때 필요한 준비

@mildang/icons 를 따로 설치한다. DS 패키지에 아이콘 컴포넌트가 포함되지 않는다.

@mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.

API Reference

MenuButton Props

Prop

Type

Default

children
필수

ReactNode

지정 안 함

align

"end" | "start" | "center"

지정 안 함

aria-label

string

지정 안 함

contentProps

Omit<MenuContentProps, "children" | "sideOffset" | "align" | "side">

지정 안 함

defaultOpen

boolean

지정 안 함

disabled

boolean

지정 안 함

endIcon

ReactNode

지정 안 함

label

ReactNode

지정 안 함

onOpenChange

(open: boolean) => void

지정 안 함

onSearchChange

(value: string) => void

지정 안 함

open

boolean

지정 안 함

searchable

boolean

false

searchPlaceholder

string

지정 안 함

side

"bottom" | "left" | "right" | "top"

지정 안 함

sideOffset

number

4

size

"xs" | "sm" | "md" | "lg" | "xl"

지정 안 함

startIcon

ReactNode

지정 안 함

trigger

ReactNode

지정 안 함

variant

"primary" | "impact" | "secondary" | "tertiary" | "quaternary"

지정 안 함

사용 가이드

권장

  • 트리거 레이블만으로 메뉴의 목적을 알 수 있게 한다.
  • 메뉴 항목은 짧고 구체적인 행동 문구로 쓴다.

지양

  • 하나의 행동만 있는 경우 MenuButton을 사용하지 않는다.
  • 접근 가능한 이름 없는 트리거를 만들지 않는다.

예제

기본 사용

기본 IconButton 트리거와 두 메뉴 그룹을 조합합니다.

import { MenuButton } from '@mildang/design-system/MenuButton';
import { IconButton } from '@mildang/design-system/IconButton';
import StarFill from '@mildang/icons/react/star-fill';
import { Menu } from '@mildang/design-system/Menu';

const MenuButtonDefaultExample = () => (
    <MenuButton
      trigger={
        <IconButton variant="primary" aria-label="즐겨찾기 메뉴 열기">
          <StarFill />
        </IconButton>
      }
    >
      <Menu.Group>
        <Menu.Label>Label</Menu.Label>
        <Menu.Item>Option</Menu.Item>
        <Menu.Item>Option</Menu.Item>
        <Menu.Item>Option</Menu.Item>
      </Menu.Group>
      <Menu.Separator />
      <Menu.Group>
        <Menu.Label>Label</Menu.Label>
        <Menu.Item>Option</Menu.Item>
        <Menu.Item>Option</Menu.Item>
        <Menu.Item>Option</Menu.Item>
      </Menu.Group>
    </MenuButton>
  );

export default MenuButtonDefaultExample;

Chevron이 있는 레이블 트리거

import { MenuButton } from '@mildang/design-system/MenuButton';
import ChevronDown from '@mildang/icons/react/chevron-down';
import { Menu } from '@mildang/design-system/Menu';

const MenuButtonWithChevronExample = () => (
    <MenuButton label="정렬" variant="primary" endIcon={<ChevronDown />}>
      <Menu.Item>이름순</Menu.Item>
      <Menu.Item>날짜순</Menu.Item>
      <Menu.Item>인기순</Menu.Item>
    </MenuButton>
  );

export default MenuButtonWithChevronExample;

커스텀 트리거

import { MenuButton } from '@mildang/design-system/MenuButton';
import { IconButton } from '@mildang/design-system/IconButton';
import MoreHoriz from '@mildang/icons/react/more-horiz';
import { Menu } from '@mildang/design-system/Menu';

const MenuButtonCustomTriggerExample = () => (
    <MenuButton
      trigger={
        <IconButton variant="tertiary" aria-label="더보기 메뉴 열기">
          <MoreHoriz />
        </IconButton>
      }
    >
      <Menu.Item>복사</Menu.Item>
      <Menu.Item>수정</Menu.Item>
      <Menu.Item>삭제</Menu.Item>
    </MenuButton>
  );

export default MenuButtonCustomTriggerExample;

그룹과 레이블

import { MenuButton } from '@mildang/design-system/MenuButton';
import { Menu } from '@mildang/design-system/Menu';

const MenuButtonWithGroupsAndLabelExample = () => (
    <MenuButton label="파일" variant="primary">
      <Menu.Group>
        <Menu.Label>편집</Menu.Label>
        <Menu.Item>복사</Menu.Item>
        <Menu.Item>붙여넣기</Menu.Item>
      </Menu.Group>
      <Menu.Separator />
      <Menu.Group>
        <Menu.Label>관리</Menu.Label>
        <Menu.Item description="이 폴더로 이동">이동</Menu.Item>
        <Menu.Item>삭제</Menu.Item>
      </Menu.Group>
    </MenuButton>
  );

export default MenuButtonWithGroupsAndLabelExample;

FloatingButton 트리거

import { MenuButton } from '@mildang/design-system/MenuButton';
import { FloatingButton } from '@mildang/design-system/FloatingButton';
import Add from '@mildang/icons/react/add';
import { Menu } from '@mildang/design-system/Menu';

const MenuButtonWithFloatingButtonExample = () => (
    <MenuButton trigger={<FloatingButton icon={<Add />} aria-label="새 액션 메뉴 열기" />}>
      <Menu.Item>글쓰기</Menu.Item>
      <Menu.Item>이미지 업로드</Menu.Item>
      <Menu.Separator />
      <Menu.Item>파일 첨부</Menu.Item>
    </MenuButton>
  );

export default MenuButtonWithFloatingButtonExample;

비활성

import { MenuButton } from '@mildang/design-system/MenuButton';
import { Menu } from '@mildang/design-system/Menu';

const MenuButtonDisabledExample = () => (
    <MenuButton label="비활성" variant="primary" disabled>
      <Menu.Item>Option A</Menu.Item>
    </MenuButton>
  );

export default MenuButtonDisabledExample;

검색 가능한 메뉴

코드

import { MenuButton } from '@mildang/design-system/MenuButton';
import { Menu } from '@mildang/design-system/Menu';
import { forwardRef } from 'react';
import { cx } from '@mildang/styled-system/css';
import { menu } from '@mildang/styled-system/recipes';
import Search from '@mildang/icons/react/search';
import { useMemo, useState, type ReactNode } from 'react';

const MenuListEmptyState = (() => {
const menuStyles = menu();

/** @internal MenuList 내부 재료. 앱 코드는 MenuButton/Select/Combobox wrapper를 사용하세요. */
interface MenuListEmptyStateProps {
  children: ReactNode;
  className?: string;
}

/** @internal MenuList 내부 재료. 앱 코드는 MenuButton/Select/Combobox wrapper를 사용하세요. */
const MenuListEmptyState = forwardRef<HTMLDivElement, MenuListEmptyStateProps>(
  ({ children, className, ...rest }, ref) => {
    return (
      <div ref={ref} className={cx(menuStyles.emptyState, className)} {...rest}>
        {children}
      </div>
    );
  },
);
MenuListEmptyState.displayName = 'MenuListEmptyState';
return MenuListEmptyState;
})();

// SearchableStory 렌더 헬퍼 — MenuButton 은 controlled 상태를 위해 caller 가 useState 를 잡아야 하므로
// 재사용·가독성을 위해 wrapper 컴포넌트로 분리
const MenuButtonSearchableWrapper = ({
  items,
  children,
}: {
  items: string[];
  children: (filtered: string[]) => ReactNode;
}) => {
  const [search, setSearch] = useState('');
  const filtered = useMemo(
    () => items.filter((it) => it.toLowerCase().includes(search.toLowerCase())),
    [items, search],
  );
  return (
    <MenuButton
      label="메뉴 열기"
      variant="secondary"
      searchable
      searchPlaceholder="검색"
      onSearchChange={setSearch}
    >
      {children(filtered)}
    </MenuButton>
  );
};

const MenuButtonSearchableExample = () => {
    const items = ['옵션 1', '옵션 2', '옵션 3', '옵션 A', '옵션 B', '옵션 C'];
    return (
      <MenuButtonSearchableWrapper items={items}>
        {(filtered) =>
          filtered.length > 0 ? (
            filtered.map((label) => <Menu.Item key={label}>{label}</Menu.Item>)
          ) : (
            <MenuListEmptyState>
              <Search />
              검색 결과 없음
            </MenuListEmptyState>
          )
        }
      </MenuButtonSearchableWrapper>
    );
  };

export default MenuButtonSearchableExample;