Skip to content

Commit 34db2d4

Browse files
gabrieldonadelfacebook-github-bot
authored andcommitted
feat: Add string support to the transform property (#34660)
Summary: This updates the `transform` property to support string values as requested on #34425. This also updates the existing unit tests of the `processTransform` function ensuring the style processing works as expected and updates the TransformExample on RNTester in order to facilitate the manual QA of this. ## Changelog [General] [Added] - Add string support to the transform property Pull Request resolved: #34660 Test Plan: 1. Open the RNTester app and navigate to the Transforms page 2. Check the transform style through the `Transform using a string` section https://user-images.githubusercontent.com/11707729/189550548-ee3c14dd-11c6-4fd1-bd74-f6b52ecb9eae.mov Reviewed By: lunaleaps Differential Revision: D39423409 Pulled By: cipolleschi fbshipit-source-id: 0d7b79178eb33f34ae55a070ce094360b544361f
1 parent b7add0a commit 34db2d4

File tree

5 files changed

+205
-25
lines changed

5 files changed

+205
-25
lines changed

Libraries/StyleSheet/__tests__/__snapshots__/processTransform-test.js.snap

+11-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
exports[`processTransform validation should throw on invalid transform property 1`] = `"Invalid transform translateW: {\\"translateW\\":10}"`;
44

5+
exports[`processTransform validation should throw on invalid transform property 2`] = `"Invalid transform translateW: {\\"translateW\\":10}"`;
6+
57
exports[`processTransform validation should throw on object with multiple properties 1`] = `"You must specify exactly one property per transform object. Passed properties: {\\"scale\\":0.5,\\"translateY\\":10}"`;
68

79
exports[`processTransform validation should throw when not passing an array to an array prop 1`] = `"Transform with key of matrix must have an array as the value: {\\"matrix\\":\\"not-a-matrix\\"}"`;
@@ -10,17 +12,25 @@ exports[`processTransform validation should throw when not passing an array to a
1012

1113
exports[`processTransform validation should throw when passing a matrix of the wrong size 1`] = `"Matrix transform must have a length of 9 (2d) or 16 (3d). Provided matrix has a length of 4: {\\"matrix\\":[1,1,1,1]}"`;
1214

15+
exports[`processTransform validation should throw when passing a matrix of the wrong size 2`] = `"Matrix transform must have a length of 9 (2d) or 16 (3d). Provided matrix has a length of 4: {\\"matrix\\":[1,1,1,1]}"`;
16+
1317
exports[`processTransform validation should throw when passing a perspective of 0 1`] = `"Transform with key of \\"perspective\\" cannot be zero: {\\"perspective\\":0}"`;
1418

1519
exports[`processTransform validation should throw when passing a translate of the wrong size 1`] = `"Transform with key translate must be an array of length 2 or 3, found 1: {\\"translate\\":[1]}"`;
1620

1721
exports[`processTransform validation should throw when passing a translate of the wrong size 2`] = `"Transform with key translate must be an array of length 2 or 3, found 4: {\\"translate\\":[1,1,1,1]}"`;
1822

23+
exports[`processTransform validation should throw when passing a translate of the wrong size 3`] = `"Transform with key translate must be an string with 1 or 2 parameters, found 4: translate(1px, 1px, 1px, 1px)"`;
24+
1925
exports[`processTransform validation should throw when passing an Animated.Value 1`] = `"You passed an Animated.Value to a normal component. You need to wrap that component in an Animated. For example, replace <View /> by <Animated.View />."`;
2026

2127
exports[`processTransform validation should throw when passing an invalid angle prop 1`] = `"Transform with key of \\"rotate\\" must be a string: {\\"rotate\\":10}"`;
2228

23-
exports[`processTransform validation should throw when passing an invalid angle prop 2`] = `"Rotate transform must be expressed in degrees (deg) or radians (rad): {\\"skewX\\":\\"10drg\\"}"`;
29+
exports[`processTransform validation should throw when passing an invalid angle prop 2`] = `"Transform with key of \\"rotate\\" must be a string: {\\"rotate\\":10}"`;
30+
31+
exports[`processTransform validation should throw when passing an invalid angle prop 3`] = `"Rotate transform must be expressed in degrees (deg) or radians (rad): {\\"skewX\\":\\"10drg\\"}"`;
32+
33+
exports[`processTransform validation should throw when passing an invalid angle prop 4`] = `"Rotate transform must be expressed in degrees (deg) or radians (rad): {\\"skewX\\":\\"10drg\\"}"`;
2434

2535
exports[`processTransform validation should throw when passing an invalid value to a number prop 1`] = `"Transform with key of \\"translateY\\" must be a number: {\\"translateY\\":\\"20deg\\"}"`;
2636

Libraries/StyleSheet/__tests__/processTransform-test.js

+30
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,20 @@ describe('processTransform', () => {
1818
processTransform([]);
1919
});
2020

21+
it('should accept an empty string', () => {
22+
processTransform('');
23+
});
24+
2125
it('should accept a simple valid transform', () => {
2226
processTransform([
2327
{scale: 0.5},
2428
{translateX: 10},
2529
{translateY: 20},
2630
{rotate: '10deg'},
2731
]);
32+
processTransform(
33+
'scale(0.5) translateX(10px) translateY(20px) rotate(10deg)',
34+
);
2835
});
2936

3037
it('should throw on object with multiple properties', () => {
@@ -37,6 +44,9 @@ describe('processTransform', () => {
3744
expect(() =>
3845
processTransform([{translateW: 10}]),
3946
).toThrowErrorMatchingSnapshot();
47+
expect(() =>
48+
processTransform('translateW(10)'),
49+
).toThrowErrorMatchingSnapshot();
4050
});
4151

4252
it('should throw when not passing an array to an array prop', () => {
@@ -50,19 +60,28 @@ describe('processTransform', () => {
5060

5161
it('should accept a valid matrix', () => {
5262
processTransform([{matrix: [1, 1, 1, 1, 1, 1, 1, 1, 1]}]);
63+
processTransform('matrix(1, 1, 1, 1, 1, 1, 1, 1, 1)');
5364
processTransform([
5465
{matrix: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]},
5566
]);
67+
processTransform(
68+
'matrix(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)',
69+
);
5670
});
5771

5872
it('should throw when passing a matrix of the wrong size', () => {
5973
expect(() =>
6074
processTransform([{matrix: [1, 1, 1, 1]}]),
6175
).toThrowErrorMatchingSnapshot();
76+
expect(() =>
77+
processTransform('matrix(1, 1, 1, 1)'),
78+
).toThrowErrorMatchingSnapshot();
6279
});
6380

6481
it('should accept a valid translate', () => {
6582
processTransform([{translate: [1, 1]}]);
83+
processTransform('translate(1px)');
84+
processTransform('translate(1px, 1px)');
6685
processTransform([{translate: [1, 1, 1]}]);
6786
});
6887

@@ -73,6 +92,9 @@ describe('processTransform', () => {
7392
expect(() =>
7493
processTransform([{translate: [1, 1, 1, 1]}]),
7594
).toThrowErrorMatchingSnapshot();
95+
expect(() =>
96+
processTransform('translate(1px, 1px, 1px, 1px)'),
97+
).toThrowErrorMatchingSnapshot();
7698
});
7799

78100
it('should throw when passing an invalid value to a number prop', () => {
@@ -95,16 +117,24 @@ describe('processTransform', () => {
95117

96118
it('should accept an angle in degrees or radians', () => {
97119
processTransform([{skewY: '10deg'}]);
120+
processTransform('skewY(10deg)');
98121
processTransform([{rotateX: '1.16rad'}]);
122+
processTransform('rotateX(1.16rad)');
99123
});
100124

101125
it('should throw when passing an invalid angle prop', () => {
102126
expect(() =>
103127
processTransform([{rotate: 10}]),
104128
).toThrowErrorMatchingSnapshot();
129+
expect(() =>
130+
processTransform('rotate(10)'),
131+
).toThrowErrorMatchingSnapshot();
105132
expect(() =>
106133
processTransform([{skewX: '10drg'}]),
107134
).toThrowErrorMatchingSnapshot();
135+
expect(() =>
136+
processTransform('skewX(10drg)'),
137+
).toThrowErrorMatchingSnapshot();
108138
});
109139

110140
it('should throw when passing an Animated.Value', () => {

Libraries/StyleSheet/private/_TransformStyle.js

+25-23
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,29 @@ export type ____TransformStyle_Internal = $ReadOnly<{|
2727
*
2828
* `transform([{ skewX: '45deg' }])`
2929
*/
30-
transform?: $ReadOnlyArray<
31-
| {|+perspective: number | AnimatedNode|}
32-
| {|+rotate: string | AnimatedNode|}
33-
| {|+rotateX: string | AnimatedNode|}
34-
| {|+rotateY: string | AnimatedNode|}
35-
| {|+rotateZ: string | AnimatedNode|}
36-
| {|+scale: number | AnimatedNode|}
37-
| {|+scaleX: number | AnimatedNode|}
38-
| {|+scaleY: number | AnimatedNode|}
39-
| {|+translateX: number | AnimatedNode|}
40-
| {|+translateY: number | AnimatedNode|}
41-
| {|
42-
+translate:
43-
| [number | AnimatedNode, number | AnimatedNode]
44-
| AnimatedNode,
45-
|}
46-
| {|+skewX: string|}
47-
| {|+skewY: string|}
48-
// TODO: what is the actual type it expects?
49-
| {|
50-
+matrix: $ReadOnlyArray<number | AnimatedNode> | AnimatedNode,
51-
|},
52-
>,
30+
transform?:
31+
| $ReadOnlyArray<
32+
| {|+perspective: number | AnimatedNode|}
33+
| {|+rotate: string | AnimatedNode|}
34+
| {|+rotateX: string | AnimatedNode|}
35+
| {|+rotateY: string | AnimatedNode|}
36+
| {|+rotateZ: string | AnimatedNode|}
37+
| {|+scale: number | AnimatedNode|}
38+
| {|+scaleX: number | AnimatedNode|}
39+
| {|+scaleY: number | AnimatedNode|}
40+
| {|+translateX: number | AnimatedNode|}
41+
| {|+translateY: number | AnimatedNode|}
42+
| {|
43+
+translate:
44+
| [number | AnimatedNode, number | AnimatedNode]
45+
| AnimatedNode,
46+
|}
47+
| {|+skewX: string|}
48+
| {|+skewY: string|}
49+
// TODO: what is the actual type it expects?
50+
| {|
51+
+matrix: $ReadOnlyArray<number | AnimatedNode> | AnimatedNode,
52+
|},
53+
>
54+
| string,
5355
|}>;

Libraries/StyleSheet/processTransform.js

+117-1
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,131 @@ const stringifySafe = require('../Utilities/stringifySafe').default;
2222
* interface to native code.
2323
*/
2424
function processTransform(
25-
transform: Array<Object>,
25+
transform: Array<Object> | string,
2626
): Array<Object> | Array<number> {
27+
if (typeof transform === 'string') {
28+
const regex = new RegExp(/(\w+)\(([^)]+)\)/g);
29+
let transformArray: Array<Object> = [];
30+
let matches;
31+
32+
while ((matches = regex.exec(transform))) {
33+
const {key, value} = _getKeyAndValueFromCSSTransform(
34+
matches[1],
35+
matches[2],
36+
);
37+
38+
if (value !== undefined) {
39+
transformArray.push({[key]: value});
40+
}
41+
}
42+
transform = transformArray;
43+
}
44+
2745
if (__DEV__) {
2846
_validateTransforms(transform);
2947
}
3048

3149
return transform;
3250
}
3351

52+
const _getKeyAndValueFromCSSTransform: (
53+
key:
54+
| string
55+
| $TEMPORARY$string<'matrix'>
56+
| $TEMPORARY$string<'perspective'>
57+
| $TEMPORARY$string<'rotate'>
58+
| $TEMPORARY$string<'rotateX'>
59+
| $TEMPORARY$string<'rotateY'>
60+
| $TEMPORARY$string<'rotateZ'>
61+
| $TEMPORARY$string<'scale'>
62+
| $TEMPORARY$string<'scaleX'>
63+
| $TEMPORARY$string<'scaleY'>
64+
| $TEMPORARY$string<'skewX'>
65+
| $TEMPORARY$string<'skewY'>
66+
| $TEMPORARY$string<'translate'>
67+
| $TEMPORARY$string<'translate3d'>
68+
| $TEMPORARY$string<'translateX'>
69+
| $TEMPORARY$string<'translateY'>,
70+
args: string,
71+
) => {key: string, value?: number[] | number | string} = (key, args) => {
72+
const argsWithUnitsRegex = new RegExp(/([+-]?\d+(\.\d+)?)([a-zA-Z]+)?/g);
73+
74+
switch (key) {
75+
case 'matrix':
76+
return {key, value: args.match(/[+-]?\d+(\.\d+)?/g)?.map(Number)};
77+
case 'translate':
78+
case 'translate3d':
79+
const parsedArgs = [];
80+
let missingUnitOfMeasurement = false;
81+
82+
let matches;
83+
while ((matches = argsWithUnitsRegex.exec(args))) {
84+
const value = Number(matches[1]);
85+
const unitOfMeasurement = matches[3];
86+
87+
if (value !== 0 && !unitOfMeasurement) {
88+
missingUnitOfMeasurement = true;
89+
}
90+
91+
parsedArgs.push(value);
92+
}
93+
94+
if (__DEV__) {
95+
invariant(
96+
!missingUnitOfMeasurement,
97+
`Transform with key ${key} must have units unless the provided value is 0, found %s`,
98+
`${key}(${args})`,
99+
);
100+
101+
if (key === 'translate') {
102+
invariant(
103+
parsedArgs?.length === 1 || parsedArgs?.length === 2,
104+
'Transform with key translate must be an string with 1 or 2 parameters, found %s: %s',
105+
parsedArgs?.length,
106+
`${key}(${args})`,
107+
);
108+
} else {
109+
invariant(
110+
parsedArgs?.length === 3,
111+
'Transform with key translate3d must be an string with 3 parameters, found %s: %s',
112+
parsedArgs?.length,
113+
`${key}(${args})`,
114+
);
115+
}
116+
}
117+
118+
if (parsedArgs?.length === 1) {
119+
parsedArgs.push(0);
120+
}
121+
122+
return {key: 'translate', value: parsedArgs};
123+
case 'translateX':
124+
case 'translateY':
125+
case 'perspective':
126+
const argMatches = argsWithUnitsRegex.exec(args);
127+
128+
if (!argMatches?.length) {
129+
return {key, value: undefined};
130+
}
131+
132+
const value = Number(argMatches[1]);
133+
const unitOfMeasurement = argMatches[3];
134+
135+
if (__DEV__) {
136+
invariant(
137+
value === 0 || unitOfMeasurement,
138+
`Transform with key ${key} must have units unless the provided value is 0, found %s`,
139+
`${key}(${args})`,
140+
);
141+
}
142+
143+
return {key, value};
144+
145+
default:
146+
return {key, value: !isNaN(args) ? Number(args) : args};
147+
}
148+
};
149+
34150
function _validateTransforms(transform: Array<Object>): void {
35151
transform.forEach(transformation => {
36152
const keys = Object.keys(transformation);

packages/rn-tester/js/examples/Transform/TransformExample.js

+22
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,17 @@ const styles = StyleSheet.create({
199199
backgroundColor: 'salmon',
200200
alignSelf: 'center',
201201
},
202+
box7: {
203+
backgroundColor: 'lightseagreen',
204+
height: 50,
205+
position: 'absolute',
206+
right: 0,
207+
top: 0,
208+
width: 50,
209+
},
210+
box7Transform: {
211+
transform: 'translate(-50, 35) rotate(50deg) scale(2)',
212+
},
202213
flipCardContainer: {
203214
marginVertical: 40,
204215
flex: 1,
@@ -324,4 +335,15 @@ exports.examples = [
324335
return <AnimateTransformSingleProp />;
325336
},
326337
},
338+
{
339+
title: 'Transform using a string',
340+
description: "transform: 'translate(-50, 35) rotate(50deg) scale(2)'",
341+
render(): Node {
342+
return (
343+
<View style={styles.container}>
344+
<View style={[styles.box7, styles.box7Transform]} />
345+
</View>
346+
);
347+
},
348+
},
327349
];

0 commit comments

Comments
 (0)