Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

Elementos

Button

Um botão é um alvo de pressão. Ele é responsável pelo comportamento: pressões, feedback de pressão por meio dos estilos de estado do app, os estados desabilitado e selecionado, foco do teclado e seu papel para leitores de tela. A aparência é do app: um Button não tem cores, tamanhos, preenchimentos nem rótulo próprios, então um mesmo componente se torna um botão primário, um botão de ícone, uma aba, um toggle de barra de ferramentas ou a linha de uma lista, dependendo dos estilos e dos filhos que você fornecer.

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 vem de nsh/base, o crate nsh-base em Rust. Em TSX, sua aparência é definida por class e style, com as variantes de estado active:, hover:, focus:, selected: e disabled: (ou os equivalentes activeStyle, hoverStyle, focusStyle, selectedStyle e disabledStyle). Em Rust, button() cria um botão como view() cria uma view: um setter por prop (.disabled(..), .selected(..)), um listener por evento (.on_press(..)), os métodos de estilo tipados (.px_4(), .bg(..)) para a aparência, os métodos de estado (.active(..), .hover(..), .focus(..), .selected_style(..), .disabled_style(..)) para os estados e .child(..) para o conteúdo.

#Props

Prop Rust Tipo Padrão Descrição
disabled .disabled(..) boolean false Sem pressões e sem feedback de pressão; os estilos disabled: se aplicam e os leitores de tela informam que o botão está esmaecido.
selected .selected(..) boolean false Selecionado: um toggle ligado, a aba atual. Os leitores de tela informam isso. O app é responsável pelo valor e pela estilização.
selectedStyle .selected_style(|s| ..) StyleProp<ViewStyle> nenhum O estilo enquanto selected e não desabilitado, como classes selected:.
accessibilityLabel .accessibility_label(..) string nenhum O que os leitores de tela dizem quando o conteúdo não basta, como em um ícone.
role .role(..) AccessibilityRole "button" O que o botão é para os leitores de tela quando nenhum componente nsh/base representa esse controle: "link" (abre uma URL), …
hitSlop .hit_slop(..) number | Partial<Edges> nenhum Até que distância fora de seus limites as pressões ainda contam, em pontos: um número para todos os lados, ou os lados nomeados. Como o hitSlop de uma view.
children .child(..) JSX.Element nenhum O conteúdo: texto, um ícone, qualquer layout.
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum Toda a sua aparência.
activeStyle, hoverStyle, focusStyle, disabledStyle .active(|s| ..), .hover(..), .focus(..), .disabled_style(..) StyleProp<ViewStyle> nenhum O estilo enquanto pressionado, com hover, com foco ou desabilitado, como as classes <state>:. Os estados posteriores prevalecem; disabled prevalece sobre todos.

Em Rust, cada prop é um setter que recebe um valor, um signal ou uma closure; as props omitidas mantêm seus valores padrão:

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

#Eventos

Evento Rust Descrição
onPress .on_press(|| ..) O botão foi pressionado: um toque, um clique, Return ou Space enquanto ele tem o foco do teclado, ou a ativação por um leitor de tela.
onPressDown .on_press_down(|| ..) Uma pressão começou sobre ele.
onPressUp .on_press_up(|| ..) A pressão terminou: foi solta, saiu de cima do botão ou foi cancelada.

onPressDown e onPressUp servem para um feedback de pressão próprio, como uma animação de mola ou um destaque que os estilos de estado não expressam. Um botão desabilitado não reporta nenhum deles.

#Estilização

A aparência de um botão é do seu app: é o app que decide o que é um botão primário ou secundário. Um par de pequenos componentes mantém essa decisão em um só lugar:

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

As variantes de estado são aplicadas nativamente: uma pressão escurece o botão na thread principal, sem executar JS nem Rust. Em Rust, os métodos de estado são as mesmas variantes: .active(..), .hover(..), .focus(..), .selected_style(..) e .disabled_style(..) acompanham os estados do próprio botão. As cores do seu tema funcionam da mesma forma: bg-primary em TSX, theme().primary em Rust (veja Theme tokens).

#Desabilitado

Um botão desabilitado não recebe pressões e não dá feedback, e os leitores de tela o anunciam como esmaecido. A aparência dele quando desabilitado fica a seu critério, com 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()))
}

#Um toggle com selected

selected transforma um botão em um toggle: o botão de negrito de uma barra de ferramentas, um chip de filtro, a aba atual. O app mantém o valor, o alterna em onPress e estiliza o estado selecionado; os leitores de tela anunciam o botão como selecionado.

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

Uma linha de abas é Tabs e um controle de ligado / desligado é Switch, de nsh/base: eles cuidam do papel, do estado selecionado e do teclado do seu tipo, o que um Button com role="tab" ou role="switch" não faz.

#Um alvo pequeno com hitSlop

Um botão de ícone pode ser menor que um dedo. hitSlop faz com que pressões que caem perto dele contem, em pontos além de cada lado, sem alterar seu layout nem sua aparência: um número para todos os lados, ou os lados nomeados ({ top: 8, bottom: 8 }), como o hitSlop de uma view os recebe.

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("×"))
}

#Acessibilidade

  • Um botão é um único elemento para os leitores de tela, com o papel "button", a menos que role diga outra coisa. Seu rótulo é o conteúdo de texto; forneça accessibilityLabel quando o conteúdo for um ícone ou disser pouco.
  • disabled e selected são anunciados como a plataforma os anuncia (VoiceOver, TalkBack, UI Automation, a árvore de acessibilidade do navegador).
  • No macOS e na web, um botão é uma parada do foco do teclado: Tab chega até ele, Return e Space o pressionam, e os estilos focus: mostram onde está o foco.

Veja Accessibility para rótulos, papéis e anúncios em geral.

#Notas de plataforma

  • iOS, Android e Mac Catalyst: uma pressão é um toque que termina sobre o botão; sair de cima dele a cancela, como nos botões do próprio sistema.
  • Desktops (macOS, Windows, Linux, GPUI) e web: uma pressão é um clique. No macOS e na web, Return e Space também pressionam o botão que tem o foco do teclado. Os estilos hover: se aplicam sob o ponteiro de um mouse ou trackpad.
  • Toda plataforma desenha o que o app estiliza: o feedback de pressão é active: / activeStyle ou uma animação sua.