跳到主要内容
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
开始使用WebReactSwiftUIJetpack ComposeFlutterReact Native预设你自己的设计系统与 AI 代理一起使用 Tanrei品牌设计令牌设计系统与定制Theme Studio颜色字体排印图标形状层级动效交互状态行为布局无障碍文字颜色令牌字阶令牌形状令牌层级令牌动效令牌状态令牌间距与度量令牌组件按钮主操作按钮分段按钮按钮组带菜单的按钮工具栏复制按钮复选框单选按钮开关纸片(Chip)菜单滑块滑块字段日历DateTimePicker字段组单选组复选框组下拉选择框下拉选择字段菜单栏文件上传文件选择器 进阶放置区文本输入框 进阶文本字段纸片字段字段组合框 进阶组合框字段数字输入框 进阶数字字段颜色输入框 进阶颜色字段一次性验证码输入框 进阶一次性验证码字段嵌入式文本输入框 进阶聊天输入栏卡片分隔线模态层 进阶对话框列表列表行底部面板侧边面板轮播代码块表格弹出框步骤折叠面板描述列表图示应用外壳页头树视图时间线图片预览调整手柄可调整大小的框滚动区域徽标进度指示器计量条数字滚动器加载指示器消息条(Snackbar)工具提示键盘快捷键标签(Tag)行内代码提示块(Callout)头像空状态标签组(Tag group)骨架屏文件项色块跑马灯文本标签页标签页视图应用栏导航栏导航抽屉搜索链接分页上一页/下一页链接步骤条面包屑目录链接卡片FAB 菜单状态Tanrei 是如何构建的参与贡献许可与致谢
首页组件标签页视图

标签页视图

在自己的一行标签页之下显示选中标签页的视图,也可以关联到放在别处的一行标签页;用户还可以在视图之间轻扫,标签页的指示器会跟随手指。

content_copy以 Markdown 格式查看
类别
导航
令牌
comp.tab-view
层级
组合
构成自
Tabs

概览

Web
tanrei-tab-view
schedule
SwiftUI
TanreiTabView
radio_button_unchecked
Jetpack Compose
TanreiTabView
radio_button_unchecked
Flutter
TanreiTabView
radio_button_unchecked
React Native
TabView
radio_button_unchecked

生成自 docs/status/status.json, specs/tab-view.spec.json

标签页视图为每个标签页保存一个面板,并显示选中标签页的面板。选中一个标签页时,视图以一段短促的位移、淡入淡出和模糊完成切换;用户也可以横向轻扫视图,标签页的指示器会跟随手指,并停在视图最终落到的那个标签页上。在减弱动态效果时,切换是一段仍然跟随手指的交叉淡入淡出,没有位移,也没有模糊。

带有自己一行标签页的标签页视图:选一个标签页,或用触摸或触控笔轻扫面板

三项未完成的任务,本周到期。

李华在 1 小时前评论了这份计划。

对工作区内的所有人可见。

标签页视图有两种形态:

  • 自带一行标签页,这是默认形态。 视图在自己的顶部绘制一行标签页,面板紧接在它的下方。每个面板带着对应标签页的 label、icon、labelHidden 和 disabled,所以一处声明就同时得到标签页和它的视图。视图的 variant、size、track、fullWidth、label、group 和 rememberAs 就是这一行的属性。这一行是真正的 Tabs:同样的轨道和指示器,同样的令牌、变体和行为,所以为标签页重新设定样式的层也会一并改变它。切换时,它从不移动、淡出或模糊。
  • 关联,用于放在别处的标签页。 tabs 把视图关联到应用栏或粘性页眉里的一行标签页。只要设置了它,视图就不绘制自己的那一行,即使被关联的那一行暂时不存在也是如此;视图上针对这一行的属性和面板上针对标签页的属性都不起作用。

自带一行标签页时,每一次选择(无论来自标签页还是轻扫)都由视图触发 change,这一行自己的 change 不会传到你那里。关联时,由标签页选中则标签页触发 change,由轻扫选中则视图触发,所以每一次选择只通知一次。

指定同一个 group 的标签页视图显示同一个选择,rememberAs 则把这个选择保存在用户的设备上。本站的内容标签页就是一个例子:每一组平台示例都是同属一个组的次要标签页视图,并以本站自己的键记住选择。

用法

在 Web 上它是 <tanrei-tab-view>,其中放 <tanrei-tab-panel> 子元素,也就是一行 Tabs 为自己的面板所用的同一个元素。选中的面板就是视图的 value,它与标签页的 value 保持相等:设置其中一个,另一个也随之设置,不触发事件。视图处于关联形态时,tabs 指定同一文档或同一 shadow root 中某个 <tanrei-tabs> 的 id。swipeable 写作它的反义 no-swipe,因为 HTML 的布尔属性默认为 false。

SwiftUI、Compose、Flutter 和 React Native 尚未实现标签页视图。在实现之前,这些平台上由一行标签页来显示选中的面板,做法见它自己的页面,但没有轻扫。

<!-- 自带一行标签页(默认):每个面板声明自己的标签页。 -->
<tanrei-tab-view label="项目">
  <tanrei-tab-panel value="overview" label="概览" icon="dashboard">…</tanrei-tab-panel>
  <tanrei-tab-panel value="activity" label="动态" icon="history">…</tanrei-tab-panel>
  <tanrei-tab-panel value="billing" label="账单" icon="payments" disabled>…</tanrei-tab-panel>
</tanrei-tab-view>

<!-- 关联,用于放在别处(应用栏、粘性页眉)的标签页:视图不绘制那一行。 -->
<tanrei-tabs id="project-tabs" label="项目">
  <tanrei-tab value="overview" label="概览"></tanrei-tab>
  <tanrei-tab value="activity" label="动态"></tanrei-tab>
</tanrei-tabs>
<tanrei-tab-view tabs="project-tabs">
  <tanrei-tab-panel value="overview">…</tanrei-tab-panel>
  <tanrei-tab-panel value="activity">…</tanrei-tab-panel>
</tanrei-tab-view>

<!-- 指定同一个 group 的各个视图显示同一个选择。remember-as 用你自己的键把它保存在设备上。 -->
<tanrei-tab-view variant="secondary" group="platform" remember-as="my-docs-platform" label="平台">
  <tanrei-tab-panel value="web" label="Web">…</tanrei-tab-panel>
  <tanrei-tab-panel value="flutter" label="Flutter">…</tanrei-tab-panel>
</tanrei-tab-view>

<script type="module">
  import '@tanrei/web/components/tab-view'; // 同时定义标签页和面板
  // 每次选择只有一次 change。自带一行标签页:标签页和轻扫都由视图触发。
  // 关联:标签页选中由标签页触发,轻扫由视图触发。在组里:只有用户操作的那个视图触发。
  document.addEventListener('change', (e) => console.log(e.target.localName, e.detail.value));
</script>
import { Tab, TabPanel, Tabs, TabView } from '@tanrei/react';

// 自带一行标签页:一个 onChange 就能收到每一次选择,无论来自标签页还是轻扫。
<TabView label="项目" value={view} onChange={(e) => setView(e.detail.value)}>
  <TabPanel value="overview" label="概览" icon="dashboard"><Overview /></TabPanel>
  <TabPanel value="activity" label="动态" icon="history"><Activity /></TabPanel>
</TabView>

// 关联:标签页选中由标签页通知,轻扫由视图通知。两者都要用来更新你的状态。
<Tabs id="project-tabs" label="项目" value={view} onChange={(e) => setView(e.detail.value)}>
  <Tab value="overview" label="概览" />
  <Tab value="activity" label="动态" />
</Tabs>
<TabView tabs="project-tabs" onChange={(e) => setView(e.detail.value)}>
  <TabPanel value="overview"><Overview /></TabPanel>
  <TabPanel value="activity"><Activity /></TabPanel>
</TabView>

// 共享并被记住的选择。noSwipe 是规格中 swipeable 的反义。
<TabView variant="secondary" group="platform" rememberAs="my-docs-platform" label="平台" noSwipe>
  <TabPanel value="web" label="Web"><WebSample /></TabPanel>
  <TabPanel value="flutter" label="Flutter"><FlutterSample /></TabPanel>
</TabView>

规格

填满的一行次要标签页,带图标和一个禁用的标签页;以及关联到另放一处的一行标签页的视图

三个服务,全部在运行。

2 小时前已部署。

尚未设置账单。

第 1 天:抵达,然后逛老城区。

在车站附近保存了四个地点。

博物馆周一闭馆。

构成

部件 说明
1 tabs · 可选 The row of tabs the Tab view draws above its container while it is not linked to a row placed elsewhere (TBV-16): a Tabs, whole (tab list, tabs, active indicator, underline and divider), drawn from the tabs' own tokens, with one tab for each panel, named and marked as the panel says. Linked to another row, the Tab view draws none (TBV-17).
2 container The region that holds the panels, below the row when the Tab view draws one: as wide as the Tab view and as tall as the shown panel. It draws nothing of its own, takes the swipe, and clips the panels to its corners while they travel and blur. The row is outside it and is never moved, faded or blurred by a switch.
3 panel One view, matched to the tab of the same value. While the Tab view draws its own row, the panel also says what its tab shows (a label, an icon) and whether it is disabled. One panel is shown at rest; during a switch the old and the new panel are drawn in the same place, the new one over the old. Its content is the product's.
4 focus-ring Gap-then-ring indicator shown for keyboard focus on a panel that holds nothing focusable, drawn inside the container's edge.

属性

属性 类型 默认值 说明
value string null The value of the selected tab, whose panel is shown. The Tab view keeps it equal to its tabs' value, whether it draws them or is linked to them. null shows the panel of the tab the tabs select while theirs is null (the first enabled tab).
variant primary | secondary | tonal "primary" The variant of the row the Tab view draws itself, with the Tabs' meaning: primary for the main views of a screen, secondary for views within one of them, tonal for the quiet row of a sample or a setting. A design system may add values to the tabs' axis, and the row takes them. Ignored while the Tab view is linked to a row placed elsewhere, which has its own.
size small | medium "medium" The size of the row the Tab view draws itself, with the Tabs' meaning: medium for the views of a screen, small for a row in a card's or a sample's header. A design system may add values to the tabs' axis, and the row takes them. Ignored while the Tab view is linked to a row placed elsewhere, which has its own.
track boolean null Whether the row the Tab view draws itself draws its track, with the Tabs' meaning: false draws none, true draws it, and null, the default, takes comp.tabs.track.visible, the system's choice. Ignored while the Tab view is linked to a row placed elsewhere, which has its own.
fullWidth boolean false How the row the Tab view draws itself lays out its tabs, with the Tabs' meaning: false (hug) sizes each tab to its label and scrolls the row when they do not fit; true (fill) shares the row's width among the tabs. The Tab view itself is always as wide as its parent. Ignored while the Tab view is linked to a row placed elsewhere.
label string "" The accessible name of the row the Tab view draws itself: what the views are views of. Needed when a screen has more than one row of tabs. Ignored while the Tab view is linked to a row placed elsewhere, which is named by its own label.
tabs string "" Empty (the default): the Tab view draws its own row of tabs above its panels, one per panel. Set: the Tab view is linked to a Tabs placed elsewhere (in an app bar, a sticky header) and draws no row; those tabs name its panels, move between them without a swipe and draw their indicator where a swipe has brought it. How a platform names the link is in its mapping.
swipeable boolean true Lets a person switch panels by dragging the shown panel sideways. False leaves the switch to the tabs alone, for panels whose content is itself dragged sideways (a map, a drawing surface).
transition auto | slide | fade | none "auto" The effect of switching views: the form in which the shown panel gives way to the next. slide: the panels travel, and fade and blur on the way, as the system's transition values say, and follow a swipe. fade: the two panels crossfade in place. none: the new panel is shown whole in one frame, and the row the Tab view draws puts its indicator on the new tab in that same frame (TBV-22, TBV-23). Auto takes comp.tab-view.motion.transition, the design system's answer (slide in the default system). Where motion is reduced slide is drawn as fade, and nothing is ever added to fade or none. It applies whether the Tab view draws its row or is linked to one; a linked row keeps its own transition.
group string "" The group of the row the Tab view draws itself, with the Tabs' meaning: Tab views and rows of tabs that name the same group show the same choice, and a view that has no panel of that value keeps its own. Empty (the default): the view's choice is its own. Ignored while the Tab view is linked to a row placed elsewhere, which names its own group.
rememberAs string "" The key under which the row the Tab view draws itself keeps a person's choice on their device and restores it before the view is first drawn, with the Tabs' meaning. Empty (the default): nothing is read or written. Ignored while the Tab view is linked to a row placed elsewhere, which names its own key.

事件

事件 说明
change The user selected another panel; carries its value, which value and the tabs' value now hold. The Tab view announces every selection made on its own row and every swipe. While it is linked, a selection made on the linked tabs is announced by their own change instead, so every selection is announced once.

条目

该组件重复排列的条目种类。测试样例按顺序列出条目;各平台以各自的惯用方式为每个种类命名。

panel

One view of the Tab view: the content shown while the tab with its value is selected, and, for the row the Tab view draws itself, what that tab shows.

属性 类型 默认值 说明
value string "" The value of the tab that shows this panel. Unique within the Tab view; required.
label string "" The text of this panel's tab in the row the Tab view draws itself, and the accessible name of both the tab and the panel. Required there, even when labelHidden is true; not used while the Tab view is linked, where the linked row's own tab names the panel.
icon string "" The name of an icon (a glyph of the system's icon set) for this panel's tab in the row the Tab view draws itself. Give every panel an icon, or none. Not used while the Tab view is linked.
labelHidden boolean false Draws this panel's tab as its icon alone and keeps label as its accessible name (an icon-only tab), in the row the Tab view draws itself. Not used while the Tab view is linked.
disabled boolean false Makes this panel's tab disabled in the row the Tab view draws itself: a person cannot select it, and a swipe skips its panel. Not used while the Tab view is linked, where the linked row's own tab says whether it is disabled.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-tab-panel TanreiTabPanel TanreiTabPanel TanreiTabPanel TabPanel
value · 属性 value tag value value value
label · 属性 label title label label label
icon · 属性 icon icon icon icon icon
labelHidden · 属性 labelHidden labelStyle labelHidden labelHidden labelHidden
disabled · 属性 disabled disabled enabled enabled disabled
备注 The same element the tabs' own panels are: <tanrei-tab-panel value="overview" label="Overview" icon="dashboard">, a child of tanrei-tab-view, with label-hidden and disabled attributes, as tanrei-tab has them. label, icon, label-hidden and disabled are read only by a tanrei-tab-view that draws its own row; a panel of a tanrei-tabs, or of a linked view, ignores them. TanreiTabPanel(_ title:, icon:) { … }.tag(value) in the view's builder, as the tabs' TanreiTab is; .labelStyle(.iconOnly) is labelHidden and .disabled(_:) is disabled. A linked view's panels may omit the title: TanreiTabPanel { … }.tag(value). TanreiTabPanel(value, label, icon, labelHidden, enabled) { … } called in the TanreiTabView { } content, a scope that collects the panels (as a lazy list's content does) so that the row can be drawn from their labels before a panel is composed. enabled is the inverse flag. TanreiTabPanel<T>(value:, label:, icon:, labelHidden:, enabled:, child:) entries of the view's panels: list, as the bar's TanreiTab<T> entries are; enabled is the inverse flag. <TabPanel value="overview" label="Overview" icon="dashboard">…</TabPanel> children of <TabView>; labelHidden and disabled as on the row's Tab.

状态

行为

以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。

ID 需求
TBV-1 THE Tab view SHALL show the panel whose value is the value of its tabs' selected tab, its tabs being the row it draws itself (TBV-16) or the row it is linked to (TBV-17) (TAB-11: the first enabled tab while their value is null), and hide every other panel; THE Tab view SHALL keep its own value equal to its tabs' value, and WHEN either of them is set THE Tab view SHALL set the other to the same value without emitting change.
TBV-2 WHEN its tabs' value changes while their indicatorOffset is 0 (the user selected a tab, by activating it or by releasing a drag across the row on it, TAB-1, TAB-12 and TAB-29, or the product set it) THE Tab view SHALL switch from the shown panel to the newly selected tab's panel as TBV-5 and TBV-6 say, in the form its transition names (TBV-22); WHILE it is linked THE Tab view SHALL NOT emit change for that selection, which the linked tabs have announced themselves, and WHILE it draws its own row it SHALL announce it as TBV-18 says. WHERE their value changes because they follow a choice of their group that was made on another member, or one restored from storage, THE Tab view SHALL show the panel at once instead, as TBV-20 says.
TBV-3 WHILE swipeable is true, WHEN the user drags the shown panel by touch or pen past the platform's touch slop, further sideways than up or down, THE Tab view SHALL take the drag and track it with a progress p, the drag's sideways distance divided by the width of its container and kept between 0 and 1: toward the next enabled tab of its tabs when the drag moves toward the start edge, and toward the previous enabled tab when it moves toward the end edge. In every frame of the drag THE Tab view SHALL draw the switch to that tab's panel at p (TBV-5) and set its tabs' indicatorOffset to p toward the next tab or to −p toward the previous one; WHERE there is no enabled tab in the drag's direction THE Tab view SHALL leave the panel at rest and indicatorOffset at 0. A drag that is further up or down than sideways when it passes the slop SHALL be left to vertical scrolling for its whole length. A drag that starts on the row the Tab view draws itself is the row's (its tabs are pressed, its active indicator follows the drag, TAB-28, and it scrolls sideways itself) and SHALL NOT be taken; WHILE its tabs follow a drag of their own, in its own row or in the linked row, THE Tab view SHALL take no drag and SHALL leave the shown panel at rest until that drag's release selects a tab (TBV-2): the indicator changes continuously under the finger, but which panel is shown is not decided before the release.
TBV-4 WHEN the user releases a drag the Tab view has taken THE Tab view SHALL commit it if p is at least 0.5 or the release is a fling toward the tab, and cancel it otherwise, a fling away from the tab always cancelling; a fling is a release faster than the platform's minimum fling velocity (Android's ViewConfiguration.getScaledMinimumFlingVelocity, Flutter's kMinFlingVelocity) or, where the platform names none (the web, SwiftUI, React Native), 0.5 points per millisecond. A release's velocity is the pointer's over its last moves: WHERE the pointer has not moved for longer than the time after which the platform's velocity tracker takes a pointer as stopped (40 milliseconds in Android's VelocityTracker, Jetpack Compose's VelocityTracker and Flutter's VelocityTracker) or, where the platform names none (the web, SwiftUI, React Native), 40 milliseconds before the release THE Tab view SHALL take the release as having no velocity, so that a drag that stops where it is wanted and is let go there is no fling, however fast it moved before it stopped. To commit THE Tab view SHALL set its value and its tabs' value to that tab's value and emit change exactly once with it, re-express indicatorOffset from the new tab (1 − p toward the tab it left) in the same frame, and finish the switch while bringing indicatorOffset to 0 over comp.tab-view.motion.switch-duration with comp.tab-view.motion.switch-easing, starting from where the release left them. To cancel THE Tab view SHALL bring the switch back to p = 0 and indicatorOffset to 0 over the same motion and emit nothing.
TBV-5 WHILE a switch from panel A to panel B is at progress p THE Tab view SHALL draw both panels in the same place, B over A, with a travel D of comp.tab-view.transition.distance plus comp.tab-view.transition.distance-fraction times the width of its container: A moved p × D toward the edge it leaves by (the start edge when B's tab comes after A's among its tabs, the end edge otherwise), at an opacity of 1 − p × (1 − comp.tab-view.transition.opacity) and blurred by p × comp.tab-view.transition.blur; B moved (1 − p) × D from the opposite edge, at an opacity of 1 − (1 − p) × (1 − comp.tab-view.transition.opacity) and blurred by (1 − p) × comp.tab-view.transition.blur. The travel SHALL follow p as eased, an overshoot included; the opacity and the blur SHALL follow p clamped between 0 and 1. THE Tab view SHALL clip both panels to its container, its corners rounded by comp.tab-view.container.shape, so that neither travel nor blur draws outside it or over the row it draws itself; WHERE a platform cannot blur a view THE Tab view SHALL draw the travel and the fade without the blur and say so in its README.
TBV-6 WHEN a switch starts from a selection on its tabs or by the product (TBV-2) THE Tab view SHALL run p from 0 to 1 over comp.tab-view.motion.switch-duration with comp.tab-view.motion.switch-easing; WHEN the selection changes again, or the user starts a drag, before a switch has ended THE Tab view SHALL end that switch at once, as if it had finished, and start from the panel it switched to. WHEN the Tab view first appears, or its panels are added, removed or given other values, or it is linked or unlinked (TBV-17), THE Tab view SHALL show the selected tab's panel at once, without a switch.
TBV-7 WHERE motion is reduced THE Tab view SHALL draw every switch as TBV-5 says with comp.tab-view.transition.distance, comp.tab-view.transition.distance-fraction and comp.tab-view.transition.blur taken as 0, so that only the fade is left: during a drag the panels crossfade with p, and a switch from a selection, and the rest of a switch after a release, SHALL run over comp.tab-view.motion.fade-duration with comp.tab-view.motion.fade-easing. Its tabs' indicatorOffset SHALL still follow the drag, which the person's finger moves, and SHALL return to 0 at once on release.
TBV-8 THE Tab view SHALL NOT take a drag that starts on its own row or while its tabs follow a drag of their own (TBV-3), on content that handles sideways drags itself (a region that scrolls sideways, a slider, a carousel, a text field, selected text or its handles) or that is made by mouse, trackpad or wheel, and WHILE swipeable is false THE Tab view SHALL take no drag at all; in every such case the panels switch only through the tabs (TBV-2).
TBV-9 THE Tab view SHALL expose the shown panel with the tab panel role, named by its tab and associated with it where the platform can express that (the tab controls the panel; the panel is labelled by the tab), whether the tab is in the row it draws itself or in the row it is linked to, SHALL hide every other panel from assistive technology and from focus, including a panel that a switch or a drag still draws, SHALL move that exposure to the new panel when value changes rather than when the switch's drawing ends, and SHALL make a shown panel with no focusable content itself focusable so that Tab reaches it.
TBV-10 WHEN a switch hides the panel that holds keyboard focus THE Tab view SHALL move focus to its tabs' selected tab. WHEN a panel with no focusable content receives keyboard focus THE Tab view SHALL show the focus ring inside the container's edge, its corners following comp.tab-view.container.shape, as sys.behavior.focus-style names (ring or glow), without animation; focus gained by pointer or touch SHALL NOT show it; a platform that cannot draw a choice SHALL fall back to the first value and state that in its README.
TBV-11 THE Tab view SHALL be as wide as its parent and, unless the product sizes it through the platform's own sizing, as tall as the row it draws itself (none while it is linked) plus its shown panel; WHILE a switch draws two panels its container SHALL be as tall as the taller of them, and SHALL take the new panel's height at once when the switch ends. THE Tab view SHALL place its own row at its top edge, across its whole width, with the container directly below it and no space between them. WHILE the product sizes it THE Tab view SHALL keep its own row at its own height at the top, and make each panel fill the rest and scroll up and down inside it, below the row.
TBV-12 THE Tab view SHALL keep a hidden panel and its state (its scroll position, what was typed into it), so that it is shown as it was left, and SHALL NOT discard or recreate a panel because it is hidden, nor because its tab in the row it draws itself changes.
TBV-13 WHEN the layout direction is right-to-left THE Tab view SHALL mirror the drag and the travel with the reading order: a drag toward the start edge, which is the right, moves toward the next tab, and a panel leaves by the start edge when the tab it gives way to comes after its own; THE Tab view SHALL NOT mirror a panel's content. Its own row SHALL mirror as TAB-21 says.
TBV-14 WHEN tabs names a row that is not there (on a platform that links by an object, a link that no row uses) THE Tab view SHALL show the panel whose value equals the view's value (the first panel while the view's value is null), take no drag, expose no panel with the tab panel role, draw no row of its own (TBV-17), and in debug builds report that its panels have no tabs; WHEN the selected tab has no panel THE Tab view SHALL show none and in debug builds report the value. WHILE the Tab view draws its own row and holds no panel THE Tab view SHALL draw that row with no tab and in debug builds report that it has no panels.
TBV-15 WHEN a panel's value is empty, repeats another panel's, or, while the Tab view is linked, is the value of no tab of the linked row THE Tab view SHALL still hold it, show only the first panel with a given value, give its own row no tab for any other, and in debug builds report the panel; WHEN the linked row holds panels of its own THE Tab view SHALL show and associate its own panels in their place, keep the tabs' panels hidden, and in debug builds report the tabs' panels.
TBV-16 WHILE tabs is empty THE Tab view SHALL draw its own row of Tabs above its container (TBV-11) and be linked to that row as it would be to a row placed elsewhere: one tab for each panel that TBV-15 shows, in the panels' order, whose value, label, icon, labelHidden and disabled are the panel's; the row's variant, size, track, fullWidth, label, group and rememberAs SHALL be the Tab view's own, its transition what TBV-23 says, and its value the Tab view's value. The row SHALL be a Tabs in every respect, drawn from the tabs' tokens of its variant and size with every TAB behaviour (among them the press answer of TAB-8 to TAB-10, the keyboard of TAB-12, the names of TAB-13 and TAB-22, and the indicator of TAB-17 and TAB-26), and SHALL change in the same frame as the panels it is drawn from are added, removed, reordered or given another value, label, icon, labelHidden or disabled.
TBV-17 WHILE tabs names a row THE Tab view SHALL draw no row of its own, whether or not the named row is there (TBV-14), and SHALL use none of its variant, size, track, fullWidth, label, group and rememberAs or of its panels' label, icon, labelHidden and disabled, which the linked row's own props and tabs replace; WHEN tabs names a row while variant, size, track, fullWidth, label, group or rememberAs differ from their defaults THE Tab view SHALL in debug builds report that they apply only to a row it draws itself. WHEN tabs changes from empty to a row, or back, THE Tab view SHALL remove or draw its own row at once, keep its value (giving it to the newly linked row, or taking that row's while its own is null) and show the selected tab's panel at once (TBV-6).
TBV-18 WHILE the Tab view draws its own row, WHEN the user selects another tab on that row (TAB-1, TAB-12, TAB-29) THE Tab view SHALL set its value to that tab's value and emit change exactly once with it, in the same frame as the row sets its own value, and SHALL NOT let the row's own change reach the product as a second announcement; together with TBV-4 every selection a person makes on a Tab view with its own row, by a tab or by a swipe, SHALL be announced once, by the Tab view.
TBV-19 THE Tab view SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the Tab view is shown THE Tab view SHALL draw itself for the new direction at once, without a motion of its own, keeping the selected panel and its focus.
TBV-20 WHEN its tabs' value changes because their group's choice was made on another member or restored from storage (TAB-36, TAB-38, TAB-39) THE Tab view SHALL show the newly selected tab's panel at once, with none of the travel, fade and blur of TBV-5 and without the motion of TBV-6 or TBV-7, SHALL take that panel's height in the same frame (TBV-11), SHALL keep the panel it hides and its state (TBV-12), SHALL move the panels' exposure to assistive technology in that frame (TBV-9), and SHALL emit no change. WHERE that hides the panel that holds keyboard focus THE Tab view SHALL move focus to its tabs' selected tab as TBV-10 says, without scrolling anything to show it. WHERE the Tab view follows a swipe when the choice changes THE Tab view SHALL end the swipe without committing it, emit nothing for it, and show the chosen panel at rest. THE Tab view a person selected on SHALL switch as TBV-2 and TBV-6 say and announce the selection once (TBV-18, or the linked tabs' change).
TBV-21 WHEN a person commits a swipe (TBV-4) on a Tab view whose tabs are a member of a group and, in following that choice, other members change the size of what lies before the Tab view in a region that scrolls, THE Tab view SHALL keep its container where the person is using it, as TAB-40 says of a row: in the frame that first draws the new choice the container's top edge SHALL be at the place in the window that it had in the frame before, the scroll positions of the regions it scrolls in having been moved by the difference at once and without a scrolling motion, as far as each region can scroll. A selection made on a tab SHALL be kept in place by the row (TAB-40), whether the Tab view drew that row or is linked to it.
TBV-22 THE Tab view SHALL draw every switch between two panels in the form its transition names: the name transition gives, or, WHILE transition is auto, the name comp.tab-view.motion.transition holds (slide, fade or none); WHEN transition names a value the Tab view does not define THE Tab view SHALL treat it as auto and in debug builds report it. WHILE the form is slide THE Tab view SHALL switch as TBV-3 to TBV-6 say. WHILE the form is fade THE Tab view SHALL draw every switch as TBV-5 says with comp.tab-view.transition.distance, comp.tab-view.transition.distance-fraction, comp.tab-view.transition.blur and comp.tab-view.transition.opacity all taken as 0, so that the two panels crossfade in place: a switch from a selection, and the rest of a switch after a swipe is released, SHALL run over comp.tab-view.motion.fade-duration with comp.tab-view.motion.fade-easing, and during a swipe the panels SHALL crossfade with p (TBV-3). WHILE the form is none THE Tab view SHALL show the newly selected tab's panel whole in the frame the selection is made, with none of the travel, fade and blur of TBV-5 and no motion of TBV-6, SHALL take that panel's height in the same frame (TBV-11), SHALL keep the panel it hides and its state (TBV-12) and SHALL move the panels' exposure and keyboard focus as TBV-9 and TBV-10 say — the drawing TBV-20 gives a choice made elsewhere; a swipe SHALL still be taken while swipeable is true (TBV-3), SHALL leave the shown panel at rest for its whole length while its tabs' indicatorOffset follows the finger, and WHEN it is released SHALL be committed or cancelled as TBV-4 decides, the new panel shown at once and indicatorOffset brought to 0 at once. In every form THE Tab view SHALL select, emit and announce as it does under slide (TBV-4, TBV-18): the form changes what is drawn between two panels and nothing else.
TBV-23 WHERE motion is reduced THE Tab view SHALL draw the form slide as TBV-7 says, the form fade as TBV-22 says, and the form none as none: reduced motion SHALL never add a motion that transition took away. WHILE the Tab view draws its own row (TBV-16) and the form is none, THE Tab view SHALL give that row the tabs' transition none (TAB-43), so that the indicator is on the new tab in the frame the new panel is shown and nothing is left traveling over a view that has already changed; WHILE the form is slide or fade the row's transition SHALL be auto, the tabs' own answer (comp.tabs.motion.transition). THE Tab view SHALL NOT set the transition of a row it is linked to (TBV-17), which has its own. WHEN transition changes while a switch is drawn THE Tab view SHALL end that switch at once, as if it had finished (TBV-6), and SHALL draw the next one in the new form; changing transition SHALL NOT change value, the shown panel, keyboard focus or what is emitted.
TBV-24 WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE tab view SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on the tab list of the row it draws itself (TBV-16), as the tabs of that row expose what is set on theirs — an id reference by the elements it names in the host's tree or a tree around it, a description after its own; WHILE label is not empty, label is the tab list's name, over a name given so; WHILE label is empty, a name given so is the tab list's name. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE tab view SHALL take it off that element. WHILE the tab view is linked to tabs placed elsewhere (TBV-17) it draws no row, so what the product sets on its host reaches no element: THE tab view SHALL move it all the same and report it in debug builds, and the product sets it on the linked tabs instead.

各平台名称对照

对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-tab-view TanreiTabView TanreiTabView TanreiTabView TabView
value · 属性 value selection selectedValue value value
variant · 属性 variant variant variant variant variant
size · 属性 size size size size size
track · 属性 track track track track track
fullWidth · 属性 fullWidth fullWidth fullWidth isScrollable fullWidth
label · 属性 label label contentDescription semanticLabel accessibilityLabel
tabs · 属性 tabs link link link link
swipeable · 属性 noSwipe swipeable swipeable swipeable swipeable
transition · 属性 transition transition transition transition transition
group · 属性 group selection selectedValue value value
rememberAs · 属性 rememberAs selection selectedValue value value
change · 事件 change selection onSelectedValueChange onChanged onValueChange
备注 Its own row: <tanrei-tab-view label="Project"> with tanrei-tab-panel children, and the attributes variant and full-width for the row. The view draws its row as a tanrei-tabs element of its own, placed as its first light-DOM child (in a tabs slot above the container) and marked as the view's, with one tanrei-tab per panel, and links itself to it as to any row. It is in the light DOM, not the shadow root, so that the tab's aria-controls and the panel's aria-labelledby point within one tree; the product does not edit it. The row's change stops at the view, which fires its own (TBV-18). Linked: <tanrei-tabs id="project-tabs"> … </tanrei-tabs> <tanrei-tab-view tabs="project-tabs">: tabs is the id of a tanrei-tabs in the same tree (as popovertarget is), and the tabsElement property sets the element itself; either one set means the view draws no row, even while the element is missing. The selected panel is the view's value, kept equal to its tabs' value in both directions; the view sets its own value before firing change, as a native control does. no-swipe is the inverse of swipeable, since an HTML boolean attribute defaults to false. The view writes its tabs' indicatorOffset property during a drag and its settle. The drag is Pointer Events on touch and pen with touch-action: pan-y on the container (not on the row), so vertical scrolling stays the browser's; the travel is a transform, the fade opacity and the blur filter: blur(), on the two panels only while a switch runs. The panels keep their place in the light DOM; a hidden one has hidden and inert. A shared choice: group and remember-as are attributes with the properties group and rememberAs, and the view writes both onto the tanrei-tabs it draws, which is the member of the group; a linked view has neither, and its tanrei-tabs carries them. A view that follows writes its panels' hidden and inert in the frame its row changes and starts no switch. For a committed swipe (TBV-21) the view measures and corrects its container's top as its row does for a tab (TAB-40). Sixty views of one group cost one read of localStorage, one write for a choice, and one measured element. size and track are forwarded to the tanrei-tabs the view draws, as variant is (track is true, false or null, with the attribute forms of the tabs'). transition is an attribute and a property (auto, slide, fade, none); auto reads --tanrei-comp-tab-view-motion-transition. fade runs the code path of prefers-reduced-motion (TBV-7) with the opacity end taken as 0; none runs the code path of a choice that came from the group (TBV-20) and sets transition="none" on the view's own row, removing it again when the form is no longer none. Its own row: TanreiTabView(selection: Binding<V>, variant:, fullWidth:, label:, swipeable:) { TanreiTabPanel … }; the view draws a TanreiTabs above its panels and links it through a TanreiTabLink of its own. Linked: TanreiTabView(selection:, link: TanreiTabLink, swipeable:) { … }, a second initializer without the row's parameters, so that a view cannot be given both; the link is an observable object the product creates once and passes to its tabs too (TanreiTabs(…, link:)), through which the tabs report their order and disabled tabs and read the indicator offset. Writing the binding is change: a tab on the view's own row and a swipe write it; a linked row writes it through its own binding. The drag is a DragGesture on the panels that yields to vertical scrolling; the blur is .blur(radius:), converted from the token's standard deviation. SwiftUI's page-style TabView is a full-width pager without a tab row, so it is not this component. A shared choice has no parameter here: views given the same Binding show the same choice, and the product remembers it in @AppStorage, as the tabs' mapping says. A view whose selection changes that way switches as TBV-2 says for a value the product sets, with its motion: this platform cannot tell a view that follows from one the product set, so TBV-20 and TBV-21 are not reached here, and the README says so. Not built on this platform yet: size and track are the row's, with the Tabs' meaning and type; transition (.auto, .slide, .fade, .none) is the form of a switch (TBV-22): fade is the crossfade the view already draws where motion is reduced, none the change at once it already draws for a choice made elsewhere (TBV-20), and under none the view's own row is given the tabs' transition none (TBV-23). Its own row: TanreiTabView(selectedValue, onSelectedValueChange, modifier, variant, fullWidth, contentDescription, swipeable) { TanreiTabPanel(…) { … } }; the view composes a TanreiTabs above its panels, linked through a link it remembers itself. Linked: an overload TanreiTabView(selectedValue, onSelectedValueChange, link, modifier, swipeable) { … } without the row's parameters, with val link = rememberTanreiTabLink() passed to TanreiTabs(…, link = link) too; the tabs read the indicator offset from it as a frame-time lambda, so a drag does not recompose the row. State hoisting: the caller keeps selectedValue; with its own row the view calls onSelectedValueChange for a tab and for a swipe, and linked the caller moves it from both components' callbacks. Not HorizontalPager: the travel is a short offset, not a scroll, and hidden panels stay composed (TBV-12). The blur is Modifier.blur, which draws nothing below Android 12; there the switch has no blur, as the README says. A shared choice has no parameter here: views given the same hoisted selectedValue show the same choice, and the caller remembers it in its own store, as the tabs' mapping says. A view whose selectedValue changes that way switches as TBV-2 says for a value the product sets, with its motion: this platform cannot tell a view that follows from one the product set, so TBV-20 and TBV-21 are not reached here, and the README says so. Not built on this platform yet: size and track are the row's, with the Tabs' meaning and type; transition (Auto, Slide, Fade, None) is the form of a switch (TBV-22): fade is the crossfade the view already draws where motion is reduced, none the change at once it already draws for a choice made elsewhere (TBV-20), and under none the view's own row is given the tabs' transition none (TBV-23). Its own row: TanreiTabView<T>(value:, onChanged:, variant:, isScrollable:, semanticLabel:, swipeable:, panels: [TanreiTabPanel<T>(…)]); the view builds a TanreiTabBar<T> above its panels, linked through a TanreiTabLink<T> of its own. isScrollable is the inverse of fullWidth and defaults to true (hug), as on the bar. Linked: TanreiTabView<T>.linked(link:, value:, onChanged:, swipeable:, panels:), a named constructor without the row's parameters, with the TanreiTabLink<T> the product creates and passes to TanreiTabBar(…, link:) too, as Flutter's TabController is shared by a TabBar and a TabBarView. It replaces the existing TanreiTabBarView, which named only the content; its semanticLabels give way to the tabs, through which each panel is named by its tab with Flutter's tab panel role. The blur is an ImageFiltered blur, converted from the token's standard deviation. A shared choice has no parameter here: views given the same value from one piece of the product's state show the same choice, and the product remembers it in its own store, as the bar' mapping says. A view whose value changes that way switches as TBV-2 says for a value the product sets, with its motion: this platform cannot tell a view that follows from one the product set, so TBV-20 and TBV-21 are not reached here, and the README says so. Not built on this platform yet: size and track are the row's, with the Tabs' meaning and type; transition (auto, slide, fade, none) is the form of a switch (TBV-22): fade is the crossfade the view already draws where motion is reduced, none the change at once it already draws for a choice made elsewhere (TBV-20), and under none the view's own row is given the tabs' transition none (TBV-23). Its own row: <TabView value onValueChange variant fullWidth accessibilityLabel swipeable> with <TabPanel> children; the view renders a <Tabs> above its panels from their props, linked through a link of its own. Linked: <TabView value onValueChange link swipeable>, where const link = useTabLink() is passed to <Tabs link={link}> too and carries the indicator offset as an animated value, so a drag does not re-render the row; the props type is a union, so that link and the row's props cannot be given together. Controlled: with its own row the view calls onValueChange for a tab and for a swipe; linked, the caller moves value from both components' callbacks. React Native has no tab panel role and cannot blur a view without a native module, so the switch draws its travel and fade only, as the README says. A shared choice has no prop here: views given the same controlled value show the same choice, and the product remembers it in its own store, as the tabs' mapping says. A view whose value changes that way switches as TBV-2 says for a value the product sets, with its motion: this platform cannot tell a view that follows from one the product set, so TBV-20 and TBV-21 are not reached here, and the README says so. Not built on this platform yet: size and track are the row's, with the Tabs' meaning and type; transition ("auto", "slide", "fade", "none") is the form of a switch (TBV-22): fade is the crossfade the view already draws where motion is reduced, none the change at once it already draws for a choice made elsewhere (TBV-20), and under none the view's own row is given the tabs' transition none (TBV-23).

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-tab-view-container-shape sys.shape.corner.none 0px
--tanrei-comp-tab-view-motion-fade-duration sys.motion.role.reduced 100ms
--tanrei-comp-tab-view-motion-fade-easing sys.motion.easing.state cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-tab-view-motion-switch-duration sys.motion.role.selection.travel 250ms
--tanrei-comp-tab-view-motion-switch-easing sys.motion.easing.spatial cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-tab-view-motion-transition — 0
--tanrei-comp-tab-view-transition-blur — 8px
--tanrei-comp-tab-view-transition-distance — 40px
--tanrei-comp-tab-view-transition-distance-fraction — 0
--tanrei-comp-tab-view-transition-opacity — 0

测试样例

作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。

生成自 specs/tab-view.spec.json, tokens/comp/tab-view.tokens.json, design/tokens/

指南

何时使用

当每个标签页都是同一主题的并列视图,而且用户经常在它们之间切换时,使用标签页视图:项目的“概览”“动态”和“设置”,或者同一个示例在几个平台上的写法。它在一行标签页的基础上增加了三样东西:一处声明同时得到标签页和它的面板、轻扫,以及由指示器和面板共同完成的切换。

如果面板只是普通内容,也没有人会去轻扫它们,那么一行带有自己面板的标签页就够了。标签页视图不是轮播,也不是翻页器:它为每个标签页显示一个面板,一次轻扫移动一个面板。供人浏览的一行内容项应该交给轮播。

自带一行标签页,还是关联

形态 你要写的内容 change 来自哪里 用于
自带一行标签页(默认) 只写面板。每个面板用 label 和 icon 为自己的标签页命名。 视图,标签页选中和轻扫都是。 大多数标签页视图:标签页紧挨在内容的上方。
关联(tabs) 在别处写一行 Tabs,再写一个指向它的、由面板组成的视图。 标签页选中来自标签页,轻扫来自视图。 与内容分开放置的标签页,例如放在粘性页眉里。
  • 被关联的那一行由它自己的属性决定外观、名称、组和键。关联形态的视图上的 variant、size、track、fullWidth、label、group 和 rememberAs 不起作用,设置了其中任何一个,调试构建都会报告。
  • 按阅读顺序,把关联形态的视图放在它的标签页之后,这样 Tab 键就能从选中的标签页移入面板。自带一行标签页的视图在结构上就保证了这一点。
  • 标签页没有徽标,所以面板也没有。要给标签页加徽标,请自己写那一行,再把视图关联过去。

轻扫

  • 每次轻扫一个面板。 用触摸或触控笔拖动时,如果横向的距离大于纵向的距离,面板就朝下一个或上一个可用的标签页移动,指示器跟随手指。松手时已过半程,或带有朝向该标签页的快速滑动,切换就会完成;否则退回原位。鼠标、触控板和滚轮不能轻扫:它们使用标签页。
  • 禁用的标签页会被跳过,在第一个或最后一个标签页处,面板保持静止。
  • 本身要横向拖动的内容保留自己的拖动。 横向滚动的区域、滑块、轮播、文本字段或选中的文本上的拖动,不会被当作轻扫。如果面板整体就要横向拖动,例如地图或绘图区域,请把 swipeable 设为 false,把切换交给标签页。
  • 在那一行标签页上拖动是标签页自己的手势:它可以越过多个标签页,显示中的面板保持等待,松手时才切换到所选标签页的面板。
  • 切换的方式由设计系统决定。 默认是一小段位移,加上淡入淡出和模糊;设计系统可以把它改成整幅宽度的翻页。在减弱动态效果时,只保留淡入淡出。

过渡

transition 指定显示中的面板以哪种形式让位给下一个面板。

值 它做什么
slide 面板按设计系统规定的方式位移,并跟随轻扫。
fade 两个面板在原位交叉淡入淡出。
none 新面板在一帧之内完整显示。视图自己绘制的那一行标签页,也在同一帧把指示器放到新的标签页上。
auto(默认) 采用设计系统的答案(comp.tab-view.motion.transition,默认系统中是 slide)。

供人阅读而不是观看的面板(例如代码),请使用 none。在 none 之下,轻扫仍然有效:面板保持静止,指示器跟随手指,松手时立即显示新面板。要关掉轻扫,请把 swipeable 设为 false。在减弱动态效果时,slide 按 fade 绘制,fade 和 none 则不增加任何东西。视图所关联的那一行标签页保留它自己的 transition。

共享同一个选择

同一个组的两个标签页视图:在任意一个里选一个标签页

用 npm 安装这个包。

用 pnpm 安装这个包。

用 Yarn 安装这个包。

用 npm 运行构建。

用 pnpm 运行构建。

  • 给回答同一个问题的每个视图指定同一个 group。组的成员是视图自己的那一行标签页,所以标签页页面关于组的所有说明都适用:选择按面板的 value 对应,从不按它的标签;没有这个值的面板的视图保持自己的选择。在这个演示中,Yarn 只存在于第一个视图。
  • 只有用户操作的那个视图会执行切换并触发 change。其他每个视图立即显示自己的面板,没有位移、淡入淡出和模糊,保留它所隐藏的面板的状态,也不夺取焦点。
  • 即使它上方的视图改变了高度,用户操作的那个视图在窗口中的位置也保持不变。
  • rememberAs 指定一个键,选择以这个键保存在用户的设备上,并在视图首次绘制之前恢复。没有键就什么都不保存,保存的也只有面板的值。在 Web 上,这个键是 localStorage 的一个键,原样使用。这个演示没有指定键。
  • 关联形态的视图不使用这两个属性:被关联的那一行指定自己的组和键。

尺寸与面板

  • 视图与父元素同宽,高度等于它那一行标签页加上显示中的面板。面板高度不等是正常的,所以面板切换时,视图之后的内容会移动。
  • 如果不希望下方的任何内容移动,就给视图一个自己的高度。那一行标签页留在顶部,每个面板填满剩余的空间,并在其中滚动。
  • 隐藏的面板留在原处并保留状态:其中输入过的内容,以及滚动到的位置。视图没有懒加载模式:开销大的面板,请在它的标签页首次被选中时再渲染内容。

推荐

  • 为每个面板提供 label,用 labelHidden 只显示图标时也要提供:它为标签页命名,并通过标签页为面板命名。
  • 面板要么都带图标,要么都不带。
  • 让每个面板的 value 在它所在的视图内唯一,并且在屏幕显示期间保持稳定。
  • 一个屏幕上有不止一行标签页时,用 label 为视图命名。

避免

  • 不要在面板里嵌套同一变体的标签页视图。把次要标签页放在主要标签页的视图里。
  • 不要指望靠轻扫来表明还有更多视图。说明这一点的是标签页;轻扫只是捷径。
  • 不要修改视图为自己绘制的那一行标签页。改动面板,那一行会随之变化。
  • 不要用标签页视图在应用的各个目的地之间移动。那是导航栏或导航抽屉的职责。

无障碍

角色
tabpanel
标注
The row the Tab view draws itself is a tab list named by the Tab view's label, each tab named by its panel's label. Each panel is named by the tab of the same value, in the Tab view's own row or in the linked row, and each tab controls its panel. The Tab view itself is not named: it has no role of its own beyond its row and its panels.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
Tab / Shift+Tab Moves focus from the selected tab (in the Tab view's own row, or in the linked row) into the shown panel (its first focusable element, or the panel itself when it holds none), and back.
Right Arrow / Left Arrow, Home / End (on a tab) Select another tab (TAB-12), and the Tab view switches to its panel. The Tab view adds no keys of its own: inside a panel the arrow keys belong to its content.

备注

  • The Tab view has no target of its own: its own row's tabs meet the minimum target as every tab does (TAB-5), and its panels hold the product's controls, which meet it themselves.
  • The swipe is never the only way (WCAG 2.5.1): every panel is reached by a single tap on its tab, and by the arrow keys on the tabs. With a screen reader running, a one-finger swipe belongs to the reader; the tabs remain the way to switch panels.
  • The swipe of the view and the drag across its tabs are one gesture in two places (TBV-3, TBV-4; TAB-28 to TAB-30): the tabs' active indicator follows the finger, the selection is made on release, from halfway or by a fling, a cancelled gesture returns the indicator, nothing is announced before the release, and under reduced motion the indicator still follows the finger and lands at once. They differ in reach. A swipe of the view moves to the neighbouring panel, and the panels travel with the finger. A drag across the row can pass several tabs, so the panels wait: the shown panel stays at rest during the drag, and the release switches to the chosen tab's panel as any selected tab does (TBV-2, TBV-6), its switch and the indicator's settling running over equal times.
  • One declaration makes the tab and its view: while the Tab view draws its own row, each panel carries its tab's label, icon, labelHidden and disabled, so the tabs and the panels cannot fall out of step. Its row is a Tabs, not a look-alike: the same tokens, the same variants and the same behaviours, so a system that restyles the tabs restyles the Tab view's row too.
  • Linked, for tabs placed elsewhere: a product whose tabs sit in a top app bar or a sticky header, apart from their content, sets tabs to name that row, and the Tab view draws none. The linked row names the panels, and its own props decide its look. The Tab view must then follow its tabs in reading order, so that Tab moves from the selected tab into the panel; a Tab view with its own row meets that by construction.
  • Selection belongs to the Tab view and its tabs together: they hold the same value, and each selection is announced once. With its own row, the Tab view announces every selection (TBV-18). Linked, the component the person used announces it: the linked tabs' change for a tab, the Tab view's change for a swipe; a product that keeps the selection itself updates it from both.
  • The panels' role, name and association are the Tab view's; the tabs' own panels (TAB-13) are for a row without a Tab view. Web: `tanrei-tab-panel` children of `tanrei-tab-view` get role tabpanel, aria-labelledby and the tab's aria-controls, and the view's own row is in the same tree as its panels so that these references resolve. SwiftUI, Compose, Flutter and React Native have no association between a tab and its panel; there each panel is named by its tab (Flutter exposes its tab panel role) and follows the row in reading order, and each README says so.
  • A panel that is not shown stays in place, hidden, with its state. A product whose panels are costly renders their content when their tab is first selected; the Tab view has no lazy mode of its own yet (it can be added as a prop without breaking this spec).
  • A tab of the Tab view's own row carries what a tab of the Tabs carries, and nothing more: the Tabs have no badge, so neither does a panel. A product that marks a tab with a badge writes its row itself and links it; a badge on the Tabs would reach the panel as one more tab prop.
  • Tab view is not a carousel and not a pager: it shows one panel at a time, one per tab, and a person moves one panel per swipe. A row of items to browse belongs to the carousel.
  • How a switch looks is a value decision of the active design system (tokens.md): a short travel with a fade and a blur by default, a full-width page turn in a system that pages.
  • A Tab view in a group (group) is the usual home of a shared choice: the same sample in several languages or platforms, many times on one page. Its own row is the member (TBV-16), so everything the Tabs say of a group holds: the choice is matched by value, the views that follow change silently and at once and keep their hidden panels' state (TBV-20), the row the person used stays where it is on the screen (TAB-40, TBV-21), and remembering is off unless the product names a key (TAB-39).
  • Panels of unequal height are the normal case for samples, and the view is as tall as its shown panel (TBV-11). What follows a view therefore moves when its panel changes; what a person is using does not. A product that wants no movement at all below a view sizes the view itself, and the panels then scroll inside it (TBV-11).
  • The effect of switching views has two parts with two owners: the panels' change is the Tab view's (transition: slide, fade or none; TBV-22), and the indicator's travel to the new tab is the row's (the tabs' transition: slide or none; TAB-43). Set on a Tab view that draws its row, none covers both (TBV-23), so that "no effect" means that nothing moves; fade and slide leave the row to the system. A row placed by the product, linked or alone, is set by its own prop.
  • transition changes what is drawn and never what is told: the selection, the panel's exposure and the announcement are made in the same frame in every form (TBV-9, TBV-22). None is the calm choice for panels that are read rather than watched — a code sample in several languages, a table in several units — and for a page that holds many views sharing one choice.
  • Under fade the panels finish (comp.tab-view.motion.fade-duration) before the row's indicator lands (comp.tabs.motion.indicator-duration), as they do where motion is reduced; the rule that the switch and the indicator land together holds for slide.
  • A swipe under none still switches panels and still shows the finger its progress, on the row's indicator; the panels wait for the release. A product that wants no swipe sets swipeable to false, which is a separate decision.

生成自 specs/tab-view.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • 视图自己的那一行是一个标签页列表,以视图的 label 命名;每个标签页以它的面板的 label 命名。视图本身没有角色。
  • 显示中的面板是一个标签页面板,以它的标签页命名并与之关联,无论那个标签页在视图自己的那一行里,还是在被关联的那一行里。其他每个面板都对辅助技术隐藏,也不能获得焦点,包括切换或轻扫过程中仍在绘制的面板。暴露给辅助技术的面板在选择改变时就更换,而不是等动效结束。
  • Tab 键从选中的标签页移入显示中的面板:移到面板里第一个可聚焦的元素;面板里没有这样的元素时,移到面板本身。这样的面板会在自己的边缘内侧显示焦点环,并且只在键盘聚焦时显示。
  • 如果切换隐藏了焦点所在的面板,焦点会移到选中的标签页上。
  • 视图不增加任何自己的按键。在标签页上按方向键、Home 和 End 会选中另一个标签页,视图随之切换。在面板内,方向键属于面板的内容。
  • 轻扫绝不是唯一的方式:每个面板都可以通过轻点它的标签页或使用方向键到达。屏幕阅读器运行时,单指轻扫属于屏幕阅读器,切换仍然要靠标签页。
  • 轻扫松手之前不朗读任何内容,被取消的轻扫不改变任何东西。
  • 跟随所在组的视图悄悄地变化:不朗读任何内容,焦点留在用户所在的位置。如果焦点原本在这次变化所隐藏的面板里,它会移到该视图选中的标签页上。
  • 在从右到左的布局中,轻扫和位移随阅读方向镜像。视图不会镜像面板的内容。
  • 调试构建会报告:不对应任何一行标签页的 tabs、没有面板的选中标签页、自带一行标签页却没有面板的视图、值为空或重复的面板、自己还带有面板的被关联行,以及在关联形态的视图上设置的行属性。

已知的平台差距:

  • SwiftUI、Compose、Flutter 和 React Native 尚未实现标签页视图。
  • Web:每个面板都是一个滚动容器,所以紧贴面板边缘的控件的焦点环会被裁切。
  • Web:行为测试只在 Chromium 中运行,轻扫只用合成的指针事件测试。

需要你做的事

  • 为每个面板提供 label;一个屏幕上有不止一行标签页时,也为视图提供 label。
  • 按阅读顺序,把关联形态的视图放在它的标签页之后。
  • 使用关联形态时,两个 change 事件都要用来更新你自己的状态:标签页的和视图的。
  • 面板本身要横向拖动时,把 swipeable 设为 false。
  • 同一个组里的各个视图,相同的值要用相同的标签;它们相距较远时,在第一个视图附近说明这个选择适用于整个页面。
  • rememberAs 的键由你自己选定;如果你的产品需要隐私声明,请在其中写明它。