प्रोग्राम के हिसाब से स्क्रोल करने की प्रोसेस पूरी होने के बाद, इफ़ेक्ट ट्रिगर करना

प्रोग्राम के हिसाब से किसी सेक्शन या एलिमेंट पर स्क्रोल करते समय (खास तौर पर behavior: 'smooth' के साथ), फ़ॉलो-अप कार्रवाई को तुरंत ट्रिगर करने से, इफ़ेक्ट तब चलता है, जब टारगेट अब भी ऑफ़-स्क्रीन या स्क्रोल के बीच में होता है. जैसे, हाइलाइट की गई स्थिति को फ़्लैश करना, एंट्री ऐनिमेशन शुरू करना या फ़ोकस को मूव करना.

पहले, डेवलपर को setTimeout() की मदद से स्क्रोल करने की अवधि का अनुमान लगाना पड़ता था. यह लंबे स्क्रोल पर बहुत जल्दी और छोटे स्क्रोल पर बहुत देर से ट्रिगर होता है. इसके अलावा, डेवलपर को फ़ॉलबैक टाइमर के साथ Promise में scrollend इवेंट लिसनर को मैन्युअल तरीके से रैप करना पड़ता था. ऐसा इसलिए, क्योंकि टारगेट पहले से ही व्यू में होने और स्क्रोल न होने पर scrollend ट्रिगर नहीं होता.

Element (scrollIntoView(), scroll(), scrollTo(), scrollBy()) और Window (scroll(), scrollTo(), scrollBy()) पर प्रोग्राम के हिसाब से स्क्रोल करने के तरीके, Promise दिखाते हैं. यह स्क्रोल करने की प्रोसेस पूरी होने पर दिखता है. अगर टारगेट पहले से ही अनुरोध की गई स्क्रोल पोज़िशन पर है और स्क्रोल करने की ज़रूरत नहीं है, तो वापस किया गया Promise बिना हैंग हुए तुरंत रिज़ॉल्व हो जाता है.

लागू करना

  1. हैंडलर को async के तौर पर मार्क करें: स्क्रोल को ट्रिगर करने वाले इवेंट लिसनर या नेविगेशन हैंडलर के लिए, async फ़ंक्शन का इस्तेमाल करें.
  2. स्क्रोल करने के तरीके का सीधे तौर पर इंतज़ार करें: await element.scrollIntoView({ behavior: 'smooth', ... }) (या await element.scrollTo(...), await window.scrollTo(...) वगैरह) को कॉल करें. एक्सीक्यूशन तब तक रुका रहता है, जब तक ब्राउज़र स्क्रोल ऐनिमेशन को पूरा नहीं कर लेता और स्क्रोल की पोज़िशन सेट नहीं हो जाती.
  3. स्क्रोल करने के बाद का इफ़ेक्ट लागू करना: await एक्सप्रेशन पूरा होने के बाद, हाइलाइट क्लास को टॉगल करें, ऐनिमेशन शुरू करें या फ़ोकस को दूसरी जगह ले जाएं.

स्क्रोल करने में रुकावट और फ़ोकस मैनेजमेंट

  • तेज़ी से / रुक-रुककर स्क्रोल करने की सुविधा को मैनेज करना: अगर मौजूदा स्मूद स्क्रोल खत्म होने से पहले, उसी स्क्रोल कंटेनर पर नया स्क्रोल शुरू होता है, तो ब्राउज़र मौजूदा स्मूद स्क्रोल को बंद कर देता है. साथ ही, उस कंटेनर पर स्क्रोल करने से जुड़े सभी पेंडिंग प्रॉमिस को पूरा करता है (अस्वीकार नहीं करता). अगर आपको उपयोगकर्ता के तेज़ी से क्लिक करने पर, स्क्रोल करने के बाद होने वाले इफ़ेक्ट को रोकना है, तो बढ़ते हुए ऑपरेशन आईडी को ट्रैक करें. साथ ही, अगर नया स्क्रोल शुरू किया गया है, तो await के बाद जल्दी वापस आएं:
let currentScrollId = 0;
const sections = document.querySelectorAll('section');

async function scrollToAndHighlight(targetElement) {
  const scrollId = ++currentScrollId;
  sections.forEach((sec) => sec.classList.remove('highlight'));

  await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });

  // Abort follow-up effect if another scroll was triggered while awaiting
  if (scrollId !== currentScrollId) return;

  targetElement.classList.add('highlight');
}
  • स्क्रोल करने के बाद टारगेट पर फ़ोकस करना: await targetElement.scrollIntoView(...) के बाद, कीबोर्ड फ़ोकस को टारगेट एलिमेंट (या tabindex="-1" वाले हेडिंग) पर ले जाते समय, { preventScroll: true } को focus() पर पास करें, ताकि फ़ोकस कॉल, दूसरा स्क्रोल शुरू न करे:
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetHeading.focus({ preventScroll: true });

पाबंदियां और सबसे सही तरीके

  • ज़रूरी: setTimeout() का इस्तेमाल करके स्क्रोल करने में लगने वाले समय का अनुमान लगाने के बजाय, scrollIntoView(), scroll(), scrollTo() या scrollBy() से सीधे तौर पर मिलने वाले Promise का इंतज़ार करें.
  • scrollend को मैन्युअल new Promise() में रैप न करें. ऐसा इसलिए, क्योंकि जब टारगेट पहले से ही व्यू में होता है और स्क्रोल नहीं किया जाता है, तब scrollend ट्रिगर नहीं होता है. हालांकि, स्क्रोल करने का तरीका उस मामले में अपने-आप रिज़ॉल्व हो जाता है.
  • क्या करें: स्क्रोल करने के तरीके का इंतज़ार करने के बाद, .focus() को कॉल करते समय { preventScroll: true } पास करें, ताकि ब्राउज़र दूसरी बार जंप या स्क्रोल न करे.
  • क्या करें: अगर उपयोगकर्ता की तेज़ इंटरैक्शन की वजह से, एक ही कंटेनर पर ओवरलैप होने वाले स्मूद स्क्रोल ट्रिगर हो सकते हैं, तो स्क्रोल करने के बाद होने वाले इफ़ेक्ट को अनुरोध टोकन की मदद से सुरक्षित करें. ऐसा इसलिए, क्योंकि स्मूद स्क्रोल में रुकावट डालने से, पिछला प्रॉमिस अस्वीकार होने के बजाय रिज़ॉल्व हो जाता है.
  • रूट स्क्रोलर से जुड़ी Chrome की गड़बड़ी: Chrome में एक गड़बड़ी है. इसमें, scrollIntoView() से मिला Promise बहुत जल्दी रिज़ॉल्व हो जाता है. ऐसा तब होता है, जब स्क्रोल करने वाला एलिमेंट, दस्तावेज़ का रूट स्क्रोलर हो. नेस्ट किए गए स्क्रोल कंटेनर पर scrollIntoView() का इंतज़ार करना, उम्मीद के मुताबिक काम करता है. इस गड़बड़ी को ठीक करने के लिए, अपने कॉन्टेंट को स्क्रोल कंटेनर में न रखें. जैसे, overflow-y: auto के साथ div.

फ़ॉलबैक रणनीतियां

स्क्रोल करने के तरीके के प्रॉमिस के साथ काम न करने वाले ब्राउज़र में, scrollIntoView(), scroll(), scrollTo(), और scrollBy(), Promise के बजाय undefined दिखाते हैं.

  • परतदार वृद्धि (डिफ़ॉल्ट await व्यवहार): क्योंकि await undefined एक मान्य JavaScript है, जो अगले माइक्रोटास्क पर तुरंत रिज़ॉल्व हो जाता है. इसलिए, await element.scrollIntoView({ behavior: 'smooth' }) लिखने से उन ब्राउज़र में गड़बड़ी नहीं होगी जो इस सुविधा के साथ काम नहीं करते. इसके बजाय, स्क्रोल शुरू होते ही पोस्ट-स्क्रोल कोड को चलाकर, यह बेहतर तरीके से काम करता है. कुछ समय के लिए हाइलाइट करने जैसी सामान्य विज़ुअल स्टाइल के लिए, आम तौर पर यह काफ़ी होता है.
  • फ़ेथफ़ुल फ़ॉलबैक (scrollend + टाइम आउट): अगर आपका बेसलाइन टारगेट scroll-promises के साथ काम नहीं करता है और पुराने ब्राउज़र में स्क्रोल करने की प्रोसेस पूरी होने तक, स्क्रोल करने के बाद की कार्रवाई को इंतज़ार करना पड़ता है, तो देखें कि रिटर्न वैल्यू Promise (result instanceof Promise) है या नहीं. जब यह undefined हो, तो स्क्रोल कंटेनर पर scrollend इवेंट के लिए इंतज़ार करें. साथ ही, कम समय के लिए फ़ॉलबैक टाइम आउट का इस्तेमाल करें. ऐसा तब करें, जब एलिमेंट पहले से ही दिख रहा हो और स्क्रोल न हो रहा हो:
function scrollIntoViewAsync(element, options, scroller = window) {
  const result = element.scrollIntoView(options);

  // Feature detect: in supporting browsers, scrollIntoView() returns a Promise
  if (result instanceof Promise) {
    return result;
  }

  // Fallback for browsers where scrollIntoView() returns undefined
  return new Promise((resolve) => {
    let timeoutId;

    const finish = () => {
      clearTimeout(timeoutId);
      scroller.removeEventListener('scrollend', finish);
      scroller.removeEventListener('scroll', onScroll);
      resolve();
    };

    const onScroll = () => {
      // Restart the idle timer on every scroll event. If scrollend never fires
      // (browsers without scrollend support), this resolves once scrolling goes idle.
      clearTimeout(timeoutId);
      timeoutId = setTimeout(finish, 100);
    };

    scroller.addEventListener('scrollend', finish, { once: true });
    scroller.addEventListener('scroll', onScroll, { passive: true });

    // If no scroll event fires within the idle window (e.g., already in view), resolve.
    // 100ms is an example value; it only needs to exceed the gap between scroll events.
    timeoutId = setTimeout(finish, 100);
  });
}