124 lines
3.0 KiB
Vue
124 lines
3.0 KiB
Vue
<script setup lang="ts">
|
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
|
|
const props = defineProps<{
|
|
title: string
|
|
description: string
|
|
confirmLabel: string
|
|
cancelLabel: string
|
|
pendingLabel: string
|
|
tone: 'danger' | 'neutral'
|
|
action: () => Promise<void>
|
|
}>()
|
|
|
|
const emit = defineEmits<{ close: [confirmed: boolean] }>()
|
|
|
|
const open = defineModel<boolean>('open', { default: true })
|
|
const pending = ref(false)
|
|
const error = ref('')
|
|
const cancelButton = ref<{ $el: HTMLElement } | null>(null)
|
|
let settled = false
|
|
|
|
const confirmColor = computed(() => (props.tone === 'danger' ? 'error' : 'primary'))
|
|
const confirmText = computed(() => (pending.value ? props.pendingLabel : props.confirmLabel))
|
|
|
|
onMounted(async () => {
|
|
await nextTick()
|
|
cancelButton.value?.$el?.focus()
|
|
})
|
|
|
|
function settle(confirmed: boolean) {
|
|
if (settled) {
|
|
return
|
|
}
|
|
settled = true
|
|
open.value = false
|
|
emit('close', confirmed)
|
|
}
|
|
|
|
watch(open, (value) => {
|
|
if (!value) {
|
|
settle(false)
|
|
}
|
|
})
|
|
|
|
async function onConfirm() {
|
|
if (pending.value) {
|
|
return
|
|
}
|
|
pending.value = true
|
|
error.value = ''
|
|
try {
|
|
await props.action()
|
|
settle(true)
|
|
} catch (err) {
|
|
error.value = err instanceof Error ? err.message : 'Something went wrong. Try again.'
|
|
} finally {
|
|
pending.value = false
|
|
}
|
|
}
|
|
|
|
function onCancel() {
|
|
if (pending.value) {
|
|
return
|
|
}
|
|
settle(false)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<UModal
|
|
v-model:open="open"
|
|
:title="title"
|
|
:description="description"
|
|
:dismissible="!pending"
|
|
:close="false"
|
|
:content="{ role: 'alertdialog', 'aria-busy': pending ? 'true' : undefined }"
|
|
:ui="{
|
|
content:
|
|
'divide-y-0 shadow-(--shadow-modal) ring-0 sm:top-[220px] sm:max-w-110 sm:translate-y-0 max-sm:!inset-x-4 max-sm:!top-auto max-sm:!bottom-4 max-sm:!left-4 max-sm:!w-auto max-sm:!max-w-none max-sm:!translate-x-0 max-sm:!translate-y-0',
|
|
header: 'min-h-0 px-6 pt-6 pb-0 sm:px-6 max-sm:px-5 max-sm:pt-5',
|
|
title: 'text-lg max-sm:text-[1.0625rem]/6',
|
|
description: 'mt-2 text-base text-toned max-sm:text-[0.9375rem]/[23px]',
|
|
body: 'px-6 pt-4 pb-0 sm:px-6 sm:pt-4 sm:pb-0 max-sm:px-5',
|
|
footer: 'p-6 sm:px-6 max-sm:p-5',
|
|
}"
|
|
>
|
|
<template v-if="error" #body>
|
|
<UAlert
|
|
color="error"
|
|
icon="i-lucide-circle-x"
|
|
:description="error"
|
|
variant="soft"
|
|
:close="false"
|
|
role="alert"
|
|
/>
|
|
</template>
|
|
<template #footer>
|
|
<div class="flex w-full flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
|
<UButton
|
|
ref="cancelButton"
|
|
color="neutral"
|
|
variant="outline"
|
|
block
|
|
class="h-11 justify-center text-[0.9375rem] text-highlighted disabled:text-dimmed disabled:opacity-100 sm:h-8 sm:w-auto sm:px-3 sm:text-sm"
|
|
:disabled="pending"
|
|
@click="onCancel"
|
|
>
|
|
{{ cancelLabel }}
|
|
</UButton>
|
|
<UButton
|
|
:color="confirmColor"
|
|
:loading="pending"
|
|
:disabled="pending"
|
|
block
|
|
class="h-11 justify-center text-[0.9375rem] sm:h-8 sm:w-auto sm:px-3 sm:text-sm"
|
|
@click="onConfirm"
|
|
>
|
|
{{ confirmText }}
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
</UModal>
|
|
</template>
|