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
@@ -0,0 +1,53 @@
import { renderHook } from '@testing-library/react-native';
import type { PropsWithChildren } from 'react';
import React from 'react';

import { useJSResponderHandler } from '../v3/hooks/useJSResponderHandler';
import {
JSResponderContext,
type JSResponderContextValue,
} from '../v3/scrollViewInterop';
import { type Gesture, SingleGestureName } from '../v3/types';

jest.mock('../handlers/gestures/reanimatedWrapper', () => ({
Reanimated: {
isSharedValue: (value: unknown) =>
typeof value === 'object' &&
value !== null &&
'__isFakeSharedValue' in value,
},
}));

describe('useJSResponderHandler', () => {
test('reads a SharedValue `enabled` when the touch starts', () => {
const enabled = { __isFakeSharedValue: true, value: true };
const gesture = {
type: SingleGestureName.Tap,
config: { enabled },
} as unknown as Gesture<unknown, unknown, unknown>;
const context: JSResponderContextValue = {
isRNGHResponderEvent: { current: false },
keyboardShouldPersistTaps: 'handled',
};
const wrapper = ({ children }: PropsWithChildren) => (
<JSResponderContext value={context}>{children}</JSResponderContext>
);

const { result } = renderHook(() => useJSResponderHandler(gesture), {
wrapper,
});

// No re-render between the flips, like a SharedValue changed on the UI thread
const touch = () => {
context.isRNGHResponderEvent.current = false;
result.current.handleStartShouldSetResponder();
return context.isRNGHResponderEvent.current;
};

expect(touch()).toBe(true);
enabled.value = false;
expect(touch()).toBe(false);
enabled.value = true;
expect(touch()).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { use, useCallback, useEffect, useRef, useState } from 'react';
import { use, useCallback } from 'react';

import { Worklets } from '../../handlers/gestures/reanimatedWrapper';
import {
JSResponderContext,
updateResponderEventValue,
} from '../scrollViewInterop';
import { type Gesture, type SharedValue, SingleGestureName } from '../types';
import { type Gesture, SingleGestureName } from '../types';
import { isComposedGesture, maybeUnpackValue } from './utils';
import {
getEnabledSharedValues,
SHARED_VALUE_OFFSET,
} from './utils/reanimatedUtils';

// adding 0.5 to not call Math.random and to make sure that listener ID is not an integer to avoid conflicts
let nextJSResponderContextListenerId = SHARED_VALUE_OFFSET + 0.5;
const ignoreResponderEvent = () => false;

function isSupportedGesture<
TConfig,
Expand Down Expand Up @@ -59,90 +53,23 @@ export function useJSResponderHandler<
countNativeAsHandled = true
) {
const jsResponderContext = use(JSResponderContext);
const [enabledSharedValueRevision, setEnabledSharedValueRevision] =
useState(0);
const listenerIdRef = useRef<number | null>(null);

if (listenerIdRef.current === null) {
listenerIdRef.current = nextJSResponderContextListenerId++;
}

useEffect(() => {
if (gesture === undefined) {
return;
}

const enabledSharedValues = getEnabledSharedValues(gesture);

if (Worklets === undefined || enabledSharedValues.length === 0) {
return;
}

const listenerId = listenerIdRef.current;
if (listenerId === null) {
return;
}

const { scheduleOnUI, scheduleOnRN } = Worklets;

const notifyEnabledChanged = () => {
setEnabledSharedValueRevision((revision) => revision + 1);
};

const attachListeners = (
sharedValues: SharedValue<boolean>[],
id: number,
notify: () => void
) => {
'worklet';
const listener = () => {
scheduleOnRN(notify);
};

for (const sharedValue of sharedValues) {
sharedValue.addListener(id, listener);
}
};

const detachListeners = (
sharedValues: SharedValue<boolean>[],
id: number
) => {
'worklet';
for (const sharedValue of sharedValues) {
sharedValue.removeListener(id);
}
};

scheduleOnUI(
attachListeners,
enabledSharedValues,
listenerId,
notifyEnabledChanged
);

return () => {
scheduleOnUI(detachListeners, enabledSharedValues, listenerId);
};
}, [gesture]);

const shouldHandleJSResponderEvent = useCallback(() => {
void enabledSharedValueRevision;
return (
gesture !== undefined && isSupportedGesture(gesture, countNativeAsHandled)
);
}, [enabledSharedValueRevision, gesture, countNativeAsHandled]);

// `enabled` is read when the touch starts, so a SharedValue is always current
const handleStartShouldSetResponder = useCallback(() => {
if (shouldHandleJSResponderEvent()) {
if (
gesture !== undefined &&
isSupportedGesture(gesture, countNativeAsHandled)
) {
updateResponderEventValue(jsResponderContext, true);
}

return false;
}, [jsResponderContext, shouldHandleJSResponderEvent]);
}, [jsResponderContext, gesture, countNativeAsHandled]);

return {
handleStartShouldSetResponder:
jsResponderContext == null ? () => false : handleStartShouldSetResponder,
jsResponderContext == null
? ignoreResponderEvent
: handleStartShouldSetResponder,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ import { normalizeHitSlop } from '../../../handlers/hitSlop';
import { NativeProxy } from '../../NativeProxy';
import type {
BaseGestureConfig,
Gesture,
SharedValue,
SharedValueOrT,
} from '../../types';
import { HandlerCallbacks } from './propsWhiteList';
import { isComposedGesture } from './relationUtils';

// Variant of djb2 hash function.
// Taken from https://gist.github.com/eplawless/52813b1d8ad9af510d85?permalink_comment_id=3367765#gistcomment-3367765
Expand Down Expand Up @@ -169,22 +167,3 @@ export function maybeUnpackValue<T>(

return (Reanimated?.isSharedValue(v) ? v.value : v) as T;
}

export function getEnabledSharedValues<
TConfig,
THandlerData,
TExtendedHandlerData extends THandlerData,
>(
gesture: Gesture<TConfig, THandlerData, TExtendedHandlerData>
): SharedValue<boolean>[] {
if (Reanimated === undefined) {
return [];
}

if (isComposedGesture(gesture)) {
return gesture.gestures.flatMap(getEnabledSharedValues);
}

const enabled = gesture.config.enabled;
return Reanimated.isSharedValue<boolean>(enabled) ? [enabled] : [];
}
Loading