Skip to content

Commit bb248de

Browse files
authored
[Web] Handle SVG wrapped with createAnimatedComponent (#3379)
## Description Currently, when user wraps `SVG` component with `createAnimatedComponent`, we have no way to detect that `child` node passed into [Wrap](https://github.com/software-mansion/react-native-gesture-handler/blob/6e8d88e5a3b15bb399c188d4c23603b4e0f4a8bc/src/handlers/gestures/GestureDetector/Wrap.web.tsx#L9) comes from `SVG`. This results in additional `div` with `display" contents;` being added, which effectively breaks `SVG`. >[!CAUTION] >In order to fix this, I've opened [PR against Reanimated](software-mansion/react-native-reanimated#6978) that adds inner component name into `displayName` of the `forwardRef` that `createAnimatedComponent` returns. Fixes #3356 ## Test plan <details> <summary>Tested on the code from issue</summary> ```jsx import { useMemo, useRef } from 'react'; import { Text, View, Animated, PanResponder } from "react-native"; import { Svg, Rect } from 'react-native-svg'; import { GestureDetector, Gesture, GestureHandlerRootView } from 'react-native-gesture-handler'; import RNRAnimated, { useSharedValue, useAnimatedProps} from 'react-native-reanimated' const RNRAnimatedRect = RNRAnimated.createAnimatedComponent(Rect) const AnimatedRect = Animated.createAnimatedComponent(Rect) export default function Index() { const pan = useRef(new Animated.Value(0)).current; const panResponder = useRef( PanResponder.create({ onMoveShouldSetPanResponder: () => true, onPanResponderMove: Animated.event([null, {dx: pan}]), onPanResponderRelease: () => { pan.extractOffset(); }, }), ).current; const RNTest = <View style={{borderWidth: 1}}> <Text>With RN... though this isn't ideal because it lets you drag the yellow region</Text> <Animated.View {...panResponder.panHandlers}> <Svg width={200} height={100} > <Rect width={200} height={100} fill='yellow'/> <AnimatedRect width={100} height={100} x={pan} y={0} fill='red'/> </Svg> </Animated.View> </View> const x = useSharedValue(0) const animatedX = useAnimatedProps(() => ({x: x.value}), [x]); const xStart = useSharedValue(0); const svgPanGesture = useMemo(() => { return Gesture.Pan() .onBegin(() => {xStart.value = x.value}) .onChange((e) => { x.value = xStart.value + e.translationX }) }, []) const RNGHTest = <View style={{borderWidth: 1}}> <Text>With RNGH and RNR</Text> <Svg width={200} height={100}> <Rect width={200} height={100} fill='yellow'/> <GestureDetector gesture={svgPanGesture}> <RNRAnimatedRect width={100} height={100} animatedProps={animatedX} y={0} fill='red' /> </GestureDetector> </Svg> </View> return ( <GestureHandlerRootView style={{ flex: 1 }}> <View style={{ flex: 1, justifyContent: "center", }} > {RNTest} {RNGHTest} </View> </GestureHandlerRootView> ); } ``` </details>
1 parent 30e60e6 commit bb248de

File tree

2 files changed

+17
-7
lines changed

2 files changed

+17
-7
lines changed

src/handlers/gestures/GestureDetector/Wrap.web.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import React, { forwardRef } from 'react';
22
import type { LegacyRef, PropsWithChildren } from 'react';
33
import { tagMessage } from '../../../utils';
4+
import { isRNSVGNode } from '../../../web/utils';
45

56
export const Wrap = forwardRef<HTMLDivElement, PropsWithChildren<{}>>(
67
({ children }, ref) => {
78
try {
89
// eslint-disable-next-line @typescript-eslint/no-explicit-any
910
const child: any = React.Children.only(children);
1011

11-
const isRNSVGNode =
12-
Object.getPrototypeOf(child?.type)?.name === 'WebShape';
13-
14-
if (isRNSVGNode) {
12+
if (isRNSVGNode(child)) {
1513
const clone = React.cloneElement(
1614
child,
1715
{ ref },

src/web/utils.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ function spherical2tilt(altitudeAngle: number, azimuthAngle: number) {
233233
return { tiltX, tiltY };
234234
}
235235

236-
const RNSVGElements = [
236+
export const RNSVGElements = new Set([
237237
'Circle',
238238
'ClipPath',
239239
'Ellipse',
@@ -254,7 +254,7 @@ const RNSVGElements = [
254254
'Text',
255255
'TextPath',
256256
'Use',
257-
];
257+
]);
258258

259259
// This function helps us determine whether given node is SVGElement or not. In our implementation of
260260
// findNodeHandle, we can encounter such element in 2 forms - SVG tag or ref to SVG Element. Since Gesture Handler
@@ -269,7 +269,19 @@ export function isRNSVGElement(viewRef: SVGRef | GestureHandlerRef) {
269269
const componentClassName = Object.getPrototypeOf(viewRef).constructor.name;
270270

271271
return (
272-
RNSVGElements.indexOf(componentClassName) >= 0 &&
272+
RNSVGElements.has(componentClassName) &&
273273
Object.hasOwn(viewRef, 'elementRef')
274274
);
275275
}
276+
277+
// This function checks if given node is SVGElement. Unlike the function above, this one
278+
// operates on React Nodes, not DOM nodes.
279+
//
280+
// Second condition was introduced to handle case where SVG element was wrapped with
281+
// `createAnimatedComponent` from Reanimated.
282+
export function isRNSVGNode(node: any) {
283+
return (
284+
Object.getPrototypeOf(node?.type)?.name === 'WebShape' ||
285+
RNSVGElements.has(node?.type?.displayName)
286+
);
287+
}

0 commit comments

Comments
 (0)