1
1
import { useEffect } from 'react'
2
2
3
3
/**
4
- * For improve the performance, we are using requestAnimationFrame to throttle
5
- * the number of scrolling events we process to be only 1 per frame.
4
+ * Keep in sync the horizontal scroll position of multiple containers. If the user
5
+ * scrolls one container to position X, the scroll position of the other containers is
6
+ * automatically updated to X.
7
+ *
8
+ * TODO: The solution could probably be improved by throttling the scroll events (
9
+ * using window.requestAninamtionFrame) and using a stack instead of setTimeout
10
+ * to know if the scroll events are triggered programmatically or by the user.
6
11
* @param {Array<Object> } scrollContainerRefList - List of React Refs
7
12
*/
8
13
export default function useSyncScroll ( scrollContainerRefList ) {
14
+
9
15
useEffect ( ( ) => {
10
- // Map the list of React ref to the actual list of node.
16
+ // Map the list of React refs to the actual list of node.
11
17
const containerList = scrollContainerRefList . map ( ref => ref . current )
12
- let requestAnimId
18
+ let timeoutId
13
19
14
- function onScroll ( e ) {
15
- const scrolledContainer = e . target
20
+ // Updating the scrollLeft property manually will trigger a scroll event which
21
+ // we want to ignore since it doesn't come from the user and we risk to have the
22
+ // container programmatically updated trying to also update the scroll position of the originally
23
+ // scrolled container.
24
+ // We use this activeContainer state to keep track of which container originally scrolled.
25
+ let activeContainer
26
+
27
+ function syncContainersScroll ( scrolledContainer ) {
28
+ const containersToUpdate = containerList . filter ( container => container !== scrolledContainer )
16
29
17
- containerList . forEach ( ( container ) => {
18
- // Update all containers except the one currently
19
- // scrolling.
20
- if ( container && container !== scrolledContainer ) {
30
+ // Update scroll position of all other containers
31
+ containersToUpdate . forEach ( ( container ) => {
32
+ if ( container ) {
21
33
container . scrollLeft = scrolledContainer . scrollLeft
22
34
}
23
35
} )
24
- // Stop listening to scroll events until next frame
25
- removeScrollListeners ( )
26
- window . requestAnimationFrame ( ( ) => {
27
- // Start listening again to scroll events on the
28
- // next frame.
29
- addScrollListeners ( )
30
- } )
31
36
}
32
37
33
- function removeScrollListeners ( ) {
34
- if ( requestAnimId ) {
35
- window . cancelAnimationFrame ( requestAnimId )
38
+ function onScroll ( e ) {
39
+ const currentlyScrolledContainer = e . target
40
+ if ( ! activeContainer ) {
41
+ activeContainer = currentlyScrolledContainer
36
42
}
43
+
44
+ // Ignore scroll events in other containers since they
45
+ // are very likely to be artificially triggered.
46
+ if ( currentlyScrolledContainer === activeContainer ) {
47
+ syncContainersScroll ( activeContainer )
48
+
49
+ // Delay the removal of the active container.
50
+ if ( timeoutId ) {
51
+ clearTimeout ( timeoutId )
52
+ }
53
+
54
+ // After 200ms without scroll events on the active container
55
+ // we reset the state to no containers active.
56
+ // We use this window of 200ms without any scroll events to make
57
+ // sure that all manually triggered scroll events have already been
58
+ // sent before listening to scroll events in all containers again.
59
+ timeoutId = setTimeout ( ( ) => {
60
+ activeContainer = undefined
61
+ } , 200 )
62
+ }
63
+ }
64
+
65
+ function removeScrollListeners ( ) {
37
66
containerList . forEach ( ( scrollContainer ) => {
38
67
if ( scrollContainer ) {
39
68
scrollContainer . removeEventListener ( 'scroll' , onScroll )
@@ -42,14 +71,10 @@ export default function useSyncScroll(scrollContainerRefList) {
42
71
}
43
72
44
73
function addScrollListeners ( ) {
45
- requestAnimId = window . requestAnimationFrame ( ( ) => {
46
- containerList . forEach ( ( scrollContainer ) => {
47
- if ( scrollContainer ) {
48
- scrollContainer . addEventListener ( 'scroll' , onScroll )
49
- }
50
- } )
51
-
52
- requestAnimId = null
74
+ containerList . forEach ( ( scrollContainer ) => {
75
+ if ( scrollContainer ) {
76
+ scrollContainer . addEventListener ( 'scroll' , onScroll )
77
+ }
53
78
} )
54
79
}
55
80
0 commit comments