مكونات طريقة التنفيذ - علامات تبويب كيفية التنفيذ

ملخّص

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

لتجنُّب استدعاء المُحلِّل باستخدام .innerHTML لكلّ مثيل جديد، تتم مشاركة نموذج لمحتوى shadow DOM مع جميع نُسخ <howto-tabs>.

  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() أدوات معالجة الأحداث التي أضافها connectedCallback().

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

يتمّ استدعاء _onSlotChange() عند إضافة عنصر أو إزالته من إحدى خانات 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() للعثور على فهرس العنصر المحدّد حاليًا وطرح واحد للحصول على فهرس العنصر السابق.

      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;

سيحدِّد مفتاح التبديل علامة التبويب التي يجب وضع علامة عليها كعلامة نشطة استنادًا إلى المفتاح الذي تم الضغط عليه.

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

اختَر علامة التبويب الجديدة التي تم تحديدها في حالة التبديل.

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

تحقّق مما إذا كان السمة لها قيمة مثيل. إذا كان الأمر كذلك، انسخ القيمة وأزِل سمة العنصر لكي لا تحجب أداة ضبط سمة الفئة. أخيرًا، نقْل القيمة إلى أداة ضبط سمة الفئة حتى تتمكّن من بدء أيّ آثار جانبية. ويهدف ذلك إلى تجنّب الحالات التي قد يضيف فيها إطار عمل العنصر إلى الصفحة ويضبط قيمة على إحدى خصائصه، ولكن يتم تحميل تعريفه بشكلٍ بطيء. بدون هذا الإجراء الوقائي، سيفقد العنصر الذي تمت ترقيته هذه السمة وستمنع سمة العنصر من استدعاء طريقة ضبط سمة الفئة.

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