تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

العناصر

Switch

يقوم الـ Switch بتشغيل إعداد ما أو إيقافه، ويسري التغيير فوراً. وهو المسؤول عن السلوك: الضغطات، والتفعيل عبر لوحة المفاتيح، وحالة التعطيل، ودور switch وقيمته لقارئات الشاشة. أما المظهر فهو من شأن التطبيق: Switch ليس له ألوان أو أحجام أو حركة أو تسمية خاصة به. يتكوّن من track بداخله thumb، ثم عناصره الأبناء (تسمية)، ويُنسّق التطبيق كل جزء بفئات checked: وdisabled: التي تتبع الـ Switch.

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، وهو crate nsh-base في Rust. القيمة من شأن التطبيق: تُبلغ الضغطة عن القيمة التالية عبر onChange، ويعرض الـ Switch قيمة checked التي يمررها التطبيق من جديد. في Rust، ينشئ switch() واحداً منه مع دالة ضبط (setter) لكل خاصية (.checked(..)، .track_style(..))، ومستمع لكل حدث (.on_change(..))، ودوال التنسيق المطبّعة للـ Switch نفسه و.child(..) لتسميته؛ ويقبل تنسيق الجزء دوال الحالة (.checked(..)، .disabled_style(..)) التي تتبع الـ Switch.

#الخصائص

الخاصية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>لا شيءالـ track، وهو الجزء الذي يتحرك الـ thumb على امتداده.
thumbClass، thumbStyle.thumb_style(|s| ..)string، StyleProp<ViewStyle>لا شيءالـ thumb، داخل الـ track.
hitSlop.hit_slop(..)number | Partial<Edges>لا شيءالمسافة خارج حدوده التي تظل الضغطات محتسبة ضمنها، بالنقاط: رقم واحد لكل الجوانب، أو تسمية الجوانب. كما في hitSlop الخاص بالـ view.
children.child(..)JSX.Elementلا شيءالمحتوى بعد الـ track: تسمية تنطق بها قارئات الشاشة.
class، styleدوال التنسيقstring، StyleProp<ViewStyle>لا شيءالـ Switch نفسه: الصف الذي يحيط بالـ track والتسمية.
activeStyle، hoverStyle، focusStyle، disabledStyle.active(|s| ..)، .hover(..)، .focus(..)، .disabled_style(..)StyleProp<ViewStyle>لا شيءنمط الـ Switch أثناء الضغط أو التمرير أو التركيز أو التعطيل، على هيئة فئات <state>:.

#الأحداث

الحدثRustالوصف
onChange.on_change(|checked: &bool| ..)جرى الضغط على الـ Switch: لمسة، أو نقرة، أو Space أو Return أثناء امتلاكه تركيز لوحة المفاتيح، أو تفعيل من قارئ الشاشة. ويُبلغ عن القيمة التي يطلبها الـ Switch بوصفها checked، وهي عكس checked التي يحملها.

لا يُبلغ الـ Switch المعطّل عن شيء.

#الأجزاء

الجزءبداخليُنسَّق بـالحالات
الـ Switchclass، stylechecked:، disabled:، active:، hover:، focus: (أو checkedStyle، disabledStyle، …)
الـ trackالـ Switch، أولاًtrackClass، trackStylechecked:، disabled: تتبع الـ Switch
الـ thumbالـ trackthumbClass، thumbStylechecked:، disabled: تتبع الـ Switch
التسميةالـ Switch، بعد الـ trackعناصرها الخاصة (children)group-checked: وبقية متغيرات group-*: مع group على class الخاص بالـ Switch

يتحرك الـ thumb بأنماط checked: الخاصة به: إزاحة (checked:translate-x-5)، أو هامش أو محاذاة الـ track (checked:justify-end على الـ track). وتحرّك فئة transition-* التغيير بشكل أصلي (native). ولوضع التسمية قبل الـ track، اعكس اتجاه الصف: class="flex-row-reverse justify-between".

#التعطيل

لا يقبل الـ Switch المعطّل أي ضغطات، وتعلنه قارئات الشاشة على أنه معتّم. وتُطبَّق فئات disabled: الخاصة به وبالـ track والـ thumb التابعين له، فوق فئات 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 })، كما يقبلها 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"
/>

#إمكانية الوصول

  • الـ Switch عنصر واحد لقارئات الشاشة، بدور "switch" وقيمته (تشغيل أو إيقاف)؛ والـ track والـ thumb جزءان منه. وتسميته هي نص أبنائه؛ أعطِ accessibilityLabel عندما لا يكون له نص.
  • يُعلَن disabled بالطريقة التي تعلنها بها المنصة (VoiceOver وTalkBack وUI Automation وشجرة إمكانية الوصول في المتصفح).
  • على macOS والويب يكون الـ Switch محطة من محطات تركيز لوحة المفاتيح: يصل إليه Tab، ويبدّله Space وReturn، وتُظهر أنماط focus: موضع التركيز.

راجع إمكانية الوصول للاطلاع على التسميات والأدوار والإعلانات بوجه عام.

#ملاحظات المنصات

  • iOS وAndroid وMac Catalyst: الضغطة هي لمسة تنتهي على الـ Switch، بما في ذلك تسميته.
  • أجهزة سطح المكتب (macOS وWindows وLinux وGPUI) والويب: الضغطة هي نقرة. وعلى macOS والويب، يبدّل Space وReturn أيضاً الـ Switch الذي يملك تركيز لوحة المفاتيح.
  • ترسم كل منصة ما ينسّقه التطبيق؛ ويتحرك الـ thumb وفق ما تقوله أنماط التطبيق وانتقالاته.