本頁由機器翻譯,可能有錯誤。 閱讀英文原文

元素

Button

按鈕是一個可按壓的目標。它負責行為:按壓、透過應用程式的狀態樣式提供的按壓回饋、停用與選取狀態、鍵盤焦點,以及供螢幕閱讀器使用的角色。外觀則由應用程式決定:Button 本身沒有顏色、尺寸、內距或標籤,因此同一個元件可以依據你提供的樣式與子元素,變成主要按鈕、圖示按鈕、頁籤、工具列切換鈕或清單中的一列。

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 來自 nsh/base,也就是 Rust 中的 nsh-base crate。在 TSX 中,其外觀由 class 與 style 決定,並可搭配狀態變體 active:、hover:、focus:、selected: 與 disabled:(或對應的 activeStyle、hoverStyle、focusStyle、selectedStyle 與 disabledStyle)。在 Rust 中,button() 建立按鈕的方式就如同 view() 建立檢視:每個屬性對應一個 setter(.disabled(..)、.selected(..)),每個事件對應一個監聽器(.on_press(..)),以具型別的樣式方法(.px_4()、.bg(..))設定外觀,以狀態方法(.active(..)、.hover(..)、.focus(..)、.selected_style(..)、.disabled_style(..))設定各狀態,並以 .child(..) 設定內容。

#Props

PropRust型別預設值說明
disabled.disabled(..)booleanfalse不接受按壓,也沒有按壓回饋;會套用 disabled: 樣式,且螢幕閱讀器會將其朗讀為已淡化(dimmed)。
selected.selected(..)booleanfalse已選取:例如已開啟的切換鈕、目前的頁籤。螢幕閱讀器會朗讀此狀態。其值由應用程式持有並負責設定樣式。
selectedStyle.selected_style(|s| ..)StyleProp<ViewStyle>無當 selected 且未停用時的樣式,以 selected: class 表示。
accessibilityLabel.accessibility_label(..)string無當內容不足以表達時(例如圖示),螢幕閱讀器所朗讀的內容。
role.role(..)AccessibilityRole"button"當沒有任何 nsh/base 元件屬於該控制項時,螢幕閱讀器所識別的用途:"link"(開啟網址)等。
hitSlop.hit_slop(..)number | Partial<Edges>無在邊界之外多遠的按壓仍然算數,單位為 point:可用一個數字套用於所有邊,或分別指定各邊。與檢視的 hitSlop 相同。
children.child(..)JSX.Element無內容:文字、圖示或任何版面配置。
class、style樣式方法string、StyleProp<ViewStyle>無它的外觀,全部都在這裡設定。
activeStyle、hoverStyle、focusStyle、disabledStyle.active(|s| ..)、.hover(..)、.focus(..)、.disabled_style(..)StyleProp<ViewStyle>無按下、懸停、取得焦點或停用時的樣式,以 <state>: class 表示。後面的狀態優先;disabled 優先於所有狀態。

在 Rust 中,每個 prop 都是一個 setter,可接受值、signal 或 closure;未設定的 prop 會保留預設值:

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

#Events

事件Rust說明
onPress.on_press(|| ..)按鈕被按下:包含輕點、點擊、在按鈕擁有鍵盤焦點時按下 Return 或 Space,或是螢幕閱讀器的啟動操作。
onPressDown.on_press_down(|| ..)一次按壓在按鈕上開始。
onPressUp.on_press_up(|| ..)按壓結束:放開、移開或被取消。

onPressDown 與 onPressUp 用於你自行設計的按壓回饋,例如彈簧動畫,或是狀態樣式無法表達的高亮效果。停用的按鈕不會回報其中任何一個事件。

#樣式

按鈕的外觀由你的應用程式決定:由應用程式定義什麼是主要按鈕或次要按鈕。可以用一對小型元件把這項決定集中在同一處:

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

狀態變體以原生方式套用:按壓時按鈕變暗是在主執行緒上完成的,不需要執行任何 JS 或 Rust。在 Rust 中,狀態方法即是同樣的變體:.active(..)、.hover(..)、.focus(..)、.selected_style(..) 與 .disabled_style(..) 會跟隨按鈕自身的狀態。你的主題顏色也以同樣的方式運作:TSX 中使用 bg-primary,Rust 中使用 theme().primary(請參閱 Theme tokens)。

#停用

停用的按鈕不接受按壓,也不提供回饋,且螢幕閱讀器會將其朗讀為已淡化。停用時的外觀由你決定,可使用 disabled: class 或 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()))
}

#使用 selected 建立切換鈕

selected 會讓按鈕成為切換鈕:例如工具列的粗體按鈕、篩選標籤(filter chip)、目前的頁籤。值由應用程式持有,在 onPress 中翻轉,並為選取狀態設定樣式;螢幕閱讀器會將該按鈕朗讀為已選取。

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

一列頁籤應使用 Tabs,開/關控制項則應使用 Switch,兩者皆來自 nsh/base:它們各自負責該類控制項的角色、選取狀態與鍵盤操作,而搭配 role="tab" 或 role="switch" 的 Button 並不具備這些。

#使用 hitSlop 擴大小型目標

圖示按鈕可能比手指還小。hitSlop 會讓落在按鈕附近的按壓也算數,單位為各邊向外延伸的 point,而不會改變其版面配置或外觀:可用一個數字套用於所有邊,或分別指定各邊({ top: 8, bottom: 8 }),就如同 檢視的 hitSlop 所接受的方式。

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

#無障礙

  • 對螢幕閱讀器而言,按鈕是單一元素,其角色為 "button",除非 role 另有指定。它的標籤就是其文字內容;當內容是圖示或所傳達的資訊不足時,請提供 accessibilityLabel。
  • disabled 與 selected 會依各平台自己的方式朗讀(VoiceOver、TalkBack、UI Automation、瀏覽器的無障礙樹)。
  • 在 macOS 與 Web 上,按鈕是鍵盤焦點的停駐點:Tab 可以移到它,Return 與 Space 可以按下它,而 focus: 樣式會顯示焦點所在位置。

關於標籤、角色與朗讀內容的整體說明,請參閱 Accessibility。

#平台注意事項

  • iOS、Android 與 Mac Catalyst:按壓是在按鈕上結束的輕點;移開則會取消按壓,與系統內建按鈕的行為相同。
  • 桌面平台(macOS、Windows、Linux、GPUI)與 Web:按壓即是點擊。在 macOS 與 Web 上,當按鈕擁有鍵盤焦點時,Return 與 Space 同樣可以按下它。hover: 樣式會在滑鼠或觸控板的指標下套用。
  • 每個平台都繪製應用程式所設定的樣式:按壓回饋是 active: / activeStyle,或是你自己設計的動畫。