Menu rozwijane dostosowane do widocznego obszaru

Odsłonięty panel działań lub grupa przycisków w wyskakującym okienku to przydatny wzorzec, który umożliwia użytkownikom dostęp do dodatkowych funkcji, zajmując przy tym minimalną ilość miejsca. Ten wzorzec nakładki wiąże się ze złożonością układu, ponieważ panel musi pozostać powiązany z elementem aktywującym, a jednocześnie dostosowywać się do ograniczeń widocznego obszaru. Tradycyjnie wymagało to złożonych bibliotek JavaScript (takich jak Popper.js lub Floating UI) do obliczania pozycji i obsługi kolizji.

Pozycjonowanie zakotwiczone w CSS zapewnia deklaratywny, zoptymalizowany pod kątem wydajności sposób obsługi tych relacji w całości w CSS, co pozwala przeglądarkom na natywne zarządzanie pozycjonowaniem i logiką przepełnienia.

1. Definiowanie relacji między przyciskiem a panelem

Pierwszym krokiem jest utworzenie przycisku aktywującego, który otwiera kontener nakładki za pomocą interfejsu Popover API.

OBOWIĄZKOWE rozróżnienie ułatwień dostępu: ten wzorzec wyraźnie modeluje grupę przycisków wyświetlaną w okienku wyskakującym, a nie prawdziwe menu ARIA. Nie stosuj role="menu" ani role="menuitem", chyba że w pełni wdrożysz odpowiednią umowę dotyczącą nawigacji za pomocą klawiatury (np. obsługę nawigacji przestrzennej między elementami za pomocą klawiszy strzałek). Z tego samego powodu nie dodawaj do aktywatora elementu aria-haspopup – jego wartość (menu, listbox, tree, grid lub starsza wersja true) jest obietnicą, że cel udostępnia pasującą rolę, czego ten wzorzec nie robi.

<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>

Spowoduje to utworzenie niejawnego powiązania zakotwiczenia między przyciskiem a panelem, dzięki czemu panel będzie można umieścić względem przycisku w sposób natywny bez konieczności stosowania atrybutu anchor-name na przycisku lub position-anchor na panelu.

2. Pozycjonowanie za pomocą position-area

Zamiast ręcznych przesunięć top/left użyj position-area, aby umieścić element docelowy na siatce 3 x 3 względem elementu zakotwiczonego.

.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;
}

Preferuj słowa kluczowe logiczne (span-inline-end, block-start) zamiast fizycznych (left, top), aby automatycznie obsługiwać języki pisane od prawej do lewej i różne tryby pisania.

OBOWIĄZKOWO: nie mieszaj słów kluczowych fizycznych i logicznych w position-area.

3. Wdrażanie odporności na błędy (kreacje zastępcze)

Aby zapobiec obcięciu panelu na krawędzi ekranu, zdefiniuj „taktyki”, które przeglądarka powinna wypróbować, jeśli domyślna pozycja wykracza poza ekran.

.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;
}

Jeśli menu zawiera strzałkę kierunkową lub obramowanie, których styl musi się zmieniać w zależności od wybranej pozycji zastępczej, zapoznaj się z sekcją Etykietki uwzględniające pozycję. Informacje o deklaratywnym wywoływaniu okien i elementów popover bez JavaScriptu znajdziesz w artykule Deklaratywne sterowanie oknami i elementami popover.

Strategie zastępcze

Kreacje zastępcze i obsługa wyskakujących okienek w przeglądarkach

Interfejs Popover API to głównie progresywne ulepszanie, ale jego definiujące zachowania – promowanie warstwy górnej, lekkie odrzucanie i wywoływanie popovertarget – nie mają odpowiednika tylko w CSS. Starsze przeglądarki wymagają polyfilla lub ręcznego rozwiązania zastępczego, jeśli nie chcesz go dostarczać.

Polyfill: aby obsługiwać atrybut popover w starszych przeglądarkach, warunkowo wczytuj @oddbird/popover-polyfill. OBOWIĄZKOWO: wykryj funkcję, sprawdzając właściwość popover w HTMLElement.prototype, i wczytaj polyfill tylko wtedy, gdy brakuje obsługi natywnej – NIE wczytuj go bezwarunkowo.

Za pomocą bundlera lub mapy importu:

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

Bez bundlera, importuj z sieci CDN w tagu <script type="module">:

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

Uwaga dotycząca stylów: polyfill nie może zdefiniować prawdziwej pseudoklasy :popover-open, więc zamiast tego stosuje klasę .\:popover-open. OBOWIĄZKOWO: połącz te 2 właściwości za pomocą :is() lub :where(), w przeciwnym razie przeglądarki, które nie obsługują :popover-open, odrzucą całą regułę:

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

Możesz też użyć position: fixed i ręcznie obliczyć współrzędne za pomocą getBoundingClientRect() lub polegać na domyślnym pozycjonowaniu za pomocą inset: auto, jeśli jest to akceptowalne w danym przypadku użycia.

Aby zapewnić obsługę przeglądarek bez pozycjonowania zakotwiczonego, musisz ustawić odpowiednią pozycję. Domyślnie wyskakujące okienka są wyśrodkowane na ekranie, co może być odpowiednie w Twoim przypadku.

W niektórych przypadkach możesz użyć kodu polyfill @oddbird/css-anchor-positioning, który dodaje obsługę niektórych przypadków użycia pozycjonowania zakotwiczonego. Nie obsługuje on kotwic niejawnych, więc MUSISZ dodać nazwy kotwic do aktywatora. Dodatkowo position-area nie jest obsługiwany w przypadku wyskakujących okienek przez polyfill, więc MUSISZ używać anchor() w przypadku odpowiednich wcięć.

<!-- 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;
}