Раскрывающиеся списки с учетом области просмотра

Раскрывающаяся панель действий или группа кнопок во всплывающем окне – это удобный способ предоставить пользователям доступ к дополнительным функциям, не занимая много места. Этот шаблон наложения имеет сложную структуру, поскольку панель должна оставаться привязанной к элементу триггера, адаптируясь к ограничениям области просмотра. Раньше для этого требовались сложные библиотеки JavaScript (например, Popper.js или Floating UI), которые рассчитывали положение и обрабатывали коллизии.

Позиционирование привязки CSS предоставляет декларативный, оптимизированный по производительности способ обработки этих отношений полностью в CSS, позволяя браузерам управлять логикой позиционирования и переполнения нативно.

1. Определите связь между кнопкой и панелью

Сначала нужно создать кнопку, которая будет открывать контейнер оверлея с помощью Popover API.

ОБЯЗАТЕЛЬНОЕ отличие в специальных возможностях. Этот шаблон явно моделирует группу кнопок, которая показывается во всплывающем окне, а не настоящее меню ARIA. Не применяйте role="menu" или role="menuitem", если вы не реализовали полностью соответствующий контракт навигации с помощью клавиатуры (например, обработку пространственной навигации с помощью клавиш со стрелками между элементами). По этой же причине не добавляйте в триггер aria-haspopup. Его значение (menu, listbox, tree, grid или устаревшее true) гарантирует, что цель имеет соответствующую роль, а в этом шаблоне это не так.

<button popovertarget="action-panel">
  Open Actions
</button>
<!-- Use the Popover API (`popover="auto"`) for the overlay to ensure it is placed in the top layer and handled accessibly by the browser. -->
<div id="action-panel" popover="auto" class="panel">
  <button class="action-item" type="button">Edit</button>
</div>

Это создает неявную связь привязки между кнопкой и панелью, так что панель может быть расположена относительно кнопки изначально, без необходимости использования anchor-name на кнопке или position-anchor на панели.

2. Позиционирование с помощью position-area

Вместо того чтобы вручную задавать смещение по осям top и left, используйте position-area, чтобы разместить целевой элемент в сетке 3 x 3 относительно опорного элемента.

.panel {
  /* 
     Position the panel below the anchor (block-end), 
     aligned to the start of the anchor and spanning to its end (span-inline-end).
  */
  position-area: block-end span-inline-end;
  
  /* Reset insets to allow the grid to take control */
  inset: auto;
}

Используйте логические ключевые слова (span-inline-end, block-start) вместо физических (left, top), чтобы автоматически поддерживать письмо справа налево и другие режимы письма.

ОБЯЗАТЕЛЬНО. Не смешивайте физические и логические ключевые слова в position-area.

3. Реализуйте резервные варианты

Чтобы панель не обрезалась по краю экрана, определите "тактики", которые браузер должен использовать, если панель не помещается в позиции по умолчанию.

.panel {
  /* 
     If the panel overflows the bottom, flip it to the top (flip-block).
     If it overflows the inline edges, flip it horizontally (flip-inline).
     If it overflows both (e.g., in a corner), flip on both axes (flip-block flip-inline).
  */
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}

Если в раскрывающемся или всплывающем меню есть стрелка или граница, стиль которой должен меняться в зависимости от выбранной резервной позиции, ознакомьтесь с разделом Подсказки, зависящие от позиции. О том, как декларативно вызывать диалоговые и всплывающие окна без JavaScript, читайте в статье Декларативное управление диалоговыми и всплывающими окнами.

Резервные стратегии

Резервные варианты и поддержка Popover в браузерах

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

Полифил. Чтобы обеспечить поддержку атрибута popover в старых браузерах, условно загрузите @oddbird/popover-polyfill. ОБЯЗАТЕЛЬНО. Проверьте наличие свойства popover в HTMLElement.prototype и загрузите полифил, только если встроенная поддержка отсутствует. Не загружайте его без необходимости.

С помощью сборщика или карты импорта:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

Если вы не используете сборщик, импортируйте из CDN в <script type="module">:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

Ограничение, связанное со стилями. Полифил не может определить настоящий псевдокласс :popover-open, поэтому вместо него применяется класс .\:popover-open. ОБЯЗАТЕЛЬНО. Объедините эти два правила с помощью :is() или :where(), иначе браузеры, не поддерживающие :popover-open, будут игнорировать правило целиком.

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

В качестве альтернативы для устаревшего резервного варианта без полифила используйте position: fixed и вручную рассчитайте координаты с помощью getBoundingClientRect() или используйте позиционирование по умолчанию с помощью inset: auto, если это допустимо для вашего случая.

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

В некоторых случаях можно использовать полифил @oddbird/css-anchor-positioning, который добавляет поддержку некоторых вариантов позиционирования привязки. Он не поддерживает неявные привязки, поэтому вы ДОЛЖНЫ добавить названия привязок в триггер. Кроме того, полифил не поддерживает position-area для всплывающих окон, поэтому вы ДОЛЖНЫ использовать anchor() для нужных вставок.

<!-- MANDATORY: Conditionally install the anchor positioning polyfill -->
<script type="module">
  if (!("anchorName" in document.documentElement.style)) {
    await import("https://unpkg.com/@oddbird/css-anchor-positioning");
  }
</script>
.panel {
  /* Mandatory: use explicit anchor name */
  position-anchor: --kebab-anchor;
  /* Mandatory: use insets rather than position-area for positioning */
  bottom: auto;
  right: auto;
  top: anchor(bottom);
  left: anchor(left);
  margin: 0;
}