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

العناصر

Toggle

الـ Toggle هو زر يبقى في حالة تشغيل أو إيقاف: الخط العريض في شريط أدوات النص، أو نجمة المفضلة، أو خيار في العرض. Toggle يتولى سلوك واحد منها: الضغطات، والتفعيل بلوحة المفاتيح، وحالة التعطيل، ودور الزر مع حالة الضغط لقارئات الشاشة، التي تعلنه على أنه زر تبديل. أما ToggleGroup حول عدة أزرار فهو شريط أدوات: تعلنه قارئات الشاشة على هذا النحو، وتصل إليه لوحة المفاتيح بوقفة واحدة وتنتقل بين أزرار التبديل فيه بمفاتيح الأسهم. المظهر يعود إلى التطبيق: ليس للـ Toggle ألوان أو أحجام أو أيقونة أو تسمية خاصة به. محتوياته هي أيقونة التطبيق أو تسميته، وتتبع حالته فئات aria-pressed: على الـ Toggle، أو فئات group-aria-pressed: على ما بداخله.

el-toggle.tsx
el-toggle.tsxLive
import { Canvas, mount, signal, Text, View } from "nsh";
import { Toggle, ToggleGroup } from "nsh/base";
import { desktop, focusRing, hoverFill, Log, Split, tint, ui } from "./palette";

type Mark = "bold" | "italic" | "underline";
type Align = "left" | "center" | "right";

const marks: { id: Mark; label: string; glyph: string }[] = [
  { id: "bold", label: "Bold", glyph: "B" },
  { id: "italic", label: "Italic", glyph: "I" },
  { id: "underline", label: "Underline", glyph: "U" },
];
const aligns: { id: Align; label: string }[] = [
  { id: "left", label: "Align left" },
  { id: "center", label: "Align center" },
  { id: "right", label: "Align right" },
];

/** Three lines of a paragraph, set as `align` sets them. */
function Lines(props: { align: Align; color: string }) {
  return (
    <Canvas
      style={{ pointerEvents: "none", width: 16, height: 16 }}
      draw={(cx) => {
        cx.strokeStyle = props.color;
        cx.lineWidth = 1.6;
        cx.lineCap = "round";
        [12, 7, 10].forEach((w, i) => {
          const x = props.align === "center" ? 8 - w / 2 : props.align === "right" ? 14 - w : 2;
          cx.beginPath();
          cx.moveTo(x, 4 + i * 4);
          cx.lineTo(x + w, 4 + i * 4);
          cx.stroke();
        });
      }}
    />
  );
}

function App() {
  const on = signal<Mark[]>(["bold"]);
  const align = signal<Align>("left");
  const starred = signal(false);
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 3));
  const has = (m: Mark) => on().includes(m);
  // A toolbar toggle: bare while off, raised while pressed (`pressedStyle`).
  const look = (pressed: boolean) => ({
    style: { width: desktop ? 28 : 34, height: desktop ? 28 : 34, borderRadius: 7, alignItems: "center", justifyContent: "center" } as const,
    class: "transition-colors active:opacity-70 motion-reduce:transition-none",
    pressedStyle: ui().raised,
    hoverStyle: pressed ? undefined : hoverFill(),
    focusStyle: focusRing(),
  });
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>toggle</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click a toggle, or Tab to the toolbar and move with the arrow keys" : "Tap a toggle to turn it on or off"}
      </Text>

      <Split gap={14} inspector={<Log title={`onChange · ${on().join(", ") || "none"}, ${align()}`} lines={events()} minHeight={76} />}>
        {/* One toolbar: the marks any number at once, the alignment exactly one. */}
        <ToggleGroup style={{ flexDirection: "row", alignItems: "center", alignSelf: "flex-start", gap: 2, padding: 3, borderRadius: 10, ...ui().inset }}>
          <Each of={marks}>
            {(m) => (
              <Toggle
                pressed={has(m().id)}
                accessibilityLabel={m().label}
                onChange={({ pressed }) => {
                  on.set(pressed ? [...on(), m().id] : on().filter((x) => x !== m().id));
                  log(`${m().label}: ${pressed}`);
                }}
                {...look(has(m().id))}
              >
                <Text
                  style={{
                    fontSize: 14,
                    color: has(m().id) ? ui().text : ui().muted,
                    fontWeight: m().id === "bold" ? "800" : "500",
                    fontStyle: m().id === "italic" ? "italic" : "normal",
                    textDecorationLine: m().id === "underline" ? "underline" : "none",
                  }}
                >
                  {m().glyph}
                </Text>
              </Toggle>
            )}
          </Each>
          <View style={{ width: 1, height: 16, marginHorizontal: 4, backgroundColor: ui().line }} />
          <Each of={aligns}>
            {(a) => (
              <Toggle
                pressed={align() === a().id}
                accessibilityLabel={a().label}
                onChange={({ pressed }) => {
                  // Exactly one: the pressed alignment stays pressed.
                  if (pressed) align.set(a().id);
                  log(`${a().label}: ${pressed}`);
                }}
                {...look(align() === a().id)}
              >
                <Lines align={a().id} color={align() === a().id ? ui().text : ui().muted} />
              </Toggle>
            )}
          </Each>
        </ToggleGroup>

        {/* The text the toolbar formats: the app reads its own values. */}
        <View style={{ ...ui().card, borderRadius: 14, padding: 14 }}>
          <Text
            style={{
              fontSize: 15,
              lineHeight: 22,
              color: ui().text,
              fontWeight: has("bold") ? "700" : "400",
              fontStyle: has("italic") ? "italic" : "normal",
              textDecorationLine: has("underline") ? "underline" : "none",
              textAlign: align(),
            }}
          >
            Every toggle reports the value it asks for; the app passes it back.
          </Text>
        </View>

        {/* A toggle on its own, with a label: `aria-pressed:` tints it. */}
        <Toggle
          pressed={starred()}
          onChange={({ pressed }) => {
            starred.set(pressed);
            log(`Star: ${pressed}`);
          }}
          class="transition-colors active:opacity-70 motion-reduce:transition-none"
          style={{ flexDirection: "row", alignItems: "center", alignSelf: "flex-start", gap: 6, height: desktop ? 28 : 34, paddingHorizontal: 12, borderRadius: 8, borderWidth: 1, borderColor: ui().line }}
          pressedStyle={{ backgroundColor: tint(ui().accent, 0.16), borderColor: tint(ui().accent, 0.4) }}
          hoverStyle={starred() ? undefined : hoverFill()}
          focusStyle={focusRing()}
        >
          <Text style={{ fontSize: 14, color: starred() ? ui().accent : ui().muted }}>{starred() ? "★" : "☆"}</Text>
          <Text style={{ fontSize: 13, fontWeight: "500", color: starred() ? ui().accent : ui().text }}>{starred() ? "Starred" : "Star"}</Text>
        </Toggle>
      </Split>
    </View>
  );
}

mount(App);
use nsh_base::{theme, toggle, toggle_group};
use nsh_core::prelude::*;

fn formatting() -> impl IntoElement {
    let bold = signal(true);
    let italic = signal(false);
    let item = move |label: &'static str, on: Signal<bool>| {
        toggle()
            .pressed(on)
            .accessibility_label(label)
            .on_change(move |pressed: &bool| on.set(*pressed))
            .size_8()
            .items_center()
            .justify_center()
            .rounded(move || theme().radius)
            .pressed_style(|s| s.bg(move || theme().accent))
            .child(text(&label[..1]).text_sm().text_color(move || {
                if on.get() { theme().accent_foreground } else { theme().muted_foreground }
            }))
    };
    toggle_group()
        .flex_row()
        .self_start()
        .gap_1()
        .child(item("Bold", bold))
        .child(item("Italic", italic))
}

Toggle وToggleGroup يأتيان من nsh/base، وهي الحزمة nsh-base في Rust. القيمة تخص التطبيق: يُخبَر كل Toggle إن كان مضغوطًا، وتُبلّغ الضغطة بالقيمة التي تطلبها عبر onChange، ويعرض الـ Toggle ما pressed الذي يمرّره التطبيق إليه. المجموعة أيضًا لا تحتفظ بأي قيمة، لذا فعدد أزرار التبديل التي يمكن ضغطها يعود إلى التطبيق كذلك: أي عدد (عريض ومائل ومسطّر)، أو واحد بالضبط (محاذاة، مبدّل عروض)، حيث يضغط التطبيق الزر المطلوب ويحرر الباقي ويُبقي المضغوط مضغوطًا. في Rust يُنشئ toggle() وtoggle_group() هذه العناصر، مع دالة تعيين لكل خاصية (.pressed(..))، ومستمع لكل حدث (.on_change(..))، ودوال الأنماط المطبوعة، و.child(..) لمحتواها.

#الخصائص

#Toggle

الخاصيةRustالنوعالافتراضيالوصف
pressed.pressed(..)booleanfalseمفعّل أو غير مفعّل: تقول قارئات الشاشة إنه مضغوط، وتُطبَّق أنماط aria-pressed:. تُبلّغ الضغطة بالقيمة الأخرى عبر onChange؛ ويعرض الـ Toggle القيمة التي يمرّرها التطبيق.
disabled.disabled(..)booleanfalseلا ضغطات ولا استجابة للضغط؛ وتُطبَّق أنماط disabled:، وتقول قارئات الشاشة إنه باهت (معطّل). الـ Toggle داخل ToggleGroup معطّلة يكون معطّلًا أيضًا.
pressedStyle.pressed_style(|s| ..)StyleProp<ViewStyle>لا شيءنمط الـ Toggle أثناء الضغط وعدم التعطيل، على شكل فئات aria-pressed:.
accessibilityLabel.accessibility_label(..)stringلا شيءما تقوله قارئات الشاشة: ما الذي يفعّله الـ Toggle، حين لا يحتوي محتواه على نص يقول ذلك (أيقونة).
hitSlop.hit_slop(..)number | Partial<Edges>لا شيءإلى أي مدى خارج حدوده تُحتسب الضغطات، بالنقاط: رقم واحد لكل الجوانب، أو الجوانب مسمّاة. مثل hitSlop الخاص بالعرض.
children.child(..)JSX.Elementلا شيءأيقونة التطبيق أو تسميته، وهو ما تقوله قارئات الشاشة.
class، styleدوال الأنماطstring، StyleProp<ViewStyle>لا شيءالـ Toggle نفسه: السطح المحيط بمحتواه.
activeStyle، hoverStyle، focusStyle، disabledStyle.active(|s| ..)، .hover(..)، .focus(..)، .disabled_style(..)StyleProp<ViewStyle>لا شيءنمط الـ Toggle أثناء ضغطة جارية، أو تمرير المؤشر، أو التركيز، أو التعطيل، على شكل فئات <state>:.

#ToggleGroup

الخاصيةRustالنوعالافتراضيالوصف
disabled.disabled(..)booleanfalseكل Toggle في المجموعة معطّل، أيًّا كانت قيمة disabled الخاصة به.
children.child(..)JSX.Elementلا شيءأزرار التبديل في المجموعة، وأي شيء يضعه التطبيق بينها (فاصل).
class، styleدوال الأنماطstring، StyleProp<ViewStyle>لا شيءتخطيط المجموعة ومظهرها: عمود افتراضيًا، وflex-row للصف.

#الأحداث

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

مفتاح السهم في ToggleGroup ينقل تركيز لوحة المفاتيح فقط: ولا يضغط شيئًا. الـ Toggle المعطّل لا يبلّغ عن شيء.

#الأجزاء

الجزءداخليُنسَّق بواسطةالحالات
المجموعةToggleGroup: class، style
الـ Toggleالمجموعة، أو في أي مكانToggle: class، stylearia-pressed:، disabled:، active:، hover:، focus: (أو pressedStyle، disabledStyle، …)
الأيقونة أو التسميةالـ Toggleعناصرها الخاصة (children)group-aria-pressed:، group-disabled: وسائر متغيرات group-*: مع group على class الخاص بالـ Toggle

aria-pressed: تعني أن الـ Toggle مفعّل؛ وactive: تعني ضغطة جارية، سواء كان مفعّلًا أم لا. أظهر الـ Toggle المضغوط عبر سطحه نفسه: تعبئة، أو حبة مرتفعة، أو أيقونة أقوى (انظر أدلة التصميم).

#Segmented

المجموعة التي تكون أزرار التبديل فيها متصلة ويُضغط واحد منها في كل مرة هي عنصر تحكم مقسّم (مبدّل عروض): يحتفظ التطبيق بقيمة واحدة ويضغط الـ Toggle الذي يحمل اسمها. المظهر المتصل هو فئات التطبيق:

<ToggleGroup class="flex-row self-start">
  <Each of={["grid", "list"]}>
    {(value) => (
      <Toggle
        pressed={view() === value()}
        accessibilityLabel={`${value()} view`}
        onChange={({ pressed }) => pressed && view.set(value())}
        class="-ml-px h-8 px-3 border border-border first:ml-0 first:rounded-l-(--radius) last:rounded-r-(--radius) aria-pressed:bg-accent"
      >
        <Text class="text-sm">{value()}</Text>
      </Toggle>
    )}
  </Each>
</ToggleGroup>

#معطّل

الـ Toggle المعطّل لا يقبل ضغطات، وتعلنه قارئات الشاشة على أنه باهت. تُطبَّق فئات disabled: الخاصة به فوق فئات aria-pressed:، وتتبعها فئات group-disabled: بداخله. disabled على ToggleGroup يعطّل كل Toggle فيها، أي شريط أدوات يقفله التطبيق:

<ToggleGroup disabled={readonly()} class="flex-row gap-1">
  <Toggle pressed={bold()} onChange={({ pressed }) => bold.set(pressed)} class="size-8 disabled:opacity-50">
    <Text>B</Text>
  </Toggle>
</ToggleGroup>

#هدف صغير مع hitSlop

قد يكون الـ Toggle ذو الأيقونة أصغر من إصبع. hitSlop تجعل الضغطات التي تقع بالقرب منه تُحتسب، بالنقاط بعد كل جانب، دون تغيير تخطيطه أو مظهره: رقم واحد لكل الجوانب، أو الجوانب مسمّاة ({ top: 8, bottom: 8 })، كما يقبلها hitSlop الخاص بالعرض.

<Toggle
  pressed={favorite()}
  hitSlop={12}
  accessibilityLabel="Favorite"
  onChange={({ pressed }) => favorite.set(pressed)}
  class="size-5 items-center justify-center"
>
  <Text>★</Text>
</Toggle>

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

  • الـ Toggle عنصر واحد لقارئات الشاشة، بالدور "button" وحالة الضغط: زر تبديل. تسميته هي نص محتوياته، أو accessibilityLabel.
  • ToggleGroup لها الدور "toolbar": تسمع قارئات الشاشة أزرار التبديل بداخلها على أنها شريط أدوات واحد، مع بقاء كل منها عنصرًا مستقلًا.
  • disabled يُعلَن بالطريقة التي تعلنه بها المنصة (VoiceOver وTalkBack وUI Automation وشجرة إمكانية الوصول في المتصفح).
  • على macOS والويب تكون المجموعة وقفة واحدة في تركيز لوحة المفاتيح، وتنقل مفاتيح الأسهم بين أزرار التبديل فيها (أدناه).

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

#لوحة المفاتيح

على macOS والويب، حيث يصل Tab إلى العروض التي تقبل الضغطات، تتبع ToggleGroup نمط شريط الأدوات في WAI-ARIA:

المفتاحما يفعله
Tab، Shift+Tabيدخل المجموعة عند أول Toggle مضغوط (أو أول Toggle غير معطّل إن لم يكن أي منها مضغوطًا)، ويغادرها بخطوة واحدة.
Right، Downينقل التركيز إلى الـ Toggle التالي، ومن الأخير إلى الأول.
Left، Upينقل التركيز إلى الـ Toggle السابق، ومن الأول إلى الأخير.
Home، Endينقل التركيز إلى أول Toggle أو آخره.
Space، Returnيضغط الـ Toggle الذي عليه التركيز.

تتخطى مفاتيح الأسهم أزرار التبديل المعطّلة ولا تضغط شيئًا: لا يتغير الـ Toggle إلا عند ضغطه. يعمل زوجا الأسهم كلاهما أيًّا كان تخطيط المجموعة، وهو من فئات التطبيق. في التخطيط من اليمين إلى اليسار يتبادل Left وRight موضعيهما: ينتقل Left إلى الـ Toggle التالي وRight إلى السابق؛ أما Up وDown وHome وEnd فتبقى كما هي. تُظهر أنماط focus: موضع التركيز. وToggle خارج ToggleGroup وقفة مستقلة بذاتها.

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

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