SegmentedControl 分段控制器

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

'use client'

import { useState } from 'react'
import { SegmentedControl } from '@hina-ui/react'

const options = [
  { value: 'all', label: '全部' },
  { value: 'ongoing', label: '连载中' },
  { value: 'done', label: '已完结' },
]

export default function Demo() {
  const [status, setStatus] = useState<string | number>('all')

  return (
    <SegmentedControl
      value={status}
      onValueChange={setStatus}
      options={options}
      aria-label="连载状态"
    />
  )
}
tsx

用法

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

分段控制器把几个并列的选项排成一行,任何时刻恰有一项被选中,选中项由一块滑块标示。options 的类型见 Select,value / onValueChange 绑定选中项的值;未绑定值时默认选中第一个可用项。未声明的属性都会传给根元素,请用 aria-label 或者 aria-labelledby 为整组命名。

当前排序:latest

'use client'

import { useState } from 'react'
import { SegmentedControl, Stack, Text } from '@hina-ui/react'

const options = [
  { value: 'latest', label: '最新' },
  { value: 'popular', label: '最热' },
  { value: 'rating', label: '评分' },
]

export default function Demo() {
  const [sort, setSort] = useState<string | number>('latest')

  return (
    <Stack gap="sm" align="start">
      <SegmentedControl
        value={sort}
        onValueChange={setSort}
        options={options}
        aria-label="排序方式"
      />
      <Text tone="muted" size="sm">
        当前排序:{sort}
      </Text>
    </Stack>
  )
}
tsx

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

示例

自定义内容

renderOption 属性替换每一项的内容。只放图标时,项的名称仍取 label,读屏软件照常读出。

组件从 options 推断完整选项类型,渲染函数中的 option 保留额外字段及其类型;value / onValueChange 仍绑定 value。类型定义见 Select。

'use client'

import { useState } from 'react'
import { LayoutGrid, List, Rows3 } from 'lucide-react'
import { SegmentedControl } from '@hina-ui/react'

const icons = { grid: LayoutGrid, list: List, rows: Rows3 }
const options = [
  { value: 'grid', label: '网格' },
  { value: 'list', label: '列表' },
  { value: 'rows', label: '详情' },
]

export default function Demo() {
  const [view, setView] = useState<string | number>('grid')

  return (
    <SegmentedControl
      value={view}
      onValueChange={setView}
      options={options}
      aria-label="视图"
      renderOption={({ option }) => {
        const Icon = icons[option.value as keyof typeof icons]
        return <Icon />
      }}
    />
  )
}
tsx

尺寸

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

import { SegmentedControl, Stack } from '@hina-ui/react'

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

export default function Demo() {
  return (
    <Stack gap="sm" align="start">
      <SegmentedControl size="sm" options={options} value="day" aria-label="小号" />
      <SegmentedControl size="md" options={options} value="day" aria-label="中号" />
      <SegmentedControl size="lg" options={options} value="day" aria-label="大号" />
    </Stack>
  )
}
tsx

撑满

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

'use client'

import { useState } from 'react'
import { SegmentedControl } from '@hina-ui/react'

const options = [
  { value: 'day', label: '今日' },
  { value: 'week', label: '本周' },
  { value: 'month', label: '本月' },
  { value: 'all', label: '全部' },
]

export default function Demo() {
  const [range, setRange] = useState<string | number>('week')

  return (
    <SegmentedControl
      value={range}
      onValueChange={setRange}
      options={options}
      block
      aria-label="统计范围"
      className="max-w-md"
    />
  )
}
tsx

竖排

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

'use client'

import { useState } from 'react'
import { SegmentedControl } from '@hina-ui/react'

const options = [
  { value: 'left', label: '左对齐' },
  { value: 'center', label: '居中' },
  { value: 'right', label: '右对齐' },
]

export default function Demo() {
  const [align, setAlign] = useState<string | number>('left')

  return (
    <SegmentedControl
      value={align}
      onValueChange={setAlign}
      options={options}
      orientation="vertical"
      aria-label="对齐"
    />
  )
}
tsx

状态

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

import { SegmentedControl, Stack } from '@hina-ui/react'

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

export default function Demo() {
  return (
    <Stack gap="sm" align="start">
      <SegmentedControl options={options} value="all" disabled aria-label="整组禁用" />
      <SegmentedControl options={partial} value="all" aria-label="单项禁用" />
    </Stack>
  )
}
tsx

在表单中

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

年
月
日
'use client'

import { useState } from 'react'
import * as v from 'valibot'
import { Button, DateField, Form, FormField, SegmentedControl, Text } from '@hina-ui/react'

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'],
  ),
)

export default function Demo() {
  const [values, setValues] = useState({
    mode: 'now' as string | number,
    publishAt: null as string | null,
  })
  const [saved, setSaved] = useState('')

  async function save(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 600))
    setSaved(JSON.stringify(data))
  }

  return (
    <Form values={values} rules={schema} className="w-80" onSubmit={save}>
      {({ submitting }) => (
        <>
          <FormField name="mode" label="发布方式">
            <SegmentedControl
              value={values.mode}
              onValueChange={mode => setValues({ ...values, mode })}
              options={modes}
            />
          </FormField>
          <FormField name="publishAt" label="发布日期" disabled={values.mode !== 'scheduled'}>
            <DateField
              value={values.publishAt}
              onValueChange={publishAt => setValues({ ...values, publishAt })}
            />
          </FormField>
          <Button type="submit" loading={submitting} className="self-start">
            发布
          </Button>
          {saved && (
            <Text tone="muted" size="sm">
              已发布:{saved}
            </Text>
          )}
        </>
      )}
    </Form>
  )
}
tsx

行为

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

无障碍

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

API

Props

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

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

内容属性

属性
参数
说明
renderOption
{ option: T }
每一项的内容

回调

回调
参数
说明
onValueChange
value: string | number
选中项变化