Wprowadzenie poleceń i poleceń dla

Data publikacji: 7 marca 2025 r.

Przyciski są niezbędne do tworzenia dynamicznych aplikacji internetowych. Przyciski otwierają menu, przełączają działania i przesyłają formularze. Stanowią podstawę interakcji w internecie. Ułatwianie dostępu do przycisków może prowadzić do nieoczekiwanych problemów. Deweloperzy tworzący mikrofrontendy lub systemy komponentów mogą napotkać rozwiązania, które stają się bardziej złożone niż to konieczne. Chociaż frameworki są przydatne, internet może zaoferować więcej.

Chrome 135 wprowadza nowe możliwości deklaratywnego zachowania za pomocą nowych atrybutów command i commandfor, które rozszerzają i zastępują atrybuty popovertargetaction i popovertarget. Te nowe atrybuty można dodawać do przycisków, aby umożliwić przeglądarce rozwiązywanie podstawowych problemów związanych z prostotą i dostępnością oraz udostępniać wbudowane, powszechne funkcje.

tradycyjne wzory;

Tworzenie zachowań przycisków bez ramowego rozwiązania może stanowić interesujące wyzwanie w miarę ewolucji kodu produkcyjnego. Chociaż HTML udostępnia onclickobsługi przycisków, są one często niedozwolone poza demonstracjami lub samouczkami ze względu na zasady ochrony treści (CSP). Chociaż te zdarzenia są wysyłane w przypadku elementów typu button, przyciski są zwykle umieszczane na stronie, aby sterować innymi elementami, co wymaga kodu sterującego 2 elementami jednocześnie. Musisz też zadbać o to, aby ta interakcja była dostępna dla użytkowników technologii wspomagających. Często prowadzi to do tego, że kod wygląda mniej więcej tak:

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

Takie podejście może być niestabilne, dlatego frameworki mają na celu poprawę ergonomii. Typowym wzorcem w ramach takich frameworków jak React może być mapowanie kliknięcia na zmianę stanu:

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

Wiele innych frameworków ma na celu zapewnienie podobnej ergonomii. Na przykład ten kod może być napisany w AlpineJS w ten sposób:

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

Podczas pisania tego w Svelte może wyglądać mniej więcej tak:

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

Niektóre systemy lub biblioteki projektowe mogą pójść o krok dalej, udostępniając okładki wokół elementów przycisków, które otaczają zmiany stanu. Dzięki temu stany są abstrakcyjne i zmieniają się w komponencie wyzwalacza, co powoduje pewną utratę elastyczności na rzecz lepszej ergonomii:

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

Wzór polecenia i poleceniafor

Dzięki atrybutom commandcommandfor przyciski mogą teraz wykonywać działania na innych elementach w sposób deklaratywny, co zapewnia ergonomię ramki bez poświęcania elastyczności. Przycisk commandfor przyjmuje identyfikator (podobnie jak atrybut for), a command przyjmuje wbudowane wartości, co umożliwia bardziej przenośne i intuicyjne podejście.

Przykład: przycisk otwierający menu z poleceniem i poleceniem dla

Poniższy kod HTML ustanawia deklaratywnie relacje między przyciskiem a menu, co pozwala przeglądarce obsługiwać logikę i dostępność. Nie musisz zarządzać tagiem aria-expanded ani dodawać dodatkowego kodu JavaScript.

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

Porównanie commandcommandforpopovertargetactionpopovertarget

Jeśli korzystasz już z poziomu popover, prawdopodobnie znasz atrybuty popovertargetpopovertargetaction. Te atrybuty działają podobnie jak commandforcommand, ale dotyczą tylko wyskakujących okienek. Atrybuty command i commandfor całkowicie zastępują te starsze atrybuty. Nowe atrybuty obsługują wszystkie funkcje starszych atrybutów, a także zapewniają nowe możliwości.

Wbudowane polecenia

Atrybut command ma zestaw wbudowanych zachowań, które są mapowane na różne interfejsy API elementów interaktywnych:

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

Te polecenia są mapowane na swoje odpowiedniki w JavaScriptzie, a także upraszczają obsługę (np. udostępniają relacje aria-detailsaria-expanded), zarządzanie fokusem i inne funkcje.

Przykład: okno potwierdzenia z commandcommandfor

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

Kliknięcie przycisku Usuń rekord spowoduje otwarcie okna modalnego, natomiast kliknięcie przycisków Zamknij, Anuluj lub Usuń zamknie okno, a także wyśle do okna zdarzenie "close", które ma właściwość returnValue odpowiadającą wartości przycisku. Dzięki temu nie trzeba używać JavaScriptu poza pojedynczym obiektem odbiorczym zdarzenia w dialogu, aby określić, co należy zrobić dalej:

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

Polecenia niestandardowe

Oprócz wbudowanych poleceń możesz definiować polecenia niestandardowe za pomocą prefiksu --. Polecenia niestandardowe wysyłają zdarzenie "command" do elementu docelowego (podobnie jak wbudowane polecenia), ale poza tym nigdy nie wykonują żadnej dodatkowej logiki, jak to robią wbudowane wartości. Daje to elastyczność w budowaniu komponentów, które mogą reagować na przyciski na różne sposoby bez konieczności udostępniania komponentów opakowania, przeszukiwania DOM w celu znalezienia elementu docelowego czy mapowania kliknięć przycisków na zmiany stanu. Dzięki temu możesz udostępnić interfejs API w ramach kodu HTML swoich komponentów:

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

Polecenia w ShadowDOM

Atrybut commandfor przyjmuje identyfikator, więc istnieją ograniczenia dotyczące przechodzenia do DOM-u cieni. W takich przypadkach możesz użyć interfejsu JavaScript API, aby ustawić właściwość .commandForElement, która może zmienić dowolny element w korzeniach cieni:

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

Przyszłe propozycje mogą zawierać deklaratywny sposób udostępniania odwołań w ramach granic cienia, na przykład propozycja docelowego odwołania.

Co dalej?

Będziemy nadal badać możliwości nowych wbudowanych poleceń, aby obejmowały one częste funkcje używane przez witryny. Więcej informacji o proponowanych pomysłach znajdziesz w artykule Proponowane zmiany w interfejsie. Oto kilka pomysłów, które zostały już przeanalizowane:

  • Otwieranie i zamykanie elementów <details>.
  • Polecenie "show-picker" dla elementów <input> i <select>, które jest mapowane na showPicker().
  • Polecenia odtwarzania elementów <video> i <audio>.
  • Kopiowanie tekstu z elementów.

Zapraszamy do dzielenia się opiniami. Jeśli masz sugestie, skontaktuj się z nami za pomocą Open UI Issue Tracker.

Więcej informacji

Więcej informacji o commandcommandfor znajdziesz w specyfikacjiMDN.