1
+import React, {useRef, useEffect, startTransition} from 'react';
2
+
3
+// Example of a Component that can recognize swipe gestures using a ScrollTimeline
4
+// without scrolling its own content. Allowing it to be used as an inert gesture
5
+// recognizer to drive a View Transition.
6
+export default function SwipeRecognizer({
7
+ action,
8
+ children,
9
+ direction,
10
+ gesture,
11
+}) {
12
+ if (direction == null) {
13
+ direction = 'left';
14
+ }
15
+ const axis = direction === 'left' || direction === 'right' ? 'x' : 'y';
16
+
17
+ const scrollRef = useRef(null);
18
+ const activeGesture = useRef(null);
19
+ function onScroll() {
20
+ if (activeGesture.current !== null) {
21
+ return;
22
+ }
23
+ if (typeof ScrollTimeline !== 'function') {
24
+ return;
25
+ }
26
+ // eslint-disable-next-line no-undef
27
+ const scrollTimeline = new ScrollTimeline({
28
+ source: scrollRef.current,
29
+ axis: axis,
30
+ });
31
+ activeGesture.current = gesture(scrollTimeline, {
32
+ range: [0, direction === 'left' || direction === 'up' ? 100 : 0, 100],
33
+ });
34
+ }
35
+ function onScrollEnd() {
36
+ if (activeGesture.current !== null) {
37
+ const cancelGesture = activeGesture.current;
38
+ activeGesture.current = null;
39
+ cancelGesture();
40
+ }
41
+ let changed;
42
+ const scrollElement = scrollRef.current;
43
+ if (axis === 'x') {
44
+ const halfway =
45
+ (scrollElement.scrollWidth - scrollElement.clientWidth) / 2;
46
+ changed =
47
+ direction === 'left'
48
+ ? scrollElement.scrollLeft < halfway
49
+ : scrollElement.scrollLeft > halfway;
50
+ } else {
51
+ const halfway =
52
+ (scrollElement.scrollHeight - scrollElement.clientHeight) / 2;
53
+ changed =
54
+ direction === 'up'
55
+ ? scrollElement.scrollTop < halfway
56
+ : scrollElement.scrollTop > halfway;
57
+ }
58
+ // Reset scroll
59
+ if (changed) {
60
+ // Trigger side-effects
61
+ startTransition(action);
62
+ }
63
+ }
64
+
65
+ useEffect(() => {
66
+ const scrollElement = scrollRef.current;
67
+ switch (direction) {
68
+ case 'left':
69
+ scrollElement.scrollLeft =
70
+ scrollElement.scrollWidth - scrollElement.clientWidth;
71
+ break;
72
+ case 'right':
73
+ scrollElement.scrollLeft = 0;
74
+ break;
75
+ case 'up':
76
+ scrollElement.scrollTop =
77
+ scrollElement.scrollHeight - scrollElement.clientHeight;
78
+ break;
79
+ case 'down':
80
+ scrollElement.scrollTop = 0;
81
+ break;
82
+ default:
83
+ break;
84
+ }
85
+ }, [direction]);
86
+
87
+ const scrollStyle = {
88
+ position: 'relative',
89
+ padding: '0px',
90
+ margin: '0px',
91
+ border: '0px',
92
+ width: axis === 'x' ? '100%' : null,
93
+ height: axis === 'y' ? '100%' : null,
94
+ overflow: 'scroll hidden',
95
+ touchAction: 'pan-' + direction,
96
+ // Disable overscroll on Safari which moves the sticky content.
97
+ // Unfortunately, this also means that we disable chaining. We should only disable
98
+ // it if the parent is not scrollable in this axis.
99
+ overscrollBehaviorX: axis === 'x' ? 'none' : 'auto',
100
+ overscrollBehaviorY: axis === 'y' ? 'none' : 'auto',
101
+ scrollSnapType: axis + ' mandatory',
102
+ scrollbarWidth: 'none',
103
+ };
104
+
105
+ const overScrollStyle = {
106
+ position: 'relative',
107
+ padding: '0px',
108
+ margin: '0px',
109
+ border: '0px',
110
+ width: axis === 'x' ? '200%' : null,
111
+ height: axis === 'y' ? '200%' : null,
112
+ };
113
+
114
+ const snapStartStyle = {
115
+ position: 'absolute',
116
+ padding: '0px',
117
+ margin: '0px',
118
+ border: '0px',
119
+ width: axis === 'x' ? '50%' : '100%',
120
+ height: axis === 'y' ? '50%' : '100%',
121
+ left: '0px',
122
+ top: '0px',
123
+ scrollSnapAlign: 'center',
124
+ };
125
+
126
+ const snapEndStyle = {
127
+ position: 'absolute',
128
+ padding: '0px',
129
+ margin: '0px',
130
+ border: '0px',
131
+ width: axis === 'x' ? '50%' : '100%',
132
+ height: axis === 'y' ? '50%' : '100%',
133
+ right: '0px',
134
+ bottom: '0px',
135
+ scrollSnapAlign: 'center',
136
+ };
137
+
138
+ // By placing the content in a sticky box we ensure that it doesn't move when
139
+ // we scroll. Unless done so by the View Transition.
140
+ const stickyStyle = {
141
+ position: 'sticky',
142
+ padding: '0px',
143
+ margin: '0px',
144
+ border: '0px',
145
+ left: '0px',
146
+ top: '0px',
147
+ width: axis === 'x' ? '50%' : null,
148
+ height: axis === 'y' ? '50%' : null,
149
+ overflow: 'hidden',
150
+ };
151
+
152
+ return (
153
+ <div
154
+ style={scrollStyle}
155
+ onScroll={onScroll}
156
+ onScrollEnd={onScrollEnd}
157
+ ref={scrollRef}>
158
+ <div style={overScrollStyle}>
159
+ <div style={snapStartStyle} />
160
+ <div style={snapEndStyle} />
161
+ <div style={stickyStyle}>{children}</div>
162
+ </div>
163
+ </div>
164
+ );
165
+}