Skip to content
fandhe-frontendcore v0.4.3
GitHub

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)Triggerlistbox を開きます。選択済み項目(非 disabled)があれば初期 highlight をそこへ合わせ、無ければ先頭項目へ合わせます。
ArrowDown(closed)Triggerlistbox を開き、先頭の非 disabled 項目(選択済み項目があればそちらを優先)へ初期 highlight を合わせます。
ArrowUp(closed)Triggerlistbox を開き、末尾の非 disabled 項目(選択済み項目があればそちらを優先)へ初期 highlight を合わせます。
ArrowDown / ArrowUp(open)Contenthighlight を次/前の非 disabled 項目へ移動します。data-loop-focus="true" オプトイン時のみ端で循環します。
Space / Enter(open)Contenthighlight 中の項目を選択し、listbox を閉じます(closeOnSelect 既定)。
Home / End(open)Content先頭/末尾の非 disabled 項目へ highlight を移動します(WAI-ARIA APG の拡張)。
A–Z 等(typeahead)Trigger / Content先頭一致する項目へ highlight を移動します(350ms バッファ)。closed 時は開いてから移動します。
EscapeContentlistbox を閉じます。

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 の見た目の最小例です。

CSS
[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 自体はスタイルを持ちません。

Apple
Banana

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 制約に違反するため、Radix SelectSeparator と同型の 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-select

API Reference

Arguments

NameTypeDefaultDescription
root/control/trigger/indicator/positioner/content/item/item_text/item_indicator(state)OpenStateOpenState::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

PartAttributeObserved Values
clear-triggerdata-disabled(値なし)
contentdata-stateopen
controldata-disabled(値なし)
controldata-invalid(値なし)
controldata-readonly(値なし)
controldata-stateclosed, open
indicatordata-disabled(値なし)
indicatordata-invalid(値なし)
indicatordata-readonly(値なし)
indicatordata-stateclosed, open
itemdata-highlighted(値なし)
itemdata-selected(値なし)
itemdata-stateclosed, open
itemdata-valueapple, banana
item-indicatordata-stateopen
item-textdata-highlighted(値なし)
item-textdata-stateclosed, open
labeldata-disabled(値なし)
labeldata-invalid(値なし)
labeldata-readonly(値なし)
labeldata-required(値なし)
positionerdata-stateopen
rootdata-disabled(値なし)
rootdata-invalid(値なし)
rootdata-readonly(値なし)
rootdata-stateclosed, open
triggerdata-disabled(値なし)
triggerdata-invalid(値なし)
triggerdata-placeholder-shown(値なし)
triggerdata-readonly(値なし)
triggerdata-stateclosed, open
value-textdata-bind-textselect-value-text
value-textdata-disabled(値なし)
value-textdata-invalid(値なし)
value-textdata-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 自体はスタイルを持ちません。

CSS
[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

KeyDescription
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 バッファ)。
Escapelistbox を閉じる。
readonly(data-readonly)trigger の keydown(開閉・選択いずれも)を no-op にする(combobox の readonly 是正と同型、イシュー #1619)。click 経路も同様に no-op。

WAI-ARIA

AttributeDescription
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)。装飾要素であり可視性判定・実スクロールを持たないため支援技術から隠す。