-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
fix: double press interaction on android cp-7.54.0 #18729
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4dabe00
e54e220
0a5c716
0f4bfe6
9c93892
4d31d6d
875e9b1
fa78a74
36edb85
c8d7af3
c422382
4ce6b64
1c47b61
59a9aa3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| /* eslint-disable react/prop-types */ | ||
|
|
||
| // Third party dependencies. | ||
| import React from 'react'; | ||
| import React, { useRef } from 'react'; | ||
| import { | ||
| TouchableOpacity as RNTouchableOpacity, | ||
| TouchableOpacityProps, | ||
|
|
@@ -31,11 +31,22 @@ const TouchableOpacity = ({ | |
| disabled, | ||
| children, | ||
| ...props | ||
| }: TouchableOpacityProps & { children?: React.ReactNode }) => { | ||
| }: TouchableOpacityProps & { | ||
| children?: React.ReactNode; | ||
| }) => { | ||
| // Handle both 'disabled' and 'isDisabled' props for compatibility | ||
| const isDisabled = disabled || (props as { isDisabled?: boolean }).isDisabled; | ||
|
|
||
| // Simple pass-through to main component coordination | ||
| // Main component handles ALL coordination logic | ||
|
|
||
| // Gesture detection for ScrollView compatibility on Android | ||
| // Sets timestamp FIRST, then calls parent function | ||
| const tap = Gesture.Tap() | ||
| .runOnJS(true) | ||
| .shouldCancelWhenOutside(false) | ||
| .maxDeltaX(20) // Allow some movement while tapping | ||
| .maxDeltaY(20) | ||
| .onEnd((gestureEvent) => { | ||
| if (onPress && !isDisabled) { | ||
| // Create a proper GestureResponderEvent-like object from gesture event | ||
|
|
@@ -57,19 +68,24 @@ const TouchableOpacity = ({ | |
| /* no-op for synthetic event */ | ||
| }, | ||
| } as GestureResponderEvent; | ||
|
|
||
| // Call main component function (handles coordination) | ||
| onPress(syntheticEvent); | ||
| } | ||
| }); | ||
|
|
||
| // Preserve onPress for accessibility (screen readers, keyboard navigation) | ||
| // but ensure it respects disabled state | ||
| const accessibleOnPress = isDisabled ? undefined : onPress; | ||
| // Simple accessibility handler - main component handles coordination | ||
| const accessibilityOnPress = (pressEvent: GestureResponderEvent) => { | ||
| if (onPress && !isDisabled) { | ||
| onPress(pressEvent); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <GestureDetector gesture={tap}> | ||
| <RNTouchableOpacity | ||
| disabled={isDisabled} | ||
| onPress={accessibleOnPress} // Preserve for accessibility | ||
| onPress={accessibilityOnPress} // Restored for accessibility without ScrollView conflicts | ||
| {...props} | ||
| // Ensure disabled prop is available to tests | ||
| {...(process.env.NODE_ENV === 'test' && { disabled: isDisabled })} | ||
|
|
@@ -100,6 +116,11 @@ const ButtonBase = ({ | |
| isDisabled, | ||
| }); | ||
|
|
||
| // Shared coordination system for maximum reliability | ||
| // Both custom TouchableOpacity and main component use the same timestamp reference | ||
| const lastPressTime = useRef(0); | ||
| const COORDINATION_WINDOW = 100; // 100ms window for TalkBack compatibility | ||
|
|
||
| // Disable gesture wrapper in test environments to prevent test interference | ||
| const isE2ETest = | ||
| process.env.IS_TEST === 'true' || | ||
|
|
@@ -110,15 +131,23 @@ const ButtonBase = ({ | |
| ? TouchableOpacity | ||
| : RNTouchableOpacity; | ||
|
|
||
| // Handle disabled state properly in all environments | ||
| // For custom TouchableOpacity (Android), pass original onPress and let it handle disabled state internally | ||
| // For standard TouchableOpacity, apply conditional logic to prevent disabled interaction | ||
| const conditionalOnPress = | ||
| TouchableComponent === TouchableOpacity | ||
| ? onPress | ||
| : isDisabled | ||
| ? undefined | ||
| : onPress; | ||
| const conditionalOnPress = isDisabled | ||
| ? undefined | ||
| : (_pressEvent?: GestureResponderEvent) => { | ||
| // Skip coordination logic in test environments | ||
| if (process.env.NODE_ENV === 'test') { | ||
| onPress?.(); | ||
| return; | ||
| } | ||
|
|
||
| const now = Date.now(); | ||
| const timeSinceLastPress = now - lastPressTime.current; | ||
|
|
||
| if (onPress && timeSinceLastPress > COORDINATION_WINDOW) { | ||
| lastPressTime.current = now; | ||
| onPress(); | ||
| } | ||
| }; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Inconsistent Event Handling Across PlatformsOn non-Android platforms, the |
||
|
|
||
| return ( | ||
| <TouchableComponent | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering instead of date, we could use a mutex logic here, since we want to prevent double tap, if I'm thinking correctly mutex logic would prevent that, instead of relying on timings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind that would be for async operations