Quando si scorre in modo programmatico fino a una sezione o un elemento (soprattutto con behavior: 'smooth'), l'attivazione immediata di un'azione successiva, come il lampeggiamento di uno stato di evidenziazione, l'avvio di un'animazione di ingresso o lo spostamento dello stato attivo, comporta l'esecuzione dell'effetto mentre il target è ancora fuori dallo schermo o a metà dello scorrimento.
In precedenza, gli sviluppatori dovevano indovinare la durata dello scorrimento con setTimeout() (che si attiva troppo presto negli scorrimenti lunghi e troppo tardi in quelli brevi) o eseguire manualmente il wrapping di un listener di eventi scrollend in un Promise con un timer di riserva (perché scrollend non si attiva quando il target è già visibile e non si verifica alcuno scorrimento).
I metodi di scorrimento programmatico su Element (scrollIntoView(), scroll(), scrollTo(), scrollBy()) e Window (scroll(), scrollTo(), scrollBy()) restituiscono un Promise che si risolve al termine dell'operazione di scorrimento. Se il target si trova già nella posizione di scorrimento richiesta e non è necessario scorrere, l'oggetto Promise restituito viene risolto immediatamente senza bloccarsi.
Implementazione
- Contrassegna il gestore come
async: utilizza una funzioneasyncper il listener di eventi o il gestore di navigazione che attiva lo scorrimento. - Attendi direttamente il metodo di scorrimento: chiama
await element.scrollIntoView({ behavior: 'smooth', ... })(oawait element.scrollTo(...),await window.scrollTo(...)e così via). L'esecuzione viene messa in pausa finché il browser non termina l'animazione di scorrimento e la posizione di scorrimento non si stabilizza. - Applica l'effetto post-scorrimento: attiva/disattiva la classe di evidenziazione, avvia l'animazione o sposta lo stato attivo dopo il completamento dell'espressione
await.
Scorrimenti interrotti e gestione del focus
- Gestione degli scorrimenti rapidi/interrotti: se un nuovo scorrimento inizia sullo stesso contenitore di scorrimento prima che uno scorrimento fluido in corso termini, il browser interrompe lo scorrimento fluido in corso e risolve (non rifiuta) tutte le promesse di scorrimento in attesa su quel contenitore. Se devi sopprimere l'effetto post-scorrimento per gli scorrimenti sostituiti quando un utente fa clic rapidamente, monitora un ID operazione incrementale e restituisci un valore anticipato dopo
awaitse è stato avviato uno scorrimento più recente:
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');
}
- Spostamento della selezione della tastiera sul target dopo lo scorrimento: quando sposti la selezione della tastiera sull'elemento target (o su un'intestazione al suo interno con
tabindex="-1") dopoawait targetElement.scrollIntoView(...), passa{ preventScroll: true }afocus()in modo che la chiamata di selezione non avvii un secondo scorrimento in competizione:
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetHeading.focus({ preventScroll: true });
Vincoli e best practice
- OBBLIGATORIO: attendi il
Promiserestituito direttamente dascrollIntoView(),scroll(),scrollTo()oscrollBy()anziché indovinare la durata dello scorrimento consetTimeout(). - NON inserire
scrollendin unnew Promise()manuale come implementazione principale:scrollendnon si attiva quando il target è già visibile e non viene percorsa alcuna distanza di scorrimento, mentre le promesse del metodo di scorrimento si risolvono automaticamente in questo caso. - SÌ: passa
{ preventScroll: true }quando chiami.focus()dopo aver atteso un metodo di scorrimento, in modo che il browser non salti o scorra una seconda volta. - DA FARE: proteggi gli effetti post-scorrimento con un token di richiesta se le interazioni rapide degli utenti possono attivare scorrimenti fluidi sovrapposti sullo stesso contenitore, poiché l'interruzione di uno scorrimento fluido risolve (anziché rifiutare) la promessa precedente.
- Bug noto di Chrome con lo scroller principale: Chrome presenta un bug per cui la
Promiserestituita dascrollIntoView()si risolve troppo presto quando è lo scroller principale del documento a scorrere. L'attesa di unscrollIntoView()su un container di scorrimento nidificato funziona come previsto. Non inserire i contenuti in un contenitore di scorrimento, ad esempiodivconoverflow-y: auto, come soluzione alternativa per questo bug.
Strategie di riserva
Nei browser che non supportano le promise del metodo di scorrimento, scrollIntoView(), scroll(), scrollTo() e scrollBy() restituiscono undefined anziché Promise.
- Potenziamento progressivo (comportamento predefinito di
await): poichéawait undefinedè un codice JavaScript valido che si risolve immediatamente nel microtask successivo, la scrittura diawait element.scrollIntoView({ behavior: 'smooth' })non genera un errore nei browser non supportati. Invece, si degrada gradualmente eseguendo il codice post-scorrimento non appena inizia lo scorrimento. Per elementi visivi non critici, come un'evidenziazione temporanea, questo è spesso sufficiente. - Fallback fedele (
scrollend+ timeout): se il tuo target Baseline non supportascroll-promisese l'azione post-scorrimento deve attendere il completamento dello scorrimento nei browser meno recenti, controlla se il valore restituito è unPromise(result instanceof Promise). Se èundefined, attendi l'eventoscrollendsul container di scorrimento con un breve timeout di fallback nel caso in cui l'elemento sia già visibile e non si verifichi lo scorrimento:
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);
});
}