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

元素

Switch

Switch 用來開啟或關閉某項設定,變更會立即生效。它負責行為:按壓、鍵盤啟用、停用狀態,以及供螢幕閱讀器使用的 switch 角色與其值。外觀則由應用程式決定:Switch 本身沒有顏色、尺寸、動態效果或標籤。它由一個軌道(track)與其內的滑塊(thumb)組成,接著是它的子元素(標籤),應用程式為每個部分設定樣式,並使用跟隨 switch 狀態的 checked: 和 disabled: 類別。

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 來自 nsh/base,也就是 Rust 中的 nsh-base crate。值屬於應用程式:一次按壓會以 onChange 回報下一個值,而 switch 會顯示應用程式回傳的 checked。在 Rust 中,switch() 會建立一個 switch,每個屬性都有對應的 setter(.checked(..)、.track_style(..)),每個事件都有對應的監聽器(.on_change(..)),switch 本身使用型別化的樣式方法,其標籤則使用 .child(..);各部分的樣式可使用跟隨 switch 的狀態方法(.checked(..)、.disabled_style(..))。

#屬性

屬性Rust型別預設值說明
checked.checked(..)booleanfalse開啟或關閉。按壓時以 onChange 回報下一個值;switch 會顯示應用程式回傳的值。
disabled.disabled(..)booleanfalse不接受按壓,也沒有按壓回饋;disabled: 樣式會套用於 switch 及其各部分,螢幕閱讀器會表示它呈現變暗狀態。
checkedStyle.checked_style(|s| ..)StyleProp<ViewStyle>無switch 在 checked 且未停用時的樣式,以 checked: 類別表示。
accessibilityLabel.accessibility_label(..)string無螢幕閱讀器所朗讀的內容:當子元素沒有文字可說明時,表示此 switch 開啟的是什麼。
trackClass、trackStyle.track_style(|s| ..)string、StyleProp<ViewStyle>無軌道,即滑塊移動所沿的部分。
thumbClass、thumbStyle.thumb_style(|s| ..)string、StyleProp<ViewStyle>無滑塊,位於軌道內。
hitSlop.hit_slop(..)number | Partial<Edges>無超出邊界多遠的按壓仍然有效,單位為點(points):一個數字代表所有邊,或分別指定各邊。與 view 的 hitSlop 相同。
children.child(..)JSX.Element無軌道之後的內容:標籤,螢幕閱讀器會朗讀它。
class、style樣式方法string、StyleProp<ViewStyle>無switch 本身:軌道與標籤所在的那一列。
activeStyle、hoverStyle、focusStyle、disabledStyle.active(|s| ..)、.hover(..)、.focus(..)、.disabled_style(..)StyleProp<ViewStyle>無switch 在被按壓、懸停、取得焦點或停用時的樣式,以 <state>: 類別表示。

#事件

事件Rust說明
onChange.on_change(|checked: &bool| ..)switch 被按壓:包括輕點、點擊、在取得鍵盤焦點時按下 Space 或 Return,或螢幕閱讀器的啟用操作。它會以 checked 回報 switch 要求的值,也就是與目前 checked 相反的值。

停用的 switch 不會回報任何事件。

#組成部分

部分位置樣式設定方式狀態
switchclass、stylechecked:、disabled:、active:、hover:、focus:(或 checkedStyle、disabledStyle 等)
軌道switch 內,最前面trackClass、trackStylechecked:、disabled: 跟隨 switch
滑塊軌道內thumbClass、thumbStylechecked:、disabled: 跟隨 switch
標籤switch 內,位於軌道之後自己的元素(children)switch 的 class 上帶有 group 的 group-checked: 及其他 group-*: 變體

滑塊透過它自己的 checked: 樣式移動:使用位移(checked:translate-x-5),或使用邊距,或使用軌道的對齊方式(軌道上的 checked:justify-end)。transition-* 類別會以原生方式為變化加上動畫。若要將標籤放在軌道之前,請反轉該列的排列:class="flex-row-reverse justify-between"。

#停用狀態

停用的 switch 不接受按壓,螢幕閱讀器會將它宣告為變暗狀態。它的 disabled: 類別,以及其軌道與滑塊的對應類別,會覆蓋它們的 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>

#使用 hitSlop 的小型目標

沒有標籤的 switch 可能比手指還小。hitSlop 會讓落在它附近的按壓也算數,範圍為每一邊向外延伸的點數,且不會改變它的版面配置或外觀:可以用一個數字代表所有邊,或分別指定各邊({ top: 8, bottom: 8 }),寫法與 view 的 hitSlop 相同。

<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"
/>

#無障礙

  • 對螢幕閱讀器而言,switch 是單一元素,角色為 "switch",並帶有它的值(開啟或關閉);軌道與滑塊都是它的一部分。它的標籤是其子元素的文字;若沒有子元素文字,請提供 accessibilityLabel。
  • disabled 會依各平台的方式宣告(VoiceOver、TalkBack、UI Automation、瀏覽器的無障礙樹)。
  • 在 macOS 與 web 上,switch 是鍵盤焦點的停駐點:可用 Tab 移到它,用 Space 和 Return 切換它,並由 focus: 樣式顯示焦點所在位置。

關於標籤、角色與宣告的一般說明,請參閱無障礙。

#平台說明

  • iOS、Android 與 Mac Catalyst:按壓指的是在 switch 上結束的輕點,包含其標籤。
  • 桌面平台(macOS、Windows、Linux、GPUI)與 web:按壓指的是點擊。在 macOS 與 web 上,Space 和 Return 也會切換取得鍵盤焦點的 switch。
  • 每個平台都會繪製應用程式所設定的樣式;滑塊依照應用程式的樣式與轉場設定移動。