В библиотеке Floating UI управление позиционированием всплывающих элементов строится на концепции reference element и floating element. Для вложенных меню и подменю это особенно важно, так как каждое подменю должно корректно позиционироваться относительно своего родительского элемента, учитывая границы экрана, перекрытия и смещения.
Reference element – это элемент DOM, к которому привязывается всплывающее меню. Для подменю reference elementом будет пункт родительского меню. Floating element – это само подменю, которое должно появляться рядом с reference element с учетом заданных стратегий позиционирования.
Для корректного отображения подменю используется функция
computePosition, которая возвращает координаты, необходимые
для абсолютного позиционирования элемента. Пример базового вызова:
import { computePosition, offset, flip, shift } from '@floating-ui/dom';
const parentMenuItem = document.querySelector('#parent-item');
const submenu = document.querySelector('#submenu');
computePosition(parentMenuItem, submenu, {
placement: 'right-start',
middleware: [offset(5), flip(), shift({ padding: 8 })],
}).then(({ x, y }) => {
Object.assign(submenu.style, {
left: `${x}px`,
top: `${y}px`,
});
});
Ключевые моменты:
placement определяет начальное расположение подменю
относительно родительского пункта (right-start,
left-start, bottom-start и т.д.).offset добавляет смещение, чтобы подменю не прилегало
вплотную к родительскому элементу.flip позволяет автоматически менять сторону отображения
подменю, если пространство на экране ограничено.shift корректирует позицию, чтобы подменю не выходило
за границы окна.Для вложенных меню критично правильное управление событиями мыши и клавиатуры:
parentMenuItem.addEventListener('mouseenter', () => {
submenu.style.display = 'block';
computePosition(parentMenuItem, submenu, {
placement: 'right-start',
middleware: [offset(5), flip(), shift({ padding: 8 })]
}).then(({ x, y }) => {
Object.assign(submenu.style, { left: `${x}px`, top: `${y}px` });
});
});
parentMenuItem.addEventListener('mouseleave', () => {
submenu.style.display = 'none';
});
Особенности управления вложенными меню:
focus и blur для каждого пункта меню и
подменю.Esc.Для меню с несколькими уровнями подменю стратегия позиционирования остается той же, но reference element теперь меняется на элемент родительского подменю:
const submenuLevel1 = document.querySelector('#submenu-level-1');
const submenuLevel2 = document.querySelector('#submenu-level-2');
computePosition(submenuLevel1, submenuLevel2, {
placement: 'right-start',
middleware: [offset(5), flip(), shift({ padding: 8 })],
}).then(({ x, y }) => {
Object.assign(submenuLevel2.style, { left: `${x}px`, top: `${y}px` });
});
Рекомендации при работе с несколькими уровнями:
computePosition, чтобы middleware корректно учитывал
границы экрана.flip и shift особенно важны на
нижних уровнях, где подменю могут выходить за видимую область.Floating UI предоставляет гибкий набор middleware для управления поведением подменю:
Пример с использованием стрелки подменю:
import { arrow } from '@floating-ui/dom';
const arrowEl = document.querySelector('#submenu-arrow');
computePosition(parentMenuItem, submenu, {
placement: 'right-start',
middleware: [offset(5), flip(), shift({ padding: 8 }), arrow({ element: arrowEl })],
}).then(({ x, y, middlewareData }) => {
Object.assign(submenu.style, { left: `${x}px`, top: `${y}px` });
const { x: arrowX, y: arrowY } = middlewareData.arrow;
Object.assign(arrowEl.style, { left: `${arrowX}px`, top: `${arrowY}px` });
});
Middleware arrow позволяет динамически подстраивать
положение визуальной стрелки, указывающей на родительский пункт
меню.
Для сложных меню с несколькими уровнями важно минимизировать количество вычислений позиции:
computePosition только при открытии подменю и
изменении размеров окна.requestAnimationFrame при динамическом
отслеживании позиции подменю при наведении мыши.Подменю на мобильных устройствах часто открываются по клику, а не по наведению:
parentMenuItem.addEventListener('click', () => {
const isOpen = submenu.style.display === 'block';
submenu.style.display = isOpen ? 'none' : 'block';
if (!isOpen) {
computePosition(parentMenuItem, submenu, { placement: 'bottom-start', middleware: [offset(5), flip(), shift({ padding: 8 })] })
.then(({ x, y }) => {
Object.assign(submenu.style, { left: `${x}px`, top: `${y}px` });
});
}
});
Особенности мобильного UX:
flip и shift предотвращают
обрезание подменю на маленьких экранах.computePosition.offset, flip,
shift и arrow позволяют динамически
адаптировать подменю к размеру окна и положению родительского
элемента.Эта стратегия обеспечивает гибкое, отзывчивое и корректное отображение подменю во всех сценариях, поддерживая высокое качество UX на десктопе и мобильных устройствах.