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 का इस्तेमाल करके, टैब और पैनल को सेमैटिक तरीके से लिंक करता है. नए बच्चे अपने-आप स्लॉट में जुड़ जाएंगे और 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);
    }

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

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

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

    _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(), Tab पैनल में बटन दबाने की सुविधा को मैनेज करता है.

    _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" के साथ किया जाना चाहिए, ताकि JavaScript काम न करने पर भी सेमेटिक्स का इस्तेमाल किया जा सके.

<howto-tab>, aria-controls एट्रिब्यूट की वैल्यू के तौर पर पैनल के आईडी का इस्तेमाल करके यह बताता है कि वह किस <howto-panel> से जुड़ा है.

अगर कोई यूनीक आईडी नहीं दिया गया है, तो <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 में होता है. आम तौर पर, प्रॉपर्टी सेट करने वाले फ़ंक्शन को बहुत आसान बनाएं. अगर किसी प्रॉपर्टी या एट्रिब्यूट को सेट करने से कोई साइड इफ़ेक्ट होता है, तो उस काम को attributeChangedCallback() में करें. जैसे, मिलते-जुलते ARIA एट्रिब्यूट को सेट करना. इससे, एट्रिब्यूट/प्रॉपर्टी के फिर से लागू होने के जटिल मामलों को मैनेज करने की ज़रूरत नहीं पड़ेगी.

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