本文へスキップ
ウェブエンジニア問題集
第11章

MUIのDialog・Snackbar・Menu — 画面に重なるUIの作法

約8分
この章の目次開く

ダイアログ・通知・メニューには、共通する性質があります。いずれもDOM上は <body> の末尾に描画され、書いた場所には存在しません。この仕組みをPortalと呼びます。

親要素の overflow: hidden に切られたり、z-index の積み重ねに巻き込まれたりしないための作りです。CSSで位置を調整しようとして効かないときは、たいていこれが理由です。

学習者学習者

開発者ツールで探しても、書いたはずの場所にダイアログが無い…。

<body> の一番下を見てください。そこに出ています。


Dialog — 確認や入力を割り込ませる

const [open, setOpen] = useState(false);
 
<Dialog open={open} onClose={() => setOpen(false)}>
  <DialogTitle>削除しますか?</DialogTitle>
  <DialogContent>
    <DialogContentText>この操作は取り消せません。</DialogContentText>
  </DialogContent>
  <DialogActions>
    <Button onClick={() => setOpen(false)}>キャンセル</Button>
    <Button onClick={handleDelete} color="error">削除</Button>
  </DialogActions>
</Dialog>;
tsx

open は必須です。閉じている間も要素自体は存在し続けます。

大きさの調整

prop効果
maxWidth最大幅。ブレークポイントのキー、または false で制限なし
fullWidthmaxWidth まで横幅をいっぱいに広げる
fullScreen画面全体に広げる
scroll'paper'(中身がスクロール)か 'body'(ページごとスクロール)

maxWidth だけでは中身が少ないと細いままです。fullWidth と組み合わせるのが定番です。

<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
tsx

スマートフォンでだけ全画面にしたい場合は、useMediaQuery と組み合わせます。


閉じる理由を受け取る

onClose は第2引数に閉じようとした理由を受け取ります。

<Dialog
  open={open}
  onClose={(event, reason) => {
    if (reason === 'backdropClick') return; // 背景クリックでは閉じない
    setOpen(false);
  }}
>
tsx

Dialogで渡ってくる理由は2つです。

reasonいつ渡るか
'backdropClick'背景の暗い部分をクリックした
'escapeKeyDown'Escキーを押した
確認を挟むイメージ

フォーカスはMUIが管理する

ダイアログを開くと、フォーカスがダイアログの中へ移り、Tabキーでの移動がダイアログ内に閉じ込められます。閉じると、開く前にフォーカスがあった要素へ戻ります。

自分で実装すると面倒な部分ですが、MUIが自動で処理します。余計なことをしないのが正解で、autoFocus を手当たり次第に付けるとかえって挙動が乱れます。

先生先生

スクリーンリーダー利用者にとって、フォーカスが移らないダイアログは「開いたことに気づけない」状態になります。MUIを使う利点のひとつがここです。


Snackbar — 邪魔しない通知

保存完了のような短い通知に使います。ダイアログと違い、操作をブロックしません。

<Snackbar
  open={open}
  autoHideDuration={4000}
  onClose={handleClose}
  message="保存しました"
/>
tsx
prop既定値内容
autoHideDurationnull自動で閉じるまでのミリ秒。null は閉じない
anchorOrigin{ vertical: 'bottom', horizontal: 'left' }表示位置
resumeHideDuration—マウスが離れてから閉じるまでの時間

autoHideDuration の既定は null なので、指定しないと出しっぱなしになります。

clickawayで消える問題

onClose の reason には3種類あります。

reasonいつ渡るか
'timeout'autoHideDuration が経過した
'clickaway'画面のどこかをクリックした
'escapeKeyDown'Escキーを押した

既定では画面のどこをクリックしても消えます。「読む前に消えてしまう」という苦情が出たら、clickaway を無視します。

onClose={(event, reason) => {
  if (reason === 'clickaway') return;
  setOpen(false);
}}
tsx

中身を作り込むならAlertと組み合わせる

message は文字列だけです。成功・失敗を色で分けたいときは Alert を入れます。

<Snackbar open={open} autoHideDuration={4000} onClose={handleClose}>
  <Alert severity="success" onClose={handleClose}>
    保存しました
  </Alert>
</Snackbar>
tsx

Menu は「どの要素の隣に出すか」を anchorEl で指定します。位置の計算はMUIが行います。

const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
 
<>
  <Button onClick={(e) => setAnchorEl(e.currentTarget)}>操作</Button>
  <Menu
    anchorEl={anchorEl}
    open={Boolean(anchorEl)}
    onClose={() => setAnchorEl(null)}
  >
    <MenuItem onClick={handleEdit}>編集</MenuItem>
    <MenuItem onClick={handleDelete}>削除</MenuItem>
  </Menu>
</>;
tsx

書き方の型が決まっています。

  • 開閉の状態は anchorEl そのもので表す。open は Boolean(anchorEl)
  • 閉じるときは anchorEl を null に戻す
  • MenuItem の onClick では、処理と一緒に閉じる操作も書く

よくあるハマりどころ

ダイアログが親のCSSの影響を受けない

Portalで <body> 直下に出るためです。中身のスタイルは sx かslotで当てます。

Snackbarがすぐ消える、または消えない

autoHideDuration の指定漏れ(既定は null)か、clickaway を潰していないかを確認します。

anchorEl に渡す要素が正しくありません。e.currentTarget を使ってください。e.target だと、ボタン内のアイコンなど子要素が入ることがあります。

ダイアログを閉じてもフォームの値が残る

Dialog は閉じても中身がDOMから消えません。開くたびに初期化したい場合は、open が変わったタイミングでstateをリセットするか、key を付け替えます。

Escキーを無効にしたい

v9では disableEscapeKeyDown が使えません。onClose の reason で分岐します。


ちゃんと使うためのポイント

  • Dialog・Snackbar・MenuはPortalで <body> 直下に描画される
  • onClose の第2引数の reason で、閉じ方によって処理を分けられる
  • v9では disableEscapeKeyDown が削除。reason === 'escapeKeyDown' で判定する
  • フォーカスの移動と復帰はMUIが自動で行う。手を出しすぎない
  • Snackbar の autoHideDuration の既定は null(自動で閉じない)
  • Menu は anchorEl で開閉状態を表し、open={Boolean(anchorEl)} と書く

次の章では、v9で変わったアクセシビリティ周りをまとめて扱います。


参考リンク