Skip to content

Commit ed5cd84

Browse files
committed
Expose animatable refs for v3 touchables
1 parent 183f348 commit ed5cd84

4 files changed

Lines changed: 111 additions & 3 deletions

File tree

packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { render, renderHook } from '@testing-library/react-native';
2-
import { act } from 'react';
2+
import { act, createRef } from 'react';
3+
import type { View } from 'react-native';
34

45
import GestureHandlerRootView from '../components/GestureHandlerRootView';
56
import { fireGestureHandler, getByGestureTestId } from '../jestUtils';
67
import { State } from '../State';
7-
import { RectButton, Touchable } from '../v3/components';
8+
import { Pressable, RectButton, Touchable } from '../v3/components';
89
import { usePanGesture } from '../v3/hooks/gestures';
910
import type { SingleGesture } from '../v3/types';
1011

12+
type AnimatableViewRef = View & {
13+
getAnimatableRef?: () => View | null;
14+
};
15+
1116
describe('[API v3] Hooks', () => {
1217
test('Pan gesture', () => {
1318
const onBegin = jest.fn();
@@ -34,6 +39,51 @@ describe('[API v3] Hooks', () => {
3439
});
3540

3641
describe('[API v3] Components', () => {
42+
test('Pressable exposes the native button as an animatable ref', () => {
43+
const ref = createRef<AnimatableViewRef>();
44+
45+
render(
46+
<GestureHandlerRootView>
47+
<Pressable ref={ref} />
48+
</GestureHandlerRootView>
49+
);
50+
51+
expect(ref.current?.getAnimatableRef?.()).toBe(ref.current);
52+
});
53+
54+
test('Pressable forwards function refs on mount and unmount', () => {
55+
const ref = jest.fn();
56+
57+
const { unmount } = render(
58+
<GestureHandlerRootView>
59+
<Pressable ref={ref} />
60+
</GestureHandlerRootView>
61+
);
62+
63+
expect(ref).toHaveBeenCalledWith(expect.anything());
64+
65+
unmount();
66+
67+
expect(ref).toHaveBeenLastCalledWith(null);
68+
});
69+
70+
test('Pressable animatable refs stay bound to their host instance', () => {
71+
const ref = createRef<AnimatableViewRef>();
72+
const renderPressable = (key: string) => (
73+
<GestureHandlerRootView>
74+
<Pressable key={key} ref={ref} />
75+
</GestureHandlerRootView>
76+
);
77+
const { rerender } = render(renderPressable('first'));
78+
const firstRef = ref.current;
79+
80+
rerender(renderPressable('second'));
81+
const secondRef = ref.current;
82+
83+
expect(firstRef?.getAnimatableRef?.()).toBe(firstRef);
84+
expect(secondRef?.getAnimatableRef?.()).toBe(secondRef);
85+
});
86+
3787
test('Rect Button', () => {
3888
const pressFn = jest.fn();
3989

@@ -61,6 +111,18 @@ describe('[API v3] Components', () => {
61111
});
62112

63113
describe('Touchable', () => {
114+
test('exposes the native button as an animatable ref', () => {
115+
const ref = createRef<AnimatableViewRef>();
116+
117+
render(
118+
<GestureHandlerRootView>
119+
<Touchable ref={ref} />
120+
</GestureHandlerRootView>
121+
);
122+
123+
expect(ref.current?.getAnimatableRef?.()).toBe(ref.current);
124+
});
125+
64126
test('calls onPress on successful press', () => {
65127
const pressFn = jest.fn();
66128

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
useNativeGesture,
4343
useSimultaneousGestures,
4444
} from '../hooks';
45+
import { setAndForwardAnimatableRef } from './animatableRef';
4546
import { PureNativeButton } from './GestureButtons';
4647

4748
const DEFAULT_LONG_PRESS_DURATION = 500;
@@ -72,6 +73,7 @@ const Pressable = (props: PressableProps) => {
7273
simultaneousWith,
7374
requireToFail,
7475
block,
76+
ref,
7577
...remainingProps
7678
} = props;
7779

@@ -81,10 +83,19 @@ const Pressable = (props: PressableProps) => {
8183
const pressDelayTimeoutRef = useRef<number | null>(null);
8284
const isOnPressAllowed = useRef<boolean>(true);
8385
const isCurrentlyPressed = useRef<boolean>(false);
86+
const buttonRef = useRef<React.ComponentRef<typeof PureNativeButton> | null>(
87+
null
88+
);
8489
const dimensions = useRef<PressableDimensions>({
8590
width: 0,
8691
height: 0,
8792
});
93+
const setButtonRef = useCallback(
94+
(button: React.ComponentRef<typeof PureNativeButton> | null) => {
95+
setAndForwardAnimatableRef(buttonRef, ref, button);
96+
},
97+
[ref]
98+
);
8899

89100
const normalizedHitSlop: Insets = useMemo(
90101
() =>
@@ -399,6 +410,7 @@ const Pressable = (props: PressableProps) => {
399410
<PureNativeButton
400411
{...remainingProps}
401412
{...tvProps}
413+
ref={setButtonRef}
402414
onLayout={setDimensions}
403415
accessible={accessible !== false}
404416
hitSlop={appliedHitSlop}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import GestureHandlerButton from '../../../components/GestureHandlerButton';
55
import { getTVProps } from '../../../components/utils';
66
import { NativeDetector } from '../../detectors/NativeDetector';
77
import { useNativeGesture } from '../../hooks';
8+
import { setAndForwardAnimatableRef } from '../animatableRef';
89
import type {
910
AnimationDuration,
1011
CallbackEventType,
@@ -100,6 +101,15 @@ export const Touchable = (props: TouchableProps) => {
100101
const longPressTimeout = useRef<ReturnType<typeof setTimeout> | undefined>(
101102
undefined
102103
);
104+
const buttonRef = useRef<React.ComponentRef<
105+
typeof GestureHandlerButton
106+
> | null>(null);
107+
const setButtonRef = useCallback(
108+
(button: React.ComponentRef<typeof GestureHandlerButton> | null) => {
109+
setAndForwardAnimatableRef(buttonRef, ref, button);
110+
},
111+
[ref]
112+
);
103113

104114
const wrappedLongPress = useCallback(() => {
105115
longPressDetected.current = true;
@@ -218,7 +228,7 @@ export const Touchable = (props: TouchableProps) => {
218228
{...tvProps}
219229
{...rippleProps}
220230
{...resolvedDurations}
221-
ref={ref ?? null}
231+
ref={setButtonRef}
222232
enabled={!disabled}
223233
defaultOpacity={defaultOpacity}
224234
defaultUnderlayOpacity={defaultUnderlayOpacity}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type React from 'react';
2+
3+
type AnimatableRef<T> = T & {
4+
getAnimatableRef?: () => T | null;
5+
};
6+
7+
export function setAndForwardAnimatableRef<T extends object>(
8+
localRef: React.MutableRefObject<T | null>,
9+
forwardedRef: React.Ref<T> | undefined,
10+
ref: T | null
11+
) {
12+
localRef.current = ref;
13+
14+
const animatableRef = ref as AnimatableRef<T> | null;
15+
if (animatableRef && !animatableRef.getAnimatableRef) {
16+
animatableRef.getAnimatableRef = () => ref;
17+
}
18+
19+
if (typeof forwardedRef === 'function') {
20+
forwardedRef(ref);
21+
} else if (forwardedRef) {
22+
(forwardedRef as React.MutableRefObject<T | null>).current = ref;
23+
}
24+
}

0 commit comments

Comments
 (0)