Elementos
Toolbar
Uma toolbar é uma barra de controles: a formatação de um editor de texto, os controles de transporte de um player de mídia, uma paleta vertical de ferramentas.
Toolbar cuida do teclado e da semântica de uma delas: leitores de tela anunciam uma toolbar (nomeada por seu
accessibilityLabel), e o teclado chega até ela em uma única parada e se move entre seus itens com as setas de seu
axis. Seus itens são os Buttons, Toggles e Links dentro dela, em qualquer profundidade. Um ToolbarGroup é uma sequência nomeada de
itens relacionados (desfazer e refazer, o alinhamento), e um ToolbarSeparator é uma linha que o app desenha entre sequências. A aparência é
do app: a toolbar não tem cores, padding ou espaçamentos próprios, e cada controle mantém seu próprio estado.
import { mount, signal, Text, View } from "nsh";
import { Button, Toggle, ToggleGroup, Toolbar, ToolbarGroup, ToolbarSeparator } from "nsh/base";
import { desktop, focusRing, hoverFill, Log, Split, ui } from "./palette";
type Mark = "bold" | "italic" | "underline";
type Align = "left" | "center" | "right";
type Tool = "select" | "pen" | "shape" | "text";
const marks: { id: Mark; label: string; glyph: string }[] = [
{ id: "bold", label: "Bold", glyph: "B" },
{ id: "italic", label: "Italic", glyph: "I" },
{ id: "underline", label: "Underline", glyph: "U" },
];
const aligns: { id: Align; label: string; glyph: string }[] = [
{ id: "left", label: "Align left", glyph: "⇤" },
{ id: "center", label: "Align center", glyph: "↔" },
{ id: "right", label: "Align right", glyph: "⇥" },
];
const tools: { id: Tool; label: string; glyph: string }[] = [
{ id: "select", label: "Select", glyph: "↖" },
{ id: "pen", label: "Pen", glyph: "✎" },
{ id: "shape", label: "Rectangle", glyph: "▢" },
{ id: "text", label: "Text", glyph: "T" },
];
const side = desktop ? 28 : 34;
function App() {
const on = signal<Mark[]>(["bold"]);
const align = signal<Align>("left");
const tool = signal<Tool>("select");
const history = signal<string[]>([]);
const events = signal<string[]>([]);
const log = (line: string) => events.set([line, ...events()].slice(0, 3));
const edit = (what: string) => {
history.set([...history(), what]);
log(what);
};
const has = (m: Mark) => on().includes(m);
// A tool: bare while off, raised while pressed, a hover fill, the focus ring.
const look = (pressed: boolean) => ({
style: { minWidth: side, height: side, paddingHorizontal: 6, borderRadius: 7, alignItems: "center", justifyContent: "center" } as const,
class: "transition-colors active:opacity-70 disabled:opacity-35 motion-reduce:transition-none",
pressedStyle: ui().raised,
hoverStyle: pressed ? undefined : hoverFill(),
focusStyle: focusRing(),
});
const glyph = (text: string, active: boolean) => <Text style={{ fontSize: 14, fontWeight: "600", color: active ? ui().text : ui().muted }}>{text}</Text>;
const rule = (vertical = false) => (
<ToolbarSeparator style={vertical ? { height: 1, alignSelf: "stretch", marginVertical: 4, backgroundColor: ui().line } : { width: 1, height: 16, marginHorizontal: 4, backgroundColor: ui().line }} />
);
const bar = { alignItems: "center", gap: 2, padding: 3, borderRadius: 10, ...ui().inset } as const;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>toolbar</Text>
<Text style={{ color: ui().muted }}>
{desktop ? "Tab into a toolbar, then move with the arrow keys; Home and End jump to its ends" : "Tap the tools; a screen reader hears each bar as a toolbar"}
</Text>
<Split gap={14} inspector={<Log title={`${history().length} edits · ${on().join(", ") || "no marks"}, ${align()} · ${tool()}`} lines={events()} minHeight={76} />}>
{/* A formatting bar: history, a toggle group of marks (a group of the toolbar), the alignment. */}
<Toolbar accessibilityLabel="Formatting" style={{ ...bar, alignSelf: "flex-start" }}>
<ToolbarGroup accessibilityLabel="History">
<Button accessibilityLabel="Undo" disabled={history().length === 0} onPress={() => {
history.set(history().slice(0, -1));
log("Undo");
}} {...look(false)}>
{glyph("↶", true)}
</Button>
</ToolbarGroup>
{rule()}
<ToggleGroup style={{ flexDirection: "row", gap: 2 }}>
<Each of={marks}>
{(m) => (
<Toggle
pressed={has(m().id)}
accessibilityLabel={m().label}
onChange={({ pressed }) => {
on.set(pressed ? [...on(), m().id] : on().filter((x) => x !== m().id));
edit(`${m().label}: ${pressed}`);
}}
{...look(has(m().id))}
>
{glyph(m().glyph, has(m().id))}
</Toggle>
)}
</Each>
</ToggleGroup>
{rule()}
<ToolbarGroup accessibilityLabel="Alignment" style={{ gap: 2 }}>
<Each of={aligns}>
{(a) => (
<Toggle
pressed={align() === a().id}
accessibilityLabel={a().label}
onChange={({ pressed }) => {
// Exactly one: the pressed alignment stays pressed.
if (pressed) align.set(a().id);
edit(`${a().label}`);
}}
{...look(align() === a().id)}
>
{glyph(a().glyph, align() === a().id)}
</Toggle>
)}
</Each>
</ToolbarGroup>
</Toolbar>
<View style={{ flexDirection: "row", gap: 14, alignItems: "stretch" }}>
{/* A vertical palette: Up and Down move along it. */}
<Toolbar axis="vertical" accessibilityLabel="Tools" style={{ ...bar, alignSelf: "flex-start" }}>
<Each of={tools}>
{(t) => (
<Toggle
pressed={tool() === t().id}
accessibilityLabel={t().label}
onChange={({ pressed }) => {
if (!pressed) return;
tool.set(t().id);
log(`Tool: ${t().label}`);
}}
{...look(tool() === t().id)}
>
{glyph(t().glyph, tool() === t().id)}
</Toggle>
)}
</Each>
{rule(true)}
<Button accessibilityLabel="Clear canvas" onPress={() => log("Clear canvas")} {...look(false)}>
{glyph("⌫", false)}
</Button>
</Toolbar>
<View style={{ flex: 1, ...ui().card, borderRadius: 14, padding: 14, gap: 6 }}>
<Text
style={{
fontSize: 15,
lineHeight: 22,
color: ui().text,
fontWeight: has("bold") ? "700" : "400",
fontStyle: has("italic") ? "italic" : "normal",
textDecorationLine: has("underline") ? "underline" : "none",
textAlign: align(),
}}
>
One stop of Tab per toolbar; its arrows move between the tools.
</Text>
<Text style={{ fontSize: 13, color: ui().muted }}>{`Tool: ${tools.find((t) => t.id === tool())!.label}`}</Text>
</View>
</View>
</Split>
</View>
);
}
mount(App);use nsh_base::{button, theme, toggle, toolbar, toolbar_group, toolbar_separator};
use nsh_core::prelude::*;
fn formatting() -> impl IntoElement {
let bold = signal(false);
toolbar()
.accessibility_label("Formatting")
.items_center()
.gap_1()
.child(
toolbar_group()
.accessibility_label("History")
.child(button().accessibility_label("Undo").size_8().child(text("↶")))
.child(button().accessibility_label("Redo").size_8().child(text("↷"))),
)
.child(toolbar_separator().w(px(1.0)).h_5().bg(move || theme().border))
.child(
toggle()
.pressed(bold)
.accessibility_label("Bold")
.on_change(move |pressed: &bool| bold.set(*pressed))
.size_8()
.items_center()
.justify_center()
.pressed_style(|s| s.bg(move || theme().accent))
.child(text("B")),
)
}import { mount, signal, Text } from "nsh";
import { Button, Toggle, Toolbar, ToolbarGroup, ToolbarSeparator } from "nsh/base";
function App() {
const bold = signal(false);
return (
<Toolbar accessibilityLabel="Formatting" class="items-center gap-1 self-start rounded-lg bg-muted p-1">
<ToolbarGroup accessibilityLabel="History">
<Button accessibilityLabel="Undo" class="size-8 items-center justify-center rounded-md hover:bg-accent">
<Text>↶</Text>
</Button>
<Button accessibilityLabel="Redo" class="size-8 items-center justify-center rounded-md hover:bg-accent">
<Text>↷</Text>
</Button>
</ToolbarGroup>
<ToolbarSeparator class="h-5 w-px bg-border" />
<Toggle
pressed={bold()}
accessibilityLabel="Bold"
onChange={({ pressed }) => bold.set(pressed)}
class="size-8 items-center justify-center rounded-md aria-pressed:bg-accent"
>
<Text>B</Text>
</Toggle>
</Toolbar>
);
}
mount(App);Toolbar, ToolbarGroup e ToolbarSeparator vêm de nsh/base, o crate nsh-base
em Rust. A toolbar não guarda valor: um toggle dentro dela é informado se está pressionado e reporta onChange, um
botão reporta onPress, e o app mantém o estado. Em Rust, toolbar(), toolbar_group() e
toolbar_separator() os criam, com um setter por prop (.axis(..), .disabled(..)), os métodos de estilo tipados e
.child(..) para o conteúdo deles.
#Props
#Toolbar
| Prop | Rust | Tipo | Padrão | Descrição |
|---|---|---|---|---|
axis |
.axis(..) |
"horizontal" | "vertical" |
"horizontal" |
A direção em que seus itens correm: as setas que se movem ao longo dela (Esquerda e Direita, ou Cima e Baixo) e a direção em que ela dispõe seus filhos (uma linha ou uma coluna). Lido quando a toolbar é criada. |
disabled |
.disabled(..) |
boolean |
false |
Todos os itens da toolbar ficam desabilitados, seja qual for o disabled de cada um: sem pressionamentos, sem parada do foco do teclado, esmaecidos para leitores de tela. |
accessibilityLabel |
.accessibility_label(..) |
string |
nenhum | Como os leitores de tela chamam a toolbar (“Formatação”), para uma página com mais de uma. |
children |
.child(..) |
JSX.Element |
nenhum | Os itens, seus grupos e separadores, e o que mais o app dispuser entre eles (um título, um espaçador, um input). |
class, style |
métodos de estilo | string, StyleProp<ViewStyle> |
nenhum | O layout e a aparência da barra: alinhamento, espaçamentos, padding, sua superfície. |
#ToolbarGroup
| Prop | Rust | Tipo | Padrão | Descrição |
|---|---|---|---|---|
disabled |
.disabled(..) |
boolean |
false |
Todos os itens do grupo ficam desabilitados, seja qual for o disabled de cada um. |
accessibilityLabel |
.accessibility_label(..) |
string |
nenhum | Como os leitores de tela chamam o grupo (“Histórico”, “Alinhamento”). |
children |
.child(..) |
JSX.Element |
nenhum | Os itens do grupo, e o que mais o app dispuser entre eles. |
class, style |
métodos de estilo | string, StyleProp<ViewStyle> |
nenhum | O layout e a aparência do grupo. Ele corre ao longo do eixo da toolbar. |
#ToolbarSeparator
| Prop | Rust | Tipo | Padrão | Descrição |
|---|---|---|---|---|
class, style |
métodos de estilo | string, StyleProp<ViewStyle> |
nenhum | A linha: um traço fino atravessando uma linha (h-5 w-px bg-border) ou uma coluna (h-px self-stretch bg-border). |
#Itens
Os itens de uma toolbar são os Buttons, Toggles e Links dentro dela, em seus grupos e também dentro de um
ToggleGroup nela. O gatilho de um Menu ou de um Popover também é um
item: a view em seu slot trigger, à qual o popup dá seus pressionamentos, seu foco e seu estado expanded. Uma
ferramenta que o app desenha por conta própria é um Button com as classes do app: um Button não tem aparência própria, então fica como o app
o estiliza e é um item (um View com onPress continua sendo uma parada própria, fora das setas). Um
ToggleGroup dentro de uma toolbar é um de seus grupos: seus toggles são os itens da toolbar, leitores de tela anunciam um grupo,
e seu disabled ainda desabilita seus próprios toggles. Outros controles (um Input, um Select, um Slider) continuam sendo paradas
próprias e mantêm suas setas; coloque-os no fim da barra. O que um popup aberto por um item mostra (os
campos e botões de um popover, os itens de um menu, os links de um hover card) não faz parte da toolbar. As setas percorrem os itens na ordem em que a
barra os dispõe, por mais fundo que cada um esteja, e um item que o app mostra depois toma seu lugar entre eles.
#Partes
| Parte | Dentro de | Estilizada com | Estados |
|---|---|---|---|
| A barra | class, style de Toolbar |
||
| Um grupo | a barra | class, style de ToolbarGroup |
|
| Um separador | a barra, um grupo | class, style de ToolbarSeparator |
|
| Um item | a barra, um grupo | class, style do próprio item |
os próprios: aria-pressed:, disabled:, hover:, focus:, active: |
Mostre o item em que o teclado está com estilos focus:, e um toggle pressionado por sua própria superfície (veja
Guias de design).
#Vertical
Uma paleta de ferramentas é uma coluna: axis="vertical" dispõe os itens de cima para baixo, e as teclas Cima e Baixo se movem ao longo deles.
<Toolbar axis="vertical" accessibilityLabel="Tools" class="gap-1 self-start rounded-lg bg-muted p-1">
<Each of={["select", "pen", "shape", "text"]}>
{(t) => (
<Toggle
pressed={tool() === t()}
accessibilityLabel={t()}
onChange={({ pressed }) => pressed && tool.set(t())}
class="size-8 items-center justify-center rounded-md aria-pressed:bg-accent"
>
<Text>{t()[0].toUpperCase()}</Text>
</Toggle>
)}
</Each>
</Toolbar>
#Desabilitado
disabled em uma toolbar desabilita todos os itens nela, uma barra que o app trava; em um grupo, os itens do grupo. Um item desabilitado
não recebe pressionamentos, as setas o pulam, e leitores de tela o anunciam como esmaecido:
<Toolbar disabled={readonly()} accessibilityLabel="Review" class="flex-row gap-1">
<Button onPress={approve} class="h-8 px-3 disabled:opacity-50">
<Text>Approve</Text>
</Button>
</Toolbar>
#Acessibilidade
Toolbartem o papel"toolbar", nomeado poraccessibilityLabel; seus itens continuam sendo elementos próprios. No iOS, a toolbar é um grupo semântico no qual o VoiceOver entra.ToolbarGrouptem o papel"group", nomeado por seuaccessibilityLabel, de modo que uma sequência de itens é lida como uma unidade. UmToggleGroupdentro de uma toolbar também é um grupo.ToolbarSeparatornão é um elemento para leitores de tela.disabledé anunciado como a plataforma o anuncia (VoiceOver, TalkBack, UI Automation, a árvore de acessibilidade do navegador).
Veja Acessibilidade para rótulos, papéis e anúncios em geral.
#Teclado
No macOS e na web, onde Tab alcança as views que recebem pressionamentos, uma Toolbar segue o
padrão de toolbar do WAI-ARIA:
| Tecla | O que faz |
|---|---|
| Tab, Shift+Tab | Entra na toolbar no item em que o foco esteve por último (no início, o primeiro item que não está desabilitado), e sai dela em um único passo. |
Seta para a direita, Seta para a esquerda (axis="horizontal") |
Move o foco para o item seguinte ou o anterior, do último ao primeiro e vice-versa. |
Seta para baixo, Seta para cima (axis="vertical") |
Move o foco para o item seguinte ou o anterior, do último ao primeiro e vice-versa. |
| Home, End | Move o foco para o primeiro ou o último item. |
| Espaço, Return | Pressiona o item com foco. |
As setas pulam itens desabilitados e não pressionam nada: um toggle só muda quando é pressionado. O outro par de setas fica para os itens (o gatilho de um menu abre seu menu com Seta para baixo). Em um layout da direita para a esquerda, uma toolbar horizontal corre a partir da direita, então Seta para a esquerda move para o próximo item e Seta para a direita para o anterior; uma vertical mantém Seta para baixo e Seta para cima.
#Notas de plataforma
- iOS, Android e Mac Catalyst: cada item é pressionado com um toque; a toolbar é um grupo que leitores de tela anunciam.
- Desktops (macOS, Windows, Linux, GPUI) e a web: um pressionamento é um clique. No macOS e na web a toolbar é uma única parada do Tab, e as setas, Home e End se movem entre seus itens (Teclado).
- Todas as plataformas desenham o que o app estiliza: a barra, seus grupos, seus separadores e seus itens.