Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests for existing RN syntax include 3 args, but noticed that doesnt seem to be in the type def.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, actually I couldn't find any reference to the translate param in the official docs https://reactnative.dev/docs/next/transforms#transform, maybe it's something used internally?

| 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 <AnimateTansformSingleProp />;
},
},
{
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>
);
},
},
];