Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ class RotationGestureHandler : GestureHandler() {
override fun onRotationBegin(detector: RotationGestureDetector) = true

override fun onRotationEnd(detector: RotationGestureDetector) {
end()
if (state == STATE_ACTIVE) {
end()
} else {
fail()
}
}
}

Expand All @@ -56,12 +60,11 @@ class RotationGestureHandler : GestureHandler() {
anchorX = point.x
anchorY = point.y
}
if (sourceEvent.actionMasked == MotionEvent.ACTION_UP) {
if (state == STATE_ACTIVE) {
end()
} else {
fail()
}

// ACTION_UP is already handled in rotationGestureDetector.onTouchEvent (and effectively in onRotationEnd)
// if more than one pointer was used
if (sourceEvent.actionMasked == MotionEvent.ACTION_UP && state == STATE_BEGAN) {
fail()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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();
Expand Down Expand Up @@ -202,11 +203,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<number | null>(null);
const hoverOutTimeout = useRef<number | null>(null);
Expand Down Expand Up @@ -259,7 +265,7 @@ const Pressable = (props: PressableProps) => {
);
})
.onTouchesUp(() => {
if (Platform.OS === 'android') {
if (Platform.OS === 'android' && !isScreenReaderEnabled) {
// Prevents potential soft-locks
stateMachine.reset();
handleFinalize();
Expand All @@ -280,7 +286,7 @@ const Pressable = (props: PressableProps) => {
handleFinalize();
}
}),
[stateMachine, handleFinalize, handlePressOut]
[stateMachine, handleFinalize, handlePressOut, isScreenReaderEnabled]
);

// RNButton is placed inside ButtonGesture to enable Android's ripple and to capture non-propagating events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface SwipeableProps {
/**
*
*/
ref?: React.RefObject<SwipeableMethods | null>;
ref?: React.Ref<SwipeableMethods>;

/**
* Sets a `testID` property, allowing for querying `ReanimatedSwipeable` for it in tests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ export const ALLOWED_PROPS = [
...nativeViewGestureHandlerProps,
];

// In some environments (e.g. `Next.js`) Reanimated Babel plugin might not be used.
// In that case we would wrongly suggest to add `runOnJS` to gesture configuration, even if the user doesn't use worklets at all.
// To prevent this, we check whether the plugin is enabled by defining a worklet and checking if the `__workletHash` property is available.
function emptyWorklet() {
'worklet';
}

// @ts-expect-error if `emptyWorklet` is a worklet, `__workletHash` will be available, if not then the check will return false.
const wasBabelPluginEnabled = emptyWorklet.__workletHash !== undefined;

function convertToHandlerTag(ref: GestureRef): number {
if (typeof ref === 'number') {
return ref;
Expand Down Expand Up @@ -105,7 +115,7 @@ export function checkGestureCallbacksForWorklets(gesture: GestureType) {
const areAllNotWorklets = !areSomeWorklets && areSomeNotWorklets;
// If none of the callbacks are worklets and the gesture is not explicitly marked with
// `.runOnJS(true)` show a warning
if (areAllNotWorklets && !isTestEnv()) {
if (areAllNotWorklets && wasBabelPluginEnabled && !isTestEnv()) {
console.warn(
tagMessage(
`None of the callbacks in the gesture are worklets. If you wish to run them on the JS thread use '.runOnJS(true)' modifier on the gesture to make this explicit. Otherwise, mark the callbacks as 'worklet' to run them on the UI thread.`
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,11 @@ export default class RotationGestureDetector
}

private finish(): void {
if (!this.isInProgress) {
return;
if (this.isInProgress) {
this.isInProgress = false;
this.keyPointers = [NaN, NaN];
}

this.isInProgress = false;
this.keyPointers = [NaN, NaN];
this.onRotationEnd(this);
}

Expand Down Expand Up @@ -138,9 +137,8 @@ export default class RotationGestureDetector
break;

case EventTypes.UP:
if (this.isInProgress) {
this.finish();
}
this.finish();

break;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,11 @@ export default class PinchGestureHandler extends GestureHandler {
protected onPointerUp(event: AdaptedEvent): void {
super.onPointerUp(event);
this.tracker.removeFromTracker(event.pointerId);
if (this.state !== State.ACTIVE) {
return;
}
this.scaleGestureDetector.onTouchEvent(event, this.tracker);

if (this.state === State.ACTIVE) {
// We don't have to call it in the else branch as it would simply return `true`.
this.scaleGestureDetector.onTouchEvent(event, this.tracker);

this.end();
} else {
this.fail();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ export default class RotationGestureHandler extends GestureHandler {
return true;
},
onRotationEnd: (_detector: RotationGestureDetector): void => {
this.end();
if (this.state === State.ACTIVE) {
this.end();
} else {
this.fail();
}
},
};

Expand Down Expand Up @@ -128,16 +132,6 @@ export default class RotationGestureHandler extends GestureHandler {
super.onPointerUp(event);
this.tracker.removeFromTracker(event.pointerId);
this.rotationGestureDetector.onTouchEvent(event, this.tracker);

if (this.state !== State.ACTIVE) {
return;
}

if (this.state === State.ACTIVE) {
this.end();
} else {
this.fail();
}
}

protected onPointerRemove(event: AdaptedEvent): void {
Expand Down
Loading