SegmentedControl 分段控制器

在几个并列的选项中切换其一。

<script setup lang="ts">
  import { ref } from 'vue'
  import { SegmentedControl } from '@hina-ui/vue'

  const status = ref('all')
  const options = [
    { value: 'all', label: '全部' },
    { value: 'ongoing', label: '连载中' },
    { value: 'done', label: '已完结' },
  ]
</script>

<template>
  <SegmentedControl v-model="status" :options="options" aria-label="连载状态" />
</template>
vue

用法

import { SegmentedControl } from '@hina-ui/vue'
ts

分段控制器把几个并列的选项排成一行,任何时刻恰有一项被选中,选中项由一块滑块标示。options 的类型见 Select,v-model 绑定选中项的值;未绑定值时默认选中第一个可用项。未声明的属性都会传给 role="group" 的元素,请用 aria-label 或者 aria-labelledby 为整组命名;class 追加到外层的滚动容器上。

当前排序:latest

<script setup lang="ts">
  import { ref } from 'vue'
  import { SegmentedControl, Stack, Text } from '@hina-ui/vue'

  const sort = ref('latest')
  const options = [
    { value: 'latest', label: '最新' },
    { value: 'popular', label: '最热' },
    { value: 'rating', label: '评分' },
  ]
</script>

<template>
  <Stack gap="sm" align="start">
    <SegmentedControl v-model="sort" :options="options" aria-label="排序方式" />
    <Text tone="muted" size="sm">当前排序:{{ sort }}</Text>
  </Stack>
</template>
vue

它与 RadioGroup 表达同一种选择,区别在于场合:选项不多于五个、文字简短、切换立即生效时用分段控制器;选项需要说明文字,或者选择需要提交时用单选框组。与 Tabs 的区别是它改变的是一个值,不是切换显示的内容。

示例

自定义内容

#option 插槽替换每一项的内容。只放图标时,项的名称仍取 label,读屏软件照常读出。

组件从 options 推断完整选项类型,插槽中的 option 保留额外字段及其类型;v-model 仍绑定 value。类型定义见 Select。

<script setup lang="ts">
  import { ref } from 'vue'
  import { LayoutGrid, List, Rows3 } from '@lucide/vue'
  import { SegmentedControl } from '@hina-ui/vue'

  const icons = { grid: LayoutGrid, list: List, rows: Rows3 }
  const view = ref('grid')
  const options = [
    { value: 'grid', label: '网格' },
    { value: 'list', label: '列表' },
    { value: 'rows', label: '详情' },
  ]
</script>

<template>
  <SegmentedControl v-model="view" :options="options" aria-label="视图">
    <template #option="{ option }">
      <component :is="icons[option.value as keyof typeof icons]" />
    </template>
  </SegmentedControl>
</template>
vue

尺寸

size 有 sm、md、lg 三档,整体高度与同档的输入框相等,可以与输入框、按钮排在同一行。

<script setup lang="ts">
  import { SegmentedControl, Stack } from '@hina-ui/vue'

  const options = [
    { value: 'day', label: '日' },
    { value: 'week', label: '周' },
    { value: 'month', label: '月' },
  ]
</script>

<template>
  <Stack gap="sm" align="start">
    <SegmentedControl size="sm" :options="options" model-value="day" aria-label="小号" />
    <SegmentedControl size="md" :options="options" model-value="day" aria-label="中号" />
    <SegmentedControl size="lg" :options="options" model-value="day" aria-label="大号" />
  </Stack>
</template>
vue

撑满

block 让控件占满父元素的宽度,各项等宽。

<script setup lang="ts">
  import { ref } from 'vue'
  import { SegmentedControl } from '@hina-ui/vue'

  const range = ref('week')
  const options = [
    { value: 'day', label: '今日' },
    { value: 'week', label: '本周' },
    { value: 'month', label: '本月' },
    { value: 'all', label: '全部' },
  ]
</script>

<template>
  <SegmentedControl
    v-model="range"
    :options="options"
    block
    aria-label="统计范围"
    class="max-w-md"
  />
</template>
vue

选项过多时

选项超出容器宽度时,整行可以横向滚动,并且不显示滚动条:滚动位置由两端的边缘阴影提示,鼠标可直接在该行上滚动滚轮,方向键把焦点移到视野外的项时该项滚入视野。block 放不下时各项保持内容宽度。竖排时改为纵向滚动,高度不超过父元素。

<script setup lang="ts">
  import { ref } from 'vue'
  import { SegmentedControl } from '@hina-ui/vue'

  const sort = ref('relevance')
  const options = [
    { value: 'relevance', label: '综合' },
    { value: 'updated', label: '最近更新' },
    { value: 'favorites', label: '收藏最多' },
    { value: 'comments', label: '评论最多' },
    { value: 'words', label: '字数最多' },
    { value: 'rating', label: '评分最高' },
  ]
</script>

<template>
  <SegmentedControl v-model="sort" :options="options" class="max-w-xs" aria-label="排序" />
</template>
vue

竖排

orientation="vertical" 把各项竖向排列,滑块随之上下移动。

<script setup lang="ts">
  import { ref } from 'vue'
  import { SegmentedControl } from '@hina-ui/vue'

  const align = ref('left')
  const options = [
    { value: 'left', label: '左对齐' },
    { value: 'center', label: '居中' },
    { value: 'right', label: '右对齐' },
  ]
</script>

<template>
  <SegmentedControl v-model="align" :options="options" orientation="vertical" aria-label="对齐" />
</template>
vue

状态

disabled 禁用整组;单项的 disabled 只禁用那一项,键盘导航会跳过它。

<script setup lang="ts">
  import { SegmentedControl, Stack } from '@hina-ui/vue'

  const options = [
    { value: 'all', label: '全部' },
    { value: 'ongoing', label: '连载中' },
    { value: 'done', label: '已完结' },
  ]
  const partial = [
    { value: 'all', label: '全部' },
    { value: 'ongoing', label: '连载中' },
    { value: 'done', label: '已完结', disabled: true },
  ]
</script>

<template>
  <Stack gap="sm" align="start">
    <SegmentedControl :options="options" model-value="all" disabled aria-label="整组禁用" />
    <SegmentedControl :options="partial" model-value="all" aria-label="单项禁用" />
  </Stack>
</template>
vue

在表单中

放进 FormField 后,标签通过 aria-labelledby 关联到整组;校验规则与提交交给 Form。分段控制器总有一个值,它常常决定其他字段是否必填,这类规则写在对象层,再指定错误落在哪个字段。

年
月
日
<script setup lang="ts">
  import { reactive, ref } from 'vue'
  import * as v from 'valibot'
  import { Button, DateField, Form, FormField, SegmentedControl, Text } from '@hina-ui/vue'

  const modes = [
    { label: '立即发布', value: 'now' },
    { label: '定时发布', value: 'scheduled' },
  ]

  const schema = v.pipe(
    v.object({
      mode: v.picklist(['now', 'scheduled'], '请选择发布方式'),
      publishAt: v.nullable(v.string()),
    }),
    v.forward(
      v.check(input => input.mode !== 'scheduled' || !!input.publishAt, '请选择发布日期'),
      ['publishAt'],
    ),
  )

  const values = reactive({
    mode: 'now' as string | number,
    publishAt: null as string | null,
  })
  const saved = ref('')

  async function save(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 600))
    saved.value = JSON.stringify(data)
  }
</script>

<template>
  <Form v-slot="{ submitting }" :values="values" :rules="schema" class="w-80" @submit="save">
    <FormField name="mode" label="发布方式">
      <SegmentedControl v-model="values.mode" :options="modes" />
    </FormField>
    <FormField name="publishAt" label="发布日期" :disabled="values.mode !== 'scheduled'">
      <DateField v-model="values.publishAt" />
    </FormField>
    <Button type="submit" :loading="submitting" class="self-start">发布</Button>
    <Text v-if="saved" tone="muted" size="sm">已发布:{{ saved }}</Text>
  </Form>
</template>
vue

行为

  • 点击某项即选中,滑块平移到该项;再点已选项不会取消选择。
  • 键盘 Tab 落在已选项上,方向键在各项之间移动焦点,空格或者 Enter 选中当前项,到达两端后回绕。
  • 悬停与按下的墨落在项上,已选项的墨落在滑块上。

无障碍

  • 整组是 role="group" 的元素,每一项是带 aria-pressed 的按钮。
  • 整组通过 aria-label 或者 aria-labelledby 命名;使用 #option 插槽时,每一项以 label 命名。

API

Props

T extends SelectOption 从 options 推断,默认是 SelectOption。

属性
类型
默认值
说明
modelValue
string | number
第一个可用项的值
选中项的值
options
T[]
—
选项,类型见 Select
size
'sm' | 'md' | 'lg'
'md'
尺寸
orientation
'horizontal' | 'vertical'
'horizontal'
排列方向
block
boolean
false
是否占满父元素宽度
disabled
boolean
false
是否禁用整组
class
string
—
追加至外层滚动容器的类名

插槽

插槽
参数
说明
option
{ option: T }
每一项的内容

事件

事件
参数
说明
update:modelValue
value: string | number
选中项变化