GrConfirmDialog
Asks to confirm a potentially important or dangerous action.
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- the action is irreversible — deleting, revoking access, cancelling an order: the pause before it is the point of the component;
- the confirmation goes to a server —
confirmLoadingkeeps the window open for the duration of the request,errorshows the refusal; - the button has to look dangerous —
confirmTonecolours the confirmation in the tone of the consequence; - the focus lands on cancel —
focusActionkeeps a dangerous action from being confirmed blindly withEnter.
When to take something else
| Need | Take |
|---|---|
| Ask for a value rather than for consent | GrPromptDialog |
| Call a confirmation from code | GrDialogService |
| A window with arbitrary content | GrDialog |
| A confirmation right at the button, without a window | GrPopover |
| Report a result rather than ask | GrToaster |
Harmless actions do not need confirming: a dialog that is always answered “yes” stops being read and will not stop anyone on the single occasion when it mattered. A reversible action is better covered by an undo in a toast.
The focus on opening
focusAction decides which action gets the focus: 'cancel' (the default),
'confirm' or 'none' — the focus stays on the panel of the window.
The default is 'cancel' precisely because a confirmation window exists for the sake
of a risky action: Enter pressed right after the opening has to cancel, not delete.
'confirm' is appropriate where the confirmation is a routine (“Save before
leaving?”).
The focus is set from the side of the content after rendering rather than with the
initialFocus prop of GrModal: the element is born inside the subtree of the
dialog, and returning it upwards through a prop closes the render into a loop.
The degradation is silent. If the #footer slot has been overridden, the component
does not know your buttons and the focus stays on the panel; the same happens with
focusAction="confirm" and confirmDisabled. This is not a configuration error: a
footer of your own means the consumer disposes of the focus.
Asynchronous confirmation
closeOnConfirm: false hands the closing outside: the component sends confirm and
stays open until the consumer clears v-model. For the duration of the operation
there are confirmLoading (the button shows loading and accepts no clicks) and
error (a banner with the server’s answer in the body of the window, and the
#error slot — if a banner is not enough).
persistent switches off the soft ways of closing for that time — Esc and a
click on the backdrop. The cross and “Cancel” remain: a window with no way out is a
trap, not a protection. The same prop with the same semantics exists in
GrPromptDialog and GrDrawer.
<GrConfirmDialog
v-model="open"
confirm-tone="danger"
:confirm-loading="loading"
:error="error"
:close-on-confirm="false"
persistent
@confirm="submit"
/>
useDialogService switches persistent on for all of its windows itself — there
loading means the request has already left.
Playground 17
Loading…
<GrConfirmDialog />Install
npm i @feugene/granularityImport
import { GrConfirmDialog } from '@feugene/granularity/components/GrConfirmDialog'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
title | string | undefined | undefined | — |
size | "sm" | "md" | "lg" | "xl" | "full" | undefined | undefined | — |
description | string | undefined | undefined | — |
closeOnBackdrop | boolean | undefined | true | — |
closeOnEsc | boolean | undefined | true | — |
showHeader | boolean | undefined | true | — |
showCloseButton | boolean | undefined | true | — |
headerConfig | GrDialogSectionConfig | undefined | undefined | — |
footerConfig | GrDialogSectionConfig | undefined | undefined | — |
bodyConfig | GrDialogSectionConfig | undefined | undefined | — |
closeLabel | string | undefined | undefined | The a11y label of the close button (i18n). |
buttonSize | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
confirmText | string | undefined | undefined | — |
cancelText | string | undefined | undefined | — |
confirmVariant | GrButtonVariant | undefined | "primary" | — |
confirmTone | "primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined | "primary" | — |
error | ResponseErrorInfo | null | undefined | null | The structure of a server error to show in the body of the dialog (through `GrResponseErrorBanner`). It is used by the imperative `useDialogService` for an async `onConfirm`. `null` — the block is hidden. |
confirmLoading | boolean | undefined | false | The loading state of the Confirm button (an async `onConfirm` in flight). |
confirmDisabled | boolean | undefined | false | Forcibly disables the Confirm button. |
closeOnConfirm | boolean | undefined | true | Whether to close the dialog automatically on a click of Confirm. `true` by default (the historical behaviour). `false` hands the closing outside (needed by `useDialogService`, which waits for the result of an async `onConfirm`). |
focusAction | GrConfirmDialogFocusAction | undefined | "cancel" | Which action gets the focus on opening. "Cancel" by default: a confirmation is sometimes destructive, and `Enter` right after the opening must not run it. `none` leaves the focus on the panel of the window. The name is deliberately not `initialFocus`: in `GrModal`/`GrDrawer` that is the name of the prop carrying an element, while here an action is chosen. |
persistent | boolean | undefined | false | A ban on the "soft" ways of closing (Esc, a click on the backdrop) while the confirmation is running (`confirmLoading`). The close button and "Cancel" remain: a window with no way out is a trap. |
modelValuerequired | boolean | — | — |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | The content of the dialog instead of the `message` prop. |
error | { error: ResponseErrorInfo | null; } | Your own presentation of the error instead of the built-in banner. |
footer | any | The buttons of the dialog instead of the "cancel and confirm" pair. |
Events
| Event | Type | Description |
|---|---|---|
update:modelValue | [value: boolean] | — |
confirm | [] | — |
cancel | [] | — |
Examples 4
Destructive confirmation
<script setup lang="ts">
import { ref } from 'vue'
import { GrBadge, GrButton, GrConfirmDialog } from '@feugene/granularity'
const open = ref(false)
const lastAction = ref<'confirm' | 'cancel' | 'idle'>('idle')
</script>
<template>
<div class="grid gap-3">
<div class="flex items-center gap-3">
<GrButton variant="primary" tone="danger" class="justify-self-start" @click="open = true">
Delete workspace
</GrButton>
<GrBadge size="sm" :tone="lastAction === 'confirm' ? 'danger' : 'neutral'">
{{ lastAction }}
</GrBadge>
</div>
<GrConfirmDialog
v-model="open"
title="Delete workspace?"
description="This action revokes links, members and scheduled automations."
confirm-text="Delete"
confirm-tone="danger"
@confirm="lastAction = 'confirm'"
@cancel="lastAction = 'cancel'"
/>
</div>
</template>Compact action sizes
<script setup lang="ts">
import { ref } from 'vue'
import { GrButton, GrConfirmDialog } from '@feugene/granularity'
const open = ref(false)
const approved = ref(false)
</script>
<template>
<div class="grid gap-3">
<GrButton variant="outline" class="justify-self-start" @click="open = true">
Open compact confirm
</GrButton>
<div class="text-xs text-[var(--gr-muted-fg)]">
Approved in current session: <span class="font-medium text-[var(--gr-fg)]">{{ approved ? 'yes' : 'no' }}</span>
</div>
<GrConfirmDialog
v-model="open"
title="Promote release candidate"
description="Smaller button sizes are handy in dense approval flows."
confirm-text="Promote"
cancel-text="Back"
button-size="sm"
@confirm="approved = true"
@cancel="approved = false"
/>
</div>
</template>Custom summary body
<script setup lang="ts">
import { ref } from 'vue'
import { GrBadge, GrButton, GrConfirmDialog } from '@feugene/granularity'
const open = ref(false)
const archived = ref(false)
</script>
<template>
<div class="grid gap-3">
<div class="flex items-center gap-3">
<GrButton class="justify-self-start" @click="open = true">
Archive sprint
</GrButton>
<GrBadge size="sm" :tone="archived ? 'success' : 'neutral'">
{{ archived ? 'archived' : 'active' }}
</GrBadge>
</div>
<GrConfirmDialog v-model="open" title="Archive sprint" confirm-text="Archive" @confirm="archived = true">
<div class="grid gap-3 text-sm text-[var(--gr-muted-fg)]">
<p>Кастомный slot позволяет вывести richer summary прямо внутри confirm shell.</p>
<ul class="list-disc pl-5">
<li>18 tasks will move to history</li>
<li>2 blocked items will stay pinned</li>
</ul>
</div>
</GrConfirmDialog>
</div>
</template>Async confirmation with server error
<script setup lang="ts">
import { ref } from 'vue'
import type { ResponseErrorInfo } from '@feugene/granularity'
import { GrBadge, GrButton, GrConfirmDialog, GrSegmented } from '@feugene/granularity'
const open = ref(false)
const loading = ref(false)
const error = ref<ResponseErrorInfo | null>(null)
const status = ref('Ничего не отправляли')
// Первая попытка отвечает отказом, вторая проходит — так видно и баннер, и то,
// что окно остаётся открытым для повтора.
let attempt = 0
const focusAction = ref<'cancel' | 'confirm'>('cancel')
function openDialog() {
attempt = 0
error.value = null
status.value = 'Ничего не отправляли'
open.value = true
}
async function onConfirm() {
attempt += 1
loading.value = true
error.value = null
await new Promise(resolve => setTimeout(resolve, 1500))
loading.value = false
if (attempt === 1) {
error.value = { kind: 'unknown', message: 'Сервер отклонил запрос. Попробуйте ещё раз.', raw: null }
return
}
status.value = 'Рабочая область удалена'
open.value = false
}
</script>
<template>
<div class="grid gap-3">
<div class="flex flex-wrap items-center gap-3">
<GrSegmented
v-model="focusAction"
size="sm"
:options="[
{ value: 'cancel', label: 'focusAction: cancel' },
{ value: 'confirm', label: 'focusAction: confirm' },
]"
/>
<GrButton variant="primary" tone="danger" @click="openDialog">
Удалить рабочую область
</GrButton>
<GrBadge size="sm" :tone="status.startsWith('Рабочая') ? 'danger' : 'neutral'">
{{ status }}
</GrBadge>
</div>
<GrConfirmDialog
v-model="open"
title="Удалить рабочую область?"
description="Первая попытка вернёт ошибку сервера — окно останется открытым для повтора."
confirm-text="Удалить"
confirm-tone="danger"
:focus-action="focusAction"
:confirm-loading="loading"
:error="error"
:close-on-confirm="false"
persistent
@confirm="onConfirm"
/>
</div>
</template>