본문으로 건너뛰기

PlTreeSelect

리스트가 아니라 계층에서 고르는 값입니다. field 뒤에 놓인 PlTree이고, 카테고리·폴더·지역·조직도 node처럼 평평한 리스트가 뭉개 버리는 모양을 위한 것입니다.

React
tsx
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" />;
dart
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가 있는 WidgetsAppMaterialApp이 모두 제공합니다.

Props

Prop타입기본값설명
items * readonly PlTreeSelectNode[]트리 전체를 데이터로
valuereadonly string[]고른 node의 id들. 제어하려면 onValueChange와 함께
defaultValuereadonly string[]처음부터 골라져 있을 것
onValueChange(value: string[]) => void고른 것이 바뀌었을 때
multiplebooleanfalse한 번에 둘 이상을 쥘 수 있는지
selectableBranchesbooleanfalse자식이 있는 node도 고를 수 있는지. node의 selectable이 어느 쪽으로든 덮어씁니다
expandedreadonly string[]열려 있는 가지의 id들. 제어하려면 onExpandedChange와 함께
defaultExpandedreadonly string[]처음부터 열려 있을 가지들
onExpandedChange(expanded: string[]) => void가지가 열리거나 닫혔을 때
openboolean팝업이 열려 있는지. onOpenChange와 함께 controlled로 씁니다
defaultOpenbooleanfalse팝업이 열린 채로 시작할지
onOpenChange(open: boolean) => void팝업이 열리고 닫힐 때 호출됩니다
placeholderReactNode아무것도 고르지 않았을 때 trigger에 보이는 내용
clearablebooleanfalse값을 비우는 ×를 보여 줍니다
closeOnSelectboolean!multiplenode를 고르자마자 팝업을 닫을지
searchablebooleanfalse트리 위에 걸러 내는 field를 둡니다. 맞은 node는 조상을 데리고 남습니다
searchLabelstring'Search'거르는 field에 적히는 말
emptyLabelstring'Nothing here'아무것도 걸리지 않았을 때 팝업이 하는 말
format(chosen: PlTreeSelectNode[]) => ReactNodetrigger가 쥔 것을 쓰는 방식. 기본은 label을 쉼표로 이은 것
namestring폼 제출에서 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 | 30trigger의 그림자 깊이. 팝업은 3으로 고정입니다
labelReactNodetrigger 위 라벨
descriptionReactNodetrigger 아래 보조 설명
errorReactNode오류 메시지. 존재 자체가 invalid 상태를 만듭니다
invalidboolean메시지 없이 invalid로 만듭니다
startIconReactNode값 앞의 글리프
fullWidthbooleanfalse컨테이너 너비만큼 확장
readOnlybooleanfalse값은 보이지만 바꿀 수 없고, 팝업도 열리지 않습니다
disabledbooleanfalse사용 불가
requiredbooleanfalse폼 제출 전에 값이 있어야 하는지
Prop타입기본값설명
items * List<PlTreeSelectNode>트리 전체를 데이터로
valueSet<String>{}고른 node의 id들. controlled입니다
onValueChangedValueChanged<Set<String>>?node를 고르거나 뺀 뒤 선택 집합 전체와 함께 호출됩니다
multipleboolfalse한 번에 둘 이상을 쥘 수 있는지
selectableBranchesboolfalse자식이 있는 node도 고를 수 있는지. node의 selectable이 어느 쪽으로든 덮어씁니다
expandedSet<String>?열려 있는 가지의 id들. 두지 않으면 picker가 스스로 쥡니다
onExpandedChangedValueChanged<Set<String>>?가지가 열리거나 닫힌 뒤 열린 집합 전체와 함께 호출됩니다
openbool?팝업이 떠 있는지. 두지 않으면 picker가 스스로 쥡니다
onOpenChangedValueChanged<bool>?팝업이 열리고 닫힐 때 호출됩니다
placeholderWidget?아무것도 고르지 않았을 때 trigger에 보이는 내용
clearableboolfalse값을 비우는 ×를 보여 줍니다
closeOnSelectbool?!multiplenode를 고르자마자 팝업을 닫을지
searchableboolfalse트리 위에 걸러 내는 field를 둡니다. 맞은 node는 조상을 데리고 남습니다
searchLabelString?'Search'거르는 field에 적히는 말
emptyLabelString?'Nothing here'아무것도 걸리지 않았을 때 팝업이 하는 말
formatString Function(List<PlTreeSelectNode>)?trigger가 쥔 것을 쓰는 방식. 기본은 label을 쉼표로 이은 것
variant공통PlassVariantPlassVariant.glasstrigger의 재질. PlTextField와 같은 껍데기를 씁니다
size공통PlassSizePlassSize.md높이와 타입 스케일
color공통PlassColorPlassColor.primary의미론적 색 역할. 임의 색상값은 받지 않습니다
density공통PlassDensityPlassDensity.standard여백만 바꿉니다. 높이와 글자 크기는 그대로
elevation공통int0trigger의 그림자 깊이. 팝업은 3으로 고정입니다
labelWidget?trigger 위 라벨
descriptionWidget?trigger 아래 보조 설명
errorWidget?오류 메시지. 존재 자체가 invalid 상태를 만듭니다
invalidbool?메시지 없이 invalid로 만듭니다
startIconWidget?값 앞의 글리프
fullWidthboolfalse컨테이너 너비만큼 확장
readOnlyboolfalse값은 보이지만 바꿀 수 없고, 팝업도 열리지 않습니다
disabledboolfalse사용 불가
semanticLabelString?스크린 리더가 trigger에 주는 이름
focusNodeFocusNode?바깥에서 focus를 다룹니다
autofocusboolfalse트리에 들어가면서 focus를 가져갑니다

PlTreeSelectNode

Prop타입기본값설명
id * string트리 전체에서 유일한 식별자
label * ReactNode행이 말하는 것
searchLabelstring검색이 대조하는 문자열. label이 문자열이면 그것으로, 아니면 id로 떨어집니다
iconReactNodelabel 앞의 글리프
childrenreadonly PlTreeSelectNode[]자식들. 빈 배열은 아무것도 없는 **가지**이고, undefined는 **잎**입니다
selectableboolean이 node 자체를 고를 수 있는지. 잎은 true, 가지는 selectableBranches를 따릅니다
disabledboolean트리에는 있지만 고를 수 없고, 화살표 키의 정거장도 아닙니다
Prop타입기본값설명
id * String트리 전체에서 유일한 식별자
label * String행이 말하는 것이자, 필터가 대조하는 것이자, trigger가 쓰는 것. React와 달리 위젯이 아니라 문자열입니다
iconWidget?label 앞의 글리프
childrenList<PlTreeSelectNode>?자식들. 빈 리스트는 아무것도 없는 **가지**이고, null은 **잎**입니다
selectablebool?이 node 자체를 고를 수 있는지. 잎은 true, 가지는 selectableBranches를 따릅니다
disabledboolfalse트리에는 있지만 고를 수 없고, 화살표 키의 정거장도 아닙니다

native <div> 속성은 field 래퍼로 그대로 전달됩니다. color는 위 표의 color와 충돌해서, defaultValue는 picker가 DOM 속성이 아니라 id 목록으로 쓰기 때문에, children은 트리가 items이기 때문에 제외됩니다.

className은 label과 control, 그 아래 두 줄을 담은 스택에 붙습니다. classNames는 그 안의 네 부분(label, control, description, error)에 닿습니다.

value는 **Set<String>**이고 controlled입니다. uncontrolled 형태는 없으며, 이 패키지의 모든 입력이 그렇습니다. expandedopen만 예외로, 넘기지 않으면 picker가 직접 쥡니다.

node의 label은 여기서 **String**이고 React에서는 ReactNode입니다. PlTransferItem이 이미 지고 있는 차이이고 이유도 같습니다. 필터가 label을 읽고, trigger가 그것을 쓰고, 스크린 리더가 그것을 받습니다. 텍스트라야 모든 node가 만들어질 때부터 검색 가능합니다. 이쪽에 searchLabel이 없는 것도 같은 이유입니다. label이 이미 그 말입니다.

라이브러리 전체에서 공유 축이 뜻하는 바는 prop 규약에 있습니다.

Examples

searchable

트리 위에 걸러 내는 field를 둡니다. 맞은 node는 조상을 데리고 남습니다. 아무것도 위에 없는 "Seoul"은 어느 분류에서 나온 것인지 말해 주지 않기 때문입니다. 그리고 필터가 남긴 가지는 전부 열립니다. 닫힌 부모 안에 접힌 match는 아무에게도 보여 주지 않은 match입니다.

React

field를 비우면 접힘은 다시 읽는 사람의 것이 됩니다. 직접 열어 둔 가지는 그대로 열려 있고, 필터가 연 가지는 다시 닫힙니다.

대조는 악센트와 대소문자를 함께 접습니다. joseJosé를 찾습니다.

대조는 대소문자만 접습니다. 악센트는 벗기지 않는데, Dart 코어에 String.normalize가 없고 이 패키지는 의존성을 두지 않기 때문입니다. React 쪽은 악센트까지 접습니다.

selectableBranches

기본은 꺼져 있고, 이런 트리는 대개 그런 모양입니다. 가지는 분류이고 잎이 답입니다. 고를 수 없는 가지도 여닫히기는 합니다. 그것을 누르는 것이 아래에 있는 것에 닿는 방법이기 때문입니다.

React

node 자신의 selectable이 어느 쪽으로든 덮어쓰므로, 진짜 카테고리인 "Home"은 고를 수 있게 두고 나머지 가지는 길로 남길 수 있습니다.

multiple

누를 때마다 더해지고, trigger는 쉼표로 이어서 씁니다. 팝업은 열린 채로 있습니다. 여러 답 중 첫 번째에서 닫히는 picker는 나머지마다 다시 열어야 하기 때문입니다.

React

format은 고른 node들을 받아 원하는 대로 씁니다. 답과 함께 넓어지지 않는 trigger를 만들 때 씁니다.

tsx
<PlTreeSelect items={items} multiple format={(chosen) => chosen.length + ' regions'} />

Controlled

값, 접힘, 팝업은 각각 다른 질문이고 각자 짝이 있습니다.

tsx
<PlTreeSelect
  items={items}
  value={chosen}
  onValueChange={setChosen}
  expanded={open}
  onExpandedChange={setOpen}
/>

폴더를 여는 것은 그것을 고르는 것이 아닙니다. 두 번째 짝이 따로 있는 이유입니다.

In a form

name을 주면 쥔 id 하나당 <input type="hidden"> 하나가 놓이므로, multiple picker는 반복 field로 제출됩니다.

tsx
<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 정거장 하나.
  • 는 실제로 보이는 행을 걷고, 는 가지를 연 뒤 안으로 들어가고, 는 닫거나 부모로 나가고, EnterSpace가 고릅니다.
  • 가지라서 고를 수 없을 뿐인 node에는 aria-disabled를 붙이지 않습니다. 아래 있는 것을 여는 조작 가능한 행이기 때문입니다. disabled node는 표시되고, 화살표 키의 정거장도 아닙니다.
  • 거르는 field는 searchLabel로 스스로 이름을 붙이므로, 위에 보이는 label 없이도 읽힙니다.

Released under the MIT License