स्विच कॉम्पोनेंट बनाना

रिस्पॉन्सिव और ऐक्सेस किए जा सकने वाले स्विच कॉम्पोनेंट बनाने का बुनियादी तरीका.

इस पोस्ट में, मैं स्विच कॉम्पोनेंट बनाने के तरीके के बारे में बताना चाहता हूं. डेमो आज़माएं.

डेमो

अगर आपको वीडियो देखना पसंद है, तो यहां इस पोस्ट का YouTube वर्शन दिया गया है:

खास जानकारी

स्विच, चेकबॉक्स की तरह काम करता है. हालांकि, यह साफ़ तौर पर बूलियन की चालू और बंद स्थितियों को दिखाता है.

यह डेमो अपनी ज़्यादातर सुविधाओं के लिए, <input type="checkbox" role="switch"> का इस्तेमाल करता है. इसका एक फ़ायदा यह है कि इसके लिए सीएसएस या JavaScript को पूरी तरह से काम करने और ऐक्सेस करने की ज़रूरत नहीं होती. सीएसएस को लोड करने से, भाषाओं, वर्टिकलिटी, ऐनिमेशन वगैरह के लिए सहायता मिलती है. JavaScript लोड करने से, स्विच को खींचकर छोड़ा जा सकता है और उसे छुआ जा सकता है.

कस्टम प्रॉपर्टी

नीचे दिए गए वैरिएबल, स्विच के अलग-अलग हिस्सों और उनके विकल्पों को दिखाते हैं. टॉप लेवल क्लास के तौर पर, .gui-switch में ऐसी कस्टम प्रॉपर्टी शामिल होती हैं जिनका इस्तेमाल पूरे कॉम्पोनेंट चाइल्ड के लिए किया जाता है. साथ ही, एक ही जगह से पसंद के मुताबिक बनाने के एंट्री पॉइंट भी होते हैं.

ट्रैक

लंबाई (--track-size), पैडिंग, और दो रंग:

.gui-switch {
  --track-size: calc(var(--thumb-size) * 2);
  --track-padding: 2px;

  --track-inactive: hsl(80 0% 80%);
  --track-active: hsl(80 60% 45%);

  --track-color-inactive: var(--track-inactive);
  --track-color-active: var(--track-active);

  @media (prefers-color-scheme: dark) {
    --track-inactive: hsl(80 0% 35%);
    --track-active: hsl(80 60% 60%);
  }
}

थंब

साइज़, बैकग्राउंड का रंग, और इंटरैक्शन हाइलाइट करने के लिए रंग:

.gui-switch {
  --thumb-size: 2rem;
  --thumb: hsl(0 0% 100%);
  --thumb-highlight: hsl(0 0% 0% / 25%);

  --thumb-color: var(--thumb);
  --thumb-color-highlight: var(--thumb-highlight);

  @media (prefers-color-scheme: dark) {
    --thumb: hsl(0 0% 5%);
    --thumb-highlight: hsl(0 0% 100% / 25%);
  }
}

कम मोशन

साफ़ तौर पर कोई दूसरा नाम जोड़ने और दोहराव को कम करने के लिए, PostCSS प्लग इन की मदद से, कस्टम प्रॉपर्टी में कम मोशन की प्राथमिकता वाली यूज़र मीडिया क्वेरी जोड़ी जा सकती है. यह ड्राफ़्ट के आधार पर की जाती है, जो मीडिया क्वेरी 5 में मौजूद है:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

मार्कअप

मैंने अपने <input type="checkbox" role="switch"> एलिमेंट को <label> के साथ रैप करने का विकल्प चुना. साथ ही, चेकबॉक्स और लेबल के असोसिएशन में होने वाली दिक्कतों से बचने के लिए, उनके संबंध को बंडल किया. साथ ही, उपयोगकर्ता को लेबल के साथ इंटरैक्ट करने की सुविधा दी, ताकि वह इनपुट को टॉगल कर सके.

बिना स्टाइल वाला सामान्य लेबल और चेकबॉक्स.

<label for="switch" class="gui-switch">
  Label text
  <input type="checkbox" role="switch" id="switch">
</label>

<input type="checkbox"> को एपीआई और स्टेट की मदद से, पहले से बनाया जाता है. ब्राउज़र, checked प्रॉपर्टी और इनपुट इवेंट, जैसे कि oninputऔर onchanged को मैनेज करता है.

लेआउट

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

.gui-switch

स्विच के लिए टॉप-लेवल लेआउट, फ़्लेक्सबॉक्स है. क्लास .gui-switch में, ऐसी प्राइवेट और सार्वजनिक कस्टम प्रॉपर्टी होती हैं जिनका इस्तेमाल बच्चे अपने लेआउट कैलकुलेट करने के लिए करते हैं.

Flexbox DevTools, हॉरिज़ॉन्टल लेबल और स्विच को ओवरले कर रहा है. इससे, स्पेस के लेआउट डिस्ट्रिब्यूशन की जानकारी मिलती है.

.gui-switch {
  display: flex;
  align-items: center;
  gap: 2ch;
  justify-content: space-between;
}

फ़्लेक्सबॉक्स लेआउट को बड़ा करना और उसमें बदलाव करना, किसी भी फ़्लेक्सबॉक्स लेआउट में बदलाव करने जैसा ही है. उदाहरण के लिए, किसी स्विच के ऊपर या नीचे लेबल डालने के लिए या flex-direction को बदलने के लिए:

वर्टिकल लेबल और स्विच को ओवरले करने वाला Flexbox DevTools.

<label for="light-switch" class="gui-switch" style="flex-direction: column">
  Default
  <input type="checkbox" role="switch" id="light-switch">
</label>

ट्रैक

चेकबॉक्स इनपुट की स्टाइल को स्विच ट्रैक के तौर पर सेट किया गया है. इसके लिए, चेकबॉक्स के सामान्य appearance: checkbox को हटाकर, उसका साइज़ अपने हिसाब से दिया गया है:

स्विच ट्रैक पर ओवरले किए गए Grid DevTools, जिसमें &#39;ट्रैक&#39; नाम वाले ग्रिड ट्रैक के एरिया दिख रहे हैं.

.gui-switch > input {
  appearance: none;

  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  padding: var(--track-padding);

  flex-shrink: 0;
  display: grid;
  align-items: center;
  grid: [track] 1fr / [track] 1fr;
}

ट्रैक में, दावा करने के लिए एक-एक सेल वाला ग्रिड ट्रैक एरिया भी बनता है.

थंब

स्टाइल appearance: none, ब्राउज़र से मिलने वाले विज़ुअल चेकमार्क को भी हटा देता है. इस विज़ुअल इंडिकेटर को बदलने के लिए, यह कॉम्पोनेंट इनपुट पर स्यूडो-एलिमेंट और :checked स्यूडो-क्लास का इस्तेमाल करता है.

थंब, input[type="checkbox"] से जुड़ा एक स्यूडो-एलिमेंट चाइल्ड है. यह ग्रिड एरिया track पर दावा करके, ट्रैक के नीचे के बजाय सबसे ऊपर स्टैक होता है:

DevTools, सूडो-एलिमेंट थंब को सीएसएस ग्रिड में दिखा रहा है.

.gui-switch > input::before {
  content: "";
  grid-area: track;
  inline-size: var(--thumb-size);
  block-size: var(--thumb-size);
}

स्टाइल

कस्टम प्रॉपर्टी की मदद से, एक ऐसा स्विच कॉम्पोनेंट बनाया जा सकता है जो रंग योजनाओं, दाईं से बाईं ओर लिखी जाने वाली भाषाओं, और मोशन से जुड़ी प्राथमिकताओं के हिसाब से काम करता है.

स्विच और उसकी स्थितियों के लिए, हल्के और गहरे रंग वाली थीम की साथ-साथ तुलना.

टच इंटरैक्शन स्टाइल

मोबाइल पर, ब्राउज़र लेबल और इनपुट में टैप हाइलाइट और टेक्स्ट चुनने की सुविधाएं जोड़ते हैं. इनसे, स्टाइल और विज़ुअल इंटरैक्शन के सुझावों पर बुरा असर पड़ा. सीएसएस की कुछ लाइनों की मदद से, मैं उन इफ़ेक्ट को हटा सकता हूं और अपनी पसंद के मुताबिक cursor: pointer स्टाइल जोड़ सकता हूं:

.gui-switch {
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

इन स्टाइल को हमेशा हटाना सही नहीं होता, क्योंकि इनके ज़रिए विज़ुअल इंटरैक्शन के बारे में ज़रूरी सुझाव मिलते हैं. अगर उन्हें हटाया जाता है, तो उनके लिए कस्टम विकल्प ज़रूर दें.

ट्रैक

इस एलिमेंट के स्टाइल में ज़्यादातर उसके आकार और रंग की जानकारी होती है. इसे कैस्केड के ज़रिए, पैरंट .gui-switch से ऐक्सेस किया जाता है.

कस्टम ट्रैक साइज़ और रंगों के साथ स्विच के वैरिएंट.

.gui-switch > input {
  appearance: none;
  border: none;
  outline-offset: 5px;
  box-sizing: content-box;

  padding: var(--track-padding);
  background: var(--track-color-inactive);
  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  border-radius: var(--track-size);
}

स्विच ट्रैक को पसंद के मुताबिक बनाने के कई विकल्प, चार कस्टम प्रॉपर्टी से मिलते हैं. border: none को जोड़ा गया, क्योंकि appearance: none सभी ब्राउज़र के चेकबॉक्स से बॉर्डर नहीं हटाता.

थंब

थंबनेल एलिमेंट पहले से ही दाईं ओर track है, लेकिन उसे सर्कल स्टाइल की ज़रूरत है:

.gui-switch > input::before {
  background: var(--thumb-color);
  border-radius: 50%;
}

DevTools से पता चलता है कि सर्कल थंब pseudo-element को हाइलाइट किया गया है.

बातचीत

कस्टम प्रॉपर्टी का इस्तेमाल करके, ऐसे इंटरैक्शन के लिए तैयारी करें जिनमें कर्सर घुमाने पर हाइलाइट दिखें और थंब की पोज़िशन में बदलाव दिखें. मोशन या कर्सर घुमाने पर हाइलाइट करने के स्टाइल में बदलाव करने से पहले, उपयोगकर्ता की प्राथमिकता की भी जांच की जाती है.

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

पसंद की जगह

कस्टम प्रॉपर्टी, ट्रैक में थंब को पोज़िशन करने के लिए एक सोर्स मैकेनिज्म उपलब्ध कराती हैं. हमारे पास ट्रैक और थंब के साइज़ हैं. हम इनका इस्तेमाल, कैलकुलेशन में करेंगे, ताकि थंब को सही तरीके से ऑफ़सेट किया जा सके और वह ट्रैक के बीच में रहे: 0% और 100%.

input एलिमेंट के पास पोज़िशन वैरिएबल --thumb-position का मालिकाना हक होता है और थंब स्यूडो एलिमेंट इसका इस्तेमाल translateX पोज़िशन के तौर पर करता है:

.gui-switch > input {
  --thumb-position: 0%;
}

.gui-switch > input::before {
  transform: translateX(var(--thumb-position));
}

अब हम सीएसएस और चेकबॉक्स एलिमेंट पर दिए गए सूडो-क्लास से --thumb-position को बदल सकते हैं. हमने इस एलिमेंट पर, शर्तों के साथ transition: transform var(--thumb-transition-duration) ease पहले सेट किया है. इसलिए, इनमें बदलाव होने पर ये बदलाव ऐनिमेट हो सकते हैं:

/* positioned at the end of the track: track length - 100% (thumb width) */
.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
}

/* positioned in the center of the track: half the track - half the thumb */
.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
}

मुझे लगता है कि अलग-अलग ऑर्केस्ट्रेटर का इस्तेमाल करके, यह प्रोसेस अच्छी तरह से पूरी हुई. अंगूठे वाले एलिमेंट का मतलब सिर्फ़ एक स्टाइल, translateX पोज़िशन से है. इनपुट, सभी जटिलताओं और हिसाब-किताब को मैनेज कर सकता है.

वर्टिकल

कार्रवाई बदलने वाली क्लास -vertical की मदद से मदद की गई. यह input एलिमेंट में, सीएसएस ट्रांसफ़ॉर्म के साथ रोटेशन जोड़ती है.

हालांकि, 3D में घुमाए गए एलिमेंट से कॉम्पोनेंट की कुल ऊंचाई में बदलाव नहीं होता, जिससे ब्लॉक लेआउट गड़बड़ हो सकता है. --track-size और --track-padding वैरिएबल का इस्तेमाल करके, इसकी गिनती करें. यह कैलकुलेट करें कि वर्टिकल बटन को उम्मीद के मुताबिक लेआउट में फ़्लो करने के लिए, कम से कम जगह कितनी होनी चाहिए:

.gui-switch.-vertical {
  min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));

  & > input {
    transform: rotate(-90deg);
  }
}

(RTL) दाएं से बाएं

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

.gui-switch {
  --isLTR: 1;

  &:dir(rtl) {
    --isLTR: -1;
  }
}

--isLTR नाम की कस्टम प्रॉपर्टी में शुरुआत में 1 की वैल्यू होती है. इसका मतलब है कि यह true है, क्योंकि हमारा लेआउट डिफ़ॉल्ट रूप से बाएं-से-दाएं रहता है. इसके बाद, जब कॉम्पोनेंट दाईं से बाईं ओर के लेआउट में होता है, तो सीएसएस स्यूडो क्लास :dir() का इस्तेमाल करके, वैल्यू को -1 पर सेट किया जाता है.

ट्रांसफ़ॉर्म के अंदर calc() में --isLTR का इस्तेमाल करके, --isLTR को चालू करें:

.gui-switch.-vertical > input {
  transform: rotate(-90deg);
  transform: rotate(calc(90deg * var(--isLTR) * -1));
}

अब वर्टिकल स्विच के घुमाव से, दाईं से बाईं ओर के लेआउट के लिए, खातों की दूसरी तरफ़ की पोज़िशन का पता चलता है.

थंब वाले स्यूडो-एलिमेंट पर मौजूद translateX ट्रांसफ़ॉर्म को भी अपडेट करना होगा, ताकि दूसरी तरफ़ की ज़रूरतों को ध्यान में रखा जा सके:

.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
  --thumb-position: calc(
   ((var(--track-size) / 2) - (var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

इस तरीके से लॉजिकल सीएसएस ट्रांसफ़ॉर्म जैसे कॉन्सेप्ट की सभी ज़रूरतों को पूरा नहीं किया जा सकता. हालांकि, इसके इस्तेमाल के कई उदाहरणों में, कुछ DRY सिद्धांत ज़रूर दिए जाते हैं.

राज्य

पहले से मौजूद input[type="checkbox"] का इस्तेमाल करने के लिए, यह ज़रूरी है कि आप इन अलग-अलग स्थितियों को मैनेज करें: :checked, :disabled, :indeterminate, और :hover. :focus को जान-बूझकर वैसा ही रखा गया था जैसा पहले था. सिर्फ़ इसके ऑफ़सेट में बदलाव किया गया था. फ़ायरफ़ॉक्स और Safari पर फ़ोकस रिंग बेहतरीन दिख रही थी:

Firefox और Safari में किसी स्विच पर फ़ोकस की गई फ़ोकस रिंग का स्क्रीनशॉट.

सही का निशान लगाया गया

<label for="switch-checked" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>

यह स्टेटस, on स्टेटस दिखाता है. इस स्थिति में, इनपुट "ट्रैक" के बैकग्राउंड को चालू रंग पर सेट किया गया है और थंब की पोज़िशन को "आखिर में" पर सेट किया गया है.

.gui-switch > input:checked {
  background: var(--track-color-active);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

बंद है

<label for="switch-disabled" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>

:disabled बटन न सिर्फ़ अलग दिखता है, बल्कि एलिमेंट को बदले जाने से भी रोकता है. इंटरैक्शन को बदले जाने से रोकने की सुविधा, ब्राउज़र पर काम नहीं करती. हालांकि, appearance: none के इस्तेमाल की वजह से विज़ुअल स्टेटस को स्टाइल की ज़रूरत होती है.

.gui-switch > input:disabled {
  cursor: not-allowed;
  --thumb-color: transparent;

  &::before {
    cursor: not-allowed;
    box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);

    @media (prefers-color-scheme: dark) { & {
      box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
    }}
  }
}

डार्क स्टाइल वाला स्विच, बंद, सही का निशान लगा हुआ, और सही का निशान हटाया हुआ के स्टेटस में.

यह स्थिति मुश्किल है, क्योंकि इसमें गहरे और हल्के रंग वाली थीम की ज़रूरत होती है. साथ ही, इन दोनों थीम के लिए, बंद और चालू होने की स्थिति भी होनी चाहिए. मैंने इन स्थितियों के लिए कम से कम स्टाइल चुने हैं, ताकि स्टाइल के कॉम्बिनेशन को मैनेज करने में आसानी हो.

तय नहीं है

अक्सर भूलने वाली स्थिति :indeterminate होती है. इसमें चेकबॉक्स पर न तो सही का निशान लगाया जाता है और न ही उससे सही का निशान हटाया जाता है. यह एक मज़ेदार स्थिति है. यह आमंत्रित करने वाली और आसान है. यह एक अच्छा रिमाइंडर है कि बूलियन स्टेटस के बीच में, स्नीकी स्टेटस हो सकते हैं.

चेकबॉक्स को तय नहीं करना मुश्किल है, सिर्फ़ JavaScript इसे सेट कर सकता है:

<label for="switch-indeterminate" class="gui-switch">
  Indeterminate
  <input type="checkbox" role="switch" id="switch-indeterminate">
  <script>document.getElementById('switch-indeterminate').indeterminate = true</script>
</label>

यह स्थिति तब होती है, जब ट्रैक के बीच में थंबनेल दिखता है. इससे पता चलता है कि ट्रैक को पसंद किया गया है या नहीं.

मेरे हिसाब से, यह स्टेटस न तो बहुत शानदार है और न ही बहुत बुरा. इसलिए, स्विच के थंब को बीच में रखना सही लगा:

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

कर्सर घुमाएं

कर्सर घुमाने पर होने वाले इंटरैक्शन से, कनेक्ट किए गए यूज़र इंटरफ़ेस (यूआई) के लिए विज़ुअल सहायता मिलनी चाहिए. साथ ही, इंटरैक्टिव यूज़र इंटरफ़ेस (यूआई) के लिए निर्देश भी मिलना चाहिए. जब लेबल या इनपुट पर कर्सर घुमाया जाता है, तो यह स्विच अर्ध-पारदर्शी रिंग से अंगूठे को हाइलाइट करता है. इसके बाद, यह कर्सर घुमाने पर दिखने वाला ऐनिमेशन, इंटरैक्टिव थंबनेल एलिमेंट की ओर निर्देश देता है.

"हाइलाइट" इफ़ेक्ट, box-shadow का इस्तेमाल करके पूरा किया जाता है. बंद नहीं किए गए इनपुट पर कर्सर घुमाने पर, --highlight-size का साइज़ बढ़ाएं. अगर उपयोगकर्ता मोशन के साथ सहमत है, तो हम box-shadow को ट्रांज़िशन करते हैं और उसे बड़ा करते हैं. अगर उपयोगकर्ता मोशन के साथ सहमत नहीं है, तो हाइलाइट तुरंत दिखता है:

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

.gui-switch > input:not(:disabled):hover::before {
  --highlight-size: .5rem;
}

JavaScript

मुझे लगता है कि स्विच इंटरफ़ेस, किसी फ़िज़िकल इंटरफ़ेस को एमुलेट करने की कोशिश में अजीब लग सकता है. खास तौर पर, ट्रैक के अंदर सर्कल वाला यह इंटरफ़ेस. iOS ने अपने स्विच के साथ इसे सही तरीके से लागू किया है. इन्हें एक तरफ़ से दूसरी तरफ़ खींचा जा सकता है. यह विकल्प बहुत अच्छा है. इसके उलट, अगर किसी यूज़र इंटरफ़ेस (यूआई) एलिमेंट को खींचने की कोशिश की जाती है और कुछ नहीं होता, तो ऐसा लग सकता है कि वह एलिमेंट काम नहीं कर रहा है.

खींचकर छोड़े जा सकने वाले थंब

थंब स्यूडो-एलिमेंट को उसकी पोज़िशन, .gui-switch > input स्कोप किए गए var(--thumb-position) से मिलती है. JavaScript, इनपुट पर इनलाइन स्टाइल वैल्यू दे सकता है, ताकि थंब की पोज़िशन को डाइनैमिक तौर पर अपडेट किया जा सके. इससे, यह ऐसा दिखता है कि वह पॉइंटर जेस्चर का पालन कर रहा है. पॉइंटर रिलीज़ होने के बाद, इनलाइन स्टाइल हटाएं. साथ ही, कस्टम प्रॉपर्टी --thumb-position का इस्तेमाल करके यह तय करें कि खींचकर, कर्सर को बंद किया गया है या चालू किया गया है. यह समाधान का मुख्य हिस्सा है; सीएसएस कस्टम प्रॉपर्टी में बदलाव करने के लिए, पॉइंटर इवेंट, पॉइंटर की पोज़िशन को कंडीशन के हिसाब से ट्रैक करते हैं.

इस स्क्रिप्ट के दिखने से पहले, कॉम्पोनेंट पहले से ही 100% काम कर रहा था. इसलिए, मौजूदा व्यवहार को बनाए रखने के लिए काफ़ी मेहनत करनी पड़ती है. जैसे, इनपुट को टॉगल करने के लिए किसी लेबल पर क्लिक करना. हमारा JavaScript, मौजूदा फ़ीचर के खर्च पर फ़ीचर नहीं जोड़ना चाहिए.

touch-action

खींचकर छोड़ना एक जेस्चर है, जो इसे पसंद के मुताबिक बनाया गया है. इससे यह touch-action के फ़ायदों के लिए एक अच्छा उम्मीदवार बन जाता है. इस स्विच के मामले में, हमारी स्क्रिप्ट को हॉरिज़ॉन्टल जेस्चर मैनेज करना चाहिए या वर्टिकल स्विच वैरिएंट के लिए कैप्चर किया गया वर्टिकल जेस्चर. touch-action की मदद से, हम ब्राउज़र को बता सकते हैं कि इस एलिमेंट पर कौनसे जेस्चर मैनेज करने हैं, ताकि कोई स्क्रिप्ट बिना किसी रुकावट के जेस्चर मैनेज कर सके.

नीचे दी गई सीएसएस, ब्राउज़र को यह निर्देश देती है कि जब इस स्विच ट्रैक के अंदर से पॉइंटर जेस्चर शुरू होता है, तो वह वर्टिकल जेस्चर को हैंडल करे, हॉरिज़ॉन्टल वाले जेस्चर के साथ कुछ न करे:

.gui-switch > input {
  touch-action: pan-y;
}

जो नतीजा मिलता है वह एक हॉरिज़ॉन्टल जेस्चर होता है जो पेज को पैन या स्क्रोल नहीं करता. पॉइंटर, इनपुट में वर्टिकल स्क्रोल शुरू कर सकता है और पेज को स्क्रोल कर सकता है. हालांकि, हॉरिज़ॉन्टल स्क्रोल को कस्टम तरीके से मैनेज किया जाता है.

पिक्सल वैल्यू स्टाइल की सुविधाएं

सेटअप करने के दौरान और खींचकर छोड़ने के दौरान, कंप्यूट की गई संख्या की अलग-अलग वैल्यू को एलिमेंट से लेना होगा. यहां दिए गए JavaScript फ़ंक्शन, किसी सीएसएस प्रॉपर्टी के लिए, कैलकुलेट की गई पिक्सल वैल्यू दिखाते हैं. इसका इस्तेमाल सेटअप स्क्रिप्ट में इस तरह किया जाता है getStyle(checkbox, 'padding-left').

​​const getStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element).getPropertyValue(prop));
}

const getPseudoStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element, ':before').getPropertyValue(prop));
}

export {
  getStyle,
  getPseudoStyle,
}

ध्यान दें कि window.getComputedStyle() किस तरह दूसरे आर्ग्युमेंट, टारगेट pseudo एलिमेंट को स्वीकार करता है. यह बहुत अच्छा है कि JavaScript, एलिमेंट से इतनी सारी वैल्यू पढ़ सकता है. यहां तक कि सूडो एलिमेंट से भी.

dragging

यह ड्रैग लॉजिक के लिए एक खास पल है और फ़ंक्शन इवेंट हैंडलर से ध्यान देने वाली कुछ चीज़ें हैं:

const dragging = event => {
  if (!state.activethumb) return

  let {thumbsize, bounds, padding} = switches.get(state.activethumb.parentElement)
  let directionality = getStyle(state.activethumb, '--isLTR')

  let track = (directionality === -1)
    ? (state.activethumb.clientWidth * -1) + thumbsize + padding
    : 0

  let pos = Math.round(event.offsetX - thumbsize / 2)

  if (pos < bounds.lower) pos = 0
  if (pos > bounds.upper) pos = bounds.upper

  state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
}

स्क्रिप्ट का हीरो state.activethumb है. यह छोटा गोला, स्क्रिप्ट में पॉइंटर के साथ दिख रहा है. switches ऑब्जेक्ट एक Map() होता है, जिसमें कीवर्ड .gui-switch होते हैं और वैल्यू कैश मेमोरी में सेव की गई सीमाएं और साइज़ होती हैं. इनकी मदद से स्क्रिप्ट को बेहतर तरीके से चलाया जा सकता है. दाएं से बाएं हैंडल को उसी कस्टम प्रॉपर्टी का इस्तेमाल करके हैंडल किया जाता है जो सीएसएस --isLTR है. साथ ही, यह लॉजिक को उलटा करने और आरटीएल के साथ काम करना जारी रखने के लिए इसका इस्तेमाल कर सकती है. event.offsetX भी अहम है, क्योंकि इसमें डेल्टा वैल्यू है. इससे अंगूठे को सही जगह पर रखने में मदद मिलती है.

state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)

सीएसएस की यह आखिरी लाइन, थंब एलिमेंट में इस्तेमाल की जाने वाली कस्टम प्रॉपर्टी सेट करती है. वैल्यू असाइनमेंट, समय के साथ ट्रांज़िशन हो जाता है. हालांकि, पिछले पॉइंटर इवेंट ने --thumb-transition-duration को कुछ समय के लिए 0s पर सेट कर दिया है. इससे, इंटरैक्शन में आने वाली देरी को हटा दिया गया है.

dragEnd

उपयोगकर्ता को स्विच से बहुत दूर तक खींचकर छोड़ने की अनुमति देने के लिए, एक वैश्विक विंडो इवेंट रजिस्टर करना ज़रूरी है:

window.addEventListener('pointerup', event => {
  if (!state.activethumb) return

  dragEnd(event)
})

मुझे लगता है कि यह बहुत ज़रूरी है कि उपयोगकर्ता को आज़ादी से आइटम को खींचने और छोड़ने की सुविधा मिले. साथ ही, इंटरफ़ेस भी इस सुविधा के हिसाब से स्मार्ट हो. इस स्विच के साथ इसे मैनेज करने में ज़्यादा समय नहीं लगा, लेकिन डेवलपमेंट की प्रोसेस के दौरान इस पर ध्यान देने की ज़रूरत थी.

const dragEnd = event => {
  if (!state.activethumb) return

  state.activethumb.checked = determineChecked()

  if (state.activethumb.indeterminate)
    state.activethumb.indeterminate = false

  state.activethumb.style.removeProperty('--thumb-transition-duration')
  state.activethumb.style.removeProperty('--thumb-position')
  state.activethumb.removeEventListener('pointermove', dragging)
  state.activethumb = null

  padRelease()
}

एलिमेंट के साथ इंटरैक्शन पूरा हो गया है. अब इनपुट की checked प्रॉपर्टी सेट करने और सभी जेस्चर इवेंट हटाने का समय आ गया है. चेकबॉक्स को state.activethumb.checked = determineChecked() से बदल दिया गया है.

determineChecked()

dragEnd से कॉल किए जाने वाले इस फ़ंक्शन से यह तय होता है कि अंगूठे की मौजूदा स्थिति, ट्रैक के किन सीमाओं के बीच है. साथ ही, अगर यह ट्रैक के आधे हिस्से के बराबर या उससे ज़्यादा है, तो यह फ़ंक्शन 'सही' दिखाता है:

const determineChecked = () => {
  let {bounds} = switches.get(state.activethumb.parentElement)

  let curpos =
    Math.abs(
      parseInt(
        state.activethumb.style.getPropertyValue('--thumb-position')))

  if (!curpos) {
    curpos = state.activethumb.checked
      ? bounds.lower
      : bounds.upper
  }

  return curpos >= bounds.middle
}

अन्य बातें

चुने गए शुरुआती एचटीएमएल स्ट्रक्चर की वजह से, खींचें और छोड़ें जेस्चर में थोड़ा कोड डेबिट हुआ. खास तौर पर, इनपुट को लेबल में रैप करने की वजह से. लेबल, पैरंट एलिमेंट होने के नाते, इनपुट के बाद क्लिक इंटरैक्शन पाता है. dragEnd इवेंट के आखिर में, आपको padRelease() फ़ंक्शन के तौर पर कुछ अजीब दिख सकता है.

const padRelease = () => {
  state.recentlyDragged = true

  setTimeout(_ => {
    state.recentlyDragged = false
  }, 300)
}

यह उस लेबल को ध्यान में रखकर बनाया जाता है जिस पर बाद में क्लिक किया जाता है. ऐसा इसलिए, क्योंकि यह उपयोगकर्ता के किए गए इंटरैक्शन पर सही का निशान लगा देगा या उसकी जांच करेगा.

अगर मुझे यह काम फिर से करना पड़े, तो यूज़र एक्सपीरियंस (UX) को अपग्रेड करते समय, JavaScript की मदद से DOM में बदलाव करने पर शायद विचार किया जा सकता है. ऐसा इसलिए, ताकि ऐसा एलिमेंट बनाया जा सके जो लेबल पर होने वाले क्लिक को खुद मैनेज करता हो और पहले से मौजूद व्यवहार से मेल न खाता हो.

इस तरह का JavaScript लिखना मुझे बिलकुल पसंद नहीं है. मुझे कंडीशनल इवेंट बबल करने की सुविधा मैनेज नहीं करनी है:

const preventBubbles = event => {
  if (state.recentlyDragged)
    event.preventDefault() && event.stopPropagation()
}

नतीजा

अब तक के सभी जीयूआई चैलेंज में, इस छोटे स्विच कॉम्पोनेंट को बनाने में सबसे ज़्यादा समय लगा! अब आपको पता है कि मैंने यह कैसे किया, तो आप कैसे करेंगे‽ 🙂

आइए, हम अलग-अलग तरह के काम करते हैं और वेब पर काम करने के सभी तरीके सीखते हैं. एक डेमो बनाएं, मुझे ट्वीट करें लिंक, और मैं इसे नीचे दिए कम्यूनिटी रीमिक्स सेक्शन में जोड़ दूंगा!

कम्यूनिटी रीमिक्स

संसाधन

.gui-switch का सोर्स कोड, GitHub पर ढूंढें.