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

العناصر

Checkbox

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

el-checkbox.tsx
el-checkbox.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Checkbox } from "nsh/base";
import { CheckMark, desktop, focusRing, hoverFill, Log, Panel, size, Split, ui } from "./palette";

// The parts: geometry and motion in classes (`disabled:` dims the row,
// following the checkbox; `group-checked:` / `group-indeterminate:` fade in
// the mark of the indicator's state, both kept mounted so each fades out
// too, and a check turning into a dash crosses over), colors from the
// palette by the value the app owns; the pointer's hover and the keyboard's
// focus ring from the palette too. Nothing moves with reduced motion.
const row = `flex-row items-center gap-3 ${desktop ? "px-3 py-2" : "px-4 py-3"} active:opacity-70 disabled:opacity-40`;
const indicator = "group size-5 items-center justify-center rounded-md border transition-colors motion-reduce:transition-none";

/** The indicator's surface: the accent while checked or indeterminate. */
const box = (on: boolean) => (on ? ui().accentFill : { ...ui().inset, borderColor: ui().faint });

/** A mark over the box, centered: hidden and a little smaller until its state's `group-*:` classes show it. */
const layer = "absolute inset-0 items-center justify-center opacity-0 scale-75 transition duration-150 ease-out motion-reduce:transition-none";

/** A check while checked, a dash while indeterminate. */
const mark = () => (
  <>
    <View class={`${layer} group-checked:opacity-100 group-checked:scale-100`}>
      <CheckMark size={14} weight={2.5} tint={ui().onAccent} />
    </View>
    <View class={`${layer} group-indeterminate:opacity-100 group-indeterminate:scale-100`}>
      <View class="h-0.5 w-2.5 rounded-full" style={{ backgroundColor: ui().onAccent }} />
    </View>
  </>
);

const items = [
  { id: "photos", label: "Photos", detail: "2,418 items · 6.1 GB" },
  { id: "documents", label: "Documents", detail: "312 files · 840 MB" },
  { id: "messages", label: "Messages", detail: "Attachments included · 1.2 GB" },
];

function Label(props: { label: string; detail: string; strong?: boolean }) {
  return (
    <View style={{ flex: 1, gap: 2 }}>
      <Text style={{ fontSize: size.label, fontWeight: props.strong ? "600" : "500", color: ui().text }}>{props.label}</Text>
      <Text style={{ fontSize: size.detail, color: ui().muted }}>{props.detail}</Text>
    </View>
  );
}

function App() {
  const selected = signal<string[]>(["photos"]);
  const events = signal<string[]>([]);
  const log = (e: string) => events.set([e, ...events()].slice(0, 3));
  const all = () => selected().length === items.length;
  const some = () => selected().length > 0 && !all();
  const line = () => <View style={{ height: 1, marginLeft: desktop ? 44 : 48, 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 }}>checkbox</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click a row, or Tab to a checkbox and press Space" : "Tap a row: the label toggles it too"}
      </Text>

      <Split
        gap={14}
        inspector={
          <>
            <Panel title="checked · indeterminate">
              <Text style={{ fontSize: 13, color: ui().text }}>
                Back up everything: {all() ? "checked" : some() ? "indeterminate" : "unchecked"} · [{selected().join(", ")}]
              </Text>
            </Panel>
            <Log title="onChange({ checked })" lines={events()} minHeight={92} />
          </>
        }
      >
        <View style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}>
          <Checkbox
            checked={all()}
            indeterminate={some()}
            onChange={({ checked }) => {
              selected.set(checked ? items.map((i) => i.id) : []);
              log(`Back up everything → ${checked}`);
            }}
            class={row}
            hoverStyle={hoverFill()}
            focusStyle={focusRing()}
            indicatorClass={indicator}
            indicatorStyle={box(all() || some())}
            indicator={mark()}
          >
            <Label label="Back up everything" detail={`${selected().length} of ${items.length} selected`} strong />
          </Checkbox>
          <Each of={items}>
            {(item) => (
              <>
                {line()}
                <Checkbox
                  checked={selected().includes(item().id)}
                  onChange={({ checked }) => {
                    selected.set(checked ? [...selected(), item().id] : selected().filter((id) => id !== item().id));
                    log(`${item().label} → ${checked}`);
                  }}
                  class={row}
                  hoverStyle={hoverFill()}
                  focusStyle={focusRing()}
                  indicatorClass={indicator}
                  indicatorStyle={box(selected().includes(item().id))}
                  indicator={mark()}
                >
                  <Label label={item().label} detail={item().detail} />
                </Checkbox>
              </>
            )}
          </Each>
        </View>

        <View style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}>
          <Checkbox checked disabled class={row} indicatorClass={indicator} indicatorStyle={box(true)} indicator={mark()}>
            <Label label="System settings" detail="Always backed up" />
          </Checkbox>
        </View>
      </Split>
    </View>
  );
}

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

fn notify() -> impl IntoElement {
    let on = signal(false);
    checkbox()
        .checked(on)
        .on_change(move |next: &bool| on.set(*next))
        .flex_row()
        .items_center()
        .gap_2()
        .indicator_style(|s| {
            s.size(px(20.0))
                .items_center()
                .justify_center()
                .rounded_md()
                .border_2()
                .border_color(move || theme().border)
                .transition_colors()
                .checked(|s| s.bg(move || theme().primary).border_color(move || theme().primary))
        })
        .indicator(text(move || if on.get() { "✓" } else { "" }).text_color(move || theme().primary_foreground))
        .child(text("Notify me"))
}

يأتي Checkbox من nsh/base، وهي الـ crate nsh-base في Rust. والقيمة ملك للتطبيق: فالضغطة تُبلّغ بالقيمة التالية عبر onChange، ويعرض الـ checkbox القيمتين checked وindeterminate اللتين يمررهما التطبيق إليه. في Rust، ينشئ checkbox() عنصرًا بدالة ضبط لكل خاصية (.checked(..)، .indicator_style(..))، ومستمعًا لكل حدث (.on_change(..))، ودوال التنسيق المطبوعة للـ checkbox نفسه، و.child(..) لتسميته؛ أما تنسيق المؤشر فيقبل دوال الحالات (.checked(..)، .indeterminate(..)، .disabled_style(..)) التي تتبع حالة الـ checkbox.

#الخصائص

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

#الأحداث

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

لا يُبلّغ الـ checkbox المعطّل بأي شيء.

#الأجزاء

الجزءبداخليُنسَّق بـالحالات
الـ checkboxclass، stylechecked:، indeterminate:، disabled:، active:، hover:، focus: (أو checkedStyle، indeterminateStyle، disabledStyle، …)
المؤشرالـ checkbox، أولًاindicatorClass، indicatorStylechecked:، indeterminate:، disabled: تتبع الـ checkbox
العلامةالمؤشرعناصرها الخاصة (indicator)group-checked:، group-indeterminate: وبقية متغيرات group-*: مع group على indicatorClass
التسميةالـ checkbox، بعد المؤشرعناصرها الخاصة (children)متغيرات group-*: مع group على class الخاص بالـ checkbox

العلامة محتوى يقرره التطبيق، فهي تعرض ما يقرره: إشارة صح عند التحديد وشرطة عند الحالة غير الحاسمة، تُختاران من القيمة التي يحتفظ بها التطبيق، أو تُعرض العلامتان معًا مع فئات group-checked: / group-indeterminate: التي تختار الظاهرة منهما.

#الحالة غير الحاسمة

يكون الـ checkbox الخاص بـ “تحديد الكل” في حالة غير حاسمة عندما تكون بعض العناصر فقط محددة. تطلب الضغطة القيمة “محدد”، فيحدد التطبيق كل العناصر:

const selected = signal<string[]>([]);
const all = () => selected().length === items.length;
const some = () => selected().length > 0 && !all();

<Checkbox
  checked={all()}
  indeterminate={some()}
  onChange={({ checked }) => selected.set(checked ? items.map((i) => i.id) : [])}
  class="flex-row items-center gap-2"
  indicatorClass="group size-5 items-center justify-center rounded-md border-2 border-border checked:border-primary checked:bg-primary indeterminate:border-primary indeterminate:bg-primary"
  indicator={
    <>
      <Text class="hidden text-primary-foreground group-checked:flex">✓</Text>
      <View class="hidden h-0.5 w-2.5 bg-primary-foreground group-indeterminate:flex" />
    </>
  }
>
  <Text>Select all</Text>
</Checkbox>

#التعطيل

لا يقبل الـ checkbox المعطّل أي ضغطات، وتعلن قارئات الشاشة أنه معتّم. تنطبق فئات disabled: الخاصة به وبمؤشره، وتتقدم على فئات checked: وindeterminate::

<Checkbox
  checked={on()}
  disabled={locked()}
  onChange={({ checked }) => on.set(checked)}
  class="flex-row items-center gap-2 disabled:opacity-50"
  indicatorClass="size-5 rounded-md border-2 border-border checked:border-primary checked:bg-primary disabled:border-border disabled:bg-muted"
>
  <Text>Share usage data</Text>
</Checkbox>

#هدف صغير مع hitSlop

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

<Checkbox
  checked={selected()}
  hitSlop={12}
  accessibilityLabel="Select row"
  onChange={({ checked }) => selected.set(checked)}
  indicatorClass="size-4 rounded border border-border checked:bg-primary"
/>

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

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

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

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

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