order-history-panels
注文ごとに枠付きパネルを縦に積む注文履歴ブロックです。主参照 R1116(サマリ帯 + 商品行の代表構成)を軸にしています。各パネル上部は注文見出し(<h3>)+ サマリ帯(注文番号・注文日・合計 + 「注文を見る」「請求書を見る」ボタン・三点メニュー)、下部は商品見出し(<h4>)を含む商品行(画像・名称・価格・説明・配送状況の状態表示 + 商品名入りのアクセシブルネームを持つ「商品を見る」「再購入」「類似品を見る」の操作群)です。card / data-list / button / menu / image / text / heading / link / separator / icon の 10 部品を合成します。Blocks は既存部品の合成例であり、新しい UI 部品は追加しません。
本 Demo は無 JS の静的表示のみであり、<form> を含みません。注文処理・決済・送信先は一切持たず、サマリ帯・商品行のボタンと三点メニューはいずれも disabled で固定しています(押しても何も起きない要素を操作可能に見せないため)。
コンテナ幅 40rem 未満では「注文を見る」「請求書を見る」ボタンと注文日の行を隠し、同じラベルの項目を持つ三点メニューのみを見せます。40rem 以上では逆にボタン・注文日を見せてメニューを隠します。メニューはこの静的 Demo では開閉しないため、狭幅時に実際に操作できる手段ではありません。
注文番号・日付・商品名・価格はすべて架空のデータであり、実在の人物・企業・商品とは無関係です。商品画像はビルド時生成の同梱プレースホルダー SVG です。
Demo
注文 FD-3102-220
- 注文番号
- FD-3102-220
- 注文日
- 2026-09-18
- 合計
- ¥9,600
使用部品
Rust コード
use crate::blocks::dummy_assets;
use fandhe_frontend_core::{div, el, text, Node};
use fandhe_frontend_pre_styled_ui::button::{button, ButtonProps, ButtonVariant};
use fandhe_frontend_pre_styled_ui::card::{self, CardProps};
use fandhe_frontend_pre_styled_ui::data_list::{self, DataListOrientation, DataListProps};
use fandhe_frontend_pre_styled_ui::fandhe_frontend_headless_ui::state::OpenState;
use fandhe_frontend_pre_styled_ui::heading::{heading, HeadingLevel, HeadingProps};
use fandhe_frontend_pre_styled_ui::icon::{icon, IconProps};
use fandhe_frontend_pre_styled_ui::image::{image, ImageFit, ImageProps, ImageShape};
use fandhe_frontend_pre_styled_ui::link::{self, LinkProps};
use fandhe_frontend_pre_styled_ui::menu;
use fandhe_frontend_pre_styled_ui::recipe::Size;
use fandhe_frontend_pre_styled_ui::separator::{separator, SeparatorProps};
use fandhe_frontend_pre_styled_ui::text::{self as styled_text, TextProps, TextSize, TextVariant};
/// 配送状況の 3 パターン(R1118)。アイコン選択とフック値の供給にのみ使う
/// (モジュール冒頭「配送状況の 3 パターン」節参照)。
#[derive(Clone, Copy)]
enum Delivery {
Delivered,
Shipping,
Preparing,
}
impl Delivery {
/// `data-blocks-order-history-panels-status` へ渡すフック値。
const fn hook_value(self) -> &'static str {
match self {
Delivery::Delivered => "delivered",
Delivery::Shipping => "shipping",
Delivery::Preparing => "preparing",
}
}
}
/// 商品 1 件分のダミーデータ。
struct Item {
name: &'static str,
description: &'static str,
price: &'static str,
href: &'static str,
delivery: Delivery,
status: &'static str,
}
/// 注文 1 件分のダミーデータ。[`menu_id`]/[`trigger_id`] は
/// `menu::content`/`menu::trigger` を紐づける注文固有の一意 id(モジュール
/// 冒頭「`menu` の id をページ内で一意にする理由」節参照)。
struct Order {
number: &'static str,
date: &'static str,
total: &'static str,
menu_id: &'static str,
trigger_id: &'static str,
items: &'static [Item],
}
const ORDERS: [Order; 2] = [
Order {
number: "FD-3102-220",
date: "2026-09-18",
total: "¥9,600",
menu_id: "blocks-order-history-panels-menu-0",
trigger_id: "blocks-order-history-panels-menu-trigger-0",
items: &[
Item {
name: "ノイズキャンセリングイヤホン",
description: "ブラック / Bluetooth 5.3",
price: "¥8,200",
href: "https://example.com/products/noise-cancelling-earbuds",
delivery: Delivery::Delivered,
status: "配達済み(2026-09-20)",
},
Item {
name: "USB-C 急速充電ケーブル 1m",
description: "グレー",
price: "¥1,400",
href: "https://example.com/products/usb-c-cable-1m",
delivery: Delivery::Delivered,
status: "配達済み(2026-09-20)",
},
],
},
Order {
number: "FD-3102-231",
date: "2026-09-25",
total: "¥17,000",
menu_id: "blocks-order-history-panels-menu-1",
trigger_id: "blocks-order-history-panels-menu-trigger-1",
items: &[
Item {
name: "メカニカルキーボード",
description: "茶軸 / 日本語配列",
price: "¥12,800",
href: "https://example.com/products/mechanical-keyboard",
delivery: Delivery::Shipping,
status: "配送中(お届け予定 2026-09-28)",
},
Item {
name: "静音マウス",
description: "ホワイト",
price: "¥3,200",
href: "https://example.com/products/silent-mouse",
delivery: Delivery::Shipping,
status: "配送中(お届け予定 2026-09-28)",
},
Item {
name: "リストレスト",
description: "低反発ウレタン",
price: "¥1,000",
href: "https://example.com/products/wrist-rest",
delivery: Delivery::Preparing,
status: "発送準備中",
},
],
},
];
/// サマリ帯の定義リスト 1 行(ラベル + 値)。`extra_attrs` は注文日の行にだけ
/// 狭幅非表示フック(`data-blocks-order-history-panels-date`)を渡すために使う。
fn summary_row(
label: &'static str,
value: &'static str,
extra_attrs: Vec<(&'static str, &'static str)>,
) -> Node {
data_list::item(
extra_attrs,
vec![
data_list::item_label(vec![], vec![text(label)]),
data_list::item_value(vec![], vec![text(value)]),
],
)
}
/// 三点メニューの trigger に使う自作の線画アイコン(装飾用途、`aria-hidden`。
/// 参照元の絵柄アイコンは持ち込まない)。
fn more_actions_icon() -> Node {
icon(
&IconProps {
size: Size::Sm,
..IconProps::default()
},
vec![],
vec![
el(
"circle",
vec![("cx", "12"), ("cy", "5"), ("r", "1.6")],
vec![],
),
el(
"circle",
vec![("cx", "12"), ("cy", "12"), ("r", "1.6")],
vec![],
),
el(
"circle",
vec![("cx", "12"), ("cy", "19"), ("r", "1.6")],
vec![],
),
],
)
}
/// サマリ帯の三点メニュー(「注文を見る」「請求書を見る」。狭幅で隠れる
/// ボタン 2 個と同じラベルの項目を DOM 上に持つのみで、メニュー自体も
/// 静的 Demo のため実際の開閉操作は提供しない。モジュール冒頭「狭幅では
/// サマリ操作をメニュー化し注文日を隠す」節参照)。
fn action_menu(order: &Order) -> Node {
let aria_label = format!("その他の操作、注文 {}", order.number);
menu::root(
Size::Sm,
OpenState::Closed,
vec![],
vec![
menu::trigger(
OpenState::Closed,
true,
Some(order.menu_id),
vec![
("id", order.trigger_id),
("aria-label", aria_label.as_str()),
],
vec![more_actions_icon()],
),
menu::positioner(
OpenState::Closed,
vec![],
vec![menu::content(
OpenState::Closed,
Some(order.menu_id),
Some(order.trigger_id),
vec![],
vec![
menu::item("view", false, false, vec![], vec![text("注文を見る")]),
menu::separator(vec![], vec![]),
menu::item("invoice", false, false, vec![], vec![text("請求書を見る")]),
],
)],
),
],
)
}
/// パネル上部のサマリ帯(`card::header`。注文見出し + 定義リスト + 操作群)。
/// 注文見出し(`<h3>`)を商品見出し(`<h4>`、[`item_row`])より前に置き、
/// 見出し一覧(スクリーンリーダーの見出しナビゲーション)で商品がどの注文に
/// 属するか判別できるようにする(レビュー指摘対応、イシュー #3056)。
/// 定義リスト + 操作群は `card::header` 本体ではなく内側の
/// `.blocks-order-history-panels-summary-row` へ横並びレイアウトを持たせる
/// (`card::header` 自体に付けると recipe の `[data-scope="card"]
/// [data-part="header"]` セレクタ〔attribute 2 個、本 block の単一 class
/// より高い詳細度〕に `flex-direction: column` で負け、横並びにならない。
/// レビュー指摘対応、イシュー #3056)。
fn summary(order: &Order) -> Node {
card::header(
vec![("class", "blocks-order-history-panels-summary")],
vec![
heading(
HeadingLevel::H3,
&HeadingProps::default(),
vec![("class", "blocks-order-history-panels-order-heading")],
vec![text(format!("注文 {}", order.number))],
),
div(
vec![("class", "blocks-order-history-panels-summary-row")],
vec![
data_list::root(
DataListProps {
orientation: DataListOrientation::Vertical,
..DataListProps::default()
},
vec![("data-blocks-order-history-panels-summary-list", "")],
vec![
summary_row("注文番号", order.number, vec![]),
summary_row(
"注文日",
order.date,
vec![("data-blocks-order-history-panels-date", "")],
),
summary_row("合計", order.total, vec![]),
],
),
div(
vec![("class", "blocks-order-history-panels-actions")],
vec![
button(
&ButtonProps {
variant: ButtonVariant::Outline,
size: Size::Sm,
disabled: true,
..ButtonProps::default()
},
vec![("data-blocks-order-history-panels-action-button", "")],
vec![text("注文を見る")],
),
button(
&ButtonProps {
variant: ButtonVariant::Outline,
size: Size::Sm,
disabled: true,
..ButtonProps::default()
},
vec![("data-blocks-order-history-panels-action-button", "")],
vec![text("請求書を見る")],
),
action_menu(order),
],
),
],
),
],
)
}
/// 配送状況アイコン(装飾用途の自作線画、`aria-hidden`。モジュール冒頭
/// 「配送状況の 3 パターン」節参照。参照元の絵柄は持ち込まない)。
fn delivery_icon(delivery: Delivery) -> Node {
let path = match delivery {
// チェック(`more_actions_icon` ではなく `order_tracking_progress` の
// 完了マークと同形の折れ線)。
Delivery::Delivered => el(
"path",
vec![
("d", "M5 12l4 4L19 7"),
("fill", "none"),
("stroke", "currentColor"),
("stroke-width", "2"),
],
vec![],
),
// 右向き矢印(配送中)。
Delivery::Shipping => el(
"path",
vec![
("d", "M4 12h14m0 0l-5-5m5 5l-5 5"),
("fill", "none"),
("stroke", "currentColor"),
("stroke-width", "2"),
],
vec![],
),
// 時計(発送準備中)。
Delivery::Preparing => el(
"path",
vec![
("d", "M12 7v5l3 3M12 2a10 10 0 100 20 10 10 0 000-20z"),
("fill", "none"),
("stroke", "currentColor"),
("stroke-width", "2"),
],
vec![],
),
};
icon(
&IconProps {
size: Size::Sm,
..IconProps::default()
},
vec![],
vec![path],
)
}
/// 配送状況の表示行(アイコン + 文言。状態は文言で伝え、色だけに頼らない
/// 〔WCAG 1.4.1〕。モジュール冒頭「配送状況の 3 パターン」節参照)。
fn delivery_status(item: &Item) -> Node {
div(
vec![
("class", "blocks-order-history-panels-status"),
(
"data-blocks-order-history-panels-status",
item.delivery.hook_value(),
),
],
vec![
delivery_icon(item.delivery),
styled_text::text(
&TextProps {
size: TextSize::Sm,
..TextProps::default()
},
vec![],
vec![text(item.status)],
),
],
)
}
/// 商品行の操作群(「商品を見る」リンク + 「再購入」「類似品を見る」
/// ボタン、いずれも `disabled`)。可視テキストが商品間で重複するため、
/// `aria-label` へ商品名を含めて一意にし可視テキストも名前に含める
/// (WCAG 2.5.3、モジュール冒頭「商品行の操作とアクセシブルネーム」節参照。
/// レビュー指摘対応、イシュー #3056 のリンク分の判断を踏襲)。
fn item_actions(item: &Item) -> Node {
let link_label = format!("{}を見る", item.name);
let reorder_label = format!("{}を再購入", item.name);
let similar_label = format!("{}の類似品を見る", item.name);
div(
vec![("class", "blocks-order-history-panels-item-actions")],
vec![
link::root(
item.href,
&LinkProps::default(),
vec![
("data-blocks-order-history-panels-item-link", ""),
("aria-label", link_label.as_str()),
],
vec![text("商品を見る")],
),
button(
&ButtonProps {
variant: ButtonVariant::Outline,
size: Size::Sm,
disabled: true,
..ButtonProps::default()
},
vec![("aria-label", reorder_label.as_str())],
vec![text("再購入")],
),
button(
&ButtonProps {
variant: ButtonVariant::Outline,
size: Size::Sm,
disabled: true,
..ButtonProps::default()
},
vec![("aria-label", similar_label.as_str())],
vec![text("類似品を見る")],
),
],
)
}
/// 商品 1 行分(画像・名称・価格・説明・配送状況 + 操作群)。
fn item_row(item: &Item) -> Node {
div(
vec![("class", "blocks-order-history-panels-item")],
vec![
image(
&ImageProps {
fit: ImageFit::Cover,
shape: ImageShape::Rounded,
..ImageProps::new(dummy_assets::PRODUCT_SRC, item.name)
},
vec![("data-blocks-order-history-panels-image", "")],
),
div(
vec![("class", "blocks-order-history-panels-item-body")],
vec![
div(
vec![("class", "blocks-order-history-panels-item-heading")],
vec![
heading(
HeadingLevel::H4,
&HeadingProps::default(),
vec![],
vec![text(item.name)],
),
styled_text::text(
&TextProps {
size: TextSize::Sm,
..TextProps::default()
},
vec![],
vec![text(item.price)],
),
],
),
styled_text::text(
&TextProps {
variant: TextVariant::Muted,
size: TextSize::Sm,
..TextProps::default()
},
vec![],
vec![text(item.description)],
),
delivery_status(item),
],
),
item_actions(item),
],
)
}
/// 注文 1 件分のパネル(`card::root`。サマリ帯 + 区切り線 + 商品行の並び)。
fn panel(order: &Order) -> Node {
let mut body_children: Vec<Node> = Vec::new();
for (index, item) in order.items.iter().enumerate() {
if index > 0 {
body_children.push(separator(&SeparatorProps::default(), vec![]));
}
body_children.push(item_row(item));
}
card::root(
CardProps::default(),
vec![("data-blocks-order-history-panels-panel", "")],
vec![
summary(order),
separator(&SeparatorProps::default(), vec![]),
card::body(
vec![("class", "blocks-order-history-panels-items")],
body_children,
),
],
)
}
/// `order-history-panels` の Demo 本体。呼び出しごとに同一の `Node` を
/// 返す純関数。
#[must_use]
pub fn demo() -> Node {
div(
vec![("class", "blocks-order-history-panels-layout")],
ORDERS.iter().map(panel).collect(),
)
}差分メモ
- R1116(主参照): 枠付きパネル構成を採用し、コンテナ幅 40rem 未満でサマリ帯のボタン 2 個と注文日表示を三点メニューへ集約します。
- R1118: 配送状況の 3 パターン(配達済み・配送中・発送準備中)を商品行へ並べています。アイコンは装飾用途(
aria-hidden)で、状態はテキストで示します(色だけに頼らない WCAG 1.4.1 対応)。 - R1119: 注文見出し(h3)と、商品行の「再購入」「類似品を見る」ボタンを採用しています。参照元の文言・配色・アイコンは持ち込んでいません。
関連情報: Card / Data List / Button / Menu / Image / Text / Heading / Link / Separator / Icon