العناصر
Checkbox
يحدد الـ Checkbox خيارًا، أو عنصرًا من قائمة، أو كل العناصر. وهو إما محدد، أو غير محدد، أو في حالة غير حاسمة (أي لا هذا ولا ذاك: مثل “تحديد الكل” عندما يكون التحديد جزئيًا). وهو يتولى السلوك: الضغطات، والتفعيل بلوحة المفاتيح، وحالة التعطيل، ودور الـ checkbox وقيمته لقارئات الشاشة. أما المظهر فهو من مسؤولية التطبيق: فالعنصر Checkbox ليس له مربع ولا علامة ولا ألوان ولا أحجام ولا تسمية خاصة به. ويتكوّن من مؤشر (المربع) تظهر بداخله علامة التطبيق، ثم أبنائه (التسمية)، ويُنسّق التطبيق كل جزء بفئات checked: وindeterminate: وdisabled: التي تتبع حالة الـ checkbox.
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"))
}import { mount, signal, Text } from "nsh";
import { Checkbox } from "nsh/base";
function App() {
const on = signal(false);
return (
<Checkbox
checked={on()}
onChange={({ checked }) => on.set(checked)}
class="flex-row items-center gap-2"
indicatorClass="size-5 items-center justify-center rounded-md border-2 border-border transition-colors checked:border-primary checked:bg-primary"
indicator={<Text class="text-primary-foreground">{on() ? "✓" : ""}</Text>}
>
<Text>Notify me</Text>
</Checkbox>
);
}
mount(App);يأتي 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(..) | boolean | false | محدد أو غير محدد. تُبلّغ الضغطة بالقيمة التالية عبر onChange؛ ويعرض الـ checkbox القيمة التي يمررها التطبيق إليه. |
indeterminate | .indeterminate(..) | boolean | false | لا محدد ولا غير محدد، ويتقدم على checked ما دامت هذه الحالة قائمة: تعلن قارئات الشاشة أنه مختلط، وتطلب الضغطة القيمة “محدد”. |
disabled | .disabled(..) | boolean | false | لا ضغطات ولا استجابة بصرية للضغط؛ وتنطبق تنسيقات 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 المعطّل بأي شيء.
#الأجزاء
| الجزء | بداخل | يُنسَّق بـ | الحالات |
|---|---|---|---|
| الـ checkbox | class، style | checked:، indeterminate:، disabled:، active:، hover:، focus: (أو checkedStyle، indeterminateStyle، disabledStyle، …) | |
| المؤشر | الـ checkbox، أولًا | indicatorClass، indicatorStyle | checked:، 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 الحائز على تركيز لوحة المفاتيح.
- ترسم كل منصة ما يُنسّقه التطبيق: المؤشر والعلامة وانتقالاتهما.