Komponenty – instrukcje – karty z instrukcjami

Podsumowanie

<howto-tabs> ogranicza widoczność treści, rozdzielając ją na kilka paneli. W danym momencie widoczny jest tylko jeden panel, a wszystkie powiązane z nim karty są zawsze widoczne. Aby przełączyć się z jednego panelu do innego, musisz wybrać odpowiednią kartę.

Klikając lub używając klawiszy strzałek, użytkownik może zmienić wybór aktywnej karty.

Jeśli JavaScript jest wyłączony, wszystkie panele są przeplatane z odpowiednimi kartami. Karty działają teraz jako nagłówki.

Dokumentacja

Wersja demonstracyjna

Zobacz prezentację na żywo w GitHubie

Przykład użycia

<style>
  howto-tab {
    border: 1px solid black;
    padding: 20px;
  }
  howto-panel {
    padding: 20px;
    background-color: lightgray;
  }
  howto-tab[selected] {
    background-color: bisque;
  }

Jeśli JavaScript nie zostanie uruchomiony, element nie będzie pasować do :defined. W takim przypadku ten styl dodaje odstępy między kartami a poprzednim panelem.

  howto-tabs:not(:defined), howto-tab:not(:defined), howto-panel:not(:defined) {
    display: block;
  }
</style>

<howto-tabs>
  <howto-tab role="heading" slot="tab">Tab 1</howto-tab>
  <howto-panel role="region" slot="panel">Content 1</howto-panel>
  <howto-tab role="heading" slot="tab">Tab 2</howto-tab>
  <howto-panel role="region" slot="panel">Content 2</howto-panel>
  <howto-tab role="heading" slot="tab">Tab 3</howto-tab>
  <howto-panel role="region" slot="panel">Content 3</howto-panel>
</howto-tabs>

Kod

(function() {

Określaj kody klawiszy ułatwiające obsługę zdarzeń klawiatury.

  const KEYCODE = {
    DOWN: 40,
    LEFT: 37,
    RIGHT: 39,
    UP: 38,
    HOME: 36,
    END: 35,
  };

Aby uniknąć wywoływania parsera z metodą .innerHTML w przypadku każdej nowej instancji, wszystkie instancje <howto-tabs> mają wspólny szablon zawartości Shadow DOM.

  const template = document.createElement('template');
  template.innerHTML = `
    <style>
      :host {
        display: flex;
        flex-wrap: wrap;
      }
      ::slotted(howto-panel) {
        flex-basis: 100%;
      }
    </style>
    <slot name="tab"></slot>
    <slot name="panel"></slot>
  `;

HowtoTabs to element kontenera kart i paneli.

Wszystkie elementy podrzędne elementu <howto-tabs> powinny mieć wartość <howto-tab> lub <howto-tabpanel>. Ten element jest bezstanowy, co oznacza, że żadne wartości nie są przechowywane w pamięci podręcznej i dlatego nie zmieniają się podczas pracy w czasie działania.

  class HowtoTabs extends HTMLElement {
    constructor() {
      super();

Moduły obsługi zdarzeń, które nie są dołączone do tego elementu, muszą być powiązane, jeśli potrzebują dostępu do elementu this.

      this._onSlotChange = this._onSlotChange.bind(this);

Aby ulepszyć progresywnie, znaczniki powinny być naprzemienne między kartami i panelami. Elementy, które zmieniają kolejność elementów podrzędnych, nie współgrają ze schematami. Zamiast tego do zmiany kolejności elementów używany jest model shadow DOM.

      this.attachShadow({ mode: 'open' });

Zaimportuj szablon udostępniony, aby utworzyć boksy na karty i panele.

      this.shadowRoot.appendChild(template.content.cloneNode(true));

      this._tabSlot = this.shadowRoot.querySelector('slot[name=tab]');
      this._panelSlot = this.shadowRoot.querySelector('slot[name=panel]');

Ten element musi reagować na nowe elementy podrzędne, ponieważ łączy karty i panele semantycznie za pomocą elementów aria-labelledby i aria-controls. Nowe elementy podrzędne są przypisywane automatycznie i powodują uruchomienie zmiany przedziałów, więc funkcja MutationObserver nie jest potrzebna.

      this._tabSlot.addEventListener('slotchange', this._onSlotChange);
      this._panelSlot.addEventListener('slotchange', this._onSlotChange);
    }

connectedCallback() grupuje karty i panele, zmieniając ich kolejność, a potem dba o to, aby była aktywna tylko 1 karta.

    connectedCallback() {

Element musi obsługiwać ręczną obsługę zdarzeń wejściowych, aby umożliwić przełączanie za pomocą klawiszy strzałek oraz Home / End.

      this.addEventListener('keydown', this._onKeyDown);
      this.addEventListener('click', this._onClick);

      if (!this.hasAttribute('role'))
        this.setAttribute('role', 'tablist');

Do niedawna zdarzenia slotchange nie były uruchamiane, gdy element został uaktualniony przez parser. Z tego powodu element wywołuje moduł obsługi ręcznie. Gdy nowy sposób działania zostanie zastosowany we wszystkich przeglądarkach, można usunąć poniższy kod.

      Promise.all([
        customElements.whenDefined('howto-tab'),
        customElements.whenDefined('howto-panel'),
      ])
        .then(() => this._linkPanels());
    }

disconnectedCallback() usuwa detektory zdarzeń dodane przez użytkownika connectedCallback().

    disconnectedCallback() {
      this.removeEventListener('keydown', this._onKeyDown);
      this.removeEventListener('click', this._onClick);
    }

Funkcja _onSlotChange() jest wywoływana za każdym razem, gdy element zostanie dodany do jednego z boksów Shadow DOM lub z niego usunięty.

    _onSlotChange() {
      this._linkPanels();
    }

_linkPanels() łączy karty z sąsiadującymi z nimi panelami za pomocą elementów sterujących ARIA i elementu aria-labelledby. Dodatkowo aktywna jest tylko jedna karta.

    _linkPanels() {
      const tabs = this._allTabs();

Nadaj każdemu panelowi atrybut aria-labelledby odwołujący się do karty, która nim steruje.

      tabs.forEach((tab) => {
        const panel = tab.nextElementSibling;
        if (panel.tagName.toLowerCase() !== 'howto-panel') {
          console.error(`Tab #${tab.id} is not a` +
            `sibling of a <howto-panel>`);
          return;
        }

        tab.setAttribute('aria-controls', panel.id);
        panel.setAttribute('aria-labelledby', tab.id);
      });

Element sprawdza, czy któraś z kart została oznaczona jako wybrana. W przeciwnym razie wybierana jest pierwsza karta.

      const selectedTab =
        tabs.find((tab) => tab.selected) || tabs[0];

Następnie przejdź na wybraną kartę. _selectTab() oznaczy wszystkie inne karty jako odznaczone, a pozostałe panele będą ukryte.

      this._selectTab(selectedTab);
    }

_allPanels() zwraca wszystkie panele w panelu kart. Ta funkcja może zapamiętywać wynik, jeśli zapytania DOM kiedykolwiek staną się problemem z wydajnością. Wadą zapamiętywania jest to, że dynamicznie dodawane karty i panele nie są obsługiwane.

To jest metoda, a nie metoda pobierająca, ponieważ metoda getter sugeruje, że czytanie jest tanie.

    _allPanels() {
      return Array.from(this.querySelectorAll('howto-panel'));
    }

_allTabs() zwraca wszystkie karty w panelu kart.

    _allTabs() {
      return Array.from(this.querySelectorAll('howto-tab'));
    }

_panelForTab() zwraca panel sterowany przez daną kartę.

    _panelForTab(tab) {
      const panelId = tab.getAttribute('aria-controls');
      return this.querySelector(`#${panelId}`);
    }

Funkcja _prevTab() zwraca kartę poprzedzającą aktualnie wybraną kartę, zawijając się po dotarciu do pierwszej.

    _prevTab() {
      const tabs = this._allTabs();

Funkcja findIndex() pozwala znaleźć indeks obecnie wybranego elementu i odejmuje 1 z nich, aby uzyskać indeks poprzedniego elementu.

      let newIdx = tabs.findIndex((tab) => tab.selected) - 1;

Dodaj wartość tabs.length, aby mieć pewność, że indeks jest liczbą dodatnią, i w razie potrzeby uzyskaj moduł do zawijania.

      return tabs[(newIdx + tabs.length) % tabs.length];
    }

_firstTab() zwraca pierwszą kartę.

    _firstTab() {
      const tabs = this._allTabs();
      return tabs[0];
    }

_lastTab() zwraca ostatnią kartę.

    _lastTab() {
      const tabs = this._allTabs();
      return tabs[tabs.length - 1];
    }

Aplikacja _nextTab() pobiera kartę, która następuje po obecnie wybranej, zawija się po osiągnięciu ostatniej karty.

    _nextTab() {
      const tabs = this._allTabs();
      let newIdx = tabs.findIndex((tab) => tab.selected) + 1;
      return tabs[newIdx % tabs.length];
    }

reset() oznacza wszystkie karty jako odznaczone i ukrywa wszystkie panele.

    reset() {
      const tabs = this._allTabs();
      const panels = this._allPanels();

      tabs.forEach((tab) => tab.selected = false);
      panels.forEach((panel) => panel.hidden = true);
    }

_selectTab() oznacza daną kartę jako wybraną. Dodatkowo odkrywa panel odpowiadający danej karcie.

    _selectTab(newTab) {

Odznacz wszystkie karty i ukryj wszystkie panele.

      this.reset();

Pobierz panel, z którym jest powiązany zasób newTab.

      const newPanel = this._panelForTab(newTab);

Jeśli ten panel nie istnieje, przerwij.

      if (!newPanel)
        throw new Error(`No panel with id ${newPanelId}`);
      newTab.selected = true;
      newPanel.hidden = false;
      newTab.focus();
    }

_onKeyDown() obsługuje naciśnięcia klawiszy w panelu kart.

    _onKeyDown(event) {

Jeśli naciśnięcie klawisza nie pochodziło z samego elementu tabulacji, było to naciśnięcie klawisza wewnątrz panelu lub pustego miejsca. Nie musisz nic robić.

      if (event.target.getAttribute('role') !== 'tab')
        return;

Nie stosuj skrótów modyfikujących, które zwykle są używane w technologiach wspomagających osoby z niepełnosprawnością.

      if (event.altKey)
        return;

Zmiana wielkości liter określa, która karta ma być oznaczona jako aktywna w zależności od naciśniętego klawisza.

      let newTab;
      switch (event.keyCode) {
        case KEYCODE.LEFT:
        case KEYCODE.UP:
          newTab = this._prevTab();
          break;

        case KEYCODE.RIGHT:
        case KEYCODE.DOWN:
          newTab = this._nextTab();
          break;

        case KEYCODE.HOME:
          newTab = this._firstTab();
          break;

        case KEYCODE.END:
          newTab = this._lastTab();
          break;

Pozostałe naciśnięcie klawisza jest ignorowane i jest przekazywane z powrotem do przeglądarki.

        default:
          return;
      }

Niektóre natywne funkcje przeglądarki są powiązane z klawiszami strzałek lub klawiszami Home lub End. Element wywołuje funkcję preventDefault(), by uniemożliwić przeglądarce wykonywanie jakichkolwiek działań.

      event.preventDefault();

Wybierz nową kartę wskazaną w ramach przełącznika.

      this._selectTab(newTab);
    }

_onClick() obsługuje kliknięcia wewnątrz panelu kart.

    _onClick(event) {

Jeśli kliknięcie nie było kierowane na sam element karty, było to kliknięcie w panelu lub w pustym miejscu. Nie musisz nic robić.

      if (event.target.getAttribute('role') !== 'tab')
        return;

Jeśli jednak był na elemencie karty, wybierz tę kartę.

      this._selectTab(event.target);
    }
  }

  customElements.define('howto-tabs', HowtoTabs);

howtoTabCounter zlicza utworzone instancje (<howto-tab>). Służy on do generowania nowych, unikalnych identyfikatorów.

  let howtoTabCounter = 0;

HowtoTab to karta panelu karty <howto-tabs>. <howto-tab> należy zawsze używać z role="heading" w znacznikach, aby semantyka była przydatna w przypadku awarii JavaScriptu.

Element <howto-tab> określa, do którego elementu <howto-panel> należy, używając jego identyfikatora jako wartości atrybutu aria-controls.

Jeśli nie podasz żadnego identyfikatora, <howto-tab> automatycznie wygeneruje unikalny identyfikator.

  class HowtoTab extends HTMLElement {

    static get observedAttributes() {
      return ['selected'];
    }

    constructor() {
      super();
    }

    connectedCallback() {

Jeśli tak się stanie, JavaScript działa, a element zmienia swoją rolę na tab.

      this.setAttribute('role', 'tab');
      if (!this.id)
        this.id = `howto-tab-generated-${howtoTabCounter++}`;

Ustaw jasno określony stan początkowy.

      this.setAttribute('aria-selected', 'false');
      this.setAttribute('tabindex', -1);
      this._upgradeProperty('selected');
    }

Sprawdź, czy właściwość ma wartość instancji. Jeśli tak, skopiuj wartość i usuń właściwość instancji, aby nie była cieniowana przez metodę ustawiania właściwości klasy. Na koniec przekaż wartość do metody ustawiania właściwości klasy, aby mogła wywołać efekty uboczne. Ma to na celu ochronę przed przypadkami, w których na przykład platforma mogła dodać element do strony i ustawić wartość dla jednej z jej właściwości, ale leniwie wczytywała definicję. Bez tej ochrony uaktualniony element pomijałby tę właściwość, a właściwość instancji uniemożliwiłaby wywołanie metody ustawiającej właściwości klasy.

    _upgradeProperty(prop) {
      if (this.hasOwnProperty(prop)) {
        let value = this[prop];
        delete this[prop];
        this[prop] = value;
      }
    }

Właściwości i odpowiadające im atrybuty powinny się replikować. W związku z tym metoda ustawiania właściwości selected obsługuje wartości prawda/fałsz i odzwierciedla te wartości do stanu atrybutu. Pamiętaj, że narzędzie do konfigurowania właściwości nie ma żadnych skutków ubocznych. Na przykład instruktor nie ustawia aria-selected. Te zadania są wykonywane w narzędziu attributeChangedCallback. Ogólnie ustawiaj ustawienia właściwości jako bardzo głośne. Jeśli ustawienie właściwości lub atrybutu powinno spowodować efekt uboczny (np. ustawienie odpowiedniego atrybutu ARIA), będzie to działać w attributeChangedCallback(). Pozwoli to uniknąć konieczności zarządzania złożonymi scenariuszami ponownego przechowywania atrybutów i usług.

    attributeChangedCallback() {
      const value = this.hasAttribute('selected');
      this.setAttribute('aria-selected', value);
      this.setAttribute('tabindex', value ? 0 : -1);
    }

    set selected(value) {
      value = Boolean(value);
      if (value)
        this.setAttribute('selected', '');
      else
        this.removeAttribute('selected');
    }

    get selected() {
      return this.hasAttribute('selected');
    }
  }

  customElements.define('howto-tab', HowtoTab);

  let howtoPanelCounter = 0;

HowtoPanel to panel panelu karty <howto-tabs>.

  class HowtoPanel extends HTMLElement {

    constructor() {
      super();
    }

    connectedCallback() {
      this.setAttribute('role', 'tabpanel');
      if (!this.id)
        this.id = `howto-panel-generated-${howtoPanelCounter++}`;
    }
  }

  customElements.define('howto-panel', HowtoPanel);
})();