GrConfirmDialog

Пакет: @feugene/granularityядроГруппа: Слои

Берут, когда действие необратимо.

Когда брать

  • действие необратимо — удаление, отзыв доступа, отмена заказа: пауза перед ним и есть смысл компонента;
  • подтверждение идёт на сервер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

PropTypeпо умолчаниюОписание
titlestring | undefinedundefined
size"sm" | "md" | "lg" | "xl" | "full" | undefinedundefined
descriptionstring | undefinedundefined
closeOnBackdropboolean | undefinedtrue
closeOnEscboolean | undefinedtrue
showHeaderboolean | undefinedtrue
showCloseButtonboolean | undefinedtrue
headerConfigGrDialogSectionConfig | undefinedundefined
footerConfigGrDialogSectionConfig | undefinedundefined
bodyConfigGrDialogSectionConfig | undefinedundefined
closeLabelstring | undefinedundefinedA11y-лейбл кнопки закрытия (i18n).
buttonSize"xs" | "sm" | "md" | "lg" | undefinedundefined
confirmTextstring | undefinedundefined
cancelTextstring | undefinedundefined
confirmVariantGrButtonVariant | undefined"primary"
confirmTone"primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined"primary"
errorResponseErrorInfo | null | undefinednullСтруктура ошибки ответа сервера для показа в теле диалога (через `GrResponseErrorBanner`). Используется императивным `useDialogService` для async-`onConfirm`. `null` — блок скрыт.
confirmLoadingboolean | undefinedfalseСостояние загрузки кнопки Confirm (async-`onConfirm` in-flight).
confirmDisabledboolean | undefinedfalseПринудительно дизейблит кнопку Confirm.
closeOnConfirmboolean | undefinedtrueЗакрывать ли диалог автоматически по клику Confirm. По умолчанию `true` (историческое поведение). `false` — отдаёт управление закрытием наружу (нужно `useDialogService`, который ждёт результат async-`onConfirm`).
focusActionGrConfirmDialogFocusAction | undefined"cancel"Какое действие получает фокус при открытии. По умолчанию «Отмена»: подтверждение бывает деструктивным, и `Enter` сразу после открытия не должен его запускать. `none` оставляет фокус на панели окна. Имя не `initialFocus` намеренно: у `GrModal`/`GrDrawer` так называется проп с элементом, а здесь выбирается действие.
persistentboolean | undefinedfalseЗапрет закрытия «мягкими» способами (Esc, клик по бэкдропу), пока идёт подтверждение (`confirmLoading`). Кнопка закрытия и «Отмена» остаются: окно без единого выхода — ловушка.
modelValueобязательныйboolean

Slots

SlotTypeОписание
defaultanyСодержимое диалога вместо пропа `message`.
error{ error: ResponseErrorInfo | null; }Разбор ошибки вместо встроенного баннера.
footeranyКнопки диалога вместо пары «отмена и подтверждение».

Events

EventTypeОписание
update:modelValue[value: boolean]
confirm[]
cancel[]

Примеры 4

Подтверждение разрушительного действия

Главный сценарий для GrConfirmDialog: destructive action с кастомным текстом и semantic confirmTone у confirm-кнопки.

idle

Destructive
<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.

Approved in current session: no

Button Matrix
<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.

active

Custom 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>

Асинхронное подтверждение с ошибкой сервера

closeOnConfirm: false отдаёт закрытие наружу, confirmLoading держит кнопку, persistent снимает Esc и бэкдроп на время операции, а error рисует ответ сервера, не закрывая окно. focusAction выбирает, какое действие получает фокус при открытии.

Ничего не отправляли

Async Confirm
<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>

Документация компонентаВсе компоненты