Skip to content

Commit

Permalink
[Web] Adjust findNodeHandle to properly detect SVG (#3197)
Browse files Browse the repository at this point in the history
## 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>
  • Loading branch information
m-bert authored Nov 7, 2024
1 parent da9eed8 commit ef686fc
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 23 deletions.
2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"react-native-reanimated": "3.10.0",
"react-native-safe-area-context": "4.10.1",
"react-native-screens": "3.31.1",
"react-native-svg": "15.2.0",
"react-native-svg": "^15.8.0",
"react-native-web": "~0.19.10"
},
"devDependencies": {
Expand Down
19 changes: 8 additions & 11 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7165,12 +7165,8 @@ react-is@^17.0.1:
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==

"react-native-gesture-handler@link:..":
version "2.20.0"
dependencies:
"@egjs/hammerjs" "^2.0.17"
hoist-non-react-statics "^3.3.0"
invariant "^2.2.4"
prop-types "^15.7.2"
version "0.0.0"
uid ""

react-native-reanimated@3.10.0:
version "3.10.0"
Expand Down Expand Up @@ -7199,13 +7195,14 @@ react-native-screens@3.31.1:
react-freeze "^1.0.0"
warn-once "^0.1.0"

react-native-svg@15.2.0:
version "15.2.0"
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.2.0.tgz#9561a6b3bd6b44689f437ba13182afee33bd5557"
integrity sha512-R0E6IhcJfVLsL0lRmnUSm72QO+mTqcAOM5Jb8FVGxJqX3NfJMlMP0YyvcajZiaRR8CqQUpEoqrY25eyZb006kw==
react-native-svg@^15.8.0:
version "15.8.0"
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.8.0.tgz#9b5fd4f5cf5675197b3f4cbfcc77c215de2b9502"
integrity sha512-KHJzKpgOjwj1qeZzsBjxNdoIgv2zNCO9fVcoq2TEhTRsVV5DGTZ9JzUZwybd7q4giT/H3RdtqC3u44dWdO0Ffw==
dependencies:
css-select "^5.1.0"
css-tree "^1.1.3"
warn-once "0.1.1"

react-native-web@~0.19.10:
version "0.19.11"
Expand Down Expand Up @@ -8580,7 +8577,7 @@ walker@^1.0.7, walker@^1.0.8:
dependencies:
makeerror "1.0.12"

warn-once@^0.1.0:
warn-once@0.1.1, warn-once@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/warn-once/-/warn-once-0.1.1.tgz#952088f4fb56896e73fd4e6a3767272a3fccce43"
integrity sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==
Expand Down
18 changes: 10 additions & 8 deletions src/findNodeHandle.web.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
type GestureHandlerRef = {
viewTag: GestureHandlerRef;
current: HTMLElement;
};
import type { GestureHandlerRef, SVGRef } from './web/interfaces';
import { isRNSVGElement } from './web/utils';

export default function findNodeHandle(
viewRef: GestureHandlerRef | HTMLElement
): HTMLElement | number {
viewRef: GestureHandlerRef | SVGRef | HTMLElement | SVGElement
): HTMLElement | SVGElement | number {
// Old API assumes that child handler is HTMLElement.
// However, if we nest handlers, we will get ref to another handler.
// In that case, we want to recursively call findNodeHandle with new handler viewTag (which can also be ref to another handler).
if ((viewRef as GestureHandlerRef)?.viewTag !== undefined) {
return findNodeHandle((viewRef as GestureHandlerRef).viewTag);
}

if (viewRef instanceof HTMLElement) {
if (viewRef instanceof Element) {
if (viewRef.style.display === 'contents') {
return findNodeHandle(viewRef.firstChild as HTMLElement);
}

return viewRef;
}

if (isRNSVGElement(viewRef)) {
return (viewRef as SVGRef).elementRef.current;
}

// In new API, we receive ref object which `current` field points to wrapper `div` with `display: contents;`.
// We want to return the first descendant (in DFS order) that doesn't have this property.
let element = viewRef?.current;
let element = (viewRef as GestureHandlerRef)?.current;

while (element && element.style.display === 'contents') {
element = element.firstChild as HTMLElement;
Expand Down
13 changes: 11 additions & 2 deletions src/handlers/gestures/GestureDetector/Wrap.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,23 @@ export const Wrap = forwardRef<HTMLDivElement, PropsWithChildren<{}>>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const child: any = React.Children.only(children);

const isRNSVGNode =
Object.getPrototypeOf(child?.type)?.name === 'WebShape';

const additionalProps = isRNSVGNode
? { collapsable: false, ref }
: { collapsable: false };

const clone = React.cloneElement(
child,
{ collapsable: false },
additionalProps,
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
child.props.children
);

return (
return isRNSVGNode ? (
clone
) : (
<div
ref={ref as LegacyRef<HTMLDivElement>}
style={{ display: 'contents' }}>
Expand Down
9 changes: 9 additions & 0 deletions src/web/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,12 @@ export enum WheelDevice {
MOUSE,
TOUCHPAD,
}

export type GestureHandlerRef = {
viewTag: GestureHandlerRef;
current: HTMLElement;
};

export type SVGRef = {
elementRef: { current: SVGElement };
};
48 changes: 47 additions & 1 deletion src/web/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { PointerType } from '../PointerType';
import type { Point, StylusData } from './interfaces';
import type {
GestureHandlerRef,
Point,
StylusData,
SVGRef,
} from './interfaces';

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

return { tiltX, tiltY };
}

const RNSVGElements = [
'Circle',
'ClipPath',
'Ellipse',
'ForeignObject',
'G',
'Image',
'Line',
'Marker',
'Mask',
'Path',
'Pattern',
'Polygon',
'Polyline',
'Rect',
'Svg',
'Symbol',
'TSpan',
'Text',
'TextPath',
'Use',
];

// This function helps us determine whether given node is SVGElement or not. In our implementation of
// findNodeHandle, we can encounter such element in 2 forms - SVG tag or ref to SVG Element. Since Gesture Handler
// does not depend on SVG, we use our simplified SVGRef type that has `elementRef` field. This is something that is present
// in actual SVG ref object.
//
// In order to make sure that node passed into this function is in fact SVG element, first we check if its constructor name
// corresponds to one of the possible SVG elements. Then we also check if `elementRef` field exists.
// By doing both steps we decrease probability of detecting situations where, for example, user makes custom `Circle` and
// we treat it as SVG.
export function isRNSVGElement(viewRef: SVGRef | GestureHandlerRef) {
const componentClassName = Object.getPrototypeOf(viewRef).constructor.name;

return (
RNSVGElements.indexOf(componentClassName) >= 0 &&
Object.hasOwn(viewRef, 'elementRef')
);
}

0 comments on commit ef686fc

Please sign in to comment.