<script setup>
import { nextTick, onBeforeUnmount, ref, watch } from "vue";
const props = defineProps({
open: { type: Boolean, default: false },
title: { type: String, default: "" },
closeOnBackdrop: { type: Boolean, default: true }
});
const emit = defineEmits(["update:open", "close"]);
const titleId = `gn-modal-title-${Math.random().toString(36).slice(2)}`;
const dialogRef = ref(null);
const rendered = ref(false);
const stateClass = ref("");
let closeTimer = null;
let previousFocus = null;
function trapFocus(event) {
if (event.key !== "Tab" || !dialogRef.value) return;
const focusable = Array.from(
dialogRef.value.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (!focusable.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
function onKeydown(event) {
if (event.key === "Escape") {
event.preventDefault();
requestClose();
return;
}
trapFocus(event);
}
function requestClose() {
emit("update:open", false);
emit("close");
}
function openModal() {
clearTimeout(closeTimer);
previousFocus = document.activeElement;
rendered.value = true;
stateClass.value = "";
document.addEventListener("keydown", onKeydown);
nextTick(() => {
requestAnimationFrame(() => {
stateClass.value = "a-show";
dialogRef.value?.focus();
});
});
}
function closeModal() {
document.removeEventListener("keydown", onKeydown);
previousFocus?.focus?.();
previousFocus = null;
if (!rendered.value) return;
clearTimeout(closeTimer);
stateClass.value = "a-hide";
closeTimer = setTimeout(() => {
rendered.value = false;
stateClass.value = "";
}, 300);
}
watch(
() => props.open,
(open) => {
if (open) {
openModal();
} else {
closeModal();
}
},
{ immediate: true }
);
onBeforeUnmount(() => {
clearTimeout(closeTimer);
document.removeEventListener("keydown", onKeydown);
});
</script>
<template>
<Teleport to="body">
<div v-if="rendered" class="modal" :class="stateClass" :aria-hidden="props.open ? 'false' : 'true'">
<div class="modal-backdrop" @click="props.closeOnBackdrop && requestClose()" />
<div
ref="dialogRef"
class="modal-dialog"
role="dialog"
aria-modal="true"
:aria-labelledby="titleId"
tabindex="-1"
>
<header class="modal-header">
<h4 :id="titleId" class="modal-title">
<slot name="title">{{ props.title }}</slot>
</h4>
<button class="btn-icon modal-close" type="button" aria-label="Close" @click="requestClose">
<i class="ph ph-x" aria-hidden="true" />
</button>
</header>
<div class="modal-panel">
<div class="modal-body">
<slot />
</div>
<footer v-if="$slots.footer || $slots.actions" class="modal-footer">
<slot name="footer" />
<div v-if="$slots.actions" class="actions">
<slot name="actions" :close="requestClose" />
</div>
</footer>
</div>
</div>
</div>
</Teleport>
</template>