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

Elementi

Button

Un button è un bersaglio di pressione. Possiede il comportamento: le pressioni, il feedback di pressione tramite gli stili di stato dell’app, gli stati disabled e selected, il focus da tastiera e il suo ruolo per gli screen reader. L’aspetto è quello dell’app: un Button non ha colori, dimensioni, padding o etichetta propri, quindi un solo componente diventa un button primario, un button con icona, una scheda, un toggle della toolbar o la riga di una lista, a seconda degli stili e dei figli che gli fornisci.

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 proviene da nsh/base, il crate nsh-base in Rust. In TSX il suo aspetto è definito da class e style, con le varianti di stato active:, hover:, focus:, selected: e disabled: (oppure le corrispondenti activeStyle, hoverStyle, focusStyle, selectedStyle e disabledStyle). In Rust, button() ne crea uno come view() crea una view: un setter per ogni prop (.disabled(..), .selected(..)), un listener per ogni evento (.on_press(..)), i metodi di stile tipizzati (.px_4(), .bg(..)) per l’aspetto, i metodi di stato (.active(..), .hover(..), .focus(..), .selected_style(..), .disabled_style(..)) per i suoi stati e .child(..) per il contenuto.

#Props

PropRustTipoPredefinitoDescrizione
disabled.disabled(..)booleanfalseNessuna pressione e nessun feedback di pressione; si applicano gli stili disabled: e gli screen reader lo annunciano come attenuato.
selected.selected(..)booleanfalseSelezionato: un toggle attivo, la scheda corrente. Gli screen reader lo annunciano. Il valore è gestito dall’app, che ne definisce anche lo stile.
selectedStyle.selected_style(|s| ..)StyleProp<ViewStyle>nessunoLo stile mentre selected e non disabled, come classi selected:.
accessibilityLabel.accessibility_label(..)stringnessunoCiò che gli screen reader leggono quando il contenuto non basta, ad esempio per un’icona.
role.role(..)AccessibilityRole"button"Cos’è per gli screen reader quando nessun componente nsh/base è quel controllo: "link" (apre un URL), …
hitSlop.hit_slop(..)number | Partial<Edges>nessunoDi quanto, fuori dai suoi limiti, le pressioni vengono ancora considerate, in punti: un solo numero per tutti i lati, oppure i lati indicati per nome. Come hitSlop di una view.
children.child(..)JSX.ElementnessunoIl contenuto: testo, un’icona, qualsiasi layout.
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoIl suo aspetto, per intero.
activeStyle, hoverStyle, focusStyle, disabledStyle.active(|s| ..), .hover(..), .focus(..), .disabled_style(..)StyleProp<ViewStyle>nessunoLo stile mentre è premuto, in hover, con il focus o disabilitato, come classi <state>:. Gli stati successivi prevalgono; disabled prevale su tutti.

In Rust ogni prop è un setter che accetta un valore, un signal o una closure; le prop omesse mantengono i valori predefiniti:

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

#Eventi

EventoRustDescrizione
onPress.on_press(|| ..)Il button è stato premuto: un tap, un clic, Invio o Spazio mentre ha il focus da tastiera, oppure l’attivazione da parte di uno screen reader.
onPressDown.on_press_down(|| ..)Una pressione è iniziata su di esso.
onPressUp.on_press_up(|| ..)La pressione è terminata: rilasciata, spostata altrove o annullata.

onPressDown e onPressUp servono per un feedback di pressione personalizzato, ad esempio una molla o un’evidenziazione che gli stili di stato non permettono di esprimere. Un button disabilitato non ne segnala nessuno.

#Stile

L’aspetto di un button è quello della tua app: è l’app a decidere cosa sia un button primario o secondario. Una coppia di piccoli componenti mantiene questa decisione in un unico punto:

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

Le varianti di stato vengono applicate in modo nativo: una pressione scurisce il button sul main thread senza eseguire JS o Rust. In Rust i metodi di stato sono le stesse varianti: .active(..), .hover(..), .focus(..), .selected_style(..) e .disabled_style(..) seguono gli stati del button stesso. I colori del tuo tema funzionano allo stesso modo: bg-primary in TSX, theme().primary in Rust (vedi Theme tokens).

#Disabled

Un button disabilitato non accetta pressioni e non dà feedback, e gli screen reader lo annunciano come attenuato. Il suo aspetto quando è disabilitato dipende da te, con le classi disabled: oppure 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()))
}

#Un toggle con selected

selected trasforma un button in un toggle: il button del grassetto di una toolbar, un chip di filtro, la scheda corrente. L’app mantiene il valore, lo inverte in onPress e definisce lo stile dello stato selezionato; gli screen reader annunciano il button come selezionato.

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

Una riga di schede è Tabs e un controllo on / off è Switch, da nsh/base: possiedono il ruolo, lo stato selezionato e la gestione della tastiera del proprio tipo, cosa che un Button con role="tab" o role="switch" non fa.

#Un bersaglio piccolo con hitSlop

Un button con icona può essere più piccolo di un dito. hitSlop fa sì che le pressioni che cadono nelle sue vicinanze vengano considerate, in punti oltre ciascun lato, senza modificarne il layout o l’aspetto: un solo numero per tutti i lati, oppure i lati indicati per nome ({ top: 8, bottom: 8 }), nello stesso modo in cui li accetta hitSlop di una view.

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 button è un unico elemento per gli screen reader, con il ruolo "button" a meno che role non indichi diversamente. La sua etichetta è il contenuto testuale; fornisci accessibilityLabel quando il contenuto è un’icona o dice troppo poco.
  • disabled e selected vengono annunciati come li annuncia la piattaforma (VoiceOver, TalkBack, UI Automation, l’albero di accessibilità del browser).
  • Su macOS e sul web un button è una tappa del focus da tastiera: Tab lo raggiunge, Invio e Spazio lo premono 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 button; spostarsi altrove la annulla, come con i button di sistema.
  • Desktop (macOS, Windows, Linux, GPUI) e web: una pressione è un clic. Su macOS e sul web, Invio e Spazio premono anche il button che ha il focus da tastiera. Gli stili hover: si applicano con il puntatore di un mouse o di un trackpad.
  • Ogni piattaforma disegna ciò che lo stile dell’app definisce: il feedback di pressione è active: / activeStyle oppure un’animazione personalizzata.