「入力内容の確認」画面の実装のつらさ
「入力画面と確認画面のあいだの画面遷移」……それは、全 React エンジニアの敵。(主語がデカい)
- 入力内容を保持して確認画面に遷移する
- 確認画面でブラウザの「戻る」ボタンを押した時の挙動のケア
- サーバー側から返ってくる修正可能なエラー(たとえば、IDが既存と被っている)のケア
- 確認→入力画面に戻す
- etc.
このように、細かく配慮しようとするとコードの複雑性が加速してしまうのが、React における「入力内容の確認」画面だと思います。
じゃあ、無くしちゃおうか
なので、実装者の都合だけを考えるなら、確認画面なんて無くしちゃえば良いのですが、ユーザーの使い勝手を完全に無視してしまうのも気が引けます。
特に、「新しい商品を登録するボタン」「商品を削除するボタン」のように、取り返しの付かないアクション では、ワンクッション置かず急に処理が始まるのは不親切ですよね?
モードレスダイアログ — UX観点の特徴
「ダイアログ」と「モーダル」がほぼ同義語として使う人もいますが、ダイアログには、「モーダル」と「モードレス」の二種類あり、Popover はモードレスなダイアログの一種です。 (非モーダルとも言いますね)
上記の記事を参考に、「入力内容の確認画面」というコンテキストから、
- 操作の方法を制限されないので、少し迷う可能性がある
- 入力フォームが後ろに隠れないようにする実装が可能
- ワーキングメモリを消費せずに済み、便利そう
- ただし、入力フォームを見るだけで、内容の確認が十分に出来る場合に限る
- できない例:markdownのプレビュー
- 入力画面の中にプレビューを組み込めば強引に解決?
というメリット・デメリットがあると考えられます。
- 最初から見えている「登録する」ボタンは、ダイアログを開くためだけのボタン (
type="button"
) - 真の送信ボタン (
type="submit"
) はダイアログの中に配置される
という仕様にするのが良いと考えました。
(ちなみに今回利用した Mantine UI の Popover は、スクロールをロックしないため、表示したままでもページ上部に書かれている内容を確認することができます。)
Popover 以外の名称
ライブラリによって名前が少しずつ違うようです。
と、その引用元のドキュメントを参照すると、
-
Inline dialog
- Attlassian Design System
-
Popconfirm
- Ant Design
-
Popover
- Chakra UI
- Bootstrap
- Mantine UI
- Material UI *
Popconfirm は Yes/No の確認に特化したAPI設計になっています。
また、 Material UI の Popover は少しモーダルっぽい挙動を示すので、他とは少し違います。
実際に動いている様子
今回は、 Mantine-UI の Popover コンポーネントを使って実装しています。
開いたままでも内容を確認することが可能で、スクロールを禁止していないので上部を見に行けていることが分かると思います。
▼ 公式のドキュメント・動作デモはこちら
使用した主なライブラリのバージョン
- @mantine/core — 5.7.2
- @emotion/react — 11.10.5
- next — 13.0.3
- react — 18.2.0
- react-hook-form — 7.39.4
ページ全体のコード全文はこちら
import {
Box,
Button,
Checkbox,
Container,
Divider,
Flex,
Paper,
Popover,
Stack,
Text,
Textarea,
TextInput,
Title,
} from "@mantine/core";
import Head from "next/head";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
type FormValues = {
somethingName: string;
description1: string;
description2: string;
agreedToCode: boolean;
};
/* ページ本体の記述 */
const FormPage = () => {
const router = useRouter();
// react-hook-form の設定
const { register, handleSubmit } = useForm<FormValues>();
// popover の開閉状態を管理するステートと関数
const [popoverOpen, setPopoverOpen] = useState(false);
const togglePopoverOpen = () => setPopoverOpen((b) => !b);
const onSubmit = (data: FormValues) => {
setPopoverOpen(false);
window.alert(JSON.stringify(data));
router.push("/");
};
return (
<Box sx={{ minHeight: "100vh" }}>
<Head>
<title>Form using Mantine UI</title>
</Head>
<Container pb={240}>
<Paper shadow="sm" withBorder mt="lg" component="main">
<form onSubmit={handleSubmit(onSubmit)}>
<Box p="lg">
<Title>〇〇新規作成</Title>
</Box>
<Divider />
<Stack p="lg" spacing="lg">
<TextInput
{...register("somethingName")}
label="〇〇名称"
withAsterisk
description="必須 48文字以内で入力して下さい"
/>
<Textarea
{...register("description1")}
label="説明文"
description="480文字以内で入力して下さい"
minRows={5}
/>
<Textarea
{...register("description2")}
label="説明文2"
description="480文字以内で入力して下さい"
minRows={5}
/>
<Checkbox
{...register("agreedToCode")}
label="利用規約に同意しています。"
/>
</Stack>
<Divider />
<Flex p="lg" justify="center">
<Popover
opened={popoverOpen}
onChange={setPopoverOpen}
withArrow
offset={-12}
shadow="lg"
trapFocus
>
<Popover.Target>
<Button type="button" onClick={togglePopoverOpen} size="lg">
〇〇を登録する
</Button>
</Popover.Target>
<Popover.Dropdown>
<Stack p="md" spacing="md">
<Title size="h3" align="center">
確認
</Title>
<Text align="center">
この内容で新しい〇〇を登録しますか?
</Text>
<Button type="submit" variant="light" size="md" fullWidth>
はい、登録します。
</Button>
<Text color="dimmed" size="sm" align="center">
このフキダシの以外を操作すると自動で閉じます
</Text>
</Stack>
</Popover.Dropdown>
</Popover>
</Flex>
</form>
</Paper>
</Container>
</Box>
);
};
export default FormPage;
Mantine-UI の Popover コンポーネントの使い方
Popover は Compound Component (複合的コンポーネント) という形を取っています。
以下のように、ネストする形でコンポーネントを利用する必要があります。
const [popoverOpen, setPopoverOpen] = useState(false);
const togglePopoverOpen = () => setPopoverOpen((b) => !b);
// Controlled な書き方の場合
<Popover
opened={popoverOpen}
onChange={setPopoverOpen}
// このコンポーネントの Prop を使って
// その他各種設定を行う
>
<Popover.Target>
ここにボタンを設置。
togglePopoverOpen は明示的に呼び出す必要がある。
</Popover.Target>
<Popover.Dropdown>
ここに小ダイアログの表示内容を設置
<Popover.Dropdown>
</Popover>
また、 Popover コンポーネントは、その挙動やスタイルを調整できるように、多くの Props が生えています。(公式ドキュメント - Popover の Prop 一覧はこちら)
例を挙げると、
- withArrow, arrowOffset, arrowRadius, arrowSize
- フキダシのツノみたいな部分の設定
- shadow
- 影の設定
- trapFocus
- 開いた時にフォーカスが小ダイアログ内に補足される(?)
などです。
あとがき
UX をよさげに保ちつつ、実装の複雑さを軽減できる Popover は最高だぜ!