Skip to content

Commit ef686fc

Browse files
authored
[Web] Adjust findNodeHandle to properly detect SVG (#3197)
## Description #3127 introduced our own implementation of `findNodeHandle` on web. Unfortunately, it doesn't work if someone uses `GestureDetector` on SVG elements, e.g.: ```jsx <Svg width={200} height={200}> <GestureDetector gesture={tapGestureCircle}> <Circle r={200} cx={210} cy={210} fill={circleFill} /> </GestureDetector> </Svg> ``` Code above would render additional `div` element with `display: 'contents';`, which would break `SVG`. This PR does the following things: 1. Bumps `react-native-svg` version 2. Modifies `Wrap` component such that now if element is part of `SVG` it doesn't wrap it into additional `div` 3. Adjusts `findNodeHandle` function to properly handle `SVG` refs ## Test plan <details> <summary>Tested on the following example:</summary> ```jsx import { GestureHandlerRootView, GestureDetector, Gesture, } from 'react-native-gesture-handler'; import { View } from 'react-native'; import { Svg, Circle } from 'react-native-svg'; import { useState, useCallback } from 'react'; export default function App() { const [circleFill, setCircleFill] = useState('blue'); const switchCircleColor = useCallback( () => setCircleFill((old) => (old === 'blue' ? 'brown' : 'blue')), [setCircleFill] ); const tapGestureCircle = Gesture.Tap().runOnJS(true).onEnd(switchCircleColor); return ( <GestureHandlerRootView style={{ flex: 1, paddingTop: 200 }}> <View style={{ padding: 10, borderWidth: 1, alignSelf: 'flex-start' }}> <Svg width={200} height={200}> <GestureDetector gesture={tapGestureCircle}> <Circle r={200} cx={210} cy={210} fill={circleFill} /> </GestureDetector> </Svg> </View> </GestureHandlerRootView> ); } ``` </details>
1 parent da9eed8 commit ef686fc

File tree

6 files changed

+86
-23
lines changed

6 files changed

+86
-23
lines changed

example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"react-native-reanimated": "3.10.0",
3939
"react-native-safe-area-context": "4.10.1",
4040
"react-native-screens": "3.31.1",
41-
"react-native-svg": "15.2.0",
41+
"react-native-svg": "^15.8.0",
4242
"react-native-web": "~0.19.10"
4343
},
4444
"devDependencies": {

example/yarn.lock

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7165,12 +7165,8 @@ react-is@^17.0.1:
71657165
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
71667166

71677167
"react-native-gesture-handler@link:..":
7168-
version "2.20.0"
7169-
dependencies:
7170-
"@egjs/hammerjs" "^2.0.17"
7171-
hoist-non-react-statics "^3.3.0"
7172-
invariant "^2.2.4"
7173-
prop-types "^15.7.2"
7168+
version "0.0.0"
7169+
uid ""
71747170

71757171
react-native-reanimated@3.10.0:
71767172
version "3.10.0"
@@ -7199,13 +7195,14 @@ react-native-screens@3.31.1:
71997195
react-freeze "^1.0.0"
72007196
warn-once "^0.1.0"
72017197

7202-
react-native-svg@15.2.0:
7203-
version "15.2.0"
7204-
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.2.0.tgz#9561a6b3bd6b44689f437ba13182afee33bd5557"
7205-
integrity sha512-R0E6IhcJfVLsL0lRmnUSm72QO+mTqcAOM5Jb8FVGxJqX3NfJMlMP0YyvcajZiaRR8CqQUpEoqrY25eyZb006kw==
7198+
react-native-svg@^15.8.0:
7199+
version "15.8.0"
7200+
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.8.0.tgz#9b5fd4f5cf5675197b3f4cbfcc77c215de2b9502"
7201+
integrity sha512-KHJzKpgOjwj1qeZzsBjxNdoIgv2zNCO9fVcoq2TEhTRsVV5DGTZ9JzUZwybd7q4giT/H3RdtqC3u44dWdO0Ffw==
72067202
dependencies:
72077203
css-select "^5.1.0"
72087204
css-tree "^1.1.3"
7205+
warn-once "0.1.1"
72097206

72107207
react-native-web@~0.19.10:
72117208
version "0.19.11"
@@ -8580,7 +8577,7 @@ walker@^1.0.7, walker@^1.0.8:
85808577
dependencies:
85818578
makeerror "1.0.12"
85828579

8583-
warn-once@^0.1.0:
8580+
warn-once@0.1.1, warn-once@^0.1.0:
85848581
version "0.1.1"
85858582
resolved "https://registry.yarnpkg.com/warn-once/-/warn-once-0.1.1.tgz#952088f4fb56896e73fd4e6a3767272a3fccce43"
85868583
integrity sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==

src/findNodeHandle.web.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,31 @@
1-
type GestureHandlerRef = {
2-
viewTag: GestureHandlerRef;
3-
current: HTMLElement;
4-
};
1+
import type { GestureHandlerRef, SVGRef } from './web/interfaces';
2+
import { isRNSVGElement } from './web/utils';
53

64
export default function findNodeHandle(
7-
viewRef: GestureHandlerRef | HTMLElement
8-
): HTMLElement | number {
5+
viewRef: GestureHandlerRef | SVGRef | HTMLElement | SVGElement
6+
): HTMLElement | SVGElement | number {
97
// Old API assumes that child handler is HTMLElement.
108
// However, if we nest handlers, we will get ref to another handler.
119
// In that case, we want to recursively call findNodeHandle with new handler viewTag (which can also be ref to another handler).
1210
if ((viewRef as GestureHandlerRef)?.viewTag !== undefined) {
1311
return findNodeHandle((viewRef as GestureHandlerRef).viewTag);
1412
}
1513

16-
if (viewRef instanceof HTMLElement) {
14+
if (viewRef instanceof Element) {
1715
if (viewRef.style.display === 'contents') {
1816
return findNodeHandle(viewRef.firstChild as HTMLElement);
1917
}
2018

2119
return viewRef;
2220
}
2321

22+
if (isRNSVGElement(viewRef)) {
23+
return (viewRef as SVGRef).elementRef.current;
24+
}
25+
2426
// In new API, we receive ref object which `current` field points to wrapper `div` with `display: contents;`.
2527
// We want to return the first descendant (in DFS order) that doesn't have this property.
26-
let element = viewRef?.current;
28+
let element = (viewRef as GestureHandlerRef)?.current;
2729

2830
while (element && element.style.display === 'contents') {
2931
element = element.firstChild as HTMLElement;

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,23 @@ export const Wrap = forwardRef<HTMLDivElement, PropsWithChildren<{}>>(
1212
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1313
const child: any = React.Children.only(children);
1414

15+
const isRNSVGNode =
16+
Object.getPrototypeOf(child?.type)?.name === 'WebShape';
17+
18+
const additionalProps = isRNSVGNode
19+
? { collapsable: false, ref }
20+
: { collapsable: false };
21+
1522
const clone = React.cloneElement(
1623
child,
17-
{ collapsable: false },
24+
additionalProps,
1825
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1926
child.props.children
2027
);
2128

22-
return (
29+
return isRNSVGNode ? (
30+
clone
31+
) : (
2332
<div
2433
ref={ref as LegacyRef<HTMLDivElement>}
2534
style={{ display: 'contents' }}>

src/web/interfaces.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,12 @@ export enum WheelDevice {
179179
MOUSE,
180180
TOUCHPAD,
181181
}
182+
183+
export type GestureHandlerRef = {
184+
viewTag: GestureHandlerRef;
185+
current: HTMLElement;
186+
};
187+
188+
export type SVGRef = {
189+
elementRef: { current: SVGElement };
190+
};

src/web/utils.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { PointerType } from '../PointerType';
2-
import type { Point, StylusData } from './interfaces';
2+
import type {
3+
GestureHandlerRef,
4+
Point,
5+
StylusData,
6+
SVGRef,
7+
} from './interfaces';
38

49
export function isPointerInBounds(view: HTMLElement, { x, y }: Point): boolean {
510
const rect: DOMRect = view.getBoundingClientRect();
@@ -227,3 +232,44 @@ function spherical2tilt(altitudeAngle: number, azimuthAngle: number) {
227232

228233
return { tiltX, tiltY };
229234
}
235+
236+
const RNSVGElements = [
237+
'Circle',
238+
'ClipPath',
239+
'Ellipse',
240+
'ForeignObject',
241+
'G',
242+
'Image',
243+
'Line',
244+
'Marker',
245+
'Mask',
246+
'Path',
247+
'Pattern',
248+
'Polygon',
249+
'Polyline',
250+
'Rect',
251+
'Svg',
252+
'Symbol',
253+
'TSpan',
254+
'Text',
255+
'TextPath',
256+
'Use',
257+
];
258+
259+
// This function helps us determine whether given node is SVGElement or not. In our implementation of
260+
// findNodeHandle, we can encounter such element in 2 forms - SVG tag or ref to SVG Element. Since Gesture Handler
261+
// does not depend on SVG, we use our simplified SVGRef type that has `elementRef` field. This is something that is present
262+
// in actual SVG ref object.
263+
//
264+
// In order to make sure that node passed into this function is in fact SVG element, first we check if its constructor name
265+
// corresponds to one of the possible SVG elements. Then we also check if `elementRef` field exists.
266+
// By doing both steps we decrease probability of detecting situations where, for example, user makes custom `Circle` and
267+
// we treat it as SVG.
268+
export function isRNSVGElement(viewRef: SVGRef | GestureHandlerRef) {
269+
const componentClassName = Object.getPrototypeOf(viewRef).constructor.name;
270+
271+
return (
272+
RNSVGElements.indexOf(componentClassName) >= 0 &&
273+
Object.hasOwn(viewRef, 'elementRef')
274+
);
275+
}

0 commit comments

Comments
 (0)