Field.ChipInput
Field 안의 제어되는 칩과 입력입니다. 필드는 칩을 직접 추가하지 않습니다. 부모의 onCommit이 결정합니다. 장식 요소는 칩 흐름 밖, Field.Root에 남습니다. ChipInput 안의 칩은 모든 밀도에서 입력과 같은 필드 한 줄 높이입니다. chipLayout="row"는 ChipInput 안에서 칩을 입력 위의 전체 너비 줄에 놓습니다. Icon과 Affix는 첫 줄에 놓입니다. 칩이 없으면 Field.TextInput과 같은 높이의 한 줄 필드입니다. row에서 maxRows는 칩 줄만 세고, 인라인 흐름처럼 잘라 냅니다. Label.Root htmlFor나 Root의 aria-label로 레이블을 붙이세요.
이 페이지의 내용
<Field.Root aria-label="Tags">
<Field.ChipInput
chips={chips}
inputValue={inputValue}
onInputValueChange={setInputValue}
onCommit={(value) => {
setChips((prev) => (prev.includes(value) ? prev : [...prev, value]));
setInputValue('');
}}
onChipRemove={(index) => setChips((prev) => prev.filter((_, i) => i !== index))}
/>
</Field.Root>
선택 사항인 useChipField는 칩과 입력을 보관하고, 그 getChipInputProps()를 Field.ChipInput에 펼쳐 넘깁니다.
const field = useChipField();
<Field.Root aria-label="Tags">
<Field.ChipInput {...field.getChipInputProps()} placeholder="Add a tag" />
</Field.Root>
이번 단계에서 미룬 것: 내장 지우기, 붙여넣기 분할, 클릭 가능한 +N, 칩 화살표 키 로빙.
확정 키
commitKeys={['Enter', ',']}. 알 수 없는 키는 버려집니다.
줄 수
maxRows가 설정되면 흐름은 첫 페인트부터 토큰에서 계산한 줄 수로 제한되고, 첫 레이아웃 측정 전까지 칩을 숨깁니다. 그래서 SSR이 +N으로 접히기 전에 펼쳐지지 않습니다. 보이는 영역은 앞쪽 칩들입니다. 가장 새 칩은 +N으로 접히고 실제 흐름에서 언마운트됩니다. 이후 칩이 전체 너비로 들어가지 않으면 +N으로 옮겨집니다. 첫 칩과 +N, 입력이 한 줄을 함께 쓸 수 없으면 첫 칩은 말줄임표로 잘립니다. 접근 가능한 이름은 전체 레이블을 유지합니다.
compact와 comfortable에서의 maxRows={1}과 {2}.
상태
disabled와 invalid는 Field.Root에 있습니다.
아이콘
칩 제거 버튼은 chipRemoveIcon을 전달하지 않으면 close 글리프를 보여 줍니다. Chip과 같습니다.
필수
required는 입력란에 무엇이 있든 칩이 없는 폼을 유효하지 않게 합니다. 입력란은 확정할 때마다 비워지기 때문입니다. 브라우저는 ChipInput이 입력란 옆에 렌더링하는 네이티브 텍스트 입력에서 칩을 검사합니다. 이 입력은 화면과 보조 기술에서 숨겨지고 탭 정지점이 아니며, 칩이 없을 때만 비어 있습니다. name이 없으므로 아무것도 보내지 않습니다. 폼은 전과 같이 입력란의 텍스트를 필드의 name으로 보내고, 칩을 보내는 일은 직접 해야 합니다. Form.Root 안에서는 문제가 필드의 ErrorMessage.Root에 나타나고 포커스는 텍스트 입력으로 갑니다. 텍스트 입력은 채워야 하는 대상이 아니므로 aria-required가 붙지 않고, 대신 StringsProvider의 required 문자열로 설명됩니다. ChipInput의 required는 전에는 입력란 자체에 텍스트가 있어야 했지만, 이제는 칩이 하나 이상 있어야 합니다.
접근성을 지키는 조합
Todae가 맡는 것: 입력은 Field.Root에서 id, aria-label, aria-labelledby, aria-describedby를 받고, Root나 바깥 Fieldset에서 네이티브 disabled와 aria-invalid를 받습니다. 칩은 입력의 disabled를 따릅니다. 각 삭제 버튼은 칩 이름을 따서 이름을 갖고, 칩을 지우면 포커스가 입력으로 돌아갑니다. 입력에 텍스트가 있을 때 확정 키를 누르면 다듬은 텍스트로 onCommit을 호출하고, 그 키는 입력에 전달하지 않습니다. onCommit이 false를 반환하면 Enter와 Tab은 폼이나 다음 컨트롤로 전달되고, 문자 키는 여전히 입력되지 않습니다. 한국어 단어를 끝내는 Enter처럼 IME 조합에 속한 키는 입력에 맡깁니다. 누르고 있는 확정 키는 한 번만 확정합니다. 확정한 뒤 반복되는 Enter는 폼에 전달하지 않고, 반복되는 Tab은 포커스를 옮깁니다.
직접 제공할 것: 이름. htmlFor가 Root id인 Label이나, Root의 aria-label입니다. 칩 문구는 StringsProvider나 chipRemoveLabel, overflowLabel로 제품의 언어에 맞추세요.
주의할 점: commitKeys에 'Tab'을 넣으면 Tab이 포커스를 옮기지 않고 입력한 텍스트를 확정합니다.
언제 쓰나요
Field.ChipInput
언제: Field.Root 안의 제어되는 칩 + 입력 하나. 부모가 onCommit으로 확정/추가를 담당하거나, useChipField의 getChipInputProps()를 펼쳐 그 상태를 보관합니다.
쓰지 않을 때: 자유 형식의 단일 문자열 → TextInput. 목록에서 고르는 옵션 → Combobox 레시피 + OptionList.
Props
| 이름 | 타입 | 설명 |
|---|---|---|
chips 필수 | string[] | 제어 칩 레이블. |
inputValue 필수 | string | 제어 입력 텍스트. |
onInputValueChange 필수 | (value: string) => void | 입력 텍스트 변경. |
onCommit 필수 | (value: string) => boolean | void | 확정 키를 누르고 공백을 제거한 값이 비어 있지 않을 때 호출됩니다. 칩을 추가하지는 않습니다. 확정할 것이 없었다면 false를 반환하세요. 그러면 Enter는 폼으로 넘어가고 Tab은 포커스를 옮기며, ,처럼 문자를 입력하는 키는 계속 입력되지 않게 막습니다. |
onChipRemove 필수 | (index: number, value: string) => void | 제거 버튼 클릭 또는 빈 입력에서 Backspace. |
chipRemoveLabel | (value: string) => string | 제거 버튼의 이름. 기본값은 StringsProvider의 removeChip 문자열이며, 영어로는 Remove {value}입니다. |
chipRemoveIcon | React.ReactNode | 칩 제거 버튼의 내용. 직접 사용하는 아이콘 세트의 아이콘 등. 없으면 GlyphsProvider의 close 글리프를, 그것도 없으면 Todae의 X 모양을 씁니다. |
required | boolean | 입력란에 무엇이 있든 칩이 없으면 폼이 유효하지 않습니다. |
overflowLabel | (n: number) => string | +N의 이름. 기본값은 StringsProvider의 moreChips 문자열이며, 영어로는 {n} more입니다. |
commitKeys | ReadonlyArray<"Enter" | "Tab" | "," | ";" | " "> | 확정 키로 쓸 수 있는 키의 고정 목록. 기본값: ["Enter"] |
maxRows | number | 보이는 줄 수를 제한합니다. inline: 칩과 입력을 함께 셉니다. row: 칩 줄만 세며, 입력은 자기 줄을 유지합니다. 가장 최근 칩부터 +N으로 접힙니다. 전체 너비로 들어가지 않는 칩은 +N으로 옮겨집니다. 줄보다 넓은 칩만 잘립니다. |
chipLayout | "inline" | "row" | inline: 칩이 입력 옆으로 흐릅니다. row: 칩이 ChipInput 안에서 입력 위의 전체 너비 줄에 놓이며, Icon과 Affix는 첫 줄에 놓입니다. 칩이 없으면 TextInput 높이의 한 줄 필드로 렌더링됩니다. 기본값: "inline" |
removeOnBackspace | boolean | 빈 입력에서 Backspace를 누르면 마지막 칩을 제거합니다. 기본값: true |
placeholder | string | 네이티브 placeholder. |