HTMLのdialog要素でDialogComponentを作る(React)
2022年3月から主要ブラウザ(Chrome, Edge, Firefox, Safari)で dialog
要素が利用できるようになったそうなのでそれを使ってDialogすなわりModalのコンポーネントを作ったらどんな感じになるかを試してみました.説明少なめですが記事にします.
また,今回はできるだけ最小限のコードで作成することを意識しています.
dialog要素ってなに?
- eventで表示/非表示を切り替えることができる
- backdropという疑似要素があり,すでにOverlayが作成されている(これ結構嬉しい)
メリットはこの2点かなと思います.
わかりやすく作ってみる
Reactで最小限に作るとこんな感じですね.
const Sample = () => {
const dialogRef = useRef<HTMLDialogElement>(null);
const openDialog = () => {
if (dialogRef.current) {
dialogRef.current.showModal();
}
};
const closeDialog = () => {
if (dialogRef.current) {
dialogRef.current.close();
}
};
return (
<div>
<button onClick={openDialog}>openModal</button>
<dialog ref={dialogRef}>
<h2>Dialog title</h2>
<p>Dialog content</p>
<button onClick={closeDialog}>close</button>
</dialog>
</div>
);
};
const dialogRef = useRef<HTMLDialogElement>(null);
イベントを操作するためにDOMにアクセスできるようにrefを用意します.
これで,dialogRef.current.showModal()
を呼ぶことでdialogがOverlay付きで表示されます.(Modalなんだ😇)
同様にdialogRef.current.close()
で閉じることができます.
OverlayをクリックしたときにDialogを閉じたい
dialog要素にはonClick属性があり,
<dialog ref={dialogRef} onClick={closeDialog}>
とすれば,どこをクリックしてもDialogを閉じることができます.
しかし,Dialogの中をクリックした場合は閉じないほうが実用的です.
これを実現するには以下のようにします.
<dialog ref={dialogRef} onClick={closeDialog}>
<div onClick={(e) => e.stopPropagation()}>
<h2>Dialog title</h2>
<p>Dialog content</p>
<button onClick={closeDialog}>close</button>
</div>
</dialog>
dialog
の中をdiv
などで囲むようにし,そのクリックイベントをstopPropagation()
で中断します.
ここまで,かなり短いコードで実用的なDialogができました.
再利用性の高いコンポーネントにする
このDialogの仕組みを再利用性の高いものにします.
いろいろ考えたのですが,そのためにはちょっと複雑ですが以下のようになりそうです.
- useDialogを作成し,ref,openDialog,closeDialogの再利用可能なものにする
- refを渡せるようにforwordRefでWrapしたDialogコンポーネントを作成する
- DialogのUIを追加する際は上記の2つを毎度組み合わせて作るようにする
実際のコードはこちら
useDialog.ts
import { useRef, useCallback } from 'react';
export const useDialog = () => {
const ref = useRef<HTMLDialogElement>(null);
const showModal = useCallback(() => {
if (ref.current) {
ref.current.showModal();
}
}, []);
const closeModal = useCallback(() => {
if (ref.current) {
ref.current.close();
}
}, []);
return { ref, showModal, closeModal };
};
Dialog.ts
import { FC, forwardRef, ReactNode, Ref } from 'react';
type Props = {
ref: Ref<HTMLDialogElement>;
onClose: () => void;
children: ReactNode;
};
export const Dialog: FC<Props> = forwardRef<HTMLDialogElement, Props>(
({ onClose, children }, ref) => {
return (
<dialog
ref={ref}
onClick={onClose}
>
<div onClick={(e) => e.stopPropagation()}>
{children}
</div>
</dialog>
);
},
);
Dialog.displayName = 'Dialog'; // ESLint: react/display-nameの回避のため
使用箇所
const Sample = () => {
const { ref, showModal, closeModal } = useDialog();
return (
<div>
<h1>Sample dialog element</h1>
<button onClick={showModal}>showModal</button>
<Dialog ref={ref} onClose={closeModal}>
<h2>Dialog title</h2>
<p>Dialog content. Dialog content. Dialog content.</p>
<button onClick={closeModal}>
close
</button>
</Dialog>
</div>
);
};
Dialog.ts
においてforwardRef
を使った型の書き方が結構微妙なのですが,これが最も簡潔だと思っています.
一応要点をまとめると
- refを渡すために
forwardRef
を使用する必要がある. - refはpropsから受け取ることができない
- Propsの型にrefを(書く必要はないのだが)書かないと渡す箇所でエラーになる
こんな感じです.
OverlayのCSSのあて方
疑似要素なので
dialog::backdrop {
backgroud-color: orange;
opacty: 0.5;
}
とかするだけです.これが楽で最高!!!!!
感想
useDialog.ts
とDialog.ts
の二重管理になるのがちょっと微妙って感じですが,Overlayを自分で作らなくていいのはかなり嬉しいです.
あとはHTMLをちゃんと勉強してる感があっていいですね(謎)
今回作ったサンプルのリポジトリも置いときます.
Discussion
dialog::backdrop
はFirefoxだけ2022年9月末時点で対応してなかったんですが、全モダンブラウザ対応するようになったんですね!そうなのですね👀
バージョンにもよるかと思いますが,98では2022年3月に対応しているようです!
dialog::backdropはその後別途対応したんですかね🤔