Skip to content

Commit 38e4d00

Browse files
committed
Expose animatable refs for v3 touchables
1 parent 45e6ac8 commit 38e4d00

4 files changed

Lines changed: 77 additions & 3 deletions

File tree

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

Lines changed: 31 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,18 @@ 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+
3754
test('Rect Button', () => {
3855
const pressFn = jest.fn();
3956

@@ -61,6 +78,18 @@ describe('[API v3] Components', () => {
6178
});
6279

6380
describe('Touchable', () => {
81+
test('exposes the native button as an animatable ref', () => {
82+
const ref = createRef<AnimatableViewRef>();
83+
84+
render(
85+
<GestureHandlerRootView>
86+
<Touchable ref={ref} />
87+
</GestureHandlerRootView>
88+
);
89+
90+
expect(ref.current?.getAnimatableRef?.()).toBe(ref.current);
91+
});
92+
6493
test('calls onPress on successful press', () => {
6594
const pressFn = jest.fn();
6695

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
useNativeGesture,
4141
useSimultaneousGestures,
4242
} from '../hooks';
43+
import { setAndForwardAnimatableRef } from './animatableRef';
4344
import { PureNativeButton } from './GestureButtons';
4445

4546
const DEFAULT_LONG_PRESS_DURATION = 500;
@@ -70,6 +71,7 @@ const Pressable = (props: PressableProps) => {
7071
simultaneousWith,
7172
requireToFail,
7273
block,
74+
ref,
7375
...remainingProps
7476
} = props;
7577

@@ -79,10 +81,19 @@ const Pressable = (props: PressableProps) => {
7981
const pressDelayTimeoutRef = useRef<number | null>(null);
8082
const isOnPressAllowed = useRef<boolean>(true);
8183
const isCurrentlyPressed = useRef<boolean>(false);
84+
const buttonRef = useRef<React.ComponentRef<typeof PureNativeButton> | null>(
85+
null
86+
);
8287
const dimensions = useRef<PressableDimensions>({
8388
width: 0,
8489
height: 0,
8590
});
91+
const setButtonRef = useCallback(
92+
(button: React.ComponentRef<typeof PureNativeButton> | null) => {
93+
setAndForwardAnimatableRef(buttonRef, ref, button);
94+
},
95+
[ref]
96+
);
8697

8798
const normalizedHitSlop: Insets = useMemo(
8899
() =>
@@ -369,6 +380,7 @@ const Pressable = (props: PressableProps) => {
369380
<GestureDetector gesture={gesture}>
370381
<PureNativeButton
371382
{...remainingProps}
383+
ref={setButtonRef}
372384
onLayout={setDimensions}
373385
accessible={accessible !== false}
374386
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
@@ -4,6 +4,7 @@ import { Platform } from 'react-native';
44
import GestureHandlerButton from '../../../components/GestureHandlerButton';
55
import { NativeDetector } from '../../detectors/NativeDetector';
66
import { useNativeGesture } from '../../hooks';
7+
import { setAndForwardAnimatableRef } from '../animatableRef';
78
import type {
89
AnimationDuration,
910
CallbackEventType,
@@ -99,6 +100,15 @@ export const Touchable = (props: TouchableProps) => {
99100
const longPressTimeout = useRef<ReturnType<typeof setTimeout> | undefined>(
100101
undefined
101102
);
103+
const buttonRef = useRef<React.ComponentRef<
104+
typeof GestureHandlerButton
105+
> | null>(null);
106+
const setButtonRef = useCallback(
107+
(button: React.ComponentRef<typeof GestureHandlerButton> | null) => {
108+
setAndForwardAnimatableRef(buttonRef, ref, button);
109+
},
110+
[ref]
111+
);
102112

103113
const wrappedLongPress = useCallback(() => {
104114
longPressDetected.current = true;
@@ -214,7 +224,7 @@ export const Touchable = (props: TouchableProps) => {
214224
{...rest}
215225
{...rippleProps}
216226
{...resolvedDurations}
217-
ref={ref ?? null}
227+
ref={setButtonRef}
218228
enabled={!disabled}
219229
defaultOpacity={defaultOpacity}
220230
defaultUnderlayOpacity={defaultUnderlayOpacity}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
if (ref) {
15+
(ref as AnimatableRef<T>).getAnimatableRef = () => localRef.current;
16+
}
17+
18+
if (typeof forwardedRef === 'function') {
19+
forwardedRef(ref);
20+
} else if (forwardedRef) {
21+
(forwardedRef as React.MutableRefObject<T | null>).current = ref;
22+
}
23+
}

0 commit comments

Comments
 (0)