Обычное выпадающее меню — столбик ссылок. Он работает, пока пунктов пять-семь; дальше столбик вытягивается ниже экрана. Мегаменю раскладывает те же пункты в колонки с заголовками разделов, и структура сайта видна за один взгляд.

Когда оно нужно, а когда мешает

Мегаменю решает одну задачу: показать много пунктов сразу, не заставляя человека листать. Если пунктов мало, оно только добавляет лишний щелчок между человеком и страницей.

Что брать при разном числе пунктов
Сколько пунктов Что делать
До пяти Ссылки прямо в шапке, без выпадения вообще
От шести до пятнадцати, есть группы Мегаменю: колонка на группу, у каждого пункта строка пояснения
Больше двадцати Страница-хаб со списком, в меню — одна ссылка на неё

Панель — не склад для всех ссылок сайта

У меня в колонке «Мануалы» двенадцать пунктов, и это предел. Дальше панель перестаёт читаться и превращается в карту сайта, которую почему-то показывают поверх страницы.

Разметка: кнопка и панель

Пункт, который открывает панель, — это <button>, а не ссылка. Ссылка обещает переход на другую страницу, а здесь щелчок ничего не открывает, кроме панели. Кнопка заодно бесплатно получает от браузера обработку Enter и Пробела.

Два атрибута обязательны. aria-expanded сообщает, открыта панель или закрыта, — без него программа чтения экрана назовёт кнопку просто «Мануалы, кнопка» и умолчит о том, что она что-то разворачивает. aria-controls указывает на то, чем кнопка управляет.

Один пункт мегаменю
<li class="megamenu__item">
  <button class="megamenu__button" type="button"
          aria-expanded="false" aria-controls="panel-manualy">
    Мануалы
  </button>

  <div class="megamenu__panel" id="panel-manualy" hidden>
    <div class="container megamenu__grid">
      <div class="megamenu__col">
        <p class="megamenu__title">Блоки страницы</p>
        <ul class="megamenu__list">
          <li>
            <a href="/manualy/megamenyu/">
              Мегаменю
              <span class="megamenu__note">Меню колонками во всю ширину</span>
            </a>
          </li>
        </ul>
      </div>
    </div>
  </div>
</li>

Внутри панели — обычный список ссылок. Не <div> с обработчиком щелчка: то, что ведёт на страницу, обязано быть <a href>, иначе ссылка не откроется в новой вкладке средним щелчком и её не увидит поисковик.

Мышь и клавиатура — это два разных жеста

Мышью панель открывается наведением: привычно и быстро. Клавиатура наведения не знает, поэтому там работает Enter или Пробел на кнопке, а Esc закрывает.

Здесь меня ждали грабли. Если повесить на кнопку обычное переключение «открыть — закрыть», мышь начинает работать наоборот: к моменту щелчка панель уже раскрыта наведением, и щелчок её захлопывает. Человек навёл, увидел список, нажал — и всё пропало.

Различить жесты помогает свойство detail у события. У щелчка мышью в нём число нажатий, а у щелчка, который браузер сам сделал из Enter или Пробела, — ноль.

Щелчок мышью открывает, но не закрывает
button.addEventListener('click', (event) => {
  // detail === 0 — щелчок пришёл с клавиатуры: Enter или Пробел
  if (event.detail === 0) toggle(button);
  else open(button);
});

Закрывать тоже нужно в трёх случаях

Нажатие Esc, щелчок мимо меню и уход фокуса за пределы панели по Tab. Последнее забывают чаще всего: человек дошёл по клавишам до подвала страницы, а панель всё ещё висит поверх текста.

На телефоне мегаменю не бывает

Три колонки на экране в 360 пикселей не помещаются никаким способом. На узком экране показывается другое — список в столбик, который выезжает по кнопке-бургеру. Это не «то же меню поменьше», а отдельный кусок разметки со своим классом, и переключаются они медиавыражением.

Выезжающую панель не класть внутрь шапки

У шапки этого сайта стоит размытие фона — backdrop-filter. Такой фильтр делает элемент точкой отсчёта для всего, что внутри лежит с position: fixed, и панель во весь экран схлопывается в полоску высотой с шапку. Лечится переносом панели наружу: сразу после закрывающего тега шапки.

Что попросить у нейросети

Разметку и стили мегаменю можно списать отсюда, а вот поведение проще заказать целиком: там много мелочей, и все они про доступность.

Мой запрос

Напиши скрипт на чистом JavaScript для мегаменю в шапке сайта. Разметка такая: в теге навигации лежат пункты, у каждого кнопка с атрибутами aria-expanded и aria-controls, панель рядом с кнопкой скрыта атрибутом hidden.

Поведение: на устройстве с мышью панель открывается наведением и закрывается при уходе курсора с задержкой около 150 мс. Щелчок мышью по кнопке всегда открывает панель и никогда не закрывает её. С клавиатуры Enter и Пробел переключают, Esc закрывает и возвращает фокус на кнопку, уход фокуса за пределы меню по Tab закрывает панель.

Открытой может быть только одна панель. Скрипт должен работать без ошибок, если на странице мегаменю нет вообще. Никаких библиотек, без сборщика, с комментариями по-русски.

Ответ нейросети

Что стоит поправить в задании. Наведение лучше включать не по ширине экрана, а по возможностям устройства: медиавыражение (hover: hover) отличает мышь от пальца. Иначе на планшете наведение сработает от касания и панель откроется сама.

Задержка на закрытие обязательна. Между кнопкой и панелью почти всегда есть зазор в несколько пикселей. Если закрывать сразу по уходу курсора, панель будет захлопываться, пока человек ведёт мышь к нужному пункту.

Проверка ухода фокуса. Событие focusout происходит раньше, чем фокус встаёт на новый элемент, поэтому смотреть надо не на event.target, а на event.relatedTarget — и проверять, что он не лежит внутри меню.

Ниже — код с этими поправками; он закрывает открытую панель перед открытием следующей и ничего не делает, если разметки мегаменю на странице нет.

В Evolution CMS пункты берутся из дерева

Писать пункты руками в шаблоне — значит лезть в шаблон при каждой новой странице. За списки ссылок в Evolution отвечает сниппет DocLister: он обходит дерево ресурсов и отдаёт готовый список. Колонок в меню три, значит и вызова три — по одному на раздел.

Вызов в шаблоне вместо всей разметки
[[DocLister?
  &parents=`3`
  &sortBy=`menuindex`
  &sortDir=`ASC`
  &tvList=`menu_desc`
  &tpl=`megamenuRow`
]]

&parents — номер раздела, чьи страницы идут в колонку, &tpl — имя чанка, которым рисуется один пункт. Вёрстка остаётся вашей: сниппет решает, какие страницы вывести, а как они выглядят — ваш чанк. Строка пояснения под названием берётся из TV-поля страницы и запрашивается поимённо в &tvList; в чанке она доступна как [+tv.menu_desc+]. Порядок задан явно не для красоты: без &sortBy DocLister сортирует список по дате публикации, а не по порядку в дереве.

Частые ошибки

Чем чаще всего ломают мегаменю
Ошибка Чем плохо Как правильно
Панель открывается только наведением С клавиатуры до пунктов не добраться вообще Кнопка с Enter, Пробелом и Esc
Щелчок мышью переключает состояние Панель захлопывается ровно в момент щелчка Мышью — всегда открыть, различать по event.detail
Панель спрятана через opacity: 0 Скрытые ссылки остаются в обходе по Tab Атрибут hidden или display: none
Пункты — div с обработчиком Нет средней кнопки мыши, нет адреса, нет поисковика Обычные ссылки с атрибутом href
То же меню на телефоне Колонки не помещаются, страница едет вбок Отдельная выезжающая панель в столбик

Что должно получиться

  • Панель открывается наведением, щелчком и с клавиатуры, закрывается по Esc
  • У кнопки меняется aria-expanded, у панели снимается hidden
  • Открыта не больше одной панели сразу
  • На узком экране вместо панели работает меню в столбик
  • Обход по Tab не заходит внутрь закрытой панели