長い処理の待機画面ProgressDialogExperimental

数十秒以上かかる処理を待つあいだのダイアログです。16:9 の絵、いまの状態を伝える一文、自由に中身を入れられる補助の枠(任意)、キャンセルのボタン(任意)を持ちます。利用者の操作では閉じず、閉じるのは呼び出し側のコードです。

プレビュー

状態とバリエーション

補助の枠あり

aside に渡したものを、状態の下にそのまま置きます。

件数が分かるとき

処理が「5 件中 3 件」のように数を返すなら、value と max を渡します。

キャンセルなし

onCancel を渡さない形です。やめられない処理に使います。

動きを減らす設定

流れるバーは止まり、淡い色の帯になります。絵は、呼び出し側が用意した止めた版の SVG に替えます(picture の source)。

プロパティ

表は横にスクロールできます
プロパティ型初期値説明
variant"default" | "overlay""default"overlay は札・題・説明を絵の上に重ねます。既定は絵の下。
badgeReactNode-題の上の札(Badge など)。
openboolean-開いているか。
titleReactNode-題。ダイアログの名前として読み上げられます。
descriptionReactNode-題の下の説明。DialogDescription になります。
statusReactNode-いまの状態を言葉で。読み上げ領域(role="status")に入ります。
statusLabelReactNode-状態の上の小見出し。
mediaReactNode-16:9 の絵。img・svg・video など。
valuenumber-進み具合。分かるときだけ渡します。
maxnumber100value の上限。
valueTextstring-value の読み上げ文(例: 5 件中 3 件完了)。
asideReactNode-状態の下に置く補助の枠。
onCancel() => void-渡すとキャンセルのボタンが出ます。
cancelLabelReactNode"Cancel"キャンセルのボタンの文字。
cancelNoteReactNode-ボタンの横の短い注記。
portalContainerHTMLElement | null-描く先の要素。渡すと、その中に並べて描きます。
classNamestring-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 の記事で解説しています。