"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. Aynı anda yalnızca bir panel görünür. İlgili sekmelerin tümü ise her zaman görünür durumdadır. Bir panelden diğerine geçiş yapmak için ilgili sekmenin seçilmesi gerekir.

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

JavaScript devre dışıysa tüm paneller ilgili sekmelerle birlikte gösterilir. Sekmeler artık başlık olarak işlev görür.

Referans

Demo

Canlı demoyu GitHub'da görüntüleme

Ö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ışmıyorsa öğ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 örnek için ayrıştırıcıyı .innerHTML ile çağırmayı önlemek amacıyla, gölge DOM'un içeriği için bir şablon tüm <howto-tabs> örnekleri tarafından 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ı öğesidir.

<howto-tabs> öğesinin tüm alt öğeleri <howto-tab> veya <howto-tabpanel> olmalıdır. Bu öğe durum bilgisi içermez. Diğer bir deyişle, hiçbir değer önbelleğe alınmaz ve bu nedenle çalışma zamanında değişiklikler yapılır.

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

Bu öğeye eklenmemiş etkinlik işleyicilerin, this öğesine erişmesi 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 düzenleyen öğeler, çerçevelerle iyi çalışmaz. Bunun yerine, öğeleri aralıkları kullanarak yeniden sıralamak için gölge DOM kullanılır.

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

Sekme ve panel yuvaları oluşturmak için 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 çocuklar otomatik olarak alana eklenir ve slotchange 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 düzenleyerek gruplandırır ve tam olarak bir sekmenin etkin olduğundan emin olur.

    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 tetiklenmiyordu. Bu nedenle öğe, işleyiciyi manuel olarak çağırır. Yeni davranış tüm tarayıcılara uygulandıktan sonra 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 alanlarından birine öğe eklendiğinde veya kaldırıldığında _onSlotChange() çağrılır.

    _onSlotChange() {
      this._linkPanels();
    }

_linkPanels(), aria-controls 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 panele, onu 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çili sekmeye geçin. _selectTab(), diğer tüm sekmelerin seçimini kaldırır ve 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. Hatırlama yönteminin dezavantajı, dinamik olarak eklenen sekmelerin ve panellerin işlenmemesidir.

Geter, okumanın ucuz olduğunu ima ettiği için bu bir yöntemdir.

    _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;

Dizin pozitif bir sayı olduğundan emin olmak ve gerekirse modülün sarmalanması için tabs.length 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ç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 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ılacak bir şey yok.

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

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

      if (event.altKey)
        return;

Switch-case, 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ş basımları yoksayılır ve tarayıcıya geri iletilir.

        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ı engellemek için preventDefault() işlevini çağırır.

      event.preventDefault();

Switch-case'te belirlenen yeni sekmeyi seçin.

      this._selectTab(newTab);
    }

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

    _onClick(event) {

Tıklama, sekme öğesinin kendisinde hedeflenmemişse panel içinde veya boş alanda bir tıklamadır. Yapılacak bir şey yok.

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

Ancak, sekme öğesindeyse ilgili sekmeyi seçin.

      this._selectTab(event.target);
    }
  }

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

howtoTabCounter, oluşturulan <howto-tab> örneklerinin 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'te hata oluştuğunda anlamların kullanılabilir kalması için <howto-tab>, işaretlemede her zaman role="heading" ile birlikte kullanılmalıdır.

<howto-tab>, aria-controls özelliğinin değeri olarak ilgili panelin kimliğini kullanarak hangi <howto-panel>'ye ait olduğunu belirtir.

<howto-tab>, belirtilmemişse 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ışıyordur ve öğenin rolü tab olarak değişir.

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

İyi tanımlanmış bir ilk durum ayarlayın.

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

Bir özelliğin örnek değeri olup olmadığını kontrol edin. Bu durumda, sınıf özelliği ayarlayıcısını gölgelememesi için değeri kopyalayın ve örnek özelliğini silin. Son olarak, herhangi bir yan etkiyi tetikleyebilmesi için değeri sınıf mülk ayarlayıcısına 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, yükseltilen öğe bu mülkü kaçırırdı ve örnek mülkü, sınıf mülk ayarlayıcısının çağrılmasını engellerdi.

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

Tesisler ve ilgili özellikleri birbirini yansıtmalıdır. Bu nedenle, 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ıda herhangi bir yan etki olmadığını unutmayın. Örneğin, ayarlayıcı aria-selected değerini ayarlamaz. Bunun yerine, bu işlem attributeChangedCallback'te 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. Bu sayede, 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);
})();