Skip to content

Commit 546c668

Browse files
negative values and invalid gradient syntax handling
1 parent 8876870 commit 546c668

File tree

5 files changed

+96
-94
lines changed

5 files changed

+96
-94
lines changed

packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,7 @@ type RadialExtent =
740740
| 'closest-side'
741741
| 'farthest-corner'
742742
| 'farthest-side';
743-
type RadialGradientPosition =
743+
export type RadialGradientPosition =
744744
| {
745745
top: number | string,
746746
left: number | string,
@@ -757,15 +757,19 @@ type RadialGradientPosition =
757757
bottom: number | string,
758758
right: number | string,
759759
};
760+
761+
export type RadialGradientShape = 'circle' | 'ellipse';
762+
export type RadialGradientSize =
763+
| RadialExtent
764+
| {
765+
x: string | number,
766+
y: string | number,
767+
};
768+
760769
type RadialGradientValue = {
761770
type: 'radialGradient',
762-
shape: 'circle' | 'ellipse',
763-
size:
764-
| RadialExtent
765-
| {
766-
x: number | string,
767-
y: number | string,
768-
},
771+
shape: RadialGradientShape,
772+
size: RadialGradientSize,
769773
position: RadialGradientPosition,
770774
colorStops: $ReadOnlyArray<{
771775
color: ____ColorValue_Internal,

packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,4 +1129,19 @@ describe('processBackgroundImage', () => {
11291129
{color: processColor('blue'), position: '100%'},
11301130
]);
11311131
});
1132+
1133+
it('should not process invalid gradient syntax', () => {
1134+
const input =
1135+
'aoeusntial-gradient(red 0%, yellow 30%, green 60%, blue 100%)';
1136+
const result = processBackgroundImage(input);
1137+
expect(result).toEqual([]);
1138+
});
1139+
1140+
it('should not process negative radius in radial gradient syntax', () => {
1141+
const input = 'radial-gradient(circle -100px, red, blue)';
1142+
const input1 = 'radial-gradient(ellipse 100px -40px, red, blue)';
1143+
const result = processBackgroundImage(input);
1144+
const result1 = processBackgroundImage(input1);
1145+
expect(result).toEqual([]);
1146+
});
11321147
});

packages/react-native/Libraries/StyleSheet/processBackgroundImage.js

Lines changed: 55 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
'use strict';
1212

1313
import type {ProcessedColorValue} from './processColor';
14-
import type {BackgroundImageValue} from './StyleSheetTypes';
14+
import type {
15+
BackgroundImageValue,
16+
RadialGradientPosition,
17+
RadialGradientSize,
18+
RadialGradientShape,
19+
} from './StyleSheetTypes';
1520
const processColor = require('./processColor').default;
1621

1722
// Linear Gradient
@@ -45,35 +50,7 @@ const DEFAULT_RADIAL_POSITION: RadialGradientPosition = {
4550
top: '50%',
4651
left: '50%',
4752
};
48-
type RadialExtent =
49-
| 'closest-corner'
50-
| 'closest-side'
51-
| 'farthest-corner'
52-
| 'farthest-side';
53-
type RadialGradientSize =
54-
| RadialExtent
55-
| {
56-
x: string | number,
57-
y: string | number,
58-
};
59-
type RadialGradientShape = 'circle' | 'ellipse';
60-
type RadialGradientPosition =
61-
| {
62-
top: number | string,
63-
left: number | string,
64-
}
65-
| {
66-
top: number | string,
67-
right: number | string,
68-
}
69-
| {
70-
bottom: number | string,
71-
right: number | string,
72-
}
73-
| {
74-
bottom: number | string,
75-
left: number | string,
76-
};
53+
7754
type RadialGradientBackgroundImage = {
7855
type: 'radialGradient',
7956
shape: RadialGradientShape,
@@ -273,24 +250,25 @@ function parseBackgroundImageCSSString(
273250
cssString: string,
274251
): $ReadOnlyArray<ParsedBackgroundImageValue> {
275252
const gradients = [];
276-
let match;
277-
// matches one or more linear-gradient or radial-gradient functions in CSS
278-
const gradientRegex =
279-
/(linear|radial)-gradient\s*\(((?:\([^)]*\)|[^()])*)\)/gi;
280-
281-
while ((match = gradientRegex.exec(cssString))) {
282-
const [, type, gradientContent] = match;
283-
const isRadial = type.toLowerCase() === 'radial';
284-
285-
const gradient = isRadial
286-
? parseRadialGradientCSSString(gradientContent)
287-
: parseLinearGradientCSSString(gradientContent);
288-
289-
if (gradient != null) {
290-
gradients.push(gradient);
253+
const bgImageStrings = splitGradients(cssString);
254+
255+
for (const bgImageString of bgImageStrings) {
256+
const bgImage = bgImageString.toLowerCase();
257+
const gradientRegex = /^(linear|radial)-gradient\(((?:\([^)]*\)|[^()])*)\)/;
258+
259+
const match = gradientRegex.exec(bgImage);
260+
if (match) {
261+
const [, type, gradientContent] = match;
262+
const isRadial = type.toLowerCase() === 'radial';
263+
const gradient = isRadial
264+
? parseRadialGradientCSSString(gradientContent)
265+
: parseLinearGradientCSSString(gradientContent);
266+
267+
if (gradient != null) {
268+
gradients.push(gradient);
269+
}
291270
}
292271
}
293-
294272
return gradients;
295273
}
296274

@@ -312,6 +290,7 @@ function parseRadialGradientCSSString(
312290
let hasExplicitShape = false;
313291
const firstPartTokens = firstPartStr.split(/\s+/);
314292

293+
// firstPartTokens is the shape, size, and position
315294
while (firstPartTokens.length > 0) {
316295
let token = firstPartTokens.shift();
317296
if (token == null) {
@@ -333,7 +312,7 @@ function parseRadialGradientCSSString(
333312
hasShapeSizeOrPositionString = true;
334313
} else if (tokenTrimmed.endsWith('px') || tokenTrimmed.endsWith('%')) {
335314
let sizeX = getPositionFromCSSValue(tokenTrimmed);
336-
if (sizeX == null) {
315+
if (sizeX == null || sizeX < 0) {
337316
// If a size is invalid, return null and do not apply any gradient. Same as web.
338317
return null;
339318
}
@@ -347,7 +326,7 @@ function parseRadialGradientCSSString(
347326
tokenTrimmed = token.toLowerCase().trim();
348327
if (tokenTrimmed.endsWith('px') || tokenTrimmed.endsWith('%')) {
349328
const sizeY = getPositionFromCSSValue(tokenTrimmed);
350-
if (sizeY == null) {
329+
if (sizeY == null || sizeY < 0) {
351330
// If a size is invalid, return null and do not apply any gradient. Same as web.
352331
return null;
353332
}
@@ -801,3 +780,31 @@ function getPositionFromCSSValue(position: string) {
801780
return position;
802781
}
803782
}
783+
784+
function splitGradients(input) {
785+
const result = [];
786+
let current = '';
787+
let depth = 0;
788+
789+
for (let i = 0; i < input.length; i++) {
790+
const char = input[i];
791+
792+
if (char === '(') {
793+
depth++;
794+
} else if (char === ')') {
795+
depth--;
796+
} else if (char === ',' && depth === 0) {
797+
result.push(current.trim());
798+
current = '';
799+
continue;
800+
}
801+
802+
current += char;
803+
}
804+
805+
if (current.trim() !== '') {
806+
result.push(current.trim());
807+
}
808+
809+
return result;
810+
}

packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap

Lines changed: 10 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7635,7 +7635,7 @@ type RadialExtent =
76357635
| \\"closest-side\\"
76367636
| \\"farthest-corner\\"
76377637
| \\"farthest-side\\";
7638-
type RadialGradientPosition =
7638+
export type RadialGradientPosition =
76397639
| {
76407640
top: number | string,
76417641
left: number | string,
@@ -7652,15 +7652,17 @@ type RadialGradientPosition =
76527652
bottom: number | string,
76537653
right: number | string,
76547654
};
7655+
export type RadialGradientShape = \\"circle\\" | \\"ellipse\\";
7656+
export type RadialGradientSize =
7657+
| RadialExtent
7658+
| {
7659+
x: string | number,
7660+
y: string | number,
7661+
};
76557662
type RadialGradientValue = {
76567663
type: \\"radialGradient\\",
7657-
shape: \\"circle\\" | \\"ellipse\\",
7658-
size:
7659-
| RadialExtent
7660-
| {
7661-
x: number | string,
7662-
y: number | string,
7663-
},
7664+
shape: RadialGradientShape,
7665+
size: RadialGradientSize,
76647666
position: RadialGradientPosition,
76657667
colorStops: $ReadOnlyArray<{
76667668
color: ____ColorValue_Internal,
@@ -8015,35 +8017,6 @@ type LinearGradientBackgroundImage = {
80158017
position: ColorStopPosition,
80168018
}>,
80178019
};
8018-
type RadialExtent =
8019-
| \\"closest-corner\\"
8020-
| \\"closest-side\\"
8021-
| \\"farthest-corner\\"
8022-
| \\"farthest-side\\";
8023-
type RadialGradientSize =
8024-
| RadialExtent
8025-
| {
8026-
x: string | number,
8027-
y: string | number,
8028-
};
8029-
type RadialGradientShape = \\"circle\\" | \\"ellipse\\";
8030-
type RadialGradientPosition =
8031-
| {
8032-
top: number | string,
8033-
left: number | string,
8034-
}
8035-
| {
8036-
top: number | string,
8037-
right: number | string,
8038-
}
8039-
| {
8040-
bottom: number | string,
8041-
right: number | string,
8042-
}
8043-
| {
8044-
bottom: number | string,
8045-
left: number | string,
8046-
};
80478020
type RadialGradientBackgroundImage = {
80488021
type: \\"radialGradient\\",
80498022
shape: RadialGradientShape,

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/RadialGradient.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,13 @@ internal class RadialGradient(
233233
}
234234
}
235235

236+
// max is used to handle 0 radius user input. Radius has to be a positive float
237+
val radius = max(radiusX, 0.00001f)
238+
236239
val shader = AndroidRadialGradient(
237240
centerX,
238241
centerY,
239-
radiusX,
242+
radius,
240243
colors,
241244
positions,
242245
Shader.TileMode.CLAMP

0 commit comments

Comments
 (0)