Skip to content

Commit

Permalink
feat: Add string support to the transform property (#34660)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gabrieldonadel authored and facebook-github-bot committed Sep 22, 2022
1 parent b7add0a commit 34db2d4
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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

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

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}"`;

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\\"}"`;
Expand All @@ -10,17 +12,25 @@ exports[`processTransform validation should throw when not passing an array to a

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]}"`;

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]}"`;

exports[`processTransform validation should throw when passing a perspective of 0 1`] = `"Transform with key of \\"perspective\\" cannot be zero: {\\"perspective\\":0}"`;

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]}"`;

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]}"`;

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)"`;

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 />."`;

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

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\\"}"`;
exports[`processTransform validation should throw when passing an invalid angle prop 2`] = `"Transform with key of \\"rotate\\" must be a string: {\\"rotate\\":10}"`;

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\\"}"`;

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\\"}"`;

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\\"}"`;

Expand Down
30 changes: 30 additions & 0 deletions Libraries/StyleSheet/__tests__/processTransform-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@ describe('processTransform', () => {
processTransform([]);
});

it('should accept an empty string', () => {
processTransform('');
});

it('should accept a simple valid transform', () => {
processTransform([
{scale: 0.5},
{translateX: 10},
{translateY: 20},
{rotate: '10deg'},
]);
processTransform(
'scale(0.5) translateX(10px) translateY(20px) rotate(10deg)',
);
});

it('should throw on object with multiple properties', () => {
Expand All @@ -37,6 +44,9 @@ describe('processTransform', () => {
expect(() =>
processTransform([{translateW: 10}]),
).toThrowErrorMatchingSnapshot();
expect(() =>
processTransform('translateW(10)'),
).toThrowErrorMatchingSnapshot();
});

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

it('should accept a valid matrix', () => {
processTransform([{matrix: [1, 1, 1, 1, 1, 1, 1, 1, 1]}]);
processTransform('matrix(1, 1, 1, 1, 1, 1, 1, 1, 1)');
processTransform([
{matrix: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]},
]);
processTransform(
'matrix(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)',
);
});

it('should throw when passing a matrix of the wrong size', () => {
expect(() =>
processTransform([{matrix: [1, 1, 1, 1]}]),
).toThrowErrorMatchingSnapshot();
expect(() =>
processTransform('matrix(1, 1, 1, 1)'),
).toThrowErrorMatchingSnapshot();
});

it('should accept a valid translate', () => {
processTransform([{translate: [1, 1]}]);
processTransform('translate(1px)');
processTransform('translate(1px, 1px)');
processTransform([{translate: [1, 1, 1]}]);
});

Expand All @@ -73,6 +92,9 @@ describe('processTransform', () => {
expect(() =>
processTransform([{translate: [1, 1, 1, 1]}]),
).toThrowErrorMatchingSnapshot();
expect(() =>
processTransform('translate(1px, 1px, 1px, 1px)'),
).toThrowErrorMatchingSnapshot();
});

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

it('should accept an angle in degrees or radians', () => {
processTransform([{skewY: '10deg'}]);
processTransform('skewY(10deg)');
processTransform([{rotateX: '1.16rad'}]);
processTransform('rotateX(1.16rad)');
});

it('should throw when passing an invalid angle prop', () => {
expect(() =>
processTransform([{rotate: 10}]),
).toThrowErrorMatchingSnapshot();
expect(() =>
processTransform('rotate(10)'),
).toThrowErrorMatchingSnapshot();
expect(() =>
processTransform([{skewX: '10drg'}]),
).toThrowErrorMatchingSnapshot();
expect(() =>
processTransform('skewX(10drg)'),
).toThrowErrorMatchingSnapshot();
});

it('should throw when passing an Animated.Value', () => {
Expand Down
48 changes: 25 additions & 23 deletions Libraries/StyleSheet/private/_TransformStyle.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,29 @@ export type ____TransformStyle_Internal = $ReadOnly<{|
*
* `transform([{ skewX: '45deg' }])`
*/
transform?: $ReadOnlyArray<
| {|+perspective: number | AnimatedNode|}
| {|+rotate: string | AnimatedNode|}
| {|+rotateX: string | AnimatedNode|}
| {|+rotateY: string | AnimatedNode|}
| {|+rotateZ: string | AnimatedNode|}
| {|+scale: number | AnimatedNode|}
| {|+scaleX: number | AnimatedNode|}
| {|+scaleY: number | AnimatedNode|}
| {|+translateX: number | AnimatedNode|}
| {|+translateY: number | AnimatedNode|}
| {|
+translate:
| [number | AnimatedNode, number | AnimatedNode]
| AnimatedNode,
|}
| {|+skewX: string|}
| {|+skewY: string|}
// TODO: what is the actual type it expects?
| {|
+matrix: $ReadOnlyArray<number | AnimatedNode> | AnimatedNode,
|},
>,
transform?:
| $ReadOnlyArray<
| {|+perspective: number | AnimatedNode|}
| {|+rotate: string | AnimatedNode|}
| {|+rotateX: string | AnimatedNode|}
| {|+rotateY: string | AnimatedNode|}
| {|+rotateZ: string | AnimatedNode|}
| {|+scale: number | AnimatedNode|}
| {|+scaleX: number | AnimatedNode|}
| {|+scaleY: number | AnimatedNode|}
| {|+translateX: number | AnimatedNode|}
| {|+translateY: number | AnimatedNode|}
| {|
+translate:
| [number | AnimatedNode, number | AnimatedNode]
| AnimatedNode,
|}
| {|+skewX: string|}
| {|+skewY: string|}
// TODO: what is the actual type it expects?
| {|
+matrix: $ReadOnlyArray<number | AnimatedNode> | AnimatedNode,
|},
>
| string,
|}>;
118 changes: 117 additions & 1 deletion Libraries/StyleSheet/processTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,131 @@ const stringifySafe = require('../Utilities/stringifySafe').default;
* interface to native code.
*/
function processTransform(
transform: Array<Object>,
transform: Array<Object> | string,
): Array<Object> | Array<number> {
if (typeof transform === 'string') {
const regex = new RegExp(/(\w+)\(([^)]+)\)/g);
let transformArray: Array<Object> = [];
let matches;

while ((matches = regex.exec(transform))) {
const {key, value} = _getKeyAndValueFromCSSTransform(
matches[1],
matches[2],
);

if (value !== undefined) {
transformArray.push({[key]: value});
}
}
transform = transformArray;
}

if (__DEV__) {
_validateTransforms(transform);
}

return transform;
}

const _getKeyAndValueFromCSSTransform: (
key:
| string
| $TEMPORARY$string<'matrix'>
| $TEMPORARY$string<'perspective'>
| $TEMPORARY$string<'rotate'>
| $TEMPORARY$string<'rotateX'>
| $TEMPORARY$string<'rotateY'>
| $TEMPORARY$string<'rotateZ'>
| $TEMPORARY$string<'scale'>
| $TEMPORARY$string<'scaleX'>
| $TEMPORARY$string<'scaleY'>
| $TEMPORARY$string<'skewX'>
| $TEMPORARY$string<'skewY'>
| $TEMPORARY$string<'translate'>
| $TEMPORARY$string<'translate3d'>
| $TEMPORARY$string<'translateX'>
| $TEMPORARY$string<'translateY'>,
args: string,
) => {key: string, value?: number[] | number | string} = (key, args) => {
const argsWithUnitsRegex = new RegExp(/([+-]?\d+(\.\d+)?)([a-zA-Z]+)?/g);

switch (key) {
case 'matrix':
return {key, value: args.match(/[+-]?\d+(\.\d+)?/g)?.map(Number)};
case 'translate':
case 'translate3d':
const parsedArgs = [];
let missingUnitOfMeasurement = false;

let matches;
while ((matches = argsWithUnitsRegex.exec(args))) {
const value = Number(matches[1]);
const unitOfMeasurement = matches[3];

if (value !== 0 && !unitOfMeasurement) {
missingUnitOfMeasurement = true;
}

parsedArgs.push(value);
}

if (__DEV__) {
invariant(
!missingUnitOfMeasurement,
`Transform with key ${key} must have units unless the provided value is 0, found %s`,
`${key}(${args})`,
);

if (key === 'translate') {
invariant(
parsedArgs?.length === 1 || parsedArgs?.length === 2,
'Transform with key translate must be an string with 1 or 2 parameters, found %s: %s',
parsedArgs?.length,
`${key}(${args})`,
);
} else {
invariant(
parsedArgs?.length === 3,
'Transform with key translate3d must be an string with 3 parameters, found %s: %s',
parsedArgs?.length,
`${key}(${args})`,
);
}
}

if (parsedArgs?.length === 1) {
parsedArgs.push(0);
}

return {key: 'translate', value: parsedArgs};
case 'translateX':
case 'translateY':
case 'perspective':
const argMatches = argsWithUnitsRegex.exec(args);

if (!argMatches?.length) {
return {key, value: undefined};
}

const value = Number(argMatches[1]);
const unitOfMeasurement = argMatches[3];

if (__DEV__) {
invariant(
value === 0 || unitOfMeasurement,
`Transform with key ${key} must have units unless the provided value is 0, found %s`,
`${key}(${args})`,
);
}

return {key, value};

default:
return {key, value: !isNaN(args) ? Number(args) : args};
}
};

function _validateTransforms(transform: Array<Object>): void {
transform.forEach(transformation => {
const keys = Object.keys(transformation);
Expand Down
22 changes: 22 additions & 0 deletions packages/rn-tester/js/examples/Transform/TransformExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,17 @@ const styles = StyleSheet.create({
backgroundColor: 'salmon',
alignSelf: 'center',
},
box7: {
backgroundColor: 'lightseagreen',
height: 50,
position: 'absolute',
right: 0,
top: 0,
width: 50,
},
box7Transform: {
transform: 'translate(-50, 35) rotate(50deg) scale(2)',
},
flipCardContainer: {
marginVertical: 40,
flex: 1,
Expand Down Expand Up @@ -324,4 +335,15 @@ exports.examples = [
return <AnimateTransformSingleProp />;
},
},
{
title: 'Transform using a string',
description: "transform: 'translate(-50, 35) rotate(50deg) scale(2)'",
render(): Node {
return (
<View style={styles.container}>
<View style={[styles.box7, styles.box7Transform]} />
</View>
);
},
},
];

0 comments on commit 34db2d4

Please sign in to comment.