Benutzerdefinierte Button-Aktionen

Webfunktionen: Invoker-Befehle

Mit der Invoker Commands API können Schaltflächen mithilfe von HTML-Attributen deklarativ Aktionen für Zielelemente auslösen. Dadurch sind weniger manuelle Event-Listener erforderlich und die Benutzeroberfläche wird von Implementierungsdetails entkoppelt. Für benutzerdefinierte, anwendungsspezifische Aktionen können Sie eigene Befehlsnamen definieren. Benutzerdefinierten Befehlen muss ein doppelter Bindestrich (--) vorangestellt werden, um Konflikte mit zukünftigen integrierten Browserbefehlen zu vermeiden.

Implementierungsschritte

  1. Zielelement definieren: Identifizieren Sie das Element, das auf die Aktion reagieren soll. Wenn es keine eindeutige id hat, fügen Sie eine hinzu.
  2. Aufruf-Button konfigurieren: Verwenden Sie das Attribut commandfor, um auf die id des Ziels zu verweisen, und das Attribut command, um den Namen des benutzerdefinierten Befehls anzugeben (mit dem Präfix --).
  3. Befehlsereignis verarbeiten: Hängen Sie einen command-Ereignis-Listener direkt an das Zielelement an. Das Ereignisobjekt enthält die Attribute command und target (bezieht sich auf das durch commandfor identifizierte Element).
  4. ARIA-Zustände verarbeiten: Benutzerdefinierte Befehle haben keine inhärente Semantik. Sie müssen Zustände wie aria-pressed oder aria-expanded verarbeiten.

Beispiel: Benutzerdefinierte Animationssteuerelemente

<!-- The target element that will respond to custom commands -->
<div id="action-target" class="target">
  Action Target
</div>

<!-- Buttons declaratively linked to the target element -->
<!-- Each button sends a unique custom command starting with '--' -->
<button commandfor="action-target" command="--spin">
  Spin
</button>

<button commandfor="action-target" command="--grow">
  Grow
</button>

<button commandfor="action-target" command="--reset">
  Reset All
</button>

<script>
  // 1. **Optional:** Define a registry of requested actions for cleaner logic
const commandRegistry = {
  '--spin': (target, source) => {
    const isSpun = target.classList.toggle('is-spun');
    // Set ARIA states, as custom commands have no inherent semantics.
    source?.setAttribute('aria-pressed', isSpun);
  },
  '--grow': (target, source) => {
    const isGrown = target.classList.toggle('is-grown');
    source?.setAttribute('aria-pressed', isGrown);
  },
  '--reset': (target) => {
    target.classList.remove('is-spun', 'is-grown');
    // Reset all associated buttons' ARIA states
    document.querySelectorAll(`button[commandfor="${target.id}"]`).forEach(btn => {
      btn.setAttribute('aria-pressed', 'false');
    });
  },
};

  // 2. **Mandatory:** Listen for the 'command' event directly on the target element
  // (This is necessary because the native 'command' event does not bubble)
  document.getElementById('action-target').addEventListener('command', (event) => {
    const command = event.command;
    const target = event.target;
    const source = event.source; // event.source refers to the triggering button
    const action = commandRegistry[command];

    if (action) {
      action(target, source);
    }
  });
</script>

Schlüsseleinschränkungen

  • Präfix für benutzerdefinierte Befehle: OBLIGATORISCH: Alle Namen benutzerdefinierter Befehle müssen mit -- beginnen (z.B. command="--my-action").
  • Targeting: Das Attribut commandfor muss mit dem id eines Elements im selben Dokumentbaum übereinstimmen.
  • Kein Bubbling: Das command-Ereignis wird nicht weitergegeben. Wenn es mehrere mögliche Ziele gibt, fügen Sie dem Event-Handler { capture: true } hinzu und überwachen Sie ein übergeordnetes Element.
  • Shadow-Roots: Wenn sich das Ziel in einem Shadow-Root befinden könnte, verwenden Sie event.composedPath()[0] anstelle von event.target.
  • Barrierefreiheit im Internet: Benutzerdefinierte Befehle haben keine inhärente Semantik. Sie müssen alle Status explizit anwenden.

Berücksichtigung der Barrierefreiheit

  • Wenn die Schaltfläche zusätzliche Inhalte anzeigt, verwenden Sie das Attribut aria-expanded, das standardmäßig auf aria-expanded="false" und auf aria-expanded="true" gesetzt ist, wenn der begleitende Videocontent angezeigt wird.
  • Wenn die Schaltfläche selbst eine Interaktion ermöglicht (z. B. Ein/Aus-Schaltflächen, mit denen die Wiedergabe gestartet und pausiert werden kann, oder Lautstärketasten, mit denen die Stummschaltung aktiviert und deaktiviert werden kann), verwenden Sie das Attribut aria-pressed, das bei Inaktivität auf aria-pressed="false" und bei Aktivität auf aria-pressed="true" gesetzt ist.
  • Wenn die Schaltfläche ein Element mit der Rolle menu, listbox, tree, grid oder dialog öffnet, muss das Steuerelement ein aria-haspopup-Attribut haben, dessen Wert auf die Rolle des Elements festgelegt ist, das es öffnet.
  • Wenn die Schaltfläche bei der Aktivierung deaktiviert wird, verwende statt disabled die Option aria-disabled="true", damit der Fokus nicht verloren geht.

Fallback-Strategien

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