Skip to content

Commit 4dfdc7f

Browse files
committed
Merge branch 'v2-stable' into 2.31-stable
2 parents 49d50a0 + e85cdfe commit 4dfdc7f

9 files changed

Lines changed: 100 additions & 37 deletions

File tree

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/RotationGestureHandler.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ class RotationGestureHandler : GestureHandler() {
3535
override fun onRotationBegin(detector: RotationGestureDetector) = true
3636

3737
override fun onRotationEnd(detector: RotationGestureDetector) {
38-
end()
38+
if (state == STATE_ACTIVE) {
39+
end()
40+
} else {
41+
fail()
42+
}
3943
}
4044
}
4145

@@ -56,12 +60,11 @@ class RotationGestureHandler : GestureHandler() {
5660
anchorX = point.x
5761
anchorY = point.y
5862
}
59-
if (sourceEvent.actionMasked == MotionEvent.ACTION_UP) {
60-
if (state == STATE_ACTIVE) {
61-
end()
62-
} else {
63-
fail()
64-
}
63+
64+
// ACTION_UP is already handled in rotationGestureDetector.onTouchEvent (and effectively in onRotationEnd)
65+
// if more than one pointer was used
66+
if (sourceEvent.actionMasked == MotionEvent.ACTION_UP && state == STATE_BEGAN) {
67+
fail()
6568
}
6669
}
6770

packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
} from '../utils';
3838
import { getStatesConfig, StateMachineEvent } from './stateDefinitions';
3939
import { PressableStateMachine } from './StateMachine';
40+
import { useIsScreenReaderEnabled } from '../../useIsScreenReaderEnabled';
4041

4142
const DEFAULT_LONG_PRESS_DURATION = 500;
4243
const IS_TEST_ENV = isTestEnv();
@@ -202,11 +203,16 @@ const Pressable = (props: PressableProps) => {
202203
);
203204

204205
const stateMachine = useMemo(() => new PressableStateMachine(), []);
206+
const isScreenReaderEnabled = useIsScreenReaderEnabled();
205207

206208
useEffect(() => {
207-
const configuration = getStatesConfig(handlePressIn, handlePressOut);
209+
const configuration = getStatesConfig(
210+
handlePressIn,
211+
handlePressOut,
212+
isScreenReaderEnabled
213+
);
208214
stateMachine.setStates(configuration);
209-
}, [handlePressIn, handlePressOut, stateMachine]);
215+
}, [handlePressIn, handlePressOut, stateMachine, isScreenReaderEnabled]);
210216

211217
const hoverInTimeout = useRef<number | null>(null);
212218
const hoverOutTimeout = useRef<number | null>(null);
@@ -259,7 +265,7 @@ const Pressable = (props: PressableProps) => {
259265
);
260266
})
261267
.onTouchesUp(() => {
262-
if (Platform.OS === 'android') {
268+
if (Platform.OS === 'android' && !isScreenReaderEnabled) {
263269
// Prevents potential soft-locks
264270
stateMachine.reset();
265271
handleFinalize();
@@ -280,7 +286,7 @@ const Pressable = (props: PressableProps) => {
280286
handleFinalize();
281287
}
282288
}),
283-
[stateMachine, handleFinalize, handlePressOut]
289+
[stateMachine, handleFinalize, handlePressOut, isScreenReaderEnabled]
284290
);
285291

286292
// RNButton is placed inside ButtonGesture to enable Android's ripple and to capture non-propagating events

packages/react-native-gesture-handler/src/components/Pressable/stateDefinitions.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,25 @@ function getAndroidStatesConfig(
2929
];
3030
}
3131

32+
function getAndroidAccessibilityStatesConfig(
33+
handlePressIn: (event: PressableEvent) => void,
34+
handlePressOut: (event: PressableEvent) => void
35+
) {
36+
return [
37+
{
38+
eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN,
39+
callback: handlePressIn,
40+
},
41+
{
42+
eventName: StateMachineEvent.NATIVE_BEGIN,
43+
},
44+
{
45+
eventName: StateMachineEvent.FINALIZE,
46+
callback: handlePressOut,
47+
},
48+
];
49+
}
50+
3251
function getIosStatesConfig(
3352
handlePressIn: (event: PressableEvent) => void,
3453
handlePressOut: (event: PressableEvent) => void
@@ -109,10 +128,13 @@ function getUniversalStatesConfig(
109128

110129
export function getStatesConfig(
111130
handlePressIn: (event: PressableEvent) => void,
112-
handlePressOut: (event: PressableEvent) => void
131+
handlePressOut: (event: PressableEvent) => void,
132+
screenReaderActive: boolean
113133
): StateDefinition[] {
114134
if (Platform.OS === 'android') {
115-
return getAndroidStatesConfig(handlePressIn, handlePressOut);
135+
return screenReaderActive
136+
? getAndroidAccessibilityStatesConfig(handlePressIn, handlePressOut)
137+
: getAndroidStatesConfig(handlePressIn, handlePressOut);
116138
} else if (Platform.OS === 'ios') {
117139
return getIosStatesConfig(handlePressIn, handlePressOut);
118140
} else if (Platform.OS === 'web') {

packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface SwipeableProps {
1313
/**
1414
*
1515
*/
16-
ref?: React.RefObject<SwipeableMethods | null>;
16+
ref?: React.Ref<SwipeableMethods>;
1717

1818
/**
1919
* Sets a `testID` property, allowing for querying `ReanimatedSwipeable` for it in tests.

packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ export const ALLOWED_PROPS = [
3636
...nativeViewGestureHandlerProps,
3737
];
3838

39+
// In some environments (e.g. `Next.js`) Reanimated Babel plugin might not be used.
40+
// In that case we would wrongly suggest to add `runOnJS` to gesture configuration, even if the user doesn't use worklets at all.
41+
// To prevent this, we check whether the plugin is enabled by defining a worklet and checking if the `__workletHash` property is available.
42+
function emptyWorklet() {
43+
'worklet';
44+
}
45+
46+
// @ts-expect-error if `emptyWorklet` is a worklet, `__workletHash` will be available, if not then the check will return false.
47+
const wasBabelPluginEnabled = emptyWorklet.__workletHash !== undefined;
48+
3949
function convertToHandlerTag(ref: GestureRef): number {
4050
if (typeof ref === 'number') {
4151
return ref;
@@ -105,7 +115,7 @@ export function checkGestureCallbacksForWorklets(gesture: GestureType) {
105115
const areAllNotWorklets = !areSomeWorklets && areSomeNotWorklets;
106116
// If none of the callbacks are worklets and the gesture is not explicitly marked with
107117
// `.runOnJS(true)` show a warning
108-
if (areAllNotWorklets && !isTestEnv()) {
118+
if (areAllNotWorklets && wasBabelPluginEnabled && !isTestEnv()) {
109119
console.warn(
110120
tagMessage(
111121
`None of the callbacks in the gesture are worklets. If you wish to run them on the JS thread use '.runOnJS(true)' modifier on the gesture to make this explicit. Otherwise, mark the callbacks as 'worklet' to run them on the UI thread.`
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useEffect, useState } from 'react';
2+
import { AccessibilityInfo } from 'react-native';
3+
4+
export function useIsScreenReaderEnabled() {
5+
const [isEnabled, setIsEnabled] = useState(false);
6+
7+
useEffect(() => {
8+
const checkStatus = async () => {
9+
try {
10+
const res = await AccessibilityInfo.isScreenReaderEnabled();
11+
setIsEnabled(res);
12+
} catch (error) {
13+
console.warn('Could not read accessibility info: defaulting to false');
14+
}
15+
};
16+
17+
checkStatus();
18+
19+
const listener = AccessibilityInfo.addEventListener(
20+
'screenReaderChanged',
21+
(enabled) => {
22+
setIsEnabled(enabled);
23+
}
24+
);
25+
26+
return () => {
27+
listener.remove();
28+
};
29+
}, []);
30+
return isEnabled;
31+
}

packages/react-native-gesture-handler/src/web/detectors/RotationGestureDetector.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,11 @@ export default class RotationGestureDetector
7575
}
7676

7777
private finish(): void {
78-
if (!this.isInProgress) {
79-
return;
78+
if (this.isInProgress) {
79+
this.isInProgress = false;
80+
this.keyPointers = [NaN, NaN];
8081
}
8182

82-
this.isInProgress = false;
83-
this.keyPointers = [NaN, NaN];
8483
this.onRotationEnd(this);
8584
}
8685

@@ -138,9 +137,8 @@ export default class RotationGestureDetector
138137
break;
139138

140139
case EventTypes.UP:
141-
if (this.isInProgress) {
142-
this.finish();
143-
}
140+
this.finish();
141+
144142
break;
145143
}
146144

packages/react-native-gesture-handler/src/web/handlers/PinchGestureHandler.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,11 @@ export default class PinchGestureHandler extends GestureHandler {
8080
protected onPointerUp(event: AdaptedEvent): void {
8181
super.onPointerUp(event);
8282
this.tracker.removeFromTracker(event.pointerId);
83-
if (this.state !== State.ACTIVE) {
84-
return;
85-
}
86-
this.scaleGestureDetector.onTouchEvent(event, this.tracker);
8783

8884
if (this.state === State.ACTIVE) {
85+
// We don't have to call it in the else branch as it would simply return `true`.
86+
this.scaleGestureDetector.onTouchEvent(event, this.tracker);
87+
8988
this.end();
9089
} else {
9190
this.fail();

packages/react-native-gesture-handler/src/web/handlers/RotationGestureHandler.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export default class RotationGestureHandler extends GestureHandler {
3737
return true;
3838
},
3939
onRotationEnd: (_detector: RotationGestureDetector): void => {
40-
this.end();
40+
if (this.state === State.ACTIVE) {
41+
this.end();
42+
} else {
43+
this.fail();
44+
}
4145
},
4246
};
4347

@@ -128,16 +132,6 @@ export default class RotationGestureHandler extends GestureHandler {
128132
super.onPointerUp(event);
129133
this.tracker.removeFromTracker(event.pointerId);
130134
this.rotationGestureDetector.onTouchEvent(event, this.tracker);
131-
132-
if (this.state !== State.ACTIVE) {
133-
return;
134-
}
135-
136-
if (this.state === State.ACTIVE) {
137-
this.end();
138-
} else {
139-
this.fail();
140-
}
141135
}
142136

143137
protected onPointerRemove(event: AdaptedEvent): void {

0 commit comments

Comments
 (0)