- Web
tanrei-carouselschedule - SwiftUI
TanreiCarouselschedule - Jetpack Compose
TanreiCarouselschedule - Flutter
TanreiCarouselschedule - React Native
Carouselschedule
生成自 docs/status/status.json, specs/carousel.spec.json
规格已修订,尚未覆盖所有平台
本页描述的是规格当前所定的轮播。layout 属性被几项相互独立的选择取代(主项的位置、流动宽度还是固定宽度、缩小、停靠、循环),精选行还可以显示圆点并自动前进。主项(hero)现在默认居中,相邻的项在任何滚动位置都相隔一个间距。Web 组件已经做到了这一切,演示也是如此。SwiftUI、Compose、Flutter 和 React Native 在上方标记为“待跟进”:在跟上之前,它们提供的是上一版,有 layout,没有循环、圆点、自动播放以及上一个和下一个控件。参见主项默认居中和从 layout 迁移。
默认:主项居中,旁边是一个小项
轮播只有一种项:item。各平台接收这些项的方式,与它接收任何重复子元素的方式相同:在 Web 上是 <tanrei-carousel-item> 子元素,在 SwiftUI 上是视图构建器中的 TanreiCarouselItem 视图,在 Compose 上是在项 lambda 中调用 TanreiCarouselItem,在 Flutter 上是 children 中的 TanreiCarouselItem 组件,在 React Native 上是 CarouselItem 子元素。
每一项是一个操作。它容纳你的内容(通常是一张图片),并在用户激活它时触发 click(因平台而异,是 action、onClick、onTap 或 onPress)。它的 label 就是它的无障碍名称:写明内容展示的是什么,例如“海岸步道,12 张照片”。内容本身对辅助技术隐藏,其中不得再放自己的控件。
整行围绕一个突出展示的项来排列,这一项称为主项。怎么排由几个相互独立的属性决定,每个属性的默认值都适合一个供人浏览的行:主项居中,它旁边的项较窄,滚动结束时整行停靠在完整的一项上。itemWidth 是主项的宽度;不设置时采用设计系统的默认值。精选行可以加上 loop、圆点(indicator)和 autoplay。没有 autoplay 时,轮播没有需要受控的值:只有用户滚动它或移动焦点时,它才会滚动。
在这一行的两端之上,有“上一个”和“下一个”控件,每次把这一行移动一项。默认情况下,它们在使用鼠标或触控板时绘制,并在指针位于轮播上方或焦点在轮播内时出现。请用你产品的语言为它们提供 previousLabel 和 nextLabel。参见上一个和下一个控件。
Web 和 React 的示例展示的是当前规格。SwiftUI、Compose、Flutter 和 React Native 的示例展示的是上一版,也就是这些平台目前提供的版本:有 layout(默认是一行并排多个项,另有 hero 和 uncontained)和 itemWidth,没有循环、圆点和自动播放。
<tanrei-carousel previous-label="上一条步行路线" next-label="下一条步行路线" label="附近的步行路线">
<tanrei-carousel-item label="海岸步道,12 张照片"><img src="coast.jpg" alt="" /></tanrei-carousel-item>
<tanrei-carousel-item label="老城区,8 张照片"><img src="town.jpg" alt="" /></tanrei-carousel-item>
<tanrei-carousel-item label="森林环线,20 张照片"><img src="forest.jpg" alt="" /></tanrei-carousel-item>
</tanrei-carousel>
<!-- 精选行:无限循环,带圆点,自动前进。主项默认居中。 -->
<tanrei-carousel previous-label="上一条步行路线" next-label="下一条步行路线" label="精选步行路线" loop indicator autoplay
play-label="播放幻灯片" pause-label="暂停幻灯片">…</tanrei-carousel>
<!-- 从起始边缘排起的普通行:每一项宽 120(CSS 像素数),滚动在哪里结束就停在哪里。
fixed-width 和 free-scroll 分别是默认为 true 的 fluidWidth 和 snap 的反义。 -->
<tanrei-carousel previous-label="上一条步行路线" next-label="下一条步行路线" label="最近浏览" hero-alignment="start" fixed-width free-scroll item-width="120">…</tanrei-carousel>
<script type="module">
import '@tanrei/web/components/carousel';
for (const item of document.querySelectorAll('tanrei-carousel-item')) {
item.addEventListener('click', () => openWalk(item.label));
}
// 只有播放控件会触发它;暂缓不会,你设置 paused 时也不会。
document.querySelector('[label="精选步行路线"]').addEventListener('pausedchange', (e) => savePaused(e.detail.paused));
</script>import { Carousel, CarouselItem } from '@tanrei/react';
<Carousel label="最近浏览" itemWidth={120} fluidWidth={false} snap={false}>
{walks.map((walk) => (
<CarouselItem key={walk.id} label={walk.label} onClick={() => openWalk(walk)}>
<img src={walk.image} alt="" />
</CarouselItem>
))}
</Carousel>
<Carousel label="精选步行路线" loop indicator autoplay paused={paused}
onPausedChange={(e) => setPaused(e.detail.paused)}
previousLabel="上一条步行路线" nextLabel="下一条步行路线"
playLabel="播放幻灯片" pauseLabel="暂停幻灯片">
{featured.map((walk) => <CarouselItem key={walk.id} label={walk.label} onClick={() => openWalk(walk)}><img src={walk.image} alt="" /></CarouselItem>)}
</Carousel>import Tanrei
// 上一版:由 layout 选择排列方式。.hero 每次突出展示一项。
TanreiCarousel("精选步行路线", layout: .hero) {
ForEach(walks) { walk in
TanreiCarouselItem(walk.label, action: { open(walk) }) {
WalkImage(walk)
}
}
}import androidx.compose.ui.unit.dp
import io.github.chnotchy.tanrei.components.TanreiCarousel
import io.github.chnotchy.tanrei.components.TanreiCarouselItem
import io.github.chnotchy.tanrei.components.TanreiCarouselLayout
// 上一版。和 Material 的轮播一样接收一个项 lambda。itemWidth 是 Dp,为 null 时取令牌的值。
// Uncontained 是普通的一行:每一项宽度相同,在边缘处被裁切。
TanreiCarousel(
itemCount = walks.size,
contentDescription = "最近浏览",
layout = TanreiCarouselLayout.Uncontained,
itemWidth = 120.dp,
) { index ->
val walk = walks[index]
TanreiCarouselItem(label = walk.label, onClick = { open(walk) }) {
WalkImage(walk)
}
}import 'package:tanrei/tanrei.dart';
// 上一版:由 layout 选择排列方式。
TanreiCarousel(
semanticLabel: '精选步行路线',
layout: TanreiCarouselLayout.hero,
children: [
for (final Walk walk in walks)
TanreiCarouselItem(label: walk.label, onTap: () => open(walk), child: WalkImage(walk)),
],
),import { Image } from 'react-native';
import { Carousel, CarouselItem } from '@tanrei/react-native';
// 上一版的默认布局:一行并排多个项。
<Carousel accessibilityLabel="附近的步行路线">
{walks.map((walk) => (
<CarouselItem key={walk.id} label={walk.label} onPress={() => openWalk(walk)}>
<Image source={walk.image} style={{ flex: 1 }} />
</CarouselItem>
))}
</Carousel>宽到上限的主项,以及从起始边缘自由滚动的固定宽度
排列这一行:固定宽度、缩小、循环、内容靠向主项
fixed-width free-scrollfixed-width scale-others looploopitem-width=“120” content-alignment=“toward-hero” loop圆点、自动播放、进度、已暂停,以及单独的播放控件
indicatorindicator autoplayautoplay-progresspaused在开启了“减弱动态效果”设置的环境中,自动播放从不运行:这时自动播放的演示静止不动,不显示播放控件,也不显示进度。
构成
| 部件 | 说明 |
|---|---|
1 row | The row of items: as wide as the carousel, scrolling sideways, padded at its start and end. Items scroll under the padding to its edges, every two neighbours the same gap apart. |
2 item | One item of the row (an item of the item kind). The item in focus is the hero, in the middle of the row unless heroAlignment puts it at the start; with fluid widths the others narrow as they move away from it, and with scaleOthers they are drawn smaller. |
3 item.container | The item's rounded box. It clips the item's content, fills what the content leaves uncovered, and carries hover, focus and press. |
4 item.content | What the product puts in the item (an image, a poster). The carousel lays it out at the hero's width and aligns it in the container (centred, or toward the hero), so a narrower item shows part of it. |
5 focus-ring · 可选 | Gap-then-ring indicator for keyboard focus: inside an item's container, which the row would otherwise clip, and around the indicator's or the playback control's container. |
6 previous · 可选 | The previous control: an icon-only Button over the row's inline start, centred on the row's height, that moves the row back by one resting position. Drawn as navigation says, while there are at least two resting positions (CRL-41). |
7 next · 可选 | The next control: an icon-only Button over the row's inline end, centred on the row's height, that moves the row on by one resting position. Drawn as navigation says, while there are at least two resting positions (CRL-41). |
8 controls · 可选 | The controls row directly below the items, drawn while the indicator or the playback control is. It does not scroll. |
9 indicator · 可选 | The dots, centred in the controls row: one per resting position, the current one stretched, the stretch following the scroll. One control (a slider) whose container carries hover, focus and press. |
10 indicator.dot | One dot: a pill of the dot size, stretched toward the active width as the row nears its position; the current dot may fill to show the time until autoplay moves on. |
11 playback · 可选 | The playback control at the end of the controls row, drawn while autoplay is on: an icon-only button, a circle, that pauses and plays. |
12 playback.icon | The glyph of the icon set: `pause` while autoplay is playing, `play_arrow` while it is paused. Never mirrored. |
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
heroAlignment | start | center | "center" | Where the item in focus (the hero) sits: in the middle, with items on both sides (the default), or at the inner start edge (the left in a left-to-right language), with items after it only. Not a token axis: geometry, read from the same tokens. |
itemWidth | number | null | The width the hero is laid out at, in the platform's unit (CSS px, pt, dp, logical pixels); null takes comp.carousel.container.large-width. With fixed widths every item is this wide (at most the inner width); with fluid widths the hero is kept between a floor and a cap that leave room for the others (CRL-9). Choose it from the content's shape: a product's posters and thumbnails differ. |
fluidWidth | boolean | true | Whether item widths change fluidly: true narrows the items beside the hero (on both sides of a hero in the centre) to a medium and a small width, each width following the scroll; false makes every item the hero's width, the ones that do not fit cut by the carousel's edges. |
contentAlignment | center | toward-hero | "center" | With fluid widths, where a narrowed item's content is anchored: center shows its middle; toward-hero aligns the items after the hero to their start and the items before it to their end, so each shows the side nearest the hero. Ignored while fluidWidth is false. |
scaleOthers | boolean | false | Whether the items other than the hero are drawn smaller, down to comp.carousel.container.min-scale, the scale following the scroll and the gap between drawn items staying the same. It applies with fluid or fixed widths. |
snap | boolean | true | Whether the row settles at an item when a scroll ends; false lets it rest wherever the scroll ends. |
loop | boolean | false | Whether the row is endless: scrolling past the last item continues to the first, and back. Takes effect only with two or more items; with fewer it is ignored. |
navigation | auto | always | none | "auto" | Whether the previous and next controls are drawn over the ends of the row (CRL-41, CRL-42). auto draws them where the primary pointer is fine and can hover — a mouse or a trackpad, where no finger swipes the row — and shows them while the pointer is over the carousel or focus is within it; always draws them on every device and shows them at all times; none draws neither. Keep auto wherever you can: without the controls a person with a mouse moves the row only with Shift and the wheel, or with the indicator where it is drawn, and never with a single press (WCAG 2.5.7). |
indicator | boolean | false | Whether the dots are drawn below the items: one per resting position, the current one stretched. For a short featured row; a long row to browse needs none. |
autoplay | boolean | false | Whether the row advances by itself, one item every autoplayInterval. It holds while the carousel is hovered, has focus within, is pressed or is hidden, and never runs where motion is reduced (CRL-32 to CRL-34). |
autoplayInterval | number | null | How long the row rests at an item before autoplay moves on, in milliseconds; null takes comp.carousel.autoplay.interval. Give text-heavy items more time than images. |
paused | boolean | false | Whether autoplay is paused, by the person (with the playback control) or by the product. The playback control changes it and emits pausedChange; a product that sets it emits nothing. |
playbackControl | boolean | true | Whether the playback control is drawn while autoplay is on. A product may hide it, but should keep it wherever it can: on touch there is no hover, and without it a person cannot stop the motion (WCAG 2.2.2). |
autoplayProgress | boolean | false | Whether the current dot fills to show the time until autoplay moves on. Needs indicator; ignored while autoplay is off or paused. |
playLabel | string | "" | The playback control's accessible name while autoplay is paused ("Play the slideshow"). Required while the control can be drawn. |
pauseLabel | string | "" | The playback control's accessible name while autoplay is playing ("Pause the slideshow"). Required while the control can be drawn. |
previousLabel | string | "" | The previous control's accessible name ("Previous walk"). Required while the control can be drawn. |
nextLabel | string | "" | The next control's accessible name ("Next walk"). Required while the control can be drawn. |
label | string | "" | Names the carousel's list, and its indicator, for assistive technology ("Recently played"). Required. |
事件
| 事件 | 说明 |
|---|---|
pausedChange | The person paused or played autoplay with the playback control; carries the new paused value. Not emitted when the product sets paused, nor for the holds of CRL-33. |
条目
该组件重复排列的条目种类。测试样例按顺序列出条目;各平台以各自的惯用方式为每个种类命名。
item
One item of the row: the product's content in a rounded box, one action named by its label.
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
label | string | "" | The item's accessible name: what its content shows ("Coast path, 12 photos"). Required; the content itself is not read. It is also the indicator's value text while the item is in focus. |
| 插槽 | 说明 |
|---|---|
default | The item's content. A fixture item's content is the name of a glyph: the harness draws it centred in the content box (the hero's width, the row's height) at sys.icon.medium through the icon helper, with the family and fill of an unselected icon, in the item's content color, on the item's container color. |
| 事件 | 说明 |
|---|---|
click | The item was activated by pointer, touch, keyboard or assistive technology. |
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 条目 | tanrei-carousel-item | TanreiCarouselItem | TanreiCarouselItem | TanreiCarouselItem | CarouselItem |
label · 属性 | label | label | label | label | label |
click · 事件 | click | action | onClick | onTap | onPress |
| 备注 | A <tanrei-carousel-item> child; its content is its default slot. | TanreiCarouselItem(_ label: String, action: @escaping () -> Void) { content }. | TanreiCarouselItem(label, onClick) { content }, called from the carousel's item lambda. | TanreiCarouselItem(label:, onTap:, child:), one of the carousel's children. | <CarouselItem label onPress>{content}</CarouselItem>, a child of Carousel. |
状态
行为
以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。
| ID | 需求 |
|---|---|
CRL-1 | WHEN the user activates an item by pointer, touch, Enter or Space THE carousel SHALL emit that item's click exactly once; a press that turns into a scroll (a drag beyond the platform's touch slop, a fling) SHALL NOT activate the item it started on. |
CRL-2 | WHILE an item, the indicator or the playback control is hovered, keyboard-focused or pressed THE carousel SHALL render that part's container, content color and opacity, and an item's shape, from the component tokens of its highest-precedence active state, with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state. |
CRL-3 | WHEN an item, the indicator or the playback control receives keyboard focus THE carousel SHALL show the focus ring without animation, inside an item's container, which the row would otherwise clip, and around the indicator's or the playback control's container; focus gained by pointer or touch SHALL NOT show it. |
CRL-4 | THE carousel SHALL make each item's hit area its drawn container with half of comp.carousel.layout.gap on each side, which for a small item at comp.carousel.container.small-min-width is sys.target.touch wide with the default tokens, and SHALL NOT let the hit area of an item that fills a slot fall below sys.target.pointer wide for a fine pointer or sys.target.touch for touch; a system whose widths give less SHALL still extend the hit area without changing the drawn item, and the row's height SHALL NOT be below the target either. An item on its way between the last slot and an edge slot (CRL-12) is narrower than any slot: its hit area SHALL be its drawn container with half of the gap on each side and no wider, so that it never covers its neighbour's. |
CRL-5 | WHEN a carousel item or the playback control is pressed by pointer, touch, Enter or Space, or the indicator by pointer or touch, THE carousel SHALL show that part's pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner; the indicator's SHALL last as long as the press does. |
CRL-6 | WHILE a carousel item, the indicator or the playback control is pressed THE carousel SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole control and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; content color, opacity and shape still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.carousel.motion.feedback-duration with comp.carousel.motion.state-easing, fades over comp.carousel.motion.state-duration with comp.carousel.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced. |
CRL-7 | THE carousel SHALL ease the corners of its items' containers, the indicator's container and the playback control by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0) and show keyboard focus as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README. |
CRL-8 | THE carousel SHALL be as wide as its parent; its row of items SHALL be comp.carousel.layout.height tall unless the product sizes the carousel through the platform's own sizing, in which case the row takes that height less the controls row (CRL-28), and every item SHALL be as tall as the row. It SHALL lay its items out in one row in the order given, comp.carousel.layout.gap apart (g), inside comp.carousel.layout.padding-inline at its start and its end: the inner width W is the carousel's width less that padding twice. Items SHALL scroll under the padding to the carousel's edges and be clipped there. |
CRL-9 | THE carousel SHALL take L from itemWidth, or from comp.carousel.container.large-width while itemWidth is null, and SHALL make the hero slot H wide. WHILE fluidWidth is false H SHALL be L, at most W. WHILE fluidWidth is true H SHALL be L kept between a floor and a cap: WHILE heroAlignment is center, the floor (W − 4 × g − 2 × comp.carousel.container.small-max-width) / 3 and the cap W − 2 × (g + comp.carousel.container.small-max-width); WHILE heroAlignment is start, the floor (W − 2 × g − comp.carousel.container.small-max-width) / 2 and the cap W − g − comp.carousel.container.small-max-width. IF fluidWidth is true and that cap is less than comp.carousel.container.small-max-width THE carousel SHALL lay the row out as it does with fixed widths (CRL-13), H being W. |
CRL-10 | WHILE fluidWidth is true THE carousel SHALL fill the width E that the hero slot leaves on each side where items follow it (on both sides, E = (W − H) / 2 − g, WHILE heroAlignment is center; after it, E = W − H − g, WHILE heroAlignment is start) with slots g apart, going outward from the hero: IF E − g is less than 2 × comp.carousel.container.small-min-width, one small slot E wide; otherwise a medium slot M wide and then a small slot S wide, where S is (E − g) / 3 kept between comp.carousel.container.small-min-width and comp.carousel.container.small-max-width and M = E − g − S. IF M is then greater than H, M SHALL be H and S SHALL be E − g − H. Widths SHALL NOT be rounded before the items are placed; CRL-40 says where a platform that draws on whole device pixels puts the rounding. |
CRL-11 | THE carousel SHALL place the hero slot, where the item in focus sits, WHILE heroAlignment is center with its middle at the middle of the inner width (its start h at (W − H) / 2), and WHILE heroAlignment is start at the inner start edge (h is 0); and SHALL place the other slots of CRL-10, on both sides of it at the centre and after it at the start, each one g beyond the slot before it going outward, measured between the slots' drawn edges. |
CRL-12 | THE carousel SHALL place its items by a scroll position p, a number of items that advances by one for every H + g the row is scrolled. WHILE loop is not in effect (CRL-27) p SHALL run from 0 to N − 1 (N the number of items), except where CRL-13 ends the range sooner for a hero at the start; the resting positions are the whole numbers in the range and, where the range ends at a fraction, its end, D of them in all, so D is N wherever the range is not ended sooner. Item i, counted from 0, sits at slot position s = i − p. Slot 0 is the hero slot and slots 1, 2 and on follow it toward the end; WHILE heroAlignment is center slots −1, −2 and on precede it toward the start, and WHILE heroAlignment is start no slot precedes it (CRL-10, CRL-11, CRL-13). WHILE fluidWidth is true, beyond the last slot on each side lies an edge slot of no width, g beyond that slot — before a hero at the start, g before the hero slot itself — so that at a whole p nothing is drawn beyond the last slots; an item at or beyond an edge slot SHALL NOT be drawn. An item's start and width SHALL be the linear interpolation between those of the two slots around s. So at p = 0 the first item is the hero and nothing is drawn before it, and as p grows each item moves toward the start, growing from nothing at the edge slot to H as it reaches slot 0 and narrowing again to nothing as it leaves, a hero at the start narrowing against the inner start edge; at the end of the range the last item is the hero and the slots after it are empty. |
CRL-13 | WHILE fluidWidth is false THE carousel SHALL make every slot H wide, slot k starting at h + k × (H + g), h being the hero slot's start (CRL-11), with no edge slot: the slots go on beyond the carousel's edges, where the items are clipped (CRL-8), so that CRL-12's interpolation starts item i at h + s × (H + g). WHILE scaleOthers is also true the slots other than the hero's take the widths and the places CRL-26 gives them. WHILE heroAlignment is start, scaleOthers is false and loop is not in effect, the range of p SHALL end where the last item ends at the inner end edge, at the greater of 0 and (N × (H + g) − g − W) / (H + g), so that WHILE every item fits in W the row does not scroll. |
CRL-14 | WHEN the user scrolls the carousel by touch, pen, trackpad, a horizontal wheel or Shift with a vertical wheel THE carousel SHALL follow the scroll; WHILE snap is true, WHEN the scroll ends (a drag released, a wheel or trackpad gesture over) THE carousel SHALL settle at a resting position: the next one in the direction of a fling, or else the nearest one, over comp.carousel.motion.scroll-duration with comp.carousel.motion.scroll-easing, at once where motion is reduced; WHILE snap is false it SHALL come to rest wherever the scroll ends. 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 carousel 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. A horizontal wheel, a trackpad and Shift with a vertical wheel are physical in every layout direction: THE carousel SHALL move the row the way the platform's own scrolling containers move for that input and SHALL NOT reverse it in a right-to-left layout, so that Shift with a wheel turned down, which brings into view what lies further to the right, moves toward later items in left-to-right and toward earlier items in right-to-left. |
CRL-15 | THE carousel SHALL lay each item's content out H wide and as tall as the row, and SHALL clip it to the item's container and its comp.carousel.container.shape corners; WHILE fluidWidth is false or contentAlignment is center the content SHALL be centred on the container, and WHILE fluidWidth is true and contentAlignment is toward-hero an item after the hero (s above 0) SHALL align the content's start edge with its container's start edge and an item before it (s below 0) the content's end edge with its container's end edge, so that a narrowed item shows the side of its content nearest the hero and the content glides inside the item as it widens and narrows. |
CRL-16 | THE carousel SHALL fill each item's container with comp.carousel.item.container.color at rest, under its content, whatever the item's state, and SHALL hand the content comp.carousel.item.content.color as the platform's inherited foreground (the web's inherited color, SwiftUI's foreground style, Compose's content color, Flutter's default text and icon style, React Native's Tanrei text context). Because an item's content usually covers it (an image), the carousel SHALL draw the state layer over the content: comp.carousel.item.state-layer.color at sys.state.hover-state-layer-opacity, sys.state.focus-state-layer-opacity or sys.state.pressed-state-layer-opacity for the item's highest-precedence state, and the ripple of CRL-6 in the same layer, so hover, focus and press show on any content; the container color under the content is the resting fill only. |
CRL-17 | THE carousel SHALL be at most five Tab stops, in this order: the playback control while it is drawn (CRL-35), so that the motion can be stopped before anything else is reached; the previous and then the next control while they are drawn (CRL-41), before the items, so that a person can move the row before reading it; the items, as one stop, at the item that last had keyboard focus in it or else at the resting item (the item at slot 0 while fluidWidth is true, heroAlignment is center or snap is true, and otherwise the first item wholly in view); and the indicator while it is drawn (CRL-29). WHILE an item has keyboard focus Right Arrow and Left Arrow SHALL move focus to the next and previous item, from the last to the first and the first to the last WHILE loop is in effect and otherwise without wrapping, and Home and End to the first and last item; in a right-to-left layout Left Arrow SHALL move to the next item and Right Arrow to the previous. |
CRL-18 | WHEN an item receives keyboard focus or assistive-technology focus THE carousel SHALL scroll, over comp.carousel.motion.scroll-duration with comp.carousel.motion.scroll-easing (at once where motion is reduced), so that WHILE fluidWidth is true, heroAlignment is center or snap is true the item sits at slot 0 or, where the range ends first, at the resting position nearest it, going the shorter way round WHILE loop is in effect; and otherwise so that the whole item is in view, scrolling as little as that needs. |
CRL-19 | THE carousel SHALL expose its items as a list named by label, and each item as one element with the button role, named by its label, with its position among the items and their number; it SHALL hide each item's content from assistive technology (the label names the item), and SHALL keep items that are out of view exposed, in order, so that a screen reader reaches every item (CRL-18 brings it into view). The indicator (CRL-31), the playback control (CRL-35) and the previous and next controls (CRL-41) SHALL be outside the list and SHALL NOT be counted among its items. |
CRL-20 | WHILE an item's label is empty THE carousel SHALL still render it, and in debug builds report that it has no accessible name; WHILE the carousel's label is empty it SHALL report that the list and the indicator have no name; WHILE autoplay and playbackControl are true and playLabel or pauseLabel is empty it SHALL report the missing name; WHILE the previous and next controls are drawn (CRL-41) and previousLabel or nextLabel is empty it SHALL report the missing name; and WHILE indicator is true and the dots do not fit (CRL-29) it SHALL report that they are not drawn. An item's content SHALL contain no interactive element; a platform that can inspect it (the web, on every slot change) SHALL report one in debug builds, and a platform that cannot SHALL state the rule in its README. |
CRL-21 | WHEN the carousel's width or its items change THE carousel SHALL arrange its slots again and keep the resting item (CRL-17) where it was, as far as the new range allows, without motion; WHEN the number of items falls below two it SHALL stop looping (CRL-27) and advancing (CRL-32), and no longer draw the indicator, the playback control or the previous and next controls. |
CRL-22 | WHEN the colors of an item, the indicator or the playback control change between states THE carousel SHALL animate them over comp.carousel.motion.state-duration with comp.carousel.motion.state-easing, and SHALL change them at once where motion is reduced. |
CRL-23 | WHEN the layout direction is right-to-left THE carousel SHALL mirror its row and its controls row: the inner start edge at the right, items moving toward the right as p grows, the padding, the scroll direction, the start and end sides of contentAlignment, the order of the dots, the playback control's side and the sides of the previous and next controls (the previous control at the right) all mirrored; it SHALL grow a ripple from the point of contact as in left-to-right, and SHALL NOT mirror an item's content or the pause and play_arrow glyphs. The chevron_left and chevron_right glyphs of the previous and next controls SHALL be drawn mirrored exactly when the system's icon set marks the name as mirrored, by the Button's own icon helper; THE carousel SHALL NOT mirror a glyph itself. |
CRL-24 | WHEN heroAlignment, contentAlignment or navigation names a value the carousel does not define THE carousel SHALL render with that prop's default value, and in debug builds report the unknown value; WHEN itemWidth or autoplayInterval is not a positive number THE carousel SHALL treat it as null and in debug builds report it. |
CRL-25 | WHILE the text size is enlarged THE carousel SHALL keep its height, slots and items as they are: it holds no text of its own, and an item's content follows the text size itself. |
CRL-26 | WHILE scaleOthers is true THE carousel SHALL give the hero slot the scale 1 and every other slot the scale comp.carousel.container.min-scale, and SHALL draw each item's container at the scale interpolated between the two slots around s, which is 1 − (1 − comp.carousel.container.min-scale) × the lesser of 1 and |s|: as tall as the row times that scale, centred on the row's height, with its content drawn at that scale. WHILE fluidWidth is false a slot SHALL also be H times its scale wide, and the slots SHALL be placed g apart from drawn edge to drawn edge going outward from the hero slot, so that the smaller items close up to their neighbours; WHILE fluidWidth is true the slots keep the widths of CRL-10, which already fill the row, and only the height and the content take the scale. The corners of a scaled container SHALL stay the radius of comp.carousel.container.shape (or of its state's shape token) and SHALL NOT be multiplied by the scale; the item's focus ring SHALL be drawn at its own width inside the drawn container, and the item's hit area (CRL-4) SHALL follow the drawn container. The scale SHALL follow the scroll where motion is reduced as well, because the person's own scroll drives it. |
CRL-27 | WHILE loop is true and there are at least two items THE carousel SHALL scroll without end in both directions: p SHALL take any value, every whole number SHALL be a resting position, and item i SHALL sit at the one slot position s that differs from i − p by a whole multiple of N and lies above −N / 2 and at most N / 2 WHILE heroAlignment is center, or above −1 and at most N − 1 WHILE heroAlignment is start, so that scrolling past the last item continues to the first and before the first to the last. Each item SHALL be drawn once: with fluid widths and enough items for every slot and both edge slots an item passes from one side to the other at the edge slots, where it has no width, and with fewer items, or with fixed widths and too few items to reach beyond both edges, a slot stays empty and an item that passes to the other side may appear there at once. WHILE there are fewer than two items THE carousel SHALL ignore loop; loop is in effect only while both conditions hold. |
CRL-28 | WHILE the indicator (CRL-29) or the playback control (CRL-35) is drawn THE carousel SHALL draw, directly below its row of items, a controls row as wide as the carousel and comp.carousel.controls.height tall that does not scroll, with the indicator's container centred on the carousel's width and the playback control at its inline end, its end edge at the inner end edge, both centred on the row's height; WHILE neither is drawn there SHALL be no controls row. The indicator SHALL stay centred whether or not the playback control is drawn. |
CRL-29 | WHILE indicator is true and there are at least two resting positions THE carousel SHALL draw the indicator: D dots in reading order, comp.carousel.dot.gap apart, each comp.carousel.dot.size tall with round ends, inside a container that reaches comp.carousel.controls.indicator-padding-inline beyond the first and last dot, is comp.carousel.controls.button-size tall and has comp.carousel.controls.shape corners. The indicator's position q follows p: between resting positions j and j + 1 it runs linearly from j to j + 1, and WHILE loop is in effect q is p less a whole multiple of N, from 0 up to N, its distance to a dot measured around the ring. Dot j SHALL be comp.carousel.dot.size + (comp.carousel.dot.active-width − comp.carousel.dot.size) × w wide, w being the greater of 0 and 1 − the distance from q to j, and SHALL be drawn in comp.carousel.dot.color with a fill of comp.carousel.indicator.content.color over it at opacity w, across its whole width unless CRL-36 says otherwise, so that the current dot is stretched and in the active color and the stretch and the color pass to the next dot as the row scrolls. IF the container would be wider than W − 2 × (sys.target.touch + g) THE carousel SHALL NOT draw the indicator. |
CRL-30 | WHEN the person presses the indicator by pointer or touch anywhere in its hit area THE carousel SHALL move the row to the resting position of the dot nearest the press and, while the press moves along the dots, to the dot nearest it, each move over comp.carousel.motion.scroll-duration with comp.carousel.motion.scroll-easing (at once where motion is reduced); WHILE the indicator has keyboard focus Right Arrow and Left Arrow SHALL move the row to the next and previous resting position (wrapping WHILE loop is in effect, and otherwise not), Home and End to the first and last, mirrored in a right-to-left layout; WHEN assistive technology increments or decrements the indicator it SHALL move by one resting position. Focus SHALL stay on the indicator, and the items' Tab stop (CRL-17) SHALL become the resting item the move brings. |
CRL-31 | THE carousel SHALL expose the indicator as one slider (the adjustable trait on Apple platforms), named by label, horizontal, with the lowest value 1, the highest D and the value of the resting position nearest q counted from 1, and SHALL give it as value text the label of the item that position brings to slot 0 or, where the range ends first, the resting item there; its dots SHALL NOT be exposed one by one. |
CRL-32 | WHILE autoplay is true, paused is false, there are at least two resting positions, motion is not reduced (CRL-34) and no hold of CRL-33 is in effect THE carousel SHALL advance to the next resting position each time autoplayInterval (or comp.carousel.autoplay.interval while it is null) has passed with the row at rest, scrolling over comp.carousel.motion.autoplay-duration with comp.carousel.motion.autoplay-easing; from the last resting position it SHALL go on to the first, forward WHILE loop is in effect and otherwise back across the row over the same duration. An advance SHALL NOT move focus, change the items' Tab stop, or make an announcement. |
CRL-33 | WHILE a pointer is over the carousel (its row or its controls row), focus is anywhere within it however it came there, the person is pressing or dragging the row or the indicator, or the platform reports the carousel's window or document hidden, THE carousel SHALL NOT advance and SHALL hold the time that has passed; WHEN the last hold ends it SHALL continue from the time it held. WHEN the person moves the row (a scroll, a key, the indicator, the previous or the next control) the time SHALL start again from zero once the row comes to rest. WHEN the person activates the playback control to play, the hover and focus holds in effect at that moment SHALL end, and each SHALL apply again only once the pointer or focus has left the carousel and entered it again. |
CRL-34 | WHILE motion is reduced THE carousel SHALL NOT advance by itself, whatever autoplay and paused say, and SHALL NOT draw the playback control or the autoplay progress; WHEN motion stops being reduced it SHALL start autoplay again (paused permitting) with the time from zero. Where motion is reduced the width, the scale and the dots SHALL still follow a scroll the person makes. |
CRL-35 | WHILE autoplay is true, playbackControl is true, there are at least two resting positions and motion is not reduced THE carousel SHALL draw the playback control in the controls row: an icon-only button, a comp.carousel.controls.button-size square with comp.carousel.controls.shape corners, filled with comp.carousel.playback.container.color, holding the icon set's pause glyph while paused is false and play_arrow while paused is true, comp.carousel.controls.icon.size, in comp.carousel.playback.content.color, with the family and fill of an unselected icon, sys.icon.weight and its optical size clamped to sys.icon.optical-size.min and sys.icon.optical-size.max. It SHALL be exposed with the button role, named by pauseLabel while paused is false and by playLabel while paused is true, with no pressed or selected state. WHEN the person activates it THE carousel SHALL toggle paused, emit pausedChange with the new value and keep focus on it, and on play the time SHALL start from zero; WHEN the product sets paused THE carousel SHALL emit nothing. |
CRL-36 | WHILE autoplayProgress is true, the indicator is drawn, autoplay is true, paused is false and motion is not reduced THE carousel SHALL draw the fill of CRL-29 only on the dot of the resting position the row last came to rest at, from that dot's start edge across the part of its width that the time passed (CRL-33) is of the interval, growing with the time and stopping while a hold is in effect, and on no other dot, so that the dot stands in comp.carousel.dot.color and fills with comp.carousel.indicator.content.color until the row moves on, the fill fading as the dot narrows. |
CRL-37 | THE carousel SHALL give the playback control a hit area centred on it of at least sys.target.touch for touch and sys.target.pointer for a fine pointer, and the indicator a hit area as tall as the controls row and as wide as its container, at least sys.target.touch wide; with the default tokens both fit inside the controls row, and CRL-29 keeps them apart. |
CRL-38 | WHILE the icon set's font has not loaded THE carousel SHALL keep the playback control's glyph box and draw nothing in it, never the glyph's name as text. |
CRL-39 | THE carousel 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 carousel is shown THE carousel SHALL draw itself for the new direction at once, without a motion of its own, keeping the item at rest (the same item in reading order, not the same scroll offset), its playback and its focus. |
CRL-40 | THE carousel SHALL keep the distance from the drawn end edge of an item's container to the drawn start edge of the next item's container equal to comp.carousel.layout.gap, for every two neighbouring items that are both drawn, at every scroll position: at rest and at every moment of a drag, a fling, a settle, an advance and a move by the keys or the dots, in every combination of heroAlignment, fluidWidth, scaleOthers, contentAlignment, snap and loop, and in both layout directions. The edges are those of the container as it is drawn, after any scale (CRL-26), not those of a box laid out before the scale. THE carousel SHALL place every item it draws in a frame from the same scroll position p, and SHALL NOT ease, delay or animate an item's start, width or scale apart from p. A platform that draws on whole device pixels SHALL round each item's start edge and take its end edge from the next item's rounded start edge less the gap as that platform rounds it, so that the rounding falls on an item's width and never on a gap, and no two gaps in view differ. |
CRL-41 | WHILE navigation is auto or always and there are at least two resting positions THE carousel SHALL draw a previous control over the inline start of its row and a next control over its inline end, above the items: the previous control's start edge at the inner start edge and the next control's end edge at the inner end edge, both centred on the row's height. They SHALL be icon-only Buttons of the medium size in the variant comp.carousel.navigation.button.variant names, given through the channel a Button reads from the component that places it and otherwise unrestyled, showing the icon set's chevron_left and chevron_right glyphs and named by previousLabel and nextLabel. WHILE navigation is auto THE carousel SHALL draw them only where the primary pointer is fine and can hover; WHILE navigation is none, or there are fewer than two resting positions, it SHALL draw neither. |
CRL-42 | WHILE navigation is auto THE carousel SHALL show the previous and next controls only while a pointer is over the carousel (its row or its controls row) or focus is within it, however it came there, fading them in and out over comp.carousel.motion.state-duration with comp.carousel.motion.state-easing, at once where motion is reduced; hidden, they SHALL keep their place in the Tab order (CRL-17) and stay exposed to assistive technology with their names and states, so that a keyboard reaches them and focus shows them. WHILE navigation is always THE carousel SHALL show them at all times. |
CRL-43 | WHEN the person activates the previous or the next control THE carousel SHALL move the row to the resting position before or after the one it is at — while it is moving, the one it is moving to; between two resting positions, the nearest one before or after where it is — over comp.carousel.motion.scroll-duration with comp.carousel.motion.scroll-easing (at once where motion is reduced), from the first to the last and from the last to the first WHILE loop is in effect. Focus SHALL stay on the control, and the items' Tab stop (CRL-17) SHALL become the resting item the move brings. WHILE loop is not in effect THE carousel SHALL disable the previous control while the row is at, or moving to, the first resting position, and the next control at the last: drawn and exposed as disabled and refusing activation, but kept in the Tab order and keeping focus, so that the stops do not change as the row moves and focus is never lost when a control reaches its end. |
各平台名称对照
对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 组件 | tanrei-carousel | TanreiCarousel | TanreiCarousel | TanreiCarousel | Carousel |
heroAlignment · 属性 | heroAlignment | heroAlignment | heroAlignment | heroAlignment | heroAlignment |
itemWidth · 属性 | itemWidth | itemWidth | itemWidth | itemWidth | itemWidth |
fluidWidth · 属性 | fluidWidth | fluidWidth | fluidWidth | fluidWidth | fluidWidth |
contentAlignment · 属性 | contentAlignment | contentAlignment | contentAlignment | contentAlignment | contentAlignment |
scaleOthers · 属性 | scaleOthers | scaleOthers | scaleOthers | scaleOthers | scaleOthers |
snap · 属性 | snap | snap | snap | snap | snap |
loop · 属性 | loop | loop | loop | loop | loop |
navigation · 属性 | navigation | navigation | navigation | navigation | navigation |
indicator · 属性 | indicator | showsIndicator | indicator | indicator | indicator |
autoplay · 属性 | autoplay | autoplay | autoplay | autoplay | autoplay |
autoplayInterval · 属性 | autoplayInterval | autoplayInterval | autoplayInterval | autoplayInterval | autoplayInterval |
paused · 属性 | paused | isPaused | paused | paused | paused |
playbackControl · 属性 | playbackControl | showsPlaybackControl | playbackControl | playbackControl | playbackControl |
autoplayProgress · 属性 | autoplayProgress | showsAutoplayProgress | autoplayProgress | autoplayProgress | autoplayProgress |
playLabel · 属性 | playLabel | playLabel | playLabel | playLabel | playLabel |
pauseLabel · 属性 | pauseLabel | pauseLabel | pauseLabel | pauseLabel | pauseLabel |
previousLabel · 属性 | previousLabel | previousLabel | previousLabel | previousLabel | previousLabel |
nextLabel · 属性 | nextLabel | nextLabel | nextLabel | nextLabel | nextLabel |
label · 属性 | label | label | contentDescription | semanticLabel | accessibilityLabel |
pausedChange · 事件 | pausedchange | isPaused | onPausedChange | onPausedChanged | onPausedChange |
| 备注 | Attributes: hero-alignment, item-width (a number of CSS pixels; without it the property is null), content-alignment, scale-others, loop, indicator, autoplay, autoplay-interval (milliseconds), paused, autoplay-progress, play-label, pause-label, navigation (reflected), previous-label, next-label. fluidWidth, snap and playbackControl are boolean properties that default to true; their attributes are the inverses fixed-width, free-scroll and no-playback-control (an HTML boolean attribute cannot default to true). pausedchange is a CustomEvent whose detail.paused is the new value. Built on a native horizontal scroller (so wheel, trackpad and touch scroll as the platform does) whose track is as long as the scroll range; the items are placed from its scroll offset (CRL-12), and the settle animates the scroll offset over comp.carousel.motion.scroll-duration and scroll-easing once the scroll ends (scrollend, or 100 ms without a scroll event, because not every engine sends scrollend after a wheel), since a smooth scrollTo cannot follow the tokens. Fling 0.5 px/ms, measured from the finger's last touch moves over their own time stamps; after a fling the settle starts at the release, and the browser's momentum is held until it ends. While loop is in effect the track is several turns long and the offset is moved by a whole number of turns (N × (H + g)) when the row rests near either end, which draws nothing new. Autoplay reads prefers-reduced-motion and document.visibilityState when it would advance and listens for their changes; the hover hold is pointerenter / pointerleave on the host, the focus hold focusin / focusout. The items are placed from one scroll position in one pass, each start rounded to a device pixel and each end taken from the next start (CRL-40). The previous and next controls are <tanrei-button> elements with focusable-when-disabled, given their variant through the Button's group-item channel; auto draws them under (hover: hover) and (pointer: fine) and shows them while the host matches :hover or :focus-within. Snapshot harness: a 400 × 200 stage (the carousel fills it: W 368), or 400 × 248 for the fixtures with a controls row, on a surface page, scrolled to its start, the pointer outside, motion not reduced and the autoplay clock stopped at the start of its interval. | Lagging: ships an earlier revision of this spec, with layout and its three values, and no loop, dots or autoplay, and so owes the hero centred by default, the edge slots of no width, the scaled slots closed up and the constant gap as well (CRL-9 to CRL-13, CRL-26, CRL-40), and the previous and next controls with their props navigation, previousLabel and nextLabel (CRL-41 to CRL-43). Target API: TanreiCarousel(_ label: String, heroAlignment: .center, itemWidth: CGFloat? = nil, fluidWidth: true, contentAlignment: .center, scaleOthers: false, snap: true, loop: false, showsIndicator: false, autoplay: false, autoplayInterval: Duration? = nil, isPaused: Binding<Bool>, showsPlaybackControl: true, showsAutoplayProgress: false, playLabel:, pauseLabel:) { TanreiCarouselItem(…) { … } }; pausedChange is the isPaused binding, set only by the control. A horizontal ScrollView whose content is a track as long as the scroll range, the items placed from the scroll offset by a custom Layout (read from scrollPosition and the track's geometry), settled by a custom ScrollTargetBehavior (iOS 17). The list is an accessibilityElement(children: .contain) named by label, and each item a button. SwiftUI has no position-in-set property for a custom container, so VoiceOver's own count of the container's elements stands in; the README says so. Autoplay reads accessibilityReduceMotion and scenePhase. Snapshot harness: a 400 × 200 frame (400 × 248 with a controls row) on a surface page. | Lagging: ships an earlier revision of this spec, with layout and its three values, and no loop, dots or autoplay, and so owes the hero centred by default, the edge slots of no width, the scaled slots closed up and the constant gap as well (CRL-9 to CRL-13, CRL-26, CRL-40), and the previous and next controls with their props navigation, previousLabel and nextLabel (CRL-41 to CRL-43). Target API: TanreiCarousel(itemCount, contentDescription, heroAlignment, itemWidth: Dp? = null, fluidWidth = true, contentAlignment, scaleOthers = false, snap = true, loop = false, indicator = false, autoplay = false, autoplayInterval: Duration? = null, paused = false, onPausedChange, playbackControl = true, autoplayProgress = false, playLabel, pauseLabel) { index -> TanreiCarouselItem(…) { … } }, as Material's carousels take an item lambda; built on Modifier.scrollable and a custom Layout, not Material's carousel (the package does not depend on Material). Settling uses a SnapFlingBehavior whose fling threshold is ViewConfiguration's. No list role: collectionInfo on the row and collectionItemInfo with Role.Button on each item. Autoplay reads the animator duration scale (0 is reduced motion) and the lifecycle's resumed state. Snapshot harness: a 400 × 200 dp box (400 × 248 with a controls row) on a surface page. | Lagging: ships an earlier revision of this spec, with layout and its three values, and no loop, dots or autoplay, and so owes the hero centred by default, the edge slots of no width, the scaled slots closed up and the constant gap as well (CRL-9 to CRL-13, CRL-26, CRL-40), and the previous and next controls with their props navigation, previousLabel and nextLabel (CRL-41 to CRL-43). Target API: TanreiCarousel(semanticLabel:, heroAlignment: TanreiCarouselHeroAlignment.center, itemWidth: null, fluidWidth: true, contentAlignment: TanreiCarouselContentAlignment.center, scaleOthers: false, snap: true, loop: false, indicator: false, autoplay: false, autoplayInterval: null, paused: false, onPausedChanged:, playbackControl: true, autoplayProgress: false, playLabel:, pauseLabel:, children: [...]), built on the widgets library's Scrollable with a custom layout and ScrollPhysics that settle (CRL-14), not Material's CarouselView. Each item is Semantics(button: true, label:) inside IndexedSemantics, and the scrollable reports semanticChildCount, so the position and the number of items are read; the whole is a Semantics(label:, explicitChildNodes: true) container. Autoplay reads MediaQuery.disableAnimationsOf and the app lifecycle. Snapshot harness: a golden of a 400 × 200 surface (400 × 248 with a controls row) on a surface page. | Lagging: ships an earlier revision of this spec, with layout and its three values, and no loop, dots or autoplay, and so owes the hero centred by default, the edge slots of no width, the scaled slots closed up and the constant gap as well (CRL-9 to CRL-13, CRL-26, CRL-40), and the previous and next controls with their props navigation, previousLabel and nextLabel (CRL-41 to CRL-43). Target API: <Carousel accessibilityLabel heroAlignment itemWidth fluidWidth contentAlignment scaleOthers snap loop indicator autoplay autoplayInterval paused onPausedChange playbackControl autoplayProgress playLabel pauseLabel>{items}</Carousel>: a horizontal ScrollView whose content is as long as the scroll range, the items placed from an Animated value of its offset; it settles with scrollTo after the drag or momentum ends (fling 0.5 pt/ms). The carousel has role="list" and each item the button role. React Native has no position-in-set property, so an item's position and the number of items are left to the screen reader's own count of the list; the README says so. Autoplay reads AccessibilityInfo.isReduceMotionEnabled and AppState. No virtualization. Snapshot harness: a 400 × 200 view (400 × 248 with a controls row) on a surface page. |
令牌
| 令牌 | 引用 | 解析值(浅色) |
|---|---|---|
--tanrei-comp-carousel-autoplay-interval | — | 5000ms |
--tanrei-comp-carousel-container-large-width | — | 200px |
--tanrei-comp-carousel-container-min-scale | — | 0.85 |
--tanrei-comp-carousel-container-shape | sys.shape.corner.large | 12px |
--tanrei-comp-carousel-container-small-max-width | — | 56px |
--tanrei-comp-carousel-container-small-min-width | — | 40px |
--tanrei-comp-carousel-controls-button-size | — | 40px |
--tanrei-comp-carousel-controls-height | sys.target.touch | 48px |
--tanrei-comp-carousel-controls-icon-size | sys.icon.medium | 24px |
--tanrei-comp-carousel-controls-indicator-padding-inline | sys.space.space150 | 12px |
--tanrei-comp-carousel-controls-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-carousel-dot-active-width | — | 24px |
--tanrei-comp-carousel-dot-color | sys.color.outline | |
--tanrei-comp-carousel-dot-gap | sys.space.space100 | 8px |
--tanrei-comp-carousel-dot-size | — | 8px |
--tanrei-comp-carousel-focused-container-shape | — | 12px |
--tanrei-comp-carousel-focused-opacity | — | 1 |
--tanrei-comp-carousel-hovered-container-shape | — | 12px |
--tanrei-comp-carousel-hovered-opacity | — | 1 |
--tanrei-comp-carousel-indicator-container-color | — | |
--tanrei-comp-carousel-indicator-content-color | sys.color.primary | |
--tanrei-comp-carousel-indicator-focused-container-color | — | |
--tanrei-comp-carousel-indicator-focused-content-color | — | |
--tanrei-comp-carousel-indicator-focused-state-layer-color | — | |
--tanrei-comp-carousel-indicator-hovered-container-color | — | |
--tanrei-comp-carousel-indicator-hovered-content-color | — | |
--tanrei-comp-carousel-indicator-hovered-state-layer-color | — | |
--tanrei-comp-carousel-indicator-pressed-container-color | — | |
--tanrei-comp-carousel-indicator-pressed-content-color | — | |
--tanrei-comp-carousel-indicator-pressed-state-layer-color | — | |
--tanrei-comp-carousel-indicator-state-layer-color | comp.carousel.indicator.content.color | |
--tanrei-comp-carousel-item-container-color | sys.color.surface-container-highest | |
--tanrei-comp-carousel-item-content-color | sys.color.on-surface | |
--tanrei-comp-carousel-item-focused-container-color | — | |
--tanrei-comp-carousel-item-focused-content-color | — | |
--tanrei-comp-carousel-item-focused-state-layer-color | — | |
--tanrei-comp-carousel-item-hovered-container-color | — | |
--tanrei-comp-carousel-item-hovered-content-color | — | |
--tanrei-comp-carousel-item-hovered-state-layer-color | — | |
--tanrei-comp-carousel-item-pressed-container-color | — | |
--tanrei-comp-carousel-item-pressed-content-color | — | |
--tanrei-comp-carousel-item-pressed-state-layer-color | — | |
--tanrei-comp-carousel-item-state-layer-color | comp.carousel.item.content.color | |
--tanrei-comp-carousel-layout-gap | sys.space.space100 | 8px |
--tanrei-comp-carousel-layout-height | — | 200px |
--tanrei-comp-carousel-layout-padding-inline | sys.space.space200 | 16px |
--tanrei-comp-carousel-motion-autoplay-duration | sys.motion.duration.long2 | 500ms |
--tanrei-comp-carousel-motion-autoplay-easing | sys.motion.easing.standard | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-carousel-motion-feedback-duration | sys.motion.role.feedback | 450ms |
--tanrei-comp-carousel-motion-scroll-duration | sys.motion.duration.medium2 | 300ms |
--tanrei-comp-carousel-motion-scroll-easing | sys.motion.easing.emphasized-decelerate | cubic-bezier(0.05, 0.7, 0.1, 1) |
--tanrei-comp-carousel-motion-state-duration | sys.motion.role.state.large | 250ms |
--tanrei-comp-carousel-motion-state-easing | sys.motion.easing.state | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-carousel-navigation-button-variant | — | 2 |
--tanrei-comp-carousel-playback-container-color | — | |
--tanrei-comp-carousel-playback-content-color | sys.color.on-surface-variant | |
--tanrei-comp-carousel-playback-focused-container-color | — | |
--tanrei-comp-carousel-playback-focused-content-color | — | |
--tanrei-comp-carousel-playback-focused-state-layer-color | — | |
--tanrei-comp-carousel-playback-hovered-container-color | — | |
--tanrei-comp-carousel-playback-hovered-content-color | — | |
--tanrei-comp-carousel-playback-hovered-state-layer-color | — | |
--tanrei-comp-carousel-playback-pressed-container-color | — | |
--tanrei-comp-carousel-playback-pressed-content-color | — | |
--tanrei-comp-carousel-playback-pressed-state-layer-color | — | |
--tanrei-comp-carousel-playback-state-layer-color | comp.carousel.playback.content.color | |
--tanrei-comp-carousel-pressed-container-shape | — | 12px |
--tanrei-comp-carousel-pressed-opacity | — | 1 |
测试样例
作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。
centered
previousLabel: Previous walk nextLabel: Next walk label: Featured walks items: 5 fixed-width
fluidWidth: false snap: false previousLabel: Previous walk nextLabel: Next walk label: Walks near you items: 5 scaled
fluidWidth: false scaleOthers: true loop: true previousLabel: Previous walk nextLabel: Next walk label: Featured walks items: 5 loop
loop: true previousLabel: Previous walk nextLabel: Next walk label: Featured walks items: 5 content-toward-hero
itemWidth: 120 contentAlignment: toward-hero loop: true previousLabel: Previous walk nextLabel: Next walk label: Walks near you items: 6 navigation
navigation: always previousLabel: Previous walk nextLabel: Next walk label: Featured walks items: 5 indicator
indicator: true previousLabel: Previous walk nextLabel: Next walk label: Featured walks items: 6 autoplay
indicator: true autoplay: true playLabel: Play the slideshow pauseLabel: Pause the slideshow previousLabel: Previous walk nextLabel: Next walk label: Featured walks items: 6 autoplay-progress
indicator: true autoplay: true autoplayProgress: true playLabel: Play the slideshow pauseLabel: Pause the slideshow previousLabel: Previous walk nextLabel: Next walk label: Featured walks items: 6 autoplay-paused
indicator: true autoplay: true paused: true autoplayProgress: true playLabel: Play the slideshow pauseLabel: Pause the slideshow previousLabel: Previous walk nextLabel: Next walk label: Featured walks items: 6 playback-control
autoplay: true playLabel: Play the slideshow pauseLabel: Pause the slideshow previousLabel: Previous walk nextLabel: Next walk label: Featured walks items: 4 生成自 specs/carousel.spec.json, tokens/comp/carousel.tokens.json, design/tokens/
轮播用于一行视觉内容项,供人浏览并逐个打开:专辑、地点、商品、剧集。它不必占用网格那样的高度,就能表明后面还有更多项;它给视野内的项留出的空间,也比即将进入的项更多。
如果项以文本为主,或者用户要在它们之间比较或挑选,请使用列表。如果每一项都需要自己的文本和操作,请把卡片排成网格。只有简短的精选行才让轮播自动前进,并且要保留它的播放控件。
每个属性回答一个问题,它们可以自由组合。
| 属性 | 默认值 | 它决定什么 |
|---|---|---|
heroAlignment |
center |
突出展示的项位于哪里:居中(center),两侧都有项;或位于内侧的起始边缘(start),只有后面有项。 |
itemWidth |
设计系统的值 | 主项的宽度。固定宽度时,是每一项的宽度。 |
fluidWidth |
true |
为 true 时,主项旁边的项收窄为中等宽度和小宽度,各自的宽度随滚动变化。为 false 时,每一项都与主项同宽,视野内的最后一项被边缘裁切。 |
contentAlignment |
center |
流动宽度时,变窄的项显示其内容的哪一部分:中间,或最靠近主项的一侧(toward-hero)。 |
scaleOthers |
false |
把主项以外的项画得更小,大小随滚动变化,流动宽度和固定宽度下都可以用。项与项的间距保持不变:固定宽度时,变小的项会向相邻的项靠拢。 |
snap |
true |
滚动结束时,是停靠在完整的一项上,还是在哪里结束就停在哪里。 |
loop |
false |
让这一行没有尽头:最后一项之后是第一项。只在有两项或更多项时生效。 |
在 Web 上,fluidWidth 写作 fixed-width,snap 写作 free-scroll,即这两个默认为 true 的属性的反义。
几种常见的行:
| 用途 | 设置 |
|---|---|
| 浏览许多项:专辑、地点 | 什么都不用设:用默认值。 |
| 每次突出展示一项,并露出下一项的一角 | 不小于轮播宽度的 itemWidth。主项会取它所能达到的最大宽度。 |
| 两侧都有项的精选行 | 不需要更多设置:主项默认居中。常与 loop 搭配,让两侧始终填满。 |
| 从边缘开始、主项排在最前的行 | heroAlignment 设为 start。 |
| 普通的滚动行,例如最近浏览的项 | heroAlignment 设为 start,fluidWidth 和 snap 设为 false。 |
根据内容的形状选择 itemWidth:方形封面、竖长的海报和横宽的缩略图需要不同的宽度,即使在同一个产品里也是如此。流动宽度时,主项会给其他项留出空间:凡是后面还有项的一侧,它都会留下一个小项的位置,宽度不会超过这个限度;它也不会窄到让下一项比自己还宽。如果这一行窄得连这也做不到,轮播就每次显示一项,与行同宽。
与上一版相比,有两点不同。在其他平台跟上之前,这是 Web 上的行为。
- 现在
heroAlignment默认为center。 以前是start。两个值都保留。 - 相邻的项在任何滚动位置都相隔一个间距,而不只是在静止时。以前,项经过行的边缘时间距会变大,缩小的项周围也会多出空隙。
| 你原来的写法 | 现在 |
|---|---|
没有设置 heroAlignment |
主项居中,两侧各有一个较窄的项。要让这一行保持原样,把 heroAlignment 设为 start(在 Web 上是 hero-alignment="start")。 |
heroAlignment 为 start |
无需改动。 |
heroAlignment 为 center |
无需改动。可以去掉这个属性。 |
固定宽度下使用 scaleOthers |
变小的项现在与相邻的项相隔一个间距,比以前更近。无需改动。 |
itemWidth 比行更宽,且没有设置 heroAlignment |
主项比以前窄,因为两侧各留有一个小项。要得到最宽的主项,把 heroAlignment 设为 start。 |
流动宽度时,项现在从边缘处以零宽度进入这一行,并在进入的过程中逐渐变宽,而不是以完整宽度滑入。静止时,这一行看起来和以前一样。
上一版的 layout 属性已被移除。它的每个值现在都是上述属性的一种组合:
以前的 layout |
现在 |
|---|---|
| 保持默认值 | heroAlignment 设为 start。在不比手机更宽的行上,画面相同。在更宽的行上,主项会变宽,直到它的上限。 |
hero |
heroAlignment 设为 start,并把 itemWidth 设为不小于轮播的宽度,这样主项就取它所能达到的最大宽度。画面相同。 |
uncontained |
heroAlignment 设为 start,fluidWidth 和 snap 设为 false(在 Web 上是 hero-alignment="start" fixed-width free-scroll)。画面相同。 |
- 圆点。
indicator会在各项下方画一行圆点,每个停靠位置对应一个,整行相对于轮播居中。当前圆点被拉长,随着这一行滚动,这段拉长会从一个圆点过渡到下一个。所有圆点是一个控件:在它们上面的任何位置按下或拖动,都可以移动这一行。请把圆点用于简短的精选行。如果圆点太多,在播放控件旁边放不下,就不会绘制,调试构建会报告这一点。 - 自动播放。
autoplay每隔autoplayInterval毫秒(默认系统中为 5 秒)让这一行前进一项。文字多的项要比图片多留一些时间。没有loop时,到最后一项之后会回到第一项。它从不移动焦点,也不朗读任何内容。 - 暂缓。 指针位于轮播之上时、焦点在轮播内任何位置时、轮播被按下或拖动时,以及窗口被隐藏时,自动播放都会暂缓。最后一个暂缓条件解除后,它从停下的地方继续;如果用户移动过这一行,间隔会重新开始计时。
- 减弱动态效果。 在平台开启了“减弱动态效果”设置的环境中,自动播放从不运行,播放控件和进度也不绘制。宽度、缩小和圆点仍然跟随用户自己的滚动。
- 播放控件。 自动播放开启期间,圆点那一行的末尾有一个圆形按钮,用来暂停和播放,显示
pause或play_arrow。它以pauseLabel或playLabel命名,取决于按下它会执行哪个操作。它会改变paused并触发pausedChange(在 Web 上是pausedchange);你的代码也可以设置paused,这时不触发任何事件。 - 进度。 设置
autoplayProgress后,当前圆点会随着距离下一项的时间流逝而逐渐填满。
让播放始终可控。 产品可以把 playbackControl 设为 false 来隐藏播放控件,但只要有可能就应该保留它。暂缓不是停止动效的办法:触摸没有悬停;手指只有停在上面时才能让这一行暂缓;不移动焦点就能阅读的屏幕阅读器则完全不会让它暂缓。没有这个控件,手机上的用户要停下自动播放的轮播,就只能靠系统的“减弱动态效果”设置。只有当你的产品提供了另一种停止动效的办法时才隐藏它,例如一个能暂停页面上所有动态内容的开关(WCAG 2.2.2)。
没有滚动条的一行,鼠标无法通过单次按下来移动它:只能按住 Shift 滚动滚轮,或者在绘制了圆点时使用圆点。因此,轮播在这一行的两端之上绘制“上一个”和“下一个”控件。它们是带有尖角图标的纯图标 Button,变体由设计系统决定(默认系统中为 elevated),每个控件都像圆点和方向键一样,把这一行后退或前进一项。
navigation |
何处绘制 | 何时显示 |
|---|---|---|
auto(默认) |
主指针是能够悬停的鼠标或触控板时 | 指针位于轮播上方或焦点在轮播内时 |
always |
所有设备,包括触摸设备 | 始终 |
none |
从不 | 从不 |
- 为何默认不在触摸设备上绘制。 手指可以滑动这一行,而在手机上这些控件会一直盖在项上。如果你的用户在触摸设备上也需要它们,请选择
always。 - 在两端。 没有
loop时,在第一项时“上一个”控件被禁用,在最后一项时“下一个”控件被禁用。被禁用的控件仍保留焦点和它在 Tab 顺序中的位置,因此反复按“下一个”也绝不会丢失焦点。开启loop时,它们会绕回。 - 连续按下会累加。 在这一行移动期间按下,会从它正要去的位置开始计算,所以快速按两次会移动两项。
- 自动播放。 按下是用户自己的移动:这一行停下后,间隔重新开始计时。指针位于这些控件上方或焦点在它们上面时,自动播放会暂缓,和在轮播的任何其他位置一样。
- 名称。
previousLabel和nextLabel为辅助技术命名它们,例如“上一条步行路线”和“下一条步行路线”。缺少时,调试构建会报告。
只要有可能,就保持 auto。只有当页面为鼠标提供了移动这一行的其他方式(例如圆点)时,才选择 none。
- 内容按主项的宽度布局。 每一项都按主项的宽度来布局内容,因此变窄的项显示的是图片的中间部分;
contentAlignment为 toward-hero 时,显示的是最靠近主项的一侧。项变宽、变窄时,图片在项内平滑移动。请提供只看露出的那一部分也能看懂的图片。 - 项上没有文字。 项不绘制说明文字,因为图片上的文字无法保证任何对比度。请在你的内容里把说明文字放在自己的遮罩之上,或者显示在轮播之外。
- 每项一个操作。 项会打开它所展示的内容。按钮、链接和其他控件不要放进它的内容里。
- 没有禁用的项。 供人浏览的行里,不存在“显示出来却不可用”的东西。这样的项请直接去掉。
- 空的项也看得见。 无论项处于什么状态,它的容器都会在内容之下以静止时的颜色填充,所以图片尚未加载出来的项仍然可见。
- 状态显示在内容之上。 悬停、聚焦和按下画成盖在内容上的一层,涟漪也在同一层里,所以它们在任何图片上都看得见。
用户可以用触摸、触控笔、触控板、横向滚轮,或按住 Shift 滚动纵向滚轮来滚动这一行。开启 snap 时,滚动结束后这一行会停靠在完整的一项上:快速滑动(fling)会前进到该方向上的下一项,较慢的松手则去往最近的一项。快速滑动是指松手时的速度超过平台的最小快速滑动速度;平台没有给出这个值时,取每毫秒 0.5 点。没有 loop 时,在范围的末端,最后一项成为主项,它之后的槽位保持空着。开启 loop 时,每一项只绘制一次,落在主项周围的哪个位置就画在哪里,整行首尾相接、没有尽头。在减弱动态效果的环境中,轮播自己发起的每一次移动都立即完成。使用鼠标时,还可以用“上一个”和“下一个”控件每次移动一项。
轮播的宽度或它的项发生变化时,它会重新排列槽位,并让原先突出展示的项留在原处,不带动效。
- 为每一项加上标签,说明它的内容展示的是什么,使用你的产品的语言。
- 根据内容的形状选择
itemWidth。 - 为轮播提供一个为这一行命名的
label,例如“最近播放”。 - 只要轮播会自动前进,就保留播放控件,并为它提供
playLabel和pauseLabel。
- 不要在项的内容里放控件。项本身已经是一个按钮。
- 不要把轮播用于用户必须全部看到的内容,例如步骤或条款。视野之外的项很容易被错过。
- 不要为供人浏览的长行开启自动播放。它适合带圆点的简短精选行。
- 不要隐藏播放控件,除非你的产品提供了另一种停止动效的办法。
- 角色
list- 标注
- The items are a list named by label. Each item is one button named by its label, with its position and the number of items; its content is not read. The indicator is one slider named by label, whose value text is the current item's label. The playback control is a button named by pauseLabel while playing and by playLabel while paused. Items out of view are still reachable, and focusing one scrolls it into place. The previous and next controls are buttons named by previousLabel and nextLabel, disabled at the ends of a row that does not loop.
- 最小目标
- 即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px
键盘操作
| 按键 | 动作 |
|---|---|
| Tab / Shift+Tab | Moves through the carousel's stops: the playback control (while it is drawn), the previous and next controls (while they are drawn), then the items as one stop (the item that last had focus, or else the resting item), then the indicator (while it is drawn), and on out (CRL-17). |
| Right Arrow / Left Arrow | On an item: moves focus to the next or previous item and brings it into place. On the indicator: moves the row to the next or previous position. Both wrap only while the carousel loops, and are mirrored in right-to-left layouts. |
| Home / End | On an item: moves focus to the first or last item and brings it into place. On the indicator: moves the row to the first or last position. |
| Enter / Space | On an item: activates it (its click). On the playback control: pauses or plays autoplay. On the previous or the next control: moves the row back or on by one position (CRL-43). |
备注
- Scrolling is never the only way to reach an item (WCAG 2.5.7, 2.1.1): the previous and next controls move the row one position with a single press of a mouse — drawn by default wherever the primary pointer is fine and can hover —, and the arrow keys, the indicator and screen-reader focus move through every item and bring it into place. The row draws no scroll bar, so without the controls a mouse could move the row only with Shift and the wheel.
- Where the previous and next controls are, and when. They stand over the ends of the row, as the arrows of Ant Design's and Mantine's carousels and the overlay layouts of Fluent 2's do, not in the controls row, which exists only for a featured row with dots or autoplay. By default (auto) they are drawn only for a pointer that is fine and can hover, and shown while it is over the carousel or focus is within it: a touch screen moves the row with the finger's own swipe, and there the controls would stand over the items for good. A product whose people need them on touch as well sets always. Their place in the Tab sequence is the WAI-ARIA carousel example's: the playback control, then previous and next, then the slides.
- A previous or next control at the end of a row that does not loop is disabled but keeps focus and its place in the Tab order (the Button's focusableWhenDisabled), so that pressing next again and again never drops focus to the page, and the number of stops does not change with the scroll.
- An item entering or leaving at an edge of a row with fluid widths is, between resting positions, narrower than a touch target (CRL-4, CRL-12). A row that snaps never rests there; a row that does not snap can, and the arrow keys, the indicator and a further scroll all bring that item to a slot, where it has its whole target.
- Autoplay (WCAG 2.2.2 Pause, Stop, Hide). A carousel that advances by itself holds while it is hovered, while focus is anywhere in it, while it is pressed and while it is hidden, and it never advances where the platform's reduced-motion setting is on (CRL-33, CRL-34). While autoplay is on, the playback control is drawn by default and is the first Tab stop, so a keyboard user reaches it before the items (CRL-17).
- Keep playback controllable. A product may hide the playback control (playbackControl false), but it should keep it wherever it can. The holds are not a way to stop the motion: on touch there is no hover, a finger on the carousel holds it only while it stays there, and a screen reader that reads without moving focus does not hold it at all. Without the control, a person on a phone can stop an autoplaying carousel only through the system's reduced-motion setting. Hide it only where the product gives the person another way to stop the motion, such as a pause for all moving content on the page.
- Autoplay makes no announcement and never moves focus (CRL-32). A person who wants to know what changed finds the current item as the indicator's value.
- Loop keeps the arrow keys and the indicator going round, but Home and End still go to the first and last item, so a person can always find the ends of an endless row.
- The indicator is one slider, not a row of unlabelled buttons: one Tab stop, the arrow keys to move, the current item's label as its value. Its hit area is the whole container and the controls row's height, so the dots need no target of their own.
- The current dot is told apart by its stretched shape, not by color alone (WCAG 1.4.1), and the other dots use the functional outline role (3:1 against the surface).
- Web: the host has no role; a shadow element with role list, named by label, holds the item slot, so the slotted `<tanrei-carousel-item>` elements (role listitem, around a shadow element with role button and tabindex, roving: 0 on the Tab stop, -1 on the others) are its items, as the interactive card draws its button. The indicator is a shadow element with role slider and tabindex 0, and the playback control a native `<button>`; both sit after the list, outside it. Compose has no list role: the row gives `collectionInfo` and each item `collectionItemInfo` with `Role.Button`; the indicator is a `progressBarRangeInfo` with `setProgress`. Flutter reads each item's position through `IndexedSemantics`, and the indicator is a `Semantics(slider: true)` with increase and decrease actions. SwiftUI's indicator is `accessibilityAdjustableAction`, React Native's the adjustable role with increment and decrement actions. SwiftUI and React Native have no position-in-set property for a custom container, so there the screen reader's own count of the items stands in for CRL-19's position and number; their READMEs say so.
- How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.
生成自 specs/carousel.spec.json, tokens/sys/measure.tokens.json
- 轮播是一个以
label命名的列表。每一项是一个以其标签命名的按钮,并带有它在各项中的位置和项的总数。它的内容对辅助技术隐藏。 - 视野之外的项仍然按顺序暴露给辅助技术,因此屏幕阅读器能到达每一项。把屏幕阅读器的焦点移到某一项上,会把它滚动到位。
- Tab 停靠点。 轮播最多有五个:播放控件(绘制时)、“上一个”和“下一个”控件(绘制时)、所有项(合为一个停靠点)和圆点(绘制时)。播放控件排在最前,在各项之前,这样键盘用户可以在到达其他任何东西之前先停下动效。“上一个”和“下一个”控件排在各项之前,与 WAI-ARIA 的轮播示例一致。进入各项时,焦点落在上一次拥有键盘焦点的那一项上;没有这样的项时,落在停靠于主项位置的那一项上。
- 向右方向键和向左方向键把焦点移到下一项和上一项。它们在两端停住,只有轮播循环时才会绕回。Home 和 End 移到第一项和最后一项,在循环的行里也是如此。在从右到左的布局中,向左方向键移到下一项。
- 移动焦点会把获得焦点的项带到主项的位置,范围末端不允许时则尽量靠近;在循环的行里走较短的那一边。在主项位于起始边缘、宽度固定且自由滚动的行里,只是把这一项完整地带入视野。
- Enter 和 Space激活获得焦点的项。按下后变成了滚动的操作,不会激活它起始时所在的项。
- 圆点是一个以
label命名的滑块,它的值是当前项的标签。方向键、Home 和 End,以及平台的递增和递减操作,都会让这一行移动一个位置。当前圆点靠拉长的形状来区分,而不只靠颜色。 - 播放控件是一个按钮,播放时以
pauseLabel命名,暂停时以playLabel命名。它没有按下状态,所以绝不会被读成“暂停,已按下”。激活它之后,焦点仍留在它上面。它的暂停和播放图标不会镜像;在图标字体加载完成之前,也不会把图标的名称当作文字画出来。 - “上一个”和“下一个”控件是分别以
previousLabel和nextLabel命名的按钮。激活后焦点仍留在它上面。在不循环的一行的两端,控件被禁用,但仍保留焦点并留在 Tab 顺序中。在auto下隐藏时,它们仍可以通过 Tab 到达、被屏幕阅读器读出,获得焦点时会显示出来。在从右到左的布局中,“上一个”控件位于右侧,尖角图标按图标集的规定镜像。 - 焦点在轮播内任何位置时,自动播放都会暂缓;它从不移动焦点,不朗读任何内容,在减弱动态效果的环境中从不运行。
- 焦点环画在项的内侧,这样不会被这一行裁切;圆点和播放控件的焦点环则画在它们的外围。它只在键盘聚焦时显示。
- 每一项的点击区域是它的容器加上两侧各半个间距,并且绝不小于最小目标,即使是小项也一样。仍在边缘处进入或离开的项比这更窄,它的点击区域绝不会盖住相邻项的点击区域。播放控件的点击区域是触摸目标的大小,圆点的点击区域是它们的整个容器加上那一行的高度。
- 滚动绝不是到达某一项的唯一方式:“上一个”和“下一个”控件让鼠标单次按下就能移动这一行(WCAG 2.5.7),方向键、圆点和屏幕阅读器的焦点都能到达每一项。
- 文本放大时,轮播的高度和项保持不变。它自己不含任何文字,你的内容自行跟随文本大小。
- 调试构建会报告:没有标签的项、没有
label的轮播、没有playLabel或pauseLabel的播放控件、没有previousLabel或nextLabel的“上一个”和“下一个”控件、放不下的圆点、未知的heroAlignment、contentAlignment或navigation,以及不是正数的itemWidth或autoplayInterval。Web 还会报告项的内容里的可交互元素。
已知的平台差距:
- SwiftUI 和 React Native 没有可用于自定义容器的“在集合中的位置”属性,因此由屏幕阅读器自己对项的计数,来代替每一项的位置和项的总数。SwiftUI 也没有列表角色:轮播是一个以
label命名的容器。 - Compose 没有列表角色。这一行提供它的集合大小,每一项提供自己的位置,并带有按钮角色,因此 TalkBack 朗读的是一个集合,而不是列表。
- 只有 Web 能检查项的内容,所以只有 Web 会报告其中的控件。在其他平台上,这条规则要靠你来遵守。
- Web:按照规格,Tab 停靠点应是上一次拥有键盘焦点的那一项,而 Web 上它会移到任何获得焦点的项,包括因点击而获得焦点的项。触控笔的快速滑动会停靠在最近的一项,而不是下一项。屏幕阅读器的虚拟光标如果不移动焦点,就只是像浏览器那样滚动这一行,不会把该项带到主项的位置。
- SwiftUI:轮播的高度始终等于它的高度令牌,因为 SwiftUI 无法区分 frame 与栈给出的尺寸提议。请通过层的令牌来调整它的尺寸。停靠时的移动遵循滚动视图自身的减速,而不是轮播的滚动动效,在减弱动态效果的环境中也不会立即完成。它会构建每一项,并在每个滚动帧重新放置每一项。方向键、Tab 停靠点和 VoiceOver 焦点是作为规则来测试的,没有在真机上用键盘或 VoiceOver 测试。
- Compose 会组合并布局每一项,包括视野之外的项。滚轮不会产生快速滑动,因此会停靠在最近的一项。滚动只在 Robolectric 下验证过,没有在真机上验证。
- Flutter 会在每一次鼠标滚轮事件之后让开启停靠的轮播停靠一次,而不是等滚轮手势结束。它尚未在真机上用 TalkBack 和 VoiceOver 检查过。
- React Native 在每个滚动帧都在 JavaScript 线程上重新布局每一项,并且渲染每一项。方向键只在硬件键盘的事件能到达应用的环境中有效。由滚动驱动的放置、停靠、从右到左和屏幕阅读器焦点只在 Jest 中验证过,从未在真机上验证。
- 在
m3-like下,Material 的轮播使用表面颜色的地方,这里的项有意保留默认的填充,这样图片尚未加载出来的项仍然看得见。
- 为轮播提供
label,并为每一项提供说明其内容展示了什么的标签。内容不会被朗读。 - 使用自动播放时,为播放控件提供说明其作用的
playLabel和pauseLabel,并让它保持显示。 - 为“上一个”和“下一个”控件提供
previousLabel和nextLabel;除非页面为鼠标提供了移动这一行的其他方式,否则让navigation保持为auto。 - 项的内容里不要放控件。
- 如果内容带有标签没有表达的含义,例如海报上的价格,就把它写进标签。
- 在 SwiftUI、Compose 和 React Native 上,项的数量要适中:这些平台上的轮播会构建每一项,包括视野之外的项。