Shadow DOM

Popper.js — это библиотека, обеспечивающая позиционирование всплывающих элементов относительно других элементов на странице. При работе с обычным DOM все достаточно просто: Popper получает ссылку на целевой элемент (reference) и всплывающий элемент (popper), после чего вычисляет оптимальную позицию с учётом границ окна и других ограничений. Но при работе с Shadow DOM возникают специфические нюансы, которые требуют более тщательного подхода.


Особенности Shadow DOM

Shadow DOM позволяет создавать инкапсулированные компоненты с собственной разметкой и стилями. Основные свойства:

  • Изоляция стилей: стили внутри shadow root не наследуются из глобального DOM.
  • Изоляция структуры: стандартные селекторы CSS не могут напрямую обращаться к элементам внутри shadow root.
  • Ограниченная видимость: стандартные методы поиска DOM (например, document.querySelector) не видят элементы внутри shadow root.

Для Popper.js это означает, что ссылки на элементы должны быть точными и учитывать путь к shadow root, иначе позиционирование будет некорректным.


Создание поппера внутри Shadow DOM

Пример базового использования:

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 этот параметр часто нужно указывать явно.
  • Все DOM-операции должны работать с shadowRoot.querySelector, а не с document.querySelector.

Работа с boundary и rootBoundary

Popper.js использует два типа границ:

  1. boundary — элемент, ограничивающий видимую область поппера.
  2. rootBoundary — верхняя граница, обычно это viewport или document.

В контексте 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, иначе изменения внутри инкапсулированного DOM могут не фиксироваться.
  • Если popper или reference находятся внутри разных shadow root, их координаты должны учитываться относительно общего viewport.

Пример наблюдателя внутри shadow root:

const observer = new MutationObserver(() => popperInstance.update());
observer.observe(shadowRoot, { childList: true, subtree: true });

Проблемы с позиционированием внутри Shadow DOM

  1. Стили с position: fixed Внутри Shadow DOM элементы с position: fixed привязываются к viewport, а не к shadow root. Для корректного позиционирования лучше использовать absolute и обновлять позицию через Popper.js.

  2. Слои и z-index Shadow DOM создает собственный stacking context. Поппер может оказаться под другими элементами, если его shadow root не имеет достаточного z-index. Решение — явно задавать высокий z-index для popper.

  3. Передача событий Внутри shadow root события клика и ховера могут не всплывать до document. Для автоматического закрытия тултипов это нужно учитывать, используя методы composed: true при создании событий.


Модификаторы Popper.js в Shadow DOM

  • offset — работает без изменений, можно задать смещение поппера.
  • flip — позволяет менять сторону появления. Для shadow root важно задать boundary правильно.
  • preventOverflow — критичен, так как стандартный viewport может быть недоступен напрямую внутри shadow root.
  • arrow — стрелка работает как обычно, но должна быть добавлена внутри того же shadow root, что и popper.

Пример с arrow:

const arrow = document.createElement('div');
arrow.className = 'arrow';
tooltip.appendChild(arrow);

createPopper(button, tooltip, {
  placement: 'top',
  modifiers: [
    { name: 'arrow', options: { element: arrow } }
  ]
});

Рекомендации по производительности

  1. Использовать strategy: 'absolute' вместо fixed при работе с Shadow DOM.
  2. Минимизировать количество наблюдателей (ResizeObserver и MutationObserver) внутри shadow root.
  3. При большом количестве попперов использовать один экземпляр Popper на несколько элементов через динамическое обновление reference.

Popper.js полностью совместим с Shadow DOM при корректной настройке границ, стратегии позиционирования и наблюдателей. Главная задача — правильно указать элементы и контейнеры для вычислений, чтобы библиотека могла корректно вычислять координаты и ограничения всплывающих элементов.