diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/RotationGestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/RotationGestureHandler.kt index 2d660f6d4d..657f05cef3 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/RotationGestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/RotationGestureHandler.kt @@ -35,7 +35,11 @@ class RotationGestureHandler : GestureHandler() { override fun onRotationBegin(detector: RotationGestureDetector) = true override fun onRotationEnd(detector: RotationGestureDetector) { - end() + if (state == STATE_ACTIVE) { + end() + } else { + fail() + } } } @@ -56,12 +60,11 @@ class RotationGestureHandler : GestureHandler() { anchorX = point.x anchorY = point.y } - if (sourceEvent.actionMasked == MotionEvent.ACTION_UP) { - if (state == STATE_ACTIVE) { - end() - } else { - fail() - } + + // ACTION_UP is already handled in rotationGestureDetector.onTouchEvent (and effectively in onRotationEnd) + // if more than one pointer was used + if (sourceEvent.actionMasked == MotionEvent.ACTION_UP && state == STATE_BEGAN) { + fail() } } diff --git a/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx index db956d916f..c9985eb8a0 100644 --- a/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx @@ -37,6 +37,7 @@ import { } from '../utils'; import { getStatesConfig, StateMachineEvent } from './stateDefinitions'; import { PressableStateMachine } from './StateMachine'; +import { useIsScreenReaderEnabled } from '../../useIsScreenReaderEnabled'; const DEFAULT_LONG_PRESS_DURATION = 500; const IS_TEST_ENV = isTestEnv(); @@ -202,11 +203,16 @@ const Pressable = (props: PressableProps) => { ); const stateMachine = useMemo(() => new PressableStateMachine(), []); + const isScreenReaderEnabled = useIsScreenReaderEnabled(); useEffect(() => { - const configuration = getStatesConfig(handlePressIn, handlePressOut); + const configuration = getStatesConfig( + handlePressIn, + handlePressOut, + isScreenReaderEnabled + ); stateMachine.setStates(configuration); - }, [handlePressIn, handlePressOut, stateMachine]); + }, [handlePressIn, handlePressOut, stateMachine, isScreenReaderEnabled]); const hoverInTimeout = useRef(null); const hoverOutTimeout = useRef(null); @@ -259,7 +265,7 @@ const Pressable = (props: PressableProps) => { ); }) .onTouchesUp(() => { - if (Platform.OS === 'android') { + if (Platform.OS === 'android' && !isScreenReaderEnabled) { // Prevents potential soft-locks stateMachine.reset(); handleFinalize(); @@ -280,7 +286,7 @@ const Pressable = (props: PressableProps) => { handleFinalize(); } }), - [stateMachine, handleFinalize, handlePressOut] + [stateMachine, handleFinalize, handlePressOut, isScreenReaderEnabled] ); // RNButton is placed inside ButtonGesture to enable Android's ripple and to capture non-propagating events diff --git a/packages/react-native-gesture-handler/src/components/Pressable/stateDefinitions.ts b/packages/react-native-gesture-handler/src/components/Pressable/stateDefinitions.ts index 55279211cd..af4fb93057 100644 --- a/packages/react-native-gesture-handler/src/components/Pressable/stateDefinitions.ts +++ b/packages/react-native-gesture-handler/src/components/Pressable/stateDefinitions.ts @@ -29,6 +29,25 @@ function getAndroidStatesConfig( ]; } +function getAndroidAccessibilityStatesConfig( + handlePressIn: (event: PressableEvent) => void, + handlePressOut: (event: PressableEvent) => void +) { + return [ + { + eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN, + callback: handlePressIn, + }, + { + eventName: StateMachineEvent.NATIVE_BEGIN, + }, + { + eventName: StateMachineEvent.FINALIZE, + callback: handlePressOut, + }, + ]; +} + function getIosStatesConfig( handlePressIn: (event: PressableEvent) => void, handlePressOut: (event: PressableEvent) => void @@ -109,10 +128,13 @@ function getUniversalStatesConfig( export function getStatesConfig( handlePressIn: (event: PressableEvent) => void, - handlePressOut: (event: PressableEvent) => void + handlePressOut: (event: PressableEvent) => void, + screenReaderActive: boolean ): StateDefinition[] { if (Platform.OS === 'android') { - return getAndroidStatesConfig(handlePressIn, handlePressOut); + return screenReaderActive + ? getAndroidAccessibilityStatesConfig(handlePressIn, handlePressOut) + : getAndroidStatesConfig(handlePressIn, handlePressOut); } else if (Platform.OS === 'ios') { return getIosStatesConfig(handlePressIn, handlePressOut); } else if (Platform.OS === 'web') { diff --git a/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts index f81877d1b2..0c0e517e0d 100644 --- a/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts +++ b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts @@ -13,7 +13,7 @@ export interface SwipeableProps { /** * */ - ref?: React.RefObject; + ref?: React.Ref; /** * Sets a `testID` property, allowing for querying `ReanimatedSwipeable` for it in tests. diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts b/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts index 0bf2471311..fc713189b2 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts @@ -36,6 +36,16 @@ export const ALLOWED_PROPS = [ ...nativeViewGestureHandlerProps, ]; +// In some environments (e.g. `Next.js`) Reanimated Babel plugin might not be used. +// In that case we would wrongly suggest to add `runOnJS` to gesture configuration, even if the user doesn't use worklets at all. +// To prevent this, we check whether the plugin is enabled by defining a worklet and checking if the `__workletHash` property is available. +function emptyWorklet() { + 'worklet'; +} + +// @ts-expect-error if `emptyWorklet` is a worklet, `__workletHash` will be available, if not then the check will return false. +const wasBabelPluginEnabled = emptyWorklet.__workletHash !== undefined; + function convertToHandlerTag(ref: GestureRef): number { if (typeof ref === 'number') { return ref; @@ -105,7 +115,7 @@ export function checkGestureCallbacksForWorklets(gesture: GestureType) { const areAllNotWorklets = !areSomeWorklets && areSomeNotWorklets; // If none of the callbacks are worklets and the gesture is not explicitly marked with // `.runOnJS(true)` show a warning - if (areAllNotWorklets && !isTestEnv()) { + if (areAllNotWorklets && wasBabelPluginEnabled && !isTestEnv()) { console.warn( tagMessage( `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.` diff --git a/packages/react-native-gesture-handler/src/useIsScreenReaderEnabled.ts b/packages/react-native-gesture-handler/src/useIsScreenReaderEnabled.ts new file mode 100644 index 0000000000..4d7dba945b --- /dev/null +++ b/packages/react-native-gesture-handler/src/useIsScreenReaderEnabled.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react'; +import { AccessibilityInfo } from 'react-native'; + +export function useIsScreenReaderEnabled() { + const [isEnabled, setIsEnabled] = useState(false); + + useEffect(() => { + const checkStatus = async () => { + try { + const res = await AccessibilityInfo.isScreenReaderEnabled(); + setIsEnabled(res); + } catch (error) { + console.warn('Could not read accessibility info: defaulting to false'); + } + }; + + checkStatus(); + + const listener = AccessibilityInfo.addEventListener( + 'screenReaderChanged', + (enabled) => { + setIsEnabled(enabled); + } + ); + + return () => { + listener.remove(); + }; + }, []); + return isEnabled; +} diff --git a/packages/react-native-gesture-handler/src/web/detectors/RotationGestureDetector.ts b/packages/react-native-gesture-handler/src/web/detectors/RotationGestureDetector.ts index 79ffd6936b..c294d6f0c8 100644 --- a/packages/react-native-gesture-handler/src/web/detectors/RotationGestureDetector.ts +++ b/packages/react-native-gesture-handler/src/web/detectors/RotationGestureDetector.ts @@ -75,12 +75,11 @@ export default class RotationGestureDetector } private finish(): void { - if (!this.isInProgress) { - return; + if (this.isInProgress) { + this.isInProgress = false; + this.keyPointers = [NaN, NaN]; } - this.isInProgress = false; - this.keyPointers = [NaN, NaN]; this.onRotationEnd(this); } @@ -138,9 +137,8 @@ export default class RotationGestureDetector break; case EventTypes.UP: - if (this.isInProgress) { - this.finish(); - } + this.finish(); + break; } diff --git a/packages/react-native-gesture-handler/src/web/handlers/PinchGestureHandler.ts b/packages/react-native-gesture-handler/src/web/handlers/PinchGestureHandler.ts index 9242f50995..05e4584d52 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/PinchGestureHandler.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/PinchGestureHandler.ts @@ -80,12 +80,11 @@ export default class PinchGestureHandler extends GestureHandler { protected onPointerUp(event: AdaptedEvent): void { super.onPointerUp(event); this.tracker.removeFromTracker(event.pointerId); - if (this.state !== State.ACTIVE) { - return; - } - this.scaleGestureDetector.onTouchEvent(event, this.tracker); if (this.state === State.ACTIVE) { + // We don't have to call it in the else branch as it would simply return `true`. + this.scaleGestureDetector.onTouchEvent(event, this.tracker); + this.end(); } else { this.fail(); diff --git a/packages/react-native-gesture-handler/src/web/handlers/RotationGestureHandler.ts b/packages/react-native-gesture-handler/src/web/handlers/RotationGestureHandler.ts index 194cd44986..a192140e0a 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/RotationGestureHandler.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/RotationGestureHandler.ts @@ -37,7 +37,11 @@ export default class RotationGestureHandler extends GestureHandler { return true; }, onRotationEnd: (_detector: RotationGestureDetector): void => { - this.end(); + if (this.state === State.ACTIVE) { + this.end(); + } else { + this.fail(); + } }, }; @@ -128,16 +132,6 @@ export default class RotationGestureHandler extends GestureHandler { super.onPointerUp(event); this.tracker.removeFromTracker(event.pointerId); this.rotationGestureDetector.onTouchEvent(event, this.tracker); - - if (this.state !== State.ACTIVE) { - return; - } - - if (this.state === State.ACTIVE) { - this.end(); - } else { - this.fail(); - } } protected onPointerRemove(event: AdaptedEvent): void {