Обычное выпадающее меню — столбик ссылок. Он работает, пока пунктов пять-семь; дальше столбик вытягивается ниже экрана. Мегаменю раскладывает те же пункты в колонки с заголовками разделов, и структура сайта видна за один взгляд.
Когда оно нужно, а когда мешает
Мегаменю решает одну задачу: показать много пунктов сразу, не заставляя человека листать. Если пунктов мало, оно только добавляет лишний щелчок между человеком и страницей.
| Сколько пунктов | Что делать |
|---|---|
| До пяти | Ссылки прямо в шапке, без выпадения вообще |
| От шести до пятнадцати, есть группы | Мегаменю: колонка на группу, у каждого пункта строка пояснения |
| Больше двадцати | Страница-хаб со списком, в меню — одна ссылка на неё |
Панель — не склад для всех ссылок сайта
У меня в колонке «Мануалы» двенадцать пунктов, и это предел. Дальше панель перестаёт читаться и превращается в карту сайта, которую почему-то показывают поверх страницы.
Разметка: кнопка и панель
Пункт, который открывает панель, — это <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 не заходит внутрь закрытой панели