راه‌اندازی جلوه‌ها پس‌از تکمیل پیمایش برنامه‌ریزی‌شده

ویژگی‌های وب: Scroll method promises scrollIntoView()

هنگام پیمایش برنامه‌ای به یک بخش یا عنصر (به‌ویژه با behavior: 'smooth')، راه‌اندازی فوری یک کنش پیگیری (مانند چشمک زدن حالت برجسته، شروع پویانمایی ورودی، یا جابه‌جایی تمرکز) باعث می‌شود که جلوه درحالی اجرا شود که هدف هنوز خارج از صفحه یا درحال پیمایش است.

قبلاً توسعه‌دهندگان باید مدت پیمایش را با setTimeout() حدس می‌زدند (که در پیمایش‌های طولانی خیلی زود و در پیمایش‌های کوتاه خیلی دیر فعال می‌شود) یا به‌صورت دستی یک شنونده رویداد scrollend را در یک Promise با زمان‌سنج بازگشتی قرار می‌دادند (زیرا وقتی هدف ازقبل در دید باشد و پیمایشی انجام نشود، 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');
}
  • تمرکز روی هدف پس‌از پیمایش: هنگام انتقال تمرکز صفحه‌کلید به عنصر هدف (یا عنوان داخل آن با tabindex="-1") پس‌از await targetElement.scrollIntoView(...)، { preventScroll: true } را به focus() منتقل کنید تا فراخوانی تمرکز پیمایش دوم و رقیب را آغاز نکند:
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetHeading.focus({ preventScroll: true });

محدودیت‌ها و روال‌های مطلوب

  • اجباری: به‌جای حدس زدن مدت پیمایش با setTimeout()، منتظر Promise باشید که مستقیماً توسط scrollIntoView()، scroll()، scrollTo()، یا scrollBy() برگردانده می‌شود.
  • DO NOT wrap scrollend in a manual new Promise() as your primary implementation—scrollend does not fire when the target is already in view and no scroll distance is traveled, whereas scroll method promises resolve automatically in that case.
  • باید: { preventScroll: true } را هنگام فراخوانی .focus() پس‌از انتظار برای روش پیمایش ارسال کنید تا مرورگر دوباره نپرد یا پیمایش نکند.
  • باید: اگر تعاملات سریع کاربر می‌تواند باعث ایجاد پیمایش‌های روان هم‌پوشان در یک محفظه شود، از جلوه‌های پس‌از پیمایش با یک توکن درخواست محافظت کنید، زیرا قطع کردن پیمایش روان، وعده قبلی را حل می‌کند (به‌جای رد کردن).
  • اشکال شناخته‌شده Chrome با پیمایشگر ریشه: Chrome اشکالی دارد که در آن Promise برگردانده‌شده توسط scrollIntoView() خیلی زود حل می‌شود، زمانی که پیمایشگر ریشه سند درحال پیمایش است. انتظار برای scrollIntoView() در محفظه پیمایش تودرتو طبق انتظار کار می‌کند. برای حل این اشکال، محتوایتان را در محفظه پیمایش، مثل div با overflow-y: auto، قرار ندهید.

راهبردهای جایگزین

در مرورگرهایی که از وعده‌های روش پیمایش پشتیبانی نمی‌کنند، scrollIntoView()، scroll()، scrollTo()، و scrollBy() به‌جای Promise، undefined را برمی‌گردانند.

  • ارتقای پیش‌رو (رفتار پیش‌فرض await): چون await undefined جاوا اسکریپت معتبری است که بلافاصله در ریزوظیفه بعدی حل می‌شود، نوشتن await element.scrollIntoView({ behavior: 'smooth' }) در مرورگرهای پشتیبانی‌نشده خطا ایجاد نمی‌کند. درعوض، با اجرای کد پس‌از پیمایش به‌محض شروع پیمایش، به‌طور تدریجی کاهش می‌یابد. برای تزئینات بصری غیربحرانی مانند برجسته‌سازی موقت، این اغلب کافی است.
  • جایگزین وفادار (scrollend + زمان اتمام): اگر هدف Baseline شما از 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);
  });
}