Elementos
Toolbar
Una barra de herramientas es una barra de controles: el formato de un editor de texto, el transporte de un reproductor multimedia, una paleta vertical de herramientas.
Toolbar se encarga del teclado y de la semántica de una: los lectores de pantalla anuncian una barra de herramientas (nombrada por su
accessibilityLabel), y el teclado llega a ella en una sola parada y se mueve entre sus elementos con las teclas de flecha de su
axis. Sus elementos son los Button, Toggle y Link que contiene, a cualquier profundidad. Un ToolbarGroup es una serie con nombre de
elementos relacionados (deshacer y rehacer, la alineación), y un ToolbarSeparator es una línea que la app dibuja entre series. El aspecto
es de la app: la barra de herramientas no tiene colores, relleno ni espacios propios, y cada control conserva su propio 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 y ToolbarSeparator provienen de nsh/base, el crate nsh-base
en Rust. La barra de herramientas no guarda ningún valor: a un toggle dentro de ella se le indica si está pulsado y reporta onChange, un
botón reporta onPress, y la app conserva el estado. En Rust, toolbar(), toolbar_group() y
toolbar_separator() los crean, con un setter por prop (.axis(..), .disabled(..)), los métodos de estilo tipados y
.child(..) para su contenido.
#Props
#Toolbar
| Prop | Rust | Tipo | Valor por defecto | Descripción |
|---|---|---|---|---|
axis |
.axis(..) |
"horizontal" | "vertical" |
"horizontal" |
La dirección en que se disponen sus elementos: las teclas de flecha que se mueven a lo largo de ella (Izquierda y Derecha, o Arriba y Abajo) y la dirección en que distribuye a sus hijos (una fila o una columna). Se lee cuando se crea la barra de herramientas. |
disabled |
.disabled(..) |
boolean |
false |
Todos los elementos de la barra de herramientas están deshabilitados, sea cual sea su propio disabled: sin pulsaciones, sin parada del foco del teclado y atenuados para los lectores de pantalla. |
accessibilityLabel |
.accessibility_label(..) |
string |
ninguno | Cómo llaman los lectores de pantalla a la barra de herramientas (“Formato”), útil en una página con más de una. |
children |
.child(..) |
JSX.Element |
ninguno | Los elementos, sus grupos y separadores, y cualquier otra cosa que la app distribuya entre ellos (un título, un espaciador, un campo de entrada). |
class, style |
métodos de estilo | string, StyleProp<ViewStyle> |
ninguno | La disposición y el aspecto de la barra: alineación, espacios, relleno, su superficie. |
#ToolbarGroup
| Prop | Rust | Tipo | Valor por defecto | Descripción |
|---|---|---|---|---|
disabled |
.disabled(..) |
boolean |
false |
Todos los elementos del grupo están deshabilitados, sea cual sea su propio disabled. |
accessibilityLabel |
.accessibility_label(..) |
string |
ninguno | Cómo llaman los lectores de pantalla al grupo (“Historial”, “Alineación”). |
children |
.child(..) |
JSX.Element |
ninguno | Los elementos del grupo y cualquier otra cosa que la app distribuya entre ellos. |
class, style |
métodos de estilo | string, StyleProp<ViewStyle> |
ninguno | La disposición y el aspecto del grupo. Se extiende a lo largo del eje de la barra de herramientas. |
#ToolbarSeparator
| Prop | Rust | Tipo | Valor por defecto | Descripción |
|---|---|---|---|---|
class, style |
métodos de estilo | string, StyleProp<ViewStyle> |
ninguno | La línea: un trazo fino a lo ancho de una fila (h-5 w-px bg-border) o de una columna (h-px self-stretch bg-border). |
#Elementos
Los elementos de una barra de herramientas son los Button, Toggle y Link que contiene, en sus grupos y también en un
ToggleGroup dentro de ella. El activador de un Menu o de un Popover es
también un elemento: la vista en su ranura trigger, a la que el popup le da sus pulsaciones, su foco y su estado expanded. Una
herramienta que la app dibuja por sí misma es un Button con las clases de la app: un Button no tiene aspecto propio, por lo que se ve como
la app lo estiliza y es un elemento (un View con onPress sigue siendo una parada independiente, fuera de las teclas de flecha). Un
ToggleGroup dentro de una barra de herramientas es uno de sus grupos: sus toggles son los elementos de la barra, los lectores de pantalla oyen un grupo,
y su disabled sigue deshabilitando sus propios toggles. Otros controles (un Input, un Select, un Slider) siguen siendo paradas
independientes y conservan sus teclas de flecha; colócalos al final de la barra. Lo que muestra un popup que abre un elemento (los
campos y botones de un popover, los elementos de un menú, los enlaces de una hover card) no forma parte de la barra de herramientas. Las teclas de flecha recorren los elementos en el orden en que
la barra los dispone, sin importar a qué profundidad esté cada uno, y un elemento que la app muestre más tarde ocupa su lugar entre ellos.
#Partes
| Parte | Dentro de | Se estiliza con | Estados |
|---|---|---|---|
| La barra | class, style de Toolbar |
||
| Un grupo | la barra | class, style de ToolbarGroup |
|
| Un separador | la barra, un grupo | class, style de ToolbarSeparator |
|
| Un elemento | la barra, un grupo | los class, style propios del elemento |
los suyos: aria-pressed:, disabled:, hover:, focus:, active: |
Muestra el elemento en el que está el teclado con estilos focus:, y un toggle pulsado mediante su propia superficie (consulta
Guías de diseño).
#Vertical
Una paleta de herramientas es una columna: axis="vertical" dispone los elementos de arriba abajo, y Arriba y Abajo se mueven a lo largo de ellos.
<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>
#Deshabilitado
disabled en una barra de herramientas deshabilita todos sus elementos, una barra que la app bloquea; en un grupo, los elementos del grupo. Un elemento deshabilitado
no recibe pulsaciones, las teclas de flecha lo omiten y los lectores de pantalla lo anuncian como atenuado:
<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>
#Accesibilidad
Toolbartiene el rol"toolbar", nombrado poraccessibilityLabel; sus elementos siguen siendo elementos independientes. En iOS la barra de herramientas es un grupo semántico en el que entra VoiceOver.ToolbarGrouptiene el rol"group", nombrado por suaccessibilityLabel, de modo que una serie de elementos se lee como una sola unidad. UnToggleGroupdentro de una barra de herramientas también es un grupo.ToolbarSeparatorno es un elemento para los lectores de pantalla.disabledse anuncia como lo anuncia la plataforma (VoiceOver, TalkBack, UI Automation, el árbol de accesibilidad del navegador).
Consulta Accesibilidad para etiquetas, roles y anuncios en general.
#Teclado
En macOS y en la web, donde Tab llega a las vistas que reciben pulsaciones, un Toolbar sigue el
patrón de barra de herramientas de WAI-ARIA:
| Tecla | Qué hace |
|---|---|
| Tab, Shift+Tab | Entra en la barra de herramientas por el elemento donde el foco estuvo por última vez (al principio, el primer elemento que no está deshabilitado), y sale de ella en un solo paso. |
Derecha, Izquierda (axis="horizontal") |
Mueve el foco al elemento siguiente o al anterior, del último al primero y viceversa. |
Abajo, Arriba (axis="vertical") |
Mueve el foco al elemento siguiente o al anterior, del último al primero y viceversa. |
| Inicio, Fin | Mueve el foco al primer o al último elemento. |
| Espacio, Retorno | Pulsa el elemento enfocado. |
Las teclas de flecha omiten los elementos deshabilitados y no pulsan nada: un toggle solo cambia cuando se pulsa. El otro par de flechas se deja a los elementos (el activador de un menú abre su menú con Abajo). En una disposición de derecha a izquierda, una barra de herramientas horizontal avanza desde la derecha, de modo que Izquierda pasa al elemento siguiente y Derecha al anterior; una vertical mantiene Abajo y Arriba.
#Notas por plataforma
- iOS, Android y Mac Catalyst: cada elemento se pulsa con un toque; la barra de herramientas es un grupo que anuncian los lectores de pantalla.
- Escritorio (macOS, Windows, Linux, GPUI) y la web: una pulsación es un clic. En macOS y en la web la barra de herramientas es una sola parada de Tab, y las teclas de flecha, Inicio y Fin se mueven entre sus elementos (Teclado).
- Todas las plataformas dibujan lo que la app estiliza: la barra, sus grupos, sus separadores y sus elementos.