প্রোগ্র্যামাটিক স্ক্রলিং সম্পূর্ণ হওয়ার পরে এফেক্ট ট্রিগার করা

প্রোগ্রাম্যাটিক্যালি কোনও বিভাগ বা এলিমেন্টে স্ক্রল করার সময় (বিশেষত 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-এর জন্য অপেক্ষা করুন।
  • আপনার প্রাথমিক ইমপ্লিমেন্টেশন হিসেবে ম্যানুয়াল new Promise()-এ scrollend র‍্যাপ করবেন না—টার্গেট আগে থেকেই ভিউতে থাকলে এবং স্ক্রল ডিস্ট্যান্স ট্রাভেল না করলে scrollend ফায়ার করে না, যেখানে স্ক্রল পদ্ধতি সেই ক্ষেত্রে অটোমেটিক সমাধান করার প্রতিশ্রুতি দেয়।
  • করণীয়: স্ক্রল মেথডের জন্য অপেক্ষা করার পরে .focus() কল করার সময় { preventScroll: true } পাস করুন যাতে ব্রাউজার দ্বিতীয়বার জাম্প বা স্ক্রল না করে।
  • করণীয়: ব্যবহারকারীর দ্রুত ইন্টার‍্যাকশন একই কন্টেনারে ওভারল্যাপ করা স্মুথ স্ক্রল ট্রিগার করতে পারলে, রিকোয়েস্ট টোকেন দিয়ে পোস্ট-স্ক্রল এফেক্ট গার্ড করুন, কারণ স্মুথ স্ক্রল ইন্টারাপ্ট করলে আগের প্রমিস বাতিল না হয়ে সমাধান হয়ে যায়।
  • রুট স্ক্রলারের সাথে Chrome-এর পরিচিত সমস্যা: Chrome-এ একটি সমস্যা আছে যেখানে scrollIntoView()-এর মাধ্যমে রিটার্ন করা Promise খুব তাড়াতাড়ি সমাধান হয়ে যায় যখন এটি ডকুমেন্টের রুট স্ক্রলার হয় যা স্ক্রল করছে। নেস্টেড স্ক্রল কন্টেনারে scrollIntoView()-এর জন্য অপেক্ষা করা প্রত্যাশিতভাবে কাজ করে। এই সমস্যার সমাধান হিসেবে আপনার কন্টেন্ট স্ক্রল কন্টেনারে র‍্যাপ করবেন না, যেমন overflow-y: auto সহ div।

ফলব্যাক স্ট্র্যাটেজি

যেসব ব্রাউজারে স্ক্রল মেথড প্রমিস কাজ করে না, সেখানে scrollIntoView(), scroll(), scrollTo() ও scrollBy(), Promise-এর পরিবর্তে undefined রিটার্ন করে।

  • প্রোগ্রেসিভ এনহ্যান্সমেন্ট (ডিফল্ট await অ্যাকশন): যেহেতু await undefined হল সঠিক জাভাস্ক্রিপ্ট যা পরবর্তী মাইক্রোটাস্কে অবিলম্বে সমাধান করে, তাই 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);
  });
}