عند الانتقال آليًا إلى قسم أو عنصر (خاصةً باستخدام behavior: 'smooth')، فإنّ بدء إجراء متابعة على الفور، مثل وميض حالة تمييز أو بدء حركة دخول أو نقل التركيز، يؤدي إلى تشغيل التأثير بينما لا يزال الهدف خارج الشاشة أو في منتصف الانتقال.
في السابق، كان على المطوّرين تخمين مدة الانتقال باستخدام setTimeout() (الذي يتم تنشيطه مبكرًا جدًا في عمليات الانتقال الطويلة ومتأخرًا جدًا في عمليات الانتقال القصيرة) أو التفاف متتبِّع الأحداث scrollend يدويًا في Promise باستخدام موقّت احتياطي (لأنّ scrollend لا يتم تنشيطه عندما يكون الهدف معروضًا بالفعل ولا يحدث أي انتقال).
تُرجع طرق التمرير الآلي في Element (scrollIntoView() وscroll() وscrollTo() وscrollBy()) وWindow (scroll() وscrollTo() وscrollBy()) Promise يتم حلّه عند اكتمال عملية التمرير. إذا كان الهدف في موضع الانتقال المطلوب ولا حاجة إلى الانتقال، يتم حلّ Promise الذي تم عرضه على الفور بدون توقّف.
التنفيذ
- وضع علامة على المعالج كـ
async: استخدِم دالةasyncلمتتبِّع الأحداث أو معالج التنقّل الذي يؤدي إلى الانتقال إلى الصفحة. - انتظار طريقة الانتقال مباشرةً: يمكنك استدعاء
await element.scrollIntoView({ behavior: 'smooth', ... })(أوawait element.scrollTo(...)أوawait window.scrollTo(...)أو غير ذلك). يتوقف التنفيذ مؤقتًا إلى أن ينتهي المتصفّح من عرض تأثيرات التمرير ويستقر موضع التمرير. - تطبيق تأثير ما بعد الانتقال: يمكنك تفعيل فئة التمييز أو بدء الحركة أو نقل التركيز بعد اكتمال تعبير
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);
});
}