Popper.js — это библиотека, обеспечивающая позиционирование всплывающих элементов относительно других элементов на странице. При работе с обычным DOM все достаточно просто: Popper получает ссылку на целевой элемент (reference) и всплывающий элемент (popper), после чего вычисляет оптимальную позицию с учётом границ окна и других ограничений. Но при работе с Shadow DOM возникают специфические нюансы, которые требуют более тщательного подхода.
Shadow DOM позволяет создавать инкапсулированные компоненты с собственной разметкой и стилями. Основные свойства:
document.querySelector) не видят элементы внутри shadow
root.Для Popper.js это означает, что ссылки на элементы должны быть точными и учитывать путь к shadow root, иначе позиционирование будет некорректным.
Пример базового использования:
const shadowHost = document.querySelector('#shadow-host');
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
const button = document.createElement('button');
button.textContent = 'Открыть поппер';
shadowRoot.appendChild(button);
const tooltip = document.createElement('div');
tooltip.textContent = 'Привет, я поппер';
tooltip.style.position = 'absolute';
shadowRoot.appendChild(tooltip);
import { createPopper } from '@popperjs/core';
createPopper(button, tooltip, {
placement: 'top',
modifiers: [
{
name: 'offset',
options: { offset: [0, 8] }
},
{
name: 'preventOverflow',
options: { boundary: shadowRoot }
}
]
});
Ключевые моменты:
shadowRoot используется как контейнер для элементов,
включая поппер.boundary в модификаторе
preventOverflow позволяет ограничивать область, внутри
которой Popper будет пытаться разместить всплывающее окно. Для Shadow
DOM этот параметр часто нужно указывать явно.shadowRoot.querySelector, а не с
document.querySelector.boundary
и rootBoundaryPopper.js использует два типа границ:
В контексте Shadow DOM:
boundary можно установить на shadowRoot
или на конкретный контейнер внутри него.rootBoundary: 'document' обычно не учитывает
ограничения shadow root, поэтому рекомендуется использовать
rootBoundary: 'viewport' или ограничивать через
boundary.Пример с использованием rootBoundary:
createPopper(button, tooltip, {
placement: 'right',
modifiers: [
{
name: 'preventOverflow',
options: {
boundary: shadowRoot,
rootBoundary: 'viewport'
}
}
]
});
Это гарантирует, что поппер не выйдет за пределы видимой области окна, даже если shadow root расположен глубоко внутри DOM.
Popper.js автоматически отслеживает изменения размеров и положения
reference и popper через ResizeObserver и
MutationObserver. В Shadow DOM важно помнить:
Пример наблюдателя внутри shadow root:
const observer = new MutationObserver(() => popperInstance.update());
observer.observe(shadowRoot, { childList: true, subtree: true });
Стили с position: fixed Внутри
Shadow DOM элементы с position: fixed привязываются к
viewport, а не к shadow root. Для корректного позиционирования лучше
использовать absolute и обновлять позицию через
Popper.js.
Слои и z-index Shadow DOM создает собственный
stacking context. Поппер может оказаться под другими элементами, если
его shadow root не имеет достаточного z-index. Решение —
явно задавать высокий z-index для popper.
Передача событий Внутри shadow root события
клика и ховера могут не всплывать до document. Для автоматического
закрытия тултипов это нужно учитывать, используя методы
composed: true при создании событий.
boundary правильно.Пример с arrow:
const arrow = document.createElement('div');
arrow.className = 'arrow';
tooltip.appendChild(arrow);
createPopper(button, tooltip, {
placement: 'top',
modifiers: [
{ name: 'arrow', options: { element: arrow } }
]
});
strategy: 'absolute' вместо
fixed при работе с Shadow DOM.ResizeObserver
и MutationObserver) внутри shadow root.Popper на несколько элементов через динамическое обновление
reference.Popper.js полностью совместим с Shadow DOM при корректной настройке границ, стратегии позиционирования и наблюдателей. Главная задача — правильно указать элементы и контейнеры для вычислений, чтобы библиотека могла корректно вычислять координаты и ограничения всплывающих элементов.