Deklaratywne okno i element sterujący wyskakującego okienka

Użyj interfejsu Invoker Commands API, aby przełączać widoczność elementów <dialog> i [popover] bezpośrednio za pomocą przycisków HTML, co eliminuje potrzebę stosowania niestandardowych detektorów zdarzeń JavaScriptu.

Po zastosowaniu atrybutów commandfor (identyfikator docelowy) i command (działanie) do elementu <button> przeglądarka automatycznie obsługuje zmiany stanu otwarcia/zamknięcia, zarządzanie fokusem i powiązania ułatwień dostępu (takie jak aria-expanded). To podejście deklaratywne jest zalecane, ponieważ eliminuje niestabilny powtarzalny kod, zapewnia natychmiastowe działanie interakcji po przeanalizowaniu kodu HTML i gwarantuje solidne, natywnie dostępne wrażenia użytkownika.

Implementowanie deklaratywnych wyskakujących okienek

Wyskakujące okienka można otwierać i zamykać za pomocą jednego przycisku.

<!-- MANDATORY: The commandfor attribute links the invoker to the ID of the target element so the browser knows what to control. -->
<!-- MANDATORY: The command attribute specifies the action to perform. Use 'toggle-popover' to handle both open and close states automatically. -->
<button commandfor="my-popover" command="toggle-popover">
  Toggle Popover
</button>

<!-- MANDATORY: The target element must have the popover attribute to be controlled as a popover. -->
<div id="my-popover" popover>
  <p>Popover content goes here.</p>
</div>

Jeśli chcesz sterować otwieraniem i zamykaniem za pomocą oddzielnych przycisków, możesz użyć poleceń show-popover i hide-popover.

<!-- MANDATORY: Use 'show-popover' to explicitly open the popover. It will not close the popover if clicked again. -->
<button commandfor="my-explicit-popover" command="show-popover">
  Show Popover
</button>

<div id="my-explicit-popover" popover="manual">
  <p>This popover is explicitly opened and closed by separate buttons.</p>

  <!-- MANDATORY: Use 'hide-popover' to explicitly close the targeted popover. -->
  <button commandfor="my-explicit-popover" command="hide-popover">
    Hide Popover
  </button>
</div>

Wdrażanie deklaratywnych modalnych okien dialogowych

W odróżnieniu od wyskakujących okienek okna modalne zwykle mają osobne przyciski do otwierania i zamykania. Używaj polecenia show-modal, gdy chcesz otworzyć okno jako modalne.

<!-- MANDATORY: Use command="show-modal" to trigger the dialog as a modal, trapping focus and preventing interaction with the rest of the page. -->
<!-- MANDATORY: The commandfor attribute connects this button to the dialog ID. -->
<button commandfor="confirm-dialog" command="show-modal">
  Open Confirmation
</button>

<dialog id="confirm-dialog">
  <p>Are you sure you want to proceed?</p>

  <!-- MANDATORY: Use command="close" to dismiss the dialog safely. -->
  <button commandfor="confirm-dialog" command="close">
    Cancel
  </button>
</dialog>

Strategie zastępcze

Polecenia wywołujące i elementy wyskakujące nie są jeszcze powszechnie obsługiwane, dlatego w przypadku starszych przeglądarek MUSISZ używać polyfilli jako rozwiązań zastępczych.

Polecenia wywołujące polyfill

OBOWIĄZKOWO: wykryj obsługę funkcji, sprawdzając właściwość commandForElement w prototypie HTMLButtonElement. NIE sprawdzaj obiektu okna ani dokumentu. MUSISZ dynamicznie importować polyfill tylko wtedy, gdy brakuje funkcji natywnej. NIE wczytuj bezwarunkowo kodu polyfill. Obowiązkowe: nasłuchuj zdarzenia „command” bezpośrednio w elemencie docelowym, ponieważ natywne zdarzenie „command” nie jest przekazywane w górę.

Opcja 1. Użycie bundlera Zainstaluj polyfill za pomocą npm (npm install invokers-polyfill). To podejście jest przeznaczone dla projektów, które używają bundlera (np. Vite lub Webpack) lub map importu. W przypadku wszystkich innych konfiguracji użyj opcji CDN poniżej.

// MANDATORY: Feature detect 'commandForElement' on HTMLButtonElement.prototype.
// Conditionally load the invokers-polyfill only in browsers lacking native support.
if (!('commandForElement' in HTMLButtonElement.prototype)) {
  import('invokers-polyfill');
}

Opcja 2. Korzystanie z sieci CDN W przypadku projektów bez bundlera dynamicznie importuj polyfill bezpośrednio z sieci CDN w elemencie <script type="module">.

<script type="module">
  // MANDATORY: Feature detect 'commandForElement' on HTMLButtonElement.prototype.
  // Conditionally load the invokers-polyfill from a CDN only in browsers lacking native support.
  if (!('commandForElement' in HTMLButtonElement.prototype)) {
    import('https://esm.run/invokers-polyfill');
  }
</script>

Ograniczenia polyfilla wywołującego OBOWIĄZKOWE: ten polyfill nie obsługuje stanów ARIA (np. aria-expanded) przycisku polecenia w taki sposób, jak robią to natywne przeglądarki. Zdecydowanie zalecamy samodzielne obsługiwanie tych stanów, aby zapewnić pełną dostępność witryny.

Jeśli interfejs Invoker Commands API nie jest obsługiwany, zdarzenie command nie zostanie wywołane. Aby zapewnić pełną obsługę we wszystkich nowoczesnych przeglądarkach, zalecamy użycie invokers-polyfill z https://github.com/keithamus/invokers-polyfill przez npm install lub CDN.

Ten polyfill w pełni obsługuje działania niestandardowe (począwszy od --) i wysyła zdarzenie command dokładnie tak samo jak natywny interfejs API.

Import dynamiczny (optymalizacja skuteczności)

Aby uzyskać najlepszą wydajność, wczytuj polyfill tylko wtedy, gdy przeglądarka nie obsługuje interfejsu API natywnie. Pozwala to zaoszczędzić przepustowość i skrócić czas wykonywania skryptów w przypadku użytkowników nowoczesnych przeglądarek.

UWAGA: ten polyfill nie zarządza stanami ARIA (takimi jak aria-pressed lub aria-expanded) w przypadku poleceń niestandardowych. Aby zapewnić dostępność witryny, musisz ręcznie zsynchronizować te stany w detektorze zdarzeń.

// 1. Conditionally load the polyfill
const hasNativeSupport = 'commandForElement' in HTMLButtonElement.prototype;

if (!hasNativeSupport) {
  // Wrap in an async IIFE to avoid top-level await issues in older browsers
  (async () => {
    try {
      await import('https://esm.run/invokers-polyfill');
    } catch (err) {
      console.error('Error loading fallback:', err);
    }
  })();
}

// 2. Manually manage ARIA states in your listener
document.getElementById('action-target').addEventListener('command', (event) => {
  const command = event.command;
  const target = event.target;
  const source = event.source; // The button that triggered the command

  if (command === '--spin') {
    const isSpun = target.classList.toggle('is-spun');
    
    // Polyfill tip: Manually update ARIA to match the new state
    source?.setAttribute('aria-pressed', isSpun);
  }
});

Ręczne przywracanie (wzorzec tradycyjny)

Jeśli nie chcesz używać kodu polyfill, możesz użyć kombinacji delegowania zdarzeń do wysyłania zdarzeń i rejestru poleceń do obsługi działań. Jest to powszechny wzorzec architektury w tradycyjnym programowaniu w języku JavaScript, który pozostaje bardzo wydajny i skalowalny.

// 1. **Optional:** Define a registry of requested actions for cleaner logic
const commandRegistry = {
  '--spin': (target) => target.classList.toggle('is-spun'),
  '--grow': (target) => target.classList.toggle('is-grown'),
  '--reset': (target) => target.classList.remove('is-spun', 'is-grown'),
};

// 2. If CommandEvent doesn't exist, we assume no native support and provide the fallback
if (!globalThis.CommandEvent) {
  globalThis.CommandEvent = class CommandEvent extends Event {
    constructor(type, { source, command, ...options } = {}) {
      super(type, options);
      this.source = source;
      this.command = command;
    }
  }
}

// 3. The fallback: Dispatch events manually if native support is missing
  document.addEventListener('click', (event) => {
    const button = event.composedPath().find((el) => el.matches?.("button[commandfor]"));
    if (!button) return;

    const target = document.getElementById(button.getAttribute('commandfor'));
    const command = button.getAttribute('command');

    if (target && command) {
      target.dispatchEvent(new CommandEvent('command', { 
        command, 
        source: button,
      }));
    }
  });

// 4. **Mandatory:** Register the unified listener directly on the target element
document.getElementById('action-target').addEventListener('command', (event) => {
  const command = event.command;
  const target = event.target;
  const action = commandRegistry[command];

  if (action) {
    action(target);
  }
 });

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.