Skip to content
fandhe-frontendcore v0.4.3
GitHub

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
ノイズキャンセリングイヤホン

ノイズキャンセリングイヤホン

¥8,200

ブラック / Bluetooth 5.3

配達済み(2026-09-20)

商品を見る
USB-C 急速充電ケーブル 1m

USB-C 急速充電ケーブル 1m

¥1,400

グレー

配達済み(2026-09-20)

商品を見る

注文 FD-3102-231

注文番号
FD-3102-231
注文日
2026-09-25
合計
¥17,000
メカニカルキーボード

メカニカルキーボード

¥12,800

茶軸 / 日本語配列

配送中(お届け予定 2026-09-28)

商品を見る
静音マウス

静音マウス

¥3,200

ホワイト

配送中(お届け予定 2026-09-28)

商品を見る
リストレスト

リストレスト

¥1,000

低反発ウレタン

発送準備中

商品を見る

使用部品

Rust コード

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