GrConfirmDialog
Берут, когда действие необратимо.
Когда брать
- действие необратимо — удаление, отзыв доступа, отмена заказа: пауза перед ним и есть смысл компонента;
- подтверждение идёт на сервер —
confirmLoadingдержит окно открытым на время запроса,errorпоказывает отказ; - кнопка должна выглядеть опасной —
confirmToneкрасит подтверждение в тон последствия; - фокус ставится на отмену —
focusActionне даёт подтвердить опасное действие вслепую поEnter.
Когда взять другое
| Нужно | Берите |
|---|---|
| Запросить значение, а не согласие | GrPromptDialog |
| Вызвать подтверждение из кода | GrDialogService |
| Окно с произвольным содержимым | GrDialog |
| Подтверждение прямо у кнопки, без окна | GrPopover |
| Сообщить о результате, а не спросить | GrToaster |
Подтверждать безобидные действия не нужно: диалог, который всегда отвечают «да», перестаёт читаться и не остановит в тот единственный раз, когда это было важно. Обратимое действие лучше закрывает отмена в тосте.
Фокус при открытии
focusAction решает, какое действие получает фокус: 'cancel' (по умолчанию),
'confirm' или 'none' — фокус остаётся на панели окна.
Дефолт именно 'cancel', потому что окно подтверждения существует ради
рискованного действия: Enter, нажатый сразу после открытия, обязан отменять,
а не удалять. 'confirm' уместен там, где подтверждение — рутина
(«Сохранить перед выходом?»).
Фокус ставится со стороны содержимого после отрисовки, а не пропом
initialFocus у GrModal: элемент рождается внутри поддерева диалога, и
возврат его же пропом наверх замыкает рендер в цикл.
Деградация тихая. Переопределили слот #footer — своих кнопок компонент не
знает, фокус остаётся на панели; то же при focusAction="confirm" с
confirmDisabled. Это не ошибка конфигурации: свой футер значит, что фокусом
распоряжается потребитель.
Асинхронное подтверждение
closeOnConfirm: false отдаёт закрытие наружу: компонент шлёт confirm и
остаётся открытым, пока потребитель не снимет v-model. На время операции —
confirmLoading (кнопка показывает загрузку и не принимает клики) и error
(баннер ответа сервера в теле окна, слот #error — если баннера мало).
persistent на это время отключает мягкие способы закрытия — Esc и клик
по бэкдропу. Крестик и «Отмена» остаются: окно без единого выхода — ловушка, а
не защита. Тот же проп с той же семантикой есть у GrPromptDialog и GrDrawer.
<GrConfirmDialog
v-model="open"
confirm-tone="danger"
:confirm-loading="loading"
:error="error"
:close-on-confirm="false"
persistent
@confirm="submit"
/>
useDialogService включает persistent всем своим окнам сам — там loading
означает, что запрос уже ушёл.
Playground 17
Загружается…
<GrConfirmDialog />Установка
npm i @feugene/granularityИмпорт
import { GrConfirmDialog } from '@feugene/granularity/components/GrConfirmDialog'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
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 | A11y-лейбл кнопки закрытия (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 | Структура ошибки ответа сервера для показа в теле диалога (через `GrResponseErrorBanner`). Используется императивным `useDialogService` для async-`onConfirm`. `null` — блок скрыт. |
confirmLoading | boolean | undefined | false | Состояние загрузки кнопки Confirm (async-`onConfirm` in-flight). |
confirmDisabled | boolean | undefined | false | Принудительно дизейблит кнопку Confirm. |
closeOnConfirm | boolean | undefined | true | Закрывать ли диалог автоматически по клику Confirm. По умолчанию `true` (историческое поведение). `false` — отдаёт управление закрытием наружу (нужно `useDialogService`, который ждёт результат async-`onConfirm`). |
focusAction | GrConfirmDialogFocusAction | undefined | "cancel" | Какое действие получает фокус при открытии. По умолчанию «Отмена»: подтверждение бывает деструктивным, и `Enter` сразу после открытия не должен его запускать. `none` оставляет фокус на панели окна. Имя не `initialFocus` намеренно: у `GrModal`/`GrDrawer` так называется проп с элементом, а здесь выбирается действие. |
persistent | boolean | undefined | false | Запрет закрытия «мягкими» способами (Esc, клик по бэкдропу), пока идёт подтверждение (`confirmLoading`). Кнопка закрытия и «Отмена» остаются: окно без единого выхода — ловушка. |
modelValueобязательный | boolean | — | — |
Slots
| Slot | Type | Описание |
|---|---|---|
default | any | Содержимое диалога вместо пропа `message`. |
error | { error: ResponseErrorInfo | null; } | Разбор ошибки вместо встроенного баннера. |
footer | any | Кнопки диалога вместо пары «отмена и подтверждение». |
Events
| Event | Type | Описание |
|---|---|---|
update:modelValue | [value: boolean] | — |
confirm | [] | — |
cancel | [] | — |
Примеры 4
Подтверждение разрушительного действия
Главный сценарий для GrConfirmDialog: destructive action с кастомным текстом и semantic confirmTone у confirm-кнопки.
<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>Компактные размеры действий
Отдельно проверяем buttonSize, cancelText и плотные approval flows.
<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>Своё тело окна со сводкой
Подтверждаем, что в confirm-shell можно выводить richer body через default slot, а не только plain description.
<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>Асинхронное подтверждение с ошибкой сервера
closeOnConfirm: false отдаёт закрытие наружу, confirmLoading держит кнопку, persistent снимает Esc и бэкдроп на время операции, а error рисует ответ сервера, не закрывая окно. focusAction выбирает, какое действие получает фокус при открытии.
<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>