select —— 下拉選擇器
このコンテンツはまだ日本語訳がありません。
下拉選擇器是「觸發器 + 浮層清單」模式的最簡實作。core/select 管選項查找、確定性 option id 生成、以及 APG combobox 模式的三層無障礙屬性包。定位計算不在這裡——浮層的座標由 core/shared/popper 負責(見 popper)。
- 各端實作
LhSelect。 - 單選場景;多選用級聯或 checkbox-group。
- 不該用它:選項需要樹狀層級(用
cascader)、選項超過幾百個(考慮virtual-list虛擬化)。
import { findSelectOption, getNextEnabledIndex, getSelectOptionId, getSelectTriggerAria, getSelectListboxAria, getSelectOptionAria,} from '@lh-ui/share/core/select'import type { LhSelectOption, LhSelectState } from '@lh-ui/share/core/select'getSelectOptionId(listboxId, index) 的「確定性」是 APG combobox 模式能工作的前提:觸發器用 aria-activedescendant 參照目前高亮項,兩者必須指向同一個 DOM id。與其讓 UI 層隨手生成 id 再想辦法同步,不如在 core 層規定 ${listboxId}-option-${index},兩端都按這個規則取值。
它的守衛同樣重要:listboxId 為空、index 非整數、或 index < 0(-1 表示無高亮)時傳回 undefined——undefined 的屬性會被框架 v-bind 忽略,不會產生指向不存在元素的 aria-activedescendant。
| 函式 | 簽名 | 傳回 | 作用 |
|---|---|---|---|
findSelectOption |
(options: LhSelectOption[], value: unknown) => LhSelectOption | undefined |
選項 | 用 Object.is 精確比對 value |
getNextEnabledIndex |
(options: LhSelectOption[], currentIndex: number, direction: 1 | -1) => number |
下標 | @deprecated,轉調 shared/keyboard.getNextListIndex |
getSelectOptionId |
(listboxId: string | undefined, index: number) => string | undefined |
id | 確定性拼接,非法輸入傳回 undefined |
getSelectTriggerAria |
(options: { visible: boolean; listboxId?: string; highlightedIndex: number }) => LhAriaAttrs |
aria 屬性包 | combobox 觸發器 |
getSelectListboxAria |
(id?: string) => LhAriaAttrs |
aria 屬性包 | listbox 容器 |
getSelectOptionAria |
(options: { listboxId?: string; index: number; selected: boolean; disabled?: boolean }) => LhAriaAttrs |
aria 屬性包 | 單個選項 |
export interface LhSelectOption { label: string value: unknown disabled?: boolean}
export interface LhSelectState { options: LhSelectOption[] highlightedIndex: number}三層屬性包構成完整的 combobox 模式:
// 觸發器:aria-haspopup="listbox" + aria-expanded + aria-controlsgetSelectTriggerAria({ visible: true, listboxId: 'my-listbox', highlightedIndex: 2 })// { role: 'combobox', 'aria-expanded': true, 'aria-controls': 'my-listbox',// 'aria-haspopup': 'listbox', 'aria-activedescendant': 'my-listbox-option-2' }
// 浮層:role="listbox" + id(與觸發器 aria-controls 對應)getSelectListboxAria('my-listbox') // { role: 'listbox', id: 'my-listbox' }
// 選項:role="option" + id + aria-selectedgetSelectOptionAria({ listboxId: 'my-listbox', index: 2, selected: true })// { role: 'option', id: 'my-listbox-option-2', 'aria-selected': true }鍵盤互動的完整閉環(UI 層實施,core 提供演算法):ArrowDown/Up 移動 highlightedIndex(用 shared/keyboard.getNextListIndex,首尾循環、跳過 disabled),Enter 選取,Esc 關閉。視覺高亮必須與 highlightedIndex 同源,否則會出現「aria 指向 A、眼睛看到 B」。
import { findSelectOption, getSelectOptionId, getSelectTriggerAria } from '@lh-ui/share/core/select'
const options = [ { label: '青花瓷', value: 'cerblue' }, { label: '水墨', value: 'shuimo', disabled: true },]findSelectOption(options, 'cerblue') // { label: '青花瓷', value: 'cerblue' }findSelectOption(options, 'nope') // undefinedgetSelectOptionId('lb', 0) // 'lb-option-0'getSelectOptionId('lb', -1) // undefined(無高亮)getSelectOptionId(undefined, 0) // undefined<!-- Vue3 --><div :class="['lh-select', `lh-select--${size}`, { 'lh-select--open': visible, 'lh-select--disabled': disabled }]"> <button class="lh-select__trigger" v-bind="getSelectTriggerAria({ visible, listboxId, highlightedIndex })" @click="toggle" > <span class="lh-select__value" :class="{ 'lh-select__value--placeholder': !selected }"> {{ selected ? selected.label : placeholder }} </span> <LhIcon class="lh-select__arrow" name="more" :size="12" /> </button> <div v-if="visible" :id="listboxId" class="lh-select__dropdown" v-bind="getSelectListboxAria(listboxId)"> <div v-for="(opt, i) in options" :key="i" class="lh-select__option" :class="{ 'lh-select__option--highlighted': i === highlightedIndex, 'lh-select__option--selected': i === selectedIdx, 'lh-select__option--disabled': opt.disabled }" v-bind="getSelectOptionAria({ listboxId, index: i, selected: i === selectedIdx, disabled: opt.disabled })" @click="choose(opt)" >{{ opt.label }}</div> </div></div>// React:visible 狀態 + useRef 持有 listboxId(用 useId 生成穩定 id)// uniapp:浮層用 view 絕對定位;aria-activedescendant 在非 h5 端不支援,// 鍵盤導航降級為只移動焦點(focus 項),讀屏靠 focus 事件播報| 類別名 | 語義 |
|---|---|
.lh-select |
基礎類 |
.lh-select--{small|large} / --disabled / --open |
狀態修飾 |
.lh-select__trigger |
觸發器按鈕 |
.lh-select__value / --placeholder |
顯示值 / 佔位態 |
.lh-select__arrow / __clear / __actions |
箭頭 / 清除 / 操作區 |
.lh-select__dropdown / --teleported |
浮層(teleported 表示傳送到 body) |
.lh-select__option / --highlighted / --selected / --disabled |
選項四態 |
.lh-select__empty |
無選項佔位 |
.lh-select-fade-* |
浮層出入過渡(Vue3 類別名,Vue2 端用 mixin 對齊) |
Object.is而非===:findSelectOption用Object.is,能正確處理NaN與-0這類邊界(雖然實際選項值很少用到)。getNextEnabledIndex已廢棄:實作已收斂到shared/keyboard.getNextListIndex,本函式僅轉調,保留舊簽名。新程式碼直接引核心。- id 穩定性:
listboxId在元件生命週期內不能變(否則aria-activedescendant指向舊 id),用各端的穩定 id 生成。 - 傳送門(teleport):浮層傳送到 body 時,
aria-controls仍然有效(同文件內),但焦點管理要小心——選項點選後焦點應回到觸發器。
@lh-ui/share/core/select → findSelectOption( )返回值
(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue