TreeSelect

TreeSelect

계층적 트리 데이터에서 값을 선택하는 select 컴포넌트예요. 단일, 다중, 체크박스(부모-자식 계단식) 세 가지 선택 모드를 지원해요.

출처: 문서

본문

사용법 (Usage)

TreeSelect는 계층적 트리 데이터에서 하나 이상의 값을 선택할 수 있게 해요. 단일(single), 다중(multiple), 체크박스(checkbox, 부모-자식 계단식) 세 가지 선택 모드를 지원해요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Your favorite item" data={data} />;
}

Data prop

data prop에 전달된 데이터는 Tree 컴포넌트와 같은 규칙을 따라야 해요.

  • 데이터는 TreeNodeData 객체의 배열이어야 해요
  • 각 노드는 고유한 value와 label 키를 가져야 해요
  • 각 노드는 자식 노드 배열이 있는 children 키를 가질 수 있어요
import { TreeNodeData } from '@mantine/core';

const data: TreeNodeData[] = [
  {
    value: 'fruits',
    label: 'Fruits',
    children: [
      { value: 'apple', label: 'Apple' },
      { value: 'banana', label: 'Banana' },
    ],
  },
  { value: 'milk', label: 'Milk' },
];

선택 모드 (Selection modes)

TreeSelect는 mode prop으로 제어되는 세 가지 선택 모드를 지원해요.

  • single (기본) – 단일 값 선택, 입력으로 렌더링돼요
  • multiple – 다중 값 선택, 필(pills)로 렌더링돼요
  • checkbox – 부모-자식 계단식 체크박스 선택, 필로 렌더링돼요

다중 모드 (Multiple mode)

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Your favorite items" data={data} mode="multiple" />;
}

체크박스 모드 (Checkbox mode)

체크박스 모드에서 부모 노드를 체크하면 모든 자식이 자동으로 체크돼요. 부모를 해제하면 모든 자식이 해제돼요. 일부 자식만 체크되면 부모는 불확정(indeterminate) 상태를 보여줘요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Select categories" data={data} mode="checkbox" />;
}

제어 컴포넌트 (Controlled)

import { useState } from 'react';
import { TreeSelect } from '@mantine/core';

// Single mode
function SingleDemo() {
  const [value, setValue] = useState<string | null>(null);
  return <TreeSelect data={[]} value={value} onChange={setValue} />;
}

// Multiple or checkbox mode
function MultipleDemo() {
  const [value, setValue] = useState<string[]>([]);
  return <TreeSelect data={[]} mode="multiple" value={value} onChange={setValue} />;
}

검색 가능 (Searchable)

searchable prop을 설정하면 사용자 입력으로 옵션을 필터링할 수 있어요. 검색할 때 매칭되는 노드와 그 조상들이 표시돼요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Your favorite item" data={data} searchable />;
}

아무것도 찾지 못함 (Nothing found)

nothingFoundMessage prop을 설정해 검색어에 일치하는 옵션이 없거나 데이터가 없을 때 주어진 메시지를 표시해요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Your favorite item" data={data} searchable nothingFoundMessage="Nothing found" />;
}

지우기 가능 (Clearable)

clearable prop을 설정하면 오른쪽 섹션에 지우기 버튼을 표시해요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Your favorite item" data={data} clearable />;
}

클릭 시 확장 (Expand on click)

expandOnClick prop을 설정하면 부모 노드를 클릭할 때도 (셰브론 외에) 확장을 토글할 수 있어요. 동작은 선택 모드에 따라 달라져요.

  • single과 multiple – 부모를 클릭하면 확장/축소만 돼요. 잎(leaf) 노드만 선택할 수 있어요
  • checkbox – 부모를 클릭하면 체크 상태를 토글하면서 동시에 확장해요
import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Your favorite item" data={data} expandOnClick />;
}

연결선 (Connecting lines)

TreeSelect는 기본적으로 부모와 자식 노드 사이에 연결선을 렌더링해요. withLines={false}로 설정하면 비활성화할 수 있어요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Without connecting lines" data={data} withLines={false} />;
}

엄격한 체크 (Check strictly)

checkStrictly를 설정하면 체크박스 모드에서 부모-자식 계단식을 비활성화해요. 각 노드의 체크 상태가 완전히 독립적이 돼요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Select items" data={data} mode="checkbox" checkStrictly />;
}

체크 전략 (Checked strategy)

checkedStrategy prop은 체크박스 모드에서 어떤 체크된 노드가 값과 필에 나타나는지 제어해요.

  • child (기본) – 잎 노드만 값에 나타나요
  • all – 모든 체크된 노드(부모와 자식)가 값에 나타나요
  • parent – 완전히 체크된 최상위 부모만 값에 나타나요
import { useState } from 'react';
import { Stack, TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  const [childValue, setChildValue] = useState<string[]>([]);
  const [allValue, setAllValue] = useState<string[]>([]);
  const [parentValue, setParentValue] = useState<string[]>([]);

  return (
    <Stack>
      <TreeSelect label="checkedStrategy: child (default)" data={data} mode="checkbox" value={childValue} onChange={setChildValue} />
      <TreeSelect label="checkedStrategy: all" data={data} mode="checkbox" checkedStrategy="all" value={allValue} onChange={setAllValue} />
      <TreeSelect label="checkedStrategy: parent" data={data} mode="checkbox" checkedStrategy="parent" value={parentValue} onChange={setParentValue} />
    </Stack>
  );
}

최대 값 (Max values)

maxValues prop을 설정하면 다중 및 체크박스 모드에서 선택할 수 있는 값의 개수를 제한할 수 있어요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Pick up to 3 items" data={data} mode="multiple" maxValues={3} />;
}

renderNode

renderNode 콜백으로 드롭다운에서 노드 렌더링을 커스터마이즈할 수 있어요. node, level, expanded, hasChildren, selected, checked, indeterminate, expand 속성을 포함하는 객체로 호출돼요.

import { FileTextIcon, FolderOpenIcon, FolderSimpleIcon } from '@phosphor-icons/react';
import { Group, Text, TreeSelect, TreeSelectProps } from '@mantine/core';
import { data } from './data';

const renderTreeNode: TreeSelectProps['renderNode'] = ({ node, hasChildren, expanded }) => (
  <Group gap={5}>
    {hasChildren ? (
      expanded ? <FolderOpenIcon size={16} /> : <FolderSimpleIcon size={16} />
    ) : (
      <FileTextIcon size={16} />
    )}
    <span>{node.label}</span>
  </Group>
);

function Demo() {
  return <TreeSelect label="Your favorite item" data={data} renderNode={renderTreeNode} />;
}

페이로드에는 노드의 확장 상태를 토글하는 expand 함수도 포함돼요. 확장 상태를 외부에서 제어하지 않고 renderNode 안에서 커스텀 확장 컨트롤을 만들 때 사용해요. 확장이 커스텀 컨트롤로만 일어나게 하려면 expandOnClick={false}를 설정해요.

import { CaretRightIcon } from '@phosphor-icons/react';
import { Group, TreeSelect, TreeSelectProps } from '@mantine/core';
import { data } from './data';

const renderTreeNode: TreeSelectProps['renderNode'] = ({ node, hasChildren, expanded, expand }) => (
  <Group gap={5}>
    {hasChildren ? (
      <CaretRightIcon size={16} onClick={expand} style={{ transform: expanded ? 'rotate(90deg)' : 'none' }} />
    ) : (
      <span />
    )}
    <span>{node.label}</span>
  </Group>
);

스크롤 가능한 드롭다운 (Scrollable dropdown)

기본적으로 옵션 목록은 ScrollArea.Autosize로 감싸져요. maxDropdownHeight prop으로 드롭다운 최대 높이를 제어할 수 있어요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Your favorite item" data={data} maxDropdownHeight={200} />;
}

Combobox props

Combobox props를 comboboxProps로 덮어쓸 수 있어요. TreeSelect가 노출하지 않는 일부 props(예: withinPortal)를 변경해야 할 때 유용해요.

import { TreeSelect } from '@mantine/core';

function Demo() {
  return <TreeSelect data={[]} comboboxProps={{ withinPortal: false }} />;
}

드롭다운 z-index 변경 (Change dropdown z-index)

import { TreeSelect } from '@mantine/core';

function Demo() {
  return <TreeSelect data={[]} comboboxProps={{ zIndex: 1000 }} />;
}

드롭다운 열림 상태 제어 (Control dropdown opened state)

dropdownOpened prop으로 드롭다운 열림 상태를 제어할 수 있어요. 또한 onDropdownClose와 onDropdownOpen으로 드롭다운 열림 상태 변경을 들을 수 있어요.

import { TreeSelect, Button } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { data } from './data';

function Demo() {
  const [dropdownOpened, { toggle }] = useDisclosure();
  return (
    <>
      <Button onClick={toggle}>Toggle dropdown</Button>
      <TreeSelect label="Your favorite item" data={data} dropdownOpened={dropdownOpened} />
    </>
  );
}

드롭다운 위치 (Dropdown position)

기본적으로 드롭다운은 공간이 충분하면 입력 아래에 표시되고, 그렇지 않으면 입력 위에 표시돼요. position과 middlewares props를 설정해 이 동작을 바꿀 수 있어요. 이 props는 기반 Popover 컴포넌트로 전달돼요.

항상 입력 위에 표시되는 드롭다운 예시:

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Your favorite item" data={data} position="top" />;
}

드롭다운 너비 (Dropdown width)

드롭다운 너비를 바꾸려면 comboboxProps에서 width prop을 설정해요. 기본적으로 드롭다운 너비는 입력 너비와 같아요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Your favorite item" data={data} comboboxProps={{ width: 400 }} />;
}

드롭다운 오프셋 (Dropdown offset)

드롭다운 오프셋을 바꾸려면 comboboxProps에서 offset prop을 설정해요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';
import classes from './Demo.module.css';

function Demo() {
  return <TreeSelect label="Your favorite item" data={data} comboboxProps={{ offset: 20 }} />;
}

드롭다운 애니메이션 (Dropdown animation)

기본적으로 드롭다운 애니메이션은 비활성화돼 있어요. transitionProps를 설정해 활성화할 수 있으며, 이 props는 기반 Transition 컴포넌트로 전달돼요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <TreeSelect
      label="Your favorite item"
      data={data}
      transitionProps={{ transition: 'pop', duration: 200 }}
    />
  );
}

드롭다운 패딩 (Dropdown padding)

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <>
      <TreeSelect label="Zero padding" data={data} comboboxProps={{ dropdownPadding: 0 }} />
      <TreeSelect label="10px padding" data={data} comboboxProps={{ dropdownPadding: 10 }} />
    </>
  );
}

드롭다운 그림자 (Dropdown shadow)

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Your favorite item" data={data} comboboxProps={{ shadow: 'md' }} />;
}

키보드 내비게이션 (Keyboard navigation)

TreeSelect는 드롭다운이 열려 있을 때 다음 키보드 상호작용을 지원해요.

  • ArrowRight – 강조된 부모 노드를 확장해요
  • ArrowLeft – 강조된 부모 노드를 축소하거나 그 부모로 이동해요
  • ArrowUp / ArrowDown – 옵션 사이를 이동해요
  • Enter – 강조된 옵션을 선택해요

확장 상태 (Expand state)

노드의 확장 상태를 제어할 수 있어요.

import { TreeSelect } from '@mantine/core';

// 기본적으로 특정 노드 확장
// function Demo() { return <TreeSelect data={[]} defaultExpanded={['fruits']} />; }

// 기본적으로 모든 노드 확장
// function Demo() { return <TreeSelect data={[]} defaultExpanded={['__all__']} />; }

// 제어된 확장 상태
// function Demo() {
//   const [expanded, setExpanded] = useState<string[]>([]);
//   return <TreeSelect data={[]} expanded={expanded} onExpandedChange={setExpanded} />;
// }

왼쪽·오른쪽 섹션 (Left and right sections)

TreeSelect는 leftSection과 rightSection props를 지원해요. 이 섹션들은 입력 래퍼 안에 절대 위치로 렌더링돼요. 아이콘, 입력 컨트롤, 기타 요소를 표시하는 데 사용할 수 있어요.

  • rightSection / leftSection – 입력의 해당 측면에 렌더링할 React 노드
  • rightSectionWidth / leftSectionWidth – 오른쪽 섹션의 너비와 입력 해당 측면의 패딩을 제어해요. 기본적으로 컴포넌트 size prop으로 제어돼요.
  • rightSectionPointerEvents / leftSectionPointerEvents – 섹션의 pointer-events 속성을 제어해요. 비상호작용 요소를 렌더링하려면 none으로 설정해 클릭이 입력으로 전달되게 해요.
import { TreeSelect } from '@mantine/core';
import { SquaresFourIcon } from '@phosphor-icons/react';
import { data } from './data';

function Demo() {
  const icon = <SquaresFourIcon size={16} />;
  return (
    <>
      <TreeSelect data={data} label="Your favorite item" leftSection={icon} />
      <TreeSelect data={data} label="Your favorite item" rightSection={icon} />
    </>
  );
}

Input props

TreeSelect 컴포넌트는 Input과 Input.Wrapper 컴포넌트의 기능과 모든 input 요소 props를 지원해요. TreeSelect 문서에는 컴포넌트가 지원하는 모든 기능이 포함되어 있지 않아요. 사용 가능한 모든 기능은 Input 문서에서 확인할 수 있어요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <TreeSelect
      label="Input label"
      description="Input description"
      placeholder="Pick value"
      data={data}
    />
  );
}

읽기 전용 (Read only)

readOnly를 설정하면 입력을 읽기 전용으로 만들어요. readOnly가 설정되면 TreeSelect는 제안을 표시하지 않고 onChange 함수를 호출하지 않아요.

비활성 (Disabled)

disabled를 설정하면 입력을 비활성화해요. disabled가 설정되면 사용자가 입력과 상호작용할 수 없고 TreeSelect는 제안을 표시하지 않아요.

오류 상태 (Error state)

불리언 오류 또는 오류 메시지와 함께 TreeSelect를 표시할 수 있어요.

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <>
      <TreeSelect label="Boolean error" data={data} error />
      <TreeSelect label="With error message" data={data} error="Invalid value" />
    </>
  );
}

성공 상태 (Success state)

import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return <TreeSelect label="Tree Select" description="Looks good!" data={data} />;
}

요소 ref 가져오기 (Get element ref)

import { useRef } from 'react';
import { TreeSelect } from '@mantine/core';

function Demo() {
  const ref = useRef<HTMLInputElement>(null);
  return <TreeSelect data={[]} ref={ref} />;
}

접근성 (Accessibility)

TreeSelect를 label prop 없이 사용하면 화면 판독기가 제대로 알려주지 못해요. aria-label을 설정하면 라벨이 보이지 않아도 화면 판독기가 알려줘요. label prop을 설정하면 별도로 aria-label을 지정할 필요 없이 접근성이 확보돼요.

지우기 버튼에 aria-label을 설정하려면 clearButtonProps를 사용해요. 이는 clearable이 설정된 경우에만 필요하다는 점에 주의해요.

import { TreeSelect } from '@mantine/core';

function Demo() {
  return <TreeSelect data={[]} clearable clearButtonProps={{ 'aria-label': 'Clear selection' }} />;
}

더 알아보기 (Learn more)