10
10
* governing permissions and limitations under the License.
11
11
*/
12
12
13
- import { chain , getScrollParent , isIOS , useLayoutEffect , willOpenKeyboard } from '@react-aria/utils' ;
13
+ import { chain , getScrollParent , isIOS , isScrollable , useLayoutEffect , willOpenKeyboard } from '@react-aria/utils' ;
14
14
15
15
interface PreventScrollOptions {
16
16
/** Whether the scroll lock is disabled. */
@@ -85,18 +85,35 @@ function preventScrollStandard() {
85
85
// on the window.
86
86
// 2. Set `overscroll-behavior: contain` on nested scrollable regions so they do not scroll the page when at
87
87
// the top or bottom. Work around a bug where this does not work when the element does not actually overflow
88
- // by preventing default in a `touchmove` event.
88
+ // by preventing default in a `touchmove` event. This is best effort: we can't prevent default when pinch
89
+ // zooming or when an element contains text selection, which may allow scrolling in some cases.
89
90
// 3. Prevent default on `touchend` events on input elements and handle focusing the element ourselves.
90
91
// 4. When focus moves to an input, create an off screen input and focus that temporarily. This prevents
91
92
// Safari from scrolling the page. After a small delay, focus the real input and scroll it into view
92
93
// ourselves, without scrolling the whole page.
93
94
function preventScrollMobileSafari ( ) {
94
95
let scrollable : Element ;
96
+ let allowTouchMove = false ;
95
97
let onTouchStart = ( e : TouchEvent ) => {
96
98
// Store the nearest scrollable parent element from the element that the user touched.
97
- scrollable = getScrollParent ( e . target as Element , true ) ;
98
- if ( scrollable === document . documentElement && scrollable === document . body ) {
99
- return ;
99
+ let target = e . target as Element ;
100
+ scrollable = isScrollable ( target ) ? target : getScrollParent ( target , true ) ;
101
+ allowTouchMove = false ;
102
+
103
+ // If the target is selected, don't preventDefault in touchmove to allow user to adjust selection.
104
+ let selection = target . ownerDocument . defaultView ! . getSelection ( ) ;
105
+ if ( selection && ! selection . isCollapsed && selection . containsNode ( target , true ) ) {
106
+ allowTouchMove = true ;
107
+ }
108
+
109
+ // If this is a focused input element with a selected range, allow user to drag the selection handles.
110
+ if (
111
+ 'selectionStart' in target &&
112
+ 'selectionEnd' in target &&
113
+ ( target . selectionStart as number ) < ( target . selectionEnd as number ) &&
114
+ target . ownerDocument . activeElement === target
115
+ ) {
116
+ allowTouchMove = true ;
100
117
}
101
118
} ;
102
119
@@ -114,6 +131,11 @@ function preventScrollMobileSafari() {
114
131
document . head . prepend ( style ) ;
115
132
116
133
let onTouchMove = ( e : TouchEvent ) => {
134
+ // Allow pinch-zooming.
135
+ if ( e . touches . length === 2 || allowTouchMove ) {
136
+ return ;
137
+ }
138
+
117
139
// Prevent scrolling the window.
118
140
if ( ! scrollable || scrollable === document . documentElement || scrollable === document . body ) {
119
141
e . preventDefault ( ) ;
0 commit comments