表单
RadioGroup - 单选组
一组互斥的选项。
用法
import { RadioGroup } from '@misoto22/folio'说明
一组互斥的选择。
漫游 tabindex 归 Radix 管,所以整组只占一个 Tab 停靠点,方向键在选项之间移动——这是 ARIA radiogroup 模式的要求,也是一堆手搓的 <input type="radio" 包装最常做错的地方。
放在 Field 里时,这一组的名字取自那个标签,做法是反向指回它:根节点是一个 <div role="radiogroup">,而 <label for> 绑不到它上面,所以上方那些文字点不进去,正如 <legend> 也点不进去。单独使用时,它仍然需要自己的 aria-label——一个没有名字的分组,等于三个没有标签的单选钮。
结构
| 部件 | 说明 |
|---|---|
| 组必填 | 一个把选项竖着排开的 <div role="radiogroup">。正因为它是 div,上面那个标签才是靠被指向来命名这一组的——用的是 aria-labelledby,不是 htmlFor——也正因为如此,那几个字点不进去。 |
| 选项行必填 | RadioGroupItem 把控件和文字一起裹进去的那个 <label>。它就是点击目标——光一个 18px 的圆低于所有指针目标准则——也是这个选项无障碍名称的唯一来源。 |
| 圆圈必填 | 18px 的控件本身,被选中时它的边框变成 --accent。 |
| 圆点 | 圆圈里那块 10px 的 --accent 填充,只出现在被选中的那一项上。 |
实践建议
推荐
- 给这一组一个名字。放在 Field 里时由它的 label 通过 aria-labelledby 来给;单独站着时它需要自己的 aria-label;两样都没有,这一组播报出来就是三个没有标签的单选钮。
- defaultValue 或 value 要设。这里是选中跟着焦点走,所以一个初始为空的组,只要有人用方向键进来就提交了一个答案——包括那些只是路过、正要去下一个字段的读者。
- 答案确实可以不填时,就明确加一个「无」或「不限」选项:单选一旦选中,就没有回到「什么都没选」的路,再点一次不行,键盘也不行。
避免
- 不要把开销大的副作用挂在 onValueChange 上。每按一下方向键都是一次提交,所以选项会发请求或会跳转的组,在路过那些没人想要的选项时,每按一个键就触发一次。
- 不要绕过 RadioGroupItem 直接去用 Radix 的原语,也不要自己手搓这一行:选中跟着焦点走是在这个 item 自己的 focus 处理里实现的,不在上游,所以手搓出来的那一行只会挪动焦点框,什么都不选。
- 不要用禁用某一项来表达「这里没有这个」:漫游焦点会完全跳过它,所以键盘用户从头到尾都不知道有这个选项。原因写进 Field 的 hint,选项本身别放进来。
示例
default
三个选项,一个 tab 停靠点:方向键在它们之间移动,这是 ARIA 的 radiogroup 模式,也是一摞手搓单选框通常会丢掉的那一半。aria-label 不是可选项——根节点是一个 div,所以 htmlFor、包括 Field 画出来的那一个,都绑不到东西上,这个组被播报出来时会完全没有名字。defaultValue 同样要紧:这里选择跟着焦点走,所以一个空着开场的组,会替任何一个在去下一个字段路上用方向键掠过它的人,提交一个答案。
options with detail
选项里面的第二行,价格或者额度这种真正决定选择的东西写在这儿。RadioGroupItem 自己的 label 会把交给它的东西整个包住,所以那点细节在点击目标里面、也在无障碍名称里面——把它压到那个一锤定音的事实上,因为每次读到这个选项,它都会被一起念出来。各行按第一行文字对齐,因为一个对着两行文字居中的圆点,会从它所属的那个名字旁边飘走。
a way back
「任意时间」是那个让其余选项保持可选的选项。单选一旦选中就没有退回「什么都没选」的路——再点一次不行,键盘上也不行——所以一个没有明确出口的筛选器,读者进得去、出不来。同一个道理也否掉了「用禁用某个选项来表示这里没有」:漫游焦点会把一个禁用选项整个跳过,于是用键盘的读者根本不会知道它存在。把它去掉,然后在 Field 的提示里说明原因。
组成部分
在调用处组合,而不是通过 props 配置——所以这个组件没有预料到的布局,依然表达得出来。
RadioGroupItem
一个选项,连同它的标签,构成一个点击目标。
<label> 把两者都包住,所以整行都能点——一个光秃秃的 18px 圆圈低于所有指针目标规范,在手机上更是难受。它同时也是这个控件可访问名称的来源:去掉这层包装,这个单选框就完全没有名字了。
选中跟随焦点,这是让 radiogroup 在键盘下真正好用的那一半模式:移到哪个选项就选中哪个,没人需要多按一个键来确认。这里是自己实现的而不是继承来的,因为上游原语把它挂在一个由 keyup 清除的标志上,而它跑不赢自己的焦点移动——见 ARROW_GRACE_MS。重复选中一个已经选中的选项是空操作,所以即使上游那条路径确实触发了,这里也仍然正确。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| children必填 | ReactNode | 可见的标签。渲染在包住控件的那个 `<label>` 里面。 |
同时接受 Omit<ComponentProps<typeof RadioGroupPrimitive.Item>, 'children'> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。
键盘操作
| 按键 | 作用 |
|---|---|
| Tab | 进入和离开这一组——整组只占一个 Tab 停靠点。 |
| ↑↓←→ | 在选项间移动,并且移到哪就选中哪。 |
无障碍
- 整组只有一个 Tab 停靠点,方向键在选项之间移动,符合 ARIA radiogroup 模式。
- 标签在 <label> 里面,所以整行都是点击目标。