PlTreeSelect
리스트가 아니라 계층에서 고르는 값입니다. field 뒤에 놓인 PlTree이고, 카테고리·폴더·지역·조직도 node처럼 평평한 리스트가 뭉개 버리는 모양을 위한 것입니다.
import { PlTreeSelect, type PlTreeSelectNode } from 'plass-ui';
const items: PlTreeSelectNode[] = [
{
id: 'europe',
label: 'Europe',
children: [{ id: 'france', label: 'France' }]
},
{ id: 'antarctica', label: 'Antarctica' }
];
<PlTreeSelect items={items} label="Region" placeholder="Pick a region" />;import 'package:plass_ui/plass_ui.dart';
const List<PlTreeSelectNode> items = <PlTreeSelectNode>[
PlTreeSelectNode(
id: 'europe',
label: 'Europe',
children: <PlTreeSelectNode>[PlTreeSelectNode(id: 'france', label: 'France')],
),
PlTreeSelectNode(id: 'antarctica', label: 'Antarctica'),
];
PlTreeSelect(
items: items,
label: const Text('Region'),
value: chosen,
onValueChanged: (Set<String> next) => setState(() => chosen = next),
);팝업은 트리 밖으로 스스로 떠오르므로 위에 Overlay가 필요합니다. navigator가 있는 WidgetsApp과 MaterialApp이 모두 제공합니다.
Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| items * | readonly PlTreeSelectNode[] | — | 트리 전체를 데이터로 |
| value | readonly string[] | — | 고른 node의 id들. 제어하려면 onValueChange와 함께 |
| defaultValue | readonly string[] | — | 처음부터 골라져 있을 것 |
| onValueChange | (value: string[]) => void | — | 고른 것이 바뀌었을 때 |
| multiple | boolean | false | 한 번에 둘 이상을 쥘 수 있는지 |
| selectableBranches | boolean | false | 자식이 있는 node도 고를 수 있는지. node의 selectable이 어느 쪽으로든 덮어씁니다 |
| expanded | readonly string[] | — | 열려 있는 가지의 id들. 제어하려면 onExpandedChange와 함께 |
| defaultExpanded | readonly string[] | — | 처음부터 열려 있을 가지들 |
| onExpandedChange | (expanded: string[]) => void | — | 가지가 열리거나 닫혔을 때 |
| open | boolean | — | 팝업이 열려 있는지. onOpenChange와 함께 controlled로 씁니다 |
| defaultOpen | boolean | false | 팝업이 열린 채로 시작할지 |
| onOpenChange | (open: boolean) => void | — | 팝업이 열리고 닫힐 때 호출됩니다 |
| placeholder | ReactNode | — | 아무것도 고르지 않았을 때 trigger에 보이는 내용 |
| clearable | boolean | false | 값을 비우는 ×를 보여 줍니다 |
| closeOnSelect | boolean | !multiple | node를 고르자마자 팝업을 닫을지 |
| searchable | boolean | false | 트리 위에 걸러 내는 field를 둡니다. 맞은 node는 조상을 데리고 남습니다 |
| searchLabel | string | 'Search' | 거르는 field에 적히는 말 |
| emptyLabel | string | 'Nothing here' | 아무것도 걸리지 않았을 때 팝업이 하는 말 |
| format | (chosen: PlTreeSelectNode[]) => ReactNode | — | trigger가 쥔 것을 쓰는 방식. 기본은 label을 쉼표로 이은 것 |
| name | string | — | 폼 제출에서 field를 식별합니다. 값 하나당 hidden input 하나 |
| variant공통 | 'solid' | 'glass' | 'ghost' | 'glass' | trigger의 재질. PlTextField와 같은 껍데기를 씁니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 높이와 타입 스케일 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | 'default' | 'compact' | 'default' | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | 0 | 1 | 2 | 3 | 0 | trigger의 그림자 깊이. 팝업은 3으로 고정입니다 |
| label | ReactNode | — | trigger 위 라벨 |
| description | ReactNode | — | trigger 아래 보조 설명 |
| error | ReactNode | — | 오류 메시지. 존재 자체가 invalid 상태를 만듭니다 |
| invalid | boolean | — | 메시지 없이 invalid로 만듭니다 |
| startIcon | ReactNode | — | 값 앞의 글리프 |
| fullWidth | boolean | false | 컨테이너 너비만큼 확장 |
| readOnly | boolean | false | 값은 보이지만 바꿀 수 없고, 팝업도 열리지 않습니다 |
| disabled | boolean | false | 사용 불가 |
| required | boolean | false | 폼 제출 전에 값이 있어야 하는지 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| items * | List<PlTreeSelectNode> | — | 트리 전체를 데이터로 |
| value | Set<String> | {} | 고른 node의 id들. controlled입니다 |
| onValueChanged | ValueChanged<Set<String>>? | — | node를 고르거나 뺀 뒤 선택 집합 전체와 함께 호출됩니다 |
| multiple | bool | false | 한 번에 둘 이상을 쥘 수 있는지 |
| selectableBranches | bool | false | 자식이 있는 node도 고를 수 있는지. node의 selectable이 어느 쪽으로든 덮어씁니다 |
| expanded | Set<String>? | — | 열려 있는 가지의 id들. 두지 않으면 picker가 스스로 쥡니다 |
| onExpandedChanged | ValueChanged<Set<String>>? | — | 가지가 열리거나 닫힌 뒤 열린 집합 전체와 함께 호출됩니다 |
| open | bool? | — | 팝업이 떠 있는지. 두지 않으면 picker가 스스로 쥡니다 |
| onOpenChanged | ValueChanged<bool>? | — | 팝업이 열리고 닫힐 때 호출됩니다 |
| placeholder | Widget? | — | 아무것도 고르지 않았을 때 trigger에 보이는 내용 |
| clearable | bool | false | 값을 비우는 ×를 보여 줍니다 |
| closeOnSelect | bool? | !multiple | node를 고르자마자 팝업을 닫을지 |
| searchable | bool | false | 트리 위에 걸러 내는 field를 둡니다. 맞은 node는 조상을 데리고 남습니다 |
| searchLabel | String? | 'Search' | 거르는 field에 적히는 말 |
| emptyLabel | String? | 'Nothing here' | 아무것도 걸리지 않았을 때 팝업이 하는 말 |
| format | String Function(List<PlTreeSelectNode>)? | — | trigger가 쥔 것을 쓰는 방식. 기본은 label을 쉼표로 이은 것 |
| variant공통 | PlassVariant | PlassVariant.glass | trigger의 재질. PlTextField와 같은 껍데기를 씁니다 |
| size공통 | PlassSize | PlassSize.md | 높이와 타입 스케일 |
| color공통 | PlassColor | PlassColor.primary | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | PlassDensity | PlassDensity.standard | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | int | 0 | trigger의 그림자 깊이. 팝업은 3으로 고정입니다 |
| label | Widget? | — | trigger 위 라벨 |
| description | Widget? | — | trigger 아래 보조 설명 |
| error | Widget? | — | 오류 메시지. 존재 자체가 invalid 상태를 만듭니다 |
| invalid | bool? | — | 메시지 없이 invalid로 만듭니다 |
| startIcon | Widget? | — | 값 앞의 글리프 |
| fullWidth | bool | false | 컨테이너 너비만큼 확장 |
| readOnly | bool | false | 값은 보이지만 바꿀 수 없고, 팝업도 열리지 않습니다 |
| disabled | bool | false | 사용 불가 |
| semanticLabel | String? | — | 스크린 리더가 trigger에 주는 이름 |
| focusNode | FocusNode? | — | 바깥에서 focus를 다룹니다 |
| autofocus | bool | false | 트리에 들어가면서 focus를 가져갑니다 |
PlTreeSelectNode
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| id * | string | — | 트리 전체에서 유일한 식별자 |
| label * | ReactNode | — | 행이 말하는 것 |
| searchLabel | string | — | 검색이 대조하는 문자열. label이 문자열이면 그것으로, 아니면 id로 떨어집니다 |
| icon | ReactNode | — | label 앞의 글리프 |
| children | readonly PlTreeSelectNode[] | — | 자식들. 빈 배열은 아무것도 없는 **가지**이고, undefined는 **잎**입니다 |
| selectable | boolean | — | 이 node 자체를 고를 수 있는지. 잎은 true, 가지는 selectableBranches를 따릅니다 |
| disabled | boolean | — | 트리에는 있지만 고를 수 없고, 화살표 키의 정거장도 아닙니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| id * | String | — | 트리 전체에서 유일한 식별자 |
| label * | String | — | 행이 말하는 것이자, 필터가 대조하는 것이자, trigger가 쓰는 것. React와 달리 위젯이 아니라 문자열입니다 |
| icon | Widget? | — | label 앞의 글리프 |
| children | List<PlTreeSelectNode>? | — | 자식들. 빈 리스트는 아무것도 없는 **가지**이고, null은 **잎**입니다 |
| selectable | bool? | — | 이 node 자체를 고를 수 있는지. 잎은 true, 가지는 selectableBranches를 따릅니다 |
| disabled | bool | false | 트리에는 있지만 고를 수 없고, 화살표 키의 정거장도 아닙니다 |
native <div> 속성은 field 래퍼로 그대로 전달됩니다. color는 위 표의 color와 충돌해서, defaultValue는 picker가 DOM 속성이 아니라 id 목록으로 쓰기 때문에, children은 트리가 items이기 때문에 제외됩니다.
className은 label과 control, 그 아래 두 줄을 담은 스택에 붙습니다. classNames는 그 안의 네 부분(label, control, description, error)에 닿습니다.
value는 **Set<String>**이고 controlled입니다. uncontrolled 형태는 없으며, 이 패키지의 모든 입력이 그렇습니다. expanded와 open만 예외로, 넘기지 않으면 picker가 직접 쥡니다.
node의 label은 여기서 **String**이고 React에서는 ReactNode입니다. PlTransferItem이 이미 지고 있는 차이이고 이유도 같습니다. 필터가 label을 읽고, trigger가 그것을 쓰고, 스크린 리더가 그것을 받습니다. 텍스트라야 모든 node가 만들어질 때부터 검색 가능합니다. 이쪽에 searchLabel이 없는 것도 같은 이유입니다. label이 이미 그 말입니다.
라이브러리 전체에서 공유 축이 뜻하는 바는 prop 규약에 있습니다.
Examples
searchable
트리 위에 걸러 내는 field를 둡니다. 맞은 node는 조상을 데리고 남습니다. 아무것도 위에 없는 "Seoul"은 어느 분류에서 나온 것인지 말해 주지 않기 때문입니다. 그리고 필터가 남긴 가지는 전부 열립니다. 닫힌 부모 안에 접힌 match는 아무에게도 보여 주지 않은 match입니다.
field를 비우면 접힘은 다시 읽는 사람의 것이 됩니다. 직접 열어 둔 가지는 그대로 열려 있고, 필터가 연 가지는 다시 닫힙니다.
대조는 악센트와 대소문자를 함께 접습니다. jose가 José를 찾습니다.
대조는 대소문자만 접습니다. 악센트는 벗기지 않는데, Dart 코어에 String.normalize가 없고 이 패키지는 의존성을 두지 않기 때문입니다. React 쪽은 악센트까지 접습니다.
selectableBranches
기본은 꺼져 있고, 이런 트리는 대개 그런 모양입니다. 가지는 분류이고 잎이 답입니다. 고를 수 없는 가지도 여닫히기는 합니다. 그것을 누르는 것이 아래에 있는 것에 닿는 방법이기 때문입니다.
node 자신의 selectable이 어느 쪽으로든 덮어쓰므로, 진짜 카테고리인 "Home"은 고를 수 있게 두고 나머지 가지는 길로 남길 수 있습니다.
multiple
누를 때마다 더해지고, trigger는 쉼표로 이어서 씁니다. 팝업은 열린 채로 있습니다. 여러 답 중 첫 번째에서 닫히는 picker는 나머지마다 다시 열어야 하기 때문입니다.
format은 고른 node들을 받아 원하는 대로 씁니다. 답과 함께 넓어지지 않는 trigger를 만들 때 씁니다.
<PlTreeSelect items={items} multiple format={(chosen) => chosen.length + ' regions'} />Controlled
값, 접힘, 팝업은 각각 다른 질문이고 각자 짝이 있습니다.
<PlTreeSelect
items={items}
value={chosen}
onValueChange={setChosen}
expanded={open}
onExpandedChange={setOpen}
/>폴더를 여는 것은 그것을 고르는 것이 아닙니다. 두 번째 짝이 따로 있는 이유입니다.
In a form
name을 주면 쥔 id 하나당 <input type="hidden"> 하나가 놓이므로, multiple picker는 반복 field로 제출됩니다.
<PlTreeSelect items={items} multiple name="region" defaultValue={['france', 'spain']} />Accessibility
- trigger는 다른 모든 picker와 똑같이 button이고, label과 description, error,
aria-invalid를 함께 답니다. - 팝업 안은 진짜
PlTree입니다:role="tree"와role="treeitem",aria-level,aria-expanded,aria-selected, 그리고 트리 전체에 tab 정거장 하나. - ↓와 ↑는 실제로 보이는 행을 걷고, →는 가지를 연 뒤 안으로 들어가고, ←는 닫거나 부모로 나가고, Enter나 Space가 고릅니다.
- 가지라서 고를 수 없을 뿐인 node에는
aria-disabled를 붙이지 않습니다. 아래 있는 것을 여는 조작 가능한 행이기 때문입니다.disablednode는 표시되고, 화살표 키의 정거장도 아닙니다. - 거르는 field는
searchLabel로 스스로 이름을 붙이므로, 위에 보이는 label 없이도 읽힙니다.