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

ملخّص

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

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

يعمل connectedCallback() على تجميع علامات التبويب واللوحات عن طريق إعادة ترتيبها والتأكّد من نشاط علامة تبويب واحدة فقط.

    connectedCallback() {

يحتاج العنصر إلى إجراء بعض المعالجة اليدوية لأحداث الإدخال للسماح بالتبديل باستخدام مفاتيح الأسهم وزر الشاشة الرئيسية / إنهاء.

      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 و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، حيث تشير كلمة 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) {

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

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

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

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

يجب أن تتطابق الخصائص والسمات المقابلة لها مع بعضها. لهذا السبب، تعالج سمة الضبط في selected القيم الصادقة/المزيّفة وتعكس هذه القيم إلى حالة السمة. تجدر الإشارة إلى عدم حدوث أي آثار جانبية في أداة تحديد الخاصية. على سبيل المثال، لا تُضبط دالة setter على 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);
})();