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

Éléments

Switch

Un switch active ou désactive un réglage, et le changement prend effet immédiatement. Il gère le comportement : appuis, activation au clavier, état désactivé, ainsi que le rôle switch et sa valeur pour les lecteurs d’écran. L’apparence appartient à l’app : un Switch n’a ni couleurs, ni tailles, ni animation, ni libellé propres. Il est composé d’une piste (track) contenant un curseur (thumb), puis de ses enfants (un libellé), et l’app style chaque partie, avec des classes checked: et disabled: qui suivent le switch.

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

// The parts: geometry and motion in classes (`checked:` moves the thumb,
// `disabled:` dims the row, both following the switch), colors from the
// palette by the value the app owns. A pointer gets the compact size; the
// pointer's hover and the keyboard's focus ring come from the palette, and
// the thumb and track stay still with reduced motion.
const row = `flex-row-reverse items-center justify-between gap-3 ${desktop ? "px-3 py-2" : "px-4 py-3"} active:opacity-70 disabled:opacity-40`;
const track = `${desktop ? "w-9" : "w-12"} rounded-full p-0.5 transition-colors motion-reduce:transition-none`;
const thumb = desktop ? "size-4 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-4" : "size-5.5 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-5";

function Setting(props: { label: string; detail: string; checked: boolean; disabled?: boolean; onChange: (e: { checked: boolean }) => void }) {
  return (
    <Switch
      checked={props.checked}
      disabled={props.disabled}
      onChange={props.onChange}
      class={row}
      hoverStyle={props.disabled ? undefined : hoverFill()}
      focusStyle={focusRing()}
      trackClass={track}
      trackStyle={props.checked ? ui().accentFill : ui().inset}
      thumbClass={thumb}
      thumbStyle={props.checked ? { backgroundColor: ui().onAccent } : ui().raised}
    >
      <View style={{ flex: 1, gap: 2 }}>
        <Text style={{ fontSize: size.label, fontWeight: "500", color: ui().text }}>{props.label}</Text>
        <Text style={{ fontSize: size.detail, color: ui().muted }}>{props.detail}</Text>
      </View>
    </Switch>
  );
}

function App() {
  const wifi = signal(true);
  const bluetooth = signal(false);
  const ask = signal(true);
  const events = signal<string[]>([]);
  const change = (name: string, value: { set: (v: boolean) => void }) => ({ checked }: { checked: boolean }) => {
    value.set(checked);
    events.set([`${name} → ${checked}`, ...events()].slice(0, 3));
  };
  const line = () => <View style={{ height: 1, marginLeft: desktop ? 12 : 16, 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 }}>switch</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click a row, or Tab to a switch and press Space" : "Tap a row: the label toggles it too"}
      </Text>

      <Split
        gap={14}
        inspector={
          <>
            <Panel title="checked · disabled">
              <Text style={{ fontSize: 13, color: ui().text }}>
                Wi-Fi {wifi() ? "on" : "off"} · ask {ask() ? "on" : "off"}
                {wifi() ? "" : " (disabled)"} · Bluetooth {bluetooth() ? "on" : "off"}
              </Text>
            </Panel>
            <Log title="onChange({ checked })" lines={events()} minHeight={92} />
          </>
        }
      >
        <View style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}>
          <Setting label="Wi-Fi" detail={wifi() ? "Connected to Studio" : "Off"} checked={wifi()} onChange={change("Wi-Fi", wifi)} />
          {line()}
          <Setting
            label="Ask to join networks"
            detail={wifi() ? "Known networks join on their own" : "Turn on Wi-Fi first"}
            checked={ask()}
            disabled={!wifi()}
            onChange={change("Ask to join", ask)}
          />
          {line()}
          <Setting label="Bluetooth" detail={bluetooth() ? "Discoverable as “Desk”" : "Off"} checked={bluetooth()} onChange={change("Bluetooth", bluetooth)} />
        </View>
      </Split>
    </View>
  );
}

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

fn wifi() -> impl IntoElement {
    let on = signal(false);
    switch()
        .checked(on)
        .on_change(move |next: &bool| on.set(*next))
        .flex_row()
        .items_center()
        .gap_3()
        .track_style(|s| {
            s.w(px(44.0))
                .p(px(2.0))
                .rounded_full()
                .bg(move || theme().muted)
                .transition_colors()
                .checked(|s| s.bg(move || theme().primary))
        })
        .thumb_style(|s| {
            s.size(px(20.0))
                .rounded_full()
                .bg(move || theme().background)
                .shadow_sm()
                .transition_transform()
                .checked(|s| s.translate_x(px(20.0)))
        })
        .child(text("Wi-Fi"))
}

Switch provient de nsh/base, le crate nsh-base en Rust. La valeur appartient à l’app : un appui signale la valeur suivante avec onChange, et le switch affiche la valeur checked que l’app lui renvoie. En Rust, switch() en crée un avec un setter par prop (.checked(..), .track_style(..)), un écouteur par événement (.on_change(..)), les méthodes de style typées pour le switch lui-même et .child(..) pour son libellé ; le style d’une partie accepte les méthodes d’état (.checked(..), .disabled_style(..)) qui suivent le switch.

#Props

PropRustTypeDéfautDescription
checked.checked(..)booleanfalseActivé ou désactivé. Un appui signale la valeur suivante avec onChange ; le switch affiche la valeur que l’app lui renvoie.
disabled.disabled(..)booleanfalseAucun appui et aucun retour visuel d’appui ; les styles disabled: s’appliquent au switch et à ses parties, et les lecteurs d’écran indiquent qu’il est grisé.
checkedStyle.checked_style(|s| ..)StyleProp<ViewStyle>aucunLe style du switch lorsqu’il est checked et non désactivé, sous forme de classes checked:.
accessibilityLabel.accessibility_label(..)stringaucunCe que disent les lecteurs d’écran : ce que le switch active, lorsque ses enfants ne contiennent pas de texte qui le dise.
trackClass, trackStyle.track_style(|s| ..)string, StyleProp<ViewStyle>aucunLa piste, la partie le long de laquelle le curseur se déplace.
thumbClass, thumbStyle.thumb_style(|s| ..)string, StyleProp<ViewStyle>aucunLe curseur, à l’intérieur de la piste.
hitSlop.hit_slop(..)number | Partial<Edges>aucunJusqu’où, en dehors de ses limites, les appuis 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 la piste : un libellé, que les lecteurs d’écran énoncent.
class, styleméthodes de stylestring, StyleProp<ViewStyle>aucunLe switch lui-même : la ligne qui entoure la piste et le libellé.
activeStyle, hoverStyle, focusStyle, disabledStyle.active(|s| ..), .hover(..), .focus(..), .disabled_style(..)StyleProp<ViewStyle>aucunLe style du switch lorsqu’il est pressé, survolé, focalisé ou désactivé, sous forme de classes <state>:.

#Événements

ÉvénementRustDescription
onChange.on_change(|checked: &bool| ..)Le switch a été pressé : un tap, un clic, Espace ou Entrée lorsqu’il a le focus clavier, ou une activation par un lecteur d’écran. Il signale la valeur que le switch demande sous la forme checked, l’opposé de la valeur checked qu’il a.

Un switch désactivé ne signale rien.

#Parties

PartieÀ l’intérieur deStylée avecÉtats
Le switchclass, stylechecked:, disabled:, active:, hover:, focus: (ou checkedStyle, disabledStyle, …)
La pistele switch, en premiertrackClass, trackStylechecked:, disabled: suivent le switch
Le curseurla pistethumbClass, thumbStylechecked:, disabled: suivent le switch
Le libelléle switch, après la pisteses propres éléments (children)group-checked: et les autres variantes group-*: avec group sur le class du switch

Le curseur se déplace grâce à ses propres styles checked: : une translation (checked:translate-x-5), ou une marge ou l’alignement de la piste (checked:justify-end sur la piste). Une classe transition-* anime le changement de façon native. Pour placer le libellé avant la piste, inversez la ligne : class="flex-row-reverse justify-between".

#Désactivé

Un switch désactivé n’accepte aucun appui, et les lecteurs d’écran l’annoncent comme grisé. Ses classes disabled: et celles de sa piste et de son curseur s’appliquent, par-dessus leurs classes checked: :

<Switch
  checked={on()}
  disabled={!online()}
  onChange={({ checked }) => on.set(checked)}
  class="flex-row items-center gap-3 disabled:opacity-50"
  trackClass="w-11 rounded-full bg-muted p-0.5 checked:bg-primary disabled:bg-muted"
  thumbClass="size-5 rounded-full bg-background checked:translate-x-5"
>
  <Text>Sync over cellular</Text>
</Switch>

#Une petite cible avec hitSlop

Un switch sans libellé à côté peut être plus petit qu’un doigt. hitSlop fait compter les appuis qui tombent près de lui, 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 les accepte le hitSlop d’une vue.

<Switch
  checked={on()}
  hitSlop={{ top: 8, bottom: 8 }}
  accessibilityLabel="Notifications"
  onChange={({ checked }) => on.set(checked)}
  trackClass="w-9 rounded-full bg-muted p-0.5 checked:bg-primary"
  thumbClass="size-4 rounded-full bg-background checked:translate-x-4"
/>

#Accessibilité

  • Un switch est un seul élément pour les lecteurs d’écran, avec le rôle "switch" et sa valeur (activé ou désactivé) ; la piste et le curseur en font partie. Son libellé est le texte de ses enfants ; renseignez accessibilityLabel lorsqu’il 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, un switch est un point d’arrêt du focus clavier : Tab l’atteint, Espace et Entrée le basculent, et les styles focus: indiquent où se trouve le focus.

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

#Notes sur les plateformes

  • iOS, Android et Mac Catalyst : un appui est un tap qui se termine sur le switch, libellé compris.
  • Ordinateurs de bureau (macOS, Windows, Linux, GPUI) et web : un appui est un clic. Sur macOS et le web, Espace et Entrée basculent aussi le switch qui a le focus clavier.
  • Chaque plateforme dessine ce que l’app style ; le curseur se déplace selon les styles et les transitions de l’app.