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

Elementi

Radio

Le radio offrono un insieme di opzioni tra cui l’utente ne sceglie esattamente una: un piano, un ordinamento, un tema. Ogni Radio gestisce il comportamento di una opzione: pressioni, attivazione da tastiera, stato disabilitato, e il ruolo radio con il relativo stato checked per gli screen reader. Un RadioGroup attorno ad esse costituisce l’insieme, che gli screen reader annunciano come un unico gruppo. L’aspetto è dell’app: una radio non ha un proprio cerchio, punto, colori, dimensioni o etichetta. I suoi figli sono l’indicatore e l’etichetta dell’app, e le classi checked: sulla radio, o le classi group-checked: su ciò che contiene, ne seguono lo stato.

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 e RadioGroup provengono da nsh/base, il crate nsh-base in Rust. Il valore appartiene all’app: il gruppo non ne conserva alcuno. A ogni radio viene comunicato se è selezionata; una pressione su una radio non selezionata segnala onChange({ checked: true }), l’app imposta il proprio valore, e ogni radio mostra il checked che l’app restituisce. In Rust, radio() e radio_group() le creano, con un setter per ogni prop (.checked(..)), un listener per ogni evento (.on_change(..)), i metodi di stile tipizzati e .child(..) per il loro contenuto.

#Props

#Radio

PropRustTipoPredefinitoDescrizione
checked.checked(..)booleanfalseSelezionata o no. Una pressione su una radio non selezionata segnala true con onChange; la radio mostra il valore che l’app restituisce.
disabled.disabled(..)booleanfalseNessuna pressione e nessun feedback alla pressione; si applicano gli stili disabled:, e gli screen reader dicono che è attenuata.
checkedStyle.checked_style(|s| ..)StyleProp<ViewStyle>nessunoLo stile della radio mentre è selezionata e non disabilitata, come classi checked:.
accessibilityLabel.accessibility_label(..)stringnessunoCiò che dicono gli screen reader: l’opzione, quando i figli non hanno un testo che la esprima.
hitSlop.hit_slop(..)number | Partial<Edges>nessunoDi quanto fuori dai suoi limiti le pressioni contano ancora, in punti: un numero per tutti i lati, oppure i lati indicati per nome. Come hitSlop di una view.
children.child(..)JSX.ElementnessunoL’indicatore e l’etichetta dell’app, che gli screen reader leggono.
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoLa radio stessa: la riga attorno all’indicatore e all’etichetta.
activeStyle, hoverStyle, focusStyle, disabledStyle.active(|s| ..), .hover(..), .focus(..), .disabled_style(..)StyleProp<ViewStyle>nessunoLo stile della radio mentre è premuta, in hover, con il focus o disabilitata, come classi <state>:.

#RadioGroup

PropRustTipoPredefinitoDescrizione
children.child(..)JSX.ElementnessunoLe radio dell’insieme, e qualsiasi cosa l’app disponga tra di esse.
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoIl layout e l’aspetto del gruppo: una colonna per impostazione predefinita, flex-row per una riga.

#Eventi

EventoRustDescrizione
onChange (Radio).on_change(|checked: &bool| ..)Una radio non selezionata è stata premuta: un tap, un clic, Space o Return mentre ha il focus da tastiera, oppure l’attivazione da parte di uno screen reader; oppure un tasto freccia del suo gruppo ha spostato il focus da tastiera su di essa. Chiede di essere selezionata, quindi checked è true. Viene eseguito prima che cambi qualsiasi cosa: la radio mostra il checked che l’app restituisce.

Una radio selezionata resta selezionata e non segnala nulla, poiché una radio non può deselezionarsi da sola; nemmeno una radio disabilitata segnala nulla.

#Parti

ParteDentroStilizzata conStati
Il gruppoRadioGroup: class, style
La radioil gruppoRadio: class, stylechecked:, disabled:, active:, hover:, focus: (o checkedStyle, disabledStyle, …)
L’indicatore e l’etichettala radiopropri elementi (children)group-checked:, group-disabled: e le altre varianti group-*: con group su class della radio

L’indicatore è contenuto dell’app: un anello con un punto, un segno di spunta, una card evidenziata. Con group su class della radio, le sue classi group-checked: mostrano il punto mentre la radio è selezionata.

#Disabilitato

Una radio disabilitata non accetta pressioni, e gli screen reader la annunciano come attenuata. Le sue classi disabled: si applicano sopra quelle checked:, e le classi group-disabled: al suo interno le seguono:

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

#Un’area piccola con hitSlop

Una radio senza etichetta accanto (un punto nella riga di una tabella) può essere più piccola di un dito. hitSlop fa contare le pressioni che cadono vicino ad essa, in punti oltre ciascun lato, senza cambiarne il layout o l’aspetto: un numero per tutti i lati, oppure i lati indicati per nome ({ top: 8, bottom: 8 }), come li accetta hitSlop di una view.

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

#Accessibilità

  • Una radio è un unico elemento per gli screen reader, con il ruolo "radio" e il suo stato: selezionata o no, e selected mentre è selezionata, poiché alcuni screen reader leggono l’uno e altri l’altro (le radio di UI Automation sono elementi di selezione). Il suo indicatore e la sua etichetta ne sono parti; la sua etichetta è il testo dei suoi figli, oppure accessibilityLabel.
  • Un RadioGroup ha il ruolo "radiogroup": gli screen reader sentono le radio al suo interno come un unico insieme, ciascuna comunque un elemento a sé.
  • disabled viene annunciato come lo annuncia la piattaforma (VoiceOver, TalkBack, UI Automation, l’albero di accessibilità del browser).
  • Su macOS e sul web un RadioGroup è una sola tappa del focus da tastiera, e i tasti freccia si spostano tra le sue radio e le selezionano (vedi sotto).

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

#Tastiera

Su macOS e sul web, dove Tab raggiunge le view che accettano pressioni, un RadioGroup segue il pattern WAI-ARIA per i radio group:

TastoCosa fa
Tab, Shift+TabEntra nel gruppo sulla radio selezionata (la prima radio non disabilitata se nessuna lo è), e lo lascia in un solo passaggio.
Giù, DestraSposta il focus sulla radio successiva, dall’ultima alla prima, e la seleziona.
Su, SinistraSposta il focus sulla radio precedente, dalla prima all’ultima, e la seleziona.
SpaceSeleziona la radio con il focus.

I tasti freccia saltano le radio disabilitate; la radio su cui si spostano chiede di essere selezionata con onChange, e l’app la seleziona. In un layout da destra a sinistra Sinistra e Destra si scambiano: Sinistra passa alla radio successiva e Destra alla precedente; Su e Giù mantengono le proprie. Gli stili focus: mostrano dove si trova il focus. Un Radio fuori da un RadioGroup è una tappa a sé, che Tab raggiunge e che Space e Return selezionano.

#Note sulle piattaforme

  • iOS, Android e Mac Catalyst: una pressione è un tap che termina sulla radio, etichetta inclusa.
  • Desktop (macOS, Windows, Linux, GPUI) e web: una pressione è un clic. Su macOS e sul web, Space e Return selezionano anche la radio che ha il focus da tastiera, e i tasti freccia lo spostano (Tastiera).
  • Ogni piattaforma disegna ciò che l’app stilizza: l’indicatore, l’etichetta e le loro transizioni.