GrDrawer
Берут, когда панель приходит от края.
Обзор
Когда брать
- панель приходит от края — фильтры, детали записи, настройки: содержимое связано с текущим экраном, а не заменяет его;
- содержимое высокое — вертикальная панель вмещает длинную форму лучше окна по центру;
- страница должна оставаться рабочей —
:modal="false"оставляет фон доступным и не блокирует скролл; - мобильная навигация — панель от левого края вместо меню в шапке.
Когда взять другое
| Нужно | Берите |
|---|---|
| Окно по центру со своей шапкой и подвалом | GrDialog |
| Своя раскладка модального слоя | GrModal |
| Содержимое привязано к кнопке | GrPopover |
| Постоянная боковая навигация, а не выезжающая | GrSidebar |
| Спросить «да/нет» | GrConfirmDialog |
Playground контролы выводятся из API, а не пишутся руками
Окно откроется поверх страницы с пропами, заданными справа.
0 изменено· состояние — в адресе
<GrDrawer title="Filters" v-model="value"><div class="grid gap-3"><GrCheckbox :modelValue="true">Unpaid only</GrCheckbox><GrCheckbox>Overdue</GrCheckbox><GrCheckbox>Due this week</GrCheckbox></div></GrDrawer>Примеры 3 у каждого — живое демо и его код
Детали заказа из строки списка
Детали строки приходят справа: список остаётся на месте, а закрытая панель возвращает к той же строке. Слот
footerдержит действия под содержимым. Пока идёт возврат денег,persistentвыключает Esc и бэкдроп, чтобы случайная клавиша его не смахнула; крестик остаётся — панель без выхода стала бы ловушкой.Фильтры рядом с очередью, которую они фильтруют
:modal="false"снимает бэкдроп, ловушку фокуса и блокировку скролла: очередь под панелью продолжает отвечать на клики, и каждое изменение фильтра сразу видно в результате.widthзадаёт ширину помимо шкалыsize; подвал считает совпадения и сбрасывает фильтры.Support queue · 5 ticketsСлот доставки от нижнего края
На телефоне большой палец внизу — там же и панель:
side="bottom"меняет ось, иsizeтеперь задаёт высоту. Выбор — черновик, который применяет кнопка в подвале, поэтому случайный тап по подложке оставляет слот прежним. Слотheaderзаменяет шапку целиком, вместе с «Отменой», чтобы выровнять её по колонке.Checkout · 3 items DeliveryThu 8 Oct · 14:00–16:00
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
title | string | undefined | undefined | Заголовок; если передан — покажется в хедере. Можно переопределить слотом `#title`. |
size | "sm" | "md" | "lg" | "xl" | "full" | undefined | undefined | Размер панели по её оси: ширина у боковых, высота у верхней и нижней. |
closeOnBackdrop | boolean | undefined | true | Закрывать при клике по бэкдропу. В немодальном режиме подложки нет. |
closeOnEsc | boolean | undefined | true | Закрывать по Esc. |
showHeader | boolean | undefined | true | Рендерить ли хедер (заголовок + кнопка закрытия). |
showCloseButton | boolean | undefined | true | Рендерить ли кнопку закрытия в хедере. |
headerConfig | GrDrawerSectionConfig | undefined | undefined | Паддинги и рамка секций — как у `GrDialog`. |
footerConfig | GrDrawerSectionConfig | undefined | undefined | Поля и верхняя линия подвала. Подвал рисуется, только если передан слот `#footer`. |
bodyConfig | GrDrawerSectionConfig | undefined | undefined | Поля тела панели. `bordered` у тела не применяется: своей линии у него нет. |
closeLabel | string | undefined | undefined | i18n-friendly aria-label для кнопки закрытия. |
persistent | boolean | undefined | false | Запрет закрытия «мягкими» способами (бэкдроп, Esc) — на время операции, которую нельзя бросить на полпути. Кнопка закрытия при этом остаётся: панель без единого выхода — ловушка. |
initialFocus | HTMLElement | null | undefined | null | Элемент, получающий фокус при открытии. По умолчанию — сама панель. |
modal | boolean | undefined | true | Модальная панель: подложка, блокировка скролла, `inert` остальной странице и ловушка фокуса. `false` — панель живёт рядом со страницей: с ней работают, не закрывая, а Tab уходит наружу. |
side | GrDrawerSide | undefined | undefined | Сторона, с которой выезжает панель. |
width | string | number | undefined | undefined | Произвольная ширина боковой панели. Число трактуется как пиксели; сильнее `size`. |
height | string | number | undefined | undefined | Произвольная высота верхней или нижней панели. Число трактуется как пиксели. |
modelValueобязательныйv-model | boolean | — | Контроль открытия через v-model. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | any | Содержимое панели. Тело скроллится, когда не помещается. |
title | any | Заголовок вместо пропа `title`. Даёт панели доступное имя и без видимой шапки. |
header | { title?: string | undefined; close: () => void; } | Своя шапка целиком: заголовок, кнопка закрытия и всё, что нужно рядом. |
footer | any | Подвал панели — обычно кнопки действий. Без слота подвала нет. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: boolean): void | — |
opened | (event: "opened"): void | — |
closed | (event: "closed"): void | — |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
close | () => void | Закрыть панель (эквивалент `v-model = false`), минуя `persistent`. |
focus | () => void | undefined | Вернуть фокус на панель — например после операции, уведшей его наружу. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrDrawerConfigurableProps, GrDrawerProps, GrDrawerEmits } from '@feugene/granularity/components/GrDrawer'Файлdist/types/components/GrDrawer/config.d.ts
const grDrawerConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrDrawer">;Файлdist/types/components/GrDrawer/defaults.d.ts
Пропы GrDrawer, настраиваемые глобально через
<GrConfigProvider :component-defaults="{ GrDrawer: { … } }">.
size здесь — шкала оверлея (sm…full), а не шкала контролов, поэтому
глобальный size провайдера к нему не применяется: <GrConfigProvider
size="xs"> про кегль кнопок, а не про ширину выезжающей панели. Канал один —
точечный componentDefaults.
interface GrDrawerConfigurableProps {
size: GrDrawerSize;
side: GrDrawerSide;
}Файлdist/types/components/GrDrawer/safelist.d.ts
const grDrawerSafelist: string[];Файлdist/types/components/GrDrawer/GrDrawer.vue.d.ts
interface GrDrawerProps {
/** Контроль открытия через v-model. */
modelValue: boolean;
/** Заголовок; если передан — покажется в хедере. Можно переопределить слотом `#title`. */
title?: string;
/**
* Модальная панель: подложка, блокировка скролла, `inert` остальной странице
* и ловушка фокуса. `false` — панель живёт рядом со страницей: с ней работают,
* не закрывая, а Tab уходит наружу.
*/
modal?: boolean;
/** Закрывать при клике по бэкдропу. В немодальном режиме подложки нет. */
closeOnBackdrop?: boolean;
/** Закрывать по Esc. */
closeOnEsc?: boolean;
/**
* Запрет закрытия «мягкими» способами (бэкдроп, Esc) — на время операции,
* которую нельзя бросить на полпути. Кнопка закрытия при этом остаётся:
* панель без единого выхода — ловушка.
*/
persistent?: boolean;
/** Сторона, с которой выезжает панель. */
side?: GrDrawerSide;
/** Размер панели по её оси: ширина у боковых, высота у верхней и нижней. */
size?: GrDrawerSize;
/** Произвольная ширина боковой панели. Число трактуется как пиксели; сильнее `size`. */
width?: string | number;
/** Произвольная высота верхней или нижней панели. Число трактуется как пиксели. */
height?: string | number;
/** Рендерить ли хедер (заголовок + кнопка закрытия). */
showHeader?: boolean;
/** Рендерить ли кнопку закрытия в хедере. */
showCloseButton?: boolean;
/** Паддинги и рамка секций — как у `GrDialog`. */
headerConfig?: GrDrawerSectionConfig;
/** Поля тела панели. `bordered` у тела не применяется: своей линии у него нет. */
bodyConfig?: GrDrawerSectionConfig;
/** Поля и верхняя линия подвала. Подвал рисуется, только если передан слот `#footer`. */
footerConfig?: GrDrawerSectionConfig;
/** Элемент, получающий фокус при открытии. По умолчанию — сама панель. */
initialFocus?: HTMLElement | null;
/** i18n-friendly aria-label для кнопки закрытия. */
closeLabel?: string;
}interface GrDrawerEmits {
/** Панель закрыта (`v-model`): крестиком, Esc, подложкой или методом `close`. */
(e: 'update:modelValue', value: boolean): void;
/** Панель выехала и анимация закончилась. */
(e: 'opened'): void;
/** Панель уехала и анимация закончилась. */
(e: 'closed'): void;
}Файлdist/types/components/GrDrawer/grDrawerStyles.d.ts
type GrDrawerSide = 'left' | 'right' | 'top' | 'bottom';type GrDrawerSize = GrOverlaySize;Настройка паддингов и рамки секции. Структурно совпадает с
GrDialogSectionConfig намеренно: потребитель не должен переучиваться при
переходе между двумя оверлеями библиотеки. Свой тип, а не импорт из
GrDialog, — чтобы не тянуть чужой чанк ради двух полей.
interface GrDrawerSectionConfig {
paddingX?: string;
paddingY?: string;
bordered?: boolean;
}Файлdist/types/components/GrDrawer/index.d.ts
type GrDrawerInstance = ComponentExposed<typeof GrDrawerComponent>;Доступность
| Клавиша | Действие |
|---|---|
| — | то же — но только в модальном режиме. :modal="false" снимает ловушку: Tab уводит фокус на страницу, ради работы с которой панель и открыта, а Esc по-прежнему закрывает верхний слой |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Стороны
side — right (по умолчанию), left, top, bottom. Ось решает всё
остальное: боковая панель растянута по вертикали и берёт из шкалы ширину,
верхняя и нижняя растянуты по горизонтали и берут высоту. Выезжает панель
со своей стороны.
Произвольный размер задаётся по той же оси: width — для left/right,
height — для top/bottom. Проп не своей оси не применяется и ругается в
dev-сборке: молча проигнорированный width у нижней панели выглядит как баг
компонента, а не как ошибка вызова.
<!-- bottom-sheet: высота из шкалы -->
<GrDrawer v-model="open" side="bottom" size="sm" title="Фильтры" />
<!-- своя высота -->
<GrDrawer v-model="open" side="bottom" :height="320" />Модальный и немодальный режим
По умолчанию панель модальная: подложка, блокировка скролла страницы, inert
всему остальному и ловушка фокуса — Tab ходит по кругу внутри.
:modal="false" снимает всё это. Панель остаётся поверх страницы, но страница
продолжает работать: она скроллится, кликается и принимает Tab, потому что
корень слоя пропускает клики сквозь себя (pointer-events: none), а
кликабельна только сама панель. Это режим для фильтров над таблицей и подобных
панелей, с которыми работают параллельно основному экрану.
Что остаётся в немодальном режиме: место в общем стеке слоёв (Esc закрывает
верхний слой), возврат фокуса на триггер и role="dialog" — но уже без
aria-modal. Подложки нет, поэтому closeOnBackdrop в этом режиме ни на что не
влияет.
<GrDrawer v-model="open" :modal="false" side="right" title="Фильтры">
…
</GrDrawer>Слой и подложка
Drawer живёт на --gr-z-modal — том же слое, что GrModal. Раньше это был
литерал z-50, ниже всей шкалы: панель dropdown или select (1000) рисовалась
поверх выехавшего drawer’а, а его бэкдроп её не перекрывал.
Внутри слоя высоту уточняет стек: drawer, открытый поверх окна, получает
calc(var(--gr-z-modal) + глубина) и рисуется выше него независимо от того, в
каком порядке компоненты смонтированы (../z-index.md).
Подложка — токен --gr-overlay-bg, общий с GrModal: в тёмной теме она плотнее,
иначе панель не отделяется от фона. Раньше здесь стоял bg-black/40 — единственный
оверлей-фон, не следовавший за темой.
Хедер, секции и API-паритет с GrDialog
Пропы совпадают с GrDialog намеренно: showHeader, showCloseButton,
headerConfig/bodyConfig/footerConfig (paddingX, paddingY, bordered).
Потребитель не должен переучиваться при переходе между двумя оверлеями одной
библиотеки.
Хедер рендерится, только когда есть что показать — заголовок или кнопка
закрытия. Пустой title считается отсутствующим: раньше на его месте
появлялось слово «Drawer».
Слот #header подменяет шапку целиком — вместе с кнопкой закрытия, которую в
этом случае рисует потребитель. Слот получает title и close:
<GrDrawer v-model="open" title="Фильтры">
<template #header="{ title, close }">
<GrInput v-model="query" :placeholder="title" />
<GrButton variant="ghost" @click="close">Готово</GrButton>
</template>
</GrDrawer>Имя слоя есть всегда и всегда осмысленное: заголовок связывается через
aria-labelledby, а когда шапки нет (showHeader: false) или она подменена
слотом — тот же заголовок рендерится скрытым (sr-only). Обобщённое имя из
i18n остаётся страховкой только для панели вовсе без заголовка: слой без имени —
нарушение aria-dialog-name.
Тело панели скроллится и потому попадает в таб-порядок (tabindex="0"):
длинный текст без единого фокусируемого элемента иначе не прокрутить с
клавиатуры (axe: scrollable-region-focusable).
<GrDrawer
v-model="open"
title="Фильтры"
:header-config="{ paddingX: 'px-8' }"
:body-config="{ paddingY: 'py-4' }"
>
…
<template #footer>…</template>
</GrDrawer>Размер
size — шкала оверлеев (sm…full), а не контролов; глобальный
<GrConfigProvider size="…"> её не трогает, канал один — точечный
componentDefaults (size, side). Значение читается по оси панели: ширина у
боковых, высота у верхней и нижней. width/height задают произвольный размер
и отменяют размерный класс.
<GrConfigProvider :component-defaults="{ GrDrawer: { size: 'lg', side: 'left' } }">
<GrDrawer v-model="open" :width="640" />
</GrConfigProvider>Закрытие и жизненный цикл
closeOnBackdrop и closeOnEsc управляют «мягкими» способами закрытия;
persistent запрещает оба на время операции, которую нельзя бросить на
полпути. Кнопка закрытия при этом остаётся: панель без единого выхода —
ловушка.
@opened/@closed срабатывают по окончании анимации — туда вешается дозагрузка
контента и возврат состояния. initialFocus задаёт элемент, получающий фокус
при открытии (по умолчанию — сама панель).
Императивно: close() (минуя persistent) и focus() — вернуть фокус на
панель, если операция увела его наружу.
Пропы этого примера