🕋
Svelte5でdialogタグのModalコンポーネント作成メモ
Modalコンポーネントが欲しくなったので作成しました。Esc
キー無効化に手間取ったり背景スクロール不可を手軽に実装できないか色々調べたりしたので一部スタイリング部分以外を備忘用に残しておきます。
作成したコンポーネント
REPL
コード全体
code
App.svelte
<script>
import Modal from "./Modal.svelte";
let open = $state(false);
function onclick() { open = !open; }
</script>
<button type="button" {onclick}>open</button>
<Modal bind:open>
<span>modal dialog</span>
</Modal>
{#each Array(40).fill(0).map((x,i)=>i) as num}
<p>{num}</p>
{/each}
Modal.svelte
<script module lang="ts">
export type Props = {
open: boolean,
closable: boolean,
children: Snippet,
element: HTMLDialogElement,
};
import { type Snippet, untrack } from "svelte";
import { stop } from "./util.svelte";
</script>
<script lang="ts">
let { open = $bindable(), closable = true, children, element = $bindable() }: Props = $props();
$effect.pre(() => { open;
untrack(() => { toggleDialog(); });
});
function toggleDialog() {
if (element === undefined) { return; }
if (open) {
element.showModal();
} else {
element.close();
}
}
const closeDialog = closable ? () => { open = false; } : undefined;
const preventClose = closable ? stop(()=>{}) : undefined;
const preventEsc = !closable ? (ev: KeyboardEvent) => { if (ev.key === "Escape") { ev.preventDefault(); }} : undefined;
const oncancel = closable ? () => { open = false; } : undefined;
</script>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<dialog bind:this={element} onclick={closeDialog} onkeydown={preventEsc} {oncancel}>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div onclick={preventClose}>
{@render children()}
</div>
</dialog>
<style>
:global(html:has(dialog[open])) {
overflow: hidden;
}
</style>
util.svelte
<script module lang="ts">
export function stop<T extends Element>(func?: (this: T, evt: Event) => void) {
return function(this: T, evt: Event) {
if (func !== undefined) {
evt.stopPropagation();
func.call(this, evt);
}
};
}
</script>
簡単な解説
開閉制御
dialog
タグをモーダルとして開くにはshowModal
関数の実行が必要。そのため、$props
の受け取り変数open
の状態変化に応じてshowModal
又はclose
を呼び出すようにしている。トグル用関数をbindable
にしようかとも思ったが、開閉状態を直接読み書きできたほうが利便性が高そうなため$effect.pre
で制御することにした。
追加的な閉じる制御
- モーダルウインドウ外(
backdrop
)のクリックで閉じるためにdialog
タグにonclick
イベントを登録 - モーダルウインドウ内のクリックで閉じないために中の
div
タグにstopPropagation
を含むonclick
イベントを登録 - 通常
Esc
キーで閉じるが、標準の閉じる操作を制御するclosable
変数のためにonkeydown
イベントでpreventDefault
を用いて抑制可能に-
oncancel
イベントで抑制したかったが、そうするとChromium系はEsc
キー2回押しで強制的に閉じられるため妥協
-
- 各関数をわざわざ変数化しているのはEventListenerへの不要な登録を避けるため
背景スクロールの停止
html:has(dialog[open])
でモーダルウィンドウを開いている時にCSSを有効化できる。:global
で設定を全体化。iOSの場合はこの方法で抑制できないとのこと。
雑記
モードレスな領域(ウィンドウ)はポップオーバーAPIを使用するとJSレスで簡単に実現できるようになったみたいです。色々と進化していますね。
Discussion