Как определить сторону для отображения контекстного меню, чтобы оно не выходило за пределы экрана?
Что проверяют
Умеете ли вы работать с координатами вьюпорта и размерами элемента и видите ли подводные камни: меню нужно измерить до показа, а у экрана четыре края, а не два. Следом часто спрашивают про вложенные подменю или просят назвать готовое решение.
Как отвечать
Алгоритм из трёх шагов:
- координаты: из события
contextmenuберутсяclientXиclientY, это координаты относительно вьюпорта. Меню ставится сposition: fixed, тогда прокрутку страницы учитывать не нужно; - измерение: меню сначала рендерится невидимым (
visibility: hidden, а неdisplay: none), затемgetBoundingClientRect()даёт его ширину и высоту. У элемента сdisplay: noneразмеры нулевые; - выбор стороны (flip): если
x + widthбольшеdocument.documentElement.clientWidth, меню открывается влево от курсора, то естьleftуменьшается на ширину меню. По вертикали так же: не влезает вниз, открываем вверх. Не помещается ни туда, ни туда: прижимаем к краю с отступом (shift) и включаем внутреннюю прокрутку черезmax-heightиoverflow: auto.
clientWidth у documentElement надёжнее innerWidth: он не включает полосу прокрутки, и меню не залезет под неё.
Для подменю логика та же, но опорой служит getBoundingClientRect() пункта родительского меню: открываемся справа от него, а если места нет, слева.
В продакшене это обычно не пишут руками: библиотека Floating UI делает то же через middleware flip и shift и пересчитывает позицию при прокрутке и ресайзе. В CSS есть anchor positioning с position-try-fallbacks: flip-inline, flip-block, но поддержку в целевых браузерах нужно проверить.
Где спотыкаются
Измеряют меню, пока оно скрыто через display: none, и получают нули. Проверка границ тогда всегда проходит, и меню вылезает за край.
Путают сдвиг с переворотом: код вида x = innerWidth - width просто прижимает меню к краю, и оно может закрыть курсор и пункт, по которому кликнули.
Забывают про клавиатуру и закрытие. Меню открывается и клавишей контекстного меню или Shift+F10, тогда координаты курсора бессмысленны и позицию берут от сфокусированного элемента. Закрывать меню нужно по Escape, клику снаружи, прокрутке и ресайзу.