สรุป
<howto-tabs> จำกัดเนื้อหาที่มองเห็นได้โดยแยกออกเป็นหลายแผง โดยจะเห็นแผงได้เพียงแผงเดียวในแต่ละครั้ง แต่แท็บที่เกี่ยวข้องทั้งหมดจะปรากฏอยู่เสมอ หากต้องการเปลี่ยนจากแผงหนึ่งไปยังอีกแผงหนึ่ง คุณต้องเลือกแท็บที่เกี่ยวข้อง
ผู้ใช้สามารถเปลี่ยนการเลือกแท็บที่ใช้งานอยู่ด้วยการคลิกหรือใช้แป้นลูกศร
หากปิดใช้ JavaScript แผงทั้งหมดจะแสดงสลับกับแท็บที่เกี่ยวข้อง ตอนนี้แท็บจะทําหน้าที่เป็นส่วนหัว
ข้อมูลอ้างอิง
สาธิต
ดูการสาธิตการใช้งานแบบสดบน GitHub
ตัวอย่างการใช้
<style>
  howto-tab {
    border: 1px solid black;
    padding: 20px;
  }
  howto-panel {
    padding: 20px;
    background-color: lightgray;
  }
  howto-tab[selected] {
    background-color: bisque;
  }
หาก JavaScript ไม่ทํางาน องค์ประกอบจะไม่ตรงกับ :defined ในกรณีนี้ สไตล์นี้จะเพิ่มระยะห่างระหว่างแท็บกับแผงก่อนหน้า
  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>
รหัส
(function() {
กําหนดคีย์โค้ดเพื่อช่วยจัดการเหตุการณ์แป้นพิมพ์
  const KEYCODE = {
    DOWN: 40,
    LEFT: 37,
    RIGHT: 39,
    UP: 38,
    HOME: 36,
    END: 35,
  };
อินสแตนซ์ <howto-tabs> ทั้งหมดจะแชร์เทมเพลตสำหรับเนื้อหาของ Shadow DOM เพื่อหลีกเลี่ยงการเรียกใช้โปรแกรมแยกวิเคราะห์ด้วย .innerHTML สำหรับอินสแตนซ์ใหม่ทุกรายการ
  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 เป็นองค์ประกอบคอนเทนเนอร์สำหรับแท็บและแผง
รายการย่อยทั้งหมดของ <howto-tabs> ควรเป็น <howto-tab> หรือ <howto-tabpanel> องค์ประกอบนี้เป็นแบบไม่มีสถานะ ซึ่งหมายความว่าไม่มีการแคชค่าใดๆ ดังนั้นจึงมีการเปลี่ยนแปลงระหว่างรันไทม์
  class HowtoTabs extends HTMLElement {
    constructor() {
      super();
ตัวแฮนเดิลเหตุการณ์ที่ไม่ได้แนบอยู่กับองค์ประกอบนี้ต้องได้รับการเชื่อมโยงหากต้องการเข้าถึง this
      this._onSlotChange = this._onSlotChange.bind(this);
สำหรับการเพิ่มประสิทธิภาพแบบเป็นขั้นเป็นตอน มาร์กอัปควรสลับระหว่างแท็บกับแผง องค์ประกอบที่เรียงลําดับองค์ประกอบย่อยใหม่มักจะทํางานร่วมกับเฟรมเวิร์กได้ไม่ดี แต่จะใช้ Shadow DOM เพื่อจัดเรียงองค์ประกอบใหม่โดยใช้ช่องแทน
      this.attachShadow({ mode: 'open' });
นําเข้าเทมเพลตที่แชร์เพื่อสร้างช่องสําหรับแท็บและแผง
      this.shadowRoot.appendChild(template.content.cloneNode(true));
      this._tabSlot = this.shadowRoot.querySelector('slot[name=tab]');
      this._panelSlot = this.shadowRoot.querySelector('slot[name=panel]');
องค์ประกอบนี้ต้องตอบสนองต่อองค์ประกอบย่อยใหม่เนื่องจากลิงก์แท็บและแผงโดยใช้ aria-labelledby และ aria-controls ระบบจะจัดสรรช่องใหม่โดยอัตโนมัติและทริกเกอร์ slotchange จึงไม่จำเป็นต้องใช้ MutationObserver
      this._tabSlot.addEventListener('slotchange', this._onSlotChange);
      this._panelSlot.addEventListener('slotchange', this._onSlotChange);
    }
connectedCallback() จัดกลุ่มแท็บและแผงโดยจัดเรียงใหม่และตรวจสอบว่ามีแท็บที่ใช้งานอยู่เพียงแท็บเดียว
    connectedCallback() {
เอลิเมนต์ต้องจัดการเหตุการณ์อินพุตด้วยตนเองเพื่อให้สลับด้วยแป้นลูกศรและ Home / End ได้
      this.addEventListener('keydown', this._onKeyDown);
      this.addEventListener('click', this._onClick);
      if (!this.hasAttribute('role'))
        this.setAttribute('role', 'tablist');
ก่อนหน้านี้ เหตุการณ์ slotchange จะไม่ทริกเกอร์เมื่อองค์ประกอบได้รับการอัปเกรดโดยโปรแกรมแยกวิเคราะห์ ด้วยเหตุนี้ องค์ประกอบจึงเรียกใช้ตัวแฮนเดิลด้วยตนเอง เมื่อลักษณะการทํางานใหม่ใช้งานได้ในเบราว์เซอร์ทั้งหมดแล้ว คุณสามารถนำโค้ดด้านล่างออกได้
      Promise.all([
        customElements.whenDefined('howto-tab'),
        customElements.whenDefined('howto-panel'),
      ])
        .then(() => this._linkPanels());
    }
disconnectedCallback() นำ Listener เหตุการณ์ที่ connectedCallback() เพิ่มออก
    disconnectedCallback() {
      this.removeEventListener('keydown', this._onKeyDown);
      this.removeEventListener('click', this._onClick);
    }
_onSlotChange() จะเรียกใช้ทุกครั้งที่มีการเพิ่มหรือนําองค์ประกอบออกจากช่อง Shadow DOM รายการใดรายการหนึ่ง
    _onSlotChange() {
      this._linkPanels();
    }
_linkPanels() จะลิงก์แท็บกับแผงที่อยู่ติดกันโดยใช้ aria-controls และ aria-labelledby นอกจากนี้ วิธีการนี้จะตรวจสอบว่ามีเพียงแท็บเดียวที่ใช้งานอยู่
    _linkPanels() {
      const tabs = this._allTabs();
ระบุแอตทริบิวต์ aria-labelledby ให้กับแต่ละแผงซึ่งอ้างอิงถึงแท็บที่ควบคุมแผงนั้น
      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);
      });
องค์ประกอบจะตรวจสอบว่ามีการทําเครื่องหมายแท็บใดเป็น "เลือกแล้ว" หรือไม่ หากไม่ได้เลือกไว้ ระบบจะเลือกแท็บแรกให้
      const selectedTab =
        tabs.find((tab) => tab.selected) || tabs[0];
จากนั้นเปลี่ยนไปใช้แท็บที่เลือก _selectTab() จะทําเครื่องหมายแท็บอื่นๆ ทั้งหมดเป็นยกเลิกการเลือกและซ่อนแผงอื่นๆ ทั้งหมด
      this._selectTab(selectedTab);
    }
_allPanels() แสดงแผงทั้งหมดในแผงแท็บ ฟังก์ชันนี้จะจดจำผลลัพธ์ไว้ได้หากการค้นหา DOM กลายเป็นปัญหาด้านประสิทธิภาพ ข้อเสียของการจดจําคือระบบจะไม่จัดการแท็บและแผงใหม่ที่เพิ่มแบบไดนามิก
นี่เป็นเมธอด ไม่ใช่ตัวรับค่า เนื่องจากตัวรับค่าจะบอกเป็นนัยว่าอ่านได้ง่าย
    _allPanels() {
      return Array.from(this.querySelectorAll('howto-panel'));
    }
_allTabs() แสดงแท็บทั้งหมดในแผงแท็บ
    _allTabs() {
      return Array.from(this.querySelectorAll('howto-tab'));
    }
_panelForTab() จะแสดงผลแผงที่แท็บที่ระบุควบคุม
    _panelForTab(tab) {
      const panelId = tab.getAttribute('aria-controls');
      return this.querySelector(`#${panelId}`);
    }
_prevTab() จะแสดงแท็บที่อยู่ก่อนหน้าแท็บที่เลือกอยู่ในปัจจุบัน โดยระบบจะวนกลับไปที่แท็บแรกเมื่อถึงแท็บสุดท้าย
    _prevTab() {
      const tabs = this._allTabs();
ใช้ findIndex() เพื่อค้นหาดัชนีขององค์ประกอบที่เลือกอยู่ในปัจจุบัน และลบ 1 เพื่อดูดัชนีขององค์ประกอบก่อนหน้า
      let newIdx = tabs.findIndex((tab) => tab.selected) - 1;
เพิ่ม tabs.length เพื่อให้แน่ใจว่าดัชนีเป็นจํานวนบวกและรับค่าเศษทศนิยมเพื่อตัดเศษหากจําเป็น
      return tabs[(newIdx + tabs.length) % tabs.length];
    }
_firstTab() แสดงแท็บแรก
    _firstTab() {
      const tabs = this._allTabs();
      return tabs[0];
    }
_lastTab() แสดงแท็บล่าสุด
    _lastTab() {
      const tabs = this._allTabs();
      return tabs[tabs.length - 1];
    }
_nextTab() รับแท็บที่อยู่หลังแท็บที่เลือกอยู่ในปัจจุบัน โดยระบบจะวนกลับไปที่แท็บแรกเมื่อถึงแท็บสุดท้าย
    _nextTab() {
      const tabs = this._allTabs();
      let newIdx = tabs.findIndex((tab) => tab.selected) + 1;
      return tabs[newIdx % tabs.length];
    }
reset() จะยกเลิกการเลือกแท็บทั้งหมดและซ่อนแผงทั้งหมด
    reset() {
      const tabs = this._allTabs();
      const panels = this._allPanels();
      tabs.forEach((tab) => tab.selected = false);
      panels.forEach((panel) => panel.hidden = true);
    }
_selectTab() ทำเครื่องหมายแท็บที่ระบุว่าเลือกไว้ นอกจากนี้ ยังเลิกซ่อนแผงที่เกี่ยวข้องกับแท็บนั้นด้วย
    _selectTab(newTab) {
ยกเลิกการเลือกแท็บทั้งหมดและซ่อนแผงทั้งหมด
      this.reset();
รับแผงที่ newTab เชื่อมโยงอยู่
      const newPanel = this._panelForTab(newTab);
หากไม่มีแผงดังกล่าว ให้ยกเลิก
      if (!newPanel)
        throw new Error(`No panel with id ${newPanelId}`);
      newTab.selected = true;
      newPanel.hidden = false;
      newTab.focus();
    }
_onKeyDown() จัดการการกดแป้นพิมพ์ภายในแผงแท็บ
    _onKeyDown(event) {
หากการกดแป้นไม่ได้มาจากองค์ประกอบแท็บ แสดงว่าเป็นการกดแป้นภายในแผงหรือพื้นที่ว่าง ไม่ต้องดำเนินการใดๆ
      if (event.target.getAttribute('role') !== 'tab')
        return;
อย่าจัดการแป้นพิมพ์ลัดตัวแก้ไขที่มักใช้โดยเทคโนโลยีความช่วยเหลือพิเศษ
      if (event.altKey)
        return;
Switch-case จะกำหนดว่าควรทำเครื่องหมายแท็บใดว่าใช้งานอยู่โดยขึ้นอยู่กับแป้นที่กด
      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;
ระบบจะไม่สนใจการกดแป้นอื่นๆ และส่งกลับไปยังเบราว์เซอร์
        default:
          return;
      }
เบราว์เซอร์อาจมีฟังก์ชันการทำงานบางอย่างที่เชื่อมโยงกับแป้นลูกศร แป้น Home หรือแป้น End องค์ประกอบเรียก preventDefault() เพื่อป้องกันไม่ให้เบราว์เซอร์ดำเนินการใดๆ
      event.preventDefault();
เลือกแท็บใหม่ที่กําหนดไว้ในเงื่อนไข if-else
      this._selectTab(newTab);
    }
_onClick() จัดการการคลิกภายในแผงแท็บ
    _onClick(event) {
หากการคลิกไม่ได้กำหนดเป้าหมายไปยังองค์ประกอบแท็บ แสดงว่าเป็นการคลิกภายในแผงหรือพื้นที่ว่าง ไม่ต้องดำเนินการใดๆ
      if (event.target.getAttribute('role') !== 'tab')
        return;
แต่หากอยู่ในองค์ประกอบแท็บ ให้เลือกแท็บนั้น
      this._selectTab(event.target);
    }
  }
  customElements.define('howto-tabs', HowtoTabs);
howtoTabCounter นับจํานวนอินสแตนซ์ <howto-tab> ที่สร้างขึ้น ระบบจะใช้หมายเลขดังกล่าวเพื่อสร้างรหัสใหม่ที่ไม่ซ้ำกัน
  let howtoTabCounter = 0;
HowtoTab คือแท็บสำหรับแผงแท็บ <howto-tabs> ควรใช้ <howto-tab> ร่วมกับ role="heading" ในมาร์กอัปเสมอเพื่อให้ใช้ความหมายได้อยู่เมื่อ JavaScript ไม่ทำงาน
<howto-tab> จะประกาศว่า <howto-panel> ใดเป็นของตนโดยใช้รหัสของแผงนั้นเป็นค่าสำหรับแอตทริบิวต์ aria-controls
<howto-tab> จะสร้างรหัสที่ไม่ซ้ำกันโดยอัตโนมัติหากไม่ได้ระบุ
  class HowtoTab extends HTMLElement {
    static get observedAttributes() {
      return ['selected'];
    }
    constructor() {
      super();
    }
    connectedCallback() {
หากการดําเนินการนี้ทํางานได้ แสดงว่า JavaScript ทํางานอยู่และองค์ประกอบจะเปลี่ยนบทบาทเป็น tab
      this.setAttribute('role', 'tab');
      if (!this.id)
        this.id = `howto-tab-generated-${howtoTabCounter++}`;
ตั้งค่าสถานะเริ่มต้นที่ชัดเจน
      this.setAttribute('aria-selected', 'false');
      this.setAttribute('tabindex', -1);
      this._upgradeProperty('selected');
    }
ตรวจสอบว่าพร็อพเพอร์ตี้มีค่าอินสแตนซ์หรือไม่ หากใช่ ให้คัดลอกค่าและลบพร็อพเพอร์ตี้อินสแตนซ์ออกเพื่อไม่ให้บดบังตัวตั้งค่าพร็อพเพอร์ตี้คลาส สุดท้าย ให้ส่งค่าไปยังตัวตั้งค่าพร็อพเพอร์ตี้ของคลาสเพื่อให้ทริกเกอร์ผลข้างเคียงได้ การดำเนินการนี้เป็นการปกป้องในกรณีที่เฟรมเวิร์กอาจเพิ่มองค์ประกอบลงในหน้าเว็บและตั้งค่าในพร็อพเพอร์ตี้รายการใดรายการหนึ่ง แต่โหลดคําจํากัดความแบบ Lazy หากไม่มีเงื่อนไขนี้ องค์ประกอบที่อัปเกรดจะไม่มีพร็อพเพอร์ตี้นั้น และพร็อพเพอร์ตี้อินสแตนซ์จะป้องกันไม่ให้เรียกตัวตั้งค่าพร็อพเพอร์ตี้คลาส
    _upgradeProperty(prop) {
      if (this.hasOwnProperty(prop)) {
        let value = this[prop];
        delete this[prop];
        this[prop] = value;
      }
    }
ที่พักและแอตทริบิวต์ที่เกี่ยวข้องควรตรงกัน ด้วยเหตุนี้ ตัวตั้งค่าพร็อพเพอร์ตี้สำหรับ selected จึงจัดการค่าที่เป็นจริง/เท็จและแสดงค่าเหล่านั้นในสถานะของแอตทริบิวต์ โปรดทราบว่าจะไม่มีผลข้างเคียงเกิดขึ้นในเครื่องกําหนดพร็อพเพอร์ตี้ เช่น ตัวตั้งค่าไม่ได้ตั้งค่า aria-selected แต่งานดังกล่าวจะเกิดขึ้นใน attributeChangedCallback โดยทั่วไปแล้ว ให้ตั้งค่าตัวเซ็ตพร็อพเพอร์ตี้ให้ทำงานแบบโง่ๆ และหากการตั้งค่าพร็อพเพอร์ตี้หรือแอตทริบิวต์ควรทำให้เกิดผลข้างเคียง (เช่น การตั้งค่าแอตทริบิวต์ ARIA ที่เกี่ยวข้อง) ให้ทํางานนั้นใน attributeChangedCallback() วิธีนี้จะช่วยหลีกเลี่ยงการจัดการสถานการณ์การกลับมาของแอตทริบิวต์/พร็อพเพอร์ตี้ที่ซับซ้อน
    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>
  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);
})();
 
 
        
        