From adffc115b0f289d148f33f96d148116f00bc5bb4 Mon Sep 17 00:00:00 2001 From: Thibault Malbranche Date: Mon, 10 Aug 2026 12:17:04 +0200 Subject: [PATCH] Fix radial-gradient position being dropped after an explicit size In processBackgroundImage, the explicit-size branch shifts the next token to look for a second size value and discards it when it is not a length or percentage. When that token is 'at', the whole position clause is lost: the position defaults back to center and the position values are then re-parsed as a new size, silently overriding the declared one. radial-gradient(circle 100px at 25% 75%, red, blue) previously parsed as size {x: '25%', y: '75%'} with position {top: '50%', left: '50%'}; it now parses as size {x: 100, y: 100} with position {left: '25%', top: '75%'}. The existing test for this syntax only used 'at center', which is indistinguishable from the default position, so the bug was invisible. Co-Authored-By: Claude Fable 5 --- .../__tests__/processBackgroundImage-itest.js | 16 ++++++++++++++++ .../StyleSheet/processBackgroundImage.js | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js index 08a0ca0b9700..2614d334dd9a 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js @@ -999,6 +999,22 @@ describe('processBackgroundImage', () => { expect(result[0].type).toEqual('radial-gradient'); }); + it('should handle radial gradient with explicit size and position', () => { + const input = 'radial-gradient(circle 100px at 25% 75%, red, blue)'; + const result = processBackgroundImage(input); + expect(result[0].shape).toEqual('circle'); + expect(result[0].size).toEqual({x: 100, y: 100}); + expect(result[0].position).toEqual({left: '25%', top: '75%'}); + }); + + it('should handle radial gradient with two explicit sizes and position', () => { + const input = 'radial-gradient(50px 100px at left bottom, red, blue)'; + const result = processBackgroundImage(input); + expect(result[0].shape).toEqual('ellipse'); + expect(result[0].size).toEqual({x: 50, y: 100}); + expect(result[0].position).toEqual({left: '0%', top: '100%'}); + }); + // 1. position syntax: [ left | center | right | top | bottom | ] it('should handle radial gradient position length syntax', () => { const input = 'radial-gradient(circle at 20px, red, blue)'; diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js index f733c96c9645..421884005bbe 100644 --- a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js @@ -348,6 +348,11 @@ function parseRadialGradientCSSString( size = {x: sizeX, y: sizeY}; } else { hasExplicitSingleSize = true; + // The token after the size is not a second size value (e.g. 'at' or a + // shape keyword). Put it back so the loop can process it, otherwise + // the position would be silently dropped and its values re-parsed as + // a new size. + firstPartTokens.unshift(token); } } else if (tokenTrimmed === 'at') { let top: string | number;