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

Éléments

Button

Un bouton est une cible d’appui. Il gère le comportement : les appuis, le retour visuel d’appui via les styles d’état de l’app, les états désactivé et sélectionné, le focus clavier et son rôle pour les lecteurs d’écran. L’apparence appartient à l’app : un Button n’a ni couleurs, ni tailles, ni marges internes, ni libellé propres. Un même composant devient ainsi un bouton principal, un bouton d’icône, un onglet, une bascule de barre d’outils ou la ligne d’une liste, selon les styles et les enfants que vous lui donnez.

el-button.tsx
el-button.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Button, Switch } from "nsh/base";
import { button, buttonText, desktop, Inspector, Panel, size, Stage, ui, wide } from "./palette";

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 App() {
  const presses = signal(0);
  const pressing = signal(false);
  const disabled = signal(false);
  const bold = signal(true);
  const italic = signal(false);
  const last = signal("–");
  const label = () => ({ color: ui().text, fontSize: desktop ? 13 : 15, fontWeight: "600" as const });

  const counter = () => (
    <Button
      onPress={() => {
        presses.update((n) => n + 1);
        last.set(`counter ${presses()}`);
      }}
      onPressDown={() => pressing.set(true)}
      onPressUp={() => pressing.set(false)}
      disabled={disabled()}
      {...button("accent")}
    >
      <Text style={buttonText("accent")}>Pressed {presses()}×</Text>
    </Button>
  );
  const pressState = () => `${pressing() ? "onPressDown: pressing…" : "onPressUp: released"} · disabled ${String(disabled())}`;
  const disabler = () => (
    <Switch
      checked={disabled()}
      onChange={({ checked }) => disabled.set(checked)}
      class={`flex-row items-center gap-2 active:opacity-70 ${desktop && !wide() ? "self-start" : ""}`}
      trackClass={track}
      trackStyle={disabled() ? ui().accentFill : ui().inset}
      thumbClass={thumb}
      thumbStyle={disabled() ? { backgroundColor: ui().onAccent } : ui().raised}
    >
      <Text style={label()}>{wide() ? (disabled() ? "Enable it" : "Disable it") : disabled() ? "Enable the button above" : "Disable the button above"}</Text>
    </Switch>
  );
  const toggle = (name: string, on: () => boolean, flip: () => void, text: () => object, glyph: string) => (
    <Button
      selected={on()}
      onPress={flip}
      accessibilityLabel={name}
      {...button("secondary", { on, align: "flex-start", style: { width: size.control, paddingHorizontal: 0 } })}
    >
      <Text style={{ ...text(), color: buttonText("secondary", { on }).color }}>{glyph}</Text>
    </Button>
  );
  const toggles = () => (
    <>
      {toggle("Bold", bold, () => bold.set(!bold()), () => ({ ...label(), fontWeight: "800" }), "B")}
      {toggle("Italic", italic, () => italic.set(!italic()), () => ({ ...label(), fontStyle: "italic" }), "I")}
    </>
  );
  const caption = (text: string) => <Text style={{ fontSize: 12, color: ui().muted }}>{text}</Text>;

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 12 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>button</Text>
      <Text style={{ color: ui().muted }}>{desktop ? "Click, hover, or Tab to a button and press Space" : "Tap; move your finger away to cancel"}</Text>

      {() =>
        wide() ? (
          <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
            <Stage width={360} gap={24}>
              <View style={{ gap: 8 }}>
                {caption("onPress, onPressDown, onPressUp")}
                <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
                  {counter()}
                  {disabler()}
                </View>
              </View>
              <View style={{ gap: 8 }}>
                {caption("selected: toggles")}
                <View style={{ flexDirection: "row", gap: 8 }}>{toggles()}</View>
              </View>
            </Stage>
            <Inspector>
              <Panel title="press · disabled">
                <Text style={{ fontSize: 13, color: ui().text }}>{pressState()}</Text>
              </Panel>
              <Panel title="selected">
                <Text style={{ fontSize: 13, color: ui().text }}>
                  bold {String(bold())} · italic {String(italic())}
                </Text>
              </Panel>
              <Panel title="last onPress">
                <Text style={{ fontSize: 13, color: ui().up }}>{last()}</Text>
              </Panel>
            </Inspector>
          </View>
        ) : (
          <View style={{ gap: 12 }}>
            {counter()}
            <Text style={{ color: ui().muted, fontSize: 13 }}>{pressState()}</Text>

            {disabler()}

            <Text style={{ color: ui().muted, marginTop: 6 }}>selected: toggles</Text>
            <View style={{ flexDirection: "row", gap: 8 }}>
              {toggles()}
              <Text style={{ color: ui().muted, alignSelf: "center", flex: 1 }}>
                bold {String(bold())} · italic {String(italic())}
              </Text>
            </View>

            <Text style={{ color: ui().up }}>last onPress: {last()}</Text>
          </View>
        )
      }
    </View>
  );
}

mount(App);
use nsh_base::button;
use nsh_core::prelude::*;

fn counter() -> impl IntoElement {
    let count = signal(0_u32);
    button()
        .on_press(move || count.update(|n| *n += 1))
        .self_center()
        .px_5()
        .py_3()
        .rounded_xl()
        .bg(rgb(0x2563eb))
        .active(|s| s.bg(rgb(0x1d4ed8)))
        .child(
            text(move || format!("Pressed {}", count.get()))
                .text_color(white())
                .font_weight(FontWeight::SEMIBOLD),
        )
}

Button provient de nsh/base, le crate nsh-base en Rust. En TSX, son apparence passe par class et style, avec les variantes d’état active:, hover:, focus:, selected: et disabled: (ou les activeStyle, hoverStyle, focusStyle, selectedStyle et disabledStyle correspondants). En Rust, button() en crée un comme view() crée une vue : un setter par prop (.disabled(..), .selected(..)), un écouteur par événement (.on_press(..)), les méthodes de style typées (.px_4(), .bg(..)) pour son apparence, les méthodes d’état (.active(..), .hover(..), .focus(..), .selected_style(..), .disabled_style(..)) pour ses états et .child(..) pour son contenu.

#Props

PropRustTypeDéfautDescription
disabled.disabled(..)booleanfalseAucun appui et aucun retour visuel d’appui ; les styles disabled: s’appliquent et les lecteurs d’écran l’annoncent comme grisé.
selected.selected(..)booleanfalseSélectionné : une bascule activée, l’onglet courant. Les lecteurs d’écran l’annoncent. L’app détient la valeur et la stylise.
selectedStyle.selected_style(|s| ..)StyleProp<ViewStyle>aucunLe style lorsque selected est vrai et que le bouton n’est pas désactivé, sous forme de classes selected:.
accessibilityLabel.accessibility_label(..)stringaucunCe que disent les lecteurs d’écran lorsque le contenu ne suffit pas, par exemple pour une icône.
role.role(..)AccessibilityRole"button"Ce qu’il est pour les lecteurs d’écran lorsqu’aucun composant nsh/base ne correspond à ce contrôle : "link" (il ouvre une URL), …
hitSlop.hit_slop(..)number | Partial<Edges>aucunJusqu’où, hors 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 : du texte, une icône, n’importe quelle disposition.
class, styleméthodes de stylestring, StyleProp<ViewStyle>aucunSon apparence, en totalité.
activeStyle, hoverStyle, focusStyle, disabledStyle.active(|s| ..), .hover(..), .focus(..), .disabled_style(..)StyleProp<ViewStyle>aucunLe style lorsqu’il est pressé, survolé, focalisé ou désactivé, sous forme de classes <state>:. Les états suivants l’emportent sur les précédents ; disabled l’emporte sur tous.

En Rust, chaque prop est un setter qui accepte une valeur, un signal ou une closure ; les props omises gardent leur valeur par défaut :

button()
    .disabled(move || saving.get())
    .accessibility_label("Save")

#Événements

ÉvénementRustDescription
onPress.on_press(|| ..)Le bouton a été pressé : un toucher, un clic, Return ou Espace lorsqu’il a le focus clavier, ou l’activation par un lecteur d’écran.
onPressDown.on_press_down(|| ..)Un appui a commencé sur le bouton.
onPressUp.on_press_up(|| ..)L’appui s’est terminé : relâché, éloigné ou annulé.

onPressDown et onPressUp servent à un retour visuel d’appui qui vous est propre, comme un ressort ou une surbrillance que les styles d’état n’expriment pas. Un bouton désactivé n’en signale aucun.

#Style

L’apparence d’un bouton appartient à votre app : c’est elle qui décide ce qu’est un bouton principal ou secondaire. Une paire de petits composants permet de garder cette décision en un seul endroit :

use nsh_base::button;
use nsh_core::prelude::*;

/// A filled button, darker while pressed.
fn primary(label: &'static str, on_press: impl Fn() + 'static) -> impl IntoElement {
    button()
        .on_press(on_press)
        .items_center()
        .rounded_lg()
        .px_4()
        .py_3()
        .bg(rgb(0x2563eb))
        .active(|s| s.bg(rgb(0x1d4ed8)))
        .disabled_style(|s| s.opacity(0.4))
        .child(text(label).text_color(white()).font_weight(FontWeight::SEMIBOLD))
}

/// An outlined button.
fn secondary(label: &'static str, on_press: impl Fn() + 'static) -> impl IntoElement {
    button()
        .on_press(on_press)
        .items_center()
        .rounded_lg()
        .px_4()
        .py_3()
        .border_1()
        .border_color(rgb(0xd4d4d4))
        .hover(|s| s.bg(rgb(0xfafafa)))
        .active(|s| s.bg(rgb(0xf5f5f5)))
        .child(text(label).text_color(rgb(0x171717)))
}

Les variantes d’état sont appliquées nativement : un appui assombrit le bouton sur le thread principal, sans exécuter de JS ni de Rust. En Rust, les méthodes d’état correspondent aux mêmes variantes : .active(..), .hover(..), .focus(..), .selected_style(..) et .disabled_style(..) suivent les états propres du bouton. Les couleurs de votre thème fonctionnent de la même façon : bg-primary en TSX, theme().primary en Rust (voir Theme tokens).

#Désactivé

Un bouton désactivé ne reçoit aucun appui et ne donne aucun retour visuel, et les lecteurs d’écran l’annoncent comme grisé. Son apparence une fois désactivé vous appartient, avec des classes disabled: ou disabledStyle :

fn save(saving: Signal<bool>) -> impl IntoElement {
    button()
        .disabled(saving)
        .on_press(move || saving.set(true))
        .rounded_lg()
        .px_4()
        .py_3()
        .bg(rgb(0x2563eb))
        .disabled_style(|s| s.opacity(0.4))
        .child(text("Save").text_color(white()))
}

#Une bascule avec selected

selected fait d’un bouton une bascule : le bouton gras d’une barre d’outils, une puce de filtre, l’onglet courant. L’app détient la valeur, l’inverse dans onPress et stylise l’état sélectionné ; les lecteurs d’écran annoncent le bouton comme sélectionné.

fn bold(on: Signal<bool>) -> impl IntoElement {
    button()
        .selected(on)
        .accessibility_label("Bold")
        .on_press(move || on.update(|on| *on = !*on))
        .size(px(40.0))
        .items_center()
        .justify_center()
        .rounded_md()
        .selected_style(|s| s.bg(rgb(0xdbeafe)))
        .child(text("B").font_weight(FontWeight::BOLD))
}

Une rangée d’onglets se construit avec Tabs et un contrôle activé / désactivé avec Switch, issus de nsh/base : ils gèrent le rôle, l’état sélectionné et le clavier propres à leur type, ce que ne fait pas un Button avec role="tab" ou role="switch".

#Une petite cible avec hitSlop

Un bouton d’icône 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 disposition 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.

fn close(open: Signal<bool>) -> impl IntoElement {
    button()
        .hit_slop(EdgeInsets::all(px(12.0)))
        .accessibility_label("Close")
        .on_press(move || open.set(false))
        .size(px(20.0))
        .items_center()
        .justify_center()
        .child(text("×"))
}

#Accessibilité

  • Un bouton est un seul élément pour les lecteurs d’écran, avec le rôle "button" sauf indication contraire de role. Son libellé est son contenu textuel ; donnez accessibilityLabel lorsque le contenu est une icône ou en dit trop peu.
  • disabled et selected sont annoncés comme la plateforme les annonce (VoiceOver, TalkBack, UI Automation, l’arbre d’accessibilité du navigateur).
  • Sur macOS et le web, un bouton est une étape du focus clavier : Tab l’atteint, Return et Espace l’actionnent, et les styles focus: montrent où se trouve le focus.

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

#Notes par plateforme

  • iOS, Android et Mac Catalyst : un appui est un toucher qui se termine sur le bouton ; s’éloigner l’annule, comme avec les boutons du système.
  • Ordinateurs de bureau (macOS, Windows, Linux, GPUI) et web : un appui est un clic. Sur macOS et le web, Return et Espace actionnent aussi le bouton qui a le focus clavier. Les styles hover: s’appliquent avec une souris ou le pointeur d’un trackpad.
  • Chaque plateforme dessine ce que l’app stylise : le retour visuel d’appui est active: / activeStyle ou une animation de votre cru.