Okno dialogowe

Przegląd

Używaj natywnego elementu <dialog> w przypadku modalnych i niemodalnych okien dialogowych. Zapewnia semantykę ułatwień dostępu, obsługę zaznaczenia, renderowanie warstwy górnej i obsługę Esc, które niestandardowe nakładki muszą odtwarzać ręcznie.

Wybierz typ nakładki

Wybierz pierwszą pasującą regułę. Jeśli nie masz pewności, użyj reguły 1.

Reguła 1. Nakładka przerywa użytkownikowi i blokuje stronę (domyślnie)

Używaj ich w przypadku potwierdzeń, formularzy, logowania, działań destrukcyjnych oraz wszelkich żądań „modalnych”, „okien dialogowych”, „lightboxów” lub „wyskakujących okienek”, które nie zawierają innych informacji.

Użyj elementu <dialog> otwartego jako modal, deklaratywnie z command="show-modal" (patrz Deklaratywne okno i kontrola wyskakujących okienek) lub ze skryptu z showModal().

<button type="button" command="show-modal" commandfor="confirm">Delete</button>

<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Delete this item?</h2>
  <form method="dialog">
    <button value="cancel" autofocus>Cancel</button>
    <button value="delete">Delete</button>
  </form>
</dialog>

Przeglądarka zapewnia: stronę obojętną, utrzymanie zaznaczenia, warstwę górną, ::backdrop, Esc i odrzucenie gestem wstecz oraz powrót zaznaczenia do elementu wywołującego.

  • NIE dodawaj role="dialog", aria-modal, pułapki fokusu, przełączania inert ani modułu obsługi Esc.
  • NIE otwieraj go za pomocą atrybutu show() ani open.
  • ZATRZYMAJ przewijanie strony za pomocą poniższego kodu CSS. Więcej informacji znajdziesz w artykule Zapobieganie przewijaniu strony pod oknem modalnym.
  • NIE blokuj przewijania za pomocą overflow: hidden ani position: fixed w html lub body.
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
  overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
  overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
  overscroll-behavior: contain;
}

Nie dotyczy menu ani etykietek – patrz reguła 3.

Reguła 2. Użytkownik kontynuuje pracę na stronie, gdy panel jest otwarty

Używaj w przypadku pasków wyszukiwania, inspektorów, palet i paneli tworzenia, które semantycznie nadal są oknami.

Użyj <dialog popover> w przypadku panelu najwyższej warstwy z możliwością zamknięcia wyskakującego okienka (auto umożliwia zamknięcie przez kliknięcie poza okienkiem, manual nie). Używaj dialog.show() tylko wtedy, gdy panel nie może znajdować się w górnej warstwie.

<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
  <h2 id="find-title">Find</h2>
  <input type="search" aria-label="Search">
  <button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>

Strona pozostaje interaktywna i nie jest nieaktywna.

  • DO include a visible close button.
  • NIE używaj tagu showModal(), który blokuje stronę.

Nie do blokowania potwierdzeń: patrz reguła 1.

Reguła 3. Nakładka nie jest oknem dialogowym

Używaj w menu, menu, etykietkach, kartach informacyjnych i powiadomieniach.

Użyj elementu semantycznego z atrybutem popover, a nie <dialog>.

<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
  <li><a href="/rename">Rename</a></li>
</ul>

Wybierz sposób wyłączania

Domyślna wartość zależy od sposobu otwarcia okna. Jeśli parametr closedby jest nieobecny lub nieprawidłowy, showModal() działa jak closerequest, a każdy inny sposób otwierania działa jak none.

  • Tylko klawisz Esc i gest wstecz (domyślny tryb modalny): nie ustawiaj niczego lub ustaw closedby="closerequest".
  • Kliknięcie poza obszarem powoduje zamknięcie: użyj closedby="any".
  • Tylko deweloper może zamknąć okno (np. formularz z niezapisanymi zmianami): użyj closedby="none" i dodaj ścieżkę potwierdzenia. Zobacz sekcję „Zamykanie” poniżej.

Wytyczne

Nazwa na potrzeby ułatwień dostępu i początkowy fokus

  • DO name every dialog with aria-labelledby pointing at its heading, or aria-label when there is no visible title.
  • UMIEŚĆ autofocus na głównym elemencie sterującym lub w pierwszym polu.
  • UMIEŚĆ autofocus na bezpiecznym działaniu (np. Anuluj lub Zachowaj) w potwierdzeniu destrukcyjnym.
  • NIE pisz kodu, który po zamknięciu przywraca zaznaczenie. Przeglądarka zwraca go do elementu wywołującego.

Otwieranie

  • ZALECENIE: otwieraj deklaratywnie za pomocą command="show-modal" i commandfor w <button>. Więcej informacji znajdziesz w artykule Declarative Dialog and Popover Control (w języku angielskim).
  • UŻYWAJ dialog.showModal() podczas otwierania ze skryptu.

Zakończenie

  • ZAMYKAJ za pomocą przycisków <form method="dialog"> lub command="close".
  • WYBIERZ closedby celowo: any (lekkie odrzucenie i Esc), closerequest (tylko Esc, domyślny tryb modalny) lub none (tylko zamknięcie kontrolowane przez dewelopera).
  • UŻYWAJ command="request-close" lub requestClose() w przypadku przycisków Anuluj, które muszą uwzględniać logikę niezapisanych zmian. Najpierw uruchamiają cancel i ignorują closedby.
  • TRAKTUJ closedby jako progresywne ulepszanie. Esc i gest wstecz na urządzeniach mobilnych już zamykają okna bez tego przycisku.

Podłączanie do zamknięcia

  • DO read dialog.returnValue in a close listener to learn which button was used, then reset it with dialog.returnValue = ''. Esc i inne żądania zamknięcia nie ustawiają żadnej wartości, a przeglądarki różnią się pod względem tego, czy czyszczą poprzednią wartość, więc bez resetowania może dojść do wycieku nieaktualnej wartości. Traktuj '' jako „odrzucono bez wyboru”.
  • UŻYWAJ cancel do przechwytywania Esc i request-close, ale nie polegaj na preventDefault() w przypadku wielokrotnego naciskania Esc bez aktywacji użytkownika.
  • ZALECANE: używaj closedby="none" z wyraźnym krokiem potwierdzenia w przypadku formularzy z niezapisanymi zmianami.
  • POINFORMUJ użytkownika, dlaczego okno pozostało otwarte, gdy uniemożliwiono wykonanie działania cancel: wyświetl widoczny komunikat i odwołaj się do niego za pomocą atrybutu aria-describedby z elementu sterującego, który jest zaznaczony (np. Odrzuć), aby czytnik ekranu odczytał go, gdy zaznaczenie zostanie przeniesione. Bez niego przeniesienie zaznaczenia na nowy przycisk powoduje odczytanie tylko nazwy tego przycisku.
  • NIE pozwól, aby potwierdzenie przesunęło elementy sterujące pod wskaźnik. Zarezerwuj miejsce na komunikat (ukryj go za pomocą visibility, a nie display lub hidden) i dodaj przycisk destrukcyjny w miejscu, w którym istniejące przyciski nie będą się przesuwać (np. jako pierwszy w wierszu wyrównanym do prawej), aby drugie kliknięcie w tym samym miejscu nie spowodowało odrzucenia zmian wprowadzonych przez użytkownika.

Okna niemodalne

  • UŻYWAJ <dialog popover> (auto lub manual) w przypadku panelu górnej warstwy i dialog.show(), gdy nie chcesz górnej warstwy.
  • DO include a visible close button in every non-modal dialog.

Tło i styl

Ułatwienia dostępu

  • NIE dodawaj do natywnego okna modalnego elementów role="dialog", aria-modal, ręcznej pułapki ostrości ani przełączania inert.
  • UŻYWAJ natywnego elementu <button> z nazwą na potrzeby ułatwień dostępu do elementów sterujących zamykaniem.
  • UŻYWAJ jednego spójnego poziomu nagłówka w tytułach okien.

Strategie zastępcze

closedby (dialog-closedby)

Bez closedby okno nadal zamyka się po naciśnięciu Esc i wykonaniu gestu wstecz. Nie działa tylko kliknięcie poza oknem, więc jest to łagodna degradacja. closedby="none" jest ignorowany w ten sam sposób: Esc uruchamia cancel, a następnie zamyka okno. Zabezpieczenie przed niezapisanymi zmianami powinno być obsługiwane przez cancel, a nie tylko przez closedby="none", aby chronić również tam, gdzie brakuje closedby. Jeśli Twój cel Baseline wymaga lekkiego odrzucenia, dodaj ten element tam, gdzie brakuje closedby:

if (!('closedBy' in HTMLDialogElement.prototype)) {
  // Test the pointer position, not event.target: the <dialog> is also the target for its own
  // padding and the gaps between its children, so a target check would close on those clicks
  const outside = (e, dialog) => {
    const r = dialog.getBoundingClientRect();
    return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
  };
  for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
    let pressedOutside = false;
    dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
    // Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
    dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
  }
}

overscroll-behavior

Traktuj izolację przewijania jako progresywne ulepszanie. Nie wykrywaj funkcji: @supports zgłasza właściwość jako obsługiwaną nawet wtedy, gdy kontenery bez możliwości przewijania ją ignorują. Jeśli nie jest to wymuszane, okno nadal zawiera pasek przewijania, gdy jego zawartość jest zbyt długa, a element showModal() nadal sprawia, że strona jest nieaktywna. Nie używaj overflow: hidden ani position: fixed w html lub body, ponieważ powoduje to usunięcie głównego paska przewijania i przesunięcie układu.

Polecenia wywołujące (command/commandfor)

Jeśli Twój cel podstawowy nie obejmuje poleceń wywołujących, zachowaj te same znaczniki i warunkowo wczytuj polyfill (te same wrażenia, co w przypadku progresywnego ulepszania). Wykrywanie w interfejsie i nigdy nie wczytywanie bezwarunkowo:

// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
  import('invokers-polyfill'); // npm install invokers-polyfill
}

Tylko reguły 2 i 3 używają wyskakującego okienka.

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.