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 26ab9e27e0..350fb5449f 100644 --- a/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx @@ -36,6 +36,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 LegacyPressable = (props: LegacyPressableProps) => { ); 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 LegacyPressable = (props: LegacyPressableProps) => { ); }) .onTouchesUp(() => { - if (Platform.OS === 'android') { + if (Platform.OS === 'android' && !isScreenReaderEnabled) { // Prevents potential soft-locks stateMachine.reset(); handleFinalize(); @@ -280,7 +286,7 @@ const LegacyPressable = (props: LegacyPressableProps) => { 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/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/v3/components/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx index 50d3ce15e5..245c250657 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx @@ -40,6 +40,7 @@ import { PureNativeButton } from './GestureButtons'; import { PressabilityDebugView } from '../../handlers/PressabilityDebugView'; import { INT32_MAX, isTestEnv } from '../../utils'; +import { useIsScreenReaderEnabled } from '../../useIsScreenReaderEnabled'; const DEFAULT_LONG_PRESS_DURATION = 500; const IS_TEST_ENV = isTestEnv(); @@ -199,11 +200,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); @@ -257,7 +263,7 @@ const Pressable = (props: PressableProps) => { ); }, onTouchesUp: () => { - if (Platform.OS === 'android') { + if (Platform.OS === 'android' && !isScreenReaderEnabled) { // Prevents potential soft-locks stateMachine.reset(); handleFinalize();