GrDropdown

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

Берут, когда меню со своими пунктами.

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

Обзор

Когда брать

  • меню со своими пунктами — разметку пунктов пишете вы, а слой, позиционирование и клавиатуру берёт компонент;
  • триггер нестандартный — аватар, ячейка таблицы, кнопка с бейджем: слот #trigger принимает что угодно;
  • панель открывается по наведению — openDelay и closeDelay разводят намерение и случайное движение мыши;
  • открытием управляет родитель — v-model:open вместе с trigger="manual".

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

НужноБерите
Пункты обычные: текст, иконка, группы, разделителиGrDropdownMenu
Внутри форма, фильтр или палитра — не менюGrPopover
Текстовая подсказка без интерактиваGrTooltip
Выбор значения поля формыGrSelect
Поиск по командам всего приложенияGrCommandPalette

Компонент жёстко объявляет role="menu" и водит фокус по пунктам. Для формы или подтверждения это неверная семантика — диктор объявит меню там, где меню нет; такой случай закрывает GrPopover с настраиваемой ролью.

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

Сцена

Загружается…

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

Примеры 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 из слота и поворачивает шеврон.

    Invoices
    InvoiceCustomerDueAmount
    INV-0418Northwind Traders15 Oct€1,284.50
    INV-0417Contoso Ltd12 Oct€432.00
  • Вверх от поля сообщения

    Поле сообщения живёт внизу экрана, поэтому placement="top-start" открывает панель вверх — это предпочтение, а не приказ: у края она переворачивается. Пункты в две строки — своя разметка, а напечатанная буква переводит к подходящему заголовку.

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
placementPlacement | undefined"bottom-end"Размещение панели относительно триггера; `flip` при нехватке места остаётся.
offsetnumber | undefined8Зазор между триггером и панелью, px.
disabledboolean | undefinedfalseПанель не открывается ничем; триггер остаётся фокусируемым.
openv-modelboolean | undefinedundefinedКонтролируемое состояние панели (`v-model:open`). Без пропа панель ведёт себя сама (uncontrolled), с ним — слушайте `update:open` и меняйте проп.
triggerGrDropdownTrigger | undefined"click"Чем открывается панель. В любом режиме работают клик и клавиатура.
teleportTostring | HTMLElement | undefinedundefinedТочечное переопределение точки монтирования. По умолчанию — общий портал оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).
contentClassstring | undefined""Дополнительные классы контейнера content.
widthGrDropdownWidth | undefined"12rem"Ширина панели: число — пиксели, строка — CSS-длина, `auto` — по контенту.
openDelaynumber | undefined120Задержка открытия по наведению, мс.
closeDelaynumber | undefined160Задержка закрытия после ухода курсора, мс.
closeOnContentClickboolean | undefinedtrueЗакрывать панель по клику внутри content.

Слоты

SlotTypeОписание
trigger
{ open: boolean; toggle: () => void; close: () => void; trig{ open: boolean; toggle: () => void; close: () => void; triggerProps: Record<string, unknown>; }
Триггер панели. `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

grDropdownConfigconst
const grDropdownConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrDropdown">;

Файлdist/types/components/GrDropdown/safelist.d.ts

grDropdownSafelistconst
const grDropdownSafelist: string[];

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

GrDropdownTriggertype
type GrDropdownTrigger = 'click' | 'hover';
GrDropdownPropsinterface
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;
}
GrDropdownEmitsinterface
interface GrDropdownEmits {
    /** Панель открылась/закрылась (`v-model:open`). */
    (e: 'update:open', value: boolean): void;
}

Файлdist/types/components/GrDropdown/grDropdownStyles.d.ts

GrDropdownWidthtype

Ширина панели: число — пиксели, строка — CSS-длина, auto — по контенту.

type GrDropdownWidth = number | string;

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

GrDropdownInstancetype
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 её закрывает, и иначе из поля не выйти

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

Триггер: triggerProps обязателен

Компонент не знает, что именно потребитель положит в слот #trigger, поэтому клик, клавиатуру и ARIA он отдаёт объектом, который нужно привязать к настоящему фокусируемому элементу:

vue
<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-режиме те же: компонент эмитит, применяет родитель.