Cette page a été traduite automatiquement et peut contenir des erreurs. Lire l'original en anglais

Éléments

Checkbox

Une checkbox sélectionne une option, un élément d’une liste, ou tous. Elle est cochée, décochée ou indéterminée (ni l’un ni l’autre : un « tout sélectionner » sur une sélection partielle). Elle porte le comportement : les pressions, l’activation au clavier, l’état désactivé, ainsi que le rôle checkbox et sa valeur pour les lecteurs d’écran. L’apparence appartient à l’app : un Checkbox n’a ni case, ni coche, ni couleurs, ni tailles, ni libellé propres. Il est composé d’un indicateur (la case) contenant la coche de l’app, puis de ses enfants (un libellé), et l’app style chaque partie, avec des classes checked:, indeterminate: et disabled: qui suivent la 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 vient de nsh/base, le crate nsh-base de Rust. La valeur appartient à l’app : une pression signale la valeur suivante avec onChange, et la checkbox affiche les checked et indeterminate que l’app lui renvoie. En Rust, checkbox() en construit une avec un setter par prop (.checked(..), .indicator_style(..)), un écouteur par événement (.on_change(..)), les méthodes de style typées pour la checkbox elle-même et .child(..) pour son libellé ; le style de l’indicateur accepte les méthodes d’état (.checked(..), .indeterminate(..), .disabled_style(..)) qui suivent la checkbox.

#Props

PropRustTypeDéfautDescription
checked.checked(..)booleanfalseCochée ou non. Une pression signale la valeur suivante avec onChange ; la checkbox affiche la valeur que l’app lui renvoie.
indeterminate.indeterminate(..)booleanfalseNi cochée ni décochée, par-dessus checked tant qu’elle est vraie : les lecteurs d’écran indiquent qu’elle est mixte, et une pression demande l’état coché.
disabled.disabled(..)booleanfalseAucune pression et aucun retour de pression ; les styles disabled: s’appliquent à la checkbox et à son indicateur, et les lecteurs d’écran l’annoncent comme grisée.
checkedStyle.checked_style(|s| ..)StyleProp<ViewStyle>aucunLe style de la checkbox lorsqu’elle est cochée et non désactivée, sous forme de classes checked:.
indeterminateStyle.indeterminate_style(|s| ..)StyleProp<ViewStyle>aucunLe style de la checkbox lorsqu’elle est indéterminée et non désactivée, sous forme de classes indeterminate:.
accessibilityLabel.accessibility_label(..)stringaucunCe que disent les lecteurs d’écran : ce que la checkbox sélectionne, lorsque ses enfants n’ont pas de texte qui le dise.
role.role(..)AccessibilityRole"checkbox"Ce que c’est pour les lecteurs d’écran, lorsque c’est quelque chose de plus précis (un élément de menu coché : "menuitem").
indicatorClass, indicatorStyle.indicator_style(|s| ..)string, StyleProp<ViewStyle>aucunL’indicateur, la case qui montre la valeur.
indicator.indicator(..)JSX.ElementaucunLe contenu de l’indicateur : la coche (une coche, un tiret), que l’app affiche selon sa valeur.
hitSlop.hit_slop(..)number | Partial<Edges>aucunJusqu’où, en dehors de ses limites, les pressions comptent encore, en points : un nombre pour tous les côtés, ou les côtés nommés. Comme le hitSlop d’une vue.
children.child(..)JSX.ElementaucunLe contenu après l’indicateur : un libellé, que les lecteurs d’écran énoncent.
class, styleméthodes de stylestring, StyleProp<ViewStyle>aucunLa checkbox elle-même : la ligne autour de l’indicateur et du libellé.
activeStyle, hoverStyle, focusStyle, disabledStyle.active(|s| ..), .hover(..), .focus(..), .disabled_style(..)StyleProp<ViewStyle>aucunLe style de la checkbox lorsqu’elle est pressée, survolée, qu’elle a le focus ou qu’elle est désactivée, sous forme de classes <state>:.

#Événements

ÉvénementRustDescription
onChange.on_change(|checked: &bool| ..)La checkbox a été pressée : un toucher, un clic, Espace ou Entrée lorsqu’elle a le focus clavier, ou l’activation par un lecteur d’écran. Elle signale la valeur que la checkbox demande sous la forme checked : true depuis décochée et depuis indéterminée, false depuis cochée.

Une checkbox désactivée ne signale rien.

#Parties

PartieÀ l’intérieur deStylée avecÉtats
La checkboxclass, stylechecked:, indeterminate:, disabled:, active:, hover:, focus: (ou checkedStyle, indeterminateStyle, disabledStyle, …)
L’indicateurla checkbox, en premierindicatorClass, indicatorStylechecked:, indeterminate:, disabled: suivent la checkbox
La cochel’indicateurses propres éléments (indicator)group-checked:, group-indeterminate: et les autres variantes group-*: avec group sur indicatorClass
Le libelléla checkbox, après l’indicateurses propres éléments (children)variantes group-*: avec group sur le class de la checkbox

La coche est le contenu de l’app : elle montre donc ce que l’app décide. Une coche lorsque la checkbox est cochée et un tiret lorsqu’elle est indéterminée, choisis d’après la valeur que l’app détient, ou bien les deux marques avec des classes group-checked: / group-indeterminate: qui choisissent celle qui est affichée.

#Indéterminé

Une checkbox « tout sélectionner » est indéterminée tant que certains éléments sont sélectionnés. Une pression demande l’état coché, et l’app sélectionne tous les éléments :

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>

#Désactivée

Une checkbox désactivée ne prend aucune pression, et les lecteurs d’écran l’annoncent comme grisée. Ses classes disabled: et celles de son indicateur s’appliquent, par-dessus leurs classes checked: et 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>

#Une petite cible avec hitSlop

Une checkbox sans libellé à côté (la case de sélection d’une ligne) peut être plus petite qu’un doigt. hitSlop fait compter les pressions qui tombent près d’elle, en points au-delà de chaque côté, sans changer sa mise en page ni son apparence : un nombre pour tous les côtés, ou les côtés nommés ({ top: 8, bottom: 8 }), comme le prend le hitSlop d’une vue.

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

#Accessibilité

  • Une checkbox est un seul élément pour les lecteurs d’écran, avec le rôle "checkbox" (ou son role) et sa valeur : cochée, décochée, ou mixte lorsqu’elle est indéterminée. L’indicateur et la coche en font partie. Son libellé est le texte de ses enfants ; donnez accessibilityLabel lorsqu’elle n’en a pas.
  • disabled est annoncé comme la plateforme l’annonce (VoiceOver, TalkBack, UI Automation, l’arbre d’accessibilité du navigateur).
  • Sur macOS et le web, une checkbox est un arrêt du focus clavier : Tab l’atteint, Espace et Entrée la basculent, et les styles focus: montrent où est le focus.

Voir Accessibilité pour les libellés, rôles et annonces en général.

#Notes sur les plateformes

  • iOS, Android et Mac Catalyst : une pression est un toucher qui se termine sur la checkbox, libellé compris.
  • Ordinateurs (macOS, Windows, Linux, GPUI) et le web : une pression est un clic. Sur macOS et le web, Espace et Entrée basculent aussi la checkbox qui a le focus clavier.
  • Chaque plateforme dessine ce que l’app style : l’indicateur, la coche et leurs transitions.