Diese Seite wurde maschinell übersetzt und kann Fehler enthalten. Das englische Original lesen

Elemente

Toggle

Ein Toggle ist eine Schaltfläche, die an oder aus bleibt: Fett in einer Text-Toolbar, ein Favoriten-Stern, eine Ansichtsoption. Toggle übernimmt das Verhalten eines Toggles: Drücken, Aktivierung per Tastatur, den deaktivierten Zustand und die Button-Rolle mit ihrem gedrückten Zustand für Screenreader, die ihn als Umschaltfläche ansagen. Eine ToggleGroup um mehrere Toggles ist eine Toolbar: Screenreader sagen sie als solche an, die Tastatur erreicht sie in einem Halt und wechselt mit den Pfeiltasten zwischen ihren Toggles. Das Aussehen liegt bei der App: Ein Toggle hat keine eigenen Farben, Größen, Icons oder Labels. Seine Kinder sind das Icon oder Label der App, und aria-pressed:-Klassen am Toggle oder group-aria-pressed:-Klassen auf seinem Inhalt folgen seinem Zustand.

el-toggle.tsx
el-toggle.tsxLive
import { Canvas, mount, signal, Text, View } from "nsh";
import { Toggle, ToggleGroup } from "nsh/base";
import { desktop, focusRing, hoverFill, Log, Split, tint, ui } from "./palette";

type Mark = "bold" | "italic" | "underline";
type Align = "left" | "center" | "right";

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 }[] = [
  { id: "left", label: "Align left" },
  { id: "center", label: "Align center" },
  { id: "right", label: "Align right" },
];

/** Three lines of a paragraph, set as `align` sets them. */
function Lines(props: { align: Align; color: string }) {
  return (
    <Canvas
      style={{ pointerEvents: "none", width: 16, height: 16 }}
      draw={(cx) => {
        cx.strokeStyle = props.color;
        cx.lineWidth = 1.6;
        cx.lineCap = "round";
        [12, 7, 10].forEach((w, i) => {
          const x = props.align === "center" ? 8 - w / 2 : props.align === "right" ? 14 - w : 2;
          cx.beginPath();
          cx.moveTo(x, 4 + i * 4);
          cx.lineTo(x + w, 4 + i * 4);
          cx.stroke();
        });
      }}
    />
  );
}

function App() {
  const on = signal<Mark[]>(["bold"]);
  const align = signal<Align>("left");
  const starred = signal(false);
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 3));
  const has = (m: Mark) => on().includes(m);
  // A toolbar toggle: bare while off, raised while pressed (`pressedStyle`).
  const look = (pressed: boolean) => ({
    style: { width: desktop ? 28 : 34, height: desktop ? 28 : 34, borderRadius: 7, alignItems: "center", justifyContent: "center" } as const,
    class: "transition-colors active:opacity-70 motion-reduce:transition-none",
    pressedStyle: ui().raised,
    hoverStyle: pressed ? undefined : hoverFill(),
    focusStyle: focusRing(),
  });
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>toggle</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click a toggle, or Tab to the toolbar and move with the arrow keys" : "Tap a toggle to turn it on or off"}
      </Text>

      <Split gap={14} inspector={<Log title={`onChange · ${on().join(", ") || "none"}, ${align()}`} lines={events()} minHeight={76} />}>
        {/* One toolbar: the marks any number at once, the alignment exactly one. */}
        <ToggleGroup style={{ flexDirection: "row", alignItems: "center", alignSelf: "flex-start", gap: 2, padding: 3, borderRadius: 10, ...ui().inset }}>
          <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));
                  log(`${m().label}: ${pressed}`);
                }}
                {...look(has(m().id))}
              >
                <Text
                  style={{
                    fontSize: 14,
                    color: has(m().id) ? ui().text : ui().muted,
                    fontWeight: m().id === "bold" ? "800" : "500",
                    fontStyle: m().id === "italic" ? "italic" : "normal",
                    textDecorationLine: m().id === "underline" ? "underline" : "none",
                  }}
                >
                  {m().glyph}
                </Text>
              </Toggle>
            )}
          </Each>
          <View style={{ width: 1, height: 16, marginHorizontal: 4, backgroundColor: ui().line }} />
          <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);
                  log(`${a().label}: ${pressed}`);
                }}
                {...look(align() === a().id)}
              >
                <Lines align={a().id} color={align() === a().id ? ui().text : ui().muted} />
              </Toggle>
            )}
          </Each>
        </ToggleGroup>

        {/* The text the toolbar formats: the app reads its own values. */}
        <View style={{ ...ui().card, borderRadius: 14, padding: 14 }}>
          <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(),
            }}
          >
            Every toggle reports the value it asks for; the app passes it back.
          </Text>
        </View>

        {/* A toggle on its own, with a label: `aria-pressed:` tints it. */}
        <Toggle
          pressed={starred()}
          onChange={({ pressed }) => {
            starred.set(pressed);
            log(`Star: ${pressed}`);
          }}
          class="transition-colors active:opacity-70 motion-reduce:transition-none"
          style={{ flexDirection: "row", alignItems: "center", alignSelf: "flex-start", gap: 6, height: desktop ? 28 : 34, paddingHorizontal: 12, borderRadius: 8, borderWidth: 1, borderColor: ui().line }}
          pressedStyle={{ backgroundColor: tint(ui().accent, 0.16), borderColor: tint(ui().accent, 0.4) }}
          hoverStyle={starred() ? undefined : hoverFill()}
          focusStyle={focusRing()}
        >
          <Text style={{ fontSize: 14, color: starred() ? ui().accent : ui().muted }}>{starred() ? "★" : "☆"}</Text>
          <Text style={{ fontSize: 13, fontWeight: "500", color: starred() ? ui().accent : ui().text }}>{starred() ? "Starred" : "Star"}</Text>
        </Toggle>
      </Split>
    </View>
  );
}

mount(App);
use nsh_base::{theme, toggle, toggle_group};
use nsh_core::prelude::*;

fn formatting() -> impl IntoElement {
    let bold = signal(true);
    let italic = signal(false);
    let item = move |label: &'static str, on: Signal<bool>| {
        toggle()
            .pressed(on)
            .accessibility_label(label)
            .on_change(move |pressed: &bool| on.set(*pressed))
            .size_8()
            .items_center()
            .justify_center()
            .rounded(move || theme().radius)
            .pressed_style(|s| s.bg(move || theme().accent))
            .child(text(&label[..1]).text_sm().text_color(move || {
                if on.get() { theme().accent_foreground } else { theme().muted_foreground }
            }))
    };
    toggle_group()
        .flex_row()
        .self_start()
        .gap_1()
        .child(item("Bold", bold))
        .child(item("Italic", italic))
}

Toggle und ToggleGroup stammen aus nsh/base, dem nsh-base-Crate in Rust. Der Wert gehört der App: Jedem Toggle wird mitgeteilt, ob er gedrückt ist, ein Druck meldet den gewünschten Wert mit onChange, und der Toggle zeigt, was die App als pressed zurückgibt. Auch die Gruppe hält keinen Wert, daher entscheidet ebenfalls die App, wie viele Toggles gedrückt sein dürfen: beliebig viele (Fett, Kursiv und Unterstrichen) oder genau einer (eine Ausrichtung, ein View-Umschalter). Dann drückt die App den angeforderten Toggle, lässt die anderen los und hält den gedrückten gedrückt. In Rust erzeugen toggle() und toggle_group() sie, mit einem Setter pro Prop (.pressed(..)), einem Listener pro Ereignis (.on_change(..)), den typisierten Style-Methoden und .child(..) für ihren Inhalt.

#Props

#Toggle

Prop Rust Typ Standard Beschreibung
pressed .pressed(..) boolean false An oder aus: Screenreader sagen, dass er gedrückt ist, und aria-pressed:-Stile greifen. Ein Druck meldet den anderen Wert mit onChange; der Toggle zeigt den Wert, den die App zurückgibt.
disabled .disabled(..) boolean false Keine Drücke und kein Feedback beim Drücken; disabled:-Stile greifen, und Screenreader sagen, dass er abgeblendet ist. Ein Toggle in einer deaktivierten ToggleGroup ist ebenfalls deaktiviert.
pressedStyle .pressed_style(|s| ..) StyleProp<ViewStyle> keine Der Stil des Toggles im gedrückten, nicht deaktivierten Zustand, als aria-pressed:-Klassen.
accessibilityLabel .accessibility_label(..) string keine Was Screenreader ansagen: was der Toggle einschaltet, wenn seine Kinder keinen Text enthalten, der das sagt (ein Icon).
hitSlop .hit_slop(..) number | Partial<Edges> keine Wie weit außerhalb seiner Grenzen Drücke noch zählen, in Punkten: eine Zahl für alle Seiten oder die benannten Seiten. Wie hitSlop einer View.
children .child(..) JSX.Element keine Das Icon oder Label der App, das Screenreader ansagen.
class, style Style-Methoden string, StyleProp<ViewStyle> keine Der Toggle selbst: die Fläche um seinen Inhalt.
activeStyle, hoverStyle, focusStyle, disabledStyle .active(|s| ..), .hover(..), .focus(..), .disabled_style(..) StyleProp<ViewStyle> keine Der Stil des Toggles, während ein Druck läuft, bei Hover, bei Fokus oder wenn er deaktiviert ist, als <state>:-Klassen.

#ToggleGroup

Prop Rust Typ Standard Beschreibung
disabled .disabled(..) boolean false Jeder Toggle der Gruppe ist deaktiviert, unabhängig von seinem eigenen disabled.
children .child(..) JSX.Element keine Die Toggles der Gruppe und alles, was die App dazwischen anordnet (ein Trenner).
class, style Style-Methoden string, StyleProp<ViewStyle> keine Layout und Aussehen der Gruppe: standardmäßig eine Spalte, flex-row für eine Zeile.

#Ereignisse

Ereignis Rust Beschreibung
onChange (Toggle) .on_change(|pressed: &bool| ..) Der Toggle wurde gedrückt: ein Tippen, ein Klick, Leertaste oder Return bei Tastaturfokus oder die Aktivierung durch einen Screenreader. { pressed } ist der gewünschte Wert, das Gegenteil von pressed. Das Ereignis wird ausgelöst, bevor sich etwas ändert: Der Toggle zeigt, was die App als pressed zurückgibt.

Eine Pfeiltaste in einer ToggleGroup bewegt nur den Tastaturfokus: Sie drückt nichts. Ein deaktivierter Toggle meldet nichts.

#Bestandteile

Bestandteil Enthalten in Gestaltet mit Zustände
Die Gruppe class, style von ToggleGroup
Der Toggle der Gruppe oder überall class, style von Toggle aria-pressed:, disabled:, active:, hover:, focus: (oder pressedStyle, disabledStyle, …)
Das Icon oder Label der Toggle eigene Elemente (children) group-aria-pressed:, group-disabled: und die anderen group-*:-Varianten mit group auf dem class des Toggles

aria-pressed: bedeutet, dass der Toggle an ist; active: ist ein laufender Druck, an oder aus. Zeige einen gedrückten Toggle über seine eigene Fläche: eine Füllung, eine erhabene Pille, ein kräftigeres Icon (siehe Design-Leitfäden).

#Segmentiert

Eine Gruppe, deren Toggles verbunden sind und von denen jeweils einer gedrückt ist, ist ein segmentiertes Steuerelement (ein View-Umschalter): Die App hält einen Wert und drückt den Toggle, der ihn benennt. Das verbundene Aussehen sind die Klassen der App:

<ToggleGroup class="flex-row self-start">
  <Each of={["grid", "list"]}>
    {(value) => (
      <Toggle
        pressed={view() === value()}
        accessibilityLabel={`${value()} view`}
        onChange={({ pressed }) => pressed && view.set(value())}
        class="-ml-px h-8 px-3 border border-border first:ml-0 first:rounded-l-(--radius) last:rounded-r-(--radius) aria-pressed:bg-accent"
      >
        <Text class="text-sm">{value()}</Text>
      </Toggle>
    )}
  </Each>
</ToggleGroup>

#Deaktiviert

Ein deaktivierter Toggle nimmt keine Drücke an, und Screenreader sagen ihn als abgeblendet an. Seine disabled:-Klassen haben Vorrang vor seinen aria-pressed:-Klassen, und group-disabled:-Klassen darin folgen ihm. disabled an einer ToggleGroup deaktiviert jeden Toggle darin, eine von der App gesperrte Toolbar:

<ToggleGroup disabled={readonly()} class="flex-row gap-1">
  <Toggle pressed={bold()} onChange={({ pressed }) => bold.set(pressed)} class="size-8 disabled:opacity-50">
    <Text>B</Text>
  </Toggle>
</ToggleGroup>

#Ein kleines Ziel mit hitSlop

Ein Icon-Toggle kann kleiner sein als ein Finger. hitSlop lässt Drücke in seiner Nähe zählen, in Punkten über jede Seite hinaus, ohne sein Layout oder sein Aussehen zu ändern: eine Zahl für alle Seiten oder die benannten Seiten ({ top: 8, bottom: 8 }), so wie hitSlop einer View sie annimmt.

<Toggle
  pressed={favorite()}
  hitSlop={12}
  accessibilityLabel="Favorite"
  onChange={({ pressed }) => favorite.set(pressed)}
  class="size-5 items-center justify-center"
>
  <Text>★</Text>
</Toggle>

#Barrierefreiheit

  • Ein Toggle ist für Screenreader ein einzelnes Element mit der Rolle "button" und seinem gedrückten Zustand: eine Umschaltfläche. Sein Label ist der Text seiner Kinder oder accessibilityLabel.
  • ToggleGroup hat die Rolle "toolbar": Screenreader hören die Toggles darin als eine Toolbar, jeder weiterhin ein eigenes Element.
  • disabled wird so angesagt, wie die Plattform es ansagt (VoiceOver, TalkBack, UI Automation, der Accessibility-Baum des Browsers).
  • Unter macOS und im Web ist die Gruppe ein Halt des Tastaturfokus, und die Pfeiltasten wechseln zwischen ihren Toggles (siehe unten).

Siehe Barrierefreiheit für Labels, Rollen und Ansagen im Allgemeinen.

#Tastatur

Unter macOS und im Web, wo Tab die Views erreicht, die Drücke annehmen, folgt eine ToggleGroup dem WAI-ARIA-Toolbar-Muster:

Taste Wirkung
Tab, Shift+Tab Betritt die Gruppe beim ersten gedrückten Toggle (beim ersten nicht deaktivierten Toggle, falls keiner gedrückt ist) und verlässt sie in einem Schritt.
Pfeil rechts, Pfeil unten Bewegt den Fokus zum nächsten Toggle, vom letzten zum ersten.
Pfeil links, Pfeil oben Bewegt den Fokus zum vorherigen Toggle, vom ersten zum letzten.
Pos1, Ende Bewegt den Fokus zum ersten oder zum letzten Toggle.
Leertaste, Return Drückt den fokussierten Toggle.

Die Pfeiltasten überspringen deaktivierte Toggles und drücken nichts: Ein Toggle ändert sich nur, wenn er gedrückt wird. Beide Pfeilpaare bewegen den Fokus, unabhängig vom Layout der Gruppe, das Sache der Klassen der App ist. In einem Rechts-nach-links-Layout tauschen Pfeil links und Pfeil rechts die Plätze: Pfeil links bewegt zum nächsten Toggle und Pfeil rechts zum vorherigen; Pfeil oben, Pfeil unten, Pos1 und Ende behalten ihre Bedeutung. focus:-Stile zeigen, wo der Fokus liegt. Ein Toggle außerhalb einer ToggleGroup ist ein eigener Halt.

#Hinweise zu Plattformen

  • iOS, Android und Mac Catalyst: Ein Druck ist ein Tippen, das auf dem Toggle endet, einschließlich seines Inhalts.
  • Desktops (macOS, Windows, Linux, GPUI) und das Web: Ein Druck ist ein Klick. Unter macOS und im Web drücken außerdem Leertaste und Return den Toggle mit Tastaturfokus, und die Pfeiltasten, Pos1 und Ende bewegen den Fokus (Tastatur).
  • Jede Plattform zeichnet, was die App gestaltet: die Toggles, die Fläche der gedrückten Toggles und die Spur der Gruppe.