Cette page a été traduite automatiquement et peut contenir des erreurs. Lire l'original en anglais

Éléments

Toolbar

Une barre d’outils est une barre de contrôles : la mise en forme d’un éditeur de texte, les commandes de lecture d’un lecteur multimédia, une palette verticale d’outils. Toolbar gère le clavier et la sémantique d’une barre d’outils : les lecteurs d’écran l’annoncent comme telle (nommée par son accessibilityLabel), et le clavier l’atteint en un seul arrêt puis circule entre ses éléments avec les flèches de son axis. Ses éléments sont les Button, Toggle et Link qu’elle contient, à n’importe quelle profondeur. ToolbarGroup désigne une suite nommée d’éléments liés (annuler et rétablir, l’alignement), et ToolbarSeparator un trait que l’app dessine entre deux suites. L’apparence revient à l’app : la barre d’outils n’a ni couleurs, ni marges intérieures, ni espacements propres, et chaque contrôle conserve son propre état.

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 et ToolbarSeparator proviennent de nsh/base, la crate nsh-base en Rust. La barre d’outils ne détient aucune valeur : un toggle qu’elle contient reçoit l’information de son état pressé et signale onChange, un bouton signale onPress, et l’app conserve l’état. En Rust, toolbar(), toolbar_group() et toolbar_separator() les construisent, avec un setter par prop (.axis(..), .disabled(..)), les méthodes de style typées et .child(..) pour leur contenu.

#Props

#Toolbar

PropRustTypeDéfautDescription
axis.axis(..)"horizontal" | "vertical""horizontal"La direction dans laquelle courent ses éléments : les flèches qui s’y déplacent (Gauche et Droite, ou Haut et Bas) et le sens dans lequel elle dispose ses enfants (une ligne ou une colonne). Lue à la création de la barre d’outils.
disabled.disabled(..)booleanfalseTous les éléments de la barre d’outils sont désactivés, quel que soit leur propre disabled : aucun appui, aucun arrêt du focus clavier, atténués pour les lecteurs d’écran.
accessibilityLabel.accessibility_label(..)stringaucunLe nom que les lecteurs d’écran donnent à la barre d’outils (« Mise en forme »), pour une page qui en comporte plusieurs.
children.child(..)JSX.ElementaucunLes éléments, leurs groupes et séparateurs, et tout ce que l’app dispose parmi eux (un titre, un espaceur, un champ de saisie).
class, styleméthodes de stylestring, StyleProp<ViewStyle>aucunLa mise en page et l’apparence de la barre : alignement, espacements, marges intérieures, sa surface.

#ToolbarGroup

PropRustTypeDéfautDescription
disabled.disabled(..)booleanfalseTous les éléments du groupe sont désactivés, quel que soit leur propre disabled.
accessibilityLabel.accessibility_label(..)stringaucunLe nom que les lecteurs d’écran donnent au groupe (« Historique », « Alignement »).
children.child(..)JSX.ElementaucunLes éléments du groupe, et tout ce que l’app dispose parmi eux.
class, styleméthodes de stylestring, StyleProp<ViewStyle>aucunLa mise en page et l’apparence du groupe. Il suit l’axe de la barre d’outils.

#ToolbarSeparator

PropRustTypeDéfautDescription
class, styleméthodes de stylestring, StyleProp<ViewStyle>aucunLe trait : un filet fin en travers d’une ligne (h-5 w-px bg-border) ou d’une colonne (h-px self-stretch bg-border).

#Éléments

Les éléments d’une barre d’outils sont les Button, Toggle et Link qu’elle contient, dans ses groupes et aussi dans un ToggleGroup placé à l’intérieur. Le déclencheur d’un Menu ou d’un Popover est aussi un élément : la vue placée dans son slot trigger, à laquelle le popup donne ses appuis, son focus et son état expanded. Un outil que l’app dessine elle-même est un Button avec les classes de l’app : un Button n’a pas d’apparence propre, il se présente donc comme l’app le stylise et constitue un élément (un View avec onPress reste un arrêt à part entière, hors des flèches). Un ToggleGroup placé dans une barre d’outils est l’un de ses groupes : ses toggles sont les éléments de la barre, les lecteurs d’écran annoncent un groupe, et son disabled continue de désactiver ses propres toggles. Les autres contrôles (un Input, un Select, un Slider) restent des arrêts à part entière et conservent leurs flèches ; placez-les à la fin de la barre. Ce qu’affiche un popup ouvert par un élément (champs et boutons d’un popover, éléments d’un menu, liens d’une hover card) ne fait pas partie de la barre d’outils. Les flèches parcourent les éléments dans l’ordre où la barre les dispose, quelle que soit leur profondeur, et un élément que l’app affiche plus tard prend sa place parmi eux.

#Parties

PartieContenue dansStylisée avecÉtats
La barreclass, style de Toolbar
Un groupela barreclass, style de ToolbarGroup
Un séparateurla barre, un groupeclass, style de ToolbarSeparator
Un élémentla barre, un groupeclass, style propres à l’élémentles siens : aria-pressed:, disabled:, hover:, focus:, active:

Montrez l’élément sur lequel se trouve le clavier avec des styles focus:, et un toggle pressé par sa propre surface (voir Guides de conception).

#Vertical

Une palette d’outils est une colonne : axis="vertical" dispose les éléments de haut en bas, et Haut et Bas permettent de les parcourir.

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

#Désactivé

disabled sur une barre d’outils désactive tous ses éléments, une barre que l’app verrouille ; sur un groupe, les éléments du groupe. Un élément désactivé ne reçoit aucun appui, les flèches le sautent, et les lecteurs d’écran l’annoncent comme atténué :

<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 a le rôle "toolbar", nommé par accessibilityLabel ; ses éléments restent des éléments à part entière. Sur iOS, la barre d’outils est un groupe sémantique dans lequel VoiceOver entre.
  • ToolbarGroup a le rôle "group", nommé par son accessibilityLabel, de sorte qu’une suite d’éléments se lit comme une seule unité. Un ToggleGroup placé dans une barre d’outils est aussi un groupe.
  • ToolbarSeparator n’est pas un élément pour les lecteurs d’écran.
  • disabled est annoncé comme la plateforme l’annonce (VoiceOver, TalkBack, UI Automation, l’arbre d’accessibilité du navigateur).

Voir Accessibilité pour les libellés, les rôles et les annonces en général.

#Clavier

Sur macOS et le web, où Tab atteint les vues qui reçoivent des appuis, un Toolbar suit le modèle de barre d’outils WAI-ARIA :

ToucheEffet
Tab, Shift+TabEntre dans la barre d’outils à l’élément que le focus a quitté en dernier (au début, le premier élément non désactivé), et en sort en une seule étape.
Droite, Gauche (axis="horizontal")Déplace le focus vers l’élément suivant ou précédent, du dernier au premier et inversement.
Bas, Haut (axis="vertical")Déplace le focus vers l’élément suivant ou précédent, du dernier au premier et inversement.
Home, EndDéplace le focus vers le premier ou le dernier élément.
Espace, ReturnAppuie sur l’élément ayant le focus.

Les flèches sautent les éléments désactivés et n’appuient sur rien : un toggle ne change que lorsqu’il est pressé. L’autre paire de flèches est laissée aux éléments (le déclencheur d’un menu ouvre son menu avec Bas). Dans une mise en page de droite à gauche, une barre d’outils horizontale part de la droite : Gauche passe donc à l’élément suivant et Droite au précédent ; une barre verticale conserve Bas et Haut.

#Notes par plateforme

  • iOS, Android et Mac Catalyst : chaque élément s’active d’un toucher ; la barre d’outils est un groupe que les lecteurs d’écran annoncent.
  • Postes de bureau (macOS, Windows, Linux, GPUI) et web : un appui est un clic. Sur macOS et le web, la barre d’outils est un seul arrêt de Tab, et les flèches, Home et End circulent entre ses éléments (Clavier).
  • Chaque plateforme dessine ce que l’app stylise : la barre, ses groupes, ses séparateurs et ses éléments.