Skip to content

Commit c173e17

Browse files
authored
[android] fix talkback on pressable (#4017)
## Description When TalkBack is activated, pressable cannot be clicked on android. This PR fixes the issue. The reason was that Pressable requires a specific order of events. This order of events is hardcoded and found empirically. However, with talback enabled the order of events is different, I added a function which queries native side whether or not talback is enabled and adjusts expected event order accordingly. Moreover with talback enabled longPress gets both onPointerUp and onPointerDown before native side receives its events, thus we must not reset the state machine state on onPointerUp. ## Test plan Tested on the following example: <details> ```tsx import React from 'react'; import { Text, StyleSheet, View } from 'react-native'; import { Pressable, GestureHandlerRootView } from 'react-native-gesture-handler' const PressableExample = () => { const [count, setCount] = React.useState(0); return ( <GestureHandlerRootView style={styles.container}> <Pressable onPress={() => setCount((c) => c + 1)}> <View style={styles.pressable}> <Text>{count}</Text> </View> </Pressable> </GestureHandlerRootView > ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, pressable: { alignItems: 'center', justifyContent: 'center', backgroundColor: 'green', width: 100, height: 100 }, wrapperCustom: { borderRadius: 8, padding: 16, minWidth: 150, alignItems: 'center', }, text: { fontSize: 18, color: 'white', fontWeight: '600', }, }); export default PressableExample; ``` </details>
1 parent b33f704 commit c173e17

File tree

4 files changed

+74
-9
lines changed

4 files changed

+74
-9
lines changed

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

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

4041
const DEFAULT_LONG_PRESS_DURATION = 500;
4142
const IS_TEST_ENV = isTestEnv();
@@ -202,11 +203,16 @@ const LegacyPressable = (props: LegacyPressableProps) => {
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 LegacyPressable = (props: LegacyPressableProps) => {
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 LegacyPressable = (props: LegacyPressableProps) => {
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') {
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/v3/components/Pressable.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { PureNativeButton } from './GestureButtons';
4040

4141
import { PressabilityDebugView } from '../../handlers/PressabilityDebugView';
4242
import { INT32_MAX, isTestEnv } from '../../utils';
43+
import { useIsScreenReaderEnabled } from '../../useIsScreenReaderEnabled';
4344

4445
const DEFAULT_LONG_PRESS_DURATION = 500;
4546
const IS_TEST_ENV = isTestEnv();
@@ -199,11 +200,16 @@ const Pressable = (props: PressableProps) => {
199200
);
200201

201202
const stateMachine = useMemo(() => new PressableStateMachine(), []);
203+
const isScreenReaderEnabled = useIsScreenReaderEnabled();
202204

203205
useEffect(() => {
204-
const configuration = getStatesConfig(handlePressIn, handlePressOut);
206+
const configuration = getStatesConfig(
207+
handlePressIn,
208+
handlePressOut,
209+
isScreenReaderEnabled
210+
);
205211
stateMachine.setStates(configuration);
206-
}, [handlePressIn, handlePressOut, stateMachine]);
212+
}, [handlePressIn, handlePressOut, stateMachine, isScreenReaderEnabled]);
207213

208214
const hoverInTimeout = useRef<number | null>(null);
209215
const hoverOutTimeout = useRef<number | null>(null);
@@ -257,7 +263,7 @@ const Pressable = (props: PressableProps) => {
257263
);
258264
},
259265
onTouchesUp: () => {
260-
if (Platform.OS === 'android') {
266+
if (Platform.OS === 'android' && !isScreenReaderEnabled) {
261267
// Prevents potential soft-locks
262268
stateMachine.reset();
263269
handleFinalize();

0 commit comments

Comments
 (0)