try-scroll.ts
1 import type { Logger } from '@amp/web-apps-logger'; 2 export interface ScrollableElement { 3 scrollTop: number; 4 scrollHeight: number; 5 offsetHeight: number; 6 } 7 8 // Global is okay here as this only runs in the browser 9 let nextTry: number | null = null; 10 11 export function tryScroll( 12 log: Logger, 13 getScrollablePageElement: Function, 14 scrollY: number, 15 ): void { 16 let tries = 0; 17 18 if (nextTry !== null) { 19 window.cancelAnimationFrame(nextTry); 20 } 21 22 nextTry = window.requestAnimationFrame(function doNextTry() { 23 // At 16ms per frame, this is 1600ms 24 // See: https://github.com/DockYard/ember-router-scroll/blob/2f17728f/addon/services/router-scroll.js#L56 25 if (++tries >= 100) { 26 log.warn("wasn't able to restore scroll within 100 frames"); 27 nextTry = null; 28 return; 29 } 30 31 let element = getScrollablePageElement(); 32 if (!element) { 33 log.warn( 34 'could not restore scroll: the scrollable element is missing', 35 ); 36 return; 37 } 38 const { scrollHeight, offsetHeight } = element; 39 40 // Only scroll once we're able to get a full screen of content when 41 // scrollTop is set to scrollY 42 // 43 // +16 is a bit of a fudge factor to count for imperfections in 44 // features like lazy loading. If the scroll position to restore is 45 // the very bottom of the page, then scrollY + offsetHeight must be 46 // exactly scrollHeight. But if lazy loading components (for example) 47 // cause the page to grow by a few pixels, then this will never hold. 48 // Thus, we fudge by a few pixels to be more forgiving in this scenario. 49 const canScroll = scrollY + offsetHeight <= scrollHeight + 16; 50 51 if (!canScroll) { 52 log.info('page is not tall enough for scroll yet', { 53 scrollHeight, 54 offsetHeight, 55 }); 56 57 nextTry = window.requestAnimationFrame(doNextTry); 58 return; 59 } 60 61 element.scrollTop = scrollY; 62 log.info('scroll restored to', scrollY); 63 nextTry = null; 64 }); 65 }