본문으로 건너뛰기

Combobox

입력한 글자로 목록을 걸러내면서 값을 고르는 필드입니다. 선택지가 많아 Select로는 찾기 어려울 때, 또는 목록에 없는 값도 받아야 할 때 씁니다.

tsx
import { Combobox } from 'neba';

<Combobox
  label="프레임워크"
  placeholder="검색하거나 직접 입력하세요"
  items={[
    { value: 'react', label: 'React' },
    { value: 'vue', label: 'Vue' }
  ]}
/>;

Props

Prop타입기본값설명
localestringBCP 47 태그. 결과 없음 문구와 지우기 · 삭제 버튼의 이름을 이 언어로 씁니다
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 | 30필드의 그림자 깊이. 팝업은 자기 그림자를 따로 가집니다
items * readonly ComboboxOption[]옵션 목록. { value, label?, disabled? } 배열이고, label은 ReactNode가 아니라 string입니다
multiplebooleanfalse값을 여러 개 들 수 있는지. 고른 값은 필드 안의 칩이 됩니다
valuestring | number | (string | number)[] | null선택된 값. multiple이면 배열입니다. onValueChange와 함께 제어 컴포넌트로 씁니다
defaultValuestring | number | (string | number)[] | null초기 선택 값
onValueChange(value) => void선택이 바뀔 때
onInputValueChange(inputValue: string) => void입력란의 글자가 바뀔 때. 값이 아니라 필터 질의입니다
filterfalse | ((option: ComboboxOption, query: string) => boolean)타이핑이 목록을 좁히는 방식. false면 거르지 않습니다. 서버가 이미 좁혀 준 목록에 씁니다
allowCustombooleantrue목록에 없는 값을 확정할 수 있는지. 입력한 글자가 목록 맨 끝에 자기 행으로 제안됩니다. 검색되는 select와 combobox를 가르는 지점입니다
customLabel(query: string) => ReactNodeAdd “…”그 행이 하는 말
clearablebooleanfalse필드를 비우는 ×. 한 번에 비워지는 필드는 실수로도 비워지는 필드입니다
emptyMessageReactNode일치하는 것이 없고 값을 추가할 수도 없을 때 팝업이 하는 말
limitnumber-1한 번에 보여 줄 최대 행 수. -1은 전부
placeholderstring아무것도 입력하지 않았을 때 보이는 내용
labelReactNode컨트롤과 연결되는 라벨. Base UI Field가 묶어 줍니다
descriptionReactNode보조 설명
errorReactNode오류 메시지. 값이 있으면 invalid 상태도 함께 켜지고 색 계열이 danger로 넘어갑니다
invalidboolean!!error메시지 없이 invalid만 켭니다. 외부 폼 라이브러리가 유효성을 가질 때
startIconReactNode입력란 앞에 놓이는 내용
fullWidthbooleanfalse컨테이너 너비만큼 확장
removeLabel(label: string) => stringRemove …칩 제거 버튼의 접근성 이름. 칩의 라벨을 받습니다
clearLabelstring× 버튼의 접근성 이름
namestring폼 제출 시의 필드 이름
readOnlybooleanfalse값은 보이지만 바꿀 수 없음. 색과 가장자리는 유지한 채 채도만 빠집니다
disabledbooleanfalse사용 불가. 색 계열을 버리고 중립 회색이 됩니다
shortcutsNebaShortcuts<HTMLInputElement>Shortcut이 그리는 그대로 쓴 키 조합과 그때 할 일. { 'Mod+Enter': send } 형태입니다. Mod는 Mac에서 Command, 나머지에서 Control입니다. Combobox에서는 이것이 유일한 통로입니다. 화살표·Escape·Enter는 목록의 키라서 root의 onKeyDown에는 아예 오지 않습니다. 목록보다 먼저 실행되지만 목록이 하는 일을 대신하지는 않습니다
openboolean팝업이 열려 있는지. `onOpenChange`와 함께 제어 컴포넌트로 씁니다
defaultOpenboolean팝업이 열린 채로 시작할지
onOpenChange(open: boolean) => void팝업이 열리거나 닫힐 때
requiredboolean폼을 제출하기 전에 값을 골라야 하는지
inputRefRef<HTMLInputElement>글자를 입력하는 `<input>`을 가리키는 ref
classNamesNebaSlots<'label' | 'shell' | 'control' | 'description' | 'error' | 'chip' | 'popup' | 'item'>루트 뒤에 있는 각 파트의 class. 루트 자체는 className이 맡으므로 root 키는 없습니다

<div>의 native 속성은 root로 전달됩니다. colordefaultValue만 위 표와 이름이 겹쳐 제외됩니다.

items

Select와 같은 배열 형태이며, label의 타입만 다릅니다.

ts
interface ComboboxOption {
  value: string | number;
  label?: string; // ReactNode가 아니라 string
  disabled?: boolean;
}

필터가 이 label을 상대로 매칭하고 입력란이 이 값을 그대로 채우기 때문에 string이어야 합니다.

예시

multiple

고른 값은 필드 안에서 Chip으로 표시되고, 입력란은 그 뒤로도 계속 필터로 동작합니다. 입력란이 비어 있을 때 Backspace를 누르면 마지막 chip으로 focus가 이동합니다.

allowCustom · customLabel · emptyMessage

allowCustom은 기본값이 켜짐입니다. 입력한 글자가 목록 맨 끝에 별도 행으로 제안되므로, Enter나 클릭, 방향키 모두 다른 행과 같은 방식으로 닿습니다. focus가 빠질 때 조용히 확정되지는 않습니다.

값이 닫힌 집합이라면 allowCustom={false}로 끄세요. 이때 일치하는 항목이 없으면 emptyMessage가 표시됩니다.

variant

TextField와 같은 세 가지 무게를 같은 shell 위에 그립니다.

size

단일 선택 Combobox는 같은 sizeTextField와 높이가 같습니다. multiple에서는 chip이 줄바꿈하는 만큼 필드가 높아지므로 고정 높이를 갖지 않습니다.

disabled · readOnly · error

clearable · limit

clearable은 값을 비우는 버튼을 붙입니다. limit은 팝업에 한 번에 표시할 항목 수를 제한합니다.

filter

기본값은 입력한 글자로 목록을 좁히는 것입니다. 각 옵션의 label에 대해 대소문자와 악센트를 무시하고 비교합니다.

서버가 이미 좁혀 준 목록에는 filter={false}가 필요합니다. 키워드나 설명, 동의어로 검색한 결과에는 눈에 보이는 label에 질의가 들어 있지 않은 행이 섞여 있고, 여기서 한 번 더 거르면 검색이 찾아 준 결과가 바로 그 행들과 함께 사라집니다. onInputValueChange에서 요청하고, 받은 결과를 items에 넘기고, 거르지 않으면 됩니다.

tsx
<Combobox
  items={results}
  filter={false}
  onInputValueChange={(query) => search(query)}
  label="Customer"
/>

함수를 넘기면 옵션마다 직접 판단합니다. label뿐 아니라 value까지 보거나, 단어 중간이 아니라 앞부터 맞는 것만 남기는 식입니다. 입력한 값을 추가하겠다고 제안하는 행은 어떤 경우에도 걸러지지 않습니다.

팝업

Select의 팝업과 동일합니다. <body> 끝으로 portal되며 positioner에 neba-portal 클래스가 붙습니다.

shortcuts

Combobox에서는 이것이 유일한 통로입니다. 화살표는 highlight를 옮기고 Escape는 팝업을 닫고 Enter는 확정합니다. 이 키들은 목록의 것이라 root에 쓴 onKeyDown에는 아예 도달하지 않습니다.

tsx
<Combobox label="Framework" items={frameworks} shortcuts={{ 'Mod+Enter': createAndOpen }} />

조합은 Shortcut이 그리는 표기 그대로 쓰고, Mod는 Mac에서 Command, 그 밖에서는 Control이며 modifier는 정확히 일치해야 합니다.

<input>에 붙어 목록이 키를 처리하기 전에 실행되지만, 목록이 하는 일을 대신하지는 않습니다. Enter에 건 shortcut은 확정과 함께 실행되지 그것을 막지 않습니다. 키를 온전히 가져야 한다면 목록이 관심 없는 조합을 쓰세요.

classNames

className은 루트(라벨과 shell, 그 아래 두 줄을 담는 열)에 붙고, <input> 자체는 classNames.control로 갑니다.

tsx
<Combobox
  items={frameworks}
  label="Framework"
  multiple
  classNames={{ control: 'font-mono', chip: 'rounded-none', popup: 'max-h-40' }}
/>

slot은 label, shell, control, description, error, chip, popup, item입니다. chip은 multiple 모드에서 input 앞에 놓이는 토큰 하나입니다. popupitem<body> 끝에 그려지므로 루트를 기준으로 쓴 것으로는 닿지 않습니다. 넘긴 class가 컴포넌트 자신의 class와 어떻게 겨루는지는 prop 규약을 보세요.

접근성

  • trigger는 combobox, 목록은 listbox role을 갖고 label이 accessible name이 됩니다.
  • 필터링, 팝업 위치 계산과 뒤집힘, 목록과 chip을 가로지르는 방향키 이동, 폼 제출용 hidden input이 모두 처리됩니다.
  • disabled 항목은 목록에 남은 채 aria-disabled로 보고됩니다.
  • chip의 제거 버튼 이름은 removeLabel이 chip 라벨을 받아 만듭니다.
  • 결과 없음 문구와 지우기 · 삭제 버튼의 이름을 locale이 정합니다. emptyMessage, clearLabel, removeLabel로 직접 쓸 수도 있습니다.

Released under the MIT License