GrDrawer

  • ядро
  • Слои
  • API · 17 props
  • ⌨ клавиши

Берут, когда панель приходит от края.

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

Обзор

Когда брать

  • панель приходит от края — фильтры, детали записи, настройки: содержимое связано с текущим экраном, а не заменяет его;
  • содержимое высокое — вертикальная панель вмещает длинную форму лучше окна по центру;
  • страница должна оставаться рабочей — :modal="false" оставляет фон доступным и не блокирует скролл;
  • мобильная навигация — панель от левого края вместо меню в шапке.

Когда взять другое

НужноБерите
Окно по центру со своей шапкой и подваломGrDialog
Своя раскладка модального слояGrModal
Содержимое привязано к кнопкеGrPopover
Постоянная боковая навигация, а не выезжающаяGrSidebar
Спросить «да/нет»GrConfirmDialog

Playground контролы выводятся из API, а не пишутся руками

Сцена

Окно откроется поверх страницы с пропами, заданными справа.

0 изменено· состояние — в адресе

Примеры 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

PropTypeпо умолчаниюОписание
titlestring | undefinedundefinedЗаголовок; если передан — покажется в хедере. Можно переопределить слотом `#title`.
size"sm" | "md" | "lg" | "xl" | "full" | undefinedundefinedРазмер панели по её оси: ширина у боковых, высота у верхней и нижней.
closeOnBackdropboolean | undefinedtrueЗакрывать при клике по бэкдропу. В немодальном режиме подложки нет.
closeOnEscboolean | undefinedtrueЗакрывать по Esc.
showHeaderboolean | undefinedtrueРендерить ли хедер (заголовок + кнопка закрытия).
showCloseButtonboolean | undefinedtrueРендерить ли кнопку закрытия в хедере.
headerConfigGrDrawerSectionConfig | undefinedundefinedПаддинги и рамка секций — как у `GrDialog`.
footerConfigGrDrawerSectionConfig | undefinedundefinedПоля и верхняя линия подвала. Подвал рисуется, только если передан слот `#footer`.
bodyConfigGrDrawerSectionConfig | undefinedundefinedПоля тела панели. `bordered` у тела не применяется: своей линии у него нет.
closeLabelstring | undefinedundefinedi18n-friendly aria-label для кнопки закрытия.
persistentboolean | undefinedfalseЗапрет закрытия «мягкими» способами (бэкдроп, Esc) — на время операции, которую нельзя бросить на полпути. Кнопка закрытия при этом остаётся: панель без единого выхода — ловушка.
initialFocusHTMLElement | null | undefinednullЭлемент, получающий фокус при открытии. По умолчанию — сама панель.
modalboolean | undefinedtrueМодальная панель: подложка, блокировка скролла, `inert` остальной странице и ловушка фокуса. `false` — панель живёт рядом со страницей: с ней работают, не закрывая, а Tab уходит наружу.
sideGrDrawerSide | undefinedundefinedСторона, с которой выезжает панель.

Слоты

SlotTypeОписание
defaultanyСодержимое панели. Тело скроллится, когда не помещается.
titleanyЗаголовок вместо пропа `title`. Даёт панели доступное имя и без видимой шапки.
header{ title?: string | undefined; close: () => void; }Своя шапка целиком: заголовок, кнопка закрытия и всё, что нужно рядом.
footeranyПодвал панели — обычно кнопки действий. Без слота подвала нет.

События

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

grDrawerConfigconst
const grDrawerConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrDrawer">;

Файлdist/types/components/GrDrawer/defaults.d.ts

GrDrawerConfigurablePropsinterface

Пропы 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

grDrawerSafelistconst
const grDrawerSafelist: string[];

Файлdist/types/components/GrDrawer/GrDrawer.vue.d.ts

GrDrawerPropsinterface
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;
}
GrDrawerEmitsinterface
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

GrDrawerSidetype
type GrDrawerSide = 'left' | 'right' | 'top' | 'bottom';
GrDrawerSizetype
type GrDrawerSize = GrOverlaySize;
GrDrawerSectionConfiginterface

Настройка паддингов и рамки секции. Структурно совпадает с GrDialogSectionConfig намеренно: потребитель не должен переучиваться при переходе между двумя оверлеями библиотеки. Свой тип, а не импорт из GrDialog, — чтобы не тянуть чужой чанк ради двух полей.

interface GrDrawerSectionConfig {
    paddingX?: string;
    paddingY?: string;
    bordered?: boolean;
}

Файлdist/types/components/GrDrawer/index.d.ts

GrDrawerInstancetype
type GrDrawerInstance = ComponentExposed<typeof GrDrawerComponent>;

Доступность

КлавишаДействие
—то же — но только в модальном режиме. :modal="false" снимает ловушку: Tab уводит фокус на страницу, ради работы с которой панель и открыта, а Esc по-прежнему закрывает верхний слой

Использование из прозы библиотеки

Стороны

side — right (по умолчанию), left, top, bottom. Ось решает всё остальное: боковая панель растянута по вертикали и берёт из шкалы ширину, верхняя и нижняя растянуты по горизонтали и берут высоту. Выезжает панель со своей стороны.

Произвольный размер задаётся по той же оси: width — для left/right, height — для top/bottom. Проп не своей оси не применяется и ругается в dev-сборке: молча проигнорированный width у нижней панели выглядит как баг компонента, а не как ошибка вызова.

vue
<!-- 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 в этом режиме ни на что не влияет.

vue
<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:

vue
<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).

vue
<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 задают произвольный размер и отменяют размерный класс.

vue
<GrConfigProvider :component-defaults="{ GrDrawer: { size: 'lg', side: 'left' } }">
  <GrDrawer v-model="open" :width="640" />
</GrConfigProvider>
Закрытие и жизненный цикл

closeOnBackdrop и closeOnEsc управляют «мягкими» способами закрытия; persistent запрещает оба на время операции, которую нельзя бросить на полпути. Кнопка закрытия при этом остаётся: панель без единого выхода — ловушка.

@opened/@closed срабатывают по окончании анимации — туда вешается дозагрузка контента и возврат состояния. initialFocus задаёт элемент, получающий фокус при открытии (по умолчанию — сама панель).

Императивно: close() (минуя persistent) и focus() — вернуть фокус на панель, если операция увела его наружу.