HowTo कॉम्पोनेंट – कैसे करें टैब

ईवा गैस्परोविच

खास जानकारी

<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 के साथ पार्सर को शुरू करने से बचने के लिए, सभी <howto-tabs> इंस्टेंस, शैडो DOM के कॉन्टेंट के लिए एक टेंप्लेट शेयर करते हैं.

  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 का इस्तेमाल करके, टैब और पैनल को सिमैंटिक तौर पर लिंक करता है. नए बच्चे अपने-आप स्लॉट चुन लिए जाएंगे और उनकी वजह से स्लॉट चेंज चालू हो जाएंगे. इसलिए, म्यूटेशन ऑब्ज़र्वर की ज़रूरत नहीं है.

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

जब भी शैडो DOM स्लॉट में से किसी एक एलिमेंट को जोड़ा या हटाया जाता है, तो _onSlotChange() को कॉल किया जाता है.

    _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 क्वेरी कभी परफ़ॉर्मेंस से जुड़ी समस्या बन जाती हैं, तो यह फ़ंक्शन नतीजे को याद रख सकता है. याद रखने में दिक्कत यह है कि डाइनैमिक तौर पर जोड़े गए टैब और पैनल मैनेज नहीं किए जाएंगे.

यह तरीका है, गैटर नहीं, क्योंकि गैटर का मतलब है कि पढ़ना सस्ता है.

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

देखें कि प्रॉपर्टी में कोई इंस्टेंस वैल्यू है या नहीं. अगर ऐसा है, तो वैल्यू को कॉपी करें और इंस्टेंस प्रॉपर्टी को मिटाएं, ताकि क्लास प्रॉपर्टी सेटर को शैडो न किया जा सके. आखिर में, वैल्यू को क्लास प्रॉपर्टी सेटर को पास करें, ताकि यह किसी भी खराब असर को ट्रिगर कर सके. यह उन मामलों से सुरक्षित रखने के लिए है जहां, उदाहरण के लिए, हो सकता है कि किसी फ़्रेमवर्क ने पेज में एलिमेंट जोड़ा हो और उसकी किसी प्रॉपर्टी पर वैल्यू सेट की हो, लेकिन लेज़ी ने उसकी परिभाषा को लोड कर दिया हो. इस गार्ड के बिना, अपग्रेड किए गए एलिमेंट में वह प्रॉपर्टी शामिल नहीं हो पाएगी और इंस्टेंस प्रॉपर्टी, क्लास प्रॉपर्टी सेटर को कभी कॉल नहीं कर पाएगी.

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