Présentation de command et commandfor

Publié le 7 mars 2025

Les boutons sont essentiels à la création d'applications Web dynamiques. Les boutons ouvrent des menus, activent/désactivent des actions et envoient des formulaires. Ils constituent la base de l'interactivité sur le Web. Rendre les boutons simples et accessibles peut entraîner des défis surprenants. Les développeurs qui créent des micro-frontends ou des systèmes de composants peuvent rencontrer des solutions qui deviennent plus complexes que nécessaire. Bien que les frameworks soient utiles, le Web peut faire plus.

Chrome 135 introduit de nouvelles fonctionnalités permettant de fournir un comportement déclaratif avec les nouveaux attributs command et commandfor, en améliorant et en remplaçant les attributs popovertargetaction et popovertarget. Ces nouveaux attributs peuvent être ajoutés aux boutons, ce qui permet au navigateur de résoudre certains problèmes fondamentaux liés à la simplicité et à l'accessibilité, et de fournir des fonctionnalités courantes intégrées.

Modèles traditionnels

La création de comportements de bouton sans framework peut poser des défis intéressants à mesure que le code de production évolue. Bien que le code HTML propose des gestionnaires onclick pour les boutons, ceux-ci sont souvent interdits en dehors des démonstrations ou des tutoriels en raison des règles du CSP (Content Security Policy). Bien que ces événements soient distribués sur des éléments de bouton, les boutons sont généralement placés sur une page pour contrôler d'autres éléments nécessitant du code pour contrôler deux éléments à la fois. Vous devez également vous assurer que cette interaction est accessible aux utilisateurs de technologies d'assistance. Cela entraîne souvent un code qui ressemble à ceci:

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

Cette approche peut être un peu fragile, et les frameworks visent à améliorer l'ergonomie. Un modèle courant avec un framework comme React peut consister à mapper le clic sur un changement d'état:

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

De nombreux autres frameworks visent également à fournir une ergonomie similaire. Par exemple, cela peut être écrit en AlpineJS comme suit:

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

Voici à quoi cela peut ressembler dans Svelte:

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

Certains systèmes ou bibliothèques de conception peuvent aller plus loin en fournissant des wrappers autour des éléments de bouton qui encapsulent les changements d'état. Cela permet d'extraire les modifications d'état derrière un composant de déclencheur, en échange d'une certaine flexibilité pour une ergonomie améliorée:

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

Modèle de commande et de commandefor

Avec les attributs command et commandfor, les boutons peuvent désormais effectuer des actions sur d'autres éléments de manière déclarative, apportant l'ergonomie d'un framework sans sacrifier la flexibilité. Le bouton commandfor accepte un ID, semblable à l'attribut for, tandis que command accepte des valeurs intégrées, ce qui permet une approche plus portable et intuitive.

Exemple: Bouton d'ouverture de menu avec commande et commandfor

Le code HTML suivant établit des relations déclaratives entre le bouton et le menu, ce qui permet au navigateur de gérer la logique et l'accessibilité à votre place. Vous n'avez pas besoin de gérer aria-expanded ni d'ajouter de code JavaScript supplémentaire.

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

Comparaison de command et commandfor avec popovertargetaction et popovertarget

Si vous avez déjà utilisé popover, vous connaissez peut-être les attributs popovertarget et popovertargetaction. Ils fonctionnent de la même manière que commandfor et command, sauf qu'ils sont spécifiques aux popovers. Les attributs command et commandfor remplacent complètement ces anciens attributs. Les nouveaux attributs sont compatibles avec toutes les fonctionnalités des anciens attributs, tout en ajoutant de nouvelles fonctionnalités.

Commandes intégrées

L'attribut command possède un ensemble de comportements intégrés qui correspondent à différentes API pour les éléments interactifs:

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

Ces commandes sont mappées sur leurs homologues JavaScript, tout en simplifiant l'accessibilité (par exemple, en fournissant les relations équivalentes aria-details et aria-expanded), la gestion du focus, etc.

Exemple: Boîte de dialogue de confirmation avec command et 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>

Cliquez sur le bouton Supprimer l'enregistrement pour ouvrir la boîte de dialogue en mode modal, ou sur les boutons Fermer, Annuler ou Supprimer pour fermer la boîte de dialogue tout en diffusant un événement "close" dans la boîte de dialogue, qui possède une propriété returnValue correspondant à la valeur du bouton. Cela réduit le besoin de JavaScript au-delà d'un seul écouteur d'événement dans la boîte de dialogue pour déterminer la suite des opérations:

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

Commandes personnalisées

En plus des commandes intégrées, vous pouvez définir des commandes personnalisées à l'aide d'un préfixe --. Les commandes personnalisées distribuent un événement "command" sur l'élément cible (tout comme les commandes intégrées), mais n'effectuent jamais de logique supplémentaire, comme le font les valeurs intégrées. Cela permet de créer des composants pouvant réagir aux boutons de différentes manières sans avoir à fournir de composants de wrapper, à parcourir le DOM pour l'élément cible ni à mapper les clics de bouton sur les changements d'état. Cela vous permet de fournir une API dans le code HTML pour vos composants:

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

Commandes dans le ShadowDOM

Étant donné que l'attribut commandfor accepte un ID, il existe des restrictions concernant le franchissement du Shadow DOM. Dans ce cas, vous pouvez utiliser l'API JavaScript pour définir la propriété .commandForElement, qui peut définir n'importe quel élément, sur les racines d'ombre:

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

Les futures propositions peuvent fournir un moyen déclaratif de partager des références au-delà des limites d'ombre, comme la proposition de cible de référence.

Étape suivante

Nous continuerons d'explorer les possibilités de nouvelles commandes intégrées pour couvrir les fonctionnalités courantes utilisées par les sites Web. Les idées proposées sont décrites dans la Proposition d'interface utilisateur ouverte. Voici quelques-unes des idées déjà explorées:

  • Ouverture et fermeture des éléments <details>.
  • Commande "show-picker" pour les éléments <input> et <select>, mappant sur showPicker().
  • Commandes de lecture pour les éléments <video> et <audio>.
  • Copier le contenu textuel d'éléments

Nous accueillons les commentaires de la communauté. Si vous avez des suggestions, n'hésitez pas à signaler un problème dans l'outil de suivi des problèmes de l'UI ouverte.

En savoir plus

Pour en savoir plus sur command et commandfor, consultez la spécification et le MDN.