Komponenty – instrukcje – karty z instrukcjami

Podsumowanie

<howto-tabs> ogranicz widoczne treści, rozdzielając je na kilka paneli. Tylko w danym momencie widoczny jest tylko jeden panel, a wszystkie odpowiadające jej karty są zawsze widoczne widoczne. Aby przełączyć się między panelami, odpowiednia karta musi być zaznaczono.

Użytkownik może zmienić aktywnej karty.

Jeśli JavaScript jest wyłączony, wszystkie panele są przeplatane z przy użyciu odpowiednich kart. Karty pełnią teraz funkcję nagłówków.

Dokumentacja

Wersja demonstracyjna

Zobacz demonstrację 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 zostanie dopasowany 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() {

Zdefiniuj kody klawiszy, które ułatwiają obsługę zdarzeń klawiatury.

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

Aby uniknąć wywoływania parsera za pomocą funkcji .innerHTML w przypadku każdej nowej instancji, wszystkie instancje <howto-tabs> korzystają z szablonu treś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 kontener na karty i panele.

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ą zapisywane w pamięci podręcznej i dlatego zmieniają się podczas 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 this.

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

Aby ulepszyć działanie tej funkcji, znaczniki powinny się przełączać między kartami i panelami. Elementy, które zmieniają kolejność dzieci, zwykle nie radzą sobie dobrze z strukturami. Zamiast tego do zmiany kolejności elementów za pomocą przedziałów jest używany model Shadow DOM.

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

Zaimportuj udostępniony szablon, 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 będą automatycznie przydzielane do przedziałów i spowodują uruchomienie zmiany przedziałów, więc nie jest potrzebna MutationObserver.

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

connectedCallback() grupuje karty i panele przez zmianę ich kolejności i dba o to, aby była aktywna dokładnie 1 karta.

    connectedCallback() {

Element musi ręcznie obsługiwać zdarzenia wejściowe, aby umożliwić przełączanie za pomocą klawiszy strzałek i opcji 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 po uaktualnieniu elementu przez parser. Z tego powodu element ręcznie wywołuje moduł. Gdy nowy sposób działania pojawi się we wszystkich przeglądarkach, możesz usunąć poniższy kod.

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

Funkcja 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 jest dodawany do jednego z boksów shadow DOM lub z niego usuwany.

    _onSlotChange() {
      this._linkPanels();
    }

_linkPanels() łączy karty z przyległymi panelami za pomocą elementów sterujących ARIA i elementu aria-labelledby. Dodatkowo metoda gwarantuje, że aktywna jest tylko jedna karta.

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

Przypisz każdemu panelowi atrybut aria-labelledby odnoszą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 zostanie wybrana pierwsza karta.

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

Następnie przejdź do wybranej karty. _selectTab() oznaczy wszystkie pozostałe karty jako odznaczone i ukryje wszystkie pozostałe panele.

      this._selectTab(selectedTab);
    }

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

Jest to metoda, a nie metoda getter, ponieważ metoda get wskazuje, że jest tanio.

    _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}`);
    }

_prevTab() zwraca kartę znajdującą się przed obecnie wybraną, zawijając ją po przejściu do pierwszej.

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

Użyj funkcji findIndex(), aby znaleźć indeks aktualnie wybranego elementu i odejmuje 1, aby uzyskać indeks poprzedniego elementu.

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

Dodaj tabs.length, aby sprawdzić, czy indeks jest liczbą dodatnią, i w razie potrzeby uzyskaj zawinięcie modułu.

      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];
    }

_nextTab() wyświetla kartę, która pojawia się po obecnie wybranej, i zawija się po przejściu do 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 element newTab.

      const newPanel = this._panelForTab(newTab);

Jeśli taki 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 z samego elementu karty, było to naciśnięcie w panelu lub w pustym miejscu. Nic nie rób.

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

Nie używaj skrótów modyfikujących, które są zwykle używane przez technologię wspomagającą osoby z niepełnosprawnością.

      if (event.altKey)
        return;

Wielkość liter w przypadku danego klawisza zależy od tego, która karta powinna zostać 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;

Każde inne naciśnięcie klawisza jest ignorowane i przekazywane z powrotem do przeglądarki.

        default:
          return;
      }

Przeglądarka może mieć pewne funkcje natywne powiązane z klawiszami strzałek (Home lub End). Element wywołuje preventDefault(), by uniemożliwić przeglądarce wykonywanie jakichkolwiek działań.

      event.preventDefault();

Wybierz nową kartę, która została określona w przypadku danego przełącznika.

      this._selectTab(newTab);
    }

_onClick() obsługuje kliknięcia w panelu kart.

    _onClick(event) {

Jeśli kliknięcie nie było kierowane na element karty, było to kliknięcie w panelu lub w pustym obszarze. Nic nie rób.

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

Jeśli był on umieszczony w elemencie karty, wybierz tę kartę.

      this._selectTab(event.target);
    }
  }

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

Funkcja howtoTabCounter zlicza utworzone <howto-tab> instancje. Numer służy do generowania nowych, unikalnych identyfikatorów.

  let howtoTabCounter = 0;

HowtoTab to karta panelu <howto-tabs>. Aby semantyka działała w przypadku awarii JavaScriptu, zawsze używaj parametru <howto-tab> razem z atrybutem role="heading".

Element <howto-tab> określa element <howto-panel>, do którego należy, używając identyfikatora tego panelu 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 to zrobisz, JavaScript będzie działać, a element zmieni swoją rolę na tab.

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

Ustaw dobrze zdefiniowany stan początkowy.

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

Sprawdź, czy usługa ma wartość instancji. Jeśli tak, skopiuj wartość i usuń właściwość instancji, aby nie powielała funkcji ustawiającej właściwości klasy. Na koniec przekaż wartość do metody ustawiania właściwości klasy, aby mogła ona aktywować 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 leniwe ładowanie jej definicji. Gdyby nie było tego zabezpieczenia, uaktualniony element nie miałby tej właściwości, 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 być takie same. W tym celu funkcja ustawiająca właściwości selected obsługuje wartości prawda/fałszywe i odzwierciedla je zgodnie ze stanem atrybutu. Co ważne, w ustawieniu usługi nie ma żadnych efektów ubocznych. Na przykład metoda ustawiająca nie ustawia elementu aria-selected. Zamiast tego dzieje się to w attributeChangedCallback. Ogólnie rzecz biorąc, osoby ustawiające właściwości powinny być bardzo głupie. Jeśli ustawienie właściwości lub atrybutu powinno wywołać efekt uboczny (np. ustawienie odpowiedniego atrybutu ARIA), będzie on działał w attributeChangedCallback(). Dzięki temu unikniesz zarządzania skomplikowanymi scenariuszami ponownego udostępniania atrybutów lub 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 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);
})();