Элементы
Checkbox
Чекбокс выбирает вариант, элемент списка или все сразу. Он может быть отмечен, не отмечен или неопределён (ни то ни другое: «выбрать все» при частичном выборе). Он отвечает за поведение: нажатия, активацию с клавиатуры, отключённое состояние, а также роль checkbox и её значение для экранных считывателей. Внешний вид задаёт приложение: у Checkbox нет собственных рамки, отметки, цветов, размеров или подписи. Он состоит из индикатора (рамки) с отметкой приложения внутри, затем из дочерних элементов (подписи), и приложение стилизует каждую часть классами checked:, indeterminate: и disabled:, которые следуют за состоянием чекбокса.
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, крейт nsh-base для Rust. Значение принадлежит приложению: нажатие сообщает следующее значение через onChange, а чекбокс показывает checked и indeterminate, которые приложение передаёт обратно. В Rust checkbox() создаёт чекбокс с сеттером для каждого свойства (.checked(..), .indicator_style(..)), слушателем для каждого события (.on_change(..)), типизированными методами стиля для самого чекбокса и .child(..) для его подписи; стиль индикатора принимает методы состояний (.checked(..), .indeterminate(..), .disabled_style(..)), которые следуют за чекбоксом.
#Свойства
| Свойство | Rust | Тип | По умолчанию | Описание |
|---|---|---|---|---|
checked | .checked(..) | boolean | false | Отмечен или нет. Нажатие сообщает следующее значение через onChange; чекбокс показывает значение, которое приложение передаёт обратно. |
indeterminate | .indeterminate(..) | boolean | false | Ни отмечен, ни не отмечен; приоритетнее checked, пока действует. Экранные считыватели сообщают, что значение смешанное, а нажатие запрашивает отмеченное состояние. |
disabled | .disabled(..) | boolean | false | Нет нажатий и нет отклика на нажатие; стили disabled: применяются к чекбоксу и его индикатору, а экранные считыватели сообщают, что элемент недоступен. |
checkedStyle | .checked_style(|s| ..) | StyleProp<ViewStyle> | нет | Стиль чекбокса в отмеченном и не отключённом состоянии, в виде классов checked:. |
indeterminateStyle | .indeterminate_style(|s| ..) | StyleProp<ViewStyle> | нет | Стиль чекбокса в неопределённом и не отключённом состоянии, в виде классов indeterminate:. |
accessibilityLabel | .accessibility_label(..) | string | нет | Что произносят экранные считыватели: что выбирает чекбокс, если в его дочерних элементах нет текста, который это говорит. |
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> | нет | Сам чекбокс: строка вокруг индикатора и подписи. |
activeStyle, hoverStyle, focusStyle, disabledStyle | .active(|s| ..), .hover(..), .focus(..), .disabled_style(..) | StyleProp<ViewStyle> | нет | Стиль чекбокса в нажатом, наведённом, сфокусированном или отключённом состоянии, в виде классов <state>:. |
#События
| Событие | Rust | Описание |
|---|---|---|
onChange | .on_change(|checked: &bool| ..) | Чекбокс нажат: касание, клик, Space или Return при фокусе клавиатуры на нём либо активация экранным считывателем. Событие сообщает значение, которое запрашивает чекбокс, как checked: true из не отмеченного и из неопределённого состояния, false из отмеченного. |
Отключённый чекбокс ничего не сообщает.
#Части
| Часть | Внутри | Стилизуется через | Состояния |
|---|---|---|---|
| Чекбокс | class, style | checked:, indeterminate:, disabled:, active:, hover:, focus: (или checkedStyle, indeterminateStyle, disabledStyle, …) | |
| Индикатор | чекбокс, первым | indicatorClass, indicatorStyle | checked:, indeterminate:, disabled: следуют за чекбоксом |
| Отметка | индикатор | собственные элементы (indicator) | group-checked:, group-indeterminate: и другие варианты group-*: с group на indicatorClass |
| Подпись | чекбокс, после индикатора | собственные элементы (children) | варианты group-*: с group на class чекбокса |
Отметка является содержимым приложения, поэтому показывает то, что решит приложение: галочку в отмеченном состоянии и тире в неопределённом, выбранные по значению, которое хранит приложение, либо обе отметки, где показанную выбирают классы group-checked: / group-indeterminate:.
#Неопределённое состояние
Чекбокс «выбрать все» неопределён, пока выбраны некоторые элементы. Нажатие запрашивает отмеченное состояние, и приложение выбирает все элементы:
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>
#Отключённое состояние
Отключённый чекбокс не принимает нажатий, а экранные считыватели сообщают, что он недоступен. Его классы 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
Чекбокс без подписи рядом (поле выбора в строке) может быть меньше пальца. 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"(или егоrole) и значением: отмечен, не отмечен или смешанное значение в неопределённом состоянии. Индикатор и отметка входят в его состав. Его подпись это текст его дочерних элементов; задайтеaccessibilityLabel, если текста нет. disabledозвучивается так, как это делает платформа (VoiceOver, TalkBack, UI Automation, дерево доступности браузера).- В macOS и вебе чекбокс является остановкой фокуса клавиатуры: Tab переходит к нему, Space и Return переключают его, а стили
focus:показывают, где находится фокус.
Общие сведения о подписях, ролях и объявлениях см. в разделе Accessibility.
#Примечания по платформам
- iOS, Android и Mac Catalyst: нажатие это касание, закончившееся на чекбоксе, включая его подпись.
- Десктопы (macOS, Windows, Linux, GPUI) и веб: нажатие это клик. В macOS и вебе Space и Return также переключают чекбокс, на котором находится фокус клавиатуры.
- Каждая платформа рисует то, что стилизует приложение: индикатор, отметку и их переходы.