Context menus

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


Основные компоненты контекстного меню

  1. Reference Element Элемент, относительно которого будет позиционироваться меню. В случае контекстного меню это может быть весь документ, определённый контейнер или конкретный элемент списка.

  2. Popper Element Само всплывающее меню, которое появится при событии. Это может быть <div> с классами и стилями, оформляющими внешний вид меню.

  3. Modifiers Модификаторы позволяют контролировать поведение меню: смещение от элемента, предотвращение выхода за пределы экрана, автоматическое размещение, изменение размеров.


Инициализация Popper.js для контекстного меню

import { createPopper } from '@popperjs/core';

const reference = document.querySelector('#target');
const popper = document.querySelector('#context-menu');

const popperInstance = createPopper(reference, popper, {
  placement: 'bottom-start',
  modifiers: [
    {
      name: 'offset',
      options: {
        offset: [0, 8],
      },
    },
    {
      name: 'preventOverflow',
      options: {
        boundary: 'viewport',
      },
    },
  ],
});
  • placement определяет позицию меню относительно целевого элемента. В контекстных меню часто используется bottom-start или right-start.
  • Модификатор offset задаёт смещение по оси X и Y, чтобы меню не перекрывало указатель мыши.
  • Модификатор preventOverflow гарантирует, что меню не выйдет за пределы видимой области.

Управление открытием и закрытием меню

Контекстное меню обычно открывается на событие contextmenu, а закрывается при клике вне меню или на клавишу Escape.

document.addEventListener('contextmenu', (event) => {
  event.preventDefault();
  popper.style.display = 'block';
  
  popperInstance.setOptions({
    modifiers: [
      {
        name: 'eventListeners',
        enabled: true,
      },
    ],
  });

  popperInstance.update();
  popperInstance.state.elements.reference = {
    getBoundingClientRect: () => ({
      width: 0,
      height: 0,
      top: event.clientY,
      bottom: event.clientY,
      left: event.clientX,
      right: event.clientX,
    }),
    clientWidth: 0,
    clientHeight: 0,
  };
});

document.addEventListener('click', () => {
  popper.style.display = 'none';
});
  • Используется getBoundingClientRect, чтобы Popper мог позиционировать меню прямо под курсором.
  • Включение и обновление слушателей через eventListeners необходимо для корректной работы динамического позиционирования.

Использование динамических модификаторов

Для сложных интерфейсов часто требуется динамическое размещение меню, например, чтобы оно всегда оставалось в видимой области, даже если курсор находится у края экрана.

const dynamicPopper = createPopper(reference, popper, {
  placement: 'auto',
  modifiers: [
    {
      name: 'flip',
      options: {
        fallbackPlacements: ['top', 'right', 'left', 'bottom'],
      },
    },
    {
      name: 'preventOverflow',
      options: {
        mainAxis: true,
        altAxis: true,
        tether: false,
      },
    },
  ],
});
  • Модификатор flip позволяет Popper автоматически изменять позицию меню при недостатке места.
  • preventOverflow с параметрами mainAxis и altAxis контролирует обе оси и предотвращает выход меню за экран.

Анимация появления и скрытия

Для плавного UX контекстное меню часто анимируют с помощью CSS переходов, интегрируя их с Popper.js без влияния на вычисления позиции:

#context-menu {
  opacity: 0;
  transform: scale(0.95);
  transition: opacity 0.15s ease, transform 0.15s ease;
}

#context-menu.show {
  opacity: 1;
  transform: scale(1);
}
document.addEventListener('contextmenu', (event) => {
  event.preventDefault();
  popper.classList.add('show');
});

document.addEventListener('click', () => {
  popper.classList.remove('show');
});
  • Popper.js обеспечивает правильное позиционирование, а CSS отвечает за визуальную анимацию.
  • Использование transform: scale() предотвращает дрожание меню при появлении.

Поддержка вложенных и динамических меню

Для многоуровневых контекстных меню каждый уровень создаётся как отдельный Popper, позиционирующийся относительно родительского пункта:

const subMenuPopper = createPopper(parentMenuItem, subMenu, {
  placement: 'right-start',
  modifiers: [
    { name: 'offset', options: { offset: [0, 0] } },
    { name: 'preventOverflow', options: { boundary: 'viewport' } },
  ],
});
  • Такой подход обеспечивает независимое управление каждым подменю.
  • Автоматический flip и preventOverflow предотвращают выход вложенных меню за экран.

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

  • Отключать слушатели событий eventListeners, когда меню скрыто, чтобы не расходовать ресурсы.
  • Использовать strategy: 'fixed' для меню, которое должно оставаться привязанным к позиции курсора при прокрутке страницы.
const popperInstance = createPopper(reference, popper, {
  strategy: 'fixed',
});
  • Для больших приложений рекомендуется реиспользовать один Popper для нескольких элементов, меняя reference при каждом открытии.

Popper.js обеспечивает высокую точность позиционирования контекстных меню и гибкость при создании сложных интерактивных интерфейсов. Использование модификаторов, динамических ссылок на элемент и CSS-анимаций позволяет создавать как простые, так и многоуровневые контекстные меню с плавным и адаптивным поведением.