segmented —— 分段控制器
Ce contenu n’est pas encore disponible dans votre langue.
分段控制器是緊湊版單選:把選項平鋪成一帶高亮的按鈕群組,選取項用一個滑動塊標注。相比 radio 群組它省垂直空間、視覺更現代;相比 select 它把所有選項暴露出來,少一次點選。core/segmented 管的是選項的兩種寫法歸一、整組停用判定、鍵盤導覽、以及 APG radiogroup 語義。
- 各端實作
LhSegmented:檢視切換(清單/網格/卡片)、時間範圍(日/週/月)、緊湊型列舉設定。 - 選項數少(2~5)且互斥的場景。
- 不該用它:選項超過 6 個(換 select 或 radio 群組);多選(用 checkbox 群組);選項文案很長(會換行很難看)。
import { normalizeSegmentedOptions, isSegmentedDisabled, getSegmentedClassNames, moveSegmentedIndex, getSegmentedAria, getSegmentedItemAria,} from '@lh-ui/share/core/segmented'import type { LhSegmentedInput, LhSegmentedNormalized, LhSegmentedState,} from '@lh-ui/share/core/segmented'normalizeSegmentedOptions 接受兩種寫法——['日', '周', '月'] 或 [{ label: '日', value: 'day' }]——歸一為統一形態。這是「輸入寬容、內部嚴格」的取捨:UI 庫的 props 越寬容,使用者的心智負擔越低;內部統一成 LhSegmentedNormalized 後,後續的停用判定、導覽、渲染都只面對一種結構。
isSegmentedDisabled(options, disabled) 做的是兩級停用的合併:組級 disabled 或任一選項自身 disabled 都會讓整組不可用——因為分段控制器的選取塊是連動滑動的,存在「半停用」群組會讓滑塊卡住。
moveSegmentedIndex 委託給共享核心的 getNextListIndex(core/shared/keyboard),跳過停用項做循環導覽,空陣列傳回 current。
| 函式 | 簽名 | 傳回 | 作用 |
|---|---|---|---|
normalizeSegmentedOptions |
(options: LhSegmentedInput[]) => LhSegmentedNormalized[] |
規範化選項 | 字串快捷寫法歸一 |
isSegmentedDisabled |
(options: LhSegmentedNormalized[], disabled?: boolean) => boolean |
是否停用 | 組級 + 選項級合併 |
getSegmentedClassNames |
(state: LhSegmentedState) => string[] |
類別名 | disabled/block |
moveSegmentedIndex |
(options, current: number, direction: 1 | -1) => number |
目標索引 | 跳過停用項循環 |
getSegmentedAria |
(options?: { disabled?; label? }) => LhAriaAttrs |
aria 屬性包 | radiogroup 語義 |
getSegmentedItemAria |
(options: { selected; disabled? }) => LhAriaAttrs |
aria 屬性包 | radio 語義 |
export type LhSegmentedValue = string | number
export interface LhSegmentedOption { label: string value: LhSegmentedValue disabled?: boolean}
/** 輸入選項(字串快捷寫法或對像) */export type LhSegmentedInput = LhSegmentedValue | LhSegmentedOption
export interface LhSegmentedNormalized { label: string value: LhSegmentedValue disabled: boolean}
export interface LhSegmentedState { disabled: boolean block: boolean // 撐滿父寬(等寬分段)}分段控制器在無障礙層面是單選群組:容器 role="radiogroup",每個分段項 role="radio" + aria-checked。整套語義由兩個屬性包函式提供:
getSegmentedAria({ disabled: false, label: '视图' })// { role: 'radiogroup', 'aria-label': '视图' }
getSegmentedItemAria({ selected: true })// { role: 'radio', 'aria-checked': true }
getSegmentedItemAria({ selected: false, disabled: true })// { role: 'radio', 'aria-checked': false, 'aria-disabled': true }- 容器若無可見 label,用
label參數提供aria-label(可達名稱)。 - 鍵盤互動(UI 層):←/↑ 與 →/↓ 用
moveSegmentedIndex移動並直接選取(radio 模式是「移動即選取」,不是 button 模式的「移動 + Enter」)。 roving tabindex:只有選取項(或第一項)tabindex="0",其餘-1,避免 Tab 在群組內逐項停留。
import { normalizeSegmentedOptions, moveSegmentedIndex, isSegmentedDisabled } from '@lh-ui/share/core/segmented'
normalizeSegmentedOptions(['日', '周', '月'])// [{ label: '日', value: '日', disabled: false }, ...]
normalizeSegmentedOptions([{ label: '日', value: 'day' }, '周'])// [{ label: '日', value: 'day', disabled: false }, { label: '周', value: '周', disabled: false }]
const opts = normalizeSegmentedOptions([ { label: '日', value: 'day' }, { label: '周', value: 'week', disabled: true }, { label: '月', value: 'month' },])moveSegmentedIndex(opts, 0, 1) // 2(跳過停用的 week)moveSegmentedIndex(opts, 2, 1) // 0(循環)moveSegmentedIndex([], 0, 1) // 0(空陣列)isSegmentedDisabled(opts, false) // true(week 被停用 → 整組停用)<!-- Vue3 --><div :class="getSegmentedClassNames({ disabled, block })" v-bind="getSegmentedAria({ disabled, label })"> <div class="lh-segmented__indicator" :style="{ transform: `translateX(${index * 100}%)`, width: `${100 / normalized.length}%` }" /> <button v-for="(opt, i) in normalized" :key="String(opt.value)" type="button" class="lh-segmented__item" :class="{ 'lh-segmented__item--active': i === index, 'lh-segmented__item--disabled': opt.disabled }" :tabindex="i === index ? 0 : -1" v-bind="getSegmentedItemAria({ selected: i === index, disabled: opt.disabled })" @click="onSelect(i)" @keydown.left.prevent="onMove(-1)" @keydown.right.prevent="onMove(1)" > {{ opt.label }} </button></div>// React:indicator 用 CSS transition 滑動;onMove 裡 onChange(normalized[moveSegmentedIndex(normalized, index, dir)].value)// uniapp:<view> + <view> 遍歷;非 h5 端鍵盤事件不可用,觸摸直接點選| 類別名 | 語義 |
|---|---|
.lh-segmented |
基礎類(圓角容器 + 內邊距) |
.lh-segmented--disabled |
停用 |
.lh-segmented--block |
撐滿父寬,分段等寬 |
.lh-segmented__indicator |
滑動高亮塊(絕對定位 + transform) |
.lh-segmented__item |
單個分段項 |
.lh-segmented__item--active |
選取項(文字主色) |
.lh-segmented__item--disabled |
停用項 |
容器底色 --lh-fill-color(淺灰凹槽),indicator --lh-bg-color(白)+ 陰影,選取文字 --lh-primary-color。
- indicator 的定位:UI 層用
transform: translateX(index * 100%)+width: 100/count%;count 必須是規範化後的長度(normalizeSegmentedOptions的結果),否則選項增減時滑塊錯位。 - 字串寫法與物件寫法的 value:字串寫法下
value === label;UI 層v-model綁的是value而非 label,兩種寫法混用時要注意取值來源。 - 整組停用的判定時機:
isSegmentedDisabled依賴 options,選項動態變化時要重算,否則停用態不更新。 - 與 radio 的取捨:分段控制器的選取塊是視覺滑動的,動畫依賴 JS 定位 indicator;若目標端不支援 transform 動畫(部分小程序),退化為 radio 群組更穩妥。
- 空 options:
normalizeSegmentedOptions([])傳回[],moveSegmentedIndex傳回 current——UI 層要處理空態,別渲染出空的凹槽。
@lh-ui/share/core/segmented → moveSegmentedIndex( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue