# Tree 계층을 접고 펴며 그 안의 항목을 선택하는 목록입니다. > 원본: `03_ASSET_GUIDE/catalog/tree.js` · CSS: `03_ASSET_GUIDE/styles/elements/tree.css` > kind `component` · category `display` · status `stable` · specSource `measured` > [!NOTE] > **범위는 펼침/접기와 선택까지입니다** > 드래그로 항목의 순서나 부모를 바꾸는 동작은 이 문서의 범위 밖입니다. 마크업·클래스·상태도 > 그 전제로 정의되어 있으니, 재정렬이 필요하면 이 파일을 늘리기 전에 별도 항목으로 다루세요. > [!CAUTION] > **이 저장소에는 트리 로직이 없습니다** > `tree.css` 는 모양과 상태만 소유합니다. 화살표 키 이동, roving tabindex, 접힌 자식에 > `hidden` 을 붙이는 일, 부모의 `aria-checked="mixed"` 계산은 모두 구현하는 쪽이 붙이는 > 스크립트의 몫입니다. 이 문서는 그 스크립트가 무엇을 써야 하는지를 정합니다. > (구현 `implement/src/components/tree.tsx` 가 그 스크립트를 담당합니다.) --- ## 1. 구성 Tree 는 초점 하나를 받는 **Tree**, 한 줄인 **Item**, 그 안에서 실제로 칠해지는 **Row**, 그리고 자식을 묶는 **Group** 으로 구성됩니다. ``` ┌ Tree ─────────────────────────────────────┐ │ ┌ Item ─────────────────────────────────┐ │ │ │ Row: [›] [☐] [▭] Parent (2) │ │ │ │ ┌ Group ────────────────────────────┐ │ │ │ │ │ Item › Row: [ ] [☑] Child A │ │ │ │ │ │ Item › Row: [ ] [☐] Child B │ │ │ │ │ └───────────────────────────────────┘ │ │ │ └───────────────────────────────────────┘ │ └───────────────────────────────────────────┘ ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨(Group) ``` | # | 요소 | 필수 | 클래스 | 설명 | |---|---|---|---|---| | ① | Tree | 필수 | `atom-tree` | `role="tree"`. Tab 한 번을 받는 단위이고, 안쪽 이동은 화살표가 맡습니다. | | ② | Item | 필수 | `atom-tree__item` | `role="treeitem"`. 펼침·선택·비활성 같은 상태가 전부 여기에 붙습니다. | | ③ | Row | 필수 | `atom-tree__row` | 폭 전체를 쓰는 시각 행입니다. hover·초점·선택의 채움이 여기서 그려집니다. | | ④ | Caret | 필수 | `atom-tree__caret` · `atom-tree__caret--empty` | 펼침/접기 표시입니다. 자식이 없으면 자리만 남기고 글리프를 뺍니다. | | ⑤ | Checkbox | 선택 | `atom-tree__checkbox` (`__check` · `__dash`) | 다중 선택 모드에서만 나옵니다. 선택 사항입니다. | | ⑥ | Icon | 선택 | `atom-tree__icon` | 라벨 앞의 슬롯입니다. 선택 사항입니다. | | ⑦ | Label | 필수 | `atom-tree__label` | 항목 이름. 기본은 한 줄로 자릅니다. | | ⑧ | Badge | 선택 | `atom-tree__badge` | 라벨 뒤의 슬롯입니다. 선택 사항입니다. | | ⑨ | Group | 자식이 있을 때 | `atom-tree__group` | `role="group"`. 접히면 `hidden` 이 붙습니다. | > [!WARNING] > **깊이는 CSS 변수 값으로 받습니다** > 들여쓰기는 Item 에 인라인으로 들어오는 `--atom-tree-depth` 로 계산합니다. > `--depth_1` … `--depth_9` 같은 클래스를 두면 스타일시트에 정의된 단계보다 깊은 트리를 > 표현할 수 없습니다. --- ## 2. 종류 / 크기 ### 계층과 들여쓰기 Item 하나마다 `style="--atom-tree-depth: 2"` 처럼 자기 깊이를 받고, Row 는 그 값으로 왼쪽 패딩을 계산합니다. 한 단계는 20px 입니다. 들여쓰기는 **Row 의 패딩**으로 줍니다. Group 의 마진으로 주면 hover 와 선택의 채움도 함께 밀려, 행이 트리 폭 전체를 채우지 못하고 떨어진 상자 여러 개로 보입니다. 가이드라인(`--guides`)은 부모의 caret 중앙에서 내려오는 세로선이며, **Group 이 그립니다.** Row 마다 그리면 중간 형제가 펼쳐져 있을 때 그 서브트리 구간에서 선이 끊깁니다. 그 구간의 행은 더 깊은 단계라 얕은 단계의 선을 그리지 않기 때문입니다. | 대상 | 값 | 비고 | | --- | --- | --- | | 한 단계 들여쓰기 | 20px | `--atom-tree-indent` | | 행 높이 | 32px | `--atom-tree-row-h` | | 가이드라인 x 위치 | 부모 caret 중앙 | Group 의 `::before` | Row 의 왼쪽 패딩 = `{space.x2}` + `--atom-tree-depth` × `--atom-tree-indent`. 가이드라인 x = `{space.x2}` + (부모의) `--atom-tree-depth` × `--atom-tree-indent` + 10px. Group 은 부모 Item 의 깊이를 상속하고, 그 안의 Item 은 depth + 1 을 스스로 선언합니다. `aria-level` 은 `--atom-tree-depth` + 1 입니다. ### 펼침과 접기 Item 에 `data-state="expanded"` 또는 `"collapsed"` 를 주고, 같은 값을 `aria-expanded` 로도 알립니다. 펼쳐지면 caret 이 90도 돌아갑니다. | 상태 | 표시 | |---|---| | `data-state="expanded"` · `aria-expanded="true"` | caret 90도 회전 | | `data-state="collapsed"` | 자식 group 에 hidden. 화면에서만 감추지 않는다 | | leaf | `caret--empty` 로 자리만 남긴다. `aria-expanded` 는 붙이지 않는다 | 자식이 없는 항목(leaf)은 **caret 자리를 빈 칸으로 남깁니다.** 빼면 같은 단계의 leaf 만 18px 왼쪽으로 밀려 들여쓰기가 어긋납니다. leaf 에는 접을 자식이 없으므로 `aria-expanded` 를 붙이지 않습니다. > [!WARNING] > **접힌 자식은 화면에서만 감추지 않습니다** > Group 에 `hidden` 을 붙이거나 DOM 에서 빼세요. `opacity: 0` 이나 `height: 0` 으로 가리면 > 화면에는 없는 항목을 스크린리더가 계속 읽습니다. ### Selection 두 모드가 있습니다. 단일 선택은 `aria-selected="true"` 로 행 하나를 강조하고, 다중 선택은 행마다 체크박스를 두고 `aria-checked` 로 상태를 알립니다. | 모드 | Tree | Item | 표시 | | --- | --- | --- | --- | | 단일 | — | `aria-selected="true"` | 행 채움 + 왼쪽 2px 표시 + 굵기 | | 다중 | `aria-multiselectable="true"` + `--multi` | `aria-checked="true \| false \| mixed"` | 체크박스 | 다중 모드(`--multi`)에서는 행의 채움을 쓰지 않습니다. 선택 상태는 체크박스가 표시하고, 채움은 hover 와 초점 표시에 씁니다. `mixed` 는 체크·해제와 별개인 **세 번째 값**입니다. 자식 일부만 선택된 부모가 이 상태이고, 누르면 전부 선택으로 넘어갑니다. 체크박스는 [Checkbox](./checkbox.md) 의 작은 사이즈와 같은 모양이며, 프리뷰 카드가 스타일시트를 하나만 읽기 때문에 `tree.css` 안에서 다시 그립니다. ### Slots 라벨 앞에 아이콘, 뒤에 배지를 둘 수 있습니다. 둘 다 선택 사항이고, 어떤 조합이어도 라벨의 시작 위치는 같습니다. 배지는 개수처럼 **행을 열지 않고도 알 수 있어야 하는 정보**에만 씁니다. 접힌 폴더 안에 몇 개가 들어 있는지가 대표적입니다. ### Label 이름이 길면 **자릅니다.** 한 단계마다 쓸 수 있는 폭이 20px 씩 줄어들므로, 줄바꿈을 허용하면 깊은 단계에서 행 높이가 제각각이 됩니다. 전체 이름이 반드시 보여야 하는 화면이라면 `--wrap` 으로 바꿀 수 있지만, 기본은 자르기이고 잘린 라벨에는 `title` 로 전체 이름을 남깁니다. ### State hover · focus · 선택 · 비활성은 [3. 상태](#3-상태) 에 옮겼습니다. ### 클래스 계약 `styles/elements/tree.css` 헤더와 원본 `registry.json` `css.blocks` 기준입니다. Tree 는 modifier 축이 없고 flag 만 있습니다. 깊이는 클래스가 아니라 인라인 `--atom-tree-depth` 입니다. | Prop | 값 | 클래스 | |---|---|---| | `showGuides` | `false` · `true` | — · `atom-tree--guides` | | `selectionMode` | `none` · `single` · `multiple` | — · — · `atom-tree--multi` | | `wrapLabel` | `false` · `true` | — · `atom-tree--wrap` | | (leaf) | | `atom-tree__caret--empty` | | Part | 클래스 | |---|---| | Group | `atom-tree__group` | | Item | `atom-tree__item` | | Row | `atom-tree__row` | | Caret | `atom-tree__caret` | | Checkbox | `atom-tree__checkbox` · `atom-tree__check` · `atom-tree__dash` | | Icon · Label · Badge | `atom-tree__icon` · `atom-tree__label` · `atom-tree__badge` | 로컬 커스텀 프로퍼티: `--atom-tree-depth` (Item 인라인) · `--atom-tree-indent` (20px) · `--atom-tree-row-h` (32px) 상태 선택자: `:hover` · `:focus` · `:focus-visible` · `[hidden]` · `[data-state="expanded|collapsed"]` · `[aria-selected="true"]` · `[aria-checked="true|mixed"]` · `[aria-disabled="true"]` · `[data-disabled]` --- ## 3. 상태 | 상태 | 표시 | 어디에 | | --- | --- | --- | | hover | 약한 채움 | `.atom-tree__row:hover` | | focus-visible | 2px 브랜드 링 | `.atom-tree__item:focus-visible > .atom-tree__row` | | 선택됨 | 채움 + 왼쪽 2px 표시 + 굵기 | `[aria-selected="true"]` | | 펼침 | caret 90도 회전 | `[data-state="expanded"]` | | 비활성 | 글자 흐림 + `not-allowed` | `[aria-disabled="true"]` | | 전체 비활성 | 포인터 차단 | Tree 의 `data-disabled` | **hover 는 채움, 초점은 링으로 표시합니다.** 트리는 화살표 키로 이동하므로, 포인터가 다른 곳에 있어도 키보드 위치가 구분되어야 합니다. | 예 | 설명 | |---|---| | 기본 | 아무 상태도 없다 | | hover | 채움만 바뀐다 | | focus-visible | Tab 으로 들어오면 링이 생긴다 | | `aria-selected="true"` | 채움 + 왼쪽 표시 + 굵기 | | `aria-disabled="true"` | 초점은 받되 실행되지 않는다 | --- ## 4. Props | Prop | Type | Default | 필수 | 설명 | |---|---|---|---|---| | `items` | `TreeNode[]` | | ● | 트리 데이터입니다. 각 노드가 `children` 을 가지면 부모가 됩니다. | | `selectionMode` | `"none" \| "single" \| "multiple"` | `"single"` | | `multiple` 이면 행에 체크박스가 붙습니다. | | `selectedKeys` | `string[]` | | | 선택된 노드입니다. `single` 이면 길이 1로 씁니다. | | `expandedKeys` | `string[]` | | | 펼쳐진 노드입니다. 여기 없는 부모는 접힙니다. | | `defaultExpandedKeys` | `string[]` | `[]` | | 처음 펼쳐 둘 노드입니다. | | `disabledKeys` | `string[]` | `[]` | | 선택되지 않는 노드입니다. 초점은 받습니다. | | `checkStrictly` | `boolean` | `false` | | `true` 면 부모와 자식의 체크를 연동하지 않습니다. `false` 일 때만 `mixed` 가 생깁니다. | | `showGuides` | `boolean` | `false` | | 들여쓰기 가이드라인을 켭니다. 깊이 3 이상에서 권장합니다. | | `wrapLabel` | `boolean` | `false` | | 긴 이름을 자르지 않고 줄바꿈합니다. | | `disabled` | `boolean` | `false` | | 트리 전체를 잠급니다. | | `onSelectionChange` | `(keys) => void` | | | 선택이 바뀔 때 호출됩니다. | | `onExpandedChange` | `(keys) => void` | | | 펼침이 바뀔 때 호출됩니다. | `TreeNode` (구현이 정한 모양 — 원본은 "`children` 을 가지면 부모" 만 정합니다) | 필드 | Type | 필수 | 설명 | |---|---|---|---| | `key` | `string` | ● | 노드 식별자. `selectedKeys` · `expandedKeys` · `disabledKeys` 가 이 값을 씁니다. | | `label` | `ReactNode` | ● | 라벨 (`atom-tree__label`) | | `children` | `TreeNode[]` | | 있으면 부모가 됩니다. | | `icon` | `ReactNode` | | 라벨 앞 슬롯 (`atom-tree__icon`) | | `badge` | `ReactNode` | | 라벨 뒤 슬롯 (`atom-tree__badge`) | | `title` | `string` | | 잘린 라벨의 전체 이름. 생략하면 문자열 라벨을 그대로 씁니다. | ### Parts (compound) | Part | 요소 | 설명 | |---|---|---| | `Tree` | `