1010 * governing permissions and limitations under the License.
1111 */
1212
13- import { chain , getScrollParent , isIOS , useLayoutEffect , willOpenKeyboard } from '@react-aria/utils' ;
13+ import { chain , getScrollParent , isIOS , isScrollable , useLayoutEffect , willOpenKeyboard } from '@react-aria/utils' ;
1414
1515interface PreventScrollOptions {
1616 /** Whether the scroll lock is disabled. */
@@ -85,18 +85,35 @@ function preventScrollStandard() {
8585// on the window.
8686// 2. Set `overscroll-behavior: contain` on nested scrollable regions so they do not scroll the page when at
8787// 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.
8990// 3. Prevent default on `touchend` events on input elements and handle focusing the element ourselves.
9091// 4. When focus moves to an input, create an off screen input and focus that temporarily. This prevents
9192// Safari from scrolling the page. After a small delay, focus the real input and scroll it into view
9293// ourselves, without scrolling the whole page.
9394function preventScrollMobileSafari ( ) {
9495 let scrollable : Element ;
96+ let allowTouchMove = false ;
9597 let onTouchStart = ( e : TouchEvent ) => {
9698 // 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 ;
100117 }
101118 } ;
102119
@@ -114,6 +131,11 @@ function preventScrollMobileSafari() {
114131 document . head . prepend ( style ) ;
115132
116133 let onTouchMove = ( e : TouchEvent ) => {
134+ // Allow pinch-zooming.
135+ if ( e . touches . length === 2 || allowTouchMove ) {
136+ return ;
137+ }
138+
117139 // Prevent scrolling the window.
118140 if ( ! scrollable || scrollable === document . documentElement || scrollable === document . body ) {
119141 e . preventDefault ( ) ;
0 commit comments