この記事でわかること
- dialog要素 を使ったモーダル実装の特徴と実装例
特徴
- キーボード操作、webアクセシビリティ要件を簡単に満たすことができる
- ios safari 15.4以上で利用可能
- 以下の機能は自分で実装する必要あり
- モーダルが開かれた時に背景を固定(背面コンテンツのスクロールを抑止)する
- 背景要素クリックで、モーダルが閉じる
- 開閉アニメーション
実装例
src/components/Modal/index.tsx
# ...
const dialog = {
padding: 0;
border: 0;
&::backdrop { // 背景の擬似要素
background: rgb(0 0 0 / 70%);
}
}
export const Modal = ({ children, id }: Props) => {
const [isClient, setIsClient] = useState<boolean>(false);
useEffect(() => {
if (typeof window === 'undefined') return;
setIsClient(true);
}, []);
if (!isClient) return <></>; // SSR時にdocumentを参照しないようにしてます
return createPortal( // createPortal(任意のDOM, document.body) でbodyタグ直下の最後の要素として任意のDOMがレンダリングされます
<dialog style={dialog} id={id}>
<div>{children}</div>
</dialog>,
document.body
);
};
src/components/Modal/hooks.tsx
# ...
// モーダルの表示非表示ロジックをhooksにすることで、任意のコンポーネントで表示非表示ロジックを利用できるようにしてます。
export const useModal = (id: string) => {
const open = useCallback(() => {
const dialog = document.getElementById(id) as HTMLDialogElement;
dialog.showModal();
}, [id]);
const close = useCallback(() => {
const dialog = document.getElementById(id) as HTMLDialogElement;
dialog.close();
}, [id]);
return { open, close };
};
src/pages/index.tsx
# ...
import { Modal } from '@/components/Modal';
import { useModal } from '@/components/Modal/hooks';
export const HOME = () => {
const { open, close } = useModal('modal');
return (
<>
<button onClick={open}>モーダルを開く</button>
<Modal id="modal">
<div>
<p>モーダル</p>
<button onClick={close}>モーダルを閉じる</button>
</div>
</Modal>
<>
);
};
完成!
追加実装 モーダルが開かれた時に背景を固定する
src/components/Modal/hooks.tsx
# ...
export const useModal = (id: string) => {
const open = useCallback(() => {
const dialog = document.getElementById(id) as HTMLDialogElement;
const body = document.body // 追加
dialog.showModal();
body.style.overflow = 'hidden'; // 追加
}, [id]);
const close = useCallback(() => {
const dialog = document.getElementById(id) as HTMLDialogElement;
const body = document.body // 追加
dialog.close();
body.style.removeProperty('overflow'); // 追加
}, [id]);
return { open, close };
};
追加実装 背景要素クリックでモーダルが閉じる
src/components/Modal/index.tsx
...
export const ModalDialog = ({
children,
id,
closeDialog,
}: {
children: ReactNode;
id: string;
closeDialog: () => void;
}) => {
// イベントリスナーを追加
useEffect(() => {
const dialog = document.getElementById(id)
if (!dialog) return;
const click = (e: MouseEvent) => {
e.target === dialog && closeDialog();
};
const keydown = (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.target === dialog && closeDialog();
}
};
dialog.addEventListener('click', click);
dialog.addEventListener('keydown', keydown);
return () => {
dialog.removeEventListener('click', click);
dialog.removeEventListener('keydown', keydown);
};
}, [closeDialog, id]);
return (
<dialog style={dialog} id={id}>
<div
id="dialogInner"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
role="button"
tabIndex={-1}
>
{children}
</div>
</dialog>
);
};
追加実装 開閉アニメーション
TODO: 実装予定