GrDialog
Берут, когда окно с шапкой, телом и подвалом.
Когда брать
- окно с шапкой, телом и подвалом — типовая раскладка уже собрана: заголовок, кнопка закрытия, ряд действий;
- внутри форма — тело прокручивается независимо, подвал с кнопками остаётся на месте;
- окно открывается разметкой —
v-modelв шаблоне, а не вызовом из кода; - фокус должен встать на конкретный элемент —
initialFocusвместо первого попавшегося контрола.
Когда взять другое
| Нужно | Берите |
|---|---|
| Раскладка нестандартная: во весь экран, без шапки, свой каркас | GrModal |
| Спросить «да/нет» | GrConfirmDialog |
| Запросить одно значение | GrPromptDialog |
| Вызвать окно из кода, без разметки в шаблоне | GrDialogService |
| Панель у края экрана | GrDrawer |
| Содержимое привязано к кнопке, а не к центру экрана | GrPopover |
Секции
headerConfig, bodyConfig, footerConfig задают паддинги и рамку каждой
секции (bordered у тела игнорируется: отдельной верхней рамки у него нет).
Подвал рендерится, только если передан слот #footer.
Шапка, подвал и кнопка закрытия — отдельные компоненты того же subpath:
GrDialogHeader, GrDialogFooter, GrDialogCloseButton. Внутри GrDialog они
уже собраны; берут их напрямую, когда раскладку строят на GrModal, а шапку и
подвал хотят те же самые.
Слот #header заменяет содержимое шапки целиком — кнопка закрытия при этом
остаётся на месте. Доступное имя окна в этом случае уходит вниз sr-only
заголовком: пользовательская шапка не обязана содержать DialogTitle, а окно
без имени диктор озвучит как безымянный «диалог».
Скролл длинного содержимого
scrollBehavior:
outside(по умолчанию) — скроллится весь оверлей, окно уезжает вверх вместе со страницей;inside— шапка и подвал закреплены, едет только тело.
<GrDialog v-model="open" title="Настройки профиля" scroll-behavior="inside">
<ProfileForm />
<template #footer>
<GrButton @click="submit">Сохранить</GrButton>
</template>
</GrDialog>
Форма на двадцать полей — ровно этот случай: с outside кнопки уезжают за
экран вместе с содержимым, и до «Сохранить» надо доскроллить. Технически шапка
и подвал уходят в layout-слоты GrModal (#header/#footer), которые лежат
вне скроллящегося тела; тело при этом попадает в таб-порядок, чтобы длинный
текст без единого фокусируемого элемента можно было прокрутить с клавиатуры.
Во весь экран — size="full": панель занимает вьюпорт целиком, без полей и
скруглений.
Императивный API
open(), close() и toggle() через ref на компоненте — как у остальных
оверлеев. Диалог управляемый, и методы эмитят update:modelValue: состояние
живёт в v-model родителя, а не внутри (подробнее — GrModal.md).
Фокус и жизненный цикл
initialFocus задаёт элемент, получающий фокус при открытии; по умолчанию это
панель окна. Элемент из самого диалога сюда передавать нельзя — проп,
возвращающий наверх то, что рождено внутри поддерева, замыкает рендер в цикл.
Фокус на собственном содержимом ставится из содержимого; как это делается —
видно в GrPromptDialog.
opened и closed эмитятся после анимации. closed — единственный безопасный
момент, чтобы размонтировать содержимое или сбросить форму: сделать это по
update:modelValue значит оборвать анимацию закрытия на полпути.
Playground 9
Загружается…
<GrDialog />Установка
npm i @feugene/granularityИмпорт
import { GrDialog } from '@feugene/granularity/components/GrDialog'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
title | string | undefined | undefined | — |
size | "sm" | "md" | "lg" | "xl" | "full" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | Доступное имя окна, когда заголовка нет вовсе: `showHeader: false` без `title` и без слота `#header` оставил бы окно безымянным. |
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). |
scrollBehavior | GrModalScrollBehavior | undefined | "outside" | Кто скроллится при длинном содержимом. При `inside` шапка и подвал закреплены, а едет только тело. |
initialFocus | HTMLElement | null | undefined | null | Элемент, получающий фокус при открытии. По умолчанию — панель окна. Элемент **из самого диалога** сюда передавать нельзя: проп, возвращающий наверх то, что рождено внутри поддерева, замыкает рендер в цикл. Фокус на своём содержимом ставится из содержимого — так это сделано в `GrPromptDialog`. |
modelValueобязательный | boolean | — | — |
Slots
| Slot | Type | Описание |
|---|---|---|
default | any | — |
header | { title?: string | undefined; } | — |
footer | any | — |
Events
| Event | Type | Описание |
|---|---|---|
update:modelValue | [value: boolean] | — |
opened | [] | — |
closed | [] | — |
Methods / Expose
| Methods / Expose | Type | Описание |
|---|---|---|
open | () => void | — |
close | () => void | — |
toggle | () => void | — |
Примеры 4
Базовая оболочка окна
Показываем базовый слой над GrModal: готовый header/footer shell для review, approval и confirm-like сценариев.
<script setup lang="ts">
import { ref } from 'vue'
import { GrBadge, GrButton, GrDialog } from '@feugene/granularity'
const open = ref(false)
</script>
<template>
<div class="grid gap-3">
<GrButton class="justify-self-start" @click="open = true">
Open review dialog
</GrButton>
<GrDialog v-model="open" title="Publish weekly digest" size="sm">
<div class="grid gap-4 text-sm text-[var(--gr-muted-fg)]">
<p>
`GrDialog` assembles a ready header/footer shell on top of `GrModal`, so it is convenient for simple approval flows.
</p>
<div class="flex flex-wrap items-center gap-2">
<GrBadge size="sm" tone="info">
12 recipients
</GrBadge>
<GrBadge size="sm" tone="neutral">
Draft ready
</GrBadge>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-3">
<GrButton variant="outline" @click="open = false">
Cancel
</GrButton>
<GrButton @click="open = false">
Publish
</GrButton>
</div>
</template>
</GrDialog>
</div>
</template>Настройка секций и внутреннее состояние
Демонстрируем headerConfig / footerConfig и локальное состояние формы внутри dialog-shell.
<script setup lang="ts">
import { ref } from 'vue'
import { GrButton, GrDialog, GrCheckbox } from '@feugene/granularity'
const open = ref(false)
const confirmed = ref(false)
function openDialog() {
confirmed.value = false
open.value = true
}
</script>
<template>
<div class="grid gap-3">
<GrButton variant="outline" class="justify-self-start" @click="openDialog">
Open stateful dialog
</GrButton>
<div class="text-xs text-[var(--gr-muted-fg)]">
Footer action enabled: <span class="font-medium text-[var(--gr-fg)]">{{ confirmed ? 'yes' : 'no' }}</span>
</div>
<GrDialog
v-model="open"
title="Share workspace"
:header-config="{ paddingX: 'px-4', paddingY: 'py-3' }"
:footer-config="{ paddingX: 'px-4', paddingY: 'py-3', bordered: false }"
>
<div class="grid gap-4 text-sm text-[var(--gr-muted-fg)]">
<p>
The internal form state keeps living inside the dialog shell, while section config helps adapt density to compact workflows.
</p>
<div class="flex items-start gap-3 rounded-lg border border-[var(--gr-brd)] p-3 text-[var(--gr-fg)]">
<GrCheckbox v-model="confirmed">I reviewed access levels and notification scope.</GrCheckbox>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-3">
<GrButton variant="outline" @click="open = false">
Later
</GrButton>
<GrButton :disabled="!confirmed" @click="open = false">
Share workspace
</GrButton>
</div>
</template>
</GrDialog>
</div>
</template>Защищённый бэкдроп для критичных операций
Отдельный сценарий для closeOnBackdrop=false, когда закрытие должно происходить только по явным действиям.
<script setup lang="ts">
import { ref } from 'vue'
import { GrButton, GrDialog } from '@feugene/granularity'
const open = ref(false)
</script>
<template>
<div class="grid gap-3">
<GrButton class="justify-self-start" @click="open = true">
Open guarded dialog
</GrButton>
<GrDialog v-model="open" title="Resolve blockers" :close-on-backdrop="false" :show-close-button="false">
<div class="grid gap-3 text-sm text-[var(--gr-muted-fg)]">
<p>
In critical flows you can disable backdrop close and leave only explicit footer actions.
</p>
<ul class="list-disc pl-5">
<li>2 approvals are still pending</li>
<li>1 issue is waiting for legal review</li>
</ul>
</div>
<template #footer>
<div class="flex justify-end gap-3">
<GrButton variant="outline" @click="open = false">
Keep draft
</GrButton>
<GrButton @click="open = false">
Continue review
</GrButton>
</div>
</template>
</GrDialog>
</div>
</template>Сценарий полезен для финальных шагов publish/delete/release flows.
Длинная форма с закреплённой шапкой и подвалом
scrollBehavior: "inside" оставляет шапку и подвал на месте и скроллит только тело — форма на двадцать полей не уносит кнопки за экран. Переключатель показывает разницу с дефолтным outside.
<script setup lang="ts">
import { computed, ref } from 'vue'
import { GrButton, GrDialog, GrFormField, GrInput, GrSegmented, GrSwitch } from '@feugene/granularity'
const open = ref(false)
const scrollBehavior = ref<'inside' | 'outside'>('inside')
const saved = ref(false)
const fields = Array.from({ length: 12 }, (_, index) => `Поле ${index + 1}`)
const model = ref<Record<string, string>>({})
const hint = computed(() =>
scrollBehavior.value === 'inside'
? 'Шапка и подвал закреплены — «Сохранить» на виду с первого кадра'
: 'Скроллится вся страница окна: до кнопок надо доскроллить',
)
function save() {
saved.value = true
open.value = false
}
</script>
<template>
<div class="grid gap-3">
<GrSegmented
v-model="scrollBehavior"
size="sm"
class="justify-self-start"
:options="[
{ value: 'inside', label: 'inside' },
{ value: 'outside', label: 'outside' },
]"
/>
<div class="text-xs text-[var(--gr-muted-fg)]">
{{ hint }}
</div>
<GrButton variant="outline" class="justify-self-start" @click="open = true">
Настройки профиля
</GrButton>
<div v-if="saved" class="text-xs text-[var(--gr-muted-fg)]">
Сохранено
</div>
<GrDialog
v-model="open"
title="Настройки профиля"
:scroll-behavior="scrollBehavior"
>
<div class="grid gap-4">
<GrFormField v-for="field in fields" :key="field" :label="field">
<GrInput v-model="model[field]" :placeholder="field" />
</GrFormField>
<GrSwitch>Присылать уведомления</GrSwitch>
</div>
<template #footer>
<div class="flex justify-end gap-3">
<GrButton variant="outline" @click="open = false">
Отмена
</GrButton>
<GrButton @click="save">
Сохранить
</GrButton>
</div>
</template>
</GrDialog>
</div>
</template>