Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Элементы

Switch

Переключатель включает или выключает настройку, и изменение вступает в силу сразу. Он отвечает за поведение: нажатия, активацию с клавиатуры, состояние disabled, а также роль переключателя и его значение для скринридеров. Внешний вид определяет приложение: у 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, крейта nsh-base в Rust. Значение принадлежит приложению: нажатие сообщает следующее значение через onChange, а переключатель показывает checked, которое приложение передаёт обратно. В Rust switch() создаёт переключатель с сеттером для каждого свойства (.checked(..), .track_style(..)), слушателем для каждого события (.on_change(..)), типизированными методами стилей для самого переключателя и .child(..) для его подписи; стиль части принимает методы состояний (.checked(..), .disabled_style(..)), которые следуют за переключателем.

#Props

СвойствоRustТипПо умолчаниюОписание
checked.checked(..)booleanfalseВключён или выключен. Нажатие сообщает следующее значение через onChange; переключатель показывает значение, которое приложение передаёт обратно.
disabled.disabled(..)booleanfalseБез нажатий и без отклика на нажатие; стили disabled: применяются к переключателю и его частям, а скринридеры сообщают, что он неактивен.
checkedStyle.checked_style(|s| ..)StyleProp<ViewStyle>нетСтиль переключателя, пока он checked и не disabled, в виде классов checked:.
accessibilityLabel.accessibility_label(..)stringнетЧто говорят скринридеры: что включает переключатель, если в его дочерних элементах нет текста, который это сообщает.
trackClass, trackStyle.track_style(|s| ..)string, StyleProp<ViewStyle>нетТрек, часть, вдоль которой движется бегунок.
thumbClass, thumbStyle.thumb_style(|s| ..)string, StyleProp<ViewStyle>нетБегунок внутри трека.
hitSlop.hit_slop(..)number | Partial<Edges>нетНа сколько за пределами границ нажатия всё ещё засчитываются, в пунктах: одно число для всех сторон или отдельные стороны по именам. Как hitSlop у view.
children.child(..)JSX.ElementнетСодержимое после трека: подпись, которую произносят скринридеры.
class, styleметоды стилейstring, StyleProp<ViewStyle>нетСам переключатель: строка вокруг трека и подписи.
activeStyle, hoverStyle, focusStyle, disabledStyle.active(|s| ..), .hover(..), .focus(..), .disabled_style(..)StyleProp<ViewStyle>нетСтиль переключателя, пока он нажат, находится под курсором, в фокусе или disabled, в виде классов <state>:.

#Events

СобытиеRustОписание
onChange.on_change(|checked: &bool| ..)Переключатель нажали: касание, щелчок, Space или Return при фокусе клавиатуры на нём, либо активация скринридером. Сообщает значение, которого запрашивает переключатель, как checked, противоположное имеющемуся у него checked.

Disabled-переключатель ничего не сообщает.

#Parts

ЧастьВнутриСтилизуется черезСостояния
Переключательclass, stylechecked:, disabled:, active:, hover:, focus: (или checkedStyle, disabledStyle, …)
Трекпереключатель, первымtrackClass, trackStylechecked:, disabled: следуют за переключателем
БегуноктрекthumbClass, thumbStylechecked:, disabled: следуют за переключателем
Подписьпереключатель, после трекасобственные элементы (children)group-checked: и другие варианты group-*: с group на class переключателя

Бегунок движется за счёт собственных стилей checked:: смещения (checked:translate-x-5) либо отступа или выравнивания трека (checked:justify-end на треке). Класс transition-* анимирует изменение нативно. Чтобы поместить подпись перед треком, разверните строку: class="flex-row-reverse justify-between".

#Disabled

Disabled-переключатель не принимает нажатий, а скринридеры объявляют его как неактивный. Его классы 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>

#A small target with hitSlop

Переключатель без подписи рядом может быть меньше пальца. hitSlop делает так, что нажатия рядом с ним засчитываются, на указанное число пунктов за каждой стороной, без изменения его раскладки и внешнего вида: одно число для всех сторон или отдельные стороны по именам ({ top: 8, bottom: 8 }), так же как их принимает hitSlop у view.

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

#Accessibility

  • Для скринридеров переключатель является одним элементом с ролью "switch" и своим значением (включён или выключен); трек и бегунок являются его частями. Его подпись это текст его дочерних элементов; укажите accessibilityLabel, если текста нет.
  • disabled объявляется так, как его объявляет платформа (VoiceOver, TalkBack, UI Automation, дерево доступности браузера).
  • В macOS и вебе переключатель является точкой остановки фокуса клавиатуры: до него доходит Tab, Space и Return переключают его, а стили focus: показывают, где находится фокус.

Общие сведения о подписях, ролях и объявлениях см. в разделе Accessibility.

#Platform notes

  • iOS, Android и Mac Catalyst: нажатие это касание, которое заканчивается на переключателе, включая его подпись.
  • Десктопы (macOS, Windows, Linux, GPUI) и веб: нажатие это щелчок. В macOS и вебе Space и Return также переключают переключатель, который находится в фокусе клавиатуры.
  • Каждая платформа рисует то, что стилизует приложение; бегунок движется так, как заданы стили и переходы приложения.