Меню
Мегаменю: панели во всю ширину полосы под пунктами шапки. Наведение с задержкой намерения, безопасный треугольник, клавиатура, аккордеон на узком экране, ajax-панели — поверх обычного <details>, который работает и без скрипта.
Мегаменю — новый виджет слоя: своя разметка и свои стили
в griffinjs.css, как компонента ui его не существует,
и описан он только здесь. Дропдаун и подсказка — контроллеры
поверх существующих компонентов: их разметка не меняется, а что добавляет
скрипт, описано на странице
«Слои поверх страницы» → «С подключённым griffinjs.js».
Как работает
Полоса — обычная шапка .gr-navbar с классом .gr-megamenu
и атрибутом data-gr-megamenu. Пункт с панелью —
<details class="gr-megamenu-item"> внутри .gr-nav:
заголовок на <summary class="gr-nav-link">, панель —
.gr-megamenu-panel сразу за ним. Обычные ссылки в том же ряду
остаются ссылками.
<nav class="gr-navbar gr-megamenu" data-gr-megamenu>
<a class="gr-navbar-brand" href="/">Сайт</a>
<details class="gr-nav-toggle"><summary aria-label="Меню">☰</summary></details>
<ul class="gr-nav gr-nav-collapse">
<li>
<details class="gr-megamenu-item">
<summary class="gr-nav-link">Каталог</summary>
<div class="gr-megamenu-panel">…колонки, карточки, что угодно…</div>
</details>
</li>
<li><a class="gr-nav-link" href="/sale">Акции</a></li>
</ul>
</nav>
База без JS — этот же <details>: щелчок
по заголовку раскрывает панель, на широком экране CSS кладёт её под всю
полосу (position: absolute от .gr-megamenu), на узком —
аккордеоном под заголовком в свёрнутой навигации. Ни одной координаты
из скрипта: панель не в верхнем слое, и anchor positioning ей не нужен.
Скрипт добавляет то, чего <details> не умеет: единственную
открытую панель, наведение, клавиатуру, закрытие мимо и ajax.
| Класс | Назначение |
|---|---|
.gr-megamenu | На полосе (.gr-navbar): контейнер, от которого позиционируются панели |
.gr-megamenu-item | <details> пункта; открытый подсвечивает заголовок и переворачивает стрелку |
.gr-megamenu-panel | Панель: поверхность меню, отступ --gr-gap; на широком экране — во всю полосу, с тенью |
Параметры data-gr-megamenu
| Параметр | По умолчанию | Что делает |
|---|---|---|
hover | true | Открывать по наведению на широком экране; hover: false — только щелчком и клавиатурой |
delay | 120 | мс задержки намерения: панель не открывается от случайного пролёта курсора |
hide | 250 | мс после ухода указателя с полосы до закрытия |
bp | md | Брейкпоинт широкого режима — токен --gr-bp-md; ниже него наведение и стрелки выключены, панели — аккордеон |
На пункте: data-gr-src с адресом фрагмента — ajax-панель
(см. ниже).
С griffinjs.js и без
| Без скрипта | Со скриптом | |
|---|---|---|
| Открытие | щелчок по заголовку | щелчок, наведение с задержкой (мышь и перо), ↓ с клавиатуры |
| Несколько панелей | могут быть открыты все | открыта одна: следующая закрывает предыдущую |
| Закрытие | тот же щелчок, панель пропадает сразу | плюс щелчок мимо полосы, уход указателя, Esc, уход фокуса; уход панели анимирует тема — состояние closing |
| Клавиатура | Tab, Enter/Space на заголовке | плюс ← → Home End по ряду, ↓ в панель, ↑ ↓ внутри, Esc назад |
| Состояние для программ чтения | родное у <summary> | плюс aria-expanded и aria-controls на заголовке |
| Ajax-панели | — | data-gr-src грузится при первом открытии |
| Узкий экран | аккордеон в свёрнутой навигации | тот же аккордеон, одна открытая панель, Esc |
Наведение и касание
Поведение выбирается по pointerType каждого события,
а не по типу устройства: iPad с мышью и Surface с пером — гибриды.
Мышь и перо открывают панель после delay — случайный пролёт
курсора через ряд не раскрывает ничего. Касание наведения не знает:
тап по заголовку открывает панель ровно так же, как без скрипта, а переход
по разделу — ссылка внутри панели.
Между заголовком и открытой панелью соседний пункт не перехватывает курсор: пока указатель движется в треугольник «текущая точка — верхние углы панели», переключение откладывается. Диагональ к нужной ссылке не закрывает меню на полпути.
Клавиатура
Схема — WAI-ARIA disclosure navigation: полоса остаётся
навигацией, без ролей menu/menuitem, которые
превратили бы её в меню приложения и поменяли ожидания программ чтения.
| Где | Клавиша | Действие |
|---|---|---|
| Ряд | → ← | Следующий/предыдущий пункт ряда по кругу — заголовки и ссылки, минуя панели |
| Ряд | Home End | Первый/последний пункт ряда |
| Заголовок | ↓ | Открыть панель и перейти к первой ссылке в ней |
| Заголовок | Enter Space | Открыть/закрыть — родное поведение <summary> |
| Панель | ↓ ↑ | По ссылкам и кнопкам панели |
| Панель, заголовок | Esc | Закрыть панель и вернуть фокус на заголовок |
| Везде | Tab | Обычный обход; уход фокуса с полосы закрывает панель |
Ajax-панели
Пункт с data-gr-src открывается с пустой панелью, и скрипт
один раз загружает адрес и вставляет ответ как HTML. Состояние пишется
на пункт: data-gr-state="loading" — панель держит место
и мигает фоном, ready — загружено, error — сервер
не ответил (панель остаётся пустой, ошибка — в консоли). Вложенные
виджеты в загруженном фрагменте поднимает сканер слоя.
<details class="gr-megamenu-item" data-gr-src="/menu/brands.html">
<summary class="gr-nav-link">Бренды</summary>
<div class="gr-megamenu-panel"></div>
</details>
Ответ вставляется как есть. Адрес в data-gr-src
должен вести на свой сервер и отдавать фрагмент, которому вы доверяете:
скрипт не чистит разметку и не проверяет, что в ней нет
<script>.
Закрытие с анимацией: состояние closing
Платформа снимает open у <details> в тот же
кадр, и панель пропадает без перехода — сколько бы transition
тема ни писала. Виджет закрывает иначе: ставит на пункт
data-gr-state="closing", ждёт transitionend
или animationend самой панели (не потомка) — с таймаутом
в длительность --gr-transition плюс 50 мс — и лишь затем
снимает open. Прежнее состояние пункта (ready
у ajax-панели) возвращается на место. Щелчок по заголовку открытого
пункта, Enter и Space на нём идут тем же путём;
щелчок по закрывающемуся пункту отменяет закрытие — панель остаётся.
Так во всех браузерах, и у темы одно правило: дропдаун
на <details> закрывается так же, с состоянием на обёртке
.gr-dropdown и переходом на .gr-dropdown-panel.
.gr-megamenu-panel { transition: opacity var(--gr-transition); }
.gr-megamenu-item[data-gr-state="closing"] > .gr-megamenu-panel { opacity: 0; }
.gr-dropdown-panel { transition: opacity var(--gr-transition); }
.gr-dropdown[data-gr-state="closing"] > .gr-dropdown-panel { opacity: 0; }
CSS слоя переходов на закрытие не задаёт: анимирует тема. Без её правил
закрытие идёт через таймаут (250 мс при токене по умолчанию — на глаз
не отличить), при --gr-transition: 0s — сразу.
Почему не ::details-content. Современные
движки умеют анимировать уход содержимого <details>
сами — переходом на ::details-content с
transition-behavior: allow-discrete, — и первая версия
виджета там состояния не ставила. Замер на трёх движках
(browser/closing.spec.mjs): Chromium и WebKit анимируют,
а Firefox 153 при поддержке обоих признаков переводит
content-visibility в hidden в том же кадре,
и переход не начинается. Развилка по признакам оставляла бы Firefox
без анимации, поэтому состояние ставится везде. Переход
на ::details-content в теме не пишите: вместе с правилом
по состоянию он дал бы два ухода подряд.
Узкий экран
Ниже брейкпоинта bp (по умолчанию md, 768px) переключатель
шапки прячет навигацию, а панели ложатся аккордеоном под заголовками:
тот же <details>, только в потоке. Наведения и стрелок
здесь нет — только тап, Tab, Enter и Esc;
правило «одна открытая панель» остаётся. Переход через брейкпоинт
ловится через matchMedia по токену --gr-bp-*
подключённого CSS.
API
var menu = GriffinJS.instance(document.querySelector('[data-gr-megamenu]'), 'megamenu');
menu.items // [{ el, summary, panel, loaded }]
menu.open(menu.items[0]);
menu.close(menu.items[0]); // через состояние closing
menu.closeAll();
menu.current(); // открытый пункт или null
menu.destroy(); // снимает всё: атрибуты, слушатели, таймеры
Примеры использования
Полоса витрины на трёх приёмах: колонки ссылок из .gr-nav-vertical,
промо-блок из утилит, ajax-панель. Все примеры работают и без скрипта —
как обычный <details>; бейдж отмечает то, что добавляет
griffinjs.js.
Каталог витрины: колонки разделов и промо
Панель — обычная вёрстка Griffincss: три колонки .gr-nav-vertical
во флексе и промо-карточка. Сложная раскладка панели — сильная сторона
библиотеки, и скрипту до неё дела нет.
<details class="gr-megamenu-item">
<summary class="gr-nav-link">Каталог</summary>
<div class="gr-megamenu-panel">
<div class="gr-flex gr-gap-lg gr-flex-wrap">
<div class="gr-flex-1">
<p class="mm-title">Компьютеры</p>
<ul class="gr-nav gr-nav-vertical">…</ul>
</div>
…
<div class="gr-flex-1 mm-promo">Скидки недели</div>
</div>
</div>
</details>
Меню без наведения: только щелчок и клавиатура
Для админки или приложения, где случайное раскрытие мешает, наведение
отключают — hover: false. Остальное — единственная открытая
панель, стрелки, Esc, закрытие мимо — остаётся.
<nav class="gr-navbar gr-megamenu" data-gr-megamenu="hover: false">…</nav>
Панель с сервера
Дерево каталога в CMS меняется, и держать его в шаблоне шапки неудобно:
пункт получает data-gr-src, а панель приезжает при первом
открытии. В примере адрес — data:-URL, на сайте это путь
к фрагменту, который отдаёт сервер.
<details class="gr-megamenu-item" data-gr-src="/menu/brands.html">
<summary class="gr-nav-link">Бренды</summary>
<div class="gr-megamenu-panel"></div>
</details>
/* пока грузится */
.gr-megamenu-item[data-gr-state="loading"] > .gr-megamenu-panel { min-block-size: 6rem; }
Без скрипта
Та же разметка на странице без griffinjs.js: панель открывается
щелчком по заголовку и закрывается им же, на узком экране складывается
аккордеоном. Это и есть база, от которой скрипт только прибавляет.