11import { watch , onMounted , ref , computed , type Ref , type ComputedRef } from 'vue' ;
22import { isChromium , isSSR , useMediaRef } from './utils' ;
33
4- type UseListenersOptions = {
4+ type UseScrollOptions = {
55 isWindow : ComputedRef < boolean > ;
66 root : Ref < HTMLElement | null > ;
77 matchMedia : Ref < boolean > ;
8- _setActive : ( prevY : number , isCancel ?: { isCancel : boolean } ) => void ;
8+ onScrollUp : ( ) => void ;
9+ onScrollDown : ( { isCancel } : { isCancel : boolean } ) => void ;
10+ onEdgeReached : ( ) => void ;
911} ;
1012
1113const ONCE = { once : true } ;
1214
13- export function useScroll ( { isWindow, root, _setActive, matchMedia } : UseListenersOptions ) {
15+ export function useScroll ( {
16+ isWindow,
17+ root,
18+ matchMedia,
19+ onScrollUp,
20+ onScrollDown,
21+ onEdgeReached,
22+ } : UseScrollOptions ) {
1423 const isClick = useMediaRef ( matchMedia , false ) ;
15- const isReady = ref ( false ) ;
16- const clickY = computed ( ( ) => ( isClick . value ? getNextY ( ) : 0 ) ) ;
24+ const isIdle = ref ( false ) ;
25+ const clickY = computed ( ( ) => ( isClick . value ? getY ( ) : 0 ) ) ;
1726
18- let prevY : number ;
27+ let prevY = getY ( ) ;
1928
20- function getNextY ( ) {
21- return isWindow . value ? window . scrollY : root . value ?. scrollTop || 0 ;
29+ function getY ( ) {
30+ return isWindow . value ? window . scrollY : root . value ?. scrollTop || 0 ; // SSR safe
2231 }
2332
24- function setReady ( maxFrames : number ) {
33+ function setIdle ( maxFrames = 20 ) {
2534 let rafId : DOMHighResTimeStamp | undefined = undefined ;
26- let rafPrevY : number ;
35+ let rafPrevY = getY ( ) ;
2736 let frameCount = 0 ;
2837
2938 function scrollEnd ( ) {
30- const rafNextY = getNextY ( ) ;
31- if ( typeof rafPrevY === 'undefined' || rafPrevY !== rafNextY ) {
39+ frameCount ++ ;
40+
41+ const rafNextY = getY ( ) ;
42+
43+ if ( rafPrevY !== rafNextY ) {
3244 frameCount = 0 ;
3345 rafPrevY = rafNextY ;
3446 return requestAnimationFrame ( scrollEnd ) ;
3547 }
48+
3649 // When equal, wait for n frames after scroll to make sure is idle
37- frameCount ++ ;
3850 if ( frameCount === maxFrames ) {
39- isReady . value = true ;
51+ isIdle . value = true ;
4052 isClick . value = false ;
4153 cancelAnimationFrame ( rafId as DOMHighResTimeStamp ) ;
4254 } else {
@@ -47,15 +59,23 @@ export function useScroll({ isWindow, root, _setActive, matchMedia }: UseListene
4759 rafId = requestAnimationFrame ( scrollEnd ) ;
4860 }
4961
62+ function setActive ( { prevY, isCancel = false } : { prevY : number ; isCancel ?: boolean } ) {
63+ const nextY = getY ( ) ;
64+
65+ if ( nextY < prevY ) {
66+ onScrollUp ( ) ;
67+ } else {
68+ onScrollDown ( { isCancel } ) ;
69+ }
70+
71+ return nextY ;
72+ }
73+
5074 function onScroll ( ) {
5175 // Do not "update" results if scrolling from click
5276 if ( ! isClick . value ) {
53- const nextY = getNextY ( ) ;
54- if ( ! prevY ) {
55- prevY = nextY ;
56- }
57- _setActive ( prevY ) ;
58- prevY = nextY ;
77+ prevY = setActive ( { prevY } ) ;
78+ onEdgeReached ( ) ;
5979 }
6080 }
6181
@@ -64,45 +84,41 @@ export function useScroll({ isWindow, root, _setActive, matchMedia }: UseListene
6484 isClick . value = false ;
6585 }
6686
67- function onSpaceBar ( event : KeyboardEvent ) {
68- if ( event . code === 'Space' ) {
69- reScroll ( ) ;
70- }
71- }
72-
73- function waitForIdle ( ) {
74- setReady ( 20 ) ;
75- }
76-
7787 function onPointerDown ( event : PointerEvent ) {
7888 const isLink = ( event . target as HTMLElement ) . tagName === 'A' ;
7989 const hasLink = ( event . target as HTMLElement ) . closest ( 'a' ) ;
8090
8191 if ( ! isChromium && ! isLink && ! hasLink ) {
8292 reScroll ( ) ;
8393 // ...and force set if canceling scroll
84- _setActive ( clickY . value , { isCancel : true } ) ;
94+ setActive ( { prevY : clickY . value , isCancel : true } ) ;
95+ }
96+ }
97+
98+ function onSpaceBar ( event : KeyboardEvent ) {
99+ if ( event . code === 'Space' ) {
100+ reScroll ( ) ;
85101 }
86102 }
87103
88104 onMounted ( ( ) => {
89105 if ( matchMedia . value && location . hash ) {
90106 // Wait for any eventual scroll to hash triggered by browser to end
91- setReady ( 10 ) ;
107+ setIdle ( 10 ) ;
92108 } else {
93- isReady . value = true ;
109+ isIdle . value = true ;
94110 }
95111 } ) ;
96112
97113 watch (
98- [ isReady , matchMedia , root ] ,
99- ( [ _isReady , _matchMedia , _root ] , _ , onCleanup ) => {
114+ [ isIdle , matchMedia , root ] ,
115+ ( [ _isIdle , _matchMedia , _root ] , _ , onCleanup ) => {
100116 if ( isSSR ) {
101117 return ;
102118 }
103119
104120 const rootEl = isWindow . value ? document : _root ;
105- const isActive = rootEl && _isReady && _matchMedia ;
121+ const isActive = rootEl && _isIdle && _matchMedia ;
106122
107123 if ( isActive ) {
108124 rootEl . addEventListener ( 'scroll' , onScroll , {
@@ -116,7 +132,7 @@ export function useScroll({ isWindow, root, _setActive, matchMedia }: UseListene
116132 }
117133 } ) ;
118134 } ,
119- { immediate : true , flush : 'sync' }
135+ { flush : 'sync' }
120136 ) ;
121137
122138 watch (
@@ -125,18 +141,18 @@ export function useScroll({ isWindow, root, _setActive, matchMedia }: UseListene
125141 const rootEl = isWindow . value ? document : root . value ;
126142
127143 if ( _isClick && rootEl ) {
128- rootEl . addEventListener ( 'scroll' , waitForIdle , ONCE ) ;
129144 rootEl . addEventListener ( 'wheel' , reScroll , ONCE ) ;
130145 rootEl . addEventListener ( 'touchmove' , reScroll , ONCE ) ;
146+ rootEl . addEventListener ( 'scroll' , setIdle as unknown as EventListener , ONCE ) ;
131147 rootEl . addEventListener ( 'keydown' , onSpaceBar as EventListener , ONCE ) ;
132148 rootEl . addEventListener ( 'pointerdown' , onPointerDown as EventListener , ONCE ) ;
133149 }
134150
135151 onCleanup ( ( ) => {
136152 if ( _isClick && rootEl ) {
137- rootEl . removeEventListener ( 'scroll' , waitForIdle ) ;
138153 rootEl . removeEventListener ( 'wheel' , reScroll ) ;
139154 rootEl . removeEventListener ( 'touchmove' , reScroll ) ;
155+ rootEl . removeEventListener ( 'scroll' , setIdle as unknown as EventListener ) ;
140156 rootEl . removeEventListener ( 'keydown' , onSpaceBar as EventListener ) ;
141157 rootEl . removeEventListener ( 'pointerdown' , onPointerDown as EventListener ) ;
142158 }
@@ -145,5 +161,5 @@ export function useScroll({ isWindow, root, _setActive, matchMedia }: UseListene
145161 { flush : 'sync' }
146162 ) ;
147163
148- return isClick ;
164+ return ( ) => ( isClick . value = true ) ;
149165}
0 commit comments