Esta página se tradujo automáticamente y puede contener errores. Leer el original en inglés

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.

el-toolbar.tsx
el-toolbar.tsxLive
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")),
        )
}

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

  • Toolbar tiene el rol "toolbar", nombrado por accessibilityLabel; sus elementos siguen siendo elementos independientes. En iOS la barra de herramientas es un grupo semántico en el que entra VoiceOver.
  • ToolbarGroup tiene el rol "group", nombrado por su accessibilityLabel, de modo que una serie de elementos se lee como una sola unidad. Un ToggleGroup dentro de una barra de herramientas también es un grupo.
  • ToolbarSeparator no es un elemento para los lectores de pantalla.
  • disabled se 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.