Questa pagina è stata tradotta automaticamente e può contenere errori. Leggi l'originale in inglese

Elementi

Toolbar

Una toolbar è una barra di controlli: la formattazione di un editor di testo, i comandi di riproduzione di un lettore multimediale, una tavolozza verticale di strumenti. Toolbar gestisce la tastiera e la semantica di una toolbar: gli screen reader la annunciano (con il nome dato dal suo accessibilityLabel), e la tastiera la raggiunge con un’unica tappa e si sposta tra i suoi elementi con i tasti freccia del suo axis. I suoi elementi sono i componenti Button, Toggle e Link al suo interno, a qualsiasi profondità. Un ToolbarGroup è una serie con nome di elementi correlati (annulla e ripeti, l’allineamento), e un ToolbarSeparator è una linea che l’app disegna tra le serie. L’aspetto è dell’app: la toolbar non ha colori, padding o spaziature propri, e ogni controllo mantiene il proprio stato.

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 e ToolbarSeparator provengono da nsh/base, il crate nsh-base in Rust. La toolbar non contiene alcun valore: a un toggle al suo interno viene comunicato se è premuto e notifica onChange, un button notifica onPress, e l’app mantiene lo stato. In Rust, toolbar(), toolbar_group() e toolbar_separator() li creano, con un setter per ogni prop (.axis(..), .disabled(..)), i metodi di stile tipizzati e .child(..) per il loro contenuto.

#Prop

#Toolbar

PropRustTipoPredefinitoDescrizione
axis.axis(..)"horizontal" | "vertical""horizontal"La direzione in cui scorrono i suoi elementi: i tasti freccia che si muovono lungo di essa (Sinistra e Destra, oppure Su e Giù) e la direzione in cui dispone i figli (una riga o una colonna). Letta quando la toolbar viene creata.
disabled.disabled(..)booleanfalseOgni elemento della toolbar è disabilitato, qualunque sia il suo disabled: nessuna pressione, nessuna tappa del focus da tastiera, attenuato per gli screen reader.
accessibilityLabel.accessibility_label(..)stringnessunoIl nome con cui gli screen reader chiamano la toolbar (“Formattazione”), per una pagina che ne contiene più di una.
children.child(..)JSX.ElementnessunoGli elementi, i loro gruppi e separatori, e tutto il resto che l’app dispone tra di essi (un titolo, uno spaziatore, un input).
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoIl layout e l’aspetto della barra: allineamento, spaziature, padding, la sua superficie.

#ToolbarGroup

PropRustTipoPredefinitoDescrizione
disabled.disabled(..)booleanfalseOgni elemento del gruppo è disabilitato, qualunque sia il suo disabled.
accessibilityLabel.accessibility_label(..)stringnessunoIl nome con cui gli screen reader chiamano il gruppo (“Cronologia”, “Allineamento”).
children.child(..)JSX.ElementnessunoGli elementi del gruppo e tutto il resto che l’app dispone tra di essi.
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoIl layout e l’aspetto del gruppo. Si sviluppa lungo l’asse della toolbar.

#ToolbarSeparator

PropRustTipoPredefinitoDescrizione
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoLa linea: un filetto sottile attraverso una riga (h-5 w-px bg-border) o una colonna (h-px self-stretch bg-border).

#Elementi

Gli elementi di una toolbar sono i componenti Button, Toggle e Link al suo interno, nei suoi gruppi e anche in un ToggleGroup al suo interno. Anche il trigger di un Menu o di un Popover è un elemento: la vista nel suo slot trigger, alla quale il popup assegna le pressioni, il focus e lo stato expanded. Uno strumento che l’app disegna da sé è un Button con le classi dell’app: un Button non ha un aspetto proprio, quindi appare come lo stila l’app ed è un elemento (un View con onPress resta una tappa a sé, fuori dai tasti freccia). Un ToggleGroup dentro una toolbar è uno dei suoi gruppi: i suoi toggle sono elementi della toolbar, gli screen reader annunciano un gruppo, e il suo disabled disabilita comunque i propri toggle. Gli altri controlli (un Input, un Select, un Slider) restano tappe a sé e mantengono i propri tasti freccia; posizionali alla fine della barra. Ciò che mostra un popup aperto da un elemento (i campi e i pulsanti di un popover, le voci di un menu, i link di una hover card) non fa parte della toolbar. I tasti freccia attraversano gli elementi nell’ordine in cui la barra li dispone, a qualsiasi profondità si trovino, e un elemento che l’app mostra in seguito prende posto tra gli altri.

#Parti

ParteDentroStilizzata conStati
La barraclass, style di Toolbar
Un gruppola barraclass, style di ToolbarGroup
Un separatorela barra, un gruppoclass, style di ToolbarSeparator
Un elementola barra, un gruppoclass, style propri dell’elementopropri: aria-pressed:, disabled:, hover:, focus:, active:

Mostra l’elemento su cui si trova la tastiera con gli stili focus:, e un toggle premuto attraverso la sua superficie (vedi Guide di design).

#Verticale

Una tavolozza di strumenti è una colonna: axis="vertical" dispone gli elementi dall’alto verso il basso, e Su e Giù si muovono lungo di essi.

<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>

#Disabilitato

disabled su una toolbar disabilita ogni suo elemento, una barra che l’app blocca; su un gruppo, gli elementi del gruppo. Un elemento disabilitato non riceve pressioni, i tasti freccia lo saltano, e gli screen reader lo annunciano come attenuato:

<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>

#Accessibilità

  • Toolbar ha il ruolo "toolbar", con il nome dato da accessibilityLabel; i suoi elementi restano elementi a sé. Su iOS la toolbar è un gruppo semantico in cui entra VoiceOver.
  • ToolbarGroup ha il ruolo "group", con il nome dato dal suo accessibilityLabel, così una serie di elementi viene letta come un’unica unità. Un ToggleGroup dentro una toolbar è anch’esso un gruppo.
  • ToolbarSeparator non è un elemento per gli screen reader.
  • disabled viene annunciato come lo annuncia la piattaforma (VoiceOver, TalkBack, UI Automation, l’albero di accessibilità del browser).

Vedi Accessibilità per etichette, ruoli e annunci in generale.

#Tastiera

Su macOS e sul web, dove Tab raggiunge le viste che ricevono pressioni, un Toolbar segue il pattern toolbar di WAI-ARIA:

TastoCosa fa
Tab, Shift+TabEntra nella toolbar sull’elemento da cui il focus è uscito per ultimo (all’inizio, il primo elemento non disabilitato), e ne esce in un solo passaggio.
Destra, Sinistra (axis="horizontal")Sposta il focus sull’elemento successivo o precedente, dall’ultimo al primo e viceversa.
Giù, Su (axis="vertical")Sposta il focus sull’elemento successivo o precedente, dall’ultimo al primo e viceversa.
Home, EndSposta il focus sul primo o sull’ultimo elemento.
Space, ReturnPreme l’elemento con il focus.

I tasti freccia saltano gli elementi disabilitati e non premono nulla: un toggle cambia solo quando viene premuto. L’altra coppia di frecce è lasciata agli elementi (il trigger di un menu apre il proprio menu con Giù). In un layout da destra a sinistra una toolbar orizzontale procede da destra, quindi Sinistra passa all’elemento successivo e Destra a quello precedente; una toolbar verticale mantiene Giù e Su.

#Note sulle piattaforme

  • iOS, Android e Mac Catalyst: ogni elemento viene premuto con un tocco; la toolbar è un gruppo che gli screen reader annunciano.
  • Desktop (macOS, Windows, Linux, GPUI) e web: una pressione è un clic. Su macOS e sul web la toolbar è un’unica tappa di Tab, e i tasti freccia, Home e End si spostano tra i suoi elementi (Tastiera).
  • Ogni piattaforma disegna ciò che l’app stila: la barra, i suoi gruppi, i suoi separatori e i suoi elementi.