Gdy programowo przewijasz do sekcji lub elementu (zwłaszcza za pomocą behavior: 'smooth'), natychmiastowe wywołanie działania, takiego jak miganie stanu wyróżnienia, rozpoczęcie animacji wejścia lub przeniesienie fokusu, powoduje, że efekt jest uruchamiany, gdy cel jest nadal poza ekranem lub w trakcie przewijania.
Wcześniej deweloperzy musieli zgadywać czas przewijania za pomocą setTimeout() (który uruchamia się zbyt wcześnie przy długich przewijaniach i zbyt późno przy krótkich) lub ręcznie opakowywać detektor zdarzeń scrollend w Promise z zegarem zastępczym (ponieważ scrollend nie uruchamia się, gdy cel jest już widoczny i nie następuje przewijanie).
Metody automatycznego przewijania w Element (scrollIntoView(), scroll(), scrollTo(), scrollBy()) i Window (scroll(), scrollTo(), scrollBy()) zwracają obiekt Promise, który jest rozwiązywany po zakończeniu operacji przewijania. Jeśli element docelowy znajduje się już w żądanej pozycji przewijania i nie trzeba go przewijać, zwracany element Promise jest natychmiast rozwiązywany bez zawieszania się.
Implementacja
- Oznacz moduł obsługi jako
async: użyj funkcjiasyncw detektorze zdarzeń lub module obsługi nawigacji, który wyzwala przewijanie. - Bezpośrednie oczekiwanie na metodę przewijania: wywołaj
await element.scrollIntoView({ behavior: 'smooth', ... })(lubawait element.scrollTo(...),await window.scrollTo(...)itd.). Wykonywanie jest wstrzymywane, dopóki przeglądarka nie zakończy animacji przewijania i nie ustali się pozycja przewijania. - Zastosuj efekt po przewinięciu: przełącz klasę wyróżnienia, rozpocznij animację lub przenieś zaznaczenie po zakończeniu wyrażenia
await.
Przerwane przewijanie i zarządzanie skupieniem uwagi
- Obsługa szybkiego lub przerywanego przewijania: jeśli nowe przewijanie rozpocznie się w tym samym kontenerze przewijania, zanim zakończy się trwające płynne przewijanie, przeglądarka przerywa trwające płynne przewijanie i rozwiązuje (nie odrzuca) wszystkie oczekujące obietnice przewijania w tym kontenerze. Jeśli chcesz pominąć efekt po przewinięciu w przypadku zastąpionych przewinięć, gdy użytkownik szybko klika, śledź przyrostowy identyfikator operacji i wróć do początku po
await, jeśli rozpoczęto nowsze przewijanie:
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');
}
- Ustawianie fokusu na elemencie docelowym po przewinięciu: gdy po wywołaniu
await targetElement.scrollIntoView(...)przenosisz fokus klawiatury na element docelowy (lub nagłówek w nim za pomocątabindex="-1"), przekaż{ preventScroll: true }dofocus(), aby wywołanie fokusu nie zainicjowało drugiego, konkurencyjnego przewijania:
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetHeading.focus({ preventScroll: true });
Ograniczenia i sprawdzone metody
- OBOWIĄZKOWO: poczekaj na
Promisezwracane bezpośrednio przezscrollIntoView(),scroll(),scrollTo()lubscrollBy(), zamiast zgadywać czas trwania przewijania za pomocąsetTimeout(). - NIE umieszczaj
scrollendw ręcznymnew Promise()jako podstawowej implementacji –scrollendnie uruchamia się, gdy cel jest już widoczny i nie ma odległości przewijania, podczas gdy w takim przypadku obietnice metody przewijania są automatycznie rozwiązywane. - DO: Pass
{ preventScroll: true }when calling.focus()after awaiting a scroll method so the browser does not jump or scroll a second time. - DO: Guard post-scroll effects with a request token if rapid user interactions can trigger overlapping płynne przewijanie on the same container, since interrupting a płynne przewijanie resolves (rather than rejects) the previous promise.
- Znany błąd Chrome związany z głównym elementem przewijającym: w Chrome występuje błąd, który powoduje, że obiekt
Promisezwracany przezscrollIntoView()jest rozwiązywany zbyt wcześnie, gdy przewijany jest główny element przewijający dokumentu. Oczekiwanie nascrollIntoView()w zagnieżdżonym kontenerze przewijania działa zgodnie z oczekiwaniami. Nie umieszczaj treści w kontenerze przewijania, takim jakdivzoverflow-y: auto, aby obejść ten błąd.
Strategie zastępcze
W przeglądarkach, które nie obsługują obietnic metod przewijania, scrollIntoView(), scroll(), scrollTo() i scrollBy() zwracają undefined zamiast Promise.
- Progresywne ulepszanie (domyślne działanie
await): ponieważawait undefinedto prawidłowy kod JavaScript, który jest natychmiast rozwiązywany w następnym mikrozadaniu, zapisanieawait element.scrollIntoView({ behavior: 'smooth' })nie spowoduje błędu w nieobsługiwanych przeglądarkach. Zamiast tego kod po przewinięciu jest uruchamiany, gdy tylko rozpocznie się przewijanie. W przypadku niekrytycznych elementów wizualnych, takich jak tymczasowe podświetlenie, często jest to wystarczające. - Wierne działanie zastępcze (
scrollend+ limit czasu): jeśli Twój cel Baseline nie obsługujescroll-promises, a działanie po przewinięciu musi poczekać, aż przewijanie zakończy się w starszych przeglądarkach, sprawdź, czy zwracana wartość toPromise(result instanceof Promise). Jeśli jest toundefined, poczekaj na zdarzeniescrollendw kontenerze przewijania z krótkim limitem czasu działania zastępczego na wypadek, gdyby element był już widoczny i nie nastąpiło przewijanie:
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);
});
}