このページは機械翻訳のため、誤りを含む場合があります。 英語の原文を読む

要素

Checkbox

Checkbox は、オプション、リストの項目、またはそのすべてを選択します。チェックあり、チェックなし、または不確定(どちらでもない状態:一部だけが選択されているときの「すべて選択」)のいずれかです。動作として、押下、キーボードによる操作、無効状態、そして画面読み上げ向けの checkbox ロールとその値を担います。見た目はアプリ側が決めます。Checkbox 自体にはボックス、マーク、色、サイズ、ラベルがありません。インジケーター(ボックス)とその内側にあるアプリのマーク、続いて子要素(ラベル)で構成され、アプリは各パートを、checkbox に追従する checked:、indeterminate:、disabled: クラスでスタイル指定します。

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 は Rust の nsh-base クレートである nsh/base に由来します。値はアプリが保持します。押下すると onChange で次の値が通知され、checkbox はアプリが渡し返す checked と indeterminate を表示します。Rust では、checkbox() が各 prop 用のセッター(.checked(..)、.indicator_style(..))、各イベント用のリスナー(.on_change(..))、checkbox 自体用の型付きスタイルメソッド、そしてラベル用の .child(..) を備えたものを生成します。インジケーターのスタイルには、checkbox に追従する状態メソッド(.checked(..)、.indeterminate(..)、.disabled_style(..))を使用します。

#Props

PropRustTypeDefaultDescription
checked.checked(..)booleanfalseチェックされているかどうか。押下すると onChange で次の値が通知されます。checkbox はアプリが渡し返した値を表示します。
indeterminate.indeterminate(..)booleanfalseチェックありでもチェックなしでもない状態です。この状態の間は checked より優先されます。画面読み上げでは「混在」と読み上げられ、押下するとチェックありが要求されます。
disabled.disabled(..)booleanfalse押下も押下フィードバックも行いません。disabled: スタイルが checkbox とそのインジケーターに適用され、画面読み上げでは無効(dimmed)と読み上げられます。
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>なし境界の外側どこまで押下を有効とみなすか(ポイント単位)。すべての辺に共通の数値を 1 つ指定するか、辺ごとに指定します。ビューの hitSlop と同じです。
children.child(..)JSX.Elementなしインジケーターの後ろに置く内容。ラベルで、画面読み上げが読み上げます。
class、styleスタイルメソッドstring、StyleProp<ViewStyle>なしcheckbox 自体。インジケーターとラベルを囲む行です。
activeStyle、hoverStyle、focusStyle、disabledStyle.active(|s| ..)、.hover(..)、.focus(..)、.disabled_style(..)StyleProp<ViewStyle>なし押下中、ホバー中、フォーカス中、無効のときの checkbox のスタイルで、<state>: クラスとして指定します。

#Events

EventRustDescription
onChange.on_change(|checked: &bool| ..)checkbox が押されました。タップ、クリック、キーボードフォーカス中の Space または Return、あるいは画面読み上げによる操作が該当します。checkbox が要求する値を checked として通知します。チェックなしおよび不確定からは true、チェックありからは false です。

無効な checkbox は何も通知しません。

#Parts

PartInsideStyled withStates
checkboxclass、stylechecked:、indeterminate:、disabled:、active:、hover:、focus:(または checkedStyle、indeterminateStyle、disabledStyle など)
インジケーターcheckbox の先頭indicatorClass、indicatorStylechecked:、indeterminate:、disabled: は checkbox に追従します
マークインジケーター独自の要素(indicator)group-checked:、group-indeterminate:、および indicatorClass に group を指定したその他の group-*: バリアント
ラベルcheckbox 内のインジケーターの後ろ独自の要素(children)checkbox の class に group を指定した group-*: バリアント

マークはアプリのコンテンツであるため、アプリが決めたものが表示されます。たとえば、アプリが保持する値に応じて、チェックありのときはチェック、不確定のときはダッシュを選んで表示します。あるいは両方のマークを置き、group-checked: / group-indeterminate: クラスで表示するものを切り替えることもできます。

#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>

#Disabled

無効な checkbox は押下を受け付けず、画面読み上げでは無効(dimmed)と読み上げられます。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>

#A small target with hitSlop

行の選択ボックスのように、隣にラベルがない checkbox は、指より小さくなることがあります。hitSlop を使うと、レイアウトや見た目を変えずに、checkbox の近くで行われた押下を有効にできます。各辺の外側に向けてポイント単位で指定し、すべての辺に共通の数値を 1 つ、または辺ごとの指定({ top: 8, bottom: 8 })を渡せます。これは ビューの hitSlop と同じ形式です。

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

#Accessibility

  • checkbox は画面読み上げにとって 1 つの要素であり、ロールは "checkbox"(または role)、値はチェックあり、チェックなし、または不確定の間は「混在」です。インジケーターとマークはその一部です。ラベルは子要素のテキストです。子要素にテキストがない場合は accessibilityLabel を指定してください。
  • disabled は、各プラットフォームの方式で読み上げられます(VoiceOver、TalkBack、UI Automation、ブラウザーのアクセシビリティツリー)。
  • macOS と Web では、checkbox はキーボードフォーカスの停止位置になります。Tab で移動でき、Space と Return で切り替えられ、focus: スタイルでフォーカスの位置が示されます。

ラベル、ロール、読み上げ全般については Accessibility を参照してください。

#Platform notes

  • iOS、Android、Mac Catalyst:押下は checkbox 上(ラベルを含む)で終わるタップです。
  • デスクトップ(macOS、Windows、Linux、GPUI)と Web:押下はクリックです。macOS と Web では、キーボードフォーカスのある checkbox を Space と Return でも切り替えられます。
  • すべてのプラットフォームで、アプリがスタイル指定したものが描画されます。インジケーター、マーク、およびそれらのトランジションが対象です。