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

ملخّص

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

هذه طريقة وليست دالة getter، لأن أداة الحصول على القيمة تشير إلى أنها منخفضة التكلفة في القراءة.

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

إذا لم تنشأ الضغطة على المفتاح من عنصر Tab نفسه، فقد كانت ضغطة مفتاح داخل اللوحة أو على مساحة فارغة. لا يلزم اتخاذ أي إجراء.

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

قد يحتوي المتصفّح على بعض الوظائف الأصلية المرتبطة بمفاتيح الأسهم أو الشاشة الرئيسية أو النهاية. يطلب العنصر 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');
    }

تحقّق ممّا إذا كان للموقع الإلكتروني قيمة مثيل. إذا كان الأمر كذلك، انسخ القيمة، واحذف خاصية المثيل بحيث لا تظل تُستخدم على محدد خاصية الفئة. وأخيرًا، قم بتمرير القيمة إلى أداة ضبط خاصية الفئة بحيث يمكن أن تؤدي إلى ظهور أي آثار جانبية. ويهدف هذا الإجراء إلى توفير الحماية من الحالات التي قد يضيف فيها إطار العمل مثلاً العنصر إلى الصفحة ويضبط قيمة على إحدى خصائصه، ولكنّه حمَّل تعريفه الكسول. بدون هذا الحماية، لن يتمكن العنصر الذي تمت ترقيته من الوصول إلى تلك الخاصية، وستمنع خاصية المثيل من استدعاء دالة set خاصية الفئة.

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

يجب أن تتطابق المواقع والسمات المرتبطة بها مع بعضها. لهذا السبب، تتعامل دالة setter بشأن selected مع قيم الحقيقة/الخطأ وتعكس هذه القيم إلى حالة السمة. من المهم ملاحظة عدم حدوث أي آثار جانبية في أداة تحديد الموقع. على سبيل المثال، لا تضبط دالة setter السمة aria-selected. بدلاً من ذلك، يتم تنفيذ هذا الإجراء في "attributeChangedCallback". كقاعدة عامة، يجب أن تكون أدوات تحديد السمات غبيًا جدًا. وفي حال ضبط سمة أو سمة، من المفترض أن يؤدي ضبط سمة ARIA إلى تأثير جانبي (مثل ضبط سمة 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);
})();