PlCombobox
입력할 수도 있고 고를 수도 있는 field입니다. 입력한 글자가 목록을 거르고, 막지 않는 한 그 글자 자체가 값이 될 수도 있습니다.
import { PlCombobox } from 'plass-ui';
<PlCombobox
label="Framework"
placeholder="Search…"
items={[
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' }
]}
/>;import 'package:plass_ui/plass_ui.dart';
PlCombobox<String>(
label: const Text('Framework'),
placeholder: 'Search…',
value: framework,
onChanged: (String? next) => setState(() => framework = next),
options: const <PlComboboxOption<String>>[
PlComboboxOption<String>(value: 'react', label: 'React'),
PlComboboxOption<String>(value: 'vue', label: 'Vue'),
],
);목록은 트리 밖으로 자기를 들어 올리므로 combobox 위에 Overlay가 필요합니다. navigator가 있는 WidgetsApp과 MaterialApp 둘 다 하나씩 제공합니다.
Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| variant공통 | 'solid' | 'glass' | 'ghost' | 'glass' | field의 재질. PlTextField와 같은 껍데기를 씁니다. solid는 시트에 파인 우물 |
| 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 | field의 그림자 깊이. 팝업은 3으로 고정입니다 — 팝업은 정말로 페이지 위에 떠 있습니다 |
| items * | readonly PlComboboxOption[] | — | 옵션 목록. PlSelect와 같은 모양입니다 — 호출자가 가진 건 거의 언제나 이미 배열입니다 |
| multiple | boolean | false | 값을 여러 개 담을지. 고른 값들이 field 안의 chip이 되고 입력은 계속 필터링합니다 |
| value | string | number | (string | number)[] | null | — | 선택된 값. onValueChange와 함께 controlled로 씁니다. multiple이면 배열입니다 |
| defaultValue | string | number | (string | number)[] | null | — | uncontrolled일 때 처음 선택된 값 |
| onValueChange | (value: string | number | (string | number)[] | null) => void | — | 값이 바뀔 때 호출됩니다 |
| onInputValueChange | (inputValue: string) => void | — | 입력창의 글자가 바뀔 때 — 값이 아니라 필터 질의입니다 |
| allowCustom | boolean | true | 목록에 없는 값을 확정할 수 있는지. 입력한 글자가 목록 끝의 행으로 제안됩니다 — blur에서 조용히 확정되는 게 아니라 사용자가 고르는 것입니다 |
| customLabel | (query: string) => ReactNode | Add “{query}” | 그 행이 뭐라고 말할지 |
| clearable | boolean | false | field를 비우는 ×를 보여 줍니다. 기본이 꺼짐인 건 한 번에 비울 수 있는 field는 실수로도 비워지기 때문입니다 |
| emptyMessage | ReactNode | 'No matches' | 일치하는 것도 없고 추가할 수도 없을 때 팝업이 하는 말 |
| limit | number | -1 | 한 번에 보여 줄 최대 행 수. -1은 전부 |
| placeholder | string | — | 아무것도 입력하지 않았을 때 보이는 내용 |
| label | ReactNode | — | field 위 라벨. Base UI의 Field가 서로 엮어 줍니다 |
| description | ReactNode | — | field 아래 보조 설명 |
| error | ReactNode | — | 오류 메시지. 존재 자체가 invalid 상태를 만듭니다 |
| invalid | boolean | — | 메시지 없이 invalid로 만듭니다 |
| startIcon | ReactNode | — | 입력창 앞에 놓이는 내용. 1.2em으로 그려져 글자 크기를 따라갑니다 |
| fullWidth | boolean | false | 컨테이너 너비만큼 확장 |
| disabled | boolean | false | 사용 불가 |
| readOnly | boolean | false | 값은 보이지만 바꿀 수 없음 |
| required | boolean | false | 폼 제출 전에 값이 있어야 하는지 |
| name | string | — | 폼 제출 시 필드를 식별합니다 |
| open | boolean | — | 팝업이 열려 있는지. onOpenChange와 함께 controlled로 씁니다 |
| defaultOpen | boolean | — | 팝업이 열린 채로 시작할지 |
| onOpenChange | (open: boolean) => void | — | 팝업이 열리고 닫힐 때 호출됩니다 |
| openLabel | string | 'Open' | 목록을 여는 버튼의 접근성 이름 |
| clearLabel | string | 'Clear' | × 버튼의 접근성 이름 |
| removeLabel | (label: string) => string | Remove {label} | chip의 × 버튼 접근성 이름. chip의 라벨을 받습니다 |
| inputRef | Ref<HTMLInputElement> | — | 사용자가 입력하는 input에 대한 ref |
| hotKeys | Record<string, () => void> | — | 이 컨트롤이 답할 chord들. PlHotKeys가 그리는 것과 같은 철자입니다 — { 'Mod+Enter': save, Escape: cancel }. 맞는 chord는 **소비됩니다** |
| classNames | { label?, control?, description?, error?: string } | — | className이 닿지 않는 부분에 붙는 class. control은 실제로 조작하는 부분입니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| options * | List<PlComboboxOption<T>> | — | 선택지 목록. 필터가 이들의 label을 읽습니다 |
| value * | T? | — | 선택된 값. 단일 폼에만 있습니다 |
| onChanged | ValueChanged<T?>? | — | 값이 정해졌을 때 호출됩니다. multiple 폼에서는 ValueChanged<List<T>>입니다 |
| values * | List<T> | — | 선택된 값들. PlCombobox.multiple에만 있습니다 |
| onCreate | T Function(String query)? | — | 입력한 글자를 값으로 바꿉니다. **이걸 주는 것이 곧 allowCustom입니다** — React에서는 값이 언제나 string이나 number라 field가 스스로 만들 수 있지만, 여기서는 T이고 그걸 만드는 법은 호출자만 압니다 |
| customLabel | Widget Function(String query)? | — | 그 행이 뭐라고 말할지 |
| onQueryChanged | ValueChanged<String>? | — | 입력창의 글자가 바뀔 때 — 값이 아니라 필터 질의입니다 |
| placeholder | String? | — | 아무것도 입력하지 않았을 때 보이는 내용 |
| emptyMessage | String | 'No matches' | 일치하는 것도 없고 추가할 수도 없을 때 팝업이 하는 말 |
| limit | int? | null | 한 번에 보여 줄 최대 행 수. -1은 전부 |
| clearable | bool | false | field를 비우는 ×를 보여 줍니다. 기본이 꺼짐인 건 한 번에 비울 수 있는 field는 실수로도 비워지기 때문입니다 |
| clearLabel | String | 'Clear' | × 버튼의 접근성 이름 |
| openLabel | String | 'Open' | 목록을 여는 버튼의 접근성 이름 |
| removeLabel | String Function(String label) | Remove {label} | chip의 × 버튼 접근성 이름. chip의 라벨을 받습니다 |
| variant공통 | PlassVariant | PlassVariant.glass | field의 재질. PlTextField와 같은 껍데기를 씁니다. solid는 시트에 파인 우물 |
| size공통 | PlassSize | PlassSize.md | 높이와 타입 스케일 |
| color공통 | PlassColor | PlassColor.primary | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | PlassDensity | PlassDensity.standard | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | int | 0 | field의 그림자 깊이. 팝업은 3으로 고정입니다 — 팝업은 정말로 페이지 위에 떠 있습니다 |
| label | Widget? | — | field 위 라벨. Base UI의 Field가 서로 엮어 줍니다 |
| description | Widget? | — | field 아래 보조 설명 |
| error | Widget? | — | 오류 메시지. 존재 자체가 invalid 상태를 만듭니다 |
| invalid | bool? | — | 메시지 없이 invalid로 만듭니다 |
| startIcon | Widget? | — | 입력창 앞에 놓이는 내용. 1.2em으로 그려져 글자 크기를 따라갑니다 |
| fullWidth | bool | false | 컨테이너 너비만큼 확장 |
| readOnly | bool | false | 값은 보이지만 바꿀 수 없음 |
| disabled | bool | false | 사용 불가 |
| semanticLabel | String? | — | 보이는 label이 없는 field를 스크린 리더가 부를 이름 |
| focusNode | FocusNode? | — | 포커스를 밖에서 제어할 때 넘깁니다 |
| autofocus | bool | false | 트리에 들어가면서 포커스를 가져갑니다 |
| hotKeys | PlassHotKeys? | — | 이 컨트롤이 답할 chord들. PlHotKeys가 그리는 것과 같은 철자입니다 — { 'Mod+Enter': save, Escape: cancel }. 맞는 chord는 **소비됩니다** |
나머지 <div> 속성은 field 래퍼로 그대로 통과합니다. color는 위 표의 color와 겹쳐서, defaultValue는 DOM 속성이 아니라 값으로 쓰기 때문에, children은 옵션이 items이기 때문에 제외했습니다.
className은 label과 control, 그 아래 두 줄을 함께 담는 stack에 붙습니다. 그 안쪽 네 부분에 닿는 것이 classNames입니다: label, control(chip까지 포함한 field의 껍데기), description, error.
combobox는 값의 타입에 대해 generic이고(PlCombobox<String>, PlCombobox<Tag>) 패키지의 다른 모든 입력과 마찬가지로 controlled입니다. 여러 값을 담는 것은 두 번째 생성자인 PlCombobox.multiple이고, values를 받아 List<T>를 보고합니다. multiple 플래그 하나만 둔 위젯이라면 두 가지 모양의 값을 다 들고 있어야 하고 둘 다 타입이 붙지 않습니다.
onCreate는 React의 allowCustom에 해당하고, 플래그가 아니라 콜백인 데는 React에는 없는 이유가 있습니다. 저쪽에서 값은 언제나 string이나 number라 field가 질의로부터 스스로 하나를 만들 수 있습니다. 여기서 값은 T이고, 그걸 만드는 법은 호출자만 압니다. 그래서 허가와 만드는 법이 같은 파라미터입니다. PlCombobox<String>이라면 (String query) => query입니다.
PlComboboxOption
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| value * | string | number | — | 제출되는 값이고, value / onValueChange가 말하는 언어입니다 |
| label | string | — | 목록과 입력창과 chip에 보이는 이름. 없으면 value 자체. ReactNode가 아니라 string인 건 필터가 이걸 대상으로 검색하고 text input에 써 넣기 때문입니다 |
| disabled | boolean | false | 고를 수 없지만 목록에는 남습니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| value * | T | — | 제출되는 값이고, value / onValueChange가 말하는 언어입니다 |
| label * | String | — | 목록과 입력창과 chip에 보이는 이름. 없으면 value 자체. ReactNode가 아니라 string인 건 필터가 이걸 대상으로 검색하고 text input에 써 넣기 때문입니다 |
| disabled | bool | false | 고를 수 없지만 목록에는 남습니다 |
공유 축(variant size color density elevation)이 라이브러리 전체에서 무엇을 뜻하는지는 prop 규약에 있습니다.
PlTextField 위에 세운 것
픽셀 단위로 그렇고, PlSelect의 trigger도 마찬가지입니다. 폼 안에서 셋이 구분되지 않아야 폼이 조립된 것이 아니라 설계된 것으로 보입니다. 껍데기가 셋 중 어디도 아닌 internal/styles에 사는 이유입니다.
다른 것은 글자가 하는 일입니다. select에서 글자는 값이고, 여기서 글자는 목록을 거르며 값이 될 수도 있습니다.
Examples
고르기와 입력하기
PlSelect는 닫힌 집합에서 고르는 컨트롤입니다. 이건 집합을 검색 하는 컨트롤이고, 기본값인 allowCustom이 켜져 있으면 거기에 더할 수도 있는 컨트롤입니다.
입력한 글자는 목록 끝의 자기 행으로 제안됩니다. 그래서 그것을 확정하는 것은 사용자가 하는 선택이지, blur에서 사용자에게 일어나는 일이 아닙니다. 값이 정말로 닫힌 집합이면 allowCustom을 끄세요. 그러면 검색되는 select가 됩니다.
multiple
고른 값들이 field 안의 PlChip이 되고 입력은 그 뒤로도 계속 필터링합니다. field가 한 번도 닫히지 않은 채 태그 묶음이 만들어집니다.
이때 field는 고정 높이를 가질 수 없습니다(chip이 줄바꿈하니까요). 그래서 패딩이 (컨트롤 높이 − chip 높이) / 2가 되고, 한 줄짜리 combobox는 옆의 field와 정확히 같은 높이가 됩니다.
size
다른 모든 컨트롤과 같은 높이 사다리입니다. multiple에서는 위의 이유로 그 숫자가 높이가 아니라 최소 높이가 됩니다.
readOnly · disabled · error
error는 combobox를 invalid로도 만들고, 그러면 색 계열 전체가 danger로 옮겨 갑니다. 테두리와 ring과 caret과 메시지가 함께 넘어갑니다. invalid는 메시지 없이 같은 일을 합니다.
readOnly combobox는 값과 포커스를 유지하되 입력을 받지 않고, chip의 ×도 사라집니다. disabled는 포커스 순서에서 빠집니다.
옵션 하나만 disabled일 수도 있습니다. 그래도 목록에 남습니다. 고를 수 없다고 사라지는 옵션은 독자가 찾아 헤매게 되는 옵션입니다.
Controlled
value를 onValueChange와 함께 주세요. 값은 string이나 number이고(multiple이면 그 배열입니다) 절대 객체가 아닙니다. combobox는 form 컨트롤이고, 그 값은 form이 보내는 것입니다. 식별자를 여기 두고 객체는 반대편에서 찾으세요.
Accessibility
- Base UI가
combobox/listbox쌍을 렌더링하고aria-expanded와aria-activedescendant를 맞춰 두며, 필터링과 collator도 소유합니다. labeldescriptionerror는 Base UI의 Field가 입력창과 엮어 주므로htmlFor가 필요 없습니다.- 키보드는 primitive의 것입니다. ↑ ↓로 목록을 움직이고, Enter로 강조된 행을 취하고, Esc로 닫습니다.
multiple에서는 ← →가 chip 사이를 걷고 Backspace가 하나를 지웁니다. - 입력하는 동안 첫 일치 항목에 불이 들어와서, 화살표 없이 Enter만으로 확정됩니다. "이걸 추가" 행이 키보드로 닿을 수 있는 이유도 이것입니다. 목록에 없는 값은 유일한 일치 항목이기 때문입니다.
- "이걸 추가" 행은 키 처리의 특수 케이스가 아니라 진짜 option입니다. 클릭도, Enter도, 화살표도 다른 모든 행과 똑같은 방식으로 닿습니다.
- 행은
:hover가 아니라data-highlighted로 켜집니다. 포인터와 화살표가 같은 행을 밝힙니다. - chip의 ×는 자기 chip의 이름을 답니다.
Remove가 아니라Remove Seoul. 똑같은 버튼 여섯 개를 읽어 주는 스크린리더는 아무것도 말해 주지 않은 것과 같습니다. name이 있으면 Base UI가 hidden input을 렌더링해 값이 네이티브 form 제출에 포함됩니다.- 팝업은
<body>끝으로 portal되고 positioner가.plass-portal을 답니다. CSS 리셋을 subtree에 한정한 호스트가 같은 리셋을 걸 수 있는 자리입니다.
- field는 목록이 열려 있는지 말해 주는 text field로 읽힙니다. 각 행은 서로 배타적인 묶음 중 하나로, 취해졌는지 여부와 함께 읽힙니다.
- 키는 field에 머뭅니다. 포커스도 그렇습니다. ↑ ↓가 강조를 옮기고, Enter가 강조된 행을 취하고, Escape가 아무것도 취하지 않고 닫습니다. 목록은 field의 목록이지 두 번째로 머물 자리가 아닙니다.
- 질의가 바뀔 때마다 첫 일치 항목에 불이 들어와서, 화살표 없이 Enter만으로 확정됩니다. 생성 행이 키보드로 닿을 수 있는 이유도 이것입니다.
- 강조는 행마다의 hover 상태가 아니라 숫자 하나입니다. 그래서 포인터와 화살표가 같은 행을 밝힙니다.
- 취할 수 없는 행도 목록에 남고, 사용할 수 없다고 읽힙니다.
- chip의 ×는 자기 chip의 이름을 답니다.
- 포커스가 떠날 때 아무것도 확정되지 않습니다. 질의는 값으로 되돌아가고, 목록에 없는 값은 오직 그 행을 취해야만 값이 됩니다.
React 빌드와 다른 점
| React | Flutter | 이유 |
|---|---|---|
items | options | 패키지의 나머지가 선택지 목록을 부르는 이름입니다. |
값이 string | number | generic T | 여기서는 제출되는 것이 없으므로 값이 그 물건 자체일 수 있고, 타입 검사기가 지켜 줍니다. |
prop으로서의 multiple | 두 번째 생성자 PlCombobox.multiple | 플래그 하나짜리 위젯은 두 모양의 값을 다 들고 있어야 하고 둘 다 타입이 붙지 않습니다. |
allowCustom (기본이 켜진 boolean) | onCreate (T Function(String)) | field는 질의로부터 T를 만들 수 없습니다. 허가와 만드는 법이 같은 파라미터입니다. |
ReactNode label, Base UI collator 기반 필터 | Widget, 대소문자 접은 contains 필터 | label이 여전히 String인 것은 같은 이유입니다. 필터가 그것을 읽고, field에 써 넣습니다. |
hidden input, name, required | — | 참여할 네이티브 form 제출이 없습니다. |
className, style, 네이티브 속성 | — | 통과시킬 class 목록도 style 속성도 없습니다. |