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

العناصر

Radio

تقدّم مكوّنات Radio مجموعة من الخيارات يختار المستخدم منها خيارًا واحدًا فقط: خطة، أو ترتيب فرز، أو سمة. يتولى كل Radio سلوك خيار واحد: الضغطات، والتفعيل بلوحة المفاتيح، وحالة التعطيل، ودور radio وحالته المحددة لقارئات الشاشة. أما RadioGroup الذي يحيط بها فهو المجموعة، وتعلنها قارئات الشاشة كمجموعة واحدة. المظهر يعود إلى التطبيق: لا يملك الـ radio دائرة ولا نقطة ولا ألوانًا ولا أحجامًا ولا تسمية خاصة به. أبناؤه هم مؤشر التطبيق وتسميته، وفئات checked: على الـ radio، أو فئات group-checked: على ما بداخله، تتبع حالته.

el-radio.tsx
el-radio.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Radio, RadioGroup } from "nsh/base";
import { desktop, focusRing, hoverFill, Log, size, Split, ui } from "./palette";

const plans = [
  { id: "starter", label: "Starter", detail: "1 project · 5 GB", price: "Free" },
  { id: "pro", label: "Pro", detail: "Unlimited projects · 100 GB", price: "$12" },
  { id: "team", label: "Team", detail: "Shared workspace · 1 TB", price: "$40", locked: true },
];
const cycles = ["Monthly", "Yearly"];

function App() {
  const plan = signal("pro");
  const cycle = signal("Monthly");
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 3));
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>radio</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click an option, or Tab to a group and move with the arrow keys" : "Tap an option: its label checks it too"}
      </Text>

      <Split gap={14} inspector={<Log title={`onChange({ checked }) · ${plan()}, ${cycle()}`} lines={events()} minHeight={76} />}>
        {/* A segmented choice: each radio is a segment, `checkedStyle` raises the checked one. */}
        <RadioGroup style={{ flexDirection: "row", gap: 4, padding: 3, borderRadius: 10, ...ui().inset, ...(desktop ? { alignSelf: "flex-start" } : {}) }}>
          <Each of={cycles}>
            {(c) => (
              <Radio
                checked={cycle() === c()}
                onChange={({ checked }) => {
                  cycle.set(c());
                  log(`${c()} → ${checked}`);
                }}
                style={
                  desktop
                    ? { height: 26, paddingHorizontal: 14, borderRadius: 7, alignItems: "center", justifyContent: "center" }
                    : { flex: 1, height: 30, borderRadius: 7, alignItems: "center", justifyContent: "center" }
                }
                class="active:opacity-70"
                hoverStyle={cycle() === c() ? undefined : hoverFill()}
                focusStyle={focusRing()}
                checkedStyle={ui().raised}
              >
                <Text style={{ fontSize: 13, fontWeight: "500", color: cycle() === c() ? ui().text : ui().muted }}>{c()}</Text>
              </Radio>
            )}
          </Each>
        </RadioGroup>

        {/* A list of options: a ring with a dot, which `group-checked:` grows in (in place with reduced motion). */}
        <RadioGroup style={{ ...ui().card, borderRadius: 14, overflow: "hidden" }}>
          <Each of={plans}>
            {(item, i) => (
              <>
                {i() > 0 ? <View style={{ height: 1, marginLeft: desktop ? 40 : 48, backgroundColor: ui().line }} /> : null}
                <Radio
                  checked={plan() === item().id}
                  disabled={item().locked}
                  onChange={({ checked }) => {
                    plan.set(item().id);
                    log(`${item().label} → ${checked}`);
                  }}
                  class={`group flex-row items-center gap-3 ${desktop ? "px-3 py-2" : "px-4 py-3"} active:opacity-70 disabled:opacity-40`}
                  hoverStyle={item().locked ? undefined : hoverFill()}
                  focusStyle={focusRing()}
                >
                  <View
                    class="transition-colors motion-reduce:transition-none"
                    style={{
                      width: desktop ? 16 : 20,
                      height: desktop ? 16 : 20,
                      borderRadius: desktop ? 8 : 10,
                      borderWidth: 2,
                      borderColor: plan() === item().id ? ui().accent : ui().faint,
                      alignItems: "center",
                      justifyContent: "center",
                    }}
                  >
                    <View
                      class="opacity-0 scale-50 transition duration-150 ease-out motion-reduce:transition-none group-checked:opacity-100 group-checked:scale-100"
                      style={{ width: desktop ? 8 : 10, height: desktop ? 8 : 10, borderRadius: desktop ? 4 : 5, backgroundColor: ui().accent }}
                    />
                  </View>
                  <View style={{ flex: 1, gap: 2 }}>
                    <Text style={{ fontSize: size.label, fontWeight: "500", color: ui().text }}>{item().label}</Text>
                    <Text style={{ fontSize: size.detail, color: ui().muted }}>{item().detail}</Text>
                  </View>
                  <Text style={{ fontSize: 14, fontWeight: "600", fontVariant: ["tabular-nums"], color: ui().text }}>{item().price}</Text>
                </Radio>
              </>
            )}
          </Each>
        </RadioGroup>
      </Split>
    </View>
  );
}

mount(App);
use nsh_base::{radio, radio_group, theme};
use nsh_core::prelude::*;

fn plans() -> impl IntoElement {
    let plan = signal("basic");
    let option = move |value: &'static str, label: &'static str| {
        let checked = move || plan.get() == value;
        radio()
            .checked(checked)
            .on_change(move |_: &bool| plan.set(value))
            .flex_row()
            .items_center()
            .gap_2()
            .child(
                view()
                    .size(px(20.0))
                    .items_center()
                    .justify_center()
                    .rounded_full()
                    .border_2()
                    .border_color(move || theme().border)
                    .child(
                        view()
                            .size(px(10.0))
                            .rounded_full()
                            .bg(move || theme().primary)
                            .opacity(move || if checked() { 1.0_f32 } else { 0.0 }),
                    ),
            )
            .child(text(label))
    };
    radio_group()
        .gap_3()
        .child(option("basic", "Basic"))
        .child(option("pro", "Pro"))
}

Radio وRadioGroup يأتيان من nsh/base، وهو الـ crate nsh-base في Rust. القيمة للتطبيق: لا تحتفظ المجموعة بأي قيمة. يُخبَر كل radio ما إذا كان محددًا؛ والضغط على radio غير محدد يُبلغ عن onChange({ checked: true })، فيضبط التطبيق قيمته، ويعرض كل radio قيمة checked التي يمررها التطبيق. في Rust، radio() وradio_group() ينشئانهما، مع دالة ضبط لكل خاصية (.checked(..))، ومستمع لكل حدث (.on_change(..))، ودوال التنسيق المطبّعة و.child(..) لمحتواهما.

#الخصائص

#Radio

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

#RadioGroup

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

#الأحداث

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

يبقى الـ radio المحدد محددًا ولا يُبلغ عن شيء، إذ لا يستطيع الـ radio إلغاء تحديد نفسه؛ كما أن الـ radio المعطّل لا يُبلغ عن شيء أيضًا.

#الأجزاء

الجزءداخلالتنسيق بواسطةالحالات
المجموعةRadioGroup: class، style
الـ radioالمجموعةRadio: class، stylechecked:، disabled:، active:، hover:، focus: (أو checkedStyle، disabledStyle، …)
المؤشر والتسميةالـ radioعناصرها الخاصة (children)group-checked: وgroup-disabled: وبقية متغيرات group-*: مع group على class الخاص بالـ radio

المؤشر هو محتوى التطبيق: حلقة بنقطة، أو علامة اختيار، أو بطاقة مميزة. مع group على class الخاص بالـ radio، تُظهر فئات group-checked: فيه النقطة ما دام الـ radio محددًا.

#التعطيل

لا يستقبل الـ radio المعطّل أي ضغطات، وتعلنه قارئات الشاشة على أنه باهت. تُطبَّق فئات disabled: فيه فوق فئات checked:، وتتبعها فئات group-disabled: داخله:

<Radio
  checked={plan() === "team"}
  disabled={!eligible()}
  onChange={() => plan.set("team")}
  class="group flex-row items-center gap-2 disabled:opacity-50"
>
  <View class="size-5 items-center justify-center rounded-full border-2 border-border group-checked:border-primary">
    <View class="size-2.5 rounded-full group-checked:bg-primary" />
  </View>
  <Text>Team</Text>
</Radio>

#هدف صغير مع hitSlop

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

<Radio
  checked={plan() === "team"}
  hitSlop={12}
  accessibilityLabel="Team"
  onChange={() => plan.set("team")}
  class="group size-5 items-center justify-center rounded-full border-2 border-border group-checked:border-primary"
>
  <View class="size-2.5 rounded-full group-checked:bg-primary" />
</Radio>

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

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

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

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

على macOS والويب، حيث يصل Tab إلى الـ views التي تستقبل الضغطات، يتبع RadioGroup نمط مجموعة radio في WAI-ARIA:

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

تتخطى مفاتيح الأسهم أزرار radio المعطّلة؛ ويطلب الـ radio الذي تنتقل إليه أن يُحدَّد عبر onChange، ويحدده التطبيق. في التخطيط من اليمين إلى اليسار يتبادل Left وRight موضعيهما: ينقل Left إلى الـ radio التالي وRight إلى السابق؛ أما Up وDown فيحتفظان بوظيفتيهما. تُظهر أنماط focus: موضع التركيز. أما Radio خارج RadioGroup فهو محطة مستقلة، يصل إليها Tab ويحدده Space وReturn.

#ملاحظات حسب المنصة

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