@@ -92,8 +92,9 @@ function preventScrollStandard() {
92
92
//
93
93
// 1. Prevent default on `touchmove` events that are not in a scrollable element. This prevents touch scrolling
94
94
// on the window.
95
- // 2. Prevent default on `touchmove` events inside a scrollable element when the scroll position is at the
96
- // top or bottom. This avoids the whole page scrolling instead, but does prevent overscrolling.
95
+ // 2. Set `overscroll-behavior: contain` on nested scrollable regions so they do not scroll the page when at
96
+ // the top or bottom. Work around a bug where this does not work when the element does not actually overflow
97
+ // by preventing default in a `touchmove` event.
97
98
// 3. Prevent default on `touchend` events on input elements and handle focusing the element ourselves.
98
99
// 4. When focusing an input, apply a transform to trick Safari into thinking the input is at the top
99
100
// of the page, which prevents it from scrolling the page. After the input is focused, scroll the element
@@ -105,15 +106,20 @@ function preventScrollStandard() {
105
106
// to navigate to an input with the next/previous buttons that's outside a modal.
106
107
function preventScrollMobileSafari ( ) {
107
108
let scrollable : Element ;
108
- let lastY = 0 ;
109
+ let restoreScrollableStyles ;
109
110
let onTouchStart = ( e : TouchEvent ) => {
110
111
// Store the nearest scrollable parent element from the element that the user touched.
111
112
scrollable = getScrollParent ( e . target as Element ) ;
112
113
if ( scrollable === document . documentElement && scrollable === document . body ) {
113
114
return ;
114
115
}
115
116
116
- lastY = e . changedTouches [ 0 ] . pageY ;
117
+ // Prevent scrolling up when at the top and scrolling down when at the bottom
118
+ // of a nested scrollable area, otherwise mobile Safari will start scrolling
119
+ // the window instead.
120
+ if ( scrollable instanceof HTMLElement && window . getComputedStyle ( scrollable ) . overscrollBehavior === 'auto' ) {
121
+ restoreScrollableStyles = setStyle ( scrollable , 'overscrollBehavior' , 'contain' ) ;
122
+ }
117
123
} ;
118
124
119
125
let onTouchMove = ( e : TouchEvent ) => {
@@ -123,23 +129,15 @@ function preventScrollMobileSafari() {
123
129
return ;
124
130
}
125
131
126
- // Prevent scrolling up when at the top and scrolling down when at the bottom
127
- // of a nested scrollable area, otherwise mobile Safari will start scrolling
128
- // the window instead. Unfortunately, this disables bounce scrolling when at
129
- // the top but it's the best we can do.
130
- let y = e . changedTouches [ 0 ] . pageY ;
131
- let scrollTop = scrollable . scrollTop ;
132
- let bottom = scrollable . scrollHeight - scrollable . clientHeight ;
133
-
134
- if ( bottom === 0 ) {
135
- return ;
136
- }
137
-
138
- if ( ( scrollTop <= 0 && y > lastY ) || ( scrollTop >= bottom && y < lastY ) ) {
132
+ // overscroll-behavior should prevent scroll chaining, but currently does not
133
+ // if the element doesn't actually overflow. https://bugs.webkit.org/show_bug.cgi?id=243452
134
+ // This checks that both the width and height do not overflow, otherwise we might
135
+ // block horizontal scrolling too. In that case, adding `touch-action: pan-x` to
136
+ // the element will prevent vertical page scrolling. We can't add that automatically
137
+ // because it must be set before the touchstart event.
138
+ if ( scrollable . scrollHeight === scrollable . clientHeight && scrollable . scrollWidth === scrollable . clientWidth ) {
139
139
e . preventDefault ( ) ;
140
140
}
141
-
142
- lastY = y ;
143
141
} ;
144
142
145
143
let onTouchEnd = ( e : TouchEvent ) => {
@@ -148,6 +146,7 @@ function preventScrollMobileSafari() {
148
146
// Apply this change if we're not already focused on the target element
149
147
if ( willOpenKeyboard ( target ) && target !== document . activeElement ) {
150
148
e . preventDefault ( ) ;
149
+ setupStyles ( ) ;
151
150
152
151
// Apply a transform to trick Safari into thinking the input is at the top of the page
153
152
// so it doesn't try to scroll it into view. When tapping on an input, this needs to
@@ -158,11 +157,17 @@ function preventScrollMobileSafari() {
158
157
target . style . transform = '' ;
159
158
} ) ;
160
159
}
160
+
161
+ if ( restoreScrollableStyles ) {
162
+ restoreScrollableStyles ( ) ;
163
+ }
161
164
} ;
162
165
163
166
let onFocus = ( e : FocusEvent ) => {
164
167
let target = e . target as HTMLElement ;
165
168
if ( willOpenKeyboard ( target ) ) {
169
+ setupStyles ( ) ;
170
+
166
171
// Transform also needs to be applied in the focus event in cases where focus moves
167
172
// other than tapping on an input directly, e.g. the next/previous buttons in the
168
173
// software keyboard. In these cases, it seems applying the transform in the focus event
@@ -190,40 +195,50 @@ function preventScrollMobileSafari() {
190
195
}
191
196
} ;
192
197
193
- let onWindowScroll = ( ) => {
194
- // Last resort. If the window scrolled, scroll it back to the top.
195
- // It should always be at the top because the body will have a negative margin (see below).
196
- window . scrollTo ( 0 , 0 ) ;
197
- } ;
198
+ let restoreStyles = null ;
199
+ let setupStyles = ( ) => {
200
+ if ( restoreStyles ) {
201
+ return ;
202
+ }
198
203
199
- // Record the original scroll position so we can restore it.
200
- // Then apply a negative margin to the body to offset it by the scroll position. This will
201
- // enable us to scroll the window to the top, which is required for the rest of this to work .
202
- let scrollX = window . pageXOffset ;
203
- let scrollY = window . pageYOffset ;
204
+ let onWindowScroll = ( ) => {
205
+ // Last resort. If the window scrolled, scroll it back to the top.
206
+ // It should always be at the top because the body will have a negative margin (see below) .
207
+ window . scrollTo ( 0 , 0 ) ;
208
+ } ;
204
209
205
- let restoreStyles = chain (
206
- setStyle ( document . documentElement , 'paddingRight' , `${ window . innerWidth - document . documentElement . clientWidth } px` ) ,
207
- setStyle ( document . documentElement , 'overflow' , 'hidden' ) ,
208
- setStyle ( document . body , 'marginTop' , `-${ scrollY } px` )
209
- ) ;
210
+ // Record the original scroll position so we can restore it.
211
+ // Then apply a negative margin to the body to offset it by the scroll position. This will
212
+ // enable us to scroll the window to the top, which is required for the rest of this to work.
213
+ let scrollX = window . pageXOffset ;
214
+ let scrollY = window . pageYOffset ;
215
+
216
+ restoreStyles = chain (
217
+ addEvent ( window , 'scroll' , onWindowScroll ) ,
218
+ setStyle ( document . documentElement , 'paddingRight' , `${ window . innerWidth - document . documentElement . clientWidth } px` ) ,
219
+ setStyle ( document . documentElement , 'overflow' , 'hidden' ) ,
220
+ setStyle ( document . body , 'marginTop' , `-${ scrollY } px` ) ,
221
+ ( ) => {
222
+ window . scrollTo ( scrollX , scrollY ) ;
223
+ }
224
+ ) ;
210
225
211
- // Scroll to the top. The negative margin on the body will make this appear the same.
212
- window . scrollTo ( 0 , 0 ) ;
226
+ // Scroll to the top. The negative margin on the body will make this appear the same.
227
+ window . scrollTo ( 0 , 0 ) ;
228
+ } ;
213
229
214
230
let removeEvents = chain (
215
231
addEvent ( document , 'touchstart' , onTouchStart , { passive : false , capture : true } ) ,
216
232
addEvent ( document , 'touchmove' , onTouchMove , { passive : false , capture : true } ) ,
217
233
addEvent ( document , 'touchend' , onTouchEnd , { passive : false , capture : true } ) ,
218
- addEvent ( document , 'focus' , onFocus , true ) ,
219
- addEvent ( window , 'scroll' , onWindowScroll )
234
+ addEvent ( document , 'focus' , onFocus , true )
220
235
) ;
221
236
222
237
return ( ) => {
223
238
// Restore styles and scroll the page back to where it was.
224
- restoreStyles ( ) ;
239
+ restoreScrollableStyles ?.( ) ;
240
+ restoreStyles ?.( ) ;
225
241
removeEvents ( ) ;
226
- window . scrollTo ( scrollX , scrollY ) ;
227
242
} ;
228
243
}
229
244
0 commit comments