fix: circular-dependencies (#2381)

# Summary
- Require cycles are allowed, but can result in uninitialized values.
Consider refactoring to remove the need for a cycle.
- extract SVG web components.
- extract web types.
- extract utils function.

## Test Plan
You can test that problem by running a test example `Test1813`

### What are the steps to reproduce (after prerequisites)?
Without those changes, you can occur that problem by running a test
example `Test1813`

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |         |
| Android |         |

---------

Co-authored-by: Jakub Grzywacz <jakub.grzywacz@swmansion.com>
This commit is contained in:
Bohdan Artiukhov
2024-07-31 11:45:04 +02:00
committed by GitHub
parent 129e8ed8b5
commit a27e17f505
13 changed files with 886 additions and 808 deletions

View File

@@ -1,18 +1,17 @@
import React from 'react';
import { PlatformColor, Platform, Button, DynamicColorIOS } from 'react-native';
import {
Svg,
Circle,
Rect,
Text,
TSpan,
} from 'react-native-svg';
import {PlatformColor, Platform, Button, DynamicColorIOS} from 'react-native';
import {Svg, Circle, Rect, Text, TSpan} from 'react-native-svg';
const color = PlatformColor(Platform.select({
ios: 'systemTealColor',
android: '@android:color/holo_blue_bright',
default: 'black',
}))
const color =
Platform.OS !== 'web'
? PlatformColor(
Platform.select({
ios: 'systemTealColor',
android: '@android:color/holo_blue_bright',
default: 'black',
}),
)
: 'black';
// const customContrastDynamicTextColor = DynamicColorIOS({
// dark: 'hsla(360, 40%, 30%, 1.0)',
@@ -27,13 +26,7 @@ export default () => {
return (
<>
<Svg height="100" width="100" color={color}>
<Circle
cx="50"
cy="50"
r={test}
strokeWidth="2.5"
fill={color}
/>
<Circle cx="50" cy="50" r={test} strokeWidth="2.5" fill={color} />
<Rect
x="15"
y="15"
@@ -45,12 +38,13 @@ export default () => {
</Svg>
<Svg height="300" width="300" fill="red">
<Text x={0} y={0} fontSize={20}>
<TSpan dx={test} inlineSize={"100%"} fill="currentColor">
Testing word-wrap... Testing word-wrap... Testing word-wrap... Testing word-wrap...
<TSpan dx={test} inlineSize={'100%'} fill="currentColor">
Testing word-wrap... Testing word-wrap... Testing word-wrap...
Testing word-wrap...
</TSpan>
</Text>
</Svg>
<Button title="Click me" onPress={()=> setTest(test + 1)}/>
<Button title="Click me" onPress={() => setTest(test + 1)} />
</>
);
}
};

View File

@@ -1,61 +1,65 @@
import React from 'react';
import {
SafeAreaView,
useColorScheme,
} from 'react-native';
import {Svg, Ellipse} from 'react-native-svg';
import Animated, {createAnimatedPropAdapter, processColor, useAnimatedProps, useSharedValue, withRepeat, withTiming} from 'react-native-reanimated';
import {Colors} from 'react-native/Libraries/NewAppScreen';
const AnimatedEllipse = Animated.createAnimatedComponent(Ellipse);
const App = () => {
const isDarkMode = useColorScheme() === 'dark';
const offset = useSharedValue(0);
offset.value = withRepeat(withTiming(1.0), -1, true);
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
flex: 1,
};
const ellipseAnimatedProps =
useAnimatedProps(() =>
{
const coordinates = {cx: 50, cy: 50, rx: 40, ry: 40};
return {
cx: coordinates.cx,
cy: coordinates.cy,
rx: coordinates.rx,
ry: coordinates.ry,
stroke: 'rgb(255,0,0)',
fill: 'yellow',
opacity: offset.value,
strokeWidth: 2,
};
}
, [], createAnimatedPropAdapter(
(props) => {
if (Object.keys(props).includes('fill')) {
props.fill = {type: 0, payload: processColor(props.fill)}
}
if (Object.keys(props).includes('stroke')) {
props.stroke = {type: 0, payload: processColor(props.stroke)}
}
import React from 'react';
import {SafeAreaView, useColorScheme} from 'react-native';
import {Svg, Ellipse} from 'react-native-svg';
import Animated, {
createAnimatedPropAdapter,
processColor,
useAnimatedProps,
useSharedValue,
withRepeat,
withTiming,
} from 'react-native-reanimated';
const AnimatedEllipse = Animated.createAnimatedComponent(Ellipse);
const App = () => {
const isDarkMode = useColorScheme() === 'dark';
const offset = useSharedValue(0);
offset.value = withRepeat(withTiming(1.0), -1, true);
const backgroundStyle = {
backgroundColor: isDarkMode ? '#333333' : '#fafafa',
flex: 1,
};
const ellipseAnimatedProps = useAnimatedProps(
() => {
const coordinates = {cx: 50, cy: 50, rx: 40, ry: 40};
return {
cx: coordinates.cx,
cy: coordinates.cy,
rx: coordinates.rx,
ry: coordinates.ry,
stroke: 'rgb(255,0,0)',
fill: 'yellow',
opacity: offset.value,
strokeWidth: 2,
};
},
['fill', 'stroke']));
return (
<SafeAreaView style={backgroundStyle}>
<Svg width="100%" height="100%">
<AnimatedEllipse
// {...coordinates}
animatedProps={ellipseAnimatedProps}
[],
createAnimatedPropAdapter(
props => {
if (Object.keys(props).includes('fill')) {
props.fill = {type: 0, payload: processColor(props.fill)};
}
if (Object.keys(props).includes('stroke')) {
props.stroke = {type: 0, payload: processColor(props.stroke)};
}
},
['fill', 'stroke'],
),
);
return (
<SafeAreaView style={backgroundStyle}>
<Svg width="100%" height="100%">
<AnimatedEllipse
// {...coordinates}
animatedProps={ellipseAnimatedProps}
/>
</Svg>
</SafeAreaView>
);
};
export default App;
</Svg>
</SafeAreaView>
);
};
export default App;

View File

@@ -1,51 +1,23 @@
import Shape from './elements/Shape';
import Rect from './elements/Rect';
import Circle from './elements/Circle';
import Ellipse from './elements/Ellipse';
import Polygon from './elements/Polygon';
import Polyline from './elements/Polyline';
import Line from './elements/Line';
import Svg from './elements/Svg';
import Path from './elements/Path';
import G from './elements/G';
import Text from './elements/Text';
import TSpan from './elements/TSpan';
import TextPath from './elements/TextPath';
import Use from './elements/Use';
import Image from './elements/Image';
import Symbol from './elements/Symbol';
import Defs from './elements/Defs';
import LinearGradient from './elements/LinearGradient';
import RadialGradient from './elements/RadialGradient';
import Stop from './elements/Stop';
import ClipPath from './elements/ClipPath';
import Pattern from './elements/Pattern';
import Mask from './elements/Mask';
import Marker from './elements/Marker';
import ForeignObject from './elements/ForeignObject';
import Filter from './elements/filters/Filter';
import FeColorMatrix from './elements/filters/FeColorMatrix';
import FeGaussianBlur from './elements/filters/FeGaussianBlur';
import FeOffset from './elements/filters/FeOffset';
import {
AstProps,
camelCase,
fetchText,
JsxAST,
Middleware,
parse,
Styles,
SvgAst,
SvgFromUri,
SvgFromXml,
SvgUri,
SvgXml,
camelCase,
fetchText,
JsxAST,
Middleware,
Styles,
UriProps,
UriState,
XmlAST,
XmlProps,
XmlState,
AstProps,
} from './xml';
import {
@@ -53,6 +25,10 @@ import {
RNSVGClipPath,
RNSVGDefs,
RNSVGEllipse,
RNSVGFeColorMatrix,
RNSVGFeGaussianBlur,
RNSVGFeOffset,
RNSVGFilter,
RNSVGForeignObject,
RNSVGGroup,
RNSVGImage,
@@ -71,121 +47,90 @@ import {
RNSVGTextPath,
RNSVGTSpan,
RNSVGUse,
RNSVGFilter,
RNSVGFeColorMatrix,
RNSVGFeGaussianBlur,
RNSVGFeOffset,
} from './fabric';
export {
inlineStyles,
loadLocalRawResource,
LocalSvg,
SvgCss,
SvgCssUri,
SvgWithCss,
SvgWithCssUri,
inlineStyles,
LocalSvg,
WithLocalSvg,
loadLocalRawResource,
} from './deprecated';
export type { RectProps } from './elements/Rect';
export type { CircleProps } from './elements/Circle';
export type { EllipseProps } from './elements/Ellipse';
export type { PolygonProps } from './elements/Polygon';
export type { PolylineProps } from './elements/Polyline';
export type { LineProps } from './elements/Line';
export type { SvgProps } from './elements/Svg';
export type { PathProps } from './elements/Path';
export type { GProps } from './elements/G';
export type { TextProps } from './elements/Text';
export type { TSpanProps } from './elements/TSpan';
export type { TextPathProps } from './elements/TextPath';
export type { UseProps } from './elements/Use';
export type { ImageProps } from './elements/Image';
export type { SymbolProps } from './elements/Symbol';
export type { LinearGradientProps } from './elements/LinearGradient';
export type { RadialGradientProps } from './elements/RadialGradient';
export type { StopProps } from './elements/Stop';
export type { ClipPathProps } from './elements/ClipPath';
export type { PatternProps } from './elements/Pattern';
export type { MaskProps } from './elements/Mask';
export type { MarkerProps } from './elements/Marker';
export type { ForeignObjectProps } from './elements/ForeignObject';
export type { FilterProps } from './elements/filters/Filter';
export type { EllipseProps } from './elements/Ellipse';
export type { FeColorMatrixProps } from './elements/filters/FeColorMatrix';
export type { FeGaussianBlurProps } from './elements/filters/FeGaussianBlur';
export type { FeOffsetProps } from './elements/filters/FeOffset';
export type { FilterProps } from './elements/filters/Filter';
export type { FilterPrimitiveCommonProps } from './elements/filters/FilterPrimitive';
export type { ForeignObjectProps } from './elements/ForeignObject';
export type { GProps } from './elements/G';
export type { ImageProps } from './elements/Image';
export type { LineProps } from './elements/Line';
export type { LinearGradientProps } from './elements/LinearGradient';
export type { MarkerProps } from './elements/Marker';
export type { MaskProps } from './elements/Mask';
export type { PathProps } from './elements/Path';
export type { PatternProps } from './elements/Pattern';
export type { PolygonProps } from './elements/Polygon';
export type { PolylineProps } from './elements/Polyline';
export type { RadialGradientProps } from './elements/RadialGradient';
export type { RectProps } from './elements/Rect';
export type { StopProps } from './elements/Stop';
export type { SvgProps } from './elements/Svg';
export type { SymbolProps } from './elements/Symbol';
export type { TextProps } from './elements/Text';
export type { TextPathProps } from './elements/TextPath';
export type { TSpanProps } from './elements/TSpan';
export type { UseProps } from './elements/Use';
export * from './lib/extract/types';
export {
Svg,
Circle,
Ellipse,
G,
Text,
TSpan,
TextPath,
Path,
Polygon,
Polyline,
Line,
Rect,
Use,
Image,
Symbol,
Defs,
LinearGradient,
RadialGradient,
Stop,
ClipPath,
Pattern,
Mask,
Marker,
ForeignObject,
camelCase,
fetchText,
parse,
RNSVGCircle,
RNSVGClipPath,
RNSVGDefs,
RNSVGEllipse,
RNSVGFeColorMatrix,
RNSVGFeGaussianBlur,
RNSVGFeOffset,
RNSVGFilter,
RNSVGForeignObject,
RNSVGGroup,
RNSVGImage,
RNSVGLine,
RNSVGLinearGradient,
RNSVGMarker,
RNSVGMask,
RNSVGPath,
RNSVGPattern,
RNSVGRadialGradient,
RNSVGRect,
RNSVGSvgAndroid,
RNSVGSvgIOS,
RNSVGSymbol,
RNSVGText,
RNSVGTextPath,
RNSVGTSpan,
RNSVGUse,
Shape,
SvgAst,
SvgFromUri,
SvgFromXml,
SvgUri,
SvgXml,
camelCase,
fetchText,
Shape,
Filter,
FeColorMatrix,
FeGaussianBlur,
FeOffset,
RNSVGMarker,
RNSVGMask,
RNSVGPattern,
RNSVGClipPath,
RNSVGRadialGradient,
RNSVGLinearGradient,
RNSVGDefs,
RNSVGSymbol,
RNSVGImage,
RNSVGUse,
RNSVGTextPath,
RNSVGTSpan,
RNSVGText,
RNSVGGroup,
RNSVGPath,
RNSVGLine,
RNSVGEllipse,
RNSVGCircle,
RNSVGRect,
RNSVGSvgAndroid,
RNSVGSvgIOS,
RNSVGForeignObject,
RNSVGFilter,
RNSVGFeColorMatrix,
RNSVGFeGaussianBlur,
RNSVGFeOffset,
};
export type {
AstProps,
JsxAST,
Middleware,
Styles,
@@ -194,7 +139,7 @@ export type {
XmlAST,
XmlProps,
XmlState,
AstProps,
};
export default Svg;
export * from './elements';
export { default } from './elements';

View File

@@ -1,601 +1,57 @@
import * as React from 'react';
import type { CircleProps } from './elements/Circle';
import type { ClipPathProps } from './elements/ClipPath';
import type { EllipseProps } from './elements/Ellipse';
import type { ForeignObjectProps } from './elements/ForeignObject';
import type { GProps } from './elements/G';
import type { ImageProps } from './elements/Image';
import type { LineProps } from './elements/Line';
import type { LinearGradientProps } from './elements/LinearGradient';
import type { MarkerProps } from './elements/Marker';
import type { MaskProps } from './elements/Mask';
import type { PathProps } from './elements/Path';
import type { PatternProps } from './elements/Pattern';
import type { PolygonProps } from './elements/Polygon';
import type { PolylineProps } from './elements/Polyline';
import type { RadialGradientProps } from './elements/RadialGradient';
import type { RectProps } from './elements/Rect';
import type { StopProps } from './elements/Stop';
import type { SvgProps } from './elements/Svg';
import type { SymbolProps } from './elements/Symbol';
import type { TextProps } from './elements/Text';
import type { TextPathProps } from './elements/TextPath';
import type { TSpanProps } from './elements/TSpan';
import type { UseProps } from './elements/Use';
import type { FilterProps } from './elements/filters/Filter';
import type { FeColorMatrixProps } from './elements/filters/FeColorMatrix';
import type { FeGaussianBlurProps } from './elements/filters/FeGaussianBlur';
import type { FeOffsetProps } from './elements/filters/FeOffset';
import type {
GestureResponderEvent,
ImageProps as RNImageProps,
} from 'react-native';
import {
// @ts-ignore it is not seen in exports
unstable_createElement as createElement,
} from 'react-native';
import type {
NumberArray,
NumberProp,
TransformProps,
} from './lib/extract/types';
import SvgTouchableMixin from './lib/SvgTouchableMixin';
import { resolve } from './lib/resolve';
import {
transformsArrayToProps,
type TransformsStyleArray,
} from './lib/extract/extractTransform';
import { resolveAssetUri } from './lib/resolveAssetUri';
AstProps,
camelCase,
fetchText,
JsxAST,
Middleware,
parse,
Styles,
SvgAst,
SvgFromUri,
SvgFromXml,
SvgUri,
SvgXml,
UriProps,
UriState,
XmlAST,
XmlProps,
XmlState,
} from './xml';
type BlurEvent = object;
type FocusEvent = object;
type PressEvent = object;
type LayoutEvent = object;
type EdgeInsetsProp = object;
export {
inlineStyles,
loadLocalRawResource,
LocalSvg,
SvgCss,
SvgCssUri,
SvgWithCss,
SvgWithCssUri,
WithLocalSvg,
} from './deprecated';
interface BaseProps {
accessible?: boolean;
accessibilityLabel?: string;
accessibilityHint?: string;
accessibilityIgnoresInvertColors?: boolean;
accessibilityRole?: string;
accessibilityState?: object;
delayLongPress?: number;
delayPressIn?: number;
delayPressOut?: number;
disabled?: boolean;
hitSlop?: EdgeInsetsProp;
href?: RNImageProps['source'] | string | number;
nativeID?: string;
touchSoundDisabled?: boolean;
onBlur?: (e: BlurEvent) => void;
onFocus?: (e: FocusEvent) => void;
onLayout?: (event: LayoutEvent) => object;
onLongPress?: (event: PressEvent) => object;
onClick?: (event: PressEvent) => object;
onPress?: (event: PressEvent) => object;
onPressIn?: (event: PressEvent) => object;
onPressOut?: (event: PressEvent) => object;
pressRetentionOffset?: EdgeInsetsProp;
rejectResponderTermination?: boolean;
transform?: TransformProps['transform'];
translate?: NumberArray;
translateX?: NumberProp;
translateY?: NumberProp;
scale?: NumberArray;
scaleX?: NumberProp;
scaleY?: NumberProp;
rotation?: NumberProp;
skewX?: NumberProp;
skewY?: NumberProp;
origin?: NumberArray;
originX?: NumberProp;
originY?: NumberProp;
fontStyle?: string;
fontWeight?: NumberProp;
fontSize?: NumberProp;
fontFamily?: string;
forwardedRef?:
| React.RefCallback<SVGElement>
| React.MutableRefObject<SVGElement | null>;
style?: Iterable<unknown>;
// different tranform props
gradientTransform?: TransformProps['transform'];
patternTransform?: TransformProps['transform'];
}
const hasTouchableProperty = (props: BaseProps) =>
props.onPress || props.onPressIn || props.onPressOut || props.onLongPress;
const camelCaseToDashed = (camelCase: string) => {
return camelCase.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
export {
camelCase,
fetchText,
parse,
SvgAst,
SvgFromUri,
SvgFromXml,
SvgUri,
SvgXml,
};
function stringifyTransformProps(transformProps: TransformProps) {
const transformArray = [];
if (transformProps.translate != null) {
transformArray.push(`translate(${transformProps.translate})`);
}
if (transformProps.translateX != null || transformProps.translateY != null) {
transformArray.push(
`translate(${transformProps.translateX || 0}, ${
transformProps.translateY || 0
})`
);
}
if (transformProps.scale != null) {
transformArray.push(`scale(${transformProps.scale})`);
}
if (transformProps.scaleX != null || transformProps.scaleY != null) {
transformArray.push(
`scale(${transformProps.scaleX || 1}, ${transformProps.scaleY || 1})`
);
}
// rotation maps to rotate, not to collide with the text rotate attribute (which acts per glyph rather than block)
if (transformProps.rotation != null) {
transformArray.push(`rotate(${transformProps.rotation})`);
}
if (transformProps.skewX != null) {
transformArray.push(`skewX(${transformProps.skewX})`);
}
if (transformProps.skewY != null) {
transformArray.push(`skewY(${transformProps.skewY})`);
}
return transformArray;
}
export * from './lib/extract/types';
function parseTransformProp(
transform: TransformProps['transform'],
props?: BaseProps
) {
const transformArray: string[] = [];
props && transformArray.push(...stringifyTransformProps(props));
if (Array.isArray(transform)) {
if (typeof transform[0] === 'number') {
transformArray.push(`matrix(${transform.join(' ')})`);
} else {
const stringifiedProps = transformsArrayToProps(
transform as TransformsStyleArray
);
transformArray.push(...stringifyTransformProps(stringifiedProps));
}
} else if (typeof transform === 'string') {
transformArray.push(transform);
}
return transformArray.length ? transformArray.join(' ') : undefined;
}
/**
* `react-native-svg` supports additional props that aren't defined in the spec.
* This function replaces them in a spec conforming manner.
*
* @param {WebShape} self Instance given to us.
* @param {Object?} props Optional overridden props given to us.
* @returns {Object} Cleaned props object.
* @private
*/
const prepare = <T extends BaseProps>(
self: WebShape<T>,
props = self.props
) => {
const {
transform,
origin,
originX,
originY,
fontFamily,
fontSize,
fontWeight,
fontStyle,
style,
forwardedRef,
gradientTransform,
patternTransform,
...rest
} = props;
const clean: {
onStartShouldSetResponder?: (e: GestureResponderEvent) => boolean;
onResponderMove?: (e: GestureResponderEvent) => void;
onResponderGrant?: (e: GestureResponderEvent) => void;
onResponderRelease?: (e: GestureResponderEvent) => void;
onResponderTerminate?: (e: GestureResponderEvent) => void;
onResponderTerminationRequest?: (e: GestureResponderEvent) => boolean;
onClick?: (e: GestureResponderEvent) => void;
transform?: string;
gradientTransform?: string;
patternTransform?: string;
'transform-origin'?: string;
href?: RNImageProps['source'] | string | null;
style?: object;
ref?: unknown;
} = {
...(hasTouchableProperty(props)
? {
onStartShouldSetResponder:
self.touchableHandleStartShouldSetResponder,
onResponderTerminationRequest:
self.touchableHandleResponderTerminationRequest,
onResponderGrant: self.touchableHandleResponderGrant,
onResponderMove: self.touchableHandleResponderMove,
onResponderRelease: self.touchableHandleResponderRelease,
onResponderTerminate: self.touchableHandleResponderTerminate,
}
: null),
...rest,
};
if (origin != null) {
clean['transform-origin'] = origin.toString().replace(',', ' ');
} else if (originX != null || originY != null) {
clean['transform-origin'] = `${originX || 0} ${originY || 0}`;
}
// we do it like this because setting transform as undefined causes error in web
const parsedTransform = parseTransformProp(transform, props);
if (parsedTransform) {
clean.transform = parsedTransform;
}
const parsedGradientTransform = parseTransformProp(gradientTransform);
if (parsedGradientTransform) {
clean.gradientTransform = parsedGradientTransform;
}
const parsedPatternTransform = parseTransformProp(patternTransform);
if (parsedPatternTransform) {
clean.patternTransform = parsedPatternTransform;
}
clean.ref = (el: SVGElement | null) => {
self.elementRef.current = el;
if (typeof forwardedRef === 'function') {
forwardedRef(el);
} else if (forwardedRef) {
forwardedRef.current = el;
}
};
const styles: {
fontStyle?: string;
fontFamily?: string;
fontSize?: NumberProp;
fontWeight?: NumberProp;
} = {};
if (fontFamily != null) {
styles.fontFamily = fontFamily;
}
if (fontSize != null) {
styles.fontSize = fontSize;
}
if (fontWeight != null) {
styles.fontWeight = fontWeight;
}
if (fontStyle != null) {
styles.fontStyle = fontStyle;
}
clean.style = resolve(style, styles);
if (props.onPress != null) {
clean.onClick = props.onPress;
}
if (props.href !== null) {
clean.href = resolveAssetUri(props.href)?.uri;
}
return clean;
export * from './elements';
export { default } from './elements';
export type {
AstProps,
JsxAST,
Middleware,
Styles,
UriProps,
UriState,
XmlAST,
XmlProps,
XmlState,
};
const getBoundingClientRect = (node: SVGElement) => {
if (node) {
const isElement = node.nodeType === 1; /* Node.ELEMENT_NODE */
if (isElement && typeof node.getBoundingClientRect === 'function') {
return node.getBoundingClientRect();
}
}
throw new Error('Can not get boundingClientRect of ' + node || 'undefined');
};
const measureLayout = (
node: SVGElement,
callback: (
x: number,
y: number,
width: number,
height: number,
left: number,
top: number
) => void
) => {
const relativeNode = node?.parentNode;
if (relativeNode) {
setTimeout(() => {
// @ts-expect-error TODO: handle it better
const relativeRect = getBoundingClientRect(relativeNode);
const { height, left, top, width } = getBoundingClientRect(node);
const x = left - relativeRect.left;
const y = top - relativeRect.top;
callback(x, y, width, height, left, top);
}, 0);
}
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function remeasure(this: any) {
const tag = this.state.touchable.responderID;
if (tag === null) {
return;
}
measureLayout(tag, this._handleQueryLayout);
}
export class WebShape<
P extends BaseProps = BaseProps,
> extends React.Component<P> {
[x: string]: unknown;
protected tag?: React.ElementType;
protected prepareProps(props: P) {
return props;
}
elementRef =
React.createRef<SVGElement>() as React.MutableRefObject<SVGElement | null>;
lastMergedProps: Partial<P> = {};
/**
* disclaimer: I am not sure why the props are wrapped in a `style` attribute here, but that's how reanimated calls it
*/
setNativeProps(props: { style: P }) {
const merged = Object.assign(
{},
this.props,
this.lastMergedProps,
props.style
);
this.lastMergedProps = merged;
const clean = prepare(this, this.prepareProps(merged));
const current = this.elementRef.current;
if (current) {
for (const cleanAttribute of Object.keys(clean)) {
const cleanValue = clean[cleanAttribute as keyof typeof clean];
switch (cleanAttribute) {
case 'ref':
case 'children':
break;
case 'style':
// style can be an object here or an array, so we convert it to an array and assign each element
for (const partialStyle of ([] as unknown[]).concat(
clean.style ?? []
)) {
Object.assign(current.style, partialStyle);
}
break;
default:
// apply all other incoming prop updates as attributes on the node
// same logic as in https://github.com/software-mansion/react-native-reanimated/blob/d04720c82f5941532991b235787285d36d717247/src/reanimated2/js-reanimated/index.ts#L38-L39
// @ts-expect-error TODO: fix this
current.setAttribute(camelCaseToDashed(cleanAttribute), cleanValue);
break;
}
}
}
}
_remeasureMetricsOnActivation: () => void;
touchableHandleStartShouldSetResponder?: (
e: GestureResponderEvent
) => boolean;
touchableHandleResponderMove?: (e: GestureResponderEvent) => void;
touchableHandleResponderGrant?: (e: GestureResponderEvent) => void;
touchableHandleResponderRelease?: (e: GestureResponderEvent) => void;
touchableHandleResponderTerminate?: (e: GestureResponderEvent) => void;
touchableHandleResponderTerminationRequest?: (
e: GestureResponderEvent
) => boolean;
constructor(props: P) {
super(props);
// Do not attach touchable mixin handlers if SVG element doesn't have a touchable prop
if (hasTouchableProperty(props)) {
SvgTouchableMixin(this);
}
this._remeasureMetricsOnActivation = remeasure.bind(this);
}
render(): JSX.Element {
if (!this.tag) {
throw new Error(
'When extending `WebShape` you need to overwrite either `tag` or `render`!'
);
}
this.lastMergedProps = {};
return createElement(
this.tag,
prepare(this, this.prepareProps(this.props))
);
}
}
export class Circle extends WebShape<BaseProps & CircleProps> {
tag = 'circle' as const;
}
export class ClipPath extends WebShape<BaseProps & ClipPathProps> {
tag = 'clipPath' as const;
}
export class Defs extends WebShape {
tag = 'defs' as const;
}
export class Ellipse extends WebShape<BaseProps & EllipseProps> {
tag = 'ellipse' as const;
}
export class G extends WebShape<BaseProps & GProps> {
tag = 'g' as const;
prepareProps(props: BaseProps & GProps) {
const { x, y, ...rest } = props;
if ((x || y) && !rest.translate) {
rest.translate = `${x || 0}, ${y || 0}`;
}
return rest;
}
}
export class Image extends WebShape<BaseProps & ImageProps> {
tag = 'image' as const;
}
export class Line extends WebShape<BaseProps & LineProps> {
tag = 'line' as const;
}
export class LinearGradient extends WebShape<BaseProps & LinearGradientProps> {
tag = 'linearGradient' as const;
}
export class Path extends WebShape<BaseProps & PathProps> {
tag = 'path' as const;
}
export class Polygon extends WebShape<BaseProps & PolygonProps> {
tag = 'polygon' as const;
}
export class Polyline extends WebShape<BaseProps & PolylineProps> {
tag = 'polyline' as const;
}
export class RadialGradient extends WebShape<BaseProps & RadialGradientProps> {
tag = 'radialGradient' as const;
}
export class Rect extends WebShape<BaseProps & RectProps> {
tag = 'rect' as const;
}
export class Stop extends WebShape<BaseProps & StopProps> {
tag = 'stop' as const;
}
/* Taken from here: https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0 */
function encodeSvg(svgString: string) {
return svgString
.replace(
'<svg',
~svgString.indexOf('xmlns')
? '<svg'
: '<svg xmlns="http://www.w3.org/2000/svg"'
)
.replace(/"/g, "'")
.replace(/%/g, '%25')
.replace(/#/g, '%23')
.replace(/{/g, '%7B')
.replace(/}/g, '%7D')
.replace(/</g, '%3C')
.replace(/>/g, '%3E')
.replace(/\s+/g, ' ');
}
export class Svg extends WebShape<BaseProps & SvgProps> {
tag = 'svg' as const;
toDataURL(
callback: (data: string) => void,
options: { width?: number; height?: number } = {}
) {
const ref = this.elementRef.current;
if (ref === null) {
return;
}
const rect = getBoundingClientRect(ref);
const width = Number(options.width) || rect.width;
const height = Number(options.height) || rect.height;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', `0 0 ${rect.width} ${rect.height}`);
svg.setAttribute('width', String(width));
svg.setAttribute('height', String(height));
svg.appendChild(ref.cloneNode(true));
const img = new window.Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
context?.drawImage(img, 0, 0);
callback(canvas.toDataURL().replace('data:image/png;base64,', ''));
};
img.src = `data:image/svg+xml;utf8,${encodeSvg(
new window.XMLSerializer().serializeToString(svg)
)}`;
}
}
export class Symbol extends WebShape<BaseProps & SymbolProps> {
tag = 'symbol' as const;
}
export class Text extends WebShape<BaseProps & TextProps> {
tag = 'text' as const;
}
export class TSpan extends WebShape<BaseProps & TSpanProps> {
tag = 'tspan' as const;
}
export class TextPath extends WebShape<BaseProps & TextPathProps> {
tag = 'textPath' as const;
}
export class Use extends WebShape<BaseProps & UseProps> {
tag = 'use' as const;
}
export class Mask extends WebShape<BaseProps & MaskProps> {
tag = 'mask' as const;
}
export class ForeignObject extends WebShape<BaseProps & ForeignObjectProps> {
tag = 'foreignObject' as const;
}
export class Marker extends WebShape<BaseProps & MarkerProps> {
tag = 'marker' as const;
}
export class Pattern extends WebShape<BaseProps & PatternProps> {
tag = 'pattern' as const;
}
export class Filter extends WebShape<BaseProps & FilterProps> {
tag = 'filter' as const;
}
export class FeColorMatrix extends WebShape<BaseProps & FeColorMatrixProps> {
tag = 'feColorMatrix' as const;
}
export class FeGaussianBlur extends WebShape<BaseProps & FeGaussianBlurProps> {
tag = 'feGaussianBlur' as const;
}
export class FeOffset extends WebShape<BaseProps & FeOffsetProps> {
tag = 'feOffset' as const;
}
export default Svg;

61
src/elements.ts Normal file
View File

@@ -0,0 +1,61 @@
import Circle from './elements/Circle';
import ClipPath from './elements/ClipPath';
import Defs from './elements/Defs';
import Ellipse from './elements/Ellipse';
import ForeignObject from './elements/ForeignObject';
import G from './elements/G';
import Image from './elements/Image';
import Line from './elements/Line';
import LinearGradient from './elements/LinearGradient';
import Marker from './elements/Marker';
import Mask from './elements/Mask';
import Path from './elements/Path';
import Pattern from './elements/Pattern';
import Polygon from './elements/Polygon';
import Polyline from './elements/Polyline';
import RadialGradient from './elements/RadialGradient';
import Rect from './elements/Rect';
import Stop from './elements/Stop';
import Svg from './elements/Svg';
import Symbol from './elements/Symbol';
import TSpan from './elements/TSpan';
import Text from './elements/Text';
import TextPath from './elements/TextPath';
import Use from './elements/Use';
import FeColorMatrix from './elements/filters/FeColorMatrix';
import FeGaussianBlur from './elements/filters/FeGaussianBlur';
import FeOffset from './elements/filters/FeOffset';
import Filter from './elements/filters/Filter';
export {
Circle,
ClipPath,
Defs,
Ellipse,
FeColorMatrix,
FeGaussianBlur,
FeOffset,
Filter,
ForeignObject,
G,
Image,
Line,
LinearGradient,
Marker,
Mask,
Path,
Pattern,
Polygon,
Polyline,
RadialGradient,
Rect,
Stop,
Svg,
Symbol,
TSpan,
Text,
TextPath,
Use,
};
export default Svg;

188
src/elements.web.ts Normal file
View File

@@ -0,0 +1,188 @@
import type { CircleProps } from './elements/Circle';
import type { ClipPathProps } from './elements/ClipPath';
import type { EllipseProps } from './elements/Ellipse';
import type { FeColorMatrixProps } from './elements/filters/FeColorMatrix';
import type { FeGaussianBlurProps } from './elements/filters/FeGaussianBlur';
import type { FeOffsetProps } from './elements/filters/FeOffset';
import type { FilterProps } from './elements/filters/Filter';
import type { ForeignObjectProps } from './elements/ForeignObject';
import type { GProps } from './elements/G';
import type { ImageProps } from './elements/Image';
import type { LineProps } from './elements/Line';
import type { LinearGradientProps } from './elements/LinearGradient';
import type { MarkerProps } from './elements/Marker';
import type { MaskProps } from './elements/Mask';
import type { PathProps } from './elements/Path';
import type { PatternProps } from './elements/Pattern';
import type { PolygonProps } from './elements/Polygon';
import type { PolylineProps } from './elements/Polyline';
import type { RadialGradientProps } from './elements/RadialGradient';
import type { RectProps } from './elements/Rect';
import type { StopProps } from './elements/Stop';
import type { SvgProps } from './elements/Svg';
import type { SymbolProps } from './elements/Symbol';
import type { TextProps } from './elements/Text';
import type { TextPathProps } from './elements/TextPath';
import type { TSpanProps } from './elements/TSpan';
import type { UseProps } from './elements/Use';
import type { BaseProps } from './web/types';
import { encodeSvg, getBoundingClientRect } from './web/utils';
import { WebShape } from './web/WebShape';
export class Circle extends WebShape<BaseProps & CircleProps> {
tag = 'circle' as const;
}
export class ClipPath extends WebShape<BaseProps & ClipPathProps> {
tag = 'clipPath' as const;
}
export class Defs extends WebShape {
tag = 'defs' as const;
}
export class Ellipse extends WebShape<BaseProps & EllipseProps> {
tag = 'ellipse' as const;
}
export class FeColorMatrix extends WebShape<BaseProps & FeColorMatrixProps> {
tag = 'feColorMatrix' as const;
}
export class FeGaussianBlur extends WebShape<BaseProps & FeGaussianBlurProps> {
tag = 'feGaussianBlur' as const;
}
export class FeOffset extends WebShape<BaseProps & FeOffsetProps> {
tag = 'feOffset' as const;
}
export class Filter extends WebShape<BaseProps & FilterProps> {
tag = 'filter' as const;
}
export class ForeignObject extends WebShape<BaseProps & ForeignObjectProps> {
tag = 'foreignObject' as const;
}
export class G extends WebShape<BaseProps & GProps> {
tag = 'g' as const;
prepareProps(props: BaseProps & GProps) {
const { x, y, ...rest } = props;
if ((x || y) && !rest.translate) {
rest.translate = `${x || 0}, ${y || 0}`;
}
return rest;
}
}
export class Image extends WebShape<BaseProps & ImageProps> {
tag = 'image' as const;
}
export class Line extends WebShape<BaseProps & LineProps> {
tag = 'line' as const;
}
export class LinearGradient extends WebShape<BaseProps & LinearGradientProps> {
tag = 'linearGradient' as const;
}
export class Marker extends WebShape<BaseProps & MarkerProps> {
tag = 'marker' as const;
}
export class Mask extends WebShape<BaseProps & MaskProps> {
tag = 'mask' as const;
}
export class Path extends WebShape<BaseProps & PathProps> {
tag = 'path' as const;
}
export class Pattern extends WebShape<BaseProps & PatternProps> {
tag = 'pattern' as const;
}
export class Polygon extends WebShape<BaseProps & PolygonProps> {
tag = 'polygon' as const;
}
export class Polyline extends WebShape<BaseProps & PolylineProps> {
tag = 'polyline' as const;
}
export class RadialGradient extends WebShape<BaseProps & RadialGradientProps> {
tag = 'radialGradient' as const;
}
export class Rect extends WebShape<BaseProps & RectProps> {
tag = 'rect' as const;
}
export class Stop extends WebShape<BaseProps & StopProps> {
tag = 'stop' as const;
}
export class Svg extends WebShape<BaseProps & SvgProps> {
tag = 'svg' as const;
toDataURL(
callback: (data: string) => void,
options: { width?: number; height?: number } = {}
) {
const ref = this.elementRef.current;
if (ref === null) {
return;
}
const rect = getBoundingClientRect(ref);
const width = Number(options.width) || rect.width;
const height = Number(options.height) || rect.height;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', `0 0 ${rect.width} ${rect.height}`);
svg.setAttribute('width', String(width));
svg.setAttribute('height', String(height));
svg.appendChild(ref.cloneNode(true));
const img = new window.Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
context?.drawImage(img, 0, 0);
callback(canvas.toDataURL().replace('data:image/png;base64,', ''));
};
img.src = `data:image/svg+xml;utf8,${encodeSvg(
new window.XMLSerializer().serializeToString(svg)
)}`;
}
}
export class Symbol extends WebShape<BaseProps & SymbolProps> {
tag = 'symbol' as const;
}
export class TSpan extends WebShape<BaseProps & TSpanProps> {
tag = 'tspan' as const;
}
export class Text extends WebShape<BaseProps & TextProps> {
tag = 'text' as const;
}
export class TextPath extends WebShape<BaseProps & TextPathProps> {
tag = 'textPath' as const;
}
export class Use extends WebShape<BaseProps & UseProps> {
tag = 'use' as const;
}
export default Svg;

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { tags } from '../../tags';
import { tags } from '../../xmlTags';
import { FilterElement, Filters } from '../types';
import { parse } from './extractFiltersString';

102
src/web/WebShape.ts Normal file
View File

@@ -0,0 +1,102 @@
import React from 'react';
import {
GestureResponderEvent,
// @ts-ignore it is not seen in exports
unstable_createElement as createElement,
} from 'react-native';
import { BaseProps } from './types';
import { prepare } from './utils/prepare';
import { camelCaseToDashed, hasTouchableProperty, remeasure } from './utils';
import SvgTouchableMixin from '../lib/SvgTouchableMixin';
export class WebShape<
P extends BaseProps = BaseProps,
> extends React.Component<P> {
[x: string]: unknown;
protected tag?: React.ElementType;
protected prepareProps(props: P) {
return props;
}
elementRef =
React.createRef<SVGElement>() as React.MutableRefObject<SVGElement | null>;
lastMergedProps: Partial<P> = {};
/**
* disclaimer: I am not sure why the props are wrapped in a `style` attribute here, but that's how reanimated calls it
*/
setNativeProps(props: { style: P }) {
const merged = Object.assign(
{},
this.props,
this.lastMergedProps,
props.style
);
this.lastMergedProps = merged;
const clean = prepare(this, this.prepareProps(merged));
const current = this.elementRef.current;
if (current) {
for (const cleanAttribute of Object.keys(clean)) {
const cleanValue = clean[cleanAttribute as keyof typeof clean];
switch (cleanAttribute) {
case 'ref':
case 'children':
break;
case 'style':
// style can be an object here or an array, so we convert it to an array and assign each element
for (const partialStyle of ([] as unknown[]).concat(
clean.style ?? []
)) {
Object.assign(current.style, partialStyle);
}
break;
default:
// apply all other incoming prop updates as attributes on the node
// same logic as in https://github.com/software-mansion/react-native-reanimated/blob/d04720c82f5941532991b235787285d36d717247/src/reanimated2/js-reanimated/index.ts#L38-L39
// @ts-expect-error TODO: fix this
current.setAttribute(camelCaseToDashed(cleanAttribute), cleanValue);
break;
}
}
}
}
_remeasureMetricsOnActivation: () => void;
touchableHandleStartShouldSetResponder?: (
e: GestureResponderEvent
) => boolean;
touchableHandleResponderMove?: (e: GestureResponderEvent) => void;
touchableHandleResponderGrant?: (e: GestureResponderEvent) => void;
touchableHandleResponderRelease?: (e: GestureResponderEvent) => void;
touchableHandleResponderTerminate?: (e: GestureResponderEvent) => void;
touchableHandleResponderTerminationRequest?: (
e: GestureResponderEvent
) => boolean;
constructor(props: P) {
super(props);
// Do not attach touchable mixin handlers if SVG element doesn't have a touchable prop
if (hasTouchableProperty(props)) {
SvgTouchableMixin(this);
}
this._remeasureMetricsOnActivation = remeasure.bind(this);
}
render(): JSX.Element {
if (!this.tag) {
throw new Error(
'When extending `WebShape` you need to overwrite either `tag` or `render`!'
);
}
this.lastMergedProps = {};
return createElement(
this.tag,
prepare(this, this.prepareProps(this.props))
);
}
}

66
src/web/types.ts Normal file
View File

@@ -0,0 +1,66 @@
import type { ImageProps as RNImageProps } from 'react-native';
import type {
NumberArray,
NumberProp,
TransformProps,
} from '../lib/extract/types';
type BlurEvent = object;
type FocusEvent = object;
type PressEvent = object;
type LayoutEvent = object;
type EdgeInsetsProp = object;
export interface BaseProps {
accessible?: boolean;
accessibilityLabel?: string;
accessibilityHint?: string;
accessibilityIgnoresInvertColors?: boolean;
accessibilityRole?: string;
accessibilityState?: object;
delayLongPress?: number;
delayPressIn?: number;
delayPressOut?: number;
disabled?: boolean;
hitSlop?: EdgeInsetsProp;
href?: RNImageProps['source'] | string | number;
nativeID?: string;
touchSoundDisabled?: boolean;
onBlur?: (e: BlurEvent) => void;
onFocus?: (e: FocusEvent) => void;
onLayout?: (event: LayoutEvent) => object;
onLongPress?: (event: PressEvent) => object;
onClick?: (event: PressEvent) => object;
onPress?: (event: PressEvent) => object;
onPressIn?: (event: PressEvent) => object;
onPressOut?: (event: PressEvent) => object;
pressRetentionOffset?: EdgeInsetsProp;
rejectResponderTermination?: boolean;
transform?: TransformProps['transform'];
translate?: NumberArray;
translateX?: NumberProp;
translateY?: NumberProp;
scale?: NumberArray;
scaleX?: NumberProp;
scaleY?: NumberProp;
rotation?: NumberProp;
skewX?: NumberProp;
skewY?: NumberProp;
origin?: NumberArray;
originX?: NumberProp;
originY?: NumberProp;
fontStyle?: string;
fontWeight?: NumberProp;
fontSize?: NumberProp;
fontFamily?: string;
forwardedRef?:
| React.RefCallback<SVGElement>
| React.MutableRefObject<SVGElement | null>;
style?: Iterable<unknown>;
// different tranform props
gradientTransform?: TransformProps['transform'];
patternTransform?: TransformProps['transform'];
}

132
src/web/utils/index.ts Normal file
View File

@@ -0,0 +1,132 @@
import { BaseProps } from '../types';
import type { TransformProps } from '../../lib/extract/types';
import {
transformsArrayToProps,
TransformsStyleArray,
} from '../../lib/extract/extractTransform';
export const hasTouchableProperty = (props: BaseProps) =>
props.onPress || props.onPressIn || props.onPressOut || props.onLongPress;
export const camelCaseToDashed = (camelCase: string) => {
return camelCase.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
};
function stringifyTransformProps(transformProps: TransformProps) {
const transformArray = [];
if (transformProps.translate != null) {
transformArray.push(`translate(${transformProps.translate})`);
}
if (transformProps.translateX != null || transformProps.translateY != null) {
transformArray.push(
`translate(${transformProps.translateX || 0}, ${
transformProps.translateY || 0
})`
);
}
if (transformProps.scale != null) {
transformArray.push(`scale(${transformProps.scale})`);
}
if (transformProps.scaleX != null || transformProps.scaleY != null) {
transformArray.push(
`scale(${transformProps.scaleX || 1}, ${transformProps.scaleY || 1})`
);
}
// rotation maps to rotate, not to collide with the text rotate attribute (which acts per glyph rather than block)
if (transformProps.rotation != null) {
transformArray.push(`rotate(${transformProps.rotation})`);
}
if (transformProps.skewX != null) {
transformArray.push(`skewX(${transformProps.skewX})`);
}
if (transformProps.skewY != null) {
transformArray.push(`skewY(${transformProps.skewY})`);
}
return transformArray;
}
export function parseTransformProp(
transform: TransformProps['transform'],
props?: BaseProps
) {
const transformArray: string[] = [];
props && transformArray.push(...stringifyTransformProps(props));
if (Array.isArray(transform)) {
if (typeof transform[0] === 'number') {
transformArray.push(`matrix(${transform.join(' ')})`);
} else {
const stringifiedProps = transformsArrayToProps(
transform as TransformsStyleArray
);
transformArray.push(...stringifyTransformProps(stringifiedProps));
}
} else if (typeof transform === 'string') {
transformArray.push(transform);
}
return transformArray.length ? transformArray.join(' ') : undefined;
}
export const getBoundingClientRect = (node: SVGElement) => {
if (node) {
const isElement = node.nodeType === 1; /* Node.ELEMENT_NODE */
if (isElement && typeof node.getBoundingClientRect === 'function') {
return node.getBoundingClientRect();
}
}
throw new Error('Can not get boundingClientRect of ' + node || 'undefined');
};
const measureLayout = (
node: SVGElement,
callback: (
x: number,
y: number,
width: number,
height: number,
left: number,
top: number
) => void
) => {
const relativeNode = node?.parentNode;
if (relativeNode) {
setTimeout(() => {
// @ts-expect-error TODO: handle it better
const relativeRect = getBoundingClientRect(relativeNode);
const { height, left, top, width } = getBoundingClientRect(node);
const x = left - relativeRect.left;
const y = top - relativeRect.top;
callback(x, y, width, height, left, top);
}, 0);
}
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function remeasure(this: any) {
const tag = this.state.touchable.responderID;
if (tag === null) {
return;
}
measureLayout(tag, this._handleQueryLayout);
}
/* Taken from here: https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0 */
export function encodeSvg(svgString: string) {
return svgString
.replace(
'<svg',
~svgString.indexOf('xmlns')
? '<svg'
: '<svg xmlns="http://www.w3.org/2000/svg"'
)
.replace(/"/g, "'")
.replace(/%/g, '%25')
.replace(/#/g, '%23')
.replace(/{/g, '%7B')
.replace(/}/g, '%7D')
.replace(/</g, '%3C')
.replace(/>/g, '%3E')
.replace(/\s+/g, ' ');
}

127
src/web/utils/prepare.ts Normal file
View File

@@ -0,0 +1,127 @@
import {
GestureResponderEvent,
type ImageProps as RNImageProps,
} from 'react-native';
import { BaseProps } from '../types';
import { WebShape } from '../WebShape';
import { hasTouchableProperty, parseTransformProp } from '.';
import { resolve } from '../../lib/resolve';
import { NumberProp } from '../../lib/extract/types';
import { resolveAssetUri } from '../../lib/resolveAssetUri';
/**
* `react-native-svg` supports additional props that aren't defined in the spec.
* This function replaces them in a spec conforming manner.
*
* @param {WebShape} self Instance given to us.
* @param {Object?} props Optional overridden props given to us.
* @returns {Object} Cleaned props object.
* @private
*/
export const prepare = <T extends BaseProps>(
self: WebShape<T>,
props = self.props
) => {
const {
transform,
origin,
originX,
originY,
fontFamily,
fontSize,
fontWeight,
fontStyle,
style,
forwardedRef,
gradientTransform,
patternTransform,
...rest
} = props;
const clean: {
onStartShouldSetResponder?: (e: GestureResponderEvent) => boolean;
onResponderMove?: (e: GestureResponderEvent) => void;
onResponderGrant?: (e: GestureResponderEvent) => void;
onResponderRelease?: (e: GestureResponderEvent) => void;
onResponderTerminate?: (e: GestureResponderEvent) => void;
onResponderTerminationRequest?: (e: GestureResponderEvent) => boolean;
onClick?: (e: GestureResponderEvent) => void;
transform?: string;
gradientTransform?: string;
patternTransform?: string;
'transform-origin'?: string;
href?: RNImageProps['source'] | string | null;
style?: object;
ref?: unknown;
} = {
...(hasTouchableProperty(props)
? {
onStartShouldSetResponder:
self.touchableHandleStartShouldSetResponder,
onResponderTerminationRequest:
self.touchableHandleResponderTerminationRequest,
onResponderGrant: self.touchableHandleResponderGrant,
onResponderMove: self.touchableHandleResponderMove,
onResponderRelease: self.touchableHandleResponderRelease,
onResponderTerminate: self.touchableHandleResponderTerminate,
}
: null),
...rest,
};
if (origin != null) {
clean['transform-origin'] = origin.toString().replace(',', ' ');
} else if (originX != null || originY != null) {
clean['transform-origin'] = `${originX || 0} ${originY || 0}`;
}
// we do it like this because setting transform as undefined causes error in web
const parsedTransform = parseTransformProp(transform, props);
if (parsedTransform) {
clean.transform = parsedTransform;
}
const parsedGradientTransform = parseTransformProp(gradientTransform);
if (parsedGradientTransform) {
clean.gradientTransform = parsedGradientTransform;
}
const parsedPatternTransform = parseTransformProp(patternTransform);
if (parsedPatternTransform) {
clean.patternTransform = parsedPatternTransform;
}
clean.ref = (el: SVGElement | null) => {
self.elementRef.current = el;
if (typeof forwardedRef === 'function') {
forwardedRef(el);
} else if (forwardedRef) {
forwardedRef.current = el;
}
};
const styles: {
fontStyle?: string;
fontFamily?: string;
fontSize?: NumberProp;
fontWeight?: NumberProp;
} = {};
if (fontFamily != null) {
styles.fontFamily = fontFamily;
}
if (fontSize != null) {
styles.fontSize = fontSize;
}
if (fontWeight != null) {
styles.fontWeight = fontWeight;
}
if (fontStyle != null) {
styles.fontStyle = fontStyle;
}
clean.style = resolve(style, styles);
if (props.onPress != null) {
clean.onClick = props.onPress;
}
if (props.href !== null) {
clean.href = resolveAssetUri(props.href)?.uri;
}
return clean;
};

View File

@@ -2,7 +2,7 @@ import type { ComponentType, ComponentProps } from 'react';
import * as React from 'react';
import { Component, useEffect, useMemo, useState } from 'react';
import type { SvgProps } from './elements/Svg';
import { tags } from './tags';
import { tags } from './xmlTags';
function missingTag() {
return null;

View File

@@ -1,8 +1,13 @@
import Svg, {
import {
Circle,
ClipPath,
Defs,
Ellipse,
FeColorMatrix,
FeGaussianBlur,
FeOffset,
Filter,
ForeignObject,
G,
Image,
Line,
@@ -16,16 +21,13 @@ import Svg, {
RadialGradient,
Rect,
Stop,
Svg,
Symbol,
Text,
TextPath,
TSpan,
Use,
Symbol,
Filter,
FeColorMatrix,
FeGaussianBlur,
FeOffset,
} from './ReactNativeSVG';
} from './elements';
export const tags = {
circle: Circle,
@@ -36,6 +38,7 @@ export const tags = {
feColorMatrix: FeColorMatrix,
feGaussianBlur: FeGaussianBlur,
feOffset: FeOffset,
foreignObject: ForeignObject,
g: G,
image: Image,
line: Line,