From 7f174b90f7970bbfc337d063332599748792de18 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 29 Sep 2022 14:08:40 +0200 Subject: [PATCH 1/8] make reanimated work in web --- src/ReactNativeSVG.web.ts | 168 +++++++++++++++++++++----------------- 1 file changed, 92 insertions(+), 76 deletions(-) diff --git a/src/ReactNativeSVG.web.ts b/src/ReactNativeSVG.web.ts index 9c10b062b..56bbf7854 100644 --- a/src/ReactNativeSVG.web.ts +++ b/src/ReactNativeSVG.web.ts @@ -58,7 +58,9 @@ interface BaseProps { fontWeight?: NumberProp; fontSize?: NumberProp; fontFamily?: string; - forwardedRef: {}; + forwardedRef?: + | React.RefCallback + | React.MutableRefObject; style: Iterable<{}>; } @@ -156,9 +158,14 @@ const prepare = ( clean.transform = transform.join(' '); } - if (forwardedRef) { - clean.ref = forwardedRef; - } + 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; @@ -237,6 +244,41 @@ export class WebShape< C = {}, > extends React.Component { [x: string]: unknown; + protected tag?: React.ElementType; + protected prepareProps(props: P) { + return props; + } + + elementRef = + React.createRef() as React.MutableRefObject; + lastMergedProps: Partial

= {}; + + /** + * 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 prepared = prepare(this, this.prepareProps(merged)); + const current = this.elementRef.current; + + if (current) { + if (prepared.transform) { + // @ts-expect-error "DOM" is not part of `compilerOptions.lib` + current.setAttribute('transform', prepared.transform); + } + if (prepared.style) { + // @ts-expect-error "DOM" is not part of `compilerOptions.lib` + Object.assign(current.style, prepared.style); + } + } + } + _remeasureMetricsOnActivation: () => void; touchableHandleStartShouldSetResponder?: ( e: GestureResponderEvent, @@ -258,30 +300,35 @@ export class WebShape< this._remeasureMetricsOnActivation = remeasure.bind(this); } -} -export class Circle extends WebShape { render(): JSX.Element { - return createElement('circle', prepare(this)); + 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 { + tag = 'circle' as const; +} + export class ClipPath extends WebShape { - render(): JSX.Element { - return createElement('clipPath', prepare(this)); - } + tag = 'clipPath' as const; } export class Defs extends WebShape { - render(): JSX.Element { - return createElement('defs', prepare(this)); - } + tag = 'defs' as const; } export class Ellipse extends WebShape { - render(): JSX.Element { - return createElement('ellipse', prepare(this)); - } + tag = 'ellipse' as const; } export class G extends WebShape< @@ -291,129 +338,98 @@ export class G extends WebShape< translate?: string; } > { - render(): JSX.Element { - const { x, y, ...rest } = this.props; + tag = 'g' as const; + prepareProps( + props: BaseProps & { + x?: NumberProp; + y?: NumberProp; + translate?: string; + }, + ) { + const { x, y, ...rest } = props; if ((x || y) && !rest.translate) { rest.translate = `${x || 0}, ${y || 0}`; } - return createElement('g', prepare(this, rest)); + return rest; } } export class Image extends WebShape { - render(): JSX.Element { - return createElement('image', prepare(this)); - } + tag = 'image' as const; } export class Line extends WebShape { - render(): JSX.Element { - return createElement('line', prepare(this)); - } + tag = 'line' as const; } export class LinearGradient extends WebShape { - render(): JSX.Element { - return createElement('linearGradient', prepare(this)); - } + tag = 'linearGradient' as const; } export class Path extends WebShape { - render(): JSX.Element { - return createElement('path', prepare(this)); - } + tag = 'path' as const; } export class Polygon extends WebShape { - render(): JSX.Element { - return createElement('polygon', prepare(this)); - } + tag = 'polygon' as const; } export class Polyline extends WebShape { - render(): JSX.Element { - return createElement('polyline', prepare(this)); - } + tag = 'polyline' as const; } export class RadialGradient extends WebShape { - render(): JSX.Element { - return createElement('radialGradient', prepare(this)); - } + tag = 'radialGradient' as const; } export class Rect extends WebShape { - render(): JSX.Element { - return createElement('rect', prepare(this)); - } + tag = 'rect' as const; } export class Stop extends WebShape { - render(): JSX.Element { - return createElement('stop', prepare(this)); - } + tag = 'stop' as const; } export class Svg extends WebShape { - render(): JSX.Element { - return createElement('svg', prepare(this)); - } + tag = 'svg' as const; } export class Symbol extends WebShape { - render(): JSX.Element { - return createElement('symbol', prepare(this)); - } + tag = 'symbol' as const; } export class Text extends WebShape { - render(): JSX.Element { - return createElement('text', prepare(this)); - } + tag = 'text' as const; } export class TSpan extends WebShape { - render(): JSX.Element { - return createElement('tspan', prepare(this)); - } + tag = 'tspan' as const; } export class TextPath extends WebShape { - render(): JSX.Element { - return createElement('textPath', prepare(this)); - } + tag = 'textPath' as const; } export class Use extends WebShape { - render(): JSX.Element { - return createElement('use', prepare(this)); - } + tag = 'use' as const; } export class Mask extends WebShape { - render(): JSX.Element { - return createElement('mask', prepare(this)); - } + tag = 'mask' as const; } export class ForeignObject extends WebShape { - render(): JSX.Element { - return createElement('foreignObject', prepare(this)); - } + tag = 'foreignObject' as const; } export class Marker extends WebShape { - render(): JSX.Element { - return createElement('marker', prepare(this)); - } + tag = 'marker' as const; } export class Pattern extends WebShape { - render(): JSX.Element { - return createElement('pattern', prepare(this)); - } + tag = 'pattern' as const; } export default Svg; From 6b59da78088c4d027d3cc736a469e35157eb8224 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 30 Sep 2022 13:57:20 +0200 Subject: [PATCH 2/8] use `_touchableNode` instead of `setNativeProps` --- src/ReactNativeSVG.web.ts | 101 ++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 27 deletions(-) diff --git a/src/ReactNativeSVG.web.ts b/src/ReactNativeSVG.web.ts index 56bbf7854..b14f8662e 100644 --- a/src/ReactNativeSVG.web.ts +++ b/src/ReactNativeSVG.web.ts @@ -158,12 +158,30 @@ const prepare = ( clean.transform = transform.join(' '); } - clean.ref = (el: SVGElement | null) => { - self.elementRef.current = el; + clean.ref = (element: SVGElement | null) => { + self.elementRef.current = element; + /** + * If the TouchableMixin is used, this already has been + * set to `element` in `componentDidMount`. + * We want to override `element.setAttribute`, so we replace + * it with a proxied version (not touching the real DOM node). + */ + self._touchableNode = !element + ? element + : new Proxy(element, { + get(target, p) { + if (p === 'setAttribute') { + return self.schedulePropUpdate.bind(self); + } + // @ts-expect-error this is always non-typesafe + return target[p]; + }, + }); + // handle `forwardedRef` if it was present if (typeof forwardedRef === 'function') { - forwardedRef(el); + forwardedRef(element); } else if (forwardedRef) { - forwardedRef.current = el; + forwardedRef.current = element; } }; @@ -239,6 +257,13 @@ function remeasure() { measureLayout(tag, this._handleQueryLayout); } +function dashedToCamelCase(dashedKey: string) { + return dashedKey.replace(/-[a-z]/g, (m) => m[1].toUpperCase()); +} +function camelCaseToDashed(camelCase: string) { + return camelCase.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); +} + export class WebShape< P extends BaseProps = BaseProps, C = {}, @@ -251,30 +276,52 @@ export class WebShape< elementRef = React.createRef() as React.MutableRefObject; - lastMergedProps: Partial

= {}; - - /** - * 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, + + updatedProps: Record = {}; + propUpdatesScheduled: number | undefined; + schedulePropUpdate(dashedKey: string, value: unknown) { + const key = dashedToCamelCase(dashedKey); + this.updatedProps[key] = value; + + if (!this.propUpdatesScheduled) { + this.propUpdatesScheduled = requestAnimationFrame( + this.applyPropUpdates.bind(this), + ); + } + } + resetPropUpdates() { + this.updatedProps = {}; + if (this.propUpdatesScheduled) { + cancelAnimationFrame(this.propUpdatesScheduled); + this.propUpdatesScheduled = undefined; + } + } + applyPropUpdates() { + this.propUpdatesScheduled = undefined; + + const clean = prepare( + this, + this.prepareProps({ ...this.props, ...this.updatedProps }), ); - this.lastMergedProps = merged; - const prepared = prepare(this, this.prepareProps(merged)); - const current = this.elementRef.current; - if (current) { - if (prepared.transform) { - // @ts-expect-error "DOM" is not part of `compilerOptions.lib` - current.setAttribute('transform', prepared.transform); - } - if (prepared.style) { - // @ts-expect-error "DOM" is not part of `compilerOptions.lib` - Object.assign(current.style, prepared.style); + const current = this.elementRef.current; + if (!current) { + return; + } + for (const cleanAttribute of Object.keys(clean)) { + const cleanValue = clean[cleanAttribute as keyof typeof clean]; + switch (cleanAttribute) { + case 'ref': + case 'children': + break; + case 'style': + // @ts-expect-error "DOM" is not part of `compilerOptions.lib` + Object.assign(current.style, cleanValue); + break; + default: + // @ts-expect-error "DOM" is not part of `compilerOptions.lib` + current.setAttribute(camelCaseToDashed(cleanAttribute), cleanValue); + break; } } } @@ -307,7 +354,7 @@ export class WebShape< 'When extending `WebShape` you need to overwrite either `tag` or `render`!', ); } - this.lastMergedProps = {}; + this.resetPropUpdates(); return createElement( this.tag, prepare(this, this.prepareProps(this.props)), From 35820bef9c0b4ca24e52c2d5776f30425c267198 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 30 Sep 2022 14:58:35 +0200 Subject: [PATCH 3/8] add reanimated example --- Example/babel.config.js | 4 +++ Example/package.json | 1 + Example/src/App.tsx | 1 + Example/src/examples.tsx | 2 ++ Example/src/examples/Reanimated.tsx | 42 +++++++++++++++++++++++++++++ Example/webpack.config.js | 2 ++ Example/yarn.lock | 37 ++++++++++++++++++++++++- 7 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 Example/src/examples/Reanimated.tsx diff --git a/Example/babel.config.js b/Example/babel.config.js index f842b77fc..a812bcfa7 100644 --- a/Example/babel.config.js +++ b/Example/babel.config.js @@ -1,3 +1,7 @@ module.exports = { presets: ['module:metro-react-native-babel-preset'], + plugins: [ + '@babel/plugin-proposal-export-namespace-from', + 'react-native-reanimated/plugin', + ], }; diff --git a/Example/package.json b/Example/package.json index e04f66d14..a83b54050 100644 --- a/Example/package.json +++ b/Example/package.json @@ -16,6 +16,7 @@ "react": "18.1.0", "react-dom": "^18.2.0", "react-native": "0.70.0", + "react-native-reanimated": "^2.10.0", "react-native-svg": "link:../", "react-native-web": "^0.17.7" }, diff --git a/Example/src/App.tsx b/Example/src/App.tsx index 4ae59fa49..0212df79d 100644 --- a/Example/src/App.tsx +++ b/Example/src/App.tsx @@ -112,6 +112,7 @@ const names: (keyof typeof examples)[] = [ 'TouchEvents', 'PanResponder', 'Reusable', + 'Reanimated', ]; const initialState = { diff --git a/Example/src/examples.tsx b/Example/src/examples.tsx index a1d56f38b..64a80e950 100644 --- a/Example/src/examples.tsx +++ b/Example/src/examples.tsx @@ -15,6 +15,7 @@ import * as Image from './examples/Image'; import * as Reusable from './examples/Reusable'; import * as TouchEvents from './examples/TouchEvents'; import * as PanResponder from './examples/PanResponder'; +import * as Reanimated from './examples/Reanimated'; export { Svg, @@ -34,4 +35,5 @@ export { TouchEvents, Reusable, PanResponder, + Reanimated, }; diff --git a/Example/src/examples/Reanimated.tsx b/Example/src/examples/Reanimated.tsx new file mode 100644 index 000000000..a015ffc2b --- /dev/null +++ b/Example/src/examples/Reanimated.tsx @@ -0,0 +1,42 @@ +import React, {useEffect} from 'react'; +import {StyleSheet, Text} from 'react-native'; +import Reanimated, { + useAnimatedProps, + useSharedValue, + withRepeat, + withSpring, + withTiming, +} from 'react-native-reanimated'; +import {Svg, Rect} from 'react-native-svg'; + +const ReanimatedRect = Reanimated.createAnimatedComponent(Rect); + +function ReanimatedRectExample() { + const height = useSharedValue(10); + const position = useSharedValue(0); + + useEffect(() => { + height.value = withRepeat(withSpring(100), -1, true); + position.value = withRepeat(withTiming(300, {duration: 5000}), -1); + }); + const animatedProps = useAnimatedProps(() => ({ + width: 30, + height: height.value, + x: position.value, + y: 20, + })); + + return ( + + + + ); +} +ReanimatedRectExample.title = 'reanimated rectangle'; + +const samples = [ReanimatedRectExample]; + +const style = StyleSheet.create({text: {width: 30, height: 30}}); +const icon = R; + +export {icon, samples}; diff --git a/Example/webpack.config.js b/Example/webpack.config.js index 6ea76c2bd..562e08480 100644 --- a/Example/webpack.config.js +++ b/Example/webpack.config.js @@ -23,6 +23,7 @@ module.exports = { fromRoot('index.js'), fromRoot('src'), fromRoot('node_modules/react-native-svg'), + fromRoot('node_modules/react-native-reanimated'), ], }, { @@ -52,4 +53,5 @@ module.exports = { '.jsx', ], }, + plugins: [new (require('webpack').DefinePlugin)({process: {env: {}}})], }; diff --git a/Example/yarn.lock b/Example/yarn.lock index 59d319c89..108d9d46b 100644 --- a/Example/yarn.lock +++ b/Example/yarn.lock @@ -578,6 +578,13 @@ "@babel/helper-create-regexp-features-plugin" "^7.19.0" "@babel/helper-plugin-utils" "^7.19.0" +"@babel/plugin-transform-object-assign@^7.16.7": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.18.6.tgz#7830b4b6f83e1374a5afb9f6111bcfaea872cdd2" + integrity sha512-mQisZ3JfqWh2gVXvfqYCAAyRs6+7oev+myBsTwW5RnPhYXOTuCEw2oe3YgxlXMViXUS53lG8koulI7mJ+8JE+A== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-transform-object-super@^7.0.0": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" @@ -699,7 +706,7 @@ "@babel/helper-validator-option" "^7.18.6" "@babel/plugin-transform-flow-strip-types" "^7.18.6" -"@babel/preset-typescript@^7.13.0": +"@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.16.7": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz#ce64be3e63eddc44240c6358daefac17b3186399" integrity sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ== @@ -1440,6 +1447,11 @@ dependencies: "@types/node" "*" +"@types/invariant@^2.2.35": + version "2.2.35" + resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be" + integrity sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -5253,6 +5265,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -6544,6 +6561,19 @@ react-native-gradle-plugin@^0.70.2: resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.2.tgz#b5130f2c196e27c4c5912706503d69b8790f1937" integrity sha512-k7d+CVh0fs/VntA2WaKD58cFB2rtiSLBHYlciH18ncaT4N/B3A4qOGv9pSCEHfQikELm6vAf98KMbE3c8KnH1A== +react-native-reanimated@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.10.0.tgz#ed53be66bbb553b5b5e93e93ef4217c87b8c73db" + integrity sha512-jKm3xz5nX7ABtHzzuuLmawP0pFWP77lXNdIC6AWOceBs23OHUaJ29p4prxr/7Sb588GwTbkPsYkDqVFaE3ezNQ== + dependencies: + "@babel/plugin-transform-object-assign" "^7.16.7" + "@babel/preset-typescript" "^7.16.7" + "@types/invariant" "^2.2.35" + invariant "^2.2.4" + lodash.isequal "^4.5.0" + setimmediate "^1.0.5" + string-hash-64 "^1.0.3" + "react-native-svg@link:..": version "0.0.0" uid "" @@ -7353,6 +7383,11 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +string-hash-64@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322" + integrity sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" From eb3b0fa2a7a333c8dc5b879614bf783219f8c58b Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 10 Oct 2022 15:23:21 +0200 Subject: [PATCH 4/8] revert back to `setNativeProps` approach --- Example/yarn.lock | 37 +------------- src/ReactNativeSVG.web.ts | 101 ++++++++++---------------------------- 2 files changed, 28 insertions(+), 110 deletions(-) diff --git a/Example/yarn.lock b/Example/yarn.lock index 108d9d46b..59d319c89 100644 --- a/Example/yarn.lock +++ b/Example/yarn.lock @@ -578,13 +578,6 @@ "@babel/helper-create-regexp-features-plugin" "^7.19.0" "@babel/helper-plugin-utils" "^7.19.0" -"@babel/plugin-transform-object-assign@^7.16.7": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.18.6.tgz#7830b4b6f83e1374a5afb9f6111bcfaea872cdd2" - integrity sha512-mQisZ3JfqWh2gVXvfqYCAAyRs6+7oev+myBsTwW5RnPhYXOTuCEw2oe3YgxlXMViXUS53lG8koulI7mJ+8JE+A== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-transform-object-super@^7.0.0": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" @@ -706,7 +699,7 @@ "@babel/helper-validator-option" "^7.18.6" "@babel/plugin-transform-flow-strip-types" "^7.18.6" -"@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.16.7": +"@babel/preset-typescript@^7.13.0": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz#ce64be3e63eddc44240c6358daefac17b3186399" integrity sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ== @@ -1447,11 +1440,6 @@ dependencies: "@types/node" "*" -"@types/invariant@^2.2.35": - version "2.2.35" - resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be" - integrity sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg== - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -5265,11 +5253,6 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== - lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -6561,19 +6544,6 @@ react-native-gradle-plugin@^0.70.2: resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.2.tgz#b5130f2c196e27c4c5912706503d69b8790f1937" integrity sha512-k7d+CVh0fs/VntA2WaKD58cFB2rtiSLBHYlciH18ncaT4N/B3A4qOGv9pSCEHfQikELm6vAf98KMbE3c8KnH1A== -react-native-reanimated@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.10.0.tgz#ed53be66bbb553b5b5e93e93ef4217c87b8c73db" - integrity sha512-jKm3xz5nX7ABtHzzuuLmawP0pFWP77lXNdIC6AWOceBs23OHUaJ29p4prxr/7Sb588GwTbkPsYkDqVFaE3ezNQ== - dependencies: - "@babel/plugin-transform-object-assign" "^7.16.7" - "@babel/preset-typescript" "^7.16.7" - "@types/invariant" "^2.2.35" - invariant "^2.2.4" - lodash.isequal "^4.5.0" - setimmediate "^1.0.5" - string-hash-64 "^1.0.3" - "react-native-svg@link:..": version "0.0.0" uid "" @@ -7383,11 +7353,6 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -string-hash-64@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322" - integrity sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw== - string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" diff --git a/src/ReactNativeSVG.web.ts b/src/ReactNativeSVG.web.ts index b14f8662e..56bbf7854 100644 --- a/src/ReactNativeSVG.web.ts +++ b/src/ReactNativeSVG.web.ts @@ -158,30 +158,12 @@ const prepare = ( clean.transform = transform.join(' '); } - clean.ref = (element: SVGElement | null) => { - self.elementRef.current = element; - /** - * If the TouchableMixin is used, this already has been - * set to `element` in `componentDidMount`. - * We want to override `element.setAttribute`, so we replace - * it with a proxied version (not touching the real DOM node). - */ - self._touchableNode = !element - ? element - : new Proxy(element, { - get(target, p) { - if (p === 'setAttribute') { - return self.schedulePropUpdate.bind(self); - } - // @ts-expect-error this is always non-typesafe - return target[p]; - }, - }); - // handle `forwardedRef` if it was present + clean.ref = (el: SVGElement | null) => { + self.elementRef.current = el; if (typeof forwardedRef === 'function') { - forwardedRef(element); + forwardedRef(el); } else if (forwardedRef) { - forwardedRef.current = element; + forwardedRef.current = el; } }; @@ -257,13 +239,6 @@ function remeasure() { measureLayout(tag, this._handleQueryLayout); } -function dashedToCamelCase(dashedKey: string) { - return dashedKey.replace(/-[a-z]/g, (m) => m[1].toUpperCase()); -} -function camelCaseToDashed(camelCase: string) { - return camelCase.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); -} - export class WebShape< P extends BaseProps = BaseProps, C = {}, @@ -276,52 +251,30 @@ export class WebShape< elementRef = React.createRef() as React.MutableRefObject; - - updatedProps: Record = {}; - propUpdatesScheduled: number | undefined; - schedulePropUpdate(dashedKey: string, value: unknown) { - const key = dashedToCamelCase(dashedKey); - this.updatedProps[key] = value; - - if (!this.propUpdatesScheduled) { - this.propUpdatesScheduled = requestAnimationFrame( - this.applyPropUpdates.bind(this), - ); - } - } - resetPropUpdates() { - this.updatedProps = {}; - if (this.propUpdatesScheduled) { - cancelAnimationFrame(this.propUpdatesScheduled); - this.propUpdatesScheduled = undefined; - } - } - applyPropUpdates() { - this.propUpdatesScheduled = undefined; - - const clean = prepare( - this, - this.prepareProps({ ...this.props, ...this.updatedProps }), + lastMergedProps: Partial

= {}; + + /** + * 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 prepared = prepare(this, this.prepareProps(merged)); const current = this.elementRef.current; - if (!current) { - return; - } - for (const cleanAttribute of Object.keys(clean)) { - const cleanValue = clean[cleanAttribute as keyof typeof clean]; - switch (cleanAttribute) { - case 'ref': - case 'children': - break; - case 'style': - // @ts-expect-error "DOM" is not part of `compilerOptions.lib` - Object.assign(current.style, cleanValue); - break; - default: - // @ts-expect-error "DOM" is not part of `compilerOptions.lib` - current.setAttribute(camelCaseToDashed(cleanAttribute), cleanValue); - break; + + if (current) { + if (prepared.transform) { + // @ts-expect-error "DOM" is not part of `compilerOptions.lib` + current.setAttribute('transform', prepared.transform); + } + if (prepared.style) { + // @ts-expect-error "DOM" is not part of `compilerOptions.lib` + Object.assign(current.style, prepared.style); } } } @@ -354,7 +307,7 @@ export class WebShape< 'When extending `WebShape` you need to overwrite either `tag` or `render`!', ); } - this.resetPropUpdates(); + this.lastMergedProps = {}; return createElement( this.tag, prepare(this, this.prepareProps(this.props)), From d5d2e73584a33b8eb9c1e1a4c2d652296db71591 Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Tue, 11 Oct 2022 16:34:28 +0000 Subject: [PATCH 5/8] example rectangle color, skipLibCheck in examples --- Example/src/examples/Reanimated.tsx | 2 +- Example/tsconfig.json | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Example/src/examples/Reanimated.tsx b/Example/src/examples/Reanimated.tsx index a015ffc2b..27fccf6c0 100644 --- a/Example/src/examples/Reanimated.tsx +++ b/Example/src/examples/Reanimated.tsx @@ -28,7 +28,7 @@ function ReanimatedRectExample() { return ( - + ); } diff --git a/Example/tsconfig.json b/Example/tsconfig.json index 8888051e5..b62818238 100644 --- a/Example/tsconfig.json +++ b/Example/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../tsconfig.json", - "include": ["src/**/*"] + "include": ["src/**/*"], + "compilerOptions": { + "skipLibCheck": true + } } From 81b3210ca1a7aff35031f3b8b32302e87e2052b3 Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Wed, 12 Oct 2022 21:51:53 +0200 Subject: [PATCH 6/8] also apply other properties than `style` and `transform` --- Example/yarn.lock | 37 ++++++++++++++++++++++++++++++++++++- src/ReactNativeSVG.web.ts | 29 ++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/Example/yarn.lock b/Example/yarn.lock index 59d319c89..108d9d46b 100644 --- a/Example/yarn.lock +++ b/Example/yarn.lock @@ -578,6 +578,13 @@ "@babel/helper-create-regexp-features-plugin" "^7.19.0" "@babel/helper-plugin-utils" "^7.19.0" +"@babel/plugin-transform-object-assign@^7.16.7": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.18.6.tgz#7830b4b6f83e1374a5afb9f6111bcfaea872cdd2" + integrity sha512-mQisZ3JfqWh2gVXvfqYCAAyRs6+7oev+myBsTwW5RnPhYXOTuCEw2oe3YgxlXMViXUS53lG8koulI7mJ+8JE+A== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-transform-object-super@^7.0.0": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" @@ -699,7 +706,7 @@ "@babel/helper-validator-option" "^7.18.6" "@babel/plugin-transform-flow-strip-types" "^7.18.6" -"@babel/preset-typescript@^7.13.0": +"@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.16.7": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz#ce64be3e63eddc44240c6358daefac17b3186399" integrity sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ== @@ -1440,6 +1447,11 @@ dependencies: "@types/node" "*" +"@types/invariant@^2.2.35": + version "2.2.35" + resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be" + integrity sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -5253,6 +5265,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -6544,6 +6561,19 @@ react-native-gradle-plugin@^0.70.2: resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.2.tgz#b5130f2c196e27c4c5912706503d69b8790f1937" integrity sha512-k7d+CVh0fs/VntA2WaKD58cFB2rtiSLBHYlciH18ncaT4N/B3A4qOGv9pSCEHfQikELm6vAf98KMbE3c8KnH1A== +react-native-reanimated@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.10.0.tgz#ed53be66bbb553b5b5e93e93ef4217c87b8c73db" + integrity sha512-jKm3xz5nX7ABtHzzuuLmawP0pFWP77lXNdIC6AWOceBs23OHUaJ29p4prxr/7Sb588GwTbkPsYkDqVFaE3ezNQ== + dependencies: + "@babel/plugin-transform-object-assign" "^7.16.7" + "@babel/preset-typescript" "^7.16.7" + "@types/invariant" "^2.2.35" + invariant "^2.2.4" + lodash.isequal "^4.5.0" + setimmediate "^1.0.5" + string-hash-64 "^1.0.3" + "react-native-svg@link:..": version "0.0.0" uid "" @@ -7353,6 +7383,11 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +string-hash-64@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322" + integrity sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" diff --git a/src/ReactNativeSVG.web.ts b/src/ReactNativeSVG.web.ts index 56bbf7854..256c4e9c1 100644 --- a/src/ReactNativeSVG.web.ts +++ b/src/ReactNativeSVG.web.ts @@ -67,6 +67,10 @@ interface BaseProps { 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()); +}; + /** * `react-native-svg` supports additional props that aren't defined in the spec. * This function replaces them in a spec conforming manner. @@ -264,17 +268,24 @@ export class WebShape< props.style, ); this.lastMergedProps = merged; - const prepared = prepare(this, this.prepareProps(merged)); + const clean = prepare(this, this.prepareProps(merged)); const current = this.elementRef.current; - if (current) { - if (prepared.transform) { - // @ts-expect-error "DOM" is not part of `compilerOptions.lib` - current.setAttribute('transform', prepared.transform); - } - if (prepared.style) { - // @ts-expect-error "DOM" is not part of `compilerOptions.lib` - Object.assign(current.style, prepared.style); + for (const cleanAttribute of Object.keys(clean)) { + const cleanValue = clean[cleanAttribute as keyof typeof clean]; + switch (cleanAttribute) { + case 'ref': + case 'children': + break; + case 'style': + // @ts-expect-error "DOM" is not part of `compilerOptions.lib` + Object.assign(current.style, cleanValue); + break; + default: + // @ts-expect-error "DOM" is not part of `compilerOptions.lib` + current.setAttribute(camelCaseToDashed(cleanAttribute), cleanValue); + break; + } } } } From 6506f5da6c0b2f731148a3cc278cbd0490fd0b7b Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 13 Oct 2022 12:26:25 +0200 Subject: [PATCH 7/8] fix when style is passed in as an array --- src/ReactNativeSVG.web.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ReactNativeSVG.web.ts b/src/ReactNativeSVG.web.ts index 256c4e9c1..11dd4aa82 100644 --- a/src/ReactNativeSVG.web.ts +++ b/src/ReactNativeSVG.web.ts @@ -278,8 +278,11 @@ export class WebShape< case 'children': break; case 'style': - // @ts-expect-error "DOM" is not part of `compilerOptions.lib` - Object.assign(current.style, cleanValue); + // 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 {}[]).concat(clean.style ?? [])) { + // @ts-expect-error "DOM" is not part of `compilerOptions.lib` + Object.assign(current.style, partialStyle); + } break; default: // @ts-expect-error "DOM" is not part of `compilerOptions.lib` From ff9e30fa4ea83ca1da34906af192d230e232d9f4 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 13 Oct 2022 13:49:38 +0200 Subject: [PATCH 8/8] add comment to mimicked upstream code --- src/ReactNativeSVG.web.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ReactNativeSVG.web.ts b/src/ReactNativeSVG.web.ts index 11dd4aa82..0ae8b1556 100644 --- a/src/ReactNativeSVG.web.ts +++ b/src/ReactNativeSVG.web.ts @@ -285,6 +285,8 @@ export class WebShape< } 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 "DOM" is not part of `compilerOptions.lib` current.setAttribute(camelCaseToDashed(cleanAttribute), cleanValue); break;