"Nasıl Yapılır?" Bileşenleri – "Nasıl yapılır?" sekmeleri

Özet

<howto-tabs>, görünür içeriği birden fazla panele bölerek sınırlandırır. Yalnızca aynı anda yalnızca bir panel görünürken, karşılık gelen tüm sekmeler her zaman görünür. Bir panelden diğerine geçiş yapmak için ilgili sekmenin seçili.

Kullanıcı, tıklayarak veya ok tuşlarını kullanarak etkin sekme seçimi.

JavaScript devre dışı bırakılırsa tüm paneller, ilgili sekmeler. Sekmeler artık başlık işlevi görüyor.

Referans

Demo

GitHub'da canlı demoyu görüntüle

Örnek kullanım

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

JavaScript çalışmazsa öğe, :defined ile eşleşmez. Bu durumda, bu stil sekmeler ile önceki panel arasına boşluk ekler.

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

Klavye etkinliklerinin işlenmesine yardımcı olmak için tuş kodları tanımlayın.

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

Her yeni örnekte .innerHTML ile ayrıştırıcının çağrılmasından kaçınmak amacıyla, tüm <howto-tabs> örneklerinde gölge DOM'un içeriğine yönelik bir şablon paylaşılır.

  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, sekmeler ve paneller için bir kapsayıcı öğedir.

<howto-tabs> öğesinin tüm alt öğeleri <howto-tab> veya <howto-tabpanel> olmalıdır. Bu öğe durum bilgisizdir, yani hiçbir değer önbelleğe alınmaz ve dolayısıyla çalışma zamanı çalışması sırasında değişir.

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

Bu öğeye eklenmemiş etkinlik işleyicilerin this öğesine erişmeleri gerekiyorsa bağlanmaları gerekir.

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

Progresif geliştirme için işaretleme, sekmeler ve paneller arasında geçiş yapmalıdır. Alt öğelerini yeniden sıralayan öğeler genellikle çerçevelerle iyi performans göstermez. Bunun yerine, alanları kullanarak öğeleri yeniden sıralamak için gölge DOM kullanılır.

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

Sekmeler ve paneller için alanlar oluşturmak üzere paylaşılan şablonu içe aktarın.

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

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

Bu öğenin, aria-labelledby ve aria-controls kullanarak sekmeleri ve panelleri semantik olarak bağlaması nedeniyle yeni alt öğelere tepki vermesi gerekir. Yeni alt öğeler otomatik olarak yuvaya yerleştirilir ve slotchange'in etkinleşmesine neden olur, bu nedenle MutationObserver'a gerek yoktur.

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

connectedCallback(), sekmeleri ve panelleri yeniden sıralayarak gruplandırır ve tam olarak bir sekmenin etkin olmasını sağlar.

    connectedCallback() {

Öğenin, ok tuşları ve Home / End ile geçişe izin vermek için manuel giriş etkinliği işlemesi gerekir.

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

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

Yakın zamana kadar, bir öğe ayrıştırıcı tarafından yükseltildiğinde slotchange etkinlikleri tetiklenmedi. Bu nedenle, öğe işleyiciyi manuel olarak çağırır. Yeni davranış tüm tarayıcılara ulaştığında aşağıdaki kod kaldırılabilir.

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

disconnectedCallback(), connectedCallback() tarafından eklenen etkinlik işleyicileri kaldırır.

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

Gölge DOM yuvalarından birine bir öğe eklendiğinde veya kaldırıldığında _onSlotChange() çağrılır.

    _onSlotChange() {
      this._linkPanels();
    }

_linkPanels(), aria denetimlerini ve aria-labelledby özelliğini kullanarak sekmeleri bitişik panellerle bağlar. Ayrıca bu yöntemle yalnızca bir sekmenin etkin olmasını sağlar.

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

Her bir panele, kendisini kontrol eden sekmeyi belirten bir aria-labelledby özelliği verin.

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

Öğe, sekmelerden herhangi birinin seçili olarak işaretlenip işaretlenmediğini kontrol eder. Seçili değilse ilk sekme seçilidir.

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

Ardından, seçilen sekmeye geçin. _selectTab(), diğer tüm sekmeleri seçili değil olarak işaretleyip diğer tüm panelleri gizler.

      this._selectTab(selectedTab);
    }

_allPanels(), sekme panelindeki tüm panelleri döndürür. DOM sorguları bir performans sorunu haline gelirse bu işlev sonucu ezberleyebilir. Ezberlemenin olumsuz tarafı, dinamik olarak eklenen sekmelerin ve panellerin işlenmemesidir.

Bu bir alıcı değil, bir yöntemdir, çünkü alıcı, kitabın okunmasının ucuz olduğunu ima eder.

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

_allTabs(), sekme panelindeki tüm sekmeleri döndürür.

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

_panelForTab(), belirtilen sekmenin kontrol ettiği paneli döndürür.

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

_prevTab(), seçilen sekmeden önce gelen sekmeyi döndürür ve ilk sekmeye ulaşıldığında sarmalanır.

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

Geçerli olarak seçili öğenin dizinini bulmak için findIndex() işlevini kullanın ve bir önceki öğenin dizinini elde etmek için bir çıkarma işlemi yapın.

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

Dizinin pozitif bir sayı olduğundan emin olmak için tabs.length ekleyin ve gerekirse modülü ekleyin.

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

_firstTab() ilk sekmeyi döndürür.

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

_lastTab() son sekmeyi döndürür.

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

_nextTab(), seçilen sekmeden sonra gelen sekmeyi alır ve son sekmeye gelindiğinde kaydırılır.

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

reset(), tüm sekmeleri "seçilmemiş" olarak işaretler ve tüm panelleri gizler.

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

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

_selectTab(), ilgili sekmeyi seçili olarak işaretler. Ayrıca, ilgili sekmeye karşılık gelen paneli de gösterir.

    _selectTab(newTab) {

Tüm sekmelerin seçimini kaldırın ve tüm panelleri gizleyin.

      this.reset();

newTab öğesinin ilişkili olduğu paneli alın.

      const newPanel = this._panelForTab(newTab);

Bu panel yoksa iptal edin.

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

_onKeyDown(), sekme panelindeki tuşlara basma işlemlerini gerçekleştirir.

    _onKeyDown(event) {

Tuşa basma bir sekme öğesinin kendisinden gelmediyse panel içinde veya boş bir alanda tuşa basma işlemidir. Yapılması gereken bir şey yok.

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

Genellikle yardımcı teknolojiler tarafından kullanılan değiştirici kısayolları kullanmayın.

      if (event.altKey)
        return;

Büyük/küçük harf düzeni, basılan tuşa bağlı olarak hangi sekmenin etkin olarak işaretlenmesi gerektiğini belirler.

      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;

Diğer tuşlara basmalar yoksayılır ve tarayıcıya geri döner.

        default:
          return;
      }

Tarayıcının ok tuşlarına (ana sayfa veya son) bağlı bazı yerel işlevleri olabilir. Öğe, tarayıcının herhangi bir işlem yapmasını önlemek için preventDefault() öğesini çağırır.

      event.preventDefault();

Geçiş durumunda belirlenen yeni sekmeyi seçin.

      this._selectTab(newTab);
    }

_onClick(), sekme panelindeki tıklamaları işler.

    _onClick(event) {

Tıklama, bir sekme öğesinin kendisine hedeflenmemişse bir panel içinde veya boş alanda yapılan bir tıklamadır. Yapılması gereken bir şey yok.

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

Ancak bir sekme öğesinde yer alıyorsa bu sekmeyi seçin.

      this._selectTab(event.target);
    }
  }

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

howtoTabCounter, oluşturulan <howto-tab> örneğin sayısını sayar. Bu sayı, yeni ve benzersiz kimlikler oluşturmak için kullanılır.

  let howtoTabCounter = 0;

HowtoTab, <howto-tabs> sekme paneline ait bir sekmedir. JavaScript başarısız olduğunda anlamın kullanılabilir durumda kalması için <howto-tab> işaretlemede her zaman role="heading" ile kullanılmalıdır.

<howto-tab>, aria-controls özelliğinin değeri olarak ilgili panelin kimliğini kullanarak hangi <howto-panel> değerine ait olduğunu tanımlar.

Herhangi bir benzersiz kimlik belirtilmezse <howto-tab>, otomatik olarak benzersiz bir kimlik oluşturur.

  class HowtoTab extends HTMLElement {

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

    constructor() {
      super();
    }

    connectedCallback() {

Bu yürütülürse JavaScript çalışıyordur ve öğe, rolünü tab olarak değiştirir.

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

İyi tanımlanmış bir başlangıç durumu belirleyin.

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

Bir özelliğin örnek değeri olup olmadığını kontrol edin. Öyleyse değeri kopyalayın ve sınıf özelliği belirleyiciyi gölgelendirmemesi için örnek özelliğini silin. Son olarak, herhangi bir yan etkiyi tetikleyebilmesi için değeri sınıf özelliği belirleyiciye iletin. Bunun amacı, örneğin bir çerçevenin öğeyi sayfaya eklemiş ve özelliklerinden birinde değer ayarlamış olabileceği ancak tanımını geç yüklediği durumlara karşı koruma sağlamaktır. Bu koruma olmadan, yeni sürüme geçirilen öğe bu özelliği kaçırır ve örnek özelliği, sınıf özelliği belirleyicinin çağrılmasını engeller.

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

Özellikler ve karşılık gelen özellikler birbirini yansıtmalıdır. Bunun sonucunda, selected için özellik belirleyici, doğru/sahte değerleri işler ve bunları özelliğin durumuna yansıtır. Mülk belirleyicide hiçbir yan etki gerçekleşmediğini unutmamak önemlidir. Örneğin, belirleyici aria-selected özelliğini ayarlamaz. Bunun yerine, bu işlem attributeChangedCallback içinde gerçekleşir. Genel bir kural olarak, özellik belirleyicileri çok saçma yapın. Bir özellik veya özelliğin ayarlanması, bir yan etkiye (karşılık gelen bir ARIA özelliği ayarlamak gibi) neden olacaksa o özellik attributeChangedCallback() üzerinde işe yarar. Böylece karmaşık özellik/mülk yeniden girme senaryolarını yönetmek zorunda kalmazsınız.

    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, <howto-tabs> sekme paneli için bir paneldir.

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