Thành phần hướng dẫn – thẻ hướng dẫn

Tóm tắt

<howto-tabs> giới hạn nội dung hiển thị bằng cách chia nội dung thành nhiều bảng điều khiển. Mỗi lần chỉ hiển thị một bảng điều khiển, trong khi tất cả các thẻ tương ứng luôn hiển thị. Để chuyển từ bảng điều khiển này sang bảng điều khiển khác, bạn phải chọn thẻ tương ứng.

Bằng cách nhấp hoặc sử dụng các phím mũi tên, người dùng có thể thay đổi lựa chọn thẻ đang hoạt động.

Nếu JavaScript bị tắt, tất cả các bảng điều khiển sẽ hiển thị xen kẽ với các thẻ tương ứng. Các thẻ hiện hoạt động như tiêu đề.

Tài liệu tham khảo

Bản minh hoạ

Xem bản minh hoạ trực tiếp trên GitHub

Ví dụ về cách sử dụng

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

Nếu JavaScript không chạy, phần tử này sẽ không khớp với :defined. Trong trường hợp đó, kiểu này sẽ thêm khoảng cách giữa các thẻ và bảng điều khiển trước.

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

Xác định các mã phím để giúp xử lý các sự kiện liên quan đến bàn phím.

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

Để tránh gọi trình phân tích cú pháp bằng .innerHTML cho mọi thực thể mới, tất cả thực thể <howto-tabs> sẽ dùng chung một mẫu cho nội dung của DOM bóng.

  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 là một phần tử vùng chứa cho các thẻ và bảng điều khiển.

Tất cả phần tử con của <howto-tabs> phải là <howto-tab> hoặc <howto-tabpanel>. Phần tử này không có trạng thái, nghĩa là không có giá trị nào được lưu vào bộ nhớ đệm nên sẽ thay đổi trong thời gian chạy.

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

Bạn cần liên kết những trình xử lý sự kiện không đính kèm vào phần tử này nếu cần quyền truy cập vào this.

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

Để cải tiến tăng dần, mã đánh dấu phải xen kẽ giữa các thẻ và bảng điều khiển. Các phần tử sắp xếp lại phần tử con có xu hướng không hoạt động tốt với khung. Thay vào đó, DOM tối được dùng để sắp xếp lại các phần tử bằng cách sử dụng ô trống.

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

Nhập mẫu được chia sẻ để tạo các vùng cho thẻ và bảng điều khiển.

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

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

Phần tử này cần phản ứng với các thành phần con mới khi liên kết các thẻ và bảng điều khiển theo ngữ nghĩa bằng cách sử dụng aria-labelledbyaria-controls. Các thành phần con mới sẽ tự động được xếp vào vị trí và khiến quá trình thay đổi vị trí kích hoạt, vì vậy, bạn không cần phải có MutationObserver.

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

connectedCallback() nhóm các thẻ và bảng điều khiển bằng cách sắp xếp lại thứ tự và đảm bảo đúng một thẻ đang hoạt động.

    connectedCallback() {

Phần tử cần xử lý sự kiện nhập theo cách thủ công để cho phép chuyển đổi bằng các phím mũi tên và Home / End.

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

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

Cho đến gần đây, các sự kiện slotchange không kích hoạt khi một phần tử được trình phân tích cú pháp nâng cấp. Vì lý do này, phần tử này sẽ gọi trình xử lý theo cách thủ công. Sau khi hành vi mới chuyển đến tất cả trình duyệt, bạn có thể xoá mã bên dưới.

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

disconnectedCallback() sẽ xoá trình nghe sự kiện mà connectedCallback() đã thêm.

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

_onSlotChange() được gọi mỗi khi một phần tử được thêm vào hoặc bị xoá khỏi một trong các khe DOM tối.

    _onSlotChange() {
      this._linkPanels();
    }

_linkPanels() liên kết các thẻ với bảng điều khiển liền kề bằng các nút điều khiển aria và aria-labelledby. Ngoài ra, phương thức này đảm bảo chỉ có một thẻ đang hoạt động.

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

Cung cấp cho mỗi bảng điều khiển một thuộc tính aria-labelledby tham chiếu đến thẻ kiểm soát bảng điều khiển đó.

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

Phần tử này sẽ kiểm tra xem có thẻ nào được đánh dấu là đã chọn hay không. Nếu không, thẻ đầu tiên hiện đã được chọn.

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

Tiếp theo, hãy chuyển sang thẻ đã chọn. _selectTab() sẽ đánh dấu tất cả các thẻ khác là đã bỏ chọn và ẩn tất cả bảng điều khiển khác.

      this._selectTab(selectedTab);
    }

_allPanels() sẽ trả về tất cả bảng điều khiển trong bảng điều khiển thẻ. Hàm này có thể ghi nhớ kết quả nếu truy vấn DOM từng trở thành vấn đề về hiệu suất. Nhược điểm của tính năng ghi nhớ là các thẻ và bảng điều khiển được thêm tự động sẽ không được xử lý.

Đây là một phương thức chứ không phải phương thức getter vì phương thức getter ngụ ý rằng việc đọc sẽ rẻ.

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

_allTabs() trả về tất cả các thẻ trong bảng điều khiển thẻ.

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

_panelForTab() trả về bảng điều khiển mà thẻ đã cho điều khiển.

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

_prevTab() trả về thẻ xuất hiện trước thẻ hiện được chọn, bao bọc xung quanh khi đến thẻ đầu tiên.

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

Sử dụng findIndex() để tìm chỉ mục của phần tử đang được chọn rồi trừ đi 1 để lấy chỉ mục của phần tử trước đó.

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

Thêm tabs.length để đảm bảo chỉ mục là một số dương và lấy mô-đun để gói nếu cần.

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

_firstTab() trả về thẻ đầu tiên.

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

_lastTab() trả về thẻ cuối cùng.

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

_nextTab() nhận thẻ xuất hiện sau thẻ hiện được chọn, bao quanh khi chuyển đến thẻ cuối cùng.

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

reset() đánh dấu tất cả các thẻ là đã bỏ chọn và ẩn tất cả bảng điều khiển.

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

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

_selectTab() đánh dấu thẻ đã chọn là đã chọn. Ngoài ra, bảng này sẽ hiện bảng điều khiển tương ứng với thẻ nhất định.

    _selectTab(newTab) {

Bỏ chọn tất cả các thẻ và ẩn tất cả bảng điều khiển.

      this.reset();

Lấy bảng điều khiển liên kết với newTab.

      const newPanel = this._panelForTab(newTab);

Nếu không có bảng điều khiển đó, hãy huỷ.

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

_onKeyDown() xử lý các thao tác nhấn phím bên trong bảng điều khiển thẻ.

    _onKeyDown(event) {

Nếu thao tác nhấn phím không bắt nguồn từ một phần tử thẻ, thì đó là thao tác nhấn phím bên trong bảng điều khiển hoặc trên không gian trống. Bạn không cần làm gì cả.

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

Không xử lý các phím tắt đối tượng sửa đổi thường được công nghệ hỗ trợ sử dụng.

      if (event.altKey)
        return;

Trường hợp chuyển đổi sẽ xác định thẻ nào được đánh dấu là đang hoạt động, tuỳ thuộc vào phím đã nhấn.

      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;

Mọi thao tác nhấn phím khác sẽ bị bỏ qua và trả về trình duyệt.

        default:
          return;
      }

Trình duyệt có thể có một số chức năng gốc được liên kết với các phím mũi tên, trang chủ hoặc cuối. Phần tử này gọi preventDefault() để ngăn trình duyệt thực hiện bất kỳ hành động nào.

      event.preventDefault();

Chọn thẻ mới đã được xác định trong trường hợp chuyển đổi.

      this._selectTab(newTab);
    }

_onClick() xử lý các lượt nhấp bên trong bảng điều khiển thẻ.

    _onClick(event) {

Nếu không nhắm đến một phần tử thẻ, thì đó là lượt nhấp bên trong bảng điều khiển hoặc trên không gian trống. Bạn không cần làm gì cả.

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

Tuy nhiên, nếu thẻ này nằm trên phần tử thẻ, thì hãy chọn thẻ đó.

      this._selectTab(event.target);
    }
  }

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

howtoTabCounter đếm số lượng thực thể <howto-tab> được tạo. Số này được dùng để tạo mã nhận dạng mới, duy nhất.

  let howtoTabCounter = 0;

HowtoTab là một thẻ trong bảng điều khiển thẻ <howto-tabs>. Luôn sử dụng <howto-tab> cùng với role="heading" trong mã đánh dấu để vẫn sử dụng được ngữ nghĩa khi JavaScript không hoạt động.

<howto-tab> khai báo nó thuộc về <howto-panel> nào bằng cách sử dụng mã nhận dạng của bảng điều khiển đó làm giá trị cho thuộc tính kiểm soát aria.

<howto-tab> sẽ tự động tạo một mã nhận dạng duy nhất nếu không có mã nào được chỉ định.

  class HowtoTab extends HTMLElement {

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

    constructor() {
      super();
    }

    connectedCallback() {

Nếu được thực thi, thì JavaScript đang hoạt động và phần tử này sẽ thay đổi vai trò của nó thành tab.

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

Đặt trạng thái ban đầu được xác định rõ ràng.

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

Kiểm tra xem một thuộc tính có giá trị thực thể hay không. Nếu có, hãy sao chép giá trị và xoá thuộc tính thực thể để không đổ bóng phương thức setter thuộc tính lớp. Cuối cùng, hãy chuyển giá trị này cho phương thức setter thuộc tính của lớp để có thể kích hoạt bất kỳ hiệu ứng phụ nào. Việc này là để bảo vệ an toàn trong trường hợp một khung có thể đã thêm phần tử vào trang và đặt giá trị cho một trong các thuộc tính của nó, nhưng tải từng phần định nghĩa của nó. Nếu không có biện pháp bảo vệ này, phần tử đã nâng cấp sẽ bỏ lỡ thuộc tính đó và thuộc tính thực thể sẽ ngăn phương thức setter của thuộc tính lớp được gọi.

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

Các thuộc tính và thuộc tính tương ứng phải phản ánh lẫn nhau. Để có tác dụng này, phương thức setter thuộc tính cho selected xử lý các giá trị trung thực/giả và phản ánh các giá trị đó đến trạng thái của thuộc tính. Điều quan trọng cần lưu ý là không có hiệu ứng phụ nào diễn ra trong phương thức setter thuộc tính. Ví dụ: phương thức setter không thiết lập aria-selected. Thay vào đó, công việc đó sẽ diễn ra trong attributeChangedCallback. Theo quy tắc chung, việc thiết lập phương thức setter thuộc tính sẽ không hoạt động, và nếu việc thiết lập một thuộc tính hoặc thuộc tính có thể gây ra tác dụng phụ (chẳng hạn như đặt thuộc tính ARIA tương ứng), thì việc này sẽ diễn ra trong attributeChangedCallback(). Nhờ đó, bạn không phải quản lý các tình huống phức tạp liên quan đến việc khôi phục thuộc tính/thuộc tính.

    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 là bảng điều khiển dành cho bảng điều khiển thẻ <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);
})();