diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js index 4e3ebfbf8425..1a1bba6506df 100644 --- a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js +++ b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js @@ -197,6 +197,31 @@ describe('', () => { ).toEqual(); }); + it('resolves percentages in string translateX and translateY against the view size', () => { + const root = Fantom.createRoot(); + + const viewRef = createRef(); + Fantom.runTask(() => { + root.render( + , + ); + }); + + const viewBounds = nullthrows( + viewRef.current, + ).getBoundingClientRect(); + // 10% of the 200 width and 20% of the 50 height. + expect(viewBounds.x).toBe(20); + expect(viewBounds.y).toBe(10); + }); + [ [undefined, {x: -5, y: 0, width: 20, height: 10}], ['50% 50%', {x: -5, y: 0, width: 20, height: 10}], diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-itest.js b/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-itest.js index 83f94d7a3cd5..2f9ae5960ec9 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-itest.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-itest.js @@ -148,4 +148,35 @@ describe('processTransform', () => { ); }); }); + + describe('string parsing', () => { + it('should keep the percentage unit for translateX and translateY', () => { + expect(processTransform('translateX(10%)')).toEqual([ + {translateX: '10%'}, + ]); + expect(processTransform('translateY(-25.5%)')).toEqual([ + {translateY: '-25.5%'}, + ]); + expect(processTransform('translateX(10px)')).toEqual([{translateX: 10}]); + }); + + it('should keep the percentage unit for translate', () => { + expect(processTransform('translate(10%, 20px)')).toEqual([ + {translate: ['10%', 20]}, + ]); + }); + + it('should parse numbers without a leading zero', () => { + expect(processTransform('translateX(.5px)')).toEqual([{translateX: 0.5}]); + expect(processTransform('translateY(-.25px)')).toEqual([ + {translateY: -0.25}, + ]); + expect(processTransform('translate(.5px, .25px)')).toEqual([ + {translate: [0.5, 0.25]}, + ]); + expect(processTransform('matrix(.5, 0, 0, .5, 10, 20, 0, 0, 1)')).toEqual( + [{matrix: [0.5, 0, 0, 0.5, 10, 20, 0, 0, 1]}], + ); + }); + }); }); diff --git a/packages/react-native/Libraries/StyleSheet/processTransform.js b/packages/react-native/Libraries/StyleSheet/processTransform.js index d9085c418e2e..6715d05c9d17 100644 --- a/packages/react-native/Libraries/StyleSheet/processTransform.js +++ b/packages/react-native/Libraries/StyleSheet/processTransform.js @@ -60,11 +60,16 @@ const _getKeyAndValueFromCSSTransform: ( key, args, ) => { - const argsWithUnitsRegex = new RegExp(/([+-]?\d+(\.\d+)?)([a-zA-Z]+|%)?/g); + const argsWithUnitsRegex = new RegExp( + /([+-]?(?:\d+(?:\.\d*)?|\.\d+))([a-zA-Z]+|%)?/g, + ); switch (key) { case 'matrix': - return {key, value: args.match(/[+-]?\d+(\.\d+)?/g)?.map(Number)}; + return { + key, + value: args.match(/[+-]?(?:\d+(?:\.\d*)?|\.\d+)/g)?.map(Number), + }; case 'translate': case 'translate3d': const parsedArgs = []; @@ -73,7 +78,7 @@ const _getKeyAndValueFromCSSTransform: ( let matches; while ((matches = argsWithUnitsRegex.exec(args))) { const value = Number(matches[1]); - const unitOfMeasurement = matches[3]; + const unitOfMeasurement = matches[2]; if (value !== 0 && !unitOfMeasurement) { missingUnitOfMeasurement = true; @@ -125,7 +130,7 @@ const _getKeyAndValueFromCSSTransform: ( } const value = Number(argMatches[1]); - const unitOfMeasurement = argMatches[3]; + const unitOfMeasurement = argMatches[2]; if (__DEV__) { invariant( @@ -135,7 +140,16 @@ const _getKeyAndValueFromCSSTransform: ( ); } - return {key, value}; + // The percentage unit is meaningful for translations, so keep it in the + // value instead of dropping it. A bare number is interpreted as points. + return { + key, + value: + unitOfMeasurement === '%' && + (key === 'translateX' || key === 'translateY') + ? `${value}%` + : value, + }; default: return {key, value: !isNaN(args) ? Number(args) : args}; diff --git a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSTransformTest.cpp b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSTransformTest.cpp index 2b351ba938ff..e5b96f017aa1 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSTransformTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSTransformTest.cpp @@ -24,6 +24,16 @@ TEST(CSSTransform, matrix_basic) { EXPECT_EQ(matrix.values[5], 6.0f); } +TEST(CSSTransform, matrix_leading_dot) { + auto val = + parseCSSProperty("matrix(.5, 0, 0, -.5, 10, 20)"); + EXPECT_TRUE(std::holds_alternative(val)); + auto& matrix = std::get(val); + + EXPECT_EQ(matrix.values[0], 0.5f); + EXPECT_EQ(matrix.values[3], -0.5f); +} + TEST(CSSTransform, matrix_funky) { auto val = parseCSSProperty("mAtRiX( 1 , \n2,3, 4, \t5, 6)"); @@ -181,6 +191,16 @@ TEST(CSSTransform, translate_x_pct) { EXPECT_EQ(std::get(translate.value).value, 420.0f); } +TEST(CSSTransform, translate_x_leading_dot) { + auto val = parseCSSProperty("translateX(.5px)"); + EXPECT_TRUE(std::holds_alternative(val)); + auto& translate = std::get(val); + + EXPECT_TRUE(std::holds_alternative(translate.value)); + EXPECT_EQ(std::get(translate.value).value, 0.5f); + EXPECT_EQ(std::get(translate.value).unit, CSSLengthUnit::Px); +} + TEST(CSSTransform, translate_x_funky) { auto val = parseCSSProperty("transLaTeX(420%)"); EXPECT_TRUE(std::holds_alternative(val)); @@ -224,6 +244,15 @@ TEST(CSSTransform, translate_y_pct) { EXPECT_EQ(std::get(translate.value).value, 420.0f); } +TEST(CSSTransform, translate_y_leading_dot) { + auto val = parseCSSProperty("translateY(-.25%)"); + EXPECT_TRUE(std::holds_alternative(val)); + auto& translate = std::get(val); + + EXPECT_TRUE(std::holds_alternative(translate.value)); + EXPECT_EQ(std::get(translate.value).value, -0.25f); +} + TEST(CSSTransform, translate_y_funky) { auto val = parseCSSProperty("transLaTeY(420%)"); EXPECT_TRUE(std::holds_alternative(val));