Floating UI — это библиотека для позиционирования всплывающих элементов, таких как тултипы, поповеры, контекстные меню и модальные окна. Когда в интерфейсе присутствует несколько floating элементов, управление их позиционированием и взаимодействием становится сложнее. Основная цель — обеспечить корректное отображение, предотвращение наложений и плавную адаптацию к размерам окна и контента.
Каждый floating элемент должен быть привязан к reference элементу. В случае множественных всплывающих блоков важно правильно организовать иерархию привязок. Например, если один поповер вложен в другой, его reference элементом будет не DOM-элемент из основной страницы, а элемент внутреннего поповера. Это позволяет библиотеке точно рассчитывать позицию без пересечения с родительским элементом.
import { computePosition, offset, flip, shift } from '@floating-ui/dom';
const parentButton = document.querySelector('#parent-button');
const childButton = document.querySelector('#child-button');
const parentPopover = document.querySelector('#parent-popover');
const childPopover = document.querySelector('#child-popover');
computePosition(parentButton, parentPopover, {
middleware: [offset(10), flip(), shift({ padding: 5 })],
}).then(({ x, y }) => {
Object.assign(parentPopover.style, {
left: `${x}px`,
top: `${y}px`,
});
});
computePosition(childButton, childPopover, {
middleware: [offset(5), flip(), shift({ padding: 5 })],
}).then(({ x, y }) => {
Object.assign(childPopover.style, {
left: `${x}px`,
top: `${y}px`,
});
});
Middleware — это ключевой механизм управления поведением floating элементов. При работе с несколькими всплывающими блоками особое внимание стоит уделить следующим:
Для множественных элементов часто используется комбинация middleware с разной конфигурацией для каждого уровня вложенности. Это предотвращает наложение и обеспечивает читаемость интерфейса.
import { arrow } from '@floating-ui/dom';
computePosition(button, tooltip, {
middleware: [
offset(8),
flip(),
shift({ padding: 8 }),
arrow({ element: tooltipArrow })
],
});
Когда на странице одновременно присутствуют несколько floating
элементов, возникает необходимость контролировать их порядок
наложения. Floating UI напрямую не управляет
z-index, поэтому стоит внедрять логику динамического
назначения слоя:
let zIndexCounter = 1000;
function bringToFront(element) {
element.style.zIndex = ++zIndexCounter;
}
popover.addEventListener('mouseenter', () => bringToFront(popover));
Такой подход гарантирует, что активный элемент всегда отображается поверх остальных.
Множественные floating элементы часто содержат контент, который может изменяться по высоте или ширине. В таких случаях необходимо пересчитывать позицию при изменении размера:
const observer = new ResizeObserver(() => {
computePosition(button, popover, {
middleware: [offset(10), flip(), shift({ padding: 5 })],
}).then(({ x, y }) => {
Object.assign(popover.style, { left: `${x}px`, top: `${y}px` });
});
});
observer.observe(popover);
Это обеспечивает корректное позиционирование даже при динамическом контенте, изменении размеров окна или контекста.
В случае глубокой вложенности popover’ов рекомендуется:
const openPopovers = [];
function openPopover(popover) {
popover.style.display = 'block';
openPopovers.push(popover);
}
function closePopover(popover) {
popover.style.display = 'none';
const index = openPopovers.indexOf(popover);
if (index > -1) openPopovers.splice(index, 1);
}
При множественных floating элементах важно минимизировать количество
вызовов computePosition, особенно при анимациях или
скролле. Рекомендуется:
scroll и resize.function throttle(fn, wait) {
let timeout;
return (...args) => {
if (!timeout) {
timeout = setTimeout(() => {
fn(...args);
timeout = null;
}, wait);
}
};
}
window.addEventListener('scroll', throttle(updateAllPopovers, 16));
popoversManager, который хранит все
открытые элементы и их reference.Эти подходы позволяют поддерживать стабильное и предсказуемое поведение множественных floating элементов, минимизировать наложения и обеспечивать корректное взаимодействие с пользователем.