onboarding-checklist
見出し + 達成率バーの下に、開始タスクのチェックリストを組み合わせたスタートガイドブロックです。progress / steps / checkbox / button / heading / text / visually-hidden の 7 部品を合成します。Blocks は既存部品の合成例であり、新しい UI 部品は追加しません。
対応表 ID R0173(代表構成、集約差分なし)を参照しています。完了タスクはチェック済み checkbox + 取り消し線ラベルのみを持ち、未完了タスクは説明文と実行ボタンを持ちます。先頭の未完了タスクだけが展開表示され、他の未完了タスクは折りたたまれます。タスク名・説明はすべて架空の文言です。
本 Demo は無 JS の静的表示のみであり、<form> を含みません。
Demo
はじめの設定
5 件中 2 件が完了しました
名前と説明を入力して、最初のプロジェクトを作ります。
チャットツールと連携して、更新情報を受け取れるようにします。
外出先でも進捗を確認できるよう、モバイルアプリを設定します。
使用部品
Rust コード
Rust
use fandhe_frontend_core::{div, text, Node};
use fandhe_frontend_pre_styled_ui::button::{self, ButtonProps};
use fandhe_frontend_pre_styled_ui::checkbox::{self, CheckboxProps, CheckedState};
use fandhe_frontend_pre_styled_ui::fandhe_frontend_headless_ui::progress::Progress;
use fandhe_frontend_pre_styled_ui::fandhe_frontend_headless_ui::steps::Steps;
use fandhe_frontend_pre_styled_ui::heading::{heading, HeadingLevel, HeadingProps};
use fandhe_frontend_pre_styled_ui::progress::{self, ProgressProps};
use fandhe_frontend_pre_styled_ui::steps;
use fandhe_frontend_pre_styled_ui::text::{self as styled_text, TextProps, TextSize, TextVariant};
use fandhe_frontend_pre_styled_ui::visually_hidden;
use fandhe_frontend_pre_styled_ui::{ColorPalette, Orientation, Size};
/// 開始タスク一覧(タイトル・説明)。すべて架空の文言。
const TASKS: [(&str, &str); 5] = [
(
"プロフィールを設定する",
"顔写真と自己紹介を登録すると、チームに自分を知ってもらえます。",
),
(
"チームメンバーを招待する",
"メールアドレスを入力して、一緒に作業するメンバーを招待します。",
),
(
"最初のプロジェクトを作成する",
"名前と説明を入力して、最初のプロジェクトを作ります。",
),
(
"通知先を接続する",
"チャットツールと連携して、更新情報を受け取れるようにします。",
),
(
"モバイルアプリをインストールする",
"外出先でも進捗を確認できるよう、モバイルアプリを設定します。",
),
];
/// 完了済みタスク件数(先頭 `DONE` 件が完了・`DONE` 件目が「次にやること」
/// として展開表示される)。
const DONE: usize = 2;
/// 見出し + 達成率テキスト + 達成率バーの見出し行。
fn header(progress: &Progress) -> Node {
div(
vec![("class", "blocks-onboarding-checklist-header")],
vec![
heading(
HeadingLevel::H3,
&HeadingProps::default(),
vec![],
vec![text("はじめの設定")],
),
styled_text::text(
&TextProps {
variant: TextVariant::Muted,
size: TextSize::Sm,
..TextProps::default()
},
vec![],
vec![text(format!(
"{} 件中 {} 件が完了しました",
TASKS.len(),
DONE
))],
),
progress::root(
progress,
&ProgressProps::default(),
Some(&format!("{} 件中 {} 件完了", TASKS.len(), DONE)),
// `aria-labelledby` の自動配線は headless/styled いずれの層
// の責務でもない(`fandhe_frontend_pre_styled_ui::progress`
// rustdoc「イシュー #2049」節参照)ため呼び出し側で明示する。
vec![("aria-label", "設定の達成率")],
vec![progress.track(vec![], vec![progress::range(progress, vec![])])],
),
],
)
}
/// タスクの完了印(checkbox)。ネイティブ `disabled` で状態を固定する
/// 理由はモジュール冒頭「checkbox をネイティブ `disabled` にする理由」節
/// 参照。
fn task_mark(index: usize, title: &'static str, complete: bool) -> Node {
let props = CheckboxProps {
checked: if complete {
CheckedState::Checked
} else {
CheckedState::Unchecked
},
disabled: true,
..CheckboxProps::default()
};
let name = format!("task-{}", index + 1);
checkbox::root(
Size::Md,
ColorPalette::Accent,
&props,
vec![("data-blocks-onboarding-checklist-task", "")],
vec![
checkbox::hidden_input(&props, &name, "on", vec![]),
checkbox::control(
&props,
vec![],
vec![checkbox::indicator(&props, vec![], vec![])],
),
checkbox::label(&props, vec![], vec![text(title)]),
],
)
}
/// 未完了タスクの説明 + 実行ボタン(`steps::content`。current
/// のみ `open`、他は `hidden`。モジュール冒頭「先頭の未完了タスクだけ
/// 展開」節参照)。実行ボタンの可視テキストは全タスク共通のため、
/// `visually_hidden::root` でタスク名を不可視テキストとして補う。
fn task_body(steps: &Steps, index: usize, title: &'static str, description: &'static str) -> Node {
steps::content(
steps,
index,
vec![("class", "blocks-onboarding-checklist-task-body")],
vec![
styled_text::text(
&TextProps {
size: TextSize::Sm,
..TextProps::default()
},
vec![],
vec![text(description)],
),
button::button(
&ButtonProps {
size: Size::Sm,
// 静的デモ(無 JS)では押しても何も起きないため、checkbox
// と同様にネイティブ `disabled` で操作不能であることを
// 構造的に表現する(Codex #2979 指摘の是正。`steps::trigger`
// を避けた理由と同型)。checkbox と異なり見た目の減衰
// (既定 `disabled_declarations()`)はあえて中和しない。
disabled: true,
..ButtonProps::default()
},
vec![],
vec![
text("開始する"),
visually_hidden::root(vec![], vec![text(format!("({title})"))]),
],
),
],
)
}
/// タスク 1 件分(checkbox + 未完了時のみ本文)。
fn task_item(steps: &Steps, index: usize, title: &'static str, description: &'static str) -> Node {
let complete = index < DONE;
let mut children = vec![task_mark(index, title, complete)];
if !complete {
children.push(task_body(steps, index, title, description));
}
steps::item(steps, index, vec![], children)
}
/// `onboarding-checklist` の Demo 本体。呼び出しごとに同一の `Node` を
/// 返す純関数。
pub fn demo() -> Node {
let steps_state = Steps::new(TASKS.len(), DONE, Orientation::Vertical);
let progress_state = Progress::new(
0.0,
TASKS.len() as f64,
Some(DONE as f64),
Orientation::Horizontal,
);
let items: Vec<Node> = TASKS
.iter()
.enumerate()
.map(|(index, (title, description))| task_item(&steps_state, index, title, description))
.collect();
let steps_root = steps::root(
Size::Md,
ColorPalette::Accent,
&steps_state,
vec![("data-blocks-onboarding-checklist-steps", "")],
vec![steps::list(&steps_state, vec![], items)],
);
div(
vec![("class", "blocks-onboarding-checklist-stack")],
vec![header(&progress_state), steps_root],
)
}原案差分メモ
steps::trigger/indicator/separatorは使いません。タスクの完了印は checkbox が担うため trigger・indicator は二重表現になり、各 item は「checkbox + 本文」の縦積みブロックのため item 間の接続線(separator)は意味を持ちません。- checkbox はすべてネイティブ
disabledにしています。disabledを渡さない構成では、JS ハイドレーションを行わない docs サイトでもラベルクリックでブラウザがcheckedをネイティブに切り替えてしまい、見た目(control/indicator)は SSR 時の状態から追従しないため崩れます。disabledの既定の見た目(半透明・カーソル)は block 側 CSS で中和しています。 - 「先頭の未完了タスクだけ展開」は
Stepsのstep(current)を最初の未完了タスクの index に固定し、steps::contentのopen/hidden切り替えという部品の意味論だけで表現しています。 - 狭幅(コンテナ幅 32rem 未満)では実行ボタンが説明文の下へ折り返ります(
@containerによるコンテナクエリ判定)。
関連情報: Progress / Steps / Checkbox / Button / Heading / Text / Visually Hidden