跳到正文
册页

容器

Toolbar - 工具栏

贴在工作区边缘的那条动作栏。

用法

什么时候用它

表单滚动时仍然要够得着的那几个动作,或者一个列表上方的筛选条。不是页面头部——那是 AppShell。
TSX
import { Toolbar } from '@misoto22/folio'

说明

贴在工作区边缘的那条动作栏。

两个页面模板各自独立地做出了同一个东西——一条钉住的、底色是 --paper、沿着它所贴的那条边画着一根线的条带——这就是它该待在这里、而不是待在它们各自那里的信号。

底是不透明的 --paper,不是毛玻璃。内容是从这条栏底下滚过去的,所以任何半透明的处理都会把表格的最后一行放到提交按钮背后,两个都读不了;FloatingIconButton 用模糊,是因为它浮在一段空隙之上,那是另一个问题。

它不是 role="toolbar"。那个角色的约定是整条栏只占一个 Tab 停靠点、由方向键在里面的控件之间移动,而这里根本没有实现那一套——声明了角色却不给行为,等于叫读屏用户去按一些什么都不会发生的方向键。它是一个有名字的 group,所以控件各自保留在 Tab 顺序里的位置,而这条栏仍然会被播报。

结构

Toolbar anatomy
部件说明
栏必填一个由 label 命名的 <div role="group">,在 --z-sticky 层级上用一行 flex 装着 children。它不是 role="toolbar":那个角色承诺的是只占一个 Tab 停靠点、控件之间用方向键走,而这里根本没有实现那套东西。
底必填不透明的 --paper,而且刻意不用模糊。内容是从这条栏底下滚过去的,所以任何半透明都会把表格的最后一行摆到提交按钮背后,两样都变得没法读。
边线必填在栏所贴的那一侧画一条 --rule-2 细线:贴底就是 border-t,贴顶就是 border-b。position="static" 保留这条线,只去掉粘性。
动作必填children,排在一行 --gap 为 3 的可换行 flex 上。align 决定它们在行内轴上的位置,默认是 end,也就是表单主要动作该在的地方。

实践建议

推荐

  • label 就写这条栏是什么——「表单动作」「列表筛选」。没有名字的 group 会被播报成“group”,而一个页面上有两条,就把同样的空话播报两遍。
  • 用 position="bottom" 时,要给会滚动的那个祖先一个高度。粘性元素是在自己的滚动容器里粘住的,所以一条栏如果所在的容器和内容一样高,它就没有可粘的东西,只会老老实实待在末尾。
  • 只放动作。长出了标题、状态和面包屑的栏就是页头,而一个跟着读者往下走的页头,等于让页面能看见的部分变少了。

避免

  • 不要通过属性把 role="toolbar" 加上去。这个角色是在告诉读屏用户:方向键可以在控件之间移动;而这里没有任何东西实现 roving tabindex,所以那些键什么都不会做,这个承诺就是假的。
  • 不要为了“让内容透出来”把底做成半透明。会透出来的那点内容,正是读者正想读的那一行,和他正想按的那个按钮。

示例

form actions

一片工作表面脚下那条动作栏。底色是不透明的 --paper,而不是一层模糊,因为内容会从它底下滚过去——任何半透明的东西,都会把表单的最后一行摆到提交按钮后面。它是一个具名的 group,不是 role="toolbar":那个角色承诺了控件之间可以用方向键走,而这里没有任何东西实现了它们。

Scroll this panel — the bar below stays where it is.

A sticky element sticks within its scroll container, so the container needs a height of its own for the bar to have anything to stick to.

Keep the bar to the actions. One that has grown a title and a breadcrumb is a page header following the reader down the screen.

a filter bar on top

同一个组件,在另一条边上。position 用 top 会把这条栏粘在它滚动容器的头部,并把细线挪到底部,于是一个在长列表里往下滚的读者,始终握着塑造了这份列表的那些控件。align 用 between 是这个形状在这里的需要:筛选条件从起始边读起,动作坐在末端,于是这是一条栏,而不是一行筛选加一行按钮。一个粘性元素是粘在它的滚动容器里面的,所以需要高度的是容器,不是这条栏。

  • a1b2c3d
  • 9f8e7d6
  • 4c5b6a7
  • 77aa2b1
  • e3d4c5b
  • 0099aab
  • 12ff34e

every control is a tab stop

一条批量操作栏,以及这个角色是 group 而不是 toolbar 的理由。ARIA 的 toolbar 契约是「一个 tab 停靠点,里面的控件之间用方向键移动」;这里没有实现漫游 tabindex,所以声明那个角色,等于告诉用读屏软件的人去按一些什么都不会发生的键。作为一个具名的 group,每个控件都保住了自己在 tab 顺序里的位置——Tab 依次到达归档、删除、清除——而这条栏仍然会被按名字播报,这也是为什么 label 是必填而不是可选。

3 rows selected

类型

TSX
export type ToolbarPosition = 'bottom' | 'top' | 'static'
export type ToolbarAlign = 'start' | 'center' | 'end' | 'between'

键盘操作

Toolbar keyboard interactions
按键作用
Tab依次走到每一个控件——这条栏本身不是一个停靠点。

无障碍

  • label 必填,它就是这个 group 的可访问名称,所以一个同时有筛选条和动作条的页面播报出来的是两个不同的东西。
  • 每个控件都保留自己在 Tab 顺序里的位置,因为这条栏刻意不去声明 role="toolbar",也就不去认它那份“只占一个 Tab 停靠点”的约定。
  • 底是不透明的,所以栏上的控件对比度永远是对着 --paper 算的,而不是对着此刻正从背后滚过去的随便什么东西。