Skip to content

Commit 68c67d4

Browse files
devongovettLFDanLu
andauthored
fix: Apply touch-action by default in usePress (#8047)
* fix: Apply touch-action by default in usePress * fix test --------- Co-authored-by: Daniel Lu <[email protected]>
1 parent fd7b5dd commit 68c67d4

File tree

3 files changed

+86
-7
lines changed

3 files changed

+86
-7
lines changed

packages/@react-aria/interactions/src/usePress.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,7 @@ export function usePress(props: PressHookProps): PressResult {
176176
preventFocusOnPress,
177177
shouldCancelOnPointerExit,
178178
allowTextSelectionOnPress,
179-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
180-
ref: _, // Removing `ref` from `domProps` because TypeScript is dumb
179+
ref: domRef,
181180
...domProps
182181
} = usePressResponderContext(props);
183182

@@ -814,13 +813,26 @@ export function usePress(props: PressHookProps): PressResult {
814813
triggerSyntheticClick
815814
]);
816815

817-
// Remove user-select: none in case component unmounts immediately after pressStart
816+
// Avoid onClick delay for double tap to zoom by default.
817+
useEffect(() => {
818+
let element = domRef?.current;
819+
if (element && (element instanceof getOwnerWindow(element).Element)) {
820+
// Only apply touch-action if not already set by another CSS rule.
821+
let style = getOwnerWindow(element).getComputedStyle(element);
822+
if (style.touchAction === 'auto') {
823+
// touchAction: 'manipulation' is supposed to be equivalent, but in
824+
// Safari it causes onPointerCancel not to fire on scroll.
825+
// https://bugs.webkit.org/show_bug.cgi?id=240917
826+
(element as HTMLElement).style.touchAction = 'pan-x pan-y pinch-zoom';
827+
}
828+
}
829+
}, [domRef]);
818830

831+
// Remove user-select: none in case component unmounts immediately after pressStart
819832
useEffect(() => {
820833
let state = ref.current;
821834
return () => {
822835
if (!allowTextSelectionOnPress) {
823-
824836
restoreTextSelection(state.target ?? undefined);
825837
}
826838
for (let dispose of state.disposables) {

packages/@react-aria/interactions/stories/usePress.stories.tsx

+68-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
Modal,
2020
ModalOverlay
2121
} from 'react-aria-components';
22-
import React from 'react';
22+
import React, {useState} from 'react';
2323
import styles from './usePress-stories.css';
2424
import {usePress} from '@react-aria/interactions';
2525

@@ -234,3 +234,70 @@ export function SoftwareKeyboardIssue() {
234234
</div>
235235
);
236236
}
237+
238+
export function AndroidUnmountIssue() {
239+
let [showButton, setShowButton] = useState(true);
240+
241+
return (
242+
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
243+
<p>This story tests an Android issue where tapping a button that unmounts causes the element behind it to receive onClick.</p>
244+
<div style={{position: 'relative', width: 100, height: 100}}>
245+
<button
246+
type="button"
247+
onClick={() => {
248+
alert('button underneath was pressed');
249+
}}
250+
style={{position: 'absolute', top: 0}}>
251+
Test 2
252+
</button>
253+
{showButton && (
254+
<Button
255+
className="foo"
256+
style={{position: 'absolute', top: 0}}
257+
onPress={() => {
258+
console.log('ra Button pressed');
259+
setShowButton(false);
260+
}}>
261+
Test
262+
</Button>
263+
)}
264+
</div>
265+
</div>
266+
);
267+
}
268+
269+
export function IOSScrollIssue() {
270+
return (
271+
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
272+
<p>This story tests an iOS Safari issue that causes onPointerCancel not to be fired with touch-action: manipulation. Scrolling the list should not trigger onPress.</p>
273+
<div
274+
style={{
275+
marginTop: 10,
276+
width: 500,
277+
height: 100,
278+
overflowY: 'hidden',
279+
overflowX: 'auto',
280+
border: '1px solid black',
281+
display: 'flex',
282+
gap: 8
283+
}}>
284+
{Array.from({length: 10}).map((_, i) => (
285+
<Card key={i} />
286+
))}
287+
</div>
288+
</div>
289+
);
290+
}
291+
292+
function Card() {
293+
return (
294+
<Button
295+
className="foo"
296+
style={{height: 80, width: 150, flexShrink: 0}}
297+
onPress={() => {
298+
alert('pressed');
299+
}}>
300+
Test
301+
</Button>
302+
);
303+
}

packages/@react-aria/selection/src/useSelectableItem.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {DOMAttributes, DOMProps, FocusableElement, Key, LongPressEvent, PointerType, PressEvent, RefObject} from '@react-types/shared';
14-
import {focusSafely, PressProps, useLongPress, usePress} from '@react-aria/interactions';
14+
import {focusSafely, PressHookProps, useLongPress, usePress} from '@react-aria/interactions';
1515
import {getCollectionId, isNonContiguousSelectionModifier} from './utils';
1616
import {isCtrlKeyPressed, mergeProps, openLink, useId, useRouter} from '@react-aria/utils';
1717
import {moveVirtualFocus} from '@react-aria/focus';
@@ -239,7 +239,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
239239
// we want to be able to have the pointer down on the trigger that opens the menu and
240240
// the pointer up on the menu item rather than requiring a separate press.
241241
// For keyboard events, selection still occurs on key down.
242-
let itemPressProps: PressProps = {};
242+
let itemPressProps: PressHookProps = {ref};
243243
if (shouldSelectOnPressUp) {
244244
itemPressProps.onPressStart = (e) => {
245245
modality.current = e.pointerType;

0 commit comments

Comments
 (0)