GrConfirmDialog

Package: @feugene/granularitycoreGroup: overlays

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 serverconfirmLoading keeps the window open for the duration of the request, error shows the refusal;
  • the button has to look dangerousconfirmTone colours the confirmation in the tone of the consequence;
  • the focus lands on cancelfocusAction keeps a dangerous action from being confirmed blindly with Enter.

When to take something else

NeedTake
Ask for a value rather than for consentGrPromptDialog
Call a confirmation from codeGrDialogService
A window with arbitrary contentGrDialog
A confirmation right at the button, without a windowGrPopover
Report a result rather than askGrToaster

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…

Code
<GrConfirmDialog />

Install

npm i @feugene/granularity

Import

import { GrConfirmDialog } from '@feugene/granularity/components/GrConfirmDialog'

API

Props

PropTypedefaultDescription
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 | undefinedundefinedThe a11y label of the close button (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 | undefinednullThe 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.
confirmLoadingboolean | undefinedfalseThe loading state of the Confirm button (an async `onConfirm` in flight).
confirmDisabledboolean | undefinedfalseForcibly disables the Confirm button.
closeOnConfirmboolean | undefinedtrueWhether 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`).
focusActionGrConfirmDialogFocusAction | 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.
persistentboolean | undefinedfalseA 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.
modelValuerequiredboolean

Slots

SlotTypeDescription
defaultanyThe 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.
footeranyThe buttons of the dialog instead of the "cancel and confirm" pair.

Events

EventTypeDescription
update:modelValue[value: boolean]
confirm[]
cancel[]

Examples 4

Destructive confirmation

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>

Compact action sizes

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>

Custom summary body

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>

Async confirmation with server error

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

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>

Component documentationAll components