Files
mathew/frontend/app/components/ConfirmModal.vue
2026-09-30 01:49:35 +02:00

63 lines
2.0 KiB
Vue

<script setup lang="ts">
/* Yes/no popup, same shape as the settings and cheatsheet modals: teleported
overlay, dismissed by the backdrop or the safe button. Esc is left to the
caller, which is how the editor already orders its own popups. */
const props = defineProps<{
open: boolean
title?: string
message?: string
confirmLabel?: string
cancelLabel?: string
}>()
const { t } = useI18n()
/* the defaults live here rather than in withDefaults so they follow the
interface language as it changes */
const titleText = computed(() => props.title ?? t('common.areYouSure'))
const confirmText = computed(() => props.confirmLabel ?? t('common.confirm'))
const cancelText = computed(() => props.cancelLabel ?? t('common.cancel'))
const emit = defineEmits<{ confirm: []; cancel: [] }>()
const cancelButtonEl = ref<HTMLButtonElement | null>(null)
/* land on the safe button when the popup opens, so Enter can never trigger
the destructive action by accident */
watch(
() => props.open,
async (isOpen) => {
if (!isOpen) return
await nextTick()
cancelButtonEl.value?.focus()
},
{ flush: 'post' }
)
</script>
<template>
<Teleport to="body">
<Transition name="confirm-popup">
<div v-if="open" class="confirm-overlay" @click.self="emit('cancel')">
<div class="confirm-modal" role="dialog" aria-modal="true" :aria-label="titleText">
<h2 class="confirm-title">{{ titleText }}</h2>
<p v-if="message" class="confirm-message">{{ message }}</p>
<div class="confirm-actions">
<button
ref="cancelButtonEl"
type="button"
class="btn btn-ghost"
@click="emit('cancel')"
>
{{ cancelText }}
</button>
<button type="button" class="btn btn-danger" @click="emit('confirm')">
{{ confirmText }}
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>