כשגוללים באופן פרוגרמטי לקטע או לרכיב (במיוחד באמצעות behavior: 'smooth'), הפעלה של פעולת המשך באופן מיידי – כמו הבהוב של מצב הדגשה, הפעלה של אנימציית כניסה או העברת המיקוד – גורמת להפעלת האפקט כשהיעד עדיין מחוץ למסך או באמצע הגלילה.
בעבר, המפתחים היו צריכים לנחש את משך הגלילה באמצעות setTimeout() (שמופעל מוקדם מדי בגלילות ארוכות ומאוחר מדי בגלילות קצרות) או לעטוף באופן ידני רכיב event listener מסוג scrollend ב-Promise עם טיימר חלופי (כי scrollend לא מופעל כשהיעד כבר מוצג ולא מתבצעת גלילה).
השיטות הפרוגרמטיות לגלילה ב-Element (scrollIntoView(), scroll(), scrollTo(), scrollBy()) וב-Window (scroll(), scrollTo(), scrollBy()) מחזירות Promise שמתבצעת כשהגלילה מסתיימת. אם היעד כבר נמצא במיקום הגלילה המבוקש ואין צורך בגלילה, ה-Promise שמוחזר נפתר מיד בלי להיתקע.
הטמעה
- מסמנים את ה-handler כ-
async: משתמשים בפונקציהasyncעבור רכיב ה-event listener או ה-navigation handler שמפעילים את הגלילה. - המתנה ישירה ל-scroll method: קוראים ל-
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()אחרי שממתינים ל-method של גלילה, כדי שהדפדפן לא יקפוץ או יגלול פעם נוספת. - מומלץ: להגן על אפקטים שמופעלים אחרי גלילה באמצעות אסימון בקשה, אם אינטראקציות מהירות של משתמשים יכולות להפעיל גלילות חלקות חופפות באותו מאגר, כי הפרעה לגלילה חלקה פותרת (ולא דוחה) את ההבטחה הקודמת.
- באג ידוע ב-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+ זמן קצוב לתפוגה): אם יעד 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);
});
}