Deklarative Dialog- und Popover-Steuerung

Mit der Invoker Commands API können Sie die Sichtbarkeit von <dialog>- und [popover]-Elementen direkt über HTML-Schaltflächen ein- und ausschalten. Benutzerdefinierte JavaScript-Ereignis-Listener sind dann nicht mehr erforderlich.

Wenn Sie die Attribute commandfor (Ziel-ID) und command (Aktion) auf ein <button> anwenden, verarbeitet der Browser automatisch Änderungen des Status „Geöffnet/Geschlossen“, die Fokusverwaltung und Bindungen für die Barrierefreiheit im Internet (z. B. aria-expanded). Dieser deklarative Ansatz wird empfohlen, da er fehleranfälligen Boilerplate-Code vermeidet, dafür sorgt, dass Interaktionen sofort nach dem HTML-Parsing funktionieren, und eine robuste, nativ barrierefreie Nutzererfahrung bietet.

Deklarative Popovers implementieren

Popovers können mit einer einzigen Schaltfläche geöffnet und geschlossen werden.

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

Wenn du das Öffnen und Schließen mit separaten Tasten steuern möchtest, kannst du die Befehle show-popover und hide-popover verwenden.

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

Deklarative modale Dialogfelder implementieren

Im Gegensatz zu Popovers werden für modale Dialogfelder in der Regel separate Buttons zum Öffnen und Schließen verwendet. Verwenden Sie den Befehl show-modal, wenn Sie ein Dialogfeld als modales Fenster öffnen möchten.

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

Fallback-Strategien

Da Aufrufbefehle und Popovers noch nicht allgemein unterstützt werden, MÜSSEN Sie Polyfills als Fallbacks für ältere Browser verwenden.

Polyfilling von Aufruferbefehlen

OBLIGATORISCH: Prüfen Sie, ob die Funktion unterstützt wird, indem Sie nach dem Attribut commandForElement im Prototyp HTMLButtonElement suchen. Prüfen Sie NICHT das Fenster- oder Dokumentobjekt. Sie MÜSSEN das Polyfill nur dann dynamisch importieren, wenn die native Funktion fehlt. Laden Sie das Polyfill NICHT bedingungslos. Obligatorisch: Lauschen Sie direkt auf dem Zielelement auf das „command“-Ereignis, da das native „command“-Ereignis nicht weitergegeben wird.

Option 1: Bundler verwenden Installieren Sie das Polyfill über npm (npm install invokers-polyfill). Dieser Ansatz eignet sich für Projekte, die einen Bundler (wie Vite oder Webpack) oder Importzuordnungen verwenden. Verwenden Sie für alle anderen Konfigurationen die CDN-Option unten.

// 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');
}

Option 2: CDN verwenden Bei Projekten ohne Bundler können Sie das Polyfill dynamisch direkt aus einem CDN in ein <script type="module">-Element importieren.

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

Einschränkungen des Invokers-Polyfills OBLIGATORISCH: Dieses Polyfill verarbeitet die ARIA-Zustände (z.B. aria-expanded) der Befehlsschaltfläche nicht so wie native Browser. Es wird dringend empfohlen, diese Zustände selbst zu verarbeiten, damit Ihre Website vollständig barrierefrei ist.

Wenn die Invoker Commands API nicht unterstützt wird, wird das Ereignis command nicht ausgelöst. Für vollständigen Support in allen modernen Browsern wird empfohlen, das invokers-polyfill von https://github.com/keithamus/invokers-polyfill über npm install oder CDN zu verwenden.

Dieses Polyfill unterstützt benutzerdefinierte Aktionen (beginnend mit --) vollständig und löst das command-Ereignis genau wie die native API aus.

Dynamischer Import (Leistungsoptimierung)

Für eine optimale Leistung sollten Sie das Polyfill nur laden, wenn der Browser die API nicht nativ unterstützt. Das spart Bandbreite und verkürzt die Ausführungszeit von Skripts für Nutzer mit modernen Browsern.

HINWEIS: Dieses Polyfill verwaltet keine ARIA-Zustände (wie aria-pressed oder aria-expanded) für benutzerdefinierte Befehle. Sie müssen diese Status in Ihrem Ereignis-Listener manuell synchronisieren, damit Ihre Website barrierefrei ist.

// 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);
  }
});

Manuelles Fallback (traditionelles Muster)

Wenn Sie kein Polyfill verwenden möchten, können Sie eine Kombination aus Ereignisdelegation zum Senden von Ereignissen und einer Befehlsregistrierung zum Verarbeiten der Aktionen verwenden. Dies ist ein gängiges Architekturmuster in der herkömmlichen JavaScript-Entwicklung, das nach wie vor hocheffizient und skalierbar ist.

// 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);
  }
 });

Fallbacks und Browserunterstützung für Popover

Die Popover API ist größtenteils eine progressive Verbesserung, aber ihre definierenden Verhaltensweisen – die Hochstufung auf die oberste Ebene, das Schließen durch Klicken außerhalb des Popovers und der Aufruf von popovertarget – haben kein CSS-Äquivalent. Ältere Browser benötigen ein Polyfill oder ein manuelles Fallback, wenn Sie kein Polyfill bereitstellen möchten.

Polyfill: Damit das Attribut popover in älteren Browsern unterstützt wird, lade @oddbird/popover-polyfill bedingt. OBLIGATORISCH: Prüfe, ob die Funktion unterstützt wird, indem du nach der Property popover in HTMLElement.prototype suchst, und lade das Polyfill nur, wenn die native Unterstützung fehlt. Lade es NICHT bedingungslos.

Mit einem Bundler oder einer Importzuordnung:

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

Ohne Bundler importieren Sie aus einem CDN in einem <script type="module">:

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

Einschränkung beim Styling: Das Polyfill kann die echte Pseudoklasse :popover-open nicht definieren, daher wird stattdessen die Klasse .\:popover-open angewendet. OBLIGATORISCH: Kombinieren Sie die beiden mit :is() oder :where(). Andernfalls verwerfen Browser, die :popover-open nicht unterstützen, die gesamte Regel:

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

Alternativ können Sie für einen Legacy-Fallback ohne Polyfill position: fixed verwenden und die Koordinaten manuell über getBoundingClientRect() berechnen oder die Standardpositionierung mit inset: auto verwenden, wenn das für den Anwendungsfall akzeptabel ist.