長い処理の待機画面ProgressDialogExperimental
数十秒以上かかる処理を待つあいだのダイアログです。16:9 の絵、いまの状態を伝える一文、自由に中身を入れられる補助の枠(任意)、キャンセルのボタン(任意)を持ちます。利用者の操作では閉じず、閉じるのは呼び出し側のコードです。
プレビュー
状態とバリエーション
プロパティ
表は横にスクロールできます
| プロパティ | 型 | 初期値 | 説明 |
|---|---|---|---|
| variant | "default" | "overlay" | "default" | overlay は札・題・説明を絵の上に重ねます。既定は絵の下。 |
| badge | ReactNode | - | 題の上の札(Badge など)。 |
| open | boolean | - | 開いているか。 |
| title | ReactNode | - | 題。ダイアログの名前として読み上げられます。 |
| description | ReactNode | - | 題の下の説明。DialogDescription になります。 |
| status | ReactNode | - | いまの状態を言葉で。読み上げ領域(role="status")に入ります。 |
| statusLabel | ReactNode | - | 状態の上の小見出し。 |
| media | ReactNode | - | 16:9 の絵。img・svg・video など。 |
| value | number | - | 進み具合。分かるときだけ渡します。 |
| max | number | 100 | value の上限。 |
| valueText | string | - | value の読み上げ文(例: 5 件中 3 件完了)。 |
| aside | ReactNode | - | 状態の下に置く補助の枠。 |
| onCancel | () => void | - | 渡すとキャンセルのボタンが出ます。 |
| cancelLabel | ReactNode | "Cancel" | キャンセルのボタンの文字。 |
| cancelNote | ReactNode | - | ボタンの横の短い注記。 |
| portalContainer | HTMLElement | null | - | 描く先の要素。渡すと、その中に並べて描きます。 |
| className | string | - | DialogContent に足すクラス。 |
使い方
"use client";
import * as React from "react";
import { Badge, Button, ProgressDialog } from "@gunjo/ui";
const STEPS = ["会議の前提を読み取っています", "発言を要点に分けています", "議事録の形に整えています"];
const SCENE = "/demos/progress-dialog/meeting-minutes.svg";
const SCENE_STILL = "/demos/progress-dialog/meeting-minutes-still.svg";
// 閉じたあとも、開いていたときの高さを保ちます
function useKeptHeight(container: HTMLElement | null, open: boolean) {
const [height, setHeight] = React.useState<number>();
React.useEffect(() => {
if (!container || !open) return;
const observer = new ResizeObserver(() => setHeight(container.offsetHeight));
observer.observe(container);
return () => observer.disconnect();
}, [container, open]);
return open ? undefined : height;
}
export function CreateMinutes() {
const [container, setContainer] = React.useState<HTMLDivElement | null>(null);
const [open, setOpen] = React.useState(false);
const [step, setStep] = React.useState(0);
const [result, setResult] = React.useState("");
const timer = React.useRef<number | null>(null);
const keptHeight = useKeptHeight(container, open);
const stop = React.useCallback(() => {
if (timer.current !== null) window.clearInterval(timer.current);
timer.current = null;
}, []);
const start = React.useCallback(() => {
stop();
setStep(0);
setResult("");
setOpen(true);
let current = 0;
timer.current = window.setInterval(() => {
current += 1;
if (current >= STEPS.length) {
stop();
setOpen(false);
setResult("議事録ができました。");
return;
}
setStep(current);
}, 2200);
}, [stop]);
React.useEffect(() => {
start();
return stop;
}, [start, stop]);
const cancel = () => {
stop();
setOpen(false);
setResult("作成をやめました。入力内容は残っています。");
};
return (
<div
ref={setContainer}
className="relative grid w-full place-items-center [&>*]:[grid-area:1/1]"
style={{ minHeight: keptHeight }}
>
{open ? null : (
<div className="flex flex-col items-center gap-4 text-center">
<Button onClick={start}>もう一度作る</Button>
<p className="text-sm text-muted-foreground" role="status">{result}</p>
</div>
)}
<ProgressDialog
portalContainer={container}
variant="overlay"
open={open}
badge={<Badge variant="secondary">AIで作成中</Badge>}
title="会議を議事録にまとめています"
description="数十秒かかることがあります。この画面のままお待ちください。"
media={
<picture>
<source media="(prefers-reduced-motion: reduce)" srcSet={SCENE_STILL} />
<img src={SCENE} alt="" />
</picture>
}
statusLabel="いまの作業"
status={STEPS[step]}
onCancel={cancel}
cancelLabel="作成をやめる"
cancelNote="やめても入力内容はこの画面に残ります。"
/>
</div>
);
}設計の判断
- 利用者の操作では閉じません。Esc と外側のクリックでは閉じず、右上の閉じるボタンも出しません。うっかり閉じると、処理が続いているのか分からなくなるためです。
- 進み具合は言葉で伝えます。絵は読み上げから外します。絵の動きだけでは、見えない人にも、動きを減らす設定にしている人にも伝わりません。
- 中身が長いときは、中身だけがスクロールします。キャンセルのボタンの行は、狭い画面でも下端に残ります。
使用コンポーネント
関連コンポーネント
- ActionProgress
数秒で終わる処理向け。絵も補助の枠もキャンセルも無い。
- RouteProgress
ページの移動中に上端へ出す細いバー。画面を塞がない。
- Progress
バーだけ。画面の中に置き、操作は止めない。
- Dialog
利用者が閉じられる、ふつうのダイアログ。
いつ・なぜ使うか(UIXHERO)
「いつ・なぜ使うか」の判断は、姉妹サイト UIXHERO の記事で解説しています。