GrDropdown
Берут, когда меню со своими пунктами.
Обзор
Когда брать
- меню со своими пунктами — разметку пунктов пишете вы, а слой, позиционирование и клавиатуру берёт компонент;
- триггер нестандартный — аватар, ячейка таблицы, кнопка с бейджем: слот
#triggerпринимает что угодно; - панель открывается по наведению —
openDelayиcloseDelayразводят намерение и случайное движение мыши; - открытием управляет родитель —
v-model:openвместе сtrigger="manual".
Когда взять другое
| Нужно | Берите |
|---|---|
| Пункты обычные: текст, иконка, группы, разделители | GrDropdownMenu |
| Внутри форма, фильтр или палитра — не меню | GrPopover |
| Текстовая подсказка без интерактива | GrTooltip |
| Выбор значения поля формы | GrSelect |
| Поиск по командам всего приложения | GrCommandPalette |
Компонент жёстко объявляет role="menu" и водит фокус по пунктам. Для формы
или подтверждения это неверная семантика — диктор объявит меню там, где меню
нет; такой случай закрывает GrPopover с настраиваемой ролью.
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrDropdown placement="bottom-start" />Примеры 4 у каждого — живое демо и его код
Карточка по наведению
trigger="hover"сopen-delayотличает намерение от проходящего курсора, аclose-delayдержит панель, пока курсор идёт к ней через зазор. Клик и клавиатура её тоже открывают: панель, до которой доходит только мышь, — не панель вовсе.ACM-212 · 2 h ago
Retries are ready for review. Assigned to— she wrote the backoff policy.Уведомления под колокольчиком
Любой триггер принимает
triggerPropsна настоящую кнопку — здесь внутри счётчика.v-model:openотдаёт состояние странице, и она помечает всё прочитанным, когда панель закрывается. Строки — кнопкиmenuitem, поэтому их обходят стрелками.Acme Billing 2 unread notificationsКонтролы внутри панели
close-on-content-click="false"держит панель открытой, пока чекбоксы меняют таблицу, аcloseиз слота завершает её кнопкой «Done». Триггер читаетopenиз слота и поворачивает шеврон.InvoicesInvoice Customer Due Amount INV-0418 Northwind Traders 15 Oct €1,284.50 INV-0417 Contoso Ltd 12 Oct €432.00 Вверх от поля сообщения
Поле сообщения живёт внизу экрана, поэтому
placement="top-start"открывает панель вверх — это предпочтение, а не приказ: у края она переворачивается. Пункты в две строки — своя разметка, а напечатанная буква переводит к подходящему заголовку.
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
placement | Placement | undefined | "bottom-end" | Размещение панели относительно триггера; `flip` при нехватке места остаётся. |
offset | number | undefined | 8 | Зазор между триггером и панелью, px. |
disabled | boolean | undefined | false | Панель не открывается ничем; триггер остаётся фокусируемым. |
openv-model | boolean | undefined | undefined | Контролируемое состояние панели (`v-model:open`). Без пропа панель ведёт себя сама (uncontrolled), с ним — слушайте `update:open` и меняйте проп. |
trigger | GrDropdownTrigger | undefined | "click" | Чем открывается панель. В любом режиме работают клик и клавиатура. |
teleportTo | string | HTMLElement | undefined | undefined | Точечное переопределение точки монтирования. По умолчанию — общий портал оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`). |
contentClass | string | undefined | "" | Дополнительные классы контейнера content. |
width | GrDropdownWidth | undefined | "12rem" | Ширина панели: число — пиксели, строка — CSS-длина, `auto` — по контенту. |
openDelay | number | undefined | 120 | Задержка открытия по наведению, мс. |
closeDelay | number | undefined | 160 | Задержка закрытия после ухода курсора, мс. |
closeOnContentClick | boolean | undefined | true | Закрывать панель по клику внутри content. |
Слоты
| Slot | Type | Описание |
|---|---|---|
trigger |
| Триггер панели. `triggerProps` обязаны попасть на сам интерактивный элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls` читаются с того узла, который получает фокус, а клик приезжает вместе с ними. |
content | { close: () => void; } | Содержимое меню. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:open | (event: "update:open", value: boolean): void | — |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
close | () => void | Закрыть панель из кода. |
toggle | () => void | Переключить панель: открытую закрыть, закрытую открыть. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrDropdownTrigger, GrDropdownProps, GrDropdownEmits } from '@feugene/granularity/components/GrDropdown'Файлdist/types/components/GrDropdown/config.d.ts
const grDropdownConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrDropdown">;Файлdist/types/components/GrDropdown/safelist.d.ts
const grDropdownSafelist: string[];Файлdist/types/components/GrDropdown/GrDropdown.vue.d.ts
type GrDropdownTrigger = 'click' | 'hover';interface GrDropdownProps {
/** Размещение панели относительно триггера; `flip` при нехватке места остаётся. */
placement?: UseFloatingPlacement;
/** Зазор между триггером и панелью, px. */
offset?: number;
/** Ширина панели: число — пиксели, строка — CSS-длина, `auto` — по контенту. */
width?: GrDropdownWidth;
/** Чем открывается панель. В любом режиме работают клик и клавиатура. */
trigger?: GrDropdownTrigger;
/** Задержка открытия по наведению, мс. */
openDelay?: number;
/** Задержка закрытия после ухода курсора, мс. */
closeDelay?: number;
/** Панель не открывается ничем; триггер остаётся фокусируемым. */
disabled?: boolean;
/** Закрывать панель по клику внутри content. */
closeOnContentClick?: boolean;
/** Дополнительные классы контейнера content. */
contentClass?: string;
/**
* Точечное переопределение точки монтирования. По умолчанию — общий портал
* оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).
*/
teleportTo?: string | HTMLElement;
/**
* Контролируемое состояние панели (`v-model:open`). Без пропа панель ведёт
* себя сама (uncontrolled), с ним — слушайте `update:open` и меняйте проп.
*/
open?: boolean;
}interface GrDropdownEmits {
/** Панель открылась/закрылась (`v-model:open`). */
(e: 'update:open', value: boolean): void;
}Файлdist/types/components/GrDropdown/grDropdownStyles.d.ts
Ширина панели: число — пиксели, строка — CSS-длина, auto — по контенту.
type GrDropdownWidth = number | string;Файлdist/types/components/GrDropdown/index.d.ts
type GrDropdownInstance = ComponentExposed<typeof GrDropdownComponent>;Доступность
Паттерн APG
menu
Клавиши
↑Home / EndEnter / SpaceEsc
| Клавиша | Действие |
|---|---|
| — | Enter/Space/↓ на триггере — открыть с фокусом на первом пункте |
| ↑ | на последнем; ↓/↑ — по всему фокусируемому в панели |
| Home / End | к краям, печатный символ — typeahead по первой букве (буфер 600 мс, повтор буквы — следующий пункт на неё) |
| Enter / Space | активировать |
| Esc | закрыть |
| Tab | закрыть. Пока фокус на контроле (input/textarea/select/contenteditable) |
| — | печатные и Home/End достаются ему, а не панели; стрелки остаются за панелью — Tab её закрывает, и иначе из поля не выйти |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Триггер: triggerProps обязателен
Компонент не знает, что именно потребитель положит в слот #trigger, поэтому
клик, клавиатуру и ARIA он отдаёт объектом, который нужно привязать к
настоящему фокусируемому элементу:
<GrDropdown>
<template #trigger="{ triggerProps }">
<GrButton variant="outline" v-bind="triggerProps">Действия</GrButton>
</template>
<template #content>
<button type="button" role="menuitem">Дублировать</button>
</template>
</GrDropdown>Обёртка слота клики не ловит намеренно: иначе панель переключала бы любой клик
внутри неё — включая вложенные кнопки и ссылки. Забытый v-bind в dev-сборке
печатает предупреждение: молча неработающий триггер искали бы в чужом коде.
Слот отдаёт ещё open, toggle и close — для своей разметки состояния.
Размещение и ширина
placement— любое значение floating-ui (bottom-endпо умолчанию). Переворот при нехватке места (flip) работает всегда:placement— это предпочтение, а не приказ, иtransform-originпанели следует за фактической стороной;offset— зазор между триггером и панелью, px;width— CSS-длина: число трактуется как пиксели, строка идёт как есть,autoотдаёт ширину контенту. Tailwind-шкалы (width="48"→w-48) больше нет; строка без единиц трактуется как пиксели и в dev-сборке ругается.
Режимы открытия
trigger="click" (по умолчанию) или "hover". В режиме наведения задержки
задают openDelay/closeDelay: без первой панель выпрыгивает на любое
пересечение курсором, без второй её не удержать при переходе с триггера на
панель — между ними зазор offset.
Клик и клавиатура работают в обоих режимах: меню, доступное только мышью, недоступно с клавиатуры вовсе.
disabled закрывает панель и не даёт открыть её ни кликом, ни клавиатурой, ни
наведением. Триггер при этом остаётся фокусируемым и получает aria-disabled
— нативный disabled выкинул бы его из таб-порядка вместе с объяснением, что
происходит.
Императивный API
open(), close() и toggle() через ref на компоненте — тот же состав, что
у остальных оверлеев. В отличие от модальных, меню состояние держит само:
модели у него нет, и методы правят его напрямую. disabled императив не
обходит — иначе он открывал бы то, что закрыто для клика и клавиатуры.
Те же три метода приходят в слот #trigger (:open, :toggle, :close) —
ref нужен, когда открывать меню надо снаружи разметки триггера.
Клавиатура
Enter/Space/↓ открывают панель с фокусом на первом пункте, ↑ — на
последнем. Внутри: ↓/↑ по пунктам, Home/End к краям, Tab закрывает,
Esc закрывает и возвращает фокус на триггер (этим распоряжается общий стек
слоёв, а не сам компонент).
Печатный символ — typeahead по первой букве: буфер копится, пока паузы между нажатиями короче 600 мс, а повтор одной и той же буквы означает «следующий пункт на неё», а не поиск «аа». Ищется по видимому тексту пункта.
Space при пустом буфере не перехватывается: пункты меню — кнопки, и пробел
для них родная клавиша активации. В запрос он попадает, только когда поиск уже
идёт (правило typeahead из WAI-ARIA APG).
Контролы внутри панели
С closeOnContentClick={false} в панели живёт не меню, а содержимое — поля,
чекбоксы, переключатели. Клавиши, которые сфокусированный контрол умеет сам,
достаются ему: печатные символы и Home/End не уходят в навигацию по
пунктам, пока фокус в input, textarea, select или contenteditable.
Стрелки — намеренное исключение: они остаются за панелью даже в поле. Tab
панель закрывает, и без стрелок из поля внутри неё не было бы выхода. Ими же
до поля и добираются: в кольцо навигации входит всё фокусируемое, а не только
пункты меню.
Управление извне: v-model:open
Кроме императивного API панель управляема декларативно: без пропа open —
прежнее uncontrolled-поведение, с ним состоянием владеет родитель, а
update:open сопровождает каждое изменение (общий контракт панельных
оверлеев, как у GrPopover). Hover-задержки в controlled-режиме те же:
компонент эмитит, применяет родитель.
Пропы этого примера