Skip to content

Instantly share code, notes, and snippets.

@kobitoDevelopment
Created March 15, 2025 00:54
Show Gist options
  • Save kobitoDevelopment/032e9bc2de2dbac354c4d53f06e9d936 to your computer and use it in GitHub Desktop.
Save kobitoDevelopment/032e9bc2de2dbac354c4d53f06e9d936 to your computer and use it in GitHub Desktop.
<template>
<MyDialog ref="dialogRef">
<template #open>ダイアログを開く</template>
<template #inner>
<p>ダイアログの中身</p>
<button @click="closeDialogFromParent">閉じる</button>
</template>
</MyDialog>
</template>
<script setup lang="ts">
/* 「ダイアログを閉じる」というアクションを、閉じるボタン以外からも行えるように */
import MyDialog from "../components/MyDialog.vue";
const dialogRef = ref<InstanceType<typeof MyDialog> | null>(null);
const closeDialogFromParent = () => {
dialogRef.value?.closeDialog();
};
</script>
<template>
<!-- ダイアログを開くボタン -->
<button class="open" aria-expanded="false" @click="openDialog">
<slot name="open">
<span class="text">ダイアログを開く</span>
</slot>
</button>
<!-- ダイアログ -->
<dialog ref="dialog" class="my-dialog">
<button ref="close" class="close" aria-label="ダイアログを閉じる" @click="closeDialog">
<slot name="close">X</slot>
</button>
<div tabindex="0" class="inner" role="presentation">
<slot name="inner" />
</div>
<div tabindex="0" @focus="handleEndFocus"></div>
</dialog>
</template>
<script lang="ts" setup>
const dialog = ref<HTMLDialogElement>();
/* dialogを開く関数 */
const openDialog = async () => {
await nextTick();
if (!dialog.value) {
throw new Error("dialog要素はnull");
}
dialog.value.addEventListener("keydown", (e) => {
if (dialog.value?.open && e.key === "Escape") {
e.stopPropagation();
closeDialog();
}
});
dialog.value.showModal();
/* html要素とbody要素の両方にoverflowを記述 */
document.body.style.overflow = "hidden";
document.documentElement.style.overflow = "hidden";
};
/* dialogを閉じる関数 */
const closeDialog = () => {
if (!dialog.value) {
throw new Error("dialog要素はnull");
}
dialog.value.close();
/* html要素とbody要素の両方にoverflowを記述 */
document.body.style.overflow = "";
document.documentElement.style.overflow = "";
};
/* ダイアログ内のフォーカスを制御する */
const close = ref<HTMLElement>();
const handleEndFocus = () => {
close.value?.focus();
};
/* 「ダイアログを閉じる」というアクションを、閉じるボタン以外からも行えるように */
defineExpose({
closeDialog,
});
/* ダイアログのbackdropをクリックしたら閉じる関数 */
const handleBackdropClick = (e: MouseEvent) => {
if (e.target === dialog.value) {
closeDialog();
}
};
/* ダイアログのbackdropをクリックしたら閉じるイベントを登録 */
onMounted(() => {
if (!dialog.value) {
throw new Error("dialog要素はnull");
}
dialog.value.addEventListener("click", handleBackdropClick);
});
onBeforeUnmount(() => {
document.body.style.overflow = "";
document.documentElement.style.overflow = "";
});
</script>
<style scoped>
.open {
cursor: pointer;
}
.my-dialog {
position: fixed;
top: 50%;
left: 50%;
max-height: unset; /* dialogのデフォルトのmax-heightをリセット */
height: max-content; /* autoにすると、十分に画面縦幅がある場合でもダイアログに縦スクロールが生まれる場合がある */
max-width: initial; /* dialogのデフォルトのmax-widthをリセット */
width: 90%;
padding: 0; /* dialogのデフォルトのpaddingをリセット */
margin: 0; /* dialogのデフォルトのmarginをリセット */
background-color: rgb(0 0 0 / 0%); /* dialogにデフォルトで指定される白の背景色を透明にする */
opacity: 0;
transform: translateX(-50%) translateY(-50%);
}
.my-dialog[open] {
animation: fade-in 0.3s forwards;
}
.my-dialog::backdrop {
background-color: rgb(0 0 0 / 80%);
cursor: pointer;
}
.my-dialog > .inner {
background-color: #fff;
height: max-content;
max-height: 90vh; /* 先祖要素にmax-contentを指定した場合、その子孫要素の単位に%を使うとwebkitで値が0になる場合があるためvhを使用 */
overflow-y: auto;
width: 100%;
}
.my-dialog > .inner:focus-visible {
outline: none;
}
.my-dialog > .close {
aspect-ratio: 1;
cursor: pointer;
position: absolute;
right: 2%;
top: 2%;
width: 20px;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment