Tags Input
fandhe-frontend-headless-ui の tags_input mod が提供するタグ配列入力の unstyled 部品です。Root / Label / Control / Input / Item / ItemPreview / ItemText / ItemInput / ItemDeleteTrigger / ClearTrigger / HiddenInput / LiveRegion の 12 anatomy パーツを持ち、タグ文字列の可変長リスト + 重複拒否 + 上限 + 編集中/キーボード強調インデックスを表現する値状態機械を備えます。重複・カンマ含有・空文字列のタグはすべての入口で拒否されます。LiveRegion はタグ数の変化を aria-live="polite" で支援技術へ通知します(テキスト更新の実配線は wasm-full の後続責務)。
/themes/ 側の TagsInput(Chakra ライクな variant/size を持つスタイル層)と異なり、CSS を一切持ちません。
TagsInputProps(disabled/readonly/invalid/required)は root/label/control/input/clear-trigger/hidden-input へ data-disabled/data-invalid/data-readonly を、label にはさらに data-required を反映します。invalid: true のとき input へ aria-invalid="true"(valid のときは属性自体を省略)、readonly: true のとき input へネイティブ readonly 存在属性を付与します。タグ 1 個分の状態は TagItem(value/disabled/editing/highlighted)で表し、item/item_preview に data-value(タグ文字列)、item_preview/item_text/item_delete_trigger に data-highlighted を反映します。編集モードでは item_preview が hidden、item_input が表示されます。
キーボード操作
| キー | 対象 | 効果 |
|---|---|---|
| ArrowLeft | Control | キーボード強調(highlight)を 1 つ前へ移動します(dispatch: "highlight-prev")。強調なしなら末尾タグへ、先頭タグでそれ以上前進しません。 |
| ArrowRight | Control | 強調を 1 つ後ろへ移動します(dispatch: "highlight-next")。末尾タグ強調中は強調解除(入力欄へ戻る)。強調なしは no-op です。 |
| Backspace | Input/Control | 強調中ならそのタグを削除、なければ末尾タグを削除します(dispatch: "backspace")。 |
| Delete | Control | 強調中のタグを削除します(dispatch: "delete-highlighted")。 |
| Enter | Control/ItemPreview | 強調中なら編集開始("edit-start")、入力欄にフォーカスがあれば入力中の値を追加("add")します。振り分けはクライアント側の責務です。 |
| Escape | Control | 強調を解除します(dispatch: "highlight-clear")、または編集中なら編集を破棄します("edit-cancel")。 |
上記キーの実際の DOM 配線(keydown/paste イベントハンドラ、fandhe-frontend-wasm-full)は本部品のスコープ外です。本部品は SSR 静的マークアップと dispatch 契約のみを提供し、キー入力を検出して自動的に dispatch する配線は未実装です。
参考サイトとの差分
Zag.js tags-input.connect.ts(ark-ui 基盤)・ark-ui 公式 Data Attributes / Keyboard Support 表と突合し、data-*/TagsInputProps/TagItem・上記のキーボード操作の dispatch 契約へ揃えました。一方、以下は意図的に合わせていません。
role="listbox"(control)/role="option"(item_preview)は撤去しました。zag/ark の control・item-preview はいずれもroleを持たず、role="listbox"の許容子はoption/groupのみだがcontrolは<input type="text">を内包しており content model 違反だったため是正しました。アクセシブルネームはlabelのfor関連付けが担います。data-focus(root/control、DOM フォーカスという ephemeral な状態)は SSR では決定できないため不採用です。data-empty(root/input のタグ 0 件)は自由関数rootがタグ列を保持しないため未実装です(別 issue 候補)。readonlyの DOM 表現は zag(controlのtabindex="0"維持 +inputへネイティブdisabled)と異なり、inputへネイティブreadonlyを採用します(pin-input/password-input/file-uploadの直近精査と同じ規約へのリポ内一貫性を優先した判断)。maxLength(タグ文字数上限)・validateコールバック・delimiter/addOnPaste/blurBehavior/allowOverflow/autoFocus/dirはアプリロジック・クライアント配線の関心のため対象外です(docs/policy/intentional-non-adoption.md§3.25)。
スタイル済みの表示例は Tags Input を参照してください。
関連 API: fandhe-frontend-headless-ui API
自前 CSS の最小例
Themes 版を使わず本部品を直接使う場合、[data-scope="tags-input"][data-part="..."] セレクタでスタイルを当てます。以下は control の枠線・折り返し、強調タグの outline、invalid 時の枠線、readonly 時の背景の最小例です。
[data-scope="tags-input"][data-part="control"] {
display: inline-flex;
flex-wrap: wrap;
gap: 0.25rem;
border: 1px solid #ccc;
padding: 0.25rem;
}
[data-scope="tags-input"][data-part="item-preview"][data-highlighted] {
outline: 2px solid #2563eb;
}
[data-scope="tags-input"][data-part="input"][data-invalid] {
border-color: #dc2626;
}
[data-scope="tags-input"][data-part="control"][data-readonly] {
background: #f3f4f6;
}Demo
枠線・余白は docs サイト側のデモ枠であり、fandhe-frontend-headless-ui 自体はスタイルを持ちません。
Features
- Root / Label / Control / Input / Item / ItemPreview / ItemText / ItemInput / ItemDeleteTrigger / ClearTrigger / HiddenInput / LiveRegion の 12 anatomy パーツと、タグ文字列の可変長リスト + 重複拒否 + 上限 + 編集中/キーボード強調インデックスを持つ値状態機械を提供する。
- 不変条件「重複タグなし・
len() <= max・カンマを含まない・空文字列を含まない」を破る入力は dispatch・hydration・コンストラクタのすべての入口で一貫して拒否する(カンマ・空文字列はフォーム送信値のカンマ結合時に曖昧さを生むため、Cursor Bugbot 指摘を踏まえ全入口で禁止)。 TagsInputProps(disabled/readonly/invalid/required)が root/label/control/input/clear_trigger/hidden_input のdata-*を一元管理する。control/item_previewはroleを持たない(イシュー #1623 で zag/ark 準拠へ是正、旧実装のrole="listbox"/role="option"は撤去。アクセシブルネームはlabelのfor関連付けが担う)。TagItem(value/disabled/editing/highlighted)が item 系 5 パーツ共通の状態束。item/item_previewにdata-value(タグ文字列)を出力する。editing/highlighted(編集中インデックス・キーボード強調インデックス)はいずれも ephemeral な DOM 状態のため hydration では運ばない。live_regionはタグ数の変化を通知する live region(role="status"+aria-live="polite"固定、root直下を推奨配置とする。テキスト更新の実配線はfandhe-frontend-wasm-fullの後続責務、イシュー #1069)。
Anatomy
root
label
control
item
item-preview
item-text
item-delete-trigger
item-input
input
clear-trigger
hidden-input
live-regionAPI Reference
Arguments
| Name | Type | Default | Description |
|---|---|---|---|
TagsInputProps { disabled, readonly, invalid, required } | struct | — | root/label/control/input/clear_trigger/hidden_input へ渡す共通状態束。 |
TagItem { value, disabled, editing, highlighted } | struct | — | item/item_preview/item_text/item_input/item_delete_trigger へ渡す共通状態束。 |
item/item_preview(item) | &TagItem | — | data-value/data-disabled/data-editing(item のみ)/data-highlighted(item_preview のみ)へ反映する。item_preview は編集中に hidden を出力する。 |
item_input(item, value) | &TagItem, &str | — | 編集中の暫定値。非編集時は hidden 存在属性のみを出力する(矢印キー操作・編集開始配線が未実装のため tabindex は固定付与せず既定の Tab 到達性を維持する)。 |
item_delete_trigger(item) | &TagItem | — | aria-label を "Delete tag {value}"(zag 既定訳)として動的生成する。tabindex は固定付与しない(矢印キー操作が未実装のため既定の Tab 到達性を維持する)。 |
input(props, value, at_max) | &TagsInputProps, &str, bool | — | 新規タグ入力用のネイティブ入力欄。at_max は data-invalid + aria-invalid="true" を出力する。autocomplete="off"/autocorrect="off"/autocapitalize="none"/enterkeyhint="done" を固定付与する。 |
hidden_input(props, name, value) | &TagsInputProps, &str, &str | — | フォーム送信名と、全タグのカンマ結合値。 |
live_region(children) | Vec<Node> | — | role="status"/aria-live="polite"/aria-atomic="true" を固定付与する live region。通知文言は children として呼び出し側が渡す(イシュー #1069)。 |
Data Attributes
| Part | Attribute | Observed Values |
|---|---|---|
control | data-invalid | (値なし) |
control | data-readonly | (値なし) |
hidden-input | data-invalid | (値なし) |
hidden-input | data-readonly | (値なし) |
input | data-empty | (値なし) |
input | data-invalid | (値なし) |
input | data-readonly | (値なし) |
item | data-editing | (値なし) |
item | data-value | a, rust, staging, wasm |
item-delete-trigger | data-highlighted | (値なし) |
item-preview | data-highlighted | (値なし) |
item-preview | data-value | a, rust, staging, wasm |
item-text | data-highlighted | (値なし) |
label | data-invalid | (値なし) |
label | data-readonly | (値なし) |
root | data-invalid | (値なし) |
root | data-readonly | (値なし) |
Examples
At max (invalid input)
上限に達した状態の input(props.invalid: true, at_max: true)。data-invalid + aria-invalid="true" が新規タグ入力欄に出力される。
Read-only
TagsInputProps::readonly を反映した状態。input にネイティブ readonly + data-readonly が出力される。
自前 CSS の最小例
headless-ui はスタイルレスであるため、[data-scope="tags-input"][data-part="..."] セレクタで CSS を当てるのは利用者の責務である(このページには適用しない)。
[data-scope="tags-input"][data-part="control"] {
display: inline-flex;
flex-wrap: wrap;
gap: 0.25rem;
border: 1px solid #ccc;
padding: 0.25rem;
}
[data-scope="tags-input"][data-part="item-preview"][data-highlighted] {
outline: 2px solid #2563eb;
}
[data-scope="tags-input"][data-part="input"][data-invalid] {
border-color: #dc2626;
}
[data-scope="tags-input"][data-part="control"][data-readonly] {
background: #f3f4f6;
}Accessibility
Keyboard Interactions
| Key | Description |
|---|---|
| ArrowLeft | 強調(highlight)を 1 つ前へ移動する("highlight-prev" dispatch)。**DOM 配線は wasm-full の別イシュー**(本モジュールは dispatch を受けた際の状態遷移のみを提供する契約、まだ keydown ハンドラは実装されていない)。 |
| ArrowRight | 強調を 1 つ後ろへ移動する("highlight-next" dispatch)。末尾タグ強調中なら入力欄へ戻る。DOM 配線は wasm-full の別イシュー(同上)。 |
| Backspace | 強調中ならそのタグを削除、なければ末尾タグを削除する("backspace" dispatch)。DOM 配線は wasm-full の別イシュー(同上)。 |
| Delete | 強調中のタグを削除する("delete-highlighted" dispatch)。DOM 配線は wasm-full の別イシュー(同上)。 |
| Enter | 強調中なら編集開始("edit-start")、そうでなければ入力欄の値を追加("add")。振り分けはクライアント側の責務。DOM 配線は wasm-full の別イシュー(同上)。 |
| Escape | 強調を解除する("highlight-clear" dispatch)。DOM 配線は wasm-full の別イシュー(同上)。 |
WAI-ARIA
| Attribute | Description |
|---|---|
| aria-invalid (input) | 上限到達(at_max)または props.invalid のときのみ "true" を出力する。 |
| aria-label (item_delete_trigger) | "Delete tag {value}"(zag 既定訳)を動的生成する(render() の既定エスケープ経由)。 |
| role="status" / aria-live="polite" / aria-atomic="true" (live_region) | タグ数の変化を通知する live region に固定付与する(イシュー #1069)。 |