Einführung von „command“ und „commandfor“

Veröffentlicht: 7. März 2025

Schaltflächen sind für dynamische Webanwendungen unerlässlich. Über Schaltflächen können Sie Menüs öffnen, Aktionen umschalten und Formulare senden. Sie bilden die Grundlage für Interaktivität im Web. Das Erstellen einfacher und barrierefreier Schaltflächen kann zu einigen überraschenden Herausforderungen führen. Entwickler, die Micro-Frontends oder Komponentensysteme erstellen, stoßen manchmal auf Lösungen, die komplexer sind als nötig. Frameworks sind zwar hilfreich, aber das Web kann hier noch mehr.

Chrome 135 führt neue Funktionen für deklaratives Verhalten mit den neuen Attributen command und commandfor ein. Damit werden die Attribute popovertargetaction und popovertarget verbessert und ersetzt. Diese neuen Attribute können Schaltflächen hinzugefügt werden, damit der Browser einige grundlegende Probleme in Bezug auf Einfachheit und Barrierefreiheit beheben und gängige Funktionen bereitstellen kann.

Traditionelle Muster

Das Erstellen von Schaltflächenverhalten ohne Framework kann bei der Entwicklung von Produktionscode einige interessante Herausforderungen mit sich bringen. HTML bietet zwar onclick-Handler für Schaltflächen, diese sind aber aufgrund von CSP-Regeln (Content Security Policy) außerhalb von Demos oder Anleitungen oft nicht zulässig. Diese Ereignisse werden an Schaltflächenelemente gesendet. Schaltflächen werden jedoch in der Regel auf einer Seite platziert, um andere Elemente zu steuern. Dazu ist Code erforderlich, um zwei Elemente gleichzeitig zu steuern. Außerdem muss diese Interaktion für Nutzer mit Hilfstechnologien zugänglich sein. Das führt oft zu Code, der in etwa so aussieht:

<div class="menu-wrapper">
  <button class="menu-opener" aria-expanded="false">
    Open Menu
  </button>
  <div popover class="menu-content">
    <!-- ... -->
  </div>
</div>
<script type="module">
document.addEventListener('click', e => {  
  const button = e.target;
  if (button.matches('.menu-opener')) {
    const menu = button
      .closest('.menu-wrapper')
      .querySelector('.menu-content');
    if (menu) {
      button.setAttribute('aria-expanded', 'true');
      menu.showPopover();
      menu.addEventListener('toggle', e => {
        // reset back to aria-expanded=false on close
        if (e.newState == 'closed') {
          button.setAttribute('aria-expanded', 'false');
        }
      }, {once: true})
    }
  }
});
</script>

Dieser Ansatz kann etwas instabil sein. Frameworks sollen die Ergonomie verbessern. Ein gängiges Muster bei einem Framework wie React könnte darin bestehen, den Klick einer Zustandsänderung zuzuordnen:

function MyMenu({ children }) {
  const [isOpen, setIsOpen] = useState(false);
  const open = useCallback(() => setIsOpen(true), []);
  const handleToggle = useCallback((e) => {
      // popovers have light dismiss which influences our state
     setIsOpen(e.newState === 'open')
  }, []);
  const popoverRef = useRef(null);
  useEffect(() => {
    if (popoverRef.current) {
      if (isOpen) {
        popoverRef.current.showPopover();
      } else {
        popoverRef.current.hidePopover();
      }
    }
  }, [popoverRef, isOpen]);
  return (
    <>
      <button onClick={open} aria-expanded={isOpen}>
        Open Menu
      </button>
      <div popover onToggle={handleToggle} ref={popoverRef}>
        {children}
      </div>
    </>
  );
}

Viele andere Frameworks streben auch eine ähnliche Ergonomie an. In AlpineJS könnte das beispielsweise so aussehen:

<div x-data="{open: false}">
  <button @click="open = !open; $refs.popover.showPopover()" :aria-expanded="open">
    Open Menu
  </button>
  <div popover x-ref="popover" @toggle="open = $event.newState === 'open'">
    <!-- ... -->
  </div>
</div>

In Svelte könnte das so aussehen:

<script>
  let popover;
  let open = false;
  function togglePopover() {
    open ? popover.hidePopover() : popover.showPopover();
    open = !open;
  }
</script>
<button on:click={togglePopover} aria-expanded={open}>
  Open Menu
</button>
<div bind:this={popover} popover>
  <!-- ... -->
</div>

Einige Designsysteme oder Bibliotheken gehen noch einen Schritt weiter und bieten Wrapper für Schaltflächenelemente, die die Statusänderungen umfassen. Dadurch werden Zustandsänderungen hinter einer Triggerkomponente abstrahiert. Dafür wird ein wenig Flexibilität gegen eine verbesserte Ergonomie eingetauscht:

import {MenuTrigger, MenuContent} from 'my-design-system';
function MyMenu({children}) {
  return (
    <MenuTrigger>
      <button>Open Menu</button>
    </MenuTrigger>
    <MenuContent>{children}</MenuContent>
  );
}

Das Muster „command“ und „commandfor“

Mit den Attributen command und commandfor können Schaltflächen jetzt deklarativ Aktionen auf anderen Elementen ausführen. So wird die Ergonomie eines Frameworks verbessert, ohne die Flexibilität zu beeinträchtigen. Für die Schaltfläche commandfor wird eine ID verwendet, ähnlich wie beim Attribut for. Bei command werden dagegen vordefinierte Werte akzeptiert, was einen übertragbareren und intuitiveren Ansatz ermöglicht.

Beispiel: Eine Schaltfläche zum Öffnen eines Menüs mit „command“ und „commandfor“

Im folgenden HTML-Code werden deklarative Beziehungen zwischen der Schaltfläche und dem Menü eingerichtet, sodass der Browser die Logik und Barrierefreiheit für Sie übernimmt. Sie müssen aria-expanded nicht verwalten oder zusätzliches JavaScript hinzufügen.

<button commandfor="my-menu" command="show-popover">
Open Menu
</button>
<div popover id="my-menu">
  <!-- ... -->
</div>

command und commandfor mit popovertargetaction und popovertarget vergleichen

Wenn Sie popover bereits verwendet haben, sind Ihnen die Attribute popovertarget und popovertargetaction möglicherweise bereits bekannt. Sie funktionieren ähnlich wie commandfor und command, sind aber nur für Pop-ups spezifisch. Die Attribute command und commandfor ersetzen diese älteren Attribute vollständig. Die neuen Attribute unterstützen alle Funktionen der älteren Attribute und bieten darüber hinaus neue Möglichkeiten.

Integrierte Befehle

Das command-Attribut hat eine Reihe von integrierten Verhaltensweisen, die verschiedenen APIs für interaktive Elemente zugeordnet sind:

  • show-popover: Wird el.showPopover() zugeordnet.
  • hide-popover: Wird el.hidePopover() zugeordnet.
  • toggle-popover: Wird el.togglePopover() zugeordnet.
  • show-modal: Wird dialogEl.showModal() zugeordnet.
  • close: Wird dialogEl.close() zugeordnet.

Diese Befehle werden ihren JavaScript-Entsprechungen zugeordnet. Außerdem werden die Barrierefreiheit (z. B. durch die Bereitstellung der aria-details- und aria-expanded-entsprechenden Beziehungen) und die Fokusverwaltung optimiert.

Beispiel: Ein Bestätigungsdialogfeld mit command und commandfor

<button commandfor="confirm-dialog" command="show-modal">
  Delete Record
</button>
<dialog id="confirm-dialog">
  <header>
    <h1>Delete Record?</h1>
    <button commandfor="confirm-dialog" command="close" aria-label="Close" value="close">
      <img role="none" src="/close-icon.svg">
    </button>
  </header>
  <p>Are you sure? This action cannot be undone</p>
  <footer>
    <button commandfor="confirm-dialog" command="close" value="cancel">
      Cancel
    </button>
    <button commandfor="confirm-dialog" command="close" value="delete">
      Delete
    </button>
  </footer>
</dialog>

Wenn Sie auf die Schaltfläche Delete Record (Eintrag löschen) klicken, wird das Dialogfeld als modales Dialogfeld geöffnet. Wenn Sie auf die Schaltflächen Close (Schließen), Cancel (Abbrechen) oder Delete (Löschen) klicken, wird das Dialogfeld geschlossen und gleichzeitig ein "close"-Ereignis mit einer returnValue-Eigenschaft gesendet, die dem Wert der Schaltfläche entspricht. Dadurch ist nur noch ein einziger Ereignis-Listener im Dialogfeld erforderlich, um zu bestimmen, was als Nächstes zu tun ist:

dialog.addEventListener("close", (event) => {
  if (event.target.returnValue == "cancel") {
    console.log("cancel was clicked");
  } else if (event.target.returnValue == "close") {
    console.log("close was clicked");
  } else if (event.target.returnValue == "delete") {
    console.log("delete was clicked");
  }
});

Benutzerdefinierte Befehle

Zusätzlich zu den vordefinierten Befehlen können Sie benutzerdefinierte Befehle mit dem Präfix -- definieren. Benutzerdefinierte Befehle senden genau wie die vordefinierten Befehle ein "command"-Ereignis an das Zielelement. Andernfalls wird keine zusätzliche Logik ausgeführt, wie es bei den vordefinierten Werten der Fall ist. Das bietet Flexibilität beim Erstellen von Komponenten, die auf Schaltflächen auf unterschiedliche Weise reagieren können, ohne dass es notwendig ist, Wrapper-Komponenten bereitzustellen, das DOM nach dem Zielelement zu durchsuchen oder Schaltflächenklicks Zustandsänderungen zuzuordnen. So können Sie eine API in HTML für Ihre Komponenten bereitstellen:

<button commandfor="the-image" command="--rotate-landscape">
 Landscape
</button>
<button commandfor="the-image" command="--rotate-portrait">
 Portrait
</button>

<img id="the-image" src="photo.jpg">

<script type="module">
  const image = document.getElementById("the-image");
  image.addEventListener("command", (event) => {
   if ( event.command == "--rotate-landscape" ) {
    image.style.rotate = "-90deg"
   } else if ( event.command == "--rotate-portrait" ) {
    image.style.rotate = "0deg"
   }
  });
</script>

Befehle im ShadowDOM

Da das commandfor-Attribut eine ID annimmt, gibt es Einschränkungen beim Überschreiten des Shadow DOM. In diesen Fällen können Sie die JavaScript API verwenden, um die Eigenschaft .commandForElement festzulegen. Damit können Sie jedes Element über Schattenwurzeln hinweg festlegen:

<my-element>
  <template shadowrootmode=open>
    <button command="show-popover">Show popover</button>
    <slot></slot>
  </template>
  <div popover><!-- ... --></div>
</my-element>
<script>
customElements.define("my-element", class extends HTMLElement {
  connectedCallback() {
    const popover = this.querySelector('[popover]');
    // The commandForElement can set cross-shadow root elements.
    this.shadowRoot.querySelector('button').commandForElement = popover;
  }
});
</script>

Künftige Vorschläge können eine deklarative Möglichkeit bieten, Referenzen über Schattengrenzen hinweg zu teilen, z. B. der Reference Target Proposal.

Nächste Schritte

Wir werden weiterhin nach Möglichkeiten für neue integrierte Befehle suchen, um gängige Funktionen von Websites abzudecken. Vorgeschlagene Ideen werden im Vorschlag für eine offene Benutzeroberfläche behandelt. Einige der bereits untersuchten Ideen:

  • Öffnen und Schließen von <details>-Elementen
  • Ein "show-picker"-Befehl für <input>- und <select>-Elemente, der auf showPicker() zugeordnet ist.
  • Wiedergabebefehle für <video>- und <audio>-Elemente.
  • Textinhalte aus Elementen kopieren

Wir freuen uns über Feedback von der Community. Wenn Sie Vorschläge haben, können Sie ein Problem im Open UI Issue Tracker melden.

Weitere Informationen

Weitere Informationen zu command und commandfor finden Sie in der Spezifikation und auf der MDN.