HowTo Components – howto-tabs

خلاصه

<howto-tabs> محتوای قابل مشاهده را با جدا کردن آن در چند پانل محدود می کند. فقط یک پانل در یک زمان قابل مشاهده است، در حالی که همه برگه های مربوطه همیشه قابل مشاهده هستند. برای جابجایی از یک پانل به پانل دیگر، باید برگه مربوطه را انتخاب کنید.

با کلیک کردن یا با استفاده از کلیدهای جهت دار، کاربر می تواند انتخاب برگه فعال را تغییر دهد.

اگر جاوا اسکریپت غیرفعال باشد، همه پانل ها به صورت درهم با برگه های مربوطه نشان داده می شوند. اکنون برگه ها به عنوان سرفصل عمل می کنند.

ارجاع

نسخه ی نمایشی

نمایش دمو زنده در GitHub

مثال استفاده

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

اگر جاوا اسکریپت اجرا نشود، عنصر با :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 برای هر نمونه جدید، یک الگو برای محتویات 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);

برای بهبود تدریجی، نشانه گذاری باید بین برگه ها و پانل ها به طور متناوب باشد. عناصری که فرزندان خود را مجدداً مرتب می کنند، معمولاً با چارچوب ها کار نمی کنند. در عوض از سایه 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() {

این عنصر باید مدیریت رویدادهای ورودی دستی را انجام دهد تا امکان جابجایی با کلیدهای جهت‌نما و صفحه اصلی / پایان را فراهم کند.

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

مرورگر ممکن است دارای برخی از عملکردهای بومی باشد که به کلیدهای جهت دار، صفحه اصلی یا پایان محدود شده اند. عنصر preventDefault() را فراخوانی می کند تا از انجام هر گونه اقدامی توسط مرورگر جلوگیری کند.

      event.preventDefault();

برگه جدید را که در حالت switch-case مشخص شده است انتخاب کنید.

      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" در نشانه گذاری استفاده شود تا زمانی که جاوا اسکریپت خراب است، معنایی قابل استفاده باقی بماند.

یک <howto-tab> با استفاده از شناسه آن پانل به عنوان مقدار مشخصه aria-controls، مشخص می کند که به کدام <howto-panel> تعلق دارد.

یک <howto-tab> به طور خودکار یک شناسه منحصر به فرد ایجاد می کند اگر هیچ یک مشخص نشده باشد.

  class HowtoTab extends HTMLElement {

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

    constructor() {
      super();
    }

    connectedCallback() {

اگر این اجرا شود، جاوا اسکریپت کار می کند و عنصر نقش خود را به 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');
    }

بررسی کنید که آیا یک ویژگی دارای مقدار نمونه است یا خیر. اگر چنین است، مقدار را کپی کنید و ویژگی instance را حذف کنید تا تنظیم کننده ویژگی کلاس را تحت الشعاع قرار ندهد. در نهایت، مقدار را به تنظیم کننده ویژگی کلاس منتقل کنید تا بتواند هر گونه عوارض جانبی را ایجاد کند. این برای محافظت در برابر مواردی است که به عنوان مثال، یک چارچوب ممکن است عنصر را به صفحه اضافه کرده باشد و روی یکی از ویژگی های آن مقداری تعیین کرده باشد، اما lazy تعریف آن را بارگذاری کند. بدون این محافظ، عنصر ارتقا یافته آن ویژگی را از دست می‌دهد و ویژگی instance مانع از فراخوانی تنظیم‌کننده ویژگی کلاس می‌شود.

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

ویژگی ها و ویژگی های مربوط به آنها باید منعکس کننده یکدیگر باشند. برای این منظور، تنظیم‌کننده ویژگی برای selected ، مقادیر true/falsy را کنترل می‌کند و آن‌ها را به حالت ویژگی منعکس می‌کند. توجه به این نکته مهم است که هیچ عارضه جانبی در تنظیم کننده ملک وجود ندارد. به عنوان مثال، تنظیم کننده 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);
})();