Skip to content

Commit 32b24de

Browse files
committed
Merge branch 'main' into @mbert/tvos-buttons
2 parents 4a2418e + a423a6e commit 32b24de

8 files changed

Lines changed: 90 additions & 48 deletions

File tree

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@ import com.facebook.react.views.swiperefresh.ReactSwipeRefreshLayout
1414
import com.facebook.react.views.text.ReactTextView
1515
import com.facebook.react.views.textinput.ReactEditText
1616
import com.facebook.react.views.view.ReactViewGroup
17-
import com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager
1817
import com.swmansion.gesturehandler.react.RNGestureHandlerRootHelper
1918
import com.swmansion.gesturehandler.react.events.eventbuilders.NativeGestureHandlerEventDataBuilder
20-
import com.swmansion.gesturehandler.react.isScreenReaderOn
2119

2220
class NativeViewGestureHandler : GestureHandler() {
2321
override val isContinuous = true
@@ -121,17 +119,6 @@ class NativeViewGestureHandler : GestureHandler() {
121119

122120
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
123121
val view = view!!
124-
125-
val isTouchExplorationEnabled = view.context.isScreenReaderOn()
126-
127-
if (view is RNGestureHandlerButtonViewManager.ButtonViewGroup && isTouchExplorationEnabled) {
128-
// Fix for: https://github.com/software-mansion/react-native-gesture-handler/issues/2808
129-
// When TalkBack is enabled, events are often not being sent to the orchestrator for processing.
130-
// Instead, states will be changed directly by an alternative mechanism added in this PR:
131-
// https://github.com/software-mansion/react-native-gesture-handler/pull/2234
132-
return
133-
}
134-
135122
if (event.actionMasked == MotionEvent.ACTION_UP) {
136123
if (state == STATE_UNDETERMINED && !hook.canBegin(event)) {
137124
cancel()

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import androidx.core.view.children
2727
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
2828
import com.facebook.react.R
2929
import com.facebook.react.bridge.Dynamic
30+
import com.facebook.react.bridge.ReadableArray
3031
import com.facebook.react.module.annotations.ReactModule
3132
import com.facebook.react.uimanager.BackgroundStyleApplicator
3233
import com.facebook.react.uimanager.LengthPercentage
@@ -347,6 +348,7 @@ class RNGestureHandlerButtonViewManager :
347348
super.onAfterUpdateTransaction(view)
348349

349350
view.updateBackground()
351+
view.updateLongPressAccessibility()
350352
}
351353

352354
override fun getDelegate(): ViewManagerDelegate<ButtonViewGroup>? = mDelegate
@@ -434,6 +436,23 @@ class RNGestureHandlerButtonViewManager :
434436
invalidate()
435437
}
436438

439+
fun updateLongPressAccessibility() {
440+
val hasLongPress = hasLongPressAccessibilityAction()
441+
setOnLongClickListener(if (hasLongPress) dummyLongClickListener else null)
442+
isLongClickable = hasLongPress
443+
}
444+
445+
private fun hasLongPressAccessibilityAction(): Boolean {
446+
val actions = getTag(R.id.accessibility_actions) as? ReadableArray ?: return false
447+
for (i in 0 until actions.size()) {
448+
if (actions.getMap(i)?.getString("name") == "longpress") {
449+
return true
450+
}
451+
}
452+
453+
return false
454+
}
455+
437456
override fun setBackgroundColor(color: Int) {
438457
BackgroundStyleApplicator.setBackgroundColor(this, color)
439458
}
@@ -876,10 +895,11 @@ class RNGestureHandlerButtonViewManager :
876895
}
877896

878897
override fun performClick(): Boolean {
879-
// don't preform click when a child button is pressed (mainly to prevent sound effect of
898+
// don't perform click when a child button is pressed (mainly to prevent sound effect of
880899
// a parent button from playing)
881900
return if (!isChildTouched()) {
882-
if (context.isScreenReaderOn()) {
901+
// Don't activate native handlers when isPressed is true (motion events are passing through)
902+
if (context.isScreenReaderOn() && !isPressed) {
883903
RNGestureHandlerRootView.findGestureHandlerRootView(this)?.activateNativeHandlers(this)
884904
} else if (receivedKeyEvent) {
885905
RNGestureHandlerRootView.findGestureHandlerRootView(this)?.activateNativeHandlers(this)
@@ -936,7 +956,14 @@ class RNGestureHandlerButtonViewManager :
936956
var resolveOutValue = TypedValue()
937957
var touchResponder: ButtonViewGroup? = null
938958
var soundResponder: ButtonViewGroup? = null
939-
var dummyClickListener = OnClickListener { }
959+
val dummyClickListener = OnClickListener { }
960+
val dummyLongClickListener = OnLongClickListener { view ->
961+
if (view.context.isScreenReaderOn()) {
962+
view.performAccessibilityAction(AccessibilityNodeInfo.ACTION_LONG_CLICK, null)
963+
} else {
964+
false
965+
}
966+
}
940967
}
941968
}
942969

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
gestureTouchToPressableEvent,
3535
isTouchWithinInset,
3636
numberAsInset,
37+
viewCenterToPressableEvent,
3738
} from './utils';
3839

3940
const DEFAULT_LONG_PRESS_DURATION = 500;
@@ -301,6 +302,13 @@ const LegacyPressable = (props: LegacyPressableProps) => {
301302
}
302303
})
303304
.onBegin(() => {
305+
if (Platform.OS === 'android' && isScreenReaderEnabled) {
306+
stateMachine.handleEvent(
307+
StateMachineEvent.NATIVE_BEGIN,
308+
viewCenterToPressableEvent(dimensions.current)
309+
);
310+
return;
311+
}
304312
stateMachine.handleEvent(StateMachineEvent.NATIVE_BEGIN);
305313
})
306314
.onStart(() => {
@@ -325,7 +333,7 @@ const LegacyPressable = (props: LegacyPressableProps) => {
325333
}
326334
})
327335
.shouldActivateOnStart(Platform.OS === 'web'),
328-
[stateMachine, handlePressOut, handleFinalize]
336+
[stateMachine, handlePressOut, handleFinalize, isScreenReaderEnabled]
329337
);
330338

331339
const isPressableEnabled = disabled !== true;

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { PressableEvent } from './PressableProps';
33
export interface StateDefinition {
44
eventName: string;
55
callback?: (event: PressableEvent) => void;
6+
optional?: boolean;
67
}
78

89
class PressableStateMachine {
@@ -30,9 +31,23 @@ class PressableStateMachine {
3031
return;
3132
}
3233

33-
const step = this.states[this.currentStepIndex];
3434
this.eventPayload = eventPayload || this.eventPayload;
3535

36+
// Skip past optional steps that don't match the incoming event
37+
while (
38+
this.currentStepIndex < this.states.length &&
39+
this.states[this.currentStepIndex].eventName !== eventName &&
40+
this.states[this.currentStepIndex].optional
41+
) {
42+
this.currentStepIndex++;
43+
}
44+
45+
if (this.currentStepIndex >= this.states.length) {
46+
this.reset();
47+
return;
48+
}
49+
50+
const step = this.states[this.currentStepIndex];
3651
if (step.eventName !== eventName) {
3752
if (this.currentStepIndex > 0) {
3853
// retry with position at index 0

packages/react-native-gesture-handler/src/components/Pressable/stateDefinitions.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ function getAndroidAccessibilityStatesConfig(
3636
) {
3737
return [
3838
{
39-
eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN,
39+
eventName: StateMachineEvent.NATIVE_BEGIN,
4040
callback: handlePressIn,
4141
},
4242
{
43-
eventName: StateMachineEvent.NATIVE_BEGIN,
43+
eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN,
44+
optional: true,
4445
},
4546
{
4647
eventName: StateMachineEvent.FINALIZE,

packages/react-native-gesture-handler/src/components/Pressable/utils.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -142,26 +142,20 @@ const gestureTouchToPressableEvent = (
142142
};
143143
};
144144

145-
const mockCenterPressableEvent = (
145+
const viewCenterToPressableEvent = (
146146
dimensions: PressableDimensions
147147
): PressableEvent => {
148148
const timestamp = Date.now();
149-
150-
// As far as I can see, there isn't a conventional way of getting targetId with the data we get
151149
const targetId = 0;
152-
153-
// tvOS activates a focused button via the Select button rather than a touch, so there are no
154-
// touch coordinates. Report the view's center as the press location — a sensible representative
155-
// point for the event handed to user callbacks.
156-
const locationX = dimensions.width / 2;
157-
const locationY = dimensions.height / 2;
150+
const centerX = dimensions.width / 2;
151+
const centerY = dimensions.height / 2;
158152

159153
const pressEvent: InnerPressableEvent = {
160-
identifier: targetId,
161-
locationX,
162-
locationY,
163-
pageX: locationX,
164-
pageY: locationY,
154+
identifier: 0,
155+
locationX: centerX,
156+
locationY: centerY,
157+
pageX: -1,
158+
pageY: -1,
165159
target: targetId,
166160
timestamp,
167161
touches: [],
@@ -172,11 +166,11 @@ const mockCenterPressableEvent = (
172166
nativeEvent: {
173167
touches: [pressEvent],
174168
changedTouches: [pressEvent],
175-
identifier: targetId,
176-
locationX,
177-
locationY,
178-
pageX: locationX,
179-
pageY: locationY,
169+
identifier: 0,
170+
locationX: centerX,
171+
locationY: centerY,
172+
pageX: -1,
173+
pageY: -1,
180174
target: targetId,
181175
timestamp,
182176
force: undefined,
@@ -189,6 +183,6 @@ export {
189183
gestureToPressableEvent,
190184
gestureTouchToPressableEvent,
191185
isTouchWithinInset,
192-
mockCenterPressableEvent,
193186
numberAsInset,
187+
viewCenterToPressableEvent,
194188
};

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ import type {
4141
UserSelect,
4242
} from '../handlers/gestureHandlerCommon';
4343
import { MouseButton } from '../handlers/gestureHandlerCommon';
44-
import { GestureDetector } from '../v3/detectors';
44+
import {
45+
InterceptingGestureDetector,
46+
VirtualGestureDetector,
47+
} from '../v3/detectors';
4548
import type { PanGestureActiveEvent } from '../v3/hooks/gestures';
4649
import { usePanGesture, useTapGesture } from '../v3/hooks/gestures';
4750
import type { WithSharedValue } from '../v3/types';
@@ -657,12 +660,12 @@ const DrawerLayout = function DrawerLayout(
657660
);
658661

659662
return (
660-
<GestureDetector
663+
<InterceptingGestureDetector
661664
gesture={panGesture}
662665
userSelect={userSelect}
663666
enableContextMenu={enableContextMenu}>
664667
<Animated.View style={styles.main} onLayout={handleContainerLayout}>
665-
<GestureDetector
668+
<VirtualGestureDetector
666669
gesture={overlayDismissGesture}
667670
userSelect={userSelect}>
668671
<Animated.View
@@ -680,7 +683,7 @@ const DrawerLayout = function DrawerLayout(
680683
style={[styles.overlay, overlayAnimatedStyle]}
681684
/>
682685
</Animated.View>
683-
</GestureDetector>
686+
</VirtualGestureDetector>
684687
<Animated.View
685688
pointerEvents="box-none"
686689
animatedProps={drawerAnimatedProps}
@@ -694,7 +697,7 @@ const DrawerLayout = function DrawerLayout(
694697
</Animated.View>
695698
</Animated.View>
696699
</Animated.View>
697-
</GestureDetector>
700+
</InterceptingGestureDetector>
698701
);
699702
};
700703

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ import {
2828
gestureToPressableEvent,
2929
gestureTouchToPressableEvent,
3030
isTouchWithinInset,
31-
mockCenterPressableEvent,
3231
numberAsInset,
32+
viewCenterToPressableEvent,
3333
} from '../../components/Pressable/utils';
3434
import { getTVProps } from '../../components/utils';
3535
import { PressabilityDebugView } from '../../handlers/PressabilityDebugView';
@@ -313,7 +313,14 @@ const Pressable = (props: PressableProps) => {
313313
// The press state machine is touch-based and never
314314
// receives LONG_PRESS_TOUCHES_DOWN here, so bypass it and drive the press handlers directly.
315315
// A focus-driven press has no coordinates, so skip the hit-slop bounds check entirely.
316-
handlePressIn(mockCenterPressableEvent(dimensions.current), true);
316+
handlePressIn(viewCenterToPressableEvent(dimensions.current), true);
317+
return;
318+
}
319+
if (Platform.OS === 'android' && isScreenReaderEnabled) {
320+
stateMachine.handleEvent(
321+
StateMachineEvent.NATIVE_BEGIN,
322+
viewCenterToPressableEvent(dimensions.current)
323+
);
317324
return;
318325
}
319326
stateMachine.handleEvent(StateMachineEvent.NATIVE_BEGIN);
@@ -332,7 +339,7 @@ const Pressable = (props: PressableProps) => {
332339

333340
if (Platform.isTV) {
334341
handlePressOut(
335-
mockCenterPressableEvent(dimensions.current),
342+
viewCenterToPressableEvent(dimensions.current),
336343
!event.canceled
337344
);
338345
handleFinalize();

0 commit comments

Comments
 (0)