Меню

Мегаменю: панели во всю ширину полосы под пунктами шапки. Наведение с задержкой намерения, безопасный треугольник, клавиатура, аккордеон на узком экране, ajax-панели — поверх обычного <details>, который работает и без скрипта.

GriffinJS packages/ui/src/griffinjs/widgets/megamenu.js scss/griffinjs/_megamenu.scss

Мегаменю — новый виджет слоя: своя разметка и свои стили в 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

ПараметрПо умолчаниюЧто делает
hovertrueОткрывать по наведению на широком экране; hover: false — только щелчком и клавиатурой
delay120мс задержки намерения: панель не открывается от случайного пролёта курсора
hide250мс после ухода указателя с полосы до закрытия
bpmdБрейкпоинт широкого режима — токен --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>
Наведите на «Каталог» или нажмите с клавиатуры нужен griffinjs.js

Меню без наведения: только щелчок и клавиатура

Для админки или приложения, где случайное раскрытие мешает, наведение отключают — hover: false. Остальное — единственная открытая панель, стрелки, Esc, закрытие мимо — остаётся.

<nav class="gr-navbar gr-megamenu" data-gr-megamenu="hover: false">…</nav>
Щелчок, , , Esc; наведение не открывает нужен griffinjs.js

Панель с сервера

Дерево каталога в 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

Без скрипта

Та же разметка на странице без griffinjs.js: панель открывается щелчком по заголовку и закрывается им же, на узком экране складывается аккордеоном. Это и есть база, от которой скрипт только прибавляет.