É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.
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 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
| Prop | Rust | Type | Défaut | Description |
|---|---|---|---|---|
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(..) | boolean | false | Tous 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(..) | string | aucun | Le nom que les lecteurs d’écran donnent à la barre d’outils (« Mise en forme »), pour une page qui en comporte plusieurs. |
children | .child(..) | JSX.Element | aucun | Les é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, style | méthodes de style | string, StyleProp<ViewStyle> | aucun | La mise en page et l’apparence de la barre : alignement, espacements, marges intérieures, sa surface. |
#ToolbarGroup
| Prop | Rust | Type | Défaut | Description |
|---|---|---|---|---|
disabled | .disabled(..) | boolean | false | Tous les éléments du groupe sont désactivés, quel que soit leur propre disabled. |
accessibilityLabel | .accessibility_label(..) | string | aucun | Le nom que les lecteurs d’écran donnent au groupe (« Historique », « Alignement »). |
children | .child(..) | JSX.Element | aucun | Les éléments du groupe, et tout ce que l’app dispose parmi eux. |
class, style | méthodes de style | string, StyleProp<ViewStyle> | aucun | La mise en page et l’apparence du groupe. Il suit l’axe de la barre d’outils. |
#ToolbarSeparator
| Prop | Rust | Type | Défaut | Description |
|---|---|---|---|---|
class, style | méthodes de style | string, StyleProp<ViewStyle> | aucun | Le 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
| Partie | Contenue dans | Stylisée avec | États |
|---|---|---|---|
| La barre | class, style de Toolbar | ||
| Un groupe | la barre | class, style de ToolbarGroup | |
| Un séparateur | la barre, un groupe | class, style de ToolbarSeparator | |
| Un élément | la barre, un groupe | class, style propres à l’élément | les 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é
Toolbara le rôle"toolbar", nommé paraccessibilityLabel; 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.ToolbarGroupa le rôle"group", nommé par sonaccessibilityLabel, de sorte qu’une suite d’éléments se lit comme une seule unité. UnToggleGroupplacé dans une barre d’outils est aussi un groupe.ToolbarSeparatorn’est pas un élément pour les lecteurs d’écran.disabledest 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 :
| Touche | Effet |
|---|---|
| Tab, Shift+Tab | Entre 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, End | Déplace le focus vers le premier ou le dernier élément. |
| Espace, Return | Appuie 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.