콤보박스
찾아서 고르는 칸입니다. 칸은 입력칸과 같은 파인 칸이고, 열린 목록은 선택 목록과 같은 뜸 판(층 60)입니다. 하나 · 여럿(칩) · 새 값 만들기 · 서버 검색을 한 부품이 맡습니다.
- import
import { Combobox } from "@uyoolabs/ui/form";- 쓰는 경우
- 고를 것이 많거나(20개 넘음) 서버에서 찾을 때 씁니다. 담당 · 참석자(여럿), 라벨(만들기), 알릴 채널(하나), 멤버 초대(빈 결과 → 이메일로 초대), 표 거르기 칩의 값 편집이 그렇습니다. 7~20개이고 찾을 일이 없으면 선택 목록(Select)입니다.
- 쓰지 않는 경우
- 자유 입력 칸 대신 쓰지 않습니다(목록에 없는 값을 넣지 못합니다. 새 값이 필요하면 creatable 을 켭니다). 하이라이트에 주 색(파랑)이나 굵기만 쓰지 않습니다(고른 항목의 600 과 헷갈립니다). 칩을 한 줄에 억지로 넣고 가로로 굴리지 않습니다(줄을 바꾸고, 떠나면 +N 으로 접습니다).
- 빈 결과
- 빈 결과에는 다음 할 일(초대 · 만들기 · 거르기 풀기)을 같이 둡니다. 무엇을 찾는지(멤버 · 라벨)는 labels.empty · labels.create 로 덮어 씁니다.
- 사람
- 아바타 + 이름 + 팀으로 보입니다. 같은 이름이 있으면 팀 · 이메일로 가립니다. 고를 수 없는 사람은 목록에서 빼지 않고 비활성 + 설명 줄로 이유를 적습니다.
하나 고르기
고른 값은 칸 안의 글자로 보입니다(앞에 아바타 · 아이콘). 키보드로 옮긴 항목은 파인 면 + 안쪽 링, 손을 올린 항목은 면만입니다.
고른 값: 김서연
여럿 — 칩
Enter 로 넣고 빼며 목록은 열려 있습니다. 빈 칸의 Backspace 는 마지막 칩을 먼저 고르고(링), 한 번 더 누르면 뺍니다.
만들기 · 빈 결과
서버 검색
‘실패’ 를 치면 오류 줄과 다시 시도가 보입니다.
찾은 말 강조(Highlight)
거르기와 하이라이트가 같은 함수(match)에서 나와 거른 항목에는 늘 칠한 곳이 있습니다. 칠한 범위는 원문 위치라 NFD 한글 · 전각 글자 · 길이가 바뀌는 글자(İ)에서도 어긋나지 않습니다. 명령 팔레트 · 검색 결과 · 표 찾기가 같이 씁니다.
- 환불 정책 v2 — 부분 환불 수수료
- 새 대화 시작
- 김서연 · 플랫폼팀
- カナ入力の設定
키보드
APG Editable Combobox With List Autocomplete 입니다. 포커스는 늘 칸에 있고 활성 항목은 aria-activedescendant 로 가리킵니다. 항목은 Tab 정지점이 아닙니다.
| 자리 | 키 | 동작 |
|---|---|---|
| 칸 | ↓ | 열기 · 다음(끝에서 처음으로). 비활성 항목은 건너뜁니다 |
| 칸 | ↑ | 이전(처음에서 끝으로) |
| 칸 | Alt + ↓ | 활성 없이 엽니다 |
| 칸 | Enter | 활성 항목을 고릅니다. 한 · 일 조합 중이면 글자 확정만 합니다(isComposing 가드) |
| 칸 | Esc | 닫습니다. 닫혀 있으면 검색 글자를 지웁니다. 조합 중이면 조합만 취소하고 목록은 그대로입니다 |
| 칸 | Tab | 고르지 않고 닫힙니다. 여럿이면 검색 글자를 비웁니다. 빈 결과의 다음 할 일이 있으면 그리로 갑니다 |
| 칸 | Home · End | 글자 커서입니다(목록이 아닙니다) |
| 칸(여럿) | Backspace | 빈 칸에서 마지막 칩을 먼저 고르고(링), 한 번 더 누르면 뺍니다 |
| 칸(여럿) | ← | 커서가 맨 앞이면 칩으로 갑니다. ← → 로 칩 사이, 끝에서 → 로 칸 |
| 칩 | Delete · Backspace | 그 칩을 뺍니다. 포커스는 다음 칩(없으면 칸)으로 갑니다 |
결과 수는 입력이 멈추고 500ms 뒤 한 번 읽힙니다(‘결과 5개’). 칩을 넣고 뺄 때는 ‘한서윤 넣음’ · ‘한서윤 뺌’ 이 읽히고, 오류 줄은 role=alert 입니다. 한국어 화면은 초성 검색(ㄱㅅㅇ → 김서연)이 기본이고 일본어 화면은 히라가나 · 가타카나를 같은 글자로 봅니다.
내장 문구
lang(ko · en · ja)으로 고르고 labels 로 일부를 바꿉니다(ADR 0011). 따옴표는 언어마다 다릅니다.
| 키 | ko | en | ja |
|---|---|---|---|
| open · clear | 목록 열기 · 모두 지우기 | Show options · Clear all | 一覧を開く · すべてクリア |
| removeChip(name) | {name} 빼기 | Remove {name} | {name}を削除 |
| more(n) | +{n} | +{n} | +{n} |
| empty(q) | ‘{q}’에 맞는 항목이 없습니다. | No results match ‘{q}’. | 「{q}」に一致する項目はありません。 |
| noItems | 고를 항목이 없습니다. | No options. | 選べる項目がありません。 |
| create(q) | ‘{q}’ 만들기 | Create ‘{q}’ | 「{q}」を作成 |
| searching | 더 찾는 중 | Searching… | 検索中… |
| error · retry | 불러오지 못했습니다. · 다시 시도 | Couldn’t load results. · Try again | 読み込めませんでした。 · 再試行 |
| results(n) | 결과 {n}개 | {n} results | {n}件 |
| added · removed | {name} 넣음 · {name} 뺌 | Added {name} · Removed {name} | {name}を追加 · {name}を削除 |
| hint* | 이동 · 고르기 · 닫기 · 넣기 · 빼기 · 마지막 칩 · 칩으로 | to move · to select · to close · add/remove · last chip · to chips | 移動 · 選択 · 閉じる · 追加・削除 · 最後のチップ · チップへ |
props
소스의 타입과 JSDoc 에서 생성한 표입니다. 컴포넌트를 고치면 pnpm gen 으로 다시 만듭니다.
Combobox
| prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| items | readonly T[] | — | 고정 목록. 서버에서 찾으면 onSearch(둘 중 하나) |
| onSearch | (query: string, signal: AbortSignal) => Promise<T[]> | — | 서버 검색. 입력이 멈추고 200ms 뒤 부른다. 앞 요청은 signal 로 끊고, 늦게 온 앞 응답은 버린다(마지막 응답만). 결과는 거르지 않고 그대로 보이며 맞은 글자만 칠한다. 거절되면 목록 안 오류 줄 + 다시 시도 |
| getLabel필수 | (item: T) => string | — | |
| getKey필수 | (item: T) => string | — | |
| renderItem | (item: T, state: { query: string; selected: boolean; active: boolean }) => ReactNode | — | 항목 그리기. 기본은 아이콘(getIcon) + 이름(하이라이트) + 설명 |
| getDescription | (item: T) => ReactNode | — | 둘째 줄(팀 · 이메일). 비활성 이유도 여기에 쓴다. 항목의 접근성 이름에 이어 붙는다 |
| getIcon | (item: T) => ReactNode | — | 이름 앞 아이콘 · 아바타. 항목 · 칩 · 하나 고른 값 앞에 함께 보인다(크기는 자리마다 CSS 가 맞춘다) |
| isDisabled | (item: T) => boolean | — | |
| groupBy | (item: T) => string | — | 그룹 머리. 처음 나온 순서대로 묶는다 |
| creatable | boolean | — | 친 글과 같은 이름이 없으면 마지막 줄에 ‘{query}’ 만들기 |
| onCreate | (query: string) => T | Promise<T> | — | 만든 값이 곧 고른 값이 된다 |
| emptyAction | ReactNode | — | 빈 결과 아래 다음 할 일(‘이메일로 초대’). Tab 으로 닿는다 |
| placeholder | string | — | |
| loading | boolean | — | 끝 스피너 + 목록 끝 ‘더 찾는 중’(300ms 뒤에 보인다). onSearch 를 쓰면 스스로 켠다 |
| error | string | null | — | 목록 안 오류 줄(role=alert) + ‘다시 시도’ |
| onRetry | () => void | — | 다시 시도. 없으면 onSearch 를 다시 부른다 |
| matchOptions | MatchOptions | — | 거르기 · 하이라이트 정규화. 기본: 한국어면 초성 검색, 일본어면 가나 접기 |
| defaultInputValue | string | — | 처음 칸에 든 검색 글자(비제어) |
| name | string | — | 폼 값. 고른 값의 getKey 를 hidden input 으로 낸다(여럿이면 같은 name 여러 개) |
| required | boolean | — | |
| disabled | boolean | — | |
| readOnly | boolean | — | 평평한 칩 · 값만 보이고 끝 단추가 없다. 목록이 열리지 않는다 |
| invalid | boolean | — | 오류 경계. aria-invalid 를 단다. 오류 문장은 Field 가 맡는다 |
| open | boolean | — | |
| defaultOpen | boolean | — | |
| onOpenChange | (open: boolean) => void | — | |
| hints | boolean | — | 목록 바닥의 키 안내(기본 true) |
| lang | "ko" | "en" | "ja" | — | |
| labels | Partial<ComboboxLabels> | — | |
| id | string | — | 칸(input) id. Field 의 control props 를 펼치면 들어온다 |
| aria-describedby | string | — | |
| aria-invalid | boolean | — | |
| aria-required | boolean | — | |
| aria-label | string | — | |
| aria-labelledby | string | — | |
| className | string | — | |
| multiple | false | true | — | |
| value | T | null | readonly T[] | — | |
| defaultValue | T | null | readonly T[] | — | |
| onValueChange | (value: T | null) => void | (value: T[]) => void | — | |
| collapseChips | boolean | — | 포커스가 떠나면 두 줄을 넘는 칩을 +N 으로 접는다 |
Highlight
| prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| text필수 | string | — | 원문 |
| query필수 | string | — | 친 글. 비었으면 원문 그대로 |
| options | MatchOptions | — | 거르기와 같은 정규화 선택(가나 접기 · 공백 · 초성). 목록을 거른 옵션과 같은 것을 넘긴다 |