تفعيل التأثيرات بعد اكتمال الانتقال الآلي

عند الانتقال آليًا إلى قسم أو عنصر (خاصةً باستخدام 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 });

القيود وأفضل الممارسات

  • إجراء إلزامي: يجب انتظار Promise الذي يتم عرضه مباشرةً من خلال scrollIntoView() أو scroll() أو scrollTo() أو scrollBy() بدلاً من تخمين مُدد الانتقال باستخدام setTimeout().
  • لا تغلِّف scrollend في new Promise() يدوي كطريقة أساسية للتنفيذ، لأنّ scrollend لا يتم تفعيله عندما يكون الهدف معروضًا على الشاشة ولا يتم الانتقال إلى أي مسافة، في حين أنّ طريقة الانتقال إلى الأسفل أو الأعلى تضمن الحلّ تلقائيًا في هذه الحالة.
  • ما يجب فعله: مرِّر { preventScroll: true } عند استدعاء .focus() بعد انتظار طريقة الانتقال حتى لا ينتقل المتصفّح أو يتخطّى مرة ثانية.
  • يجب: حماية تأثيرات ما بعد الانتقال باستخدام رمز مميّز للطلب إذا كان بإمكان تفاعلات المستخدم السريعة أن تؤدي إلى تداخل عمليات الانتقال السلسة في الحاوية نفسها، لأنّ مقاطعة الانتقال السلس يؤدي إلى حلّ (بدلاً من رفض) الوعد السابق.
  • خطأ معروف في Chrome يتعلّق بأداة الانتقال الجذرية: يتضمّن Chrome خطأً يتم فيه حلّ Promise الذي يعرضه scrollIntoView() في وقت مبكر جدًا عندما تكون أداة الانتقال الجذرية للمستند هي التي تنتقل. يعمل انتظار scrollIntoView() في حاوية الانتقال المُدمَجة على النحو المتوقّع. لا تضع المحتوى في حاوية قابلة للتمرير، مثل div مع overflow-y: auto، كحل بديل لهذا الخطأ.

استراتيجيات احتياطية

في المتصفّحات التي لا تتيح استخدام وعود طريقة التمرير، تُعرِض scrollIntoView() وscroll() وscrollTo() وscrollBy() القيمة undefined بدلاً من Promise.

  • التحسين التدريجي (السلوك التلقائي لـ 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);
  });
}