Combobox
직접 입력할 수도, 목록에서 고를 수도 있는 필드입니다. 입력한 글자는 목록을 걸러내고, 끄지 않는 한 그 자체로 값이 될 수도 있습니다.
import { Combobox } from 'neba';
<Combobox
label="프레임워크"
placeholder="검색하거나 직접 입력하세요"
items={[
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' }
]}
/>;Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| variant공통 | 'solid' | 'outline' | 'text' | 'outline' | 표면의 무게. TextField와 같은 셸이므로 폼 안에서 두 컨트롤이 구분되지 않습니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 높이와 타입 스케일. multiple에서는 칩이 줄바꿈하는 만큼 자라므로 최소 높이가 됩니다 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 표면은 흰색이므로 가장자리·포커스 링·칩에 나타납니다 |
| density공통 | 'default' | 'compact' | 'default' | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | 0 | 1 | 2 | 3 | 0 | 필드의 그림자 깊이. 팝업은 자기 그림자를 따로 가집니다 |
| items * | readonly ComboboxOption[] | — | 옵션 목록. { value, label?, disabled? } 배열이고, label은 ReactNode가 아니라 string입니다 |
| multiple | boolean | false | 값을 여러 개 들 수 있는지. 고른 값은 필드 안의 칩이 됩니다 |
| value | string | number | (string | number)[] | null | — | 선택된 값. multiple이면 배열입니다. onValueChange와 함께 제어 컴포넌트로 씁니다 |
| defaultValue | string | number | (string | number)[] | null | — | 초기 선택 값 |
| onValueChange | (value) => void | — | 선택이 바뀔 때 |
| onInputValueChange | (inputValue: string) => void | — | 입력란의 글자가 바뀔 때. 값이 아니라 필터 질의입니다 |
| allowCustom | boolean | true | 목록에 없는 값을 확정할 수 있는지. 입력한 글자가 목록 맨 끝에 자기 행으로 제안됩니다 — 검색되는 select와 combobox를 가르는 지점입니다 |
| customLabel | (query: string) => ReactNode | Add “…” | 그 행이 하는 말 |
| clearable | boolean | false | 필드를 비우는 ×. 한 번에 비워지는 필드는 실수로도 비워지는 필드입니다 |
| emptyMessage | ReactNode | 'No matches' | 일치하는 것이 없고 값을 추가할 수도 없을 때 팝업이 하는 말 |
| limit | number | -1 | 한 번에 보여 줄 최대 행 수. -1은 전부 |
| placeholder | string | — | 아무것도 입력하지 않았을 때 보이는 내용 |
| label | ReactNode | — | 컨트롤과 연결되는 라벨. Base UI Field가 묶어 줍니다 |
| description | ReactNode | — | 보조 설명 |
| error | ReactNode | — | 오류 메시지. 값이 있으면 invalid 상태도 함께 켜지고 색 계열이 danger로 넘어갑니다 |
| invalid | boolean | !!error | 메시지 없이 invalid만 켭니다. 외부 폼 라이브러리가 유효성을 가질 때 |
| startIcon | ReactNode | — | 입력란 앞에 놓이는 내용 |
| fullWidth | boolean | false | 컨테이너 너비만큼 확장 |
| removeLabel | (label: string) => string | Remove … | 칩 제거 버튼의 접근성 이름. 칩의 라벨을 받습니다 |
| clearLabel | string | 'Clear' | × 버튼의 접근성 이름 |
| name | string | — | 폼 제출 시의 필드 이름 |
| readOnly | boolean | false | 값은 보이지만 바꿀 수 없음. 색과 가장자리는 유지한 채 채도만 빠집니다 |
| disabled | boolean | false | 사용 불가. 색 계열을 버리고 중립 회색이 됩니다 |
옵션은 데이터입니다
Select와 같은 모양이고, 한 가지만 다릅니다.
interface ComboboxOption {
value: string | number;
label?: string; // ReactNode가 아니라 string입니다
disabled?: boolean;
}여기서 label이 string인 것은 필터가 이 값을 상대로 매칭하고 입력란이 이 값을 그대로 써 넣기 때문입니다 — 엘리먼트로는 둘 다 할 수 없습니다. 값은 여전히 문자열과 숫자입니다. combobox는 폼 컨트롤이고, 그 값은 제출되는 것입니다.
예시
여러 개 고르기
고른 값은 필드 안에서 Chip이 되고, 입력란은 그 뒤로도 계속 필터 역할을 합니다. 필드가 한 번도 닫히지 않은 채로 태그 묶음이 만들어집니다. 입력란이 비어 있을 때 Backspace를 누르면 마지막 칩으로 손이 갑니다.
목록에 없는 값
이것이 검색되는 select와 combobox를 가르는 지점이고, 기본으로 켜져 있습니다.
입력한 글자는 포커스가 빠질 때 조용히 확정되는 대신, 목록 맨 끝에 자기 행으로 제안됩니다. 의도된 것입니다 — 쓰다 만 단어를 포커스가 떠나는 순간 값으로 만들어 버리는 필드는 데이터를 지어내는 필드입니다. 행으로 만들면 Enter도, 클릭도, 방향키도 다른 모든 행과 똑같은 방식으로 그곳에 닿고, 스크린 리더도 하나의 옵션으로 읽습니다.
타이핑하는 동안 첫 번째 일치 항목에 불이 들어오므로, 필요한 것은 여전히 키 하나입니다. 목록에 있는 것을 치면 Enter가 그것을 고르고, 없는 것을 치면 Enter가 그것을 더합니다.
값이 닫힌 집합인 필드라면 allowCustom={false}로 끄세요. 그때는 아무것도 찾지 못한 검색이 하는 말이 emptyMessage입니다.
Variant
TextField와 같은 세 가지 무게를, 같은 셸 위에 그립니다.
크기
필드 안의 칩은 컨트롤 사다리에서 한 칸 아래에 앉고, 남는 만큼이 필드의 상하 여백이 됩니다 — 그래서 한 줄짜리 combobox는 옆에 선 필드와 정확히 같은 높이입니다. multiple에서는 칩이 줄바꿈하는 만큼 필드가 자라며, 고정 높이를 갖지 않습니다.
상태
팝업
Select의 것과 동일합니다. combobox의 목록과 select의 목록은 같은 목록이기 때문입니다 — 3단계 그림자를 단 떠 있는 표면이고, <body> 끝으로 포털되며, CSS 리셋을 서브트리에 한정해 둔 호스트를 위해 positioner에 neba-portal이 걸려 있습니다.
접근성
- 필터링과 그 콜레이터, 팝업의 위치 계산과 뒤집힘,
combobox/listbox연결, 목록과 칩을 가로지르는 방향키 이동, 폼 제출을 가능하게 하는 숨은 input은 모두 Base UI가 담당합니다. label이 접근성 이름이 됩니다.- 비활성 옵션은 목록에 남은 채
aria-disabled로 보고됩니다 — 옵션은 존재하고, 다만 고를 수 없을 뿐입니다. - 칩의 제거 버튼 이름은
removeLabel이 짓고, 칩의 라벨을 받습니다. 그래서 버튼은 그냥 _Remove_가 아니라 _Remove documentation_이라고 말합니다.