GrContextMenu
Берут, когда действия над строкой списка или узлом дерева.
Обзор
Когда брать
- действия над строкой списка или узлом дерева — правый клик там, где пользователь уже смотрит, вместо поездки к кнопке «⋯» в конце строки;
- быстрые операции над выделенным — переименовать, дублировать, удалить: те же пункты, что в тулбаре, но без движения мыши через весь экран;
- действия над областью — холст, карта, свободное место списка: кликать некуда, и меню у курсора остаётся единственным местом для «Вставить»;
- меню, зависящее от того, по чему кликнули —
beforeOpenотдаёт цель до открытия, иitemsуспевают собраться под конкретную строку.
Когда взять другое
| Нужно | Берите |
|---|---|
| Меню висит на видимой кнопке или «⋯» | GrDropdownMenu |
| Пункты нестандартные, разметку пишете сами | GrDropdown |
| Внутри форма, фильтр или палитра — не меню | GrPopover |
| Поиск по командам всего приложения | GrCommandPalette |
| Подтверждение опасного действия | GrConfirmDialog |
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrContextMenu :items="items"><GrButton variant="outline">Row actions</GrButton></GrContextMenu>Примеры 3 у каждого — живое демо и его код
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
placement | Placement | undefined | "bottom-start" | Сторона раскрытия от точки вызова. По умолчанию `bottom-start` — вниз и вправо от курсора. |
disabled | boolean | undefined | false | Меню не открывается ничем — ни мышью, ни клавиатурой, ни из кода. |
openv-model | boolean | undefined | undefined | Открыто ли меню. Без пропа компонент ведёт состояние сам. |
ariaLabel | string | undefined | undefined | Доступное имя панели `role="menu"`. По умолчанию — переведённое «Context menu». |
items | GrDropdownMenuEntry[] | undefined | undefined | Декларативное меню. Слот `#content` сильнее. |
trigger | GrContextMenuTrigger | undefined | "contextmenu" | Чем открывается меню: `contextmenu` — правым кликом по области, `manual` — только из кода. `Shift+F10` и клавиша `ContextMenu` работают в обоих режимах. По умолчанию `contextmenu`. |
offsetPx | number | undefined | 0 | Зазор от якоря. Ноль по умолчанию: меню липнет к курсору. |
minWidth | string | number | undefined | "11rem" | Меню из коротких слов не должно быть шириной в слово. |
labelledBy | string | undefined | undefined | `id` видимого заголовка, который называет меню, — альтернатива `ariaLabel`. |
teleportTo | string | HTMLElement | undefined | undefined | Точечное переопределение точки монтирования; по умолчанию — общий портал оверлеев. |
contentClass | string | undefined | undefined | Дополнительные классы панели. |
listClass | string | undefined | undefined | Дополнительные классы списка пунктов внутри панели. |
dividers | boolean | undefined | false | Разделители между пунктами. |
closeOnScroll | boolean | undefined | true | Закрывать при прокрутке страницы. Якорь — точка вьюпорта, и панель осталась бы висеть на экране, пока контент под ней уезжает. Так же ведут себя нативные меню. |
allowNativeMenu | boolean | undefined | true | Пропускать `Shift`+правый клик к браузеру. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | { open: boolean; } | Область, по которой вызывается меню. |
content | { close: () => void; } | Содержимое меню. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:open | (event: "update:open", value: boolean): void | — |
select | (event: "select", item: GrDropdownMenuAction): void | — |
beforeOpen | (event: "beforeOpen", context: GrContextMenuOpenContext): void | — |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
openAt | (at: MouseEvent | GrFloatingAnchorRect) => void | Открыть у точки курсора (`MouseEvent`) или у произвольного прямоугольника вьюпорта. |
openAtElement | (element: Element) => void | Открыть у прямоугольника элемента — клавиатурный путь для внешних композитов. |
close | () => void | Закрыть меню из кода; `update:open` придёт с `false`. |
toggle | () => void | Открыть закрытое меню или закрыть открытое. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrContextMenuTrigger, GrContextMenuSource, GrContextMenuOpenContext } from '@feugene/granularity/components/GrContextMenu'Файлdist/types/components/GrContextMenu/GrContextMenu.vue.d.ts
Чем открывается меню. manual — только императивно, из кода потребителя.
type GrContextMenuTrigger = 'contextmenu' | 'manual';Откуда пришло открытие: указатель, клавиатура или вызов из кода.
type GrContextMenuSource = 'pointer' | 'keyboard' | 'api';interface GrContextMenuOpenContext {
source: GrContextMenuSource;
/** По чему кликнули или что было в фокусе — отсюда потребитель берёт строку. */
target: HTMLElement | null;
anchor: GrFloatingAnchorRect;
event: MouseEvent | KeyboardEvent | null;
}interface GrContextMenuProps {
/** Декларативное меню. Слот `#content` сильнее. */
items?: GrDropdownMenuEntry[];
/** Открыто ли меню. Без пропа компонент ведёт состояние сам. */
open?: boolean;
/**
* Чем открывается меню: `contextmenu` — правым кликом по области, `manual` — только из кода.
* `Shift+F10` и клавиша `ContextMenu` работают в обоих режимах. По умолчанию `contextmenu`.
*/
trigger?: GrContextMenuTrigger;
/** Меню не открывается ничем — ни мышью, ни клавиатурой, ни из кода. */
disabled?: boolean;
/** Сторона раскрытия от точки вызова. По умолчанию `bottom-start` — вниз и вправо от курсора. */
placement?: UseFloatingPlacement;
/** Зазор от якоря. Ноль по умолчанию: меню липнет к курсору. */
offsetPx?: number;
/** Меню из коротких слов не должно быть шириной в слово. */
minWidth?: number | string;
/** Доступное имя панели `role="menu"`. По умолчанию — переведённое «Context menu». */
ariaLabel?: string;
/** `id` видимого заголовка, который называет меню, — альтернатива `ariaLabel`. */
labelledBy?: string;
/** Точечное переопределение точки монтирования; по умолчанию — общий портал оверлеев. */
teleportTo?: string | HTMLElement;
/** Дополнительные классы панели. */
contentClass?: string;
/** Дополнительные классы списка пунктов внутри панели. */
listClass?: string;
/** Разделители между пунктами. */
dividers?: boolean;
/**
* Закрывать при прокрутке страницы. Якорь — точка вьюпорта, и панель осталась
* бы висеть на экране, пока контент под ней уезжает. Так же ведут себя
* нативные меню.
*/
closeOnScroll?: boolean;
/** Пропускать `Shift`+правый клик к браузеру. */
allowNativeMenu?: boolean;
}interface GrContextMenuEmits {
/** Меню открылось или закрылось (`v-model:open`). */
(e: 'update:open', value: boolean): void;
/**
* Меню собирается открыться у этой цели. Приходит **до** открытия, чтобы
* потребитель успел пересобрать `items` под неё: пункты для папки и для файла
* разные, а узнать цель иначе неоткуда. Пустая модель после этого просто не
* откроется — отдельный способ отменить открытие не нужен.
*/
(e: 'beforeOpen', context: GrContextMenuOpenContext): void;
/** Выбран пункт из `items`; пункт с подменю его не шлёт. После выбора меню закрывается. */
(e: 'select', item: GrDropdownMenuAction): void;
}Файлdist/types/components/GrContextMenu/config.d.ts
const grContextMenuConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrContextMenu">;Файлdist/types/components/GrContextMenu/index.d.ts
type GrContextMenuInstance = ComponentExposed<typeof GrContextMenuComponent>;Доступность
Паттерн APG
menu
| Клавиша | Действие |
|---|---|
| — | Shift+F10 и клавиша ContextMenu на области — открыть у прямоугольника сфокусированного элемента (в поле ввода не перехватывается: там нативное меню полезнее); внутри панели — то же, что у GrDropdown, минус триггерная часть, включая подменю по →/←. Shift+правый клик отдаётся браузеру |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Пункты собираются под цель, а не задаются раз и навсегда
beforeOpen приходит до открытия, и это единственный момент, когда меню можно собрать под то, по чему кликнули: у
папки одни действия, у файла другие. Отдельного способа отменить открытие не нужно — пустая модель просто не откроется.
<GrContextMenu :items="items" @before-open="onBeforeOpen" @select="onSelect">
<GrTree :data="data" node-key="id"/>
</GrContextMenu>function onBeforeOpen(context: GrContextMenuOpenContext): void {
// Цель берём из DOM, а не из события мыши: тот же код обслуживает Shift+F10,
// у которого события мыши нет вовсе.
const row = context.target?.closest('[data-gr-tree-node-key]')
items.value = row ? itemsFor(row.dataset.grTreeNodeKey!) : []
}Пропы доезжают до компонента следующим рендером, поэтому меню открывается на тик позже вызова — иначе оно показывало бы пункты от предыдущей цели.
Две точки входа, и они не дублируют друг друга
Обёртка — содержимое слота по умолчанию. Она ловит правый клик и, что важнее, клавиатуру: keydown всплывает от
сфокусированного элемента, и поймать его может только предок.
openAt(event) через ref — когда цель не выводится из DOM: событие пришло не от указателя, координаты считает сама
страница (холст, карта), или меню принадлежит объекту, которого в разметке нет.
<GrContextMenu ref="menu" trigger="manual" :items="items" @select="onSelect">
<canvas @contextmenu.prevent="onCanvasMenu"/>
</GrContextMenu>trigger="manual" выключает только открытие указателем. Клавиатурный путь остаётся всегда — иначе меню было бы
недоступно с клавиатуры, и этот обработчик пришлось бы писать заново на каждой странице.
Клавиатура — условие приёмки, а не полировка
Меню, доступное только правым кликом, для клавиатуры не существует. Обёртка слушает Shift+F10 и клавишу ContextMenu,
и якорем становится прямоугольник сфокусированного элемента, а не точка: меню принадлежит строке и переворачивается
вместе с ней, когда снизу не хватает места.
Внутри поля ввода вызов не перехватывается — там нативное меню с проверкой орфографии и буфером обмена полезнее.
Строки, по которым открывается меню, стоит делать фокусируемыми — иначе после закрытия фокус возвращать некуда, и
Tab пойдёт с начала документа. GrTree это условие уже выполняет.
Shift+правый клик отдан браузеру
Нативное меню — не помеха, а инструмент: перевод страницы, просмотр кода, сохранение картинки. Shift+правый клик
пропускается к браузеру (в Firefox это задокументированный способ), выключается пропом allowNativeMenu.
Ctrl в escape-hatch не входит намеренно: на macOS Ctrl+клик и есть правый клик, и меню перестало бы открываться у
части пользователей.
Прокрутка закрывает меню
Якорь — точка вьюпорта, у которой нет элемента, поэтому при прокрутке панель осталась бы висеть на экране, пока контент
под ней уезжает. Так же ведут себя нативные меню; поведение выключается пропом closeOnScroll.
Второй уровень: подменю
Модель пунктов общая с GrDropdownMenu, поэтому и
подменю здесь то же самое: пункт с непустыми children раскрывает следующий
уровень, select по нему не приходит.
const items: GrDropdownMenuEntry[] = [
{ key: 'rename', label: 'Переименовать' },
{ key: 'export', label: 'Экспорт', children: [{ key: 'pdf', label: 'PDF' }] },
]Клавиатура, коридор для курсора и правила закрытия цепочки описаны там же —
«Второй уровень: подменю». Здесь
отличается только корень: выбор в подменю закрывает контекстное меню, а Escape
на верхнем уровне возвращает к нижнему, не закрывая всё.
Границы
- длинное меню у нижнего края экрана сдвигается, а не сжимается со скроллом. Ограничение высоты панели в пакете не реализовано ни у одного якорного слоя;
- долгого нажатия на сенсорном экране компонент не эмулирует.
contextmenuпо long-press шлёт Android Chrome и не шлёт iOS Safari, а своя эмуляция конфликтует со скроллом и выделением текста. Дублируйте действия видимой кнопкой; - пустое меню не открывается. Панель без единого пункта нечем фокусировать, и она стала бы ловушкой, из которой есть
только
Esc.
Пропы этого примера