Skip to content

[android] fix talkback on pressable#4017

Draft
akwasniewski wants to merge 1 commit intomainfrom
@akwasniewski/android-fix-talback
Draft

[android] fix talkback on pressable#4017
akwasniewski wants to merge 1 commit intomainfrom
@akwasniewski/android-fix-talback

Conversation

@akwasniewski
Copy link
Contributor

@akwasniewski akwasniewski commented Mar 6, 2026

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 module 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
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;

Copilot AI review requested due to automatic review settings March 6, 2026 12:09
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Android TalkBack interactions with Pressable by adapting the internal Pressable state machine to the different event ordering produced when accessibility services are active.

Changes:

  • Added a new native module API (isAccessibilityEnabled) to detect accessibility state from JS.
  • Adjusted Android Pressable state machine event ordering when accessibility is enabled.
  • Prevented an Android-only state-machine reset on onTouchesUp when accessibility is enabled (to avoid breaking long-press flows under TalkBack).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/react-native-gesture-handler/src/v3/components/Pressable.tsx Skips the Android onTouchesUp reset path when accessibility is enabled.
packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts Extends the TurboModule spec with isAccessibilityEnabled(): boolean.
packages/react-native-gesture-handler/src/components/Pressable/stateDefinitions.ts Adds an Android accessibility-specific state config and selects it when accessibility is enabled.
packages/react-native-gesture-handler/src/RNGestureHandlerModule.web.ts Adds a web stub for isAccessibilityEnabled (currently unimplemented).
packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt Implements isAccessibilityEnabled() on Android via AccessibilityManager.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Context.ACCESSIBILITY_SERVICE,
) as AccessibilityManager?

return accessibilityManager?.isEnabled ?: false
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AccessibilityManager.isEnabled is broader than TalkBack/screen reader and can be true for other accessibility features (which would switch Pressable into the altered event-order path unexpectedly). If the intent is specifically TalkBack, consider using accessibilityManager?.isTouchExplorationEnabled == true (optionally gated by isEnabled) instead.

Suggested change
return accessibilityManager?.isEnabled ?: false
return accessibilityManager?.let { it.isEnabled && it.isTouchExplorationEnabled } ?: false

Copilot uses AI. Check for mistakes.
Comment on lines +261 to +264
if (
Platform.OS === 'android' &&
!RNGestureHandlerModule.isAccessibilityEnabled()
) {
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calls the native isAccessibilityEnabled() synchronously on every onTouchesUp. Since this value typically changes rarely, consider reading it once (e.g., on mount) and caching it in a ref/state to avoid repeated sync native calls on a hot interaction path.

Copilot uses AI. Check for mistakes.
): StateDefinition[] {
if (Platform.OS === 'android') {
if (RNGestureHandlerModule.isAccessibilityEnabled()) {
console.log('accessible!!!');
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the leftover debug console.log('accessible!!!'). Logging here will leak to production apps and can be noisy; the accessibility-specific config selection should be silent (or use the library's existing debug/log mechanism if one exists).

Suggested change
console.log('accessible!!!');

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +96
isAccessibilityEnabled() {
// TODO
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isAccessibilityEnabled() is currently a stub with no return value. It should return a boolean (likely false on web) to keep behavior predictable and avoid returning undefined if this method gets used on web in the future.

Suggested change
isAccessibilityEnabled() {
// TODO
isAccessibilityEnabled(): boolean {
// Accessibility status is not wired on web; default to false.
return false;

Copilot uses AI. Check for mistakes.
dropGestureHandler: (handlerTag: Double) => void;
flushOperations: () => void;
setReanimatedAvailable: (isAvailable: boolean) => void;
isAccessibilityEnabled: () => boolean;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding isAccessibilityEnabled to the TurboModule spec requires providing native implementations on all supported platforms that build this module (notably iOS, where RNGestureHandlerModule conforms to NativeRNGestureHandlerModuleSpec). Without adding the corresponding method on iOS, this change is likely to break native compilation.

Suggested change
isAccessibilityEnabled: () => boolean;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants