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

ملخّص

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

سيحدِّد العنصر 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();

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

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