Select
fandhe-frontend-headless-ui の select mod が提供するリストボックス選択の unstyled 部品です。Root / Label / Control / Trigger / ValueText / ClearTrigger / Indicator / Positioner / Content / ItemGroup / ItemGroupLabel / Item / ItemText / ItemIndicator / HiddenSelect / Separator / ScrollUpButton / ScrollDownButton の 18 anatomy パーツを持ち、listbox の開閉と選択値(高々 1 個)を合成した状態機械を備えます。HiddenSelect はフォーム統合専用のネイティブ <select> です。Separator/ScrollUpButton/ScrollDownButton(イシュー #2186 で追加)は state 引数を持たず、可視性判定・押下時の実スクロールを含みません(下記「参考サイトとの差分」節参照)。
/themes/ 側の Select(Chakra ライクな variant/size を持つスタイル層)と異なり、CSS を一切持ちません。
SelectProps(disabled/readonly/invalid/required)は root/label/control/trigger/value-text/clear-trigger/indicator/item-group へ一律で data-disabled/data-invalid/data-readonly を付与します(data-required は label のみに付与)。trigger はさらに props.disabled のときネイティブ disabled 存在属性を、clear-trigger は props.disabled のときネイティブ disabled を持ちます。hidden-select は props.disabled/props.required をそれぞれネイティブ disabled/required 存在属性へ反映します(readonly は <select readonly> が無効な HTML のため反映しません)。
trigger は選択なしのとき data-placeholder-shown を持ちます(value-text と同じ判定)。item は root disabled が伝播した有効値(props.disabled || 個別の disabled)で data-disabled/aria-disabled を出力し、選択中のみ data-selected を追加で持ちます。item-text は item と同じ 3 状態(選択有無・disabled・highlighted)を data-state/data-disabled/data-highlighted として持ちます。item-group-label は role="presentation" を、item-indicator は aria-hidden="true" を常時持ちます。
キーボード操作
| キー | 対象 | 効果 |
|---|---|---|
| Space / Enter(closed) | Trigger | listbox を開きます。選択済み項目(非 disabled)があれば初期 highlight をそこへ合わせ、無ければ先頭項目へ合わせます。 |
| ArrowDown(closed) | Trigger | listbox を開き、先頭の非 disabled 項目(選択済み項目があればそちらを優先)へ初期 highlight を合わせます。 |
| ArrowUp(closed) | Trigger | listbox を開き、末尾の非 disabled 項目(選択済み項目があればそちらを優先)へ初期 highlight を合わせます。 |
| ArrowDown / ArrowUp(open) | Content | highlight を次/前の非 disabled 項目へ移動します。data-loop-focus="true" オプトイン時のみ端で循環します。 |
| Space / Enter(open) | Content | highlight 中の項目を選択し、listbox を閉じます(closeOnSelect 既定)。 |
| Home / End(open) | Content | 先頭/末尾の非 disabled 項目へ highlight を移動します(WAI-ARIA APG の拡張)。 |
| A–Z 等(typeahead) | Trigger / Content | 先頭一致する項目へ highlight を移動します(350ms バッファ)。closed 時は開いてから移動します。 |
| Escape | Content | listbox を閉じます。 |
data-readonly が付いた trigger は上記のいずれのキーも no-op です(click も同様)。実 DOM 配線は fandhe-frontend-wasm-full の keynav(handle_menu_or_select_trigger_keydown/initial_highlight_index)が担いますが、JS なしでも hidden-select(ネイティブ <select>)がフォーム送信そのものは成立させます。
参考サイトとの差分
ark-ui 公式 Data Attributes / Keyboard Support 表(zag select.connect.ts)・Radix Primitives select と突合し、SelectProps の一律付与・data-placeholder-shown(trigger)・root disabled 伝播・data-selected・item-text の 3 状態属性・item-group-label の role="presentation"・item-indicator の aria-hidden・readonly ガードを是正しました。一方、以下は意図的に合わせていません。
data-stateのchecked/unchecked語彙: ark-ui は item にchecked/uncheckedを使いますが、本リポジトリではcrate::state::OpenStateの"open"/"closed"に一元化しています(combobox/listbox とのクレート横断整合を優先)。- Radix 固有の
Portal/Viewport: レイアウト計測・DOM 配置の関心のため headless へ持ち込みません(docs/policy/intentional-non-adoption.md§3.25 規則 2)。ScrollUpButton/ScrollDownButtonは anatomy 自体をイシュー #2186 で追加済みですが、可視性判定(スクロール可能かの計測)・押下時の実スクロールは同じ理由(規則 2 の配置)により headless に持ち込まずfandhe-frontend-wasm-fullの後続イシューへ委ねています。 Arrow: Select は arrow を持たない構成として確定済みです(positioning ADR §4.2)。Icon/Value(Radix):Indicator/ValueTextと同義のため個別追加しません。data-focus(DOM ローカル focus): SSR 静的出力に持たせません(combobox/listboxと同じ契約)。- trigger の combobox 化(
role="combobox"+aria-activedescendant): 現行 anatomy(素のbutton)を維持し、select-only combobox パターンは別部品(combobox)の責務とします。 - multiple 選択: 高々 1 個の選択のみを扱います。
スタイル済みの表示例は Select を参照してください。
関連 API: fandhe-frontend-headless-ui API
自前 CSS の最小例
Themes 版を使わず本部品を直接使う場合、[data-scope="select"][data-part="..."] セレクタでスタイルを当てます。以下は trigger の開閉・invalid・readonly・プレースホルダー表示、highlight 中の item の見た目の最小例です。
[data-scope="select"][data-part="trigger"] {
border: 1px solid #ccc;
border-radius: 6px;
padding: 0.5rem 0.75rem;
}
[data-scope="select"][data-part="trigger"][data-state="open"] {
border-color: #6366f1;
}
[data-scope="select"][data-part="trigger"][data-placeholder-shown] {
color: #6b7280;
}
[data-scope="select"][data-part="trigger"][data-invalid] {
border-color: #dc2626;
}
[data-scope="select"][data-part="trigger"][data-readonly] {
background: #f3f4f6;
cursor: default;
}
[data-scope="select"][data-part="item"][data-highlighted] {
background: #eef2ff;
}Demo
枠線・余白は docs サイト側のデモ枠であり、fandhe-frontend-headless-ui 自体はスタイルを持ちません。
Features
- Root / Label / Control / Trigger / ValueText / ClearTrigger / Indicator / Positioner / Content / ItemGroup / ItemGroupLabel / Item / ItemText / ItemIndicator / HiddenSelect / Separator / ScrollUpButton / ScrollDownButton の 18 anatomy パーツを提供する(前 15 者は ark-ui と完全一致、イシュー #1619 参照突合で追加・削除なし。後 3 者はイシュー #2186 で追加)。
- listbox の開閉(
Disclosure)+ 選択値(SingleSelect、高々 1 個)を合成した状態機械を持つ。data-state値語彙はcrate::state::OpenStateの"open"/"closed"に一元化し、選択有無の表現にも同じ語彙を再利用する(ark-ui のchecked/uncheckedは不採用)。 SelectProps(disabled/readonly/invalid/required)を root/label/control/trigger/value-text/clear-trigger/indicator/item-group へ一律付与する(イシュー #1619。labelにのみdata-requiredを追加)。呼び出し側attrsに同名キーが混入していても fail-closed で除去する。triggerはdata-placeholder-shownを持つ(ark-ui/Radix 双方が trigger に持つ属性)。itemは root disabled の伝播(props.disabled || disabled)と、選択時のみ付与するdata-selectedを持つ。item_textはitemと同じ 3 状態属性(data-state/data-disabled/data-highlighted)を持つ。hidden_selectはフォーム統合専用のネイティブ<select>でありaria-hidden="true"+tabindex="-1"を固定付与して視覚 UI(trigger/content)との二重露出を防ぐ。未選択時は非表示 placeholder option を自動挿入し、ブラウザの「先頭 option 自動選択」による誤送信を防ぐ。props.requiredはネイティブrequiredへ反映する(readonlyは<select readonly>が無効な HTML のため非採用)。- 位置決め(
positionerのstyle/data-side/data-align)はcrate::positioning(#590)が算出した値を呼び出し側が渡す。Select は arrow を持たない。 - highlight 移動・typeahead・キーボードナビゲーション自体は CSR 挙動層のスコープであり、本モジュールは
item(highlighted)/content(activedescendant)の SSR 静的表現のみを提供する。 separator(div+aria-hidden="true")・scroll_up_button/scroll_down_button(div+aria-hidden="true")の 3 パーツをイシュー #2186 で追加した。separatorは当初role="separator"+aria-orientation="horizontal"を固定付与していたが、親content(role="listbox")はoption/groupのみを子に持てるaria-required-children制約に違反するため、RadixSelectSeparatorと同型のaria-hiddenのみへ改めた(Bugbot 指摘対応)。可視性判定(スクロール可能かの計測)・押下時の実スクロールはいずれも本モジュールの責務外でfandhe-frontend-wasm-fullの後続イシューが担う(SSR/静的描画ではstyle/hidden/寸法系属性を一切出力せず常時描画する)。- 意図的に非追随:
data-stateのchecked/unchecked化・Radix 固有の Portal/Viewport(レイアウト計測の関心、headless 層へ持ち込まない)・Arrow(Select は arrow を持たない)・data-focus(DOM ローカル focus)・data-placement/data-side(crate::positioning経由で既に提供)。
Anatomy
root
label
control
trigger
value-text
indicator
clear-trigger
positioner
content
scroll-up-button
item-group
item-group-label
item
item-text
item-indicator
separator
scroll-down-button
hidden-selectAPI Reference
Arguments
| Name | Type | Default | Description |
|---|---|---|---|
root/control/trigger/indicator/positioner/content/item/item_text/item_indicator(state) | OpenState | OpenState::Closed | 開閉(または選択有無)状態。data-state へ反映する。 |
root/label/control/trigger/value_text/clear_trigger/indicator/item_group(props) | &SelectProps | &SelectProps::default() | disabled/readonly/invalid/required の状態束(イシュー #1619)。 |
label/item_group_label(id) | Option<&str> | — | Some のとき id 属性を出力し、対応する labelledby/labelled_by 引数の関連付け先になる。 |
trigger(placeholder_shown) | bool | — | true のとき data-placeholder-shown を付与する(未選択時のプレースホルダー表示、イシュー #1619)。 |
trigger(controls) | Option<&str> | — | Some のとき aria-controls で content の id と関連付ける。 |
trigger(labelledby) | Option<&str> | — | Some のとき aria-labelledby で label と関連付ける。 |
value_text(placeholder_shown) | bool | — | true のとき data-placeholder-shown 存在属性を付与する(未選択時のプレースホルダー表示)。 |
content(id) | Option<&str> | — | trigger(controls) の参照先 id。 |
content(labelledby) | Option<&str> | — | Some のとき aria-labelledby を付与する。 |
content(activedescendant) | Option<&str> | — | Some のとき aria-activedescendant を付与する(現在ハイライト中の item の id を指す。移動自体は CSR 挙動層の責務)。 |
item_group(labelledby) | Option<&str> | — | Some のときのみ role="group" + aria-labelledby を対で付与する(名前なし group を作らない)。 |
item(disabled) | bool | — | props.disabled || disabled(root disabled 伝播)が true のとき aria-disabled="true" + data-disabled を対で付与する(div[role="option"] はネイティブ disabled を持たない)。 |
item(highlighted) | bool | — | data-highlighted へ反映する(キーボードナビゲーション等によるフォーカス位置の SSR 静的表現)。 |
item(value) | &str | — | 選択肢の値。data-value として動的値のまま出力する。選択中は data-selected も付与する(イシュー #1619)。 |
item(id) | Option<&str> | — | Some のとき、content(activedescendant) の参照先識別子になる。 |
item_text(selected_state, props, disabled, highlighted) | OpenState, &SelectProps, bool, bool | — | item の先頭 4 引数と同型。data-state/data-disabled(root disabled 伝播込み)/data-highlighted の 3 状態属性を出力する(イシュー #1619)。 |
hidden_select(selected) | Option<&str> | — | 現在選択中の値。None のとき非表示 placeholder option を先頭へ自動挿入する。 |
hidden_select(name) | Option<&str> | — | Some のとき name 属性を出力する。 |
hidden_select(props) | &SelectProps | — | props.disabled/props.required をそれぞれネイティブ disabled/required 存在属性へ反映する(イシュー #1619)。 |
hidden_select(options) | Vec<(&str, &str)> | — | (value, label) の列。各要素を <option> として組み立てる。 |
separator/scroll_up_button/scroll_down_button(attrs, children) | Vec<(&str, &str)>, Vec<Node> | — | 3 パーツとも state 引数を持たない(SSR は可視性を判定できないため常時描画する、イシュー #2186)。 |
Data Attributes
| Part | Attribute | Observed Values |
|---|---|---|
clear-trigger | data-disabled | (値なし) |
content | data-state | open |
control | data-disabled | (値なし) |
control | data-invalid | (値なし) |
control | data-readonly | (値なし) |
control | data-state | closed, open |
indicator | data-disabled | (値なし) |
indicator | data-invalid | (値なし) |
indicator | data-readonly | (値なし) |
indicator | data-state | closed, open |
item | data-highlighted | (値なし) |
item | data-selected | (値なし) |
item | data-state | closed, open |
item | data-value | apple, banana |
item-indicator | data-state | open |
item-text | data-highlighted | (値なし) |
item-text | data-state | closed, open |
label | data-disabled | (値なし) |
label | data-invalid | (値なし) |
label | data-readonly | (値なし) |
label | data-required | (値なし) |
positioner | data-state | open |
root | data-disabled | (値なし) |
root | data-invalid | (値なし) |
root | data-readonly | (値なし) |
root | data-state | closed, open |
trigger | data-disabled | (値なし) |
trigger | data-invalid | (値なし) |
trigger | data-placeholder-shown | (値なし) |
trigger | data-readonly | (値なし) |
trigger | data-state | closed, open |
value-text | data-bind-text | select-value-text |
value-text | data-disabled | (値なし) |
value-text | data-invalid | (値なし) |
value-text | data-placeholder-shown | (値なし) |
Examples
Disabled trigger, unselected
SelectProps { disabled: true, .. } + hidden_select(selected: None) の未選択・無効化状態。非表示 placeholder option が自動挿入される。
利用者スタイルの当て方
[data-scope="select"][data-part="trigger"][data-state="open"]・[data-part="item"][data-highlighted]・[data-invalid]・[data-readonly]・[data-placeholder-shown] を用いた自前 CSS の最小例。headless-ui 自体はスタイルを持たない。
利用者が data-scope / data-part / data-state / data-invalid / data-readonly / data-placeholder-shown / data-highlighted 属性セレクタで自前 CSS を当てる最小例です。headless-ui 自体はスタイルを持ちません。
[data-scope="select"][data-part="trigger"] {
border: 1px solid #ccc;
border-radius: 6px;
padding: 0.5rem 0.75rem;
}
[data-scope="select"][data-part="trigger"][data-state="open"] {
border-color: #6366f1;
}
[data-scope="select"][data-part="trigger"][data-placeholder-shown] {
color: #6b7280;
}
[data-scope="select"][data-part="trigger"][data-invalid] {
border-color: #dc2626;
}
[data-scope="select"][data-part="trigger"][data-readonly] {
background: #f3f4f6;
cursor: default;
}
[data-scope="select"][data-part="item"][data-highlighted] {
background: #eef2ff;
}
Accessibility
Keyboard Interactions
| Key | Description |
|---|---|
| Space / Enter (closed) | listbox を開く。選択済み項目(aria-selected="true" の非 disabled 項目)があれば初期 highlight をそこへ合わせ、無ければ先頭(Enter/Space)/末尾(ArrowUp)の非 disabled 項目へフォールバックする(ark-ui/Radix 準拠、イシュー #1619。実装は fandhe-frontend-wasm-full の keynav.rs::handle_menu_or_select_trigger_keydown/initial_highlight_index)。 |
| ArrowDown / ArrowUp (closed) | listbox を開く(初期 highlight の規則は Space/Enter と同じ)。 |
| ArrowDown / ArrowUp (open) | highlight を次/前の非 disabled 項目へ移動する。data-loop-focus="true" オプトイン時のみ端で循環する。 |
| Space / Enter (open) | highlight 中の項目を選択し、listbox を閉じる(closeOnSelect 既定 true)。 |
| Home / End (open) | 先頭/末尾の非 disabled 項目へ highlight を移動する(WAI-ARIA APG 拡張)。 |
| A–Z(typeahead) | 先頭一致する項目へ highlight を移動する(350ms バッファ)。 |
| Escape | listbox を閉じる。 |
readonly(data-readonly) | trigger の keydown(開閉・選択いずれも)を no-op にする(combobox の readonly 是正と同型、イシュー #1619)。click 経路も同様に no-op。 |
WAI-ARIA
| Attribute | Description |
|---|---|
| aria-haspopup="listbox" (trigger) | 固定付与。 |
| aria-expanded (trigger) | state.is_open() を反映する。 |
| role="listbox" (content) | 固定付与。aria-activedescendant の配線先(trigger は素の button のため付与しない)。 |
| role="option" / aria-selected (item) | 固定付与 + selected_state.is_open() を反映する。 |
| role="presentation" (item_group_label) | zag の ItemGroupLabel anatomy に合わせて固定付与する(イシュー #1619)。 |
| aria-hidden="true" (item_indicator) | 装飾アイコンであり item 自身の aria-selected が選択状態を伝達するため、支援技術の二重読み上げを防ぐ(イシュー #1619)。 |
| aria-disabled (item) | props.disabled || disabled(root disabled 伝播)が true のとき付与する(イシュー #1619)。 |
| aria-hidden="true" / tabindex="-1" (hidden_select) | 視覚 UI(trigger/content)との二重露出・二重フォーカスを防ぐため固定付与する。 |
| aria-hidden="true" (separator) | 固定付与(イシュー #2186。content は listbox の aria-required-children〔option/group のみ子に持てる〕制約があるため role/aria-orientation は持たず、Radix SelectSeparator と同型の aria-hidden のみへ統一する、Bugbot 指摘対応)。 |
| aria-hidden="true" (scroll_up_button / scroll_down_button) | 固定付与(イシュー #2186)。装飾要素であり可視性判定・実スクロールを持たないため支援技術から隠す。 |