63 lines
2.0 KiB
Vue
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>
|