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> इंस्टेंस के ज़रिए शेयर किया जाता है.

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

प्रोग्रेसिव एन्हैंसमेंट के लिए, टैब और पैनल के बीच मार्कअप होना चाहिए. बच्चों का क्रम बदलने वाले एलिमेंट, फ़्रेमवर्क के साथ ठीक से काम नहीं करते. इसके बजाय, शैडो डीओएम का इस्तेमाल स्लॉट का इस्तेमाल करके एलिमेंट को फिर से क्रम में लगाने के लिए किया जाता है.

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

स्विच-केस में तय किया गया नया टैब चुनें.

      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 में होता है. एक सामान्य नियम के तौर पर, प्रॉपर्टी सेटर को बहुत बेवकूफ़ बनाएं. अगर किसी प्रॉपर्टी या एट्रिब्यूट को सेट करने से खराब असर होता है (जैसे कि इससे जुड़ा 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);
})();