Menu
Випадне меню, телепортоване в body, з фліпом і притисканням до вікна.
Меню телепортується в body, а не рендериться в потоці. Це не оптимізація:
всередині контейнера з overflow: hidden або overflow-x: auto — а це будь-яка
таблиця — випадайка в потоці була б обрізана краєм контейнера.
Приклад
Обрано: —
API
Props
| Назва | Тип | Типово | Опис |
|---|---|---|---|
ariaLabel | string | undefined | Доступна назва панелі, особливо для `panelRole="dialog"`. |
placement | bottom-endbottom-startbottomtopright | "bottom-end" | Куди відкривати відносно тригера. Автоматично фліпається, якщо не влазить. |
panelRole | nonemenudialog | "menu" | Семантика панелі: menu для команд, dialog для складених контролів. |
width | string | — | Ширина панелі, напр. `"14rem"`. Без неї — за вмістом. |
disabled | boolean | — | — |
Події
| Назва | Payload | Опис |
|---|---|---|
update:open | [value: boolean] | Меню відкрито або закрито. |
Слоти
| Назва | Опис |
|---|---|
trigger | Тригер. `triggerAttrs` потрібно передати реальній кнопці через `v-bind`, а `toggle` — у `@click` без обгортки: з події він дізнається, що меню відкрили з клавіатури, і ставить фокус на перший пункт. |
content | Вміст панелі. `toggle` дає змогу закрити меню з пункту. |
Доступно через ref
| Назва | Тип | Опис |
|---|---|---|
open | (focus?: false | "first" | "last") => Promise<void> | Відкрити меню програмно; `'first'`/`'last'` переносить фокус на пункт. |
close | (restoreFocus?: boolean) => void | Закрити меню програмно. |
Коли використовувати
Для набору дій над однією сутністю, які не варті окремих кнопок: перейменувати, дублювати, експортувати, видалити.
Тригер малюєте самі через слот — компонент не нав'язує вигляду кнопки.
Передайте triggerAttrs реальній кнопці через v-bind, щоб ARIA-стан
належав фокусованому елементу, а не wrapper-у, а toggle — прямо в
@click="toggle", без обгортки: з події він дізнається, що меню відкрили
клавіатурою.
Коли НЕ використовувати
Не робіть меню з вибором значення. Для цього є UiSelect — він має
role="combobox", працює з клавіатури і повідомляє обране значення.
Не ховайте в меню головну дію екрана. Те, що потрібно всім, має бути кнопкою.
Доступність
Тригер отримує aria-haspopup і aria-expanded через triggerAttrs,
панель — role="menu".
Пунктам додавайте role="menuitem" самі: компонент не знає, чим ви їх
наповните.
Enter і Space на тригері відкривають меню й ставлять фокус на перший пункт, ArrowDown/ArrowUp — на перший чи останній. Відкрите мишею меню фокус не перехоплює.
Стрілки, Home та End переміщують фокус між пунктами, а друк літери — на пункт, що з неї починається (typeahead): буфер живе пів секунди, та сама літера поспіль перебирає пункти на неї.
Tab закриває меню й веде до елемента за тригером на сторінці, Shift+Tab —
на сам тригер: панель стоїть у кінці body, і нативний Tab вивів би фокус
за межі документа. Для складеного вмісту на кшталт налаштувань таблиці
використовуйте panelRole="dialog" і ariaLabel: тоді Tab ходить
контролами панелі й виходить лише з останнього.
Після закриття фокус повертається на тригер. Без цього Tab починав би обхід з початку сторінки, і користувач втрачав би місце.
Escape і клік «повз» ідуть через спільний стек оверлеїв: Escape отримує лише верхній шар (діалог, відкритий із пункту, закривається раніше за меню), а меню в модалці закривається від кліку деінде в тій самій модалці.
Деталі реалізації
Позиція рахується спільною геометрією з utils/overlayPosition, без
@floating-ui — компонент має копіюватись у чужий проєкт без нової
залежності. top біля верхнього краю перевертається вниз, right біля
правого — ліворуч, з вирівняними верхніми краями.
Меню, вище за вільне місце у вікні, отримує max-height і прокрутку — лише
тоді, коли справді не влазить: постійний overflow обрізав би кільця
фокуса пунктів.
Вимірювання відбувається двічі: перший раз із запасними габаритами (панелі ще немає в DOM), другий — коли вона їх отримала. Без другого проходу вибір напрямку відкриття інколи помилявся.
Слухач прокрутки навішено з capture: true. Подія scroll не спливає, тож без
цього відкрита панель лишалася б висіти на місці, поки контейнер під нею
від'їжджає.
Базовий z-index панелі — 1100; усередині глибоко вкладеного оверлея він
автоматично піднімається над шаром найближчого Modal або Drawer.