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

Elementi

Checkbox

Un checkbox seleziona un’opzione, un elemento di una lista o tutti. È selezionato, deselezionato o indeterminato (né l’uno né l’altro: un “seleziona tutto” su una selezione parziale). Gestisce il comportamento: pressioni, attivazione da tastiera, stato disabilitato, il ruolo checkbox e il suo valore per gli screen reader. L’aspetto spetta all’app: un Checkbox non ha casella, segno, colori, dimensioni o etichetta propri. È composto da un indicatore (la casella) con il segno dell’app al suo interno, poi dai suoi figli (un’etichetta), e l’app stilizza ogni parte con le classi checked:, indeterminate: e disabled: che seguono il checkbox.

el-checkbox.tsx
el-checkbox.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Checkbox } from "nsh/base";
import { CheckMark, desktop, focusRing, hoverFill, Log, Panel, size, Split, ui } from "./palette";

// The parts: geometry and motion in classes (`disabled:` dims the row,
// following the checkbox; `group-checked:` / `group-indeterminate:` fade in
// the mark of the indicator's state, both kept mounted so each fades out
// too, and a check turning into a dash crosses over), colors from the
// palette by the value the app owns; the pointer's hover and the keyboard's
// focus ring from the palette too. Nothing moves with reduced motion.
const row = `flex-row items-center gap-3 ${desktop ? "px-3 py-2" : "px-4 py-3"} active:opacity-70 disabled:opacity-40`;
const indicator = "group size-5 items-center justify-center rounded-md border transition-colors motion-reduce:transition-none";

/** The indicator's surface: the accent while checked or indeterminate. */
const box = (on: boolean) => (on ? ui().accentFill : { ...ui().inset, borderColor: ui().faint });

/** A mark over the box, centered: hidden and a little smaller until its state's `group-*:` classes show it. */
const layer = "absolute inset-0 items-center justify-center opacity-0 scale-75 transition duration-150 ease-out motion-reduce:transition-none";

/** A check while checked, a dash while indeterminate. */
const mark = () => (
  <>
    <View class={`${layer} group-checked:opacity-100 group-checked:scale-100`}>
      <CheckMark size={14} weight={2.5} tint={ui().onAccent} />
    </View>
    <View class={`${layer} group-indeterminate:opacity-100 group-indeterminate:scale-100`}>
      <View class="h-0.5 w-2.5 rounded-full" style={{ backgroundColor: ui().onAccent }} />
    </View>
  </>
);

const items = [
  { id: "photos", label: "Photos", detail: "2,418 items · 6.1 GB" },
  { id: "documents", label: "Documents", detail: "312 files · 840 MB" },
  { id: "messages", label: "Messages", detail: "Attachments included · 1.2 GB" },
];

function Label(props: { label: string; detail: string; strong?: boolean }) {
  return (
    <View style={{ flex: 1, gap: 2 }}>
      <Text style={{ fontSize: size.label, fontWeight: props.strong ? "600" : "500", color: ui().text }}>{props.label}</Text>
      <Text style={{ fontSize: size.detail, color: ui().muted }}>{props.detail}</Text>
    </View>
  );
}

function App() {
  const selected = signal<string[]>(["photos"]);
  const events = signal<string[]>([]);
  const log = (e: string) => events.set([e, ...events()].slice(0, 3));
  const all = () => selected().length === items.length;
  const some = () => selected().length > 0 && !all();
  const line = () => <View style={{ height: 1, marginLeft: desktop ? 44 : 48, backgroundColor: ui().line }} />;
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>checkbox</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click a row, or Tab to a checkbox and press Space" : "Tap a row: the label toggles it too"}
      </Text>

      <Split
        gap={14}
        inspector={
          <>
            <Panel title="checked · indeterminate">
              <Text style={{ fontSize: 13, color: ui().text }}>
                Back up everything: {all() ? "checked" : some() ? "indeterminate" : "unchecked"} · [{selected().join(", ")}]
              </Text>
            </Panel>
            <Log title="onChange({ checked })" lines={events()} minHeight={92} />
          </>
        }
      >
        <View style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}>
          <Checkbox
            checked={all()}
            indeterminate={some()}
            onChange={({ checked }) => {
              selected.set(checked ? items.map((i) => i.id) : []);
              log(`Back up everything → ${checked}`);
            }}
            class={row}
            hoverStyle={hoverFill()}
            focusStyle={focusRing()}
            indicatorClass={indicator}
            indicatorStyle={box(all() || some())}
            indicator={mark()}
          >
            <Label label="Back up everything" detail={`${selected().length} of ${items.length} selected`} strong />
          </Checkbox>
          <Each of={items}>
            {(item) => (
              <>
                {line()}
                <Checkbox
                  checked={selected().includes(item().id)}
                  onChange={({ checked }) => {
                    selected.set(checked ? [...selected(), item().id] : selected().filter((id) => id !== item().id));
                    log(`${item().label} → ${checked}`);
                  }}
                  class={row}
                  hoverStyle={hoverFill()}
                  focusStyle={focusRing()}
                  indicatorClass={indicator}
                  indicatorStyle={box(selected().includes(item().id))}
                  indicator={mark()}
                >
                  <Label label={item().label} detail={item().detail} />
                </Checkbox>
              </>
            )}
          </Each>
        </View>

        <View style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}>
          <Checkbox checked disabled class={row} indicatorClass={indicator} indicatorStyle={box(true)} indicator={mark()}>
            <Label label="System settings" detail="Always backed up" />
          </Checkbox>
        </View>
      </Split>
    </View>
  );
}

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

fn notify() -> impl IntoElement {
    let on = signal(false);
    checkbox()
        .checked(on)
        .on_change(move |next: &bool| on.set(*next))
        .flex_row()
        .items_center()
        .gap_2()
        .indicator_style(|s| {
            s.size(px(20.0))
                .items_center()
                .justify_center()
                .rounded_md()
                .border_2()
                .border_color(move || theme().border)
                .transition_colors()
                .checked(|s| s.bg(move || theme().primary).border_color(move || theme().primary))
        })
        .indicator(text(move || if on.get() { "✓" } else { "" }).text_color(move || theme().primary_foreground))
        .child(text("Notify me"))
}

Checkbox proviene da nsh/base, il crate nsh-base in Rust. Il valore spetta all’app: una pressione segnala il valore successivo con onChange, e il checkbox mostra i valori checked e indeterminate che l’app restituisce. In Rust, checkbox() ne crea uno con un setter per ogni prop (.checked(..), .indicator_style(..)), un listener per ogni evento (.on_change(..)), i metodi di stile tipizzati per il checkbox stesso e .child(..) per la sua etichetta; lo stile dell’indicatore accetta i metodi di stato (.checked(..), .indeterminate(..), .disabled_style(..)) che seguono il checkbox.

#Props

PropRustTipoPredefinitoDescrizione
checked.checked(..)booleanfalseSelezionato o no. Una pressione segnala il valore successivo con onChange; il checkbox mostra il valore che l’app restituisce.
indeterminate.indeterminate(..)booleanfalseNé selezionato né deselezionato, ha la precedenza su checked finché è attivo: gli screen reader lo annunciano come misto, e una pressione richiede selezionato.
disabled.disabled(..)booleanfalseNessuna pressione e nessun feedback alla pressione; gli stili disabled: si applicano al checkbox e al suo indicatore, e gli screen reader lo annunciano come attenuato.
checkedStyle.checked_style(|s| ..)StyleProp<ViewStyle>nessunoLo stile del checkbox quando è selezionato e non disabilitato, come classi checked:.
indeterminateStyle.indeterminate_style(|s| ..)StyleProp<ViewStyle>nessunoLo stile del checkbox quando è indeterminato e non disabilitato, come classi indeterminate:.
accessibilityLabel.accessibility_label(..)stringnessunoCiò che dicono gli screen reader: che cosa seleziona il checkbox, quando i suoi figli non hanno testo che lo dica.
role.role(..)AccessibilityRole"checkbox"Che cos’è per gli screen reader, quando è qualcosa di più specifico (una voce di menu selezionata: "menuitem").
indicatorClass, indicatorStyle.indicator_style(|s| ..)string, StyleProp<ViewStyle>nessunoL’indicatore, la casella che mostra il valore.
indicator.indicator(..)JSX.ElementnessunoIl contenuto dell’indicatore: il segno (una spunta, un trattino), che l’app mostra in base al suo valore.
hitSlop.hit_slop(..)number | Partial<Edges>nessunoQuanto fuori dai suoi limiti le pressioni contano ancora, in punti: un numero per tutti i lati, oppure i lati indicati per nome. Come il hitSlop di una view.
children.child(..)JSX.ElementnessunoIl contenuto dopo l’indicatore: un’etichetta, che gli screen reader leggono.
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoIl checkbox stesso: la riga attorno all’indicatore e all’etichetta.
activeStyle, hoverStyle, focusStyle, disabledStyle.active(|s| ..), .hover(..), .focus(..), .disabled_style(..)StyleProp<ViewStyle>nessunoLo stile del checkbox quando è premuto, in hover, con il focus o disabilitato, come classi <state>:.

#Eventi

EventoRustDescrizione
onChange.on_change(|checked: &bool| ..)Il checkbox è stato premuto: un tap, un clic, Spazio o Invio mentre ha il focus della tastiera, oppure l’attivazione da parte di uno screen reader. Segnala il valore richiesto dal checkbox come checked: true da deselezionato e da indeterminato, false da selezionato.

Un checkbox disabilitato non segnala nulla.

#Parti

ParteDentroStilizzata conStati
Il checkboxclass, stylechecked:, indeterminate:, disabled:, active:, hover:, focus: (oppure checkedStyle, indeterminateStyle, disabledStyle, …)
L’indicatoreil checkbox, per primoindicatorClass, indicatorStylechecked:, indeterminate:, disabled: seguono il checkbox
Il segnol’indicatoreelementi propri (indicator)group-checked:, group-indeterminate: e le altre varianti group-*: con group su indicatorClass
L’etichettail checkbox, dopo l’indicatoreelementi propri (children)varianti group-*: con group sul class del checkbox

Il segno è contenuto dell’app, quindi mostra ciò che l’app decide: una spunta quando è selezionato e un trattino quando è indeterminato, scelti in base al valore che l’app detiene, oppure entrambi i segni, con le classi group-checked: / group-indeterminate: che scelgono quello mostrato.

#Indeterminato

Un checkbox “seleziona tutto” è indeterminato finché alcuni elementi sono selezionati. Una pressione richiede selezionato, e l’app seleziona ogni elemento:

const selected = signal<string[]>([]);
const all = () => selected().length === items.length;
const some = () => selected().length > 0 && !all();

<Checkbox
  checked={all()}
  indeterminate={some()}
  onChange={({ checked }) => selected.set(checked ? items.map((i) => i.id) : [])}
  class="flex-row items-center gap-2"
  indicatorClass="group size-5 items-center justify-center rounded-md border-2 border-border checked:border-primary checked:bg-primary indeterminate:border-primary indeterminate:bg-primary"
  indicator={
    <>
      <Text class="hidden text-primary-foreground group-checked:flex">✓</Text>
      <View class="hidden h-0.5 w-2.5 bg-primary-foreground group-indeterminate:flex" />
    </>
  }
>
  <Text>Select all</Text>
</Checkbox>

#Disabilitato

Un checkbox disabilitato non accetta pressioni, e gli screen reader lo annunciano come attenuato. Le sue classi disabled: e quelle del suo indicatore si applicano, con precedenza su quelle checked: e indeterminate::

<Checkbox
  checked={on()}
  disabled={locked()}
  onChange={({ checked }) => on.set(checked)}
  class="flex-row items-center gap-2 disabled:opacity-50"
  indicatorClass="size-5 rounded-md border-2 border-border checked:border-primary checked:bg-primary disabled:border-border disabled:bg-muted"
>
  <Text>Share usage data</Text>
</Checkbox>

#Un bersaglio piccolo con hitSlop

Un checkbox senza etichetta accanto (la casella di selezione di una riga) può essere più piccolo di un dito. hitSlop fa contare le pressioni che cadono vicino ad esso, in punti oltre ogni 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 il hitSlop di una view.

<Checkbox
  checked={selected()}
  hitSlop={12}
  accessibilityLabel="Select row"
  onChange={({ checked }) => selected.set(checked)}
  indicatorClass="size-4 rounded border border-border checked:bg-primary"
/>

#Accessibilità

  • Un checkbox è un unico elemento per gli screen reader, con il ruolo "checkbox" (o il suo role) e il suo valore: selezionato, deselezionato o misto quando è indeterminato. L’indicatore e il segno ne sono parti. La sua etichetta è il testo dei suoi figli; indica accessibilityLabel quando non ne ha.
  • disabled viene annunciato nel modo in cui lo annuncia la piattaforma (VoiceOver, TalkBack, UI Automation, l’albero di accessibilità del browser).
  • Su macOS e sul web un checkbox è una tappa del focus da tastiera: Tab lo raggiunge, Spazio e Invio lo attivano, e gli stili focus: mostrano dove si trova il focus.

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

#Note sulle piattaforme

  • iOS, Android e Mac Catalyst: una pressione è un tap che termina sul checkbox, etichetta inclusa.
  • Desktop (macOS, Windows, Linux, GPUI) e web: una pressione è un clic. Su macOS e sul web, Spazio e Invio attivano anche il checkbox che ha il focus da tastiera.
  • Ogni piattaforma disegna ciò che l’app stilizza: l’indicatore, il segno e le loro transizioni.