Komponenty – instrukcje – karty z instrukcjami

Podsumowanie

<howto-tabs> ograniczyć widoczne treści, dzieląc je 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ę między panelami, musisz wybrać odpowiednią kartę.

Użytkownik może zmienić aktywną kartę, klikając lub używając klawiszy strzałek.

Jeśli JavaScript jest wyłączony, wszystkie panele są wyświetlane przeplatane z odpowiednimi kartami. Karty pełnią teraz funkcję nagłówków.

Dokumentacja

Prezentacja

Wyświetl 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 kod JavaScript nie zostanie wykonany, element nie będzie pasować do :defined. W takim przypadku ten styl dodaje odstęp 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() {

Definiowanie kodów klawiszy, aby ułatwić obsługę zdarzeń związanych z klawiaturą.

  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 element kontenera dla kart i paneli.

Wszystkie elementy podrzędne elementu <howto-tabs> powinny mieć wartość <howto-tab> lub <howto-tabpanel>. Ten element jest stanem bezstanowym, co oznacza, że żadne wartości nie są przechowywane w pamięci podręcznej, a zatem zmiany są wprowadzane w czasie działania.

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

Jeśli metody obsługi zdarzeń nie są przypisane do tego elementu, muszą zostać powiązane, jeśli mają mieć dostęp 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ść elementów podrzędnych, zwykle nie współpracują dobrze z ramami. Zamiast tego do zmiany kolejności elementów za pomocą slotów używany jest 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-labelledbyaria-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() pogrupowuje karty i panele, zmieniając ich kolejność, i zapewnia, że aktywna jest tylko 1 karta.

    connectedCallback() {

Element musi obsługiwać ręczne zdarzenia wprowadzania, aby umożliwić przełączanie się za pomocą klawiszy strzałek i 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 nowe zachowanie zostanie wprowadzone we wszystkich przeglądarkach, kod poniżej można usunąć.

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

disconnectedCallback() usuwa detektory zdarzeń dodane przez connectedCallback().

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

_onSlotChange() jest wywoływany za każdym razem, gdy element zostanie dodany lub usunięty z jednego z cienia okienek DOM.

    _onSlotChange() {
      this._linkPanels();
    }

_linkPanels() łączy karty z sąsiednimi panelami za pomocą atrybutów aria-controls i aria-labelledby. Dodatkowo metoda zapewnia, że tylko jedna karta jest aktywna.

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

Przypisz do każdego panelu atrybut aria-labelledby, który odwołuje się do karty sterującej.

      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 jest zaznaczona. W przeciwnym razie zostanie wybrana pierwsza karta.

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

Następnie przejdź do wybranej karty. _selectTab() odznacza wszystkie pozostałe karty i ukrywanie pozostałych paneli.

      this._selectTab(selectedTab);
    }

_allPanels() zwraca wszystkie panele w panelu kart. Ta funkcja może zapamiętać wynik, jeśli zapytania DOM staną się kiedyś problemem z 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, którym steruje dana karta.

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

_prevTab() zwraca kartę, która znajduje się przed aktualnie wybraną, a po osiągnięciu pierwszej karty wraca do pierwszej.

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

Użyj funkcji findIndex(), aby znaleźć indeks aktualnie wybranego elementu, a następnie odejmij 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() zwraca kartę, która znajduje się po bieżąco wybranej karcie, a po dotarciu do ostatniej karty wraca do pierwszej.

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

reset() odznacza wszystkie karty i ukrywanie wszystkich paneli.

    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();

Pobieranie panelu, z którym powiązany jest 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ło z elementu karty, było to naciśnięcie klawisza w panelu lub pustej przestrzeni. Nie ma nic do zrobienia.

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

Nie obsługuj skrótów klawiszowych, które są zwykle używane przez technologie wspomagające.

      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ć niektóre funkcje natywne powiązane z klawiszami strzałek, Home lub End. Element wywołuje funkcję preventDefault(), aby uniemożliwić przeglądarce podejmowanie jakichkolwiek działań.

      event.preventDefault();

Wybierz nową kartę, która została określona w sekcji switch-case.

      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 puste miejsce. Nie ma nic do zrobienia.

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

Jeśli jednak znajdował się on na elemencie karty, wybierz tę kartę.

      this._selectTab(event.target);
    }
  }

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

howtoTabCounter zlicza liczbę utworzonych instancji <howto-tab>. Numer jest używany do generowania nowych, unikalnych identyfikatorów.

  let howtoTabCounter = 0;

HowtoTab to karta panelu kart <howto-tabs>. W znacznikach <howto-tab> należy zawsze używać tagu role="heading", aby semantyka była dostępna, gdy JavaScript nie działa.

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

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

  class HowtoTab extends HTMLElement {

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

    constructor() {
      super();
    }

    connectedCallback() {

Jeśli to zostanie wykonane, JavaScript zacznie 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ć lustrzanym odbiciem siebie nawzajem. 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 te działania są wykonywane w attributeChangedCallback. Ogólnie rzecz biorąc, uczyń metody ustawiania właściwości bardzo prostymi, a jeśli ustawienie właściwości lub atrybutu ma wywołać efekt uboczny (np. ustawienie odpowiedniego atrybutu ARIA), zrób to w metodie attributeChangedCallback(). Dzięki temu unikniesz konieczności zarządzania złożonymi scenariuszami ponownego korzystania z atrybutów i właściwości.

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