Introduzione a command e commandfor

Data di pubblicazione: 7 marzo 2025

I pulsanti sono essenziali per creare applicazioni web dinamiche. I pulsanti aprono menu, attivano/disattivano azioni e inviano moduli. Forniscono le basi dell'interattività sul web. Rendere i pulsanti semplici e accessibili può comportare alcune sorprese. Gli sviluppatori che creano micro-frontend o sistemi di componenti possono imbattersi in soluzioni che diventano più complesse del necessario. Sebbene i framework siano utili, il web può fare di più.

Chrome 135 introduce nuove funzionalità per fornire un comportamento dichiarativo con i nuovi attributi command e commandfor, migliorando e sostituendo gli attributi popovertargetaction e popovertarget. Questi nuovi attributi possono essere aggiunti ai pulsanti, consentendo al browser di risolvere alcuni problemi fondamentali relativi a semplicità e accessibilità e di fornire funzionalità comuni integrate.

Motivi tradizionali

La creazione di comportamenti dei pulsanti senza un framework può presentare alcune interessanti sfide con l'evoluzione del codice di produzione. Sebbene HTML offra gestori onclick per i pulsanti, spesso questi non sono consentiti al di fuori di demo o tutorial a causa delle regole CSP (Content Security Policy). Sebbene questi eventi vengano inviati agli elementi pulsante, in genere i pulsanti vengono posizionati in una pagina per controllare altri elementi che richiedono codice per controllare due elementi contemporaneamente. Devi anche assicurarti che questa interazione sia accessibile agli utenti di tecnologie per la disabilità. Ciò spesso porta a un codice simile al seguente:

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

Questo approccio può essere un po' fragile e i framework hanno lo scopo di migliorare l'ergonomia. Uno schema comune con un framework come React potrebbe prevedere la mappatura del clic a una variazione di stato:

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

Anche molti altri framework hanno lo scopo di fornire un'ergonomia simile, ad esempio questo potrebbe essere scritto in AlpineJS come:

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

Se scrivi questo in Svelte, il codice potrebbe avere il seguente aspetto:

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

Alcuni sistemi o librerie di design potrebbero fare un passo avanti, fornendo wrapper intorno agli elementi dei pulsanti che incapsulano le modifiche dello stato. In questo modo, le modifiche dello stato vengono rimosse da un componente di trigger, a fronte di una maggiore ergonomia a scapito di un po' di flessibilità:

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

Il pattern command e commandfor

Con gli attributi command e commandfor, ora i pulsanti possono eseguire azioni su altri elementi in modo dichiarativo, offrendo l'ergonomia di un framework senza sacrificare la flessibilità. Il pulsante commandfor accetta un ID, simile all'attributo for, mentre command accetta valori integrati, consentendo un approccio più portatile e intuitivo.

Esempio: un pulsante di menu aperto con comando e commandfor

Il seguente codice HTML imposta relazioni dichiarative tra il pulsante e il menu, in modo che il browser gestisca la logica e l'accessibilità per te. Non è necessario gestire aria-expanded o aggiungere altro codice JavaScript.

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

Confronto tra command e commandfor con popovertargetaction e popovertarget

Se hai già utilizzato popover, potresti conoscere gli attributi popovertarget e popovertargetaction. Questi elementi funzionano in modo simile a commandfor e command, rispettivamente, tranne per il fatto che sono specifici per i popup. Gli attributi command e commandfor sostituiscono completamente questi attributi precedenti. I nuovi attributi supportano tutte le funzionalità degli attributi precedenti, oltre ad aggiungere nuove funzionalità.

Comandi integrati

L'attributo command ha un insieme di comportamenti integrati che mappano a varie API per gli elementi interattivi:

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

Questi comandi vengono mappati alle relative controparti JavaScript, semplificando al contempo l'accessibilità (ad esempio fornendo le relazioni equivalenti aria-details e aria-expanded), la gestione dell'attenzione e altro ancora.

Esempio: una finestra di dialogo di conferma con command e 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>

Se fai clic sul pulsante Elimina record, la finestra di dialogo si aprirà come finestra modale, mentre se fai clic sui pulsanti Chiudi, Annulla o Elimina, la finestra di dialogo si chiuderà e verrà inviato anche un evento "close" con una proprietà returnValue corrispondente al valore del pulsante. In questo modo, non è necessario utilizzare JavaScript oltre a un singolo ascoltatore di eventi nella finestra di dialogo per determinare la procedura da seguire:

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

Comandi personalizzati

Oltre ai comandi integrati, puoi definire comandi personalizzati utilizzando un prefisso --. I comandi personalizzati inviano un evento "command" all'elemento target (proprio come i comandi integrati), ma non eseguono mai alcuna logica aggiuntiva come fanno i valori integrati. Ciò offre flessibilità per creare componenti che possono reagire ai pulsanti in vari modi senza dover fornire componenti wrapper, eseguire la ricerca dell'elemento target nel DOM o mappare i clic sui pulsanti alle modifiche dello stato. In questo modo puoi fornire un'API all'interno di HTML per i tuoi componenti:

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

Comandi in ShadowDOM

Poiché l'attributo commandfor accetta un ID, esistono limitazioni per quanto riguarda il passaggio al DOM ombra. In questi casi, puoi utilizzare l'API JavaScript per impostare la proprietà .commandForElement, che può impostare qualsiasi elemento nelle radici nascoste:

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

Le proposte future potrebbero fornire un modo dichiarativo per condividere i riferimenti tra i confini in ombra, ad esempio la Proposta di destinazione di riferimento.

Passaggi successivi

Continueremo a esplorare le possibilità per nuovi comandi integrati, in modo da coprire le funzionalità comuni utilizzate dai siti web. Le idee proposte sono trattate nella Proposta di UI aperta. Ecco alcune delle idee già esplorate:

  • Elementi <details> di apertura e chiusura.
  • Un comando "show-picker" per gli elementi <input> e <select>, mappato a showPicker().
  • Comandi di riproduzione per gli elementi <video> e <audio>.
  • Copia dei contenuti di testo dagli elementi.

Accogliamo con favore il contributo della community. Se hai suggerimenti, non esitare a segnalare un problema nel tracker dei problemi relativi all'interfaccia utente aperta.

Scopri di più

Scopri di più su command e commandfor nella specifica e su MDN.