- Web
tanrei-fab-menuschedule - SwiftUI
TanreiFabMenuschedule - Jetpack Compose
TanreiFabMenuschedule - Flutter
TanreiFabMenuschedule - React Native
FabMenuschedule
生成自 docs/status/status.json, specs/fab-menu.spec.json
并非每个设计系统都有
m3-expressive-like, m3-like 提供此组件,由 @tanrei/web/m3-expressive-like, @tanrei/web/m3-like 注册。
tanrei, cupertino 中没有此组件。 它位于 m3-like 层,而这些系统并不继承该层。 A system outside this layer draws no floating action button that opens a list: its main action button opens a bottom sheet that lists the related actions. 请改用 Main action button, Bottom sheet。
基于没有此组件的系统构建的产品仍可使用它:在你自己的层中引入它(你自己的设计系统),并导入它自己的模块。这样它会用你的系统自己的令牌绘制。
"components": {
"include": [{ "name": "fab-menu", "from": "m3-like" }]
}import '@tanrei/web/components/fab-menu';关闭与打开
m3-like 显示切换按钮画成一个收起的主操作按钮,带有标签和图标。菜单里的项都是操作,每一项有一个图标和一个标签,传入的方式与各平台传入重复子项的方式相同。和主操作按钮一样,FAB 菜单不会自己定位:由你的布局来放置切换按钮,通常放在末尾侧的底角。菜单项出现在它的上方,位于平台的浮层之中。
<tanrei-fab-menu class="primary-action" label="新建">
<span slot="icon">add</span>
<tanrei-fab-menu-item label="文档" icon="description"></tanrei-fab-menu-item>
<tanrei-fab-menu-item label="文件夹" icon="create_new_folder"></tanrei-fab-menu-item>
<tanrei-fab-menu-item label="上传" icon="upload"></tanrei-fab-menu-item>
</tanrei-fab-menu>
<style>
/* 元素自己不做任何定位:由页面把切换按钮放在末尾侧的底角。 */
tanrei-fab-menu.primary-action {
position: fixed;
inset-inline-end: var(--tanrei-sys-space-space400);
inset-block-end: var(--tanrei-sys-space-space400);
}
</style>
<script type="module">
// FAB 菜单位于 m3-like 层:m3-like 系统的入口会把它和其他组件一起注册。
import '@tanrei/web/m3-like';
const fabMenu = document.querySelector('tanrei-fab-menu');
// 切换按钮只是请求打开或关闭;open 由你来设置。
fabMenu.addEventListener('toggle', (e) => (fabMenu.open = e.detail.open));
// close 带有关闭的原因:escape、outside、tab、focus-out 或 item。Web 从不发送 back。
fabMenu.addEventListener('close', () => (fabMenu.open = false));
// 菜单项的 click 会冒泡,并且先于 close 到达。
fabMenu.querySelector('[label="上传"]').addEventListener('click', upload);
</script>// FAB 菜单位于 m3-like 层:它在包含它的系统(m3-like、m3-expressive-like)的入口中,也在 /all 中。
import { FabMenu, FabMenuItem } from '@tanrei/react/m3-like';
<FabMenu
className="primary-action"
label="新建"
open={open}
onToggle={(e) => setOpen(e.detail.open)}
onClose={() => setOpen(false)}
>
<span slot="icon">add</span>
<FabMenuItem label="文档" icon="description" onClick={newDocument} />
<FabMenuItem label="文件夹" icon="create_new_folder" onClick={newFolder} />
<FabMenuItem label="上传" icon="upload" onClick={upload} />
</FabMenu>import Tanrei
// 放置方式与 FAB 相同。切换按钮和菜单项都会写入这个绑定。
DocumentList()
.overlay(alignment: .bottomTrailing) {
TanreiFabMenu("新建", icon: "add", isPresented: $showsCreate) {
TanreiFabMenuItem("文档", icon: "description", action: newDocument)
TanreiFabMenuItem("文件夹", icon: "create_new_folder", action: newFolder)
TanreiFabMenuItem("上传", icon: "upload", action: upload)
}
.padding()
}import io.github.chnotchy.tanrei.components.TanreiFabMenu
import io.github.chnotchy.tanrei.components.TanreiFabMenuItem
import io.github.chnotchy.tanrei.primitives.TanreiText
// 状态提升:expanded 由你保存。把它放进 scaffold 的 floatingActionButton 插槽,或者自己放置。
TanreiFabMenu(
expanded = expanded,
onExpandedChange = { expanded = it },
onDismissRequest = { expanded = false },
contentDescription = "新建",
icon = { TanreiText("add") },
) {
TanreiFabMenuItem(text = "文档", icon = "description", onClick = ::newDocument)
TanreiFabMenuItem(text = "文件夹", icon = "create_new_folder", onClick = ::newFolder)
TanreiFabMenuItem(text = "上传", icon = "upload", onClick = ::upload)
}import 'package:tanrei/tanrei.dart';
// FAB 菜单通过控制器自己打开和关闭;onToggle 和 onClose 报告这一变化。
Scaffold(
body: const DocumentList(),
floatingActionButton: TanreiFabMenu(
controller: createMenu,
semanticLabel: '新建',
icon: const Icon(AppSymbols.add),
items: <TanreiFabMenuItem>[
TanreiFabMenuItem(label: '文档', icon: const Icon(AppSymbols.description), onPressed: newDocument),
TanreiFabMenuItem(label: '文件夹', icon: const Icon(AppSymbols.createNewFolder), onPressed: newFolder),
TanreiFabMenuItem(label: '上传', icon: const Icon(AppSymbols.upload), onPressed: upload),
],
),
);import { View } from 'react-native';
import { FabMenu } from '@tanrei/react-native';
// 受控:在 onToggle 中设置 visible,在 onDismiss 中把它设回 false;后者会收到关闭的原因。
// 切换按钮从不自己定位:由 style 来放置它。
<View style={{ flex: 1 }}>
<DocumentList />
<FabMenu
icon="add"
accessibilityLabel="新建"
visible={open}
onToggle={setOpen}
onDismiss={() => setOpen(false)}
items={[
{ label: '文档', icon: 'description', onPress: newDocument },
{ label: '文件夹', icon: 'create_new_folder', onPress: newFolder },
{ label: '上传', icon: 'upload', onPress: upload },
]}
style={styles.primaryAction}
/>
</View>变体、尺寸与禁用
m3-like 显示构成
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
variant | filled | tonal | surface | "filled" | Emphasis, as the main action button's variants of the same names: it colors the toggle and the items. A design system may add values. |
size | small | medium | large | "medium" | The toggle's size (40 / 48 / 64, square), as the main action button's. The items keep one size. A design system may add values. |
open | boolean | false | Whether the items are shown. Where open is a plain value (web, Compose, React Native) the product sets it from toggle and sets it to false from close; where it is a binding or a controller (SwiftUI, Flutter) the FAB menu opens and closes itself through it (FBM-1, FBM-15). |
label | string | "" | The accessible name of the toggle and of the list ("Create"). Required: an icon alone is not a name. |
disabled | boolean | false | Prevents opening and removes the toggle from the focus order. Hiding a FAB menu whose actions are unavailable is usually better than disabling it. |
插槽
| 插槽 | 说明 |
|---|---|
icon | The toggle's glyph while closed (a glyph of the system's icon set). |
事件
| 事件 | 说明 |
|---|---|
toggle | The toggle asks to open or close the menu; carries the open value it asks for. |
close | The menu asks to be closed; carries the reason: escape, back, outside, tab, focus-out or item (in the platform's casing). |
条目
该组件重复排列的条目种类。测试样例按顺序列出条目;各平台以各自的惯用方式为每个种类命名。
item
One action of the menu: an icon and a label on a raised container.
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
label | string | "" | The item's text and its accessible name. Required. |
icon | string | "" | The name of the item's icon (a glyph of the system's icon set). Required. |
| 事件 | 说明 |
|---|---|
click | The item was activated by pointer, touch, keyboard or assistive technology. |
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 条目 | tanrei-fab-menu-item | TanreiFabMenuItem | TanreiFabMenuItem | TanreiFabMenuItem | FabMenuItem |
label · 属性 | label | title | text | label | label |
icon · 属性 | icon | icon | icon | icon | icon |
click · 事件 | click | action | onClick | onPressed | onPress |
| 备注 | A child element: <tanrei-fab-menu-item label="Upload" icon="upload">; its click bubbles. | TanreiFabMenuItem(_ title:, icon:, action:) in the view builder. | TanreiFabMenuItem(text, icon, onClick) called in the content lambda. | TanreiFabMenuItem(label:, icon:, onPressed:) in the items: list. | The element type of the items array: { label, icon, onPress }, where icon is the glyph's name. |
状态
行为
以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。
| ID | 需求 |
|---|---|
FBM-1 | WHEN the user activates the toggle by pointer, touch, Enter or Space THE FAB menu SHALL emit toggle exactly once with the opposite of open, and WHEN the user presses Down Arrow or Up Arrow on the toggle while open is false it SHALL emit toggle with true; the menu SHALL then open or close through open: through its binding or controller where open is one (SwiftUI, Flutter), and by the product setting open where open is a plain value (web, Compose, React Native). |
FBM-2 | WHILE disabled is true THE FAB menu SHALL NOT emit toggle or close, SHALL NOT make the toggle focusable, SHALL expose the disabled state to assistive technology, and SHALL draw the toggle closed and present no items, whatever open says. |
FBM-3 | WHILE the toggle or an item is hovered, keyboard-focused or pressed THE FAB menu SHALL render its container, icon, label, elevation and opacity from the component tokens of its variant in its own highest-precedence active state — the toggle from its closed parts (comp.fab-menu.filled.container.color for filled, and so on) or its open parts (comp.fab-menu.filled.open.container.color, and so on), an item from the item parts (comp.fab-menu.filled.item.container.color, and so on) — with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state. |
FBM-4 | WHEN the toggle or an item receives keyboard focus THE FAB menu SHALL show the focus ring around that one's own shape, without animation; focus gained by pointer or touch SHALL NOT show it. |
FBM-5 | WHERE the toggle is smaller than the minimum target THE FAB menu SHALL extend its hit area on both axes to sys.target.touch for touch input and sys.target.pointer for fine pointers, without changing its visual size; THE FAB menu SHALL make each item at least that target tall, because stacked items cannot extend a hit area over each other, and each item's whole container SHALL be its hit area; a platform that cannot tell the input type SHALL use sys.target.touch. |
FBM-6 | WHILE disabled is true THE FAB menu SHALL render the toggle's container, icon, elevation and opacity from the disabled component tokens of its variant's closed parts. |
FBM-7 | WHEN variant or size names a value that the active design system does not have THE FAB menu SHALL render with the default value's tokens, and in debug builds report the unknown value. |
FBM-8 | WHEN the toggle is pressed by pointer, touch, Enter or Space THE toggle SHALL show the pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner. |
FBM-9 | WHILE pressed THE toggle 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; icon, elevation, 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.fab-menu.motion.feedback-duration with comp.fab-menu.motion.state-easing, fades over comp.fab-menu.motion.state-duration with comp.fab-menu.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced. Each item SHALL answer its own press in the same way, within its own container and from its item parts: label, icon, elevation and opacity still come from its pressed tokens. |
FBM-10 | THE toggle SHALL ease its container corners 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 its 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. Each item SHALL ease its corners and show its keyboard focus in the same way. |
FBM-11 | THE FAB menu SHALL draw the toggle as a square whose side is the container size of its size (comp.fab-menu.container.size.medium for medium, and so on), with the corners of its size (comp.fab-menu.container.shape.medium, and so on) while closed and comp.fab-menu.container.shape-open while open, and its icon centred in it at the icon size of its size (comp.fab-menu.icon.size.medium, and so on) through the icon helper, with the family and fill of an unselected icon, sys.icon.weight, and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max: the icon slot's glyph while closed, and the glyph close of the system's icon set while open, not mirrored when the layout direction is right-to-left. THE FAB menu SHALL draw no mark of its own (no path, shape or character of its own) for the close glyph. |
FBM-12 | THE FAB menu SHALL NOT position itself: the toggle SHALL lay out at its own size where the layout that holds it places it (normally the inline-end bottom corner of the screen, clear of any navigation bar), as a main action button does (MAB-18), and the items SHALL be presented above the page rather than laid out, so that they move no content. |
FBM-13 | WHEN open becomes true THE FAB menu SHALL present its items in the platform's overlay layer (the web's top layer through the popover API, SwiftUI's presentation as the menu's, Compose's Popup, Flutter's Overlay, React Native's overlay layer that TanreiProvider renders) as a column above the toggle, comp.fab-menu.list.offset from its top edge and comp.fab-menu.list.gap apart, the first item at the top and every item's end edge aligned with the toggle's, and SHALL move focus to the first item, or to the last (the one nearest the toggle) when Up Arrow opened it; WHEN open becomes false THE FAB menu SHALL remove the items and, if focus was in them, return focus to the toggle. The FAB menu is an operated overlay, so it takes focus and returns it although it is not modal. |
FBM-14 | WHILE open is true THE FAB menu SHALL NOT trap focus, SHALL NOT make the rest of the screen inert and SHALL draw no scrim: it is an operated overlay that is not modal. |
FBM-15 | WHEN the user presses Escape or the platform's back, presses outside the items and the toggle, presses Tab or Shift+Tab on an item, or moves focus out of the items and the toggle THE FAB menu SHALL emit close once with that reason (escape, back, outside, tab, focus-out) and SHALL be closed: through its binding or controller where open is one (SwiftUI, Flutter), and by the product setting open to false where open is a plain value (web, Compose, React Native); Tab and Shift+Tab SHALL then move focus on from the toggle as they would have from it. A press on the toggle while open is not a press outside: it SHALL emit toggle with false (FBM-1) and no close. Whether a press outside also reaches the element under it SHALL follow the platform, as the menu's does, and be stated in its README. |
FBM-16 | WHEN the user activates an item by pointer, touch, Enter or Space THE FAB menu SHALL emit the item's click once and then emit close with the reason item, in that order, and SHALL be closed as FBM-15 says. |
FBM-17 | WHILE open is true and an item has keyboard focus THE FAB menu SHALL move focus to the next item with Down Arrow and to the previous one with Up Arrow, wrapping at the ends, and to the first and last item with Home and End. |
FBM-18 | THE FAB menu SHALL draw each item as a container at least comp.fab-menu.item.height tall and as wide as its content, with corners comp.fab-menu.item.shape eased by sys.shape.corner-smoothing, holding its icon and then its label comp.fab-menu.item.gap apart, padded by comp.fab-menu.item.padding-inline-start at the start and comp.fab-menu.item.padding-inline-end at the end; the label SHALL be set in comp.fab-menu.item.label.typescale on one line while it fits and SHALL wrap rather than truncate, the item growing with it; the icon SHALL be drawn at comp.fab-menu.item.icon.size through the icon helper with the family and fill of an unselected icon, sys.icon.weight, and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max; and the item SHALL cast its item elevation (comp.fab-menu.filled.item.container.elevation for filled, and so on) outside its container without clipping it. |
FBM-19 | WHEN the FAB menu opens THE FAB menu SHALL change the toggle's colors and corners and cross-fade its icon to the glyph close (a cross-fade of two glyphs, neither drawn along a path) over comp.fab-menu.motion.open-duration with comp.fab-menu.motion.open-easing, and fade and grow each item in from its end edge over the same, starting with the item nearest the toggle and delaying each next one by comp.fab-menu.motion.stagger; WHEN it closes it SHALL reverse the toggle's change and fade all items out together over comp.fab-menu.motion.close-duration with comp.fab-menu.motion.close-easing; WHERE motion is reduced it SHALL change and show or hide everything at once. |
FBM-20 | THE FAB menu SHALL expose the toggle as a button named by label that opens a menu, with whether the menu is open where the platform has those properties, and while open SHALL expose the items as a menu named by label, each a menu item named by its own label with its position and the number of items; the icons SHALL be hidden. |
FBM-21 | THE FAB menu SHALL cast the toggle's elevation, of its variant and state (comp.fab-menu.filled.container.elevation for filled at rest, comp.fab-menu.filled.open.container.elevation while open, and so on), outside its container without clipping it, and WHEN the elevation or the colors of the toggle or an item change between interaction states SHALL animate the change over comp.fab-menu.motion.state-duration with comp.fab-menu.motion.state-easing, changing them at once where motion is reduced. |
FBM-22 | WHEN the layout direction is right-to-left THE FAB menu SHALL align the items' end edges with the toggle's left edge, mirror the order of each item's icon and label (a glyph itself only as FBM-26 says) and apply its paddings by logical start and end, and SHALL grow a ripple from the point of contact as in left-to-right. |
FBM-23 | WHILE label is not empty THE FAB menu SHALL expose it as the toggle's accessible name, over a name the product gives it on the element (aria-label or aria-labelledby). WHILE label is empty and the product gives the FAB menu a name of its own on the element — aria-label, or aria-labelledby naming an element — THE FAB menu SHALL expose that name as the toggle's accessible name and name the list by the toggle. WHILE label is empty and the product gives it none, an item's label or icon is empty, or the menu holds fewer than two or more than six items, THE FAB menu SHALL still render, and in debug builds report it. |
FBM-24 | IF the icon set cannot draw the close glyph yet (its font is still loading or failed to load) WHILE open is true THE FAB menu SHALL draw nothing in the toggle's icon box, never the glyph's name as text, SHALL keep the toggle's size, colors, corners, name and expanded state, and SHALL draw the glyph once the icon set can, without the cross-fade of FBM-19. |
FBM-25 | THE FAB menu 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 FAB menu is shown THE FAB menu SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state and its focus. |
FBM-26 | WHERE the layout direction is right-to-left THE FAB menu SHALL draw the glyph of its icon slot while closed and the icon of each item mirrored left to right exactly when the system's icon set marks that glyph's name as mirrored or the product forces mirroring for that use, and unmirrored when the product forbids it: the icon helper mirrors the glyph inside its own box, about the box's vertical centre line, changing neither its place, size, color and fill nor the order FBM-22 gives. THE FAB menu SHALL NOT mirror a glyph itself, and SHALL NOT mirror content the product gives as an image or a view of its own rather than by a glyph's name. The close glyph of the open toggle is not marked and is never mirrored (FBM-11). |
FBM-27 | THE FAB menu SHALL lay out the label of each item as text of its own direction, isolated from the text around it: each takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a number with its sign or unit, a telephone number and an address keep their own order in a right-to-left layout and reorder nothing beside them; THE FAB menu SHALL still place each label and align its lines by the layout direction, not by the text's own. |
FBM-28 | 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 FAB menu 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 element that carries its toggle's role — an id reference by the elements it names in the host's tree or a tree around it, an error message as part of the description while the toggle is invalid — except those the FAB menu owns (aria-haspopup, aria-expanded, aria-controls, aria-disabled), which it SHALL move and not expose. 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 FAB menu SHALL take it off that element. An item's host carries its menu item role itself (FBM-20), so what the product writes on it is not moved, but for a name: WHILE an item's label is not empty THE FAB menu SHALL hold the product's aria-label and aria-labelledby on that host as their data-aria- twins, so that label is the item's whole name although the platform gives aria-labelledby the precedence, and WHEN label is emptied SHALL give them back as they were written; WHILE an item's label is empty a name the product gives so is the item's name and SHALL NOT be reported as having none in debug builds (FBM-23); WHEN the product removes one, or its data-aria- twin, or sets the ARIA property that reflects it to null, THE FAB menu SHALL take it away. WHEN the product sets an aria- attribute the FAB menu owns THE FAB menu SHALL report it in debug builds. |
各平台名称对照
对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 组件 | tanrei-fab-menu | TanreiFabMenu | TanreiFabMenu | TanreiFabMenu | FabMenu |
variant · 属性 | variant | variant | variant | variant | variant |
size · 属性 | size | size | size | size | size |
open · 属性 | open | isPresented | expanded | controller | visible |
label · 属性 | label | label | contentDescription | semanticLabel | accessibilityLabel |
disabled · 属性 | disabled | disabled | enabled | enabled | disabled |
toggle · 事件 | toggle | isPresented | onExpandedChange | onToggle | onToggle |
close · 事件 | close | isPresented | onDismissRequest | onClose | onDismiss |
| 备注 | <tanrei-fab-menu label="Create" variant size open> with a slot="icon" glyph and tanrei-fab-menu-item children. The toggle is a <button> in the shadow root with aria-haspopup="menu", aria-expanded and aria-controls; the items are a manual popover in the top layer with role menu, placed with CSS anchor positioning and a script fallback, as the menu is. toggle is a CustomEvent whose detail.open is the value asked for (not the popover's ToggleEvent, which does not leave the shadow root); close carries detail.reason. The page positions the element. The inline attribute, for tests, draws the open items in place (FBM-13). The open toggle's glyph is the icon helper's close in the icon part (FBM-11). It lives in the m3-like layer: @tanrei/web/m3-like registers it (with @tanrei/web/components/fab-menu, its own module), and @tanrei/web does not; its tokens are in the stylesheet of the m3-like systems (@tanrei/web/systems/m3-like.css, m3-expressive-like.css) and of a product that includes it. | TanreiFabMenu(_ label:, icon:, variant:, size:, isPresented: Binding<Bool>) { TanreiFabMenuItem … }, placed by the caller as the main action button is (an .overlay(alignment: .bottomTrailing)). The items are presented as TanreiMenu presents its surface, with a clear background, so a press outside closes them. Writing the binding is toggle and close. disabled follows .disabled(_:). iOS has no menu role: the items are buttons in a container named by label. Without an installed icon font it strokes the close glyph as a shape of its own (TanreiChipGlyph(kind: .close)) instead of drawing nothing (FBM-11, FBM-24). The FAB menu lives in the m3-like layer, and only the systems that descend from it have its tokens; whether this platform ships it in a package of its own is decided when the platform catches up with the web. | TanreiFabMenu(expanded, onExpandedChange, onDismissRequest, contentDescription, icon, variant, size, enabled) { TanreiFabMenuItem(…) }, placed through Scaffold's floatingActionButton slot or by the caller; the items are shown in a Popup. State hoisting: the caller keeps expanded, sets it from onExpandedChange and to false from onDismissRequest, which carries the reason. Material 3's FloatingActionButtonMenu is not used. enabled is the inverse flag. Without an installed icon font it strokes the close glyph as a vector of its own (TanreiDrawnGlyph) instead of drawing nothing (FBM-11, FBM-24). The FAB menu lives in the m3-like layer, and only the systems that descend from it have its tokens; whether this platform ships it in a package of its own is decided when the platform catches up with the web. | TanreiFabMenu(controller:, semanticLabel:, icon:, variant:, size:, enabled:, items: [TanreiFabMenuItem(…)], onToggle:, onClose:), built with the widgets library and placed through Scaffold.floatingActionButton or by the caller; the items are shown in the Overlay. The controller is a TanreiMenuController (the Tanrei menu's own), passed or created internally; it is open, so the FAB menu opens and closes itself; onToggle and onClose report it. enabled is a flag here, because the toggle's press is the component's own. Lagging behind FBM-24: it writes the close glyph as the Material Symbols code point U+E5CD (tanreiCloseGlyph), and without the icon font it draws whatever the fallback face has there instead of nothing. The FAB menu lives in the m3-like layer, and only the systems that descend from it have its tokens; whether this platform ships it in a package of its own is decided when the platform catches up with the web. | <FabMenu icon="add" accessibilityLabel visible onToggle onDismiss items={FabMenuItem[]} variant size disabled>; the caller positions it. The items are presented on the overlay layer that TanreiProvider renders (not a Modal, which would make the rest of the screen unreachable) and placed by measuring the toggle (measureInWindow); Android's back button (BackHandler) is close. Controlled: the caller sets visible from onToggle and to false from onDismiss, which carries the reason. Lagging behind FBM-24: without the icon font the ligature close prints as a word. The FAB menu lives in the m3-like layer, and only the systems that descend from it have its tokens; whether this platform ships it in a package of its own is decided when the platform catches up with the web. |
令牌
| 令牌 | 引用 | 解析值(浅色) |
|---|---|---|
--tanrei-comp-fab-menu-container-shape-open | sys.shape.corner.full | 9999px |
--tanrei-comp-fab-menu-container-shape-large | sys.shape.corner.extra-large | 28px |
--tanrei-comp-fab-menu-container-shape-medium | sys.shape.corner.large | 16px |
--tanrei-comp-fab-menu-container-shape-small | sys.shape.corner.medium | 12px |
--tanrei-comp-fab-menu-container-size-large | — | 96px |
--tanrei-comp-fab-menu-container-size-medium | — | 56px |
--tanrei-comp-fab-menu-container-size-small | — | 40px |
--tanrei-comp-fab-menu-disabled-opacity | — | 1 |
--tanrei-comp-fab-menu-filled-container-color | sys.color.primary | |
--tanrei-comp-fab-menu-filled-container-elevation | sys.elevation.level3 | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-filled-disabled-container-color | — | |
--tanrei-comp-fab-menu-filled-disabled-container-elevation | — | |
--tanrei-comp-fab-menu-filled-disabled-icon-color | — | |
--tanrei-comp-fab-menu-filled-disabled-item-container-color | — | |
--tanrei-comp-fab-menu-filled-disabled-item-container-elevation | — | |
--tanrei-comp-fab-menu-filled-disabled-item-icon-color | — | |
--tanrei-comp-fab-menu-filled-disabled-item-label-color | — | |
--tanrei-comp-fab-menu-filled-disabled-item-state-layer-color | — | |
--tanrei-comp-fab-menu-filled-disabled-open-container-color | — | |
--tanrei-comp-fab-menu-filled-disabled-open-container-elevation | — | |
--tanrei-comp-fab-menu-filled-disabled-open-icon-color | — | |
--tanrei-comp-fab-menu-filled-disabled-open-state-layer-color | — | |
--tanrei-comp-fab-menu-filled-disabled-state-layer-color | — | |
--tanrei-comp-fab-menu-filled-focused-container-color | — | |
--tanrei-comp-fab-menu-filled-focused-container-elevation | — | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-filled-focused-icon-color | — | |
--tanrei-comp-fab-menu-filled-focused-item-container-color | — | |
--tanrei-comp-fab-menu-filled-focused-item-container-elevation | — | 0px 1px 2px 0px rgb(0 0 0 / 0.3), 0px 2px 6px 2px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-filled-focused-item-icon-color | — | |
--tanrei-comp-fab-menu-filled-focused-item-label-color | — | |
--tanrei-comp-fab-menu-filled-focused-item-state-layer-color | — | |
--tanrei-comp-fab-menu-filled-focused-open-container-color | — | |
--tanrei-comp-fab-menu-filled-focused-open-container-elevation | — | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-filled-focused-open-icon-color | — | |
--tanrei-comp-fab-menu-filled-focused-open-state-layer-color | — | |
--tanrei-comp-fab-menu-filled-focused-state-layer-color | — | |
--tanrei-comp-fab-menu-filled-hovered-container-color | — | |
--tanrei-comp-fab-menu-filled-hovered-container-elevation | — | 0px 2px 3px 0px rgb(0 0 0 / 0.3), 0px 6px 10px 4px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-filled-hovered-icon-color | — | |
--tanrei-comp-fab-menu-filled-hovered-item-container-color | — | |
--tanrei-comp-fab-menu-filled-hovered-item-container-elevation | — | 0px 1px 2px 0px rgb(0 0 0 / 0.3), 0px 2px 6px 2px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-filled-hovered-item-icon-color | — | |
--tanrei-comp-fab-menu-filled-hovered-item-label-color | — | |
--tanrei-comp-fab-menu-filled-hovered-item-state-layer-color | — | |
--tanrei-comp-fab-menu-filled-hovered-open-container-color | — | |
--tanrei-comp-fab-menu-filled-hovered-open-container-elevation | — | 0px 2px 3px 0px rgb(0 0 0 / 0.3), 0px 6px 10px 4px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-filled-hovered-open-icon-color | — | |
--tanrei-comp-fab-menu-filled-hovered-open-state-layer-color | — | |
--tanrei-comp-fab-menu-filled-hovered-state-layer-color | — | |
--tanrei-comp-fab-menu-filled-icon-color | sys.color.on-primary | |
--tanrei-comp-fab-menu-filled-item-container-color | sys.color.primary | |
--tanrei-comp-fab-menu-filled-item-container-elevation | sys.elevation.level2 | 0px 1px 2px 0px rgb(0 0 0 / 0.3), 0px 2px 6px 2px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-filled-item-icon-color | sys.color.on-primary | |
--tanrei-comp-fab-menu-filled-item-label-color | sys.color.on-primary | |
--tanrei-comp-fab-menu-filled-item-state-layer-color | comp.fab-menu.filled.item.label.color | |
--tanrei-comp-fab-menu-filled-open-container-color | sys.color.secondary-container | |
--tanrei-comp-fab-menu-filled-open-container-elevation | sys.elevation.level3 | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-filled-open-icon-color | sys.color.on-secondary-container | |
--tanrei-comp-fab-menu-filled-open-state-layer-color | comp.fab-menu.filled.open.icon.color | |
--tanrei-comp-fab-menu-filled-pressed-container-color | — | |
--tanrei-comp-fab-menu-filled-pressed-container-elevation | — | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-filled-pressed-icon-color | — | |
--tanrei-comp-fab-menu-filled-pressed-item-container-color | — | |
--tanrei-comp-fab-menu-filled-pressed-item-container-elevation | — | 0px 1px 2px 0px rgb(0 0 0 / 0.3), 0px 2px 6px 2px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-filled-pressed-item-icon-color | — | |
--tanrei-comp-fab-menu-filled-pressed-item-label-color | — | |
--tanrei-comp-fab-menu-filled-pressed-item-state-layer-color | — | |
--tanrei-comp-fab-menu-filled-pressed-open-container-color | — | |
--tanrei-comp-fab-menu-filled-pressed-open-container-elevation | — | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-filled-pressed-open-icon-color | — | |
--tanrei-comp-fab-menu-filled-pressed-open-state-layer-color | — | |
--tanrei-comp-fab-menu-filled-pressed-state-layer-color | — | |
--tanrei-comp-fab-menu-filled-state-layer-color | comp.fab-menu.filled.icon.color | |
--tanrei-comp-fab-menu-focused-opacity | — | 1 |
--tanrei-comp-fab-menu-hovered-opacity | — | 1 |
--tanrei-comp-fab-menu-icon-size-large | sys.icon.large | 36px |
--tanrei-comp-fab-menu-icon-size-medium | sys.icon.medium | 24px |
--tanrei-comp-fab-menu-icon-size-small | sys.icon.small | 24px |
--tanrei-comp-fab-menu-item-gap | sys.space.space100 | 8px |
--tanrei-comp-fab-menu-item-height | — | 56px |
--tanrei-comp-fab-menu-item-icon-size | sys.icon.small | 24px |
--tanrei-comp-fab-menu-item-padding-inline-end | sys.space.space250 | 20px |
--tanrei-comp-fab-menu-item-padding-inline-start | sys.space.space200 | 16px |
--tanrei-comp-fab-menu-item-shape | sys.shape.corner.large | 9999px |
--tanrei-comp-fab-menu-list-gap | sys.space.space100 | 4px |
--tanrei-comp-fab-menu-list-offset | sys.space.space100 | 8px |
--tanrei-comp-fab-menu-motion-close-duration | sys.motion.duration.short3 | 150ms |
--tanrei-comp-fab-menu-motion-close-easing | sys.motion.easing.emphasized-accelerate | cubic-bezier(0.3, 0, 0.8, 0.15) |
--tanrei-comp-fab-menu-motion-feedback-duration | sys.motion.role.feedback | 450ms |
--tanrei-comp-fab-menu-motion-open-duration | sys.motion.duration.medium1 | 250ms |
--tanrei-comp-fab-menu-motion-open-easing | sys.motion.easing.emphasized-decelerate | cubic-bezier(0.05, 0.7, 0.1, 1) |
--tanrei-comp-fab-menu-motion-stagger | sys.motion.duration.stagger | 100ms |
--tanrei-comp-fab-menu-motion-state-duration | sys.motion.role.state.default | 150ms |
--tanrei-comp-fab-menu-motion-state-easing | sys.motion.easing.state | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-fab-menu-pressed-opacity | — | 1 |
--tanrei-comp-fab-menu-surface-container-color | sys.color.surface-container-lowest | |
--tanrei-comp-fab-menu-surface-container-elevation | sys.elevation.level3 | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-surface-disabled-container-color | — | |
--tanrei-comp-fab-menu-surface-disabled-container-elevation | — | |
--tanrei-comp-fab-menu-surface-disabled-icon-color | — | |
--tanrei-comp-fab-menu-surface-disabled-item-container-color | — | |
--tanrei-comp-fab-menu-surface-disabled-item-container-elevation | — | |
--tanrei-comp-fab-menu-surface-disabled-item-icon-color | — | |
--tanrei-comp-fab-menu-surface-disabled-item-label-color | — | |
--tanrei-comp-fab-menu-surface-disabled-item-state-layer-color | — | |
--tanrei-comp-fab-menu-surface-disabled-open-container-color | — | |
--tanrei-comp-fab-menu-surface-disabled-open-container-elevation | — | |
--tanrei-comp-fab-menu-surface-disabled-open-icon-color | — | |
--tanrei-comp-fab-menu-surface-disabled-open-state-layer-color | — | |
--tanrei-comp-fab-menu-surface-disabled-state-layer-color | — | |
--tanrei-comp-fab-menu-surface-focused-container-color | — | |
--tanrei-comp-fab-menu-surface-focused-container-elevation | — | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-surface-focused-icon-color | — | |
--tanrei-comp-fab-menu-surface-focused-item-container-color | — | |
--tanrei-comp-fab-menu-surface-focused-item-container-elevation | — | 0px 1px 2px 0px rgb(0 0 0 / 0.3), 0px 2px 6px 2px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-surface-focused-item-icon-color | — | |
--tanrei-comp-fab-menu-surface-focused-item-label-color | — | |
--tanrei-comp-fab-menu-surface-focused-item-state-layer-color | — | |
--tanrei-comp-fab-menu-surface-focused-open-container-color | — | |
--tanrei-comp-fab-menu-surface-focused-open-container-elevation | — | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-surface-focused-open-icon-color | — | |
--tanrei-comp-fab-menu-surface-focused-open-state-layer-color | — | |
--tanrei-comp-fab-menu-surface-focused-state-layer-color | — | |
--tanrei-comp-fab-menu-surface-hovered-container-color | — | |
--tanrei-comp-fab-menu-surface-hovered-container-elevation | — | 0px 2px 3px 0px rgb(0 0 0 / 0.3), 0px 6px 10px 4px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-surface-hovered-icon-color | — | |
--tanrei-comp-fab-menu-surface-hovered-item-container-color | — | |
--tanrei-comp-fab-menu-surface-hovered-item-container-elevation | — | 0px 1px 2px 0px rgb(0 0 0 / 0.3), 0px 2px 6px 2px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-surface-hovered-item-icon-color | — | |
--tanrei-comp-fab-menu-surface-hovered-item-label-color | — | |
--tanrei-comp-fab-menu-surface-hovered-item-state-layer-color | — | |
--tanrei-comp-fab-menu-surface-hovered-open-container-color | — | |
--tanrei-comp-fab-menu-surface-hovered-open-container-elevation | — | 0px 2px 3px 0px rgb(0 0 0 / 0.3), 0px 6px 10px 4px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-surface-hovered-open-icon-color | — | |
--tanrei-comp-fab-menu-surface-hovered-open-state-layer-color | — | |
--tanrei-comp-fab-menu-surface-hovered-state-layer-color | — | |
--tanrei-comp-fab-menu-surface-icon-color | sys.color.primary | |
--tanrei-comp-fab-menu-surface-item-container-color | sys.color.surface-container-lowest | |
--tanrei-comp-fab-menu-surface-item-container-elevation | sys.elevation.level2 | 0px 1px 2px 0px rgb(0 0 0 / 0.3), 0px 2px 6px 2px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-surface-item-icon-color | sys.color.primary | |
--tanrei-comp-fab-menu-surface-item-label-color | sys.color.on-surface | |
--tanrei-comp-fab-menu-surface-item-state-layer-color | comp.fab-menu.surface.item.label.color | |
--tanrei-comp-fab-menu-surface-open-container-color | sys.color.secondary-container | |
--tanrei-comp-fab-menu-surface-open-container-elevation | sys.elevation.level3 | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-surface-open-icon-color | sys.color.on-secondary-container | |
--tanrei-comp-fab-menu-surface-open-state-layer-color | comp.fab-menu.surface.open.icon.color | |
--tanrei-comp-fab-menu-surface-pressed-container-color | — | |
--tanrei-comp-fab-menu-surface-pressed-container-elevation | — | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-surface-pressed-icon-color | — | |
--tanrei-comp-fab-menu-surface-pressed-item-container-color | — | |
--tanrei-comp-fab-menu-surface-pressed-item-container-elevation | — | 0px 1px 2px 0px rgb(0 0 0 / 0.3), 0px 2px 6px 2px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-surface-pressed-item-icon-color | — | |
--tanrei-comp-fab-menu-surface-pressed-item-label-color | — | |
--tanrei-comp-fab-menu-surface-pressed-item-state-layer-color | — | |
--tanrei-comp-fab-menu-surface-pressed-open-container-color | — | |
--tanrei-comp-fab-menu-surface-pressed-open-container-elevation | — | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-surface-pressed-open-icon-color | — | |
--tanrei-comp-fab-menu-surface-pressed-open-state-layer-color | — | |
--tanrei-comp-fab-menu-surface-pressed-state-layer-color | — | |
--tanrei-comp-fab-menu-surface-state-layer-color | comp.fab-menu.surface.icon.color | |
--tanrei-comp-fab-menu-tonal-container-color | sys.color.secondary-container | |
--tanrei-comp-fab-menu-tonal-container-elevation | sys.elevation.level3 | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-tonal-disabled-container-color | — | |
--tanrei-comp-fab-menu-tonal-disabled-container-elevation | — | |
--tanrei-comp-fab-menu-tonal-disabled-icon-color | — | |
--tanrei-comp-fab-menu-tonal-disabled-item-container-color | — | |
--tanrei-comp-fab-menu-tonal-disabled-item-container-elevation | — | |
--tanrei-comp-fab-menu-tonal-disabled-item-icon-color | — | |
--tanrei-comp-fab-menu-tonal-disabled-item-label-color | — | |
--tanrei-comp-fab-menu-tonal-disabled-item-state-layer-color | — | |
--tanrei-comp-fab-menu-tonal-disabled-open-container-color | — | |
--tanrei-comp-fab-menu-tonal-disabled-open-container-elevation | — | |
--tanrei-comp-fab-menu-tonal-disabled-open-icon-color | — | |
--tanrei-comp-fab-menu-tonal-disabled-open-state-layer-color | — | |
--tanrei-comp-fab-menu-tonal-disabled-state-layer-color | — | |
--tanrei-comp-fab-menu-tonal-focused-container-color | — | |
--tanrei-comp-fab-menu-tonal-focused-container-elevation | — | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-tonal-focused-icon-color | — | |
--tanrei-comp-fab-menu-tonal-focused-item-container-color | — | |
--tanrei-comp-fab-menu-tonal-focused-item-container-elevation | — | 0px 1px 2px 0px rgb(0 0 0 / 0.3), 0px 2px 6px 2px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-tonal-focused-item-icon-color | — | |
--tanrei-comp-fab-menu-tonal-focused-item-label-color | — | |
--tanrei-comp-fab-menu-tonal-focused-item-state-layer-color | — | |
--tanrei-comp-fab-menu-tonal-focused-open-container-color | — | |
--tanrei-comp-fab-menu-tonal-focused-open-container-elevation | — | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-tonal-focused-open-icon-color | — | |
--tanrei-comp-fab-menu-tonal-focused-open-state-layer-color | — | |
--tanrei-comp-fab-menu-tonal-focused-state-layer-color | — | |
--tanrei-comp-fab-menu-tonal-hovered-container-color | — | |
--tanrei-comp-fab-menu-tonal-hovered-container-elevation | — | 0px 2px 3px 0px rgb(0 0 0 / 0.3), 0px 6px 10px 4px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-tonal-hovered-icon-color | — | |
--tanrei-comp-fab-menu-tonal-hovered-item-container-color | — | |
--tanrei-comp-fab-menu-tonal-hovered-item-container-elevation | — | 0px 1px 2px 0px rgb(0 0 0 / 0.3), 0px 2px 6px 2px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-tonal-hovered-item-icon-color | — | |
--tanrei-comp-fab-menu-tonal-hovered-item-label-color | — | |
--tanrei-comp-fab-menu-tonal-hovered-item-state-layer-color | — | |
--tanrei-comp-fab-menu-tonal-hovered-open-container-color | — | |
--tanrei-comp-fab-menu-tonal-hovered-open-container-elevation | — | 0px 2px 3px 0px rgb(0 0 0 / 0.3), 0px 6px 10px 4px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-tonal-hovered-open-icon-color | — | |
--tanrei-comp-fab-menu-tonal-hovered-open-state-layer-color | — | |
--tanrei-comp-fab-menu-tonal-hovered-state-layer-color | — | |
--tanrei-comp-fab-menu-tonal-icon-color | sys.color.on-secondary-container | |
--tanrei-comp-fab-menu-tonal-item-container-color | sys.color.secondary-container | |
--tanrei-comp-fab-menu-tonal-item-container-elevation | sys.elevation.level2 | 0px 1px 2px 0px rgb(0 0 0 / 0.3), 0px 2px 6px 2px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-tonal-item-icon-color | sys.color.on-secondary-container | |
--tanrei-comp-fab-menu-tonal-item-label-color | sys.color.on-secondary-container | |
--tanrei-comp-fab-menu-tonal-item-state-layer-color | comp.fab-menu.tonal.item.label.color | |
--tanrei-comp-fab-menu-tonal-open-container-color | sys.color.primary | |
--tanrei-comp-fab-menu-tonal-open-container-elevation | sys.elevation.level3 | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-tonal-open-icon-color | sys.color.on-primary | |
--tanrei-comp-fab-menu-tonal-open-state-layer-color | comp.fab-menu.tonal.open.icon.color | |
--tanrei-comp-fab-menu-tonal-pressed-container-color | — | |
--tanrei-comp-fab-menu-tonal-pressed-container-elevation | — | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-tonal-pressed-icon-color | — | |
--tanrei-comp-fab-menu-tonal-pressed-item-container-color | — | |
--tanrei-comp-fab-menu-tonal-pressed-item-container-elevation | — | 0px 1px 2px 0px rgb(0 0 0 / 0.3), 0px 2px 6px 2px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-tonal-pressed-item-icon-color | — | |
--tanrei-comp-fab-menu-tonal-pressed-item-label-color | — | |
--tanrei-comp-fab-menu-tonal-pressed-item-state-layer-color | — | |
--tanrei-comp-fab-menu-tonal-pressed-open-container-color | — | |
--tanrei-comp-fab-menu-tonal-pressed-open-container-elevation | — | 0px 1px 3px 0px rgb(0 0 0 / 0.3), 0px 4px 8px 3px rgb(0 0 0 / 0.15) |
--tanrei-comp-fab-menu-tonal-pressed-open-icon-color | — | |
--tanrei-comp-fab-menu-tonal-pressed-open-state-layer-color | — | |
--tanrei-comp-fab-menu-tonal-pressed-state-layer-color | — | |
--tanrei-comp-fab-menu-tonal-state-layer-color | comp.fab-menu.tonal.icon.color | |
--tanrei-comp-fab-menu-item-label-typescale | sys.typescale.label | 500 16px/1.5 |
测试样例
作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。
variant: filled (5)
filled-closed
variant: filled label: Create items: 3 slots: icon 以 m3-like 显示small-closed
variant: filled size: small label: Create items: 3 slots: icon 以 m3-like 显示large-closed
variant: filled size: large label: Create items: 3 slots: icon 以 m3-like 显示filled-open
variant: filled open: true label: Create items: 3 slots: icon 单独绘制在 320 × 320 的窗口中以 m3-like 显示filled-disabled
variant: filled label: Create disabled: true items: 3 slots: icon 以 m3-like 显示variant: tonal (2)
tonal-closed
variant: tonal label: Create items: 3 slots: icon 以 m3-like 显示tonal-open
variant: tonal open: true label: Create items: 3 slots: icon 单独绘制在 320 × 320 的窗口中以 m3-like 显示variant: surface (2)
surface-closed
variant: surface label: Create items: 3 slots: icon 以 m3-like 显示surface-open
variant: surface open: true label: Create items: 3 slots: icon 单独绘制在 320 × 320 的窗口中以 m3-like 显示生成自 specs/fab-menu.spec.json, tokens/systems/m3-like/comp/fab-menu.tokens.json, design/tokens/m3-like/
当屏幕的主操作有几种相关的形式时,使用 FAB 菜单:新建文档、文件夹或上传;照片或笔记。切换按钮让屏幕上唯一的悬浮操作始终触手可及,按下它,这些形式就作为带标签的操作显示在它的正上方。
只有一个操作时,使用主操作按钮。选项很多或需要说明文字时,使用从按钮或面板打开的菜单。当操作属于某一项而不是整个屏幕时,把按钮放在那一项旁边。
| 变体 | 强调程度 | 用于 |
|---|---|---|
| Filled(默认) | 最高 | 以最强的力度呈现屏幕的主操作。 |
| Tonal | 高 | 应当由内容唱主角的屏幕。 |
| Surface | 中性 | 位于本身已有颜色的内容(例如地图或照片)之上的 FAB 菜单。 |
变体同时决定切换按钮和菜单项的颜色。打开时,切换按钮采用该变体的另一种外观,变成圆形,并显示关闭图标,所以打开时看起来绝不会和关闭时一样。尺寸只决定切换按钮(40、48 或 64):菜单项只有一种尺寸。
FAB 菜单和菜单一样,是一个供操作的非模态浮层。用户要在其中操作,所以它接收焦点并把焦点交还,但屏幕上的其他部分都不会被阻挡:没有遮罩,焦点也不会被限制在其中。
- 打开。 切换按钮通过
toggle请求打开。菜单项出现在它的上方,与它的末尾侧对齐,从离它最近的一项开始依次出现,焦点移到顶部的第一项上。用向上方向键打开时,焦点则移到最后一项,也就是紧挨切换按钮的那一项。 - 关闭。 Escape、平台的返回操作、在菜单项和切换按钮之外按下、Tab,或者焦点离开,都会通过
close连同原因请求关闭。激活某一项时,先触发该项的 click,然后是原因为item的close。打开时在切换按钮上按下是切换,而不是在外部按下。 - 交还焦点。 菜单项关闭时,如果焦点原本在其中,焦点会交还给切换按钮。在某一项上按 Tab 会关闭菜单项,并从切换按钮继续向后移动,就像从切换按钮上按 Tab 一样。
- 由谁关闭。 在 Web、Compose 和 React Native 上,你根据
toggle设置open(expanded、visible),并根据close把它设回 false。在 SwiftUI 和 Flutter 上,FAB 菜单通过它的绑定或控制器自己打开和关闭。
- 给切换按钮一个说出这些操作共同点的标签,例如“新建”。打开时它保留这个名称。它的展开状态会告诉屏幕阅读器,它现在的作用是关闭这个列表。
- 给每一项一个简短的标签和一个图标。用户读的是标签;图标只是帮他们找到它。
- 保持在两到六项。按用户浏览的顺序排列:第一项画在顶部,最后一项紧挨在切换按钮的上方。
- 在可滚动内容的末尾留出空间,让切换按钮不会一直遮住最后一项。
- 不要禁用某一项。不可用的操作不要放进列表。没有“禁用的项”。
- 不要在打开时把切换按钮改名为“关闭”,也不要自己更换它的图标。这两件事都由组件来做。
- 不要在一个屏幕上放置相同尺寸的 FAB 菜单和主操作按钮。一个屏幕只有一个悬浮操作。
- 不要把 FAB 菜单用于次要的、破坏性的或设置类的操作。
- 角色
button- 标注
- label names the toggle and the list ("Create"). Each item is named by its label. Icons are decorative. The toggle keeps its name while open; its expanded state says it is open.
- 最小目标
- 即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px
键盘操作
| 按键 | 动作 |
|---|---|
| Tab / Shift+Tab | Moves focus to the toggle; from an item, closes the menu and moves focus on from the toggle. |
| Enter, Space / Down Arrow on the toggle | Opens the menu (Enter and Space also close it); focus moves to the first item. |
| Up Arrow on the toggle | Opens the menu; focus moves to the last item, the one nearest the toggle. |
| Down Arrow / Up Arrow | Moves focus to the next or previous item, wrapping at the ends. |
| Home / End | Moves focus to the first or last item. |
| Enter / Space on an item | Activates the item and closes the menu. |
| Escape | Closes the menu and returns focus to the toggle. |
备注
- Focus: the FAB menu is an operated overlay that is not modal, like the menu. It moves focus in when it opens and returns it to the toggle when it closes, but does not keep it: Tab from an item closes it.
- Its place in the focus and reading order is where the layout puts the toggle in the element tree, not where it is drawn, as for the main action button.
- Compose has no menu role: the items are buttons in a traversal group named by label, with their position in their collection info.
- Text size does not change the toggle: an icon is not text. The items grow with enlarged text and wrap their labels.
- 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/fab-menu.spec.json, tokens/sys/measure.tokens.json
- 切换按钮是一个以该标签命名的按钮。在平台有相应属性时,它暴露“它会打开菜单”以及菜单是否已打开,并在打开时保留自己的名称。
- 打开时,这些项构成一个以同一标签命名的菜单,每一项是一个以自己的标签命名的菜单项,带有它的位置和项的总数。图标被隐藏。
- 在切换按钮上按 Enter、Space 或向下方向键打开菜单,并把焦点移到第一项。向上方向键打开菜单,并把焦点移到最后一项。向下和向上方向键在各项之间移动并循环,Home 和 End 到达两端。在某一项上按 Enter 或 Space 激活它并关闭菜单。Escape 关闭菜单并把焦点交还给切换按钮。
- 这个菜单是非模态的:它不把焦点限制在其中,不让屏幕的其余部分变为惰性(inert),也不画遮罩。在某一项上按 Tab 会关闭它。
- 切换按钮的点击区域在每种尺寸下都达到最小目标。每一项的高度至少等于一个目标,它的整个容器都是点击区域。
- 切换按钮不随文本大小变大,因为图标不是文本。菜单项会变大,并让标签换行。
- 在减弱动态效果时,切换按钮的变化以及菜单项的出现和离开都立即完成。
- 调试构建会报告缺少切换按钮的标签、某一项没有标签或图标,以及少于两项或多于六项。
已知的平台差距:
- Web:Web 从不发送
back。Escape 就是它的关闭请求。 - Web:菜单项由脚本相对于切换按钮定位,而不是像规格的 Web 说明所写的那样用 CSS 锚点定位,这一点与菜单相同。
- 在所有平台上,在外部按下都会关闭菜单项。在 Web 和 Flutter 上,这次按下还会到达它下方的东西。在 SwiftUI、Compose 和 React Native 上则不会。
- SwiftUI 没有菜单角色和菜单项角色,也无法说明切换按钮会打开菜单,或菜单已经打开。菜单项是一个以该标签命名的容器里的按钮,各自以自己的位置(“2 of 3”,仅有英文)作为值。
- SwiftUI 在一个弹出层里呈现菜单项,而弹出层自己决定位置。它在菜单项背后画出自己的气泡和箭头,把这一列对准切换按钮的角居中,而不是与切换按钮的末尾侧边缘对齐,并且在默认系统中会裁剪这一列;
list.offset不决定这段距离。这个弹出层将由 Tanrei 的浮层取代。 - SwiftUI:Tab 关闭菜单项并把焦点交还给切换按钮,但焦点无法在同一次按键中从它继续向后移动。
- SwiftUI:菜单项的无障碍框把它画出的阴影也包括在内,所以相邻菜单项的框会重叠几个点。
- SwiftUI:宽大于高的 SF Symbol,例如
folder.badge.plus,会超出菜单项的图标框。 - SwiftUI:切换按钮和菜单项上的按键,以及焦点的移入和交还,是作为规则来测试的,还没有用真实的键盘测试。
- Compose 没有菜单角色,也没有说明“按钮会打开菜单”的属性。菜单项是一个以该标签命名的遍历组里的按钮,位置写在它们的集合信息中;切换按钮的展开和收起动作说明它是要打开还是关闭它们。
- Flutter 不在切换按钮上暴露 has-popup,也不暴露菜单中的位置或项的总数。
- React Native 没有说明“按钮会打开菜单”的属性。切换按钮只暴露菜单是否已打开。
- React Native 在
TanreiProvider的浮层中绘制菜单项,所以它的上层必须有一个TanreiProvider。Tab 关闭菜单项并把焦点交还给切换按钮,下一次 Tab 才从它继续向后移动。 - React Native:菜单项的位置、它们依次进入的动画、焦点的移入和移出,以及在外部按下,都还没有在真机上检查过。
- 为切换按钮和每一项写标签。单独一个图标不是名称。
- 把 FAB 菜单放在元素树中它的操作所属的位置。辅助技术按树中的顺序读到切换按钮,而不是按它画出来的位置。
- 在 Web、Compose 和 React Native 上,根据
toggle设置open,并根据close把它设为 false。FAB 菜单只是发出请求。 - 它关闭时不要自己移动焦点。FAB 菜单会把焦点交还给切换按钮。
- 让切换按钮避开导航栏,并在它的上方为菜单项留出空间。