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

Elemente

Radio

Radios bieten eine Menge von Optionen, von denen der Nutzer genau eine wählt: einen Tarif, eine Sortierreihenfolge, ein Theme. Jedes Radio übernimmt das Verhalten einer Option: Drücken, Tastaturaktivierung, den deaktivierten Zustand sowie die Radio-Rolle und ihren Checked-Zustand für Screenreader. Eine RadioGroup um die Radios herum bildet die Menge, die Screenreader als eine Gruppe ansagen. Das Aussehen gehört der App: Ein Radio hat selbst keinen Kreis, keinen Punkt, keine Farben, Größen oder Beschriftung. Seine Kinder sind der Indikator und das Label der App, und checked:-Klassen am Radio oder group-checked:-Klassen auf dem, was darin liegt, folgen seinem Zustand.

el-radio.tsx
el-radio.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Radio, RadioGroup } from "nsh/base";
import { desktop, focusRing, hoverFill, Log, size, Split, ui } from "./palette";

const plans = [
  { id: "starter", label: "Starter", detail: "1 project · 5 GB", price: "Free" },
  { id: "pro", label: "Pro", detail: "Unlimited projects · 100 GB", price: "$12" },
  { id: "team", label: "Team", detail: "Shared workspace · 1 TB", price: "$40", locked: true },
];
const cycles = ["Monthly", "Yearly"];

function App() {
  const plan = signal("pro");
  const cycle = signal("Monthly");
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 3));
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>radio</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click an option, or Tab to a group and move with the arrow keys" : "Tap an option: its label checks it too"}
      </Text>

      <Split gap={14} inspector={<Log title={`onChange({ checked }) · ${plan()}, ${cycle()}`} lines={events()} minHeight={76} />}>
        {/* A segmented choice: each radio is a segment, `checkedStyle` raises the checked one. */}
        <RadioGroup style={{ flexDirection: "row", gap: 4, padding: 3, borderRadius: 10, ...ui().inset, ...(desktop ? { alignSelf: "flex-start" } : {}) }}>
          <Each of={cycles}>
            {(c) => (
              <Radio
                checked={cycle() === c()}
                onChange={({ checked }) => {
                  cycle.set(c());
                  log(`${c()} → ${checked}`);
                }}
                style={
                  desktop
                    ? { height: 26, paddingHorizontal: 14, borderRadius: 7, alignItems: "center", justifyContent: "center" }
                    : { flex: 1, height: 30, borderRadius: 7, alignItems: "center", justifyContent: "center" }
                }
                class="active:opacity-70"
                hoverStyle={cycle() === c() ? undefined : hoverFill()}
                focusStyle={focusRing()}
                checkedStyle={ui().raised}
              >
                <Text style={{ fontSize: 13, fontWeight: "500", color: cycle() === c() ? ui().text : ui().muted }}>{c()}</Text>
              </Radio>
            )}
          </Each>
        </RadioGroup>

        {/* A list of options: a ring with a dot, which `group-checked:` grows in (in place with reduced motion). */}
        <RadioGroup style={{ ...ui().card, borderRadius: 14, overflow: "hidden" }}>
          <Each of={plans}>
            {(item, i) => (
              <>
                {i() > 0 ? <View style={{ height: 1, marginLeft: desktop ? 40 : 48, backgroundColor: ui().line }} /> : null}
                <Radio
                  checked={plan() === item().id}
                  disabled={item().locked}
                  onChange={({ checked }) => {
                    plan.set(item().id);
                    log(`${item().label} → ${checked}`);
                  }}
                  class={`group flex-row items-center gap-3 ${desktop ? "px-3 py-2" : "px-4 py-3"} active:opacity-70 disabled:opacity-40`}
                  hoverStyle={item().locked ? undefined : hoverFill()}
                  focusStyle={focusRing()}
                >
                  <View
                    class="transition-colors motion-reduce:transition-none"
                    style={{
                      width: desktop ? 16 : 20,
                      height: desktop ? 16 : 20,
                      borderRadius: desktop ? 8 : 10,
                      borderWidth: 2,
                      borderColor: plan() === item().id ? ui().accent : ui().faint,
                      alignItems: "center",
                      justifyContent: "center",
                    }}
                  >
                    <View
                      class="opacity-0 scale-50 transition duration-150 ease-out motion-reduce:transition-none group-checked:opacity-100 group-checked:scale-100"
                      style={{ width: desktop ? 8 : 10, height: desktop ? 8 : 10, borderRadius: desktop ? 4 : 5, backgroundColor: ui().accent }}
                    />
                  </View>
                  <View style={{ flex: 1, gap: 2 }}>
                    <Text style={{ fontSize: size.label, fontWeight: "500", color: ui().text }}>{item().label}</Text>
                    <Text style={{ fontSize: size.detail, color: ui().muted }}>{item().detail}</Text>
                  </View>
                  <Text style={{ fontSize: 14, fontWeight: "600", fontVariant: ["tabular-nums"], color: ui().text }}>{item().price}</Text>
                </Radio>
              </>
            )}
          </Each>
        </RadioGroup>
      </Split>
    </View>
  );
}

mount(App);
use nsh_base::{radio, radio_group, theme};
use nsh_core::prelude::*;

fn plans() -> impl IntoElement {
    let plan = signal("basic");
    let option = move |value: &'static str, label: &'static str| {
        let checked = move || plan.get() == value;
        radio()
            .checked(checked)
            .on_change(move |_: &bool| plan.set(value))
            .flex_row()
            .items_center()
            .gap_2()
            .child(
                view()
                    .size(px(20.0))
                    .items_center()
                    .justify_center()
                    .rounded_full()
                    .border_2()
                    .border_color(move || theme().border)
                    .child(
                        view()
                            .size(px(10.0))
                            .rounded_full()
                            .bg(move || theme().primary)
                            .opacity(move || if checked() { 1.0_f32 } else { 0.0 }),
                    ),
            )
            .child(text(label))
    };
    radio_group()
        .gap_3()
        .child(option("basic", "Basic"))
        .child(option("pro", "Pro"))
}

Radio und RadioGroup stammen aus nsh/base, dem nsh-base-Crate in Rust. Der Wert gehört der App: Die Gruppe hält keinen. Jedes Radio erfährt, ob es ausgewählt ist; ein Druck auf ein nicht ausgewähltes Radio meldet onChange({ checked: true }), die App setzt ihren Wert, und jedes Radio zeigt das checked, das die App zurückgibt. In Rust erzeugen radio() und radio_group() sie, mit einem Setter pro Prop (.checked(..)), einem Listener pro Event (.on_change(..)), den typisierten Style-Methoden und .child(..) für ihren Inhalt.

#Props

#Radio

Prop Rust Typ Standard Beschreibung
checked .checked(..) boolean false Ausgewählt oder nicht. Ein Druck auf ein nicht ausgewähltes Radio meldet true mit onChange; das Radio zeigt den Wert, den die App zurückgibt.
disabled .disabled(..) boolean false Keine Drücke und kein Druck-Feedback; disabled:-Styles greifen, und Screenreader sagen, dass es abgeblendet ist.
checkedStyle .checked_style(|s| ..) StyleProp<ViewStyle> keine Der Style des Radios, solange es ausgewählt und nicht deaktiviert ist, als checked:-Klassen.
accessibilityLabel .accessibility_label(..) string keine Was Screenreader sagen: die Option, wenn die Kinder keinen Text enthalten, der sie benennt.
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 Seiten einzeln benannt. Wie bei hitSlop einer View.
children .child(..) JSX.Element keine Der Indikator und das Label der App, die Screenreader vorlesen.
class, style Style-Methoden string, StyleProp<ViewStyle> keine Das Radio selbst: die Zeile um den Indikator und das Label.
activeStyle, hoverStyle, focusStyle, disabledStyle .active(|s| ..), .hover(..), .focus(..), .disabled_style(..) StyleProp<ViewStyle> keine Der Style des Radios, solange es gedrückt, überfahren, fokussiert oder deaktiviert ist, als die <state>:-Klassen.

#RadioGroup

Prop Rust Typ Standard Beschreibung
children .child(..) JSX.Element keine Die Radios der Menge und alles, was die App dazwischen anordnet.
class, style Style-Methoden string, StyleProp<ViewStyle> keine Layout und Aussehen der Gruppe: standardmäßig eine Spalte, flex-row für eine Zeile.

#Events

Event Rust Beschreibung
onChange (Radio) .on_change(|checked: &bool| ..) Ein nicht ausgewähltes Radio wurde gedrückt: durch Tippen, Klicken, Leertaste oder Return, während es den Tastaturfokus hat, oder durch die Aktivierung eines Screenreaders; oder eine Pfeiltaste seiner Gruppe hat den Tastaturfokus auf es bewegt. Es bittet darum, ausgewählt zu werden, daher ist checked gleich true. Das Event läuft, bevor sich etwas ändert: Das Radio zeigt das checked, das die App zurückgibt.

Ein ausgewähltes Radio bleibt ausgewählt und meldet nichts, da ein Radio sich nicht selbst abwählen kann; ein deaktiviertes Radio meldet ebenfalls nichts.

#Teile

Teil Enthalten in Gestaltet mit Zustände
Die Gruppe RadioGroup: class, style
Das Radio der Gruppe Radio: class, style checked:, disabled:, active:, hover:, focus: (oder checkedStyle, disabledStyle, …)
Der Indikator und das Label dem Radio ihren eigenen Elementen (children) group-checked:, group-disabled: und die anderen group-*:-Varianten mit group auf dem class des Radios

Der Indikator ist der Inhalt der App: ein Ring mit Punkt, ein Häkchen, eine hervorgehobene Karte. Mit group auf dem class des Radios zeigen seine group-checked:-Klassen den Punkt, solange das Radio ausgewählt ist.

#Deaktiviert

Ein deaktiviertes Radio nimmt keine Drücke an, und Screenreader sagen es als abgeblendet an. Seine disabled:-Klassen gelten vor seinen checked:-Klassen, und group-disabled:-Klassen darin folgen:

<Radio
  checked={plan() === "team"}
  disabled={!eligible()}
  onChange={() => plan.set("team")}
  class="group flex-row items-center gap-2 disabled:opacity-50"
>
  <View class="size-5 items-center justify-center rounded-full border-2 border-border group-checked:border-primary">
    <View class="size-2.5 rounded-full group-checked:bg-primary" />
  </View>
  <Text>Team</Text>
</Radio>

#Ein kleines Ziel mit hitSlop

Ein Radio ohne Label daneben (ein Punkt in der Zeile einer Tabelle) kann kleiner sein als ein Finger. hitSlop lässt Drücke zählen, die in seiner Nähe landen, in Punkten über jede Seite hinaus, ohne sein Layout oder sein Aussehen zu ändern: eine Zahl für alle Seiten oder die Seiten einzeln benannt ({ top: 8, bottom: 8 }), so wie hitSlop einer View sie annimmt.

<Radio
  checked={plan() === "team"}
  hitSlop={12}
  accessibilityLabel="Team"
  onChange={() => plan.set("team")}
  class="group size-5 items-center justify-center rounded-full border-2 border-border group-checked:border-primary"
>
  <View class="size-2.5 rounded-full group-checked:bg-primary" />
</Radio>

#Barrierefreiheit

  • Ein Radio ist für Screenreader ein einzelnes Element mit der Rolle "radio" und seinem Zustand: ausgewählt oder nicht, und selected, solange es ausgewählt ist, da manche Screenreader das eine und manche das andere vorlesen (die Radios von UI Automation sind Auswahlelemente). Indikator und Label sind Teile davon; sein Label ist der Text seiner Kinder oder accessibilityLabel.
  • Eine RadioGroup hat die Rolle "radiogroup": Screenreader hören die Radios darin als eine Menge, jedes weiterhin als eigenes Element.
  • disabled wird so angesagt, wie die Plattform es ansagt (VoiceOver, TalkBack, UI Automation, der Accessibility-Tree des Browsers).
  • Unter macOS und im Web ist eine RadioGroup ein einziger Halt des Tastaturfokus, und die Pfeiltasten bewegen sich zwischen ihren Radios und wählen sie aus (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 RadioGroup dem WAI-ARIA-Radiogruppen-Muster:

Taste Wirkung
Tab, Umschalt+Tab Betritt die Gruppe beim ausgewählten Radio (beim ersten nicht deaktivierten Radio, solange keines ausgewählt ist) und verlässt sie in einem Schritt.
Runter, Rechts Bewegt den Fokus zum nächsten Radio, vom letzten zum ersten, und wählt es aus.
Hoch, Links Bewegt den Fokus zum vorherigen Radio, vom ersten zum letzten, und wählt es aus.
Leertaste Wählt das fokussierte Radio aus.

Die Pfeiltasten überspringen deaktivierte Radios; das Radio, zu dem sie wechseln, bittet mit onChange darum, ausgewählt zu werden, und die App wählt es aus. In einem Rechts-nach-links-Layout tauschen Links und Rechts die Plätze: Links wechselt zum nächsten Radio und Rechts zum vorherigen; Hoch und Runter behalten ihre. focus:-Styles zeigen, wo der Fokus liegt. Ein Radio außerhalb einer RadioGroup ist ein eigener Halt, den Tab erreicht und den Leertaste und Return auswählen.

#Plattformhinweise

  • iOS, Android und Mac Catalyst: Ein Druck ist ein Tippen, das auf dem Radio endet, sein Label eingeschlossen.
  • Desktops (macOS, Windows, Linux, GPUI) und das Web: Ein Druck ist ein Klick. Unter macOS und im Web wählen auch Leertaste und Return das Radio aus, das den Tastaturfokus hat, und die Pfeiltasten bewegen ihn (Tastatur).
  • Jede Plattform zeichnet, was die App gestaltet: den Indikator, das Label und ihre Übergänge.