"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 ayırarak sınırlandırır. 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çmek için ilgili sekme seçilmelidir.

Kullanıcı, tıklayarak veya ok tuşlarını kullanarak etkin sekme seçimini değiştirebilir.

JavaScript devre dışı bırakılırsa tüm paneller, ilgili sekmelerle aralıklı olarak gösterilir. Sekmeler artık başlıklar olarak işlev görüyor.

Referans

Demo

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

Ö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 ve ö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ı olacak tuş kodlarını tanımlayın.

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

Her yeni örnekte ayrıştırıcının .innerHTML ile çağrılmasını önlemek amacıyla gölge DOM'un içerik şablonu tüm <howto-tabs> örneklerinde 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. Diğer bir deyişle, hiçbir değer önbelleğe alınmaz ve bu nedenle, çalışma zamanı sırasında değişir.

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

Bu öğeye ekli olmayan etkinlik işleyicilerin this ürününe erişmesi gerekiyorsa bağlı olmaları gerekir.

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

Progresif geliştirme için, işaretleme sekmeler ve paneller arasında değişimli olmalıdır. Alt öğelerini yeniden sıralayan öğeler çerçevelerle pek iyi sonuç vermeyebilir. Bunun yerine, öğeleri alanlar kullanarak 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 öğe, aria-labelledby ve aria-controls kullanarak sekmeler ile panelleri anlamsal olarak bağladığından yeni alt öğelere tepki vermelidir. Yeni alt öğeler otomatik olarak yuvaya yerleştirilir ve slotchange'in etkinleşmesine neden olur. Bu nedenle MutationObserver gerekli değildir.

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

connectedCallback(), yeniden sıralayarak sekmeleri ve panelleri 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 olanak tanımak için bazı manuel giriş etkinliği işlemesi yapması 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 yeni sürüme geçirildiğ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 bu alandan bir öğe kaldırıldığında _onSlotChange() çağrılır.

    _onSlotChange() {
      this._linkPanels();
    }

_linkPanels(), aria kontrolleri ve aria-labelledby kullanarak sekmeleri bitişik panelleriyle bağlar. Ayrıca bu yöntem 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 sekmelerin seçimini kaldırılmış olarak işaretlenmesini ve diğer tüm panelleri gizlemesini sağlar.

      this._selectTab(selectedTab);
    }

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

Bu, alıcı değil, yöntemidir. Çünkü alıcı, içeriğin 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çili olandan önce gelen sekmeyi döndürür ve ilk sekmeye ulaşıldığında sarmalanır.

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

Şu anda seçili olan öğenin dizinini bulmak için findIndex() operatörünü kullanın ve önceki öğenin dizinini elde etmek için bir çıkarma işlemi gerçekleştirin.

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

Endeksin pozitif bir sayı olduğundan emin olmak için tabs.length ekleyin ve gerekirse etrafında sarmalama modülünü kullanın.

      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(), o an seçili olandan sonra gelen sekmeyi alır ve son sekmeye ulaşıldığında sarmalanır.

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

reset() tüm sekmeleri seçili değil 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(), belirtilen 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 yönetir.

    _onKeyDown(event) {

Tuşa basma işlemi sekme öğesinin kendisinden kaynaklanmıyorsa panel içinde veya boş alanda yapılan bir tuşa basılmış demektir. 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;

Anahtar kılıfı, basılan anahtara 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;

Basılan diğer tuşlar yoksayılır ve tarayıcıya geri verilir.

        default:
          return;
      }

Tarayıcının ok tuşlarına, home veya end tuşlarına bağlı bazı yerel işlevleri olabilir. Öğe, tarayıcının herhangi bir işlem yapmasını engellemek için preventDefault() işlevini çağırır.

      event.preventDefault();

Anahtar kutusunda tespit edilen yeni sekmeyi seçin.

      this._selectTab(newTab);
    }

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

    _onClick(event) {

Tıklama, bir sekme öğesinin kendisine hedeflenmemişse bu, bir panelin 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 öğesindeyse o sekmeyi seçin.

      this._selectTab(event.target);
    }
  }

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

howtoTabCounter, oluşturulan <howto-tab> örneğin sayısını sayar. Bu numara, 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 anlamların kullanılabilir 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> öğesine ait olduğunu tanımlar.

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

  class HowtoTab extends HTMLElement {

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

    constructor() {
      super();
    }

    connectedCallback() {

Bu komut yürütülürse JavaScript çalışır ve öğenin 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. Kullanılıyorsa değeri kopyalayın ve sınıf özelliği ayarlayıcısını gölgelendirmemesi için örnek özelliğini silin. Son olarak, değeri sınıf özelliği ayarlayıcısına iletin. Böylece yan etkiler tetiklenebilir. Bu, örneğin bir çerçevenin öğeyi sayfaya ekleyip özelliklerinden birinde bir değer belirlemesine rağmen tanımının geç yüklenmesi gibi durumlara karşı koruma sağlamak için yapılır. Bu koruma olmadan yükseltilmiş öğe bu özelliği atlar ve örnek özelliği, sınıf özelliği ayarlayıcının hiçbir zaman çağrılmasını önler.

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

Özellikler ve karşılık gelen özellikleri birbirini yansıtmalıdır. Dolayısıyla selected için özellik ayarlayıcı, doğru/yanlış değerleri işler ve bunları özelliğin durumuna yansıtır. Mülk ayarlayıcının herhangi bir yan etkisinin olmadığını unutmayın. Örneğin, setter aria-selected ayarını yapmaz. Bunun yerine bu işlem attributeChangedCallback içinde gerçekleşir. Genel bir kural olarak, özellik belirleyicileri çok geçersiz hale getirin. Bir özellik veya özelliğin ayarlanması, yan etkiye neden olacaksa (karşılık gelen bir ARIA özelliğini ayarlamak gibi) işlemi attributeChangedCallback() üzerinde yapın. Böylece karmaşık özellik/mülk yeniden giriş 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);
})();