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

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