You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 4c001cb
Browse filesBrowse the repository at this point in the historyBrowse files
[Web] Keep detector-owned DOM props across a full config replace (#4522)
## Description
`touchAction`, `userSelect` and `enableContextMenu` are props of
`GestureDetector`, not of the gesture config. On web the detector sends
them to the handler once, as a partial `updateGestureHandlerConfig`,
when the handler attaches. Every full `setGestureHandlerConfig` (mount,
and every re-render of the component that owns the gesture) runs
`resetConfig` first, which cleared those three fields and, since #4488,
refreshed the DOM to `touch-action: none` and `user-select: none`. The
detector's prop-diff effect only re-sends when the props change, so
nothing repaired it.
Before #4488 the fields were already wiped, but the DOM was not
refreshed, so it worked by accident. v2 is not affected: the legacy
detector copies these props into each gesture's config, so the full set
carries them.
## Test plan
<details>
<summary>Tested on the following code:</summary>
```tsx
import React, { useEffect, useRef, useState } from 'react';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import {
GestureDetector,
Pressable,
usePanGesture,
} from 'react-native-gesture-handler';
import { COLORS } from '../../../common';
// Web only. The detector props `touchAction`, `userSelect` and
// `enableContextMenu` are sent to the handler once, as a partial update, when
// the detector attaches. Every full config set (mount, and each re-render of
// the owner) runs `resetConfig` first, which clears them and refreshes the DOM
// to the defaults. Expected after mount: the box reports
// `touch-action: pan-y` and `user-select: text`, and the page scrolls
// vertically over the box. Bug: `none` / `none`, no vertical scroll.
type DomStyles = {
inlineTouchAction: string;
computedTouchAction: string;
inlineUserSelect: string;
computedUserSelect: string;
};
function readDomStyles(node: unknown): DomStyles | null {
if (typeof window === 'undefined' || !(node instanceof HTMLElement)) {
return null;
}
const computed = window.getComputedStyle(node);
return {
inlineTouchAction: node.style.touchAction,
computedTouchAction: computed.touchAction,
inlineUserSelect: node.style.userSelect,
computedUserSelect: computed.userSelect,
};
}
export default function WebDetectorPropsExample() {
const [renders, setRenders] = useState(0);
const [propsSet, setPropsSet] = useState(true);
const [activations, setActivations] = useState(0);
const [domStyles, setDomStyles] = useState<DomStyles | null>(null);
const boxRef = useRef<React.ComponentRef<typeof View>>(null);
const pan = usePanGesture({
activeOffsetX: [-10, 10],
onActivate: () => {
setActivations((n) => n + 1);
},
});
useEffect(() => {
const id = setInterval(() => {
setDomStyles(readDomStyles(boxRef.current));
}, 250);
return () => clearInterval(id);
}, []);
// With the props removed the handler falls back to its defaults.
const expectedTouchAction = propsSet ? 'pan-y' : 'none';
const expectedUserSelect = propsSet ? 'text' : 'none';
const ok =
domStyles?.computedTouchAction === expectedTouchAction &&
domStyles?.computedUserSelect === expectedUserSelect;
return (
<ScrollView contentContainerStyle={styles.content}>
<Text style={styles.label} testID="status">
{domStyles == null
? 'reading DOM…'
: `touch-action: ${domStyles.computedTouchAction} (inline "${domStyles.inlineTouchAction}"), ` +
`user-select: ${domStyles.computedUserSelect} (inline "${domStyles.inlineUserSelect}") ` +
`→ ${ok ? 'OK' : 'BUG'}`}
</Text>
<Text style={styles.label}>
renders: {renders}, pan activations: {activations}, detector props:{' '}
{propsSet ? 'set (expect pan-y / text)' : 'unset (expect none / none)'}
</Text>
<Pressable
style={styles.button}
onPress={() => setRenders((n) => n + 1)}
testID="rerender">
<Text style={styles.buttonLabel}>Re-render owner</Text>
</Pressable>
<Pressable
style={styles.button}
onPress={() => setPropsSet((v) => !v)}
testID="toggleProps">
<Text style={styles.buttonLabel}>
{propsSet ? 'Unset detector props' : 'Set detector props'}
</Text>
</Pressable>
<GestureDetector
gesture={pan}
touchAction={propsSet ? 'pan-y' : undefined}
userSelect={propsSet ? 'text' : undefined}
enableContextMenu={propsSet ? false : undefined}>
<View ref={boxRef} style={styles.box} testID="box">
<Text style={styles.boxLabel}>
horizontal pan here, page should still scroll vertically
</Text>
</View>
</GestureDetector>
<Text style={styles.label}>
scroll down: the page is taller than the screen on purpose
</Text>
<View style={styles.spacer} />
<Text style={styles.label}>end of page</Text>
</ScrollView>
);
}
const styles = StyleSheet.create({
content: {
alignItems: 'center',
padding: 16,
},
label: {
fontSize: 14,
color: COLORS.NAVY,
marginVertical: 4,
textAlign: 'center',
},
button: {
marginVertical: 6,
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 8,
backgroundColor: COLORS.PURPLE,
},
buttonLabel: {
color: 'white',
fontSize: 16,
},
spacer: {
height: 1200,
},
box: {
marginTop: 12,
width: 260,
height: 200,
borderRadius: 16,
backgroundColor: COLORS.GREEN,
justifyContent: 'center',
alignItems: 'center',
padding: 12,
},
boxLabel: {
color: 'white',
fontSize: 16,
textAlign: 'center',
},
});
```
</details>
0 commit comments