Skip to content

Commit f65c873

Browse files
linear gradient transition hint syntax
1 parent 85bdd75 commit f65c873

File tree

8 files changed

+420
-63
lines changed

8 files changed

+420
-63
lines changed

packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ export type GradientValue = {
372372
// Angle or direction enums
373373
direction?: string | undefined;
374374
colorStops: ReadonlyArray<{
375-
color: ColorValue;
375+
color: ColorValue | null;
376376
positions?: ReadonlyArray<string[]> | undefined;
377377
}>;
378378
};

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,4 +697,34 @@ describe('processBackgroundImage', () => {
697697
});
698698
}
699699
});
700+
701+
it('should process color transition hint in object style', () => {
702+
const input = [
703+
{
704+
type: 'linearGradient',
705+
direction: 'To Bottom',
706+
colorStops: [{color: 'red'}, {positions: ['20%']}, {color: 'blue'}],
707+
},
708+
];
709+
const result = processBackgroundImage(input);
710+
expect(result[0].type).toBe('linearGradient');
711+
expect(result[0].direction).toEqual({type: 'angle', value: 180});
712+
expect(result[0].colorStops).toEqual([
713+
{color: processColor('red'), position: 0},
714+
{color: null, position: 0.2},
715+
{color: processColor('blue'), position: 1},
716+
]);
717+
});
718+
719+
it('should process color transition hint', () => {
720+
const input = 'linear-gradient(red, 40%, blue)';
721+
const result = processBackgroundImage(input);
722+
expect(result[0].type).toBe('linearGradient');
723+
expect(result[0].direction).toEqual({type: 'angle', value: 180});
724+
expect(result[0].colorStops).toEqual([
725+
{color: processColor('red'), position: 0},
726+
{color: null, position: 0.4},
727+
{color: processColor('blue'), position: 1},
728+
]);
729+
});
700730
});

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

Lines changed: 104 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ type LinearGradientDirection =
2222
| {type: 'angle', value: number}
2323
| {type: 'keyword', value: string};
2424

25+
type ColorStopColor = ProcessedColorValue | null;
26+
2527
type ParsedGradientValue = {
2628
type: 'linearGradient',
2729
direction: LinearGradientDirection,
2830
colorStops: $ReadOnlyArray<{
29-
color: ProcessedColorValue,
31+
color: ColorStopColor,
3032
position: number,
3133
}>,
3234
};
@@ -49,33 +51,52 @@ export default function processBackgroundImage(
4951
} else if (Array.isArray(backgroundImage)) {
5052
for (const bgImage of backgroundImage) {
5153
const processedColorStops: Array<{
52-
color: ProcessedColorValue,
54+
color: ColorStopColor,
5355
position: number | null,
5456
}> = [];
5557
for (let index = 0; index < bgImage.colorStops.length; index++) {
5658
const colorStop = bgImage.colorStops[index];
57-
const processedColor = processColor(colorStop.color);
58-
if (processedColor == null) {
59-
// If a color is invalid, return an empty array and do not apply gradient. Same as web.
60-
return [];
61-
}
62-
if (colorStop.positions != null && colorStop.positions.length > 0) {
63-
for (const position of colorStop.positions) {
64-
if (position.endsWith('%')) {
65-
processedColorStops.push({
66-
color: processedColor,
67-
position: parseFloat(position) / 100,
68-
});
69-
} else {
70-
// If a position is invalid, return an empty array and do not apply gradient. Same as web.
71-
return [];
72-
}
59+
const positions = colorStop.positions;
60+
// Color transition hint syntax (red, 20%, blue)
61+
if (
62+
colorStop.color == null &&
63+
Array.isArray(positions) &&
64+
positions.length === 1
65+
) {
66+
const position = positions[0];
67+
if (typeof position === 'string' && position.endsWith('%')) {
68+
processedColorStops.push({
69+
color: null,
70+
position: parseFloat(position) / 100,
71+
});
72+
} else {
73+
// If a position is invalid, return an empty array and do not apply gradient. Same as web.
74+
return [];
7375
}
7476
} else {
75-
processedColorStops.push({
76-
color: processedColor,
77-
position: null,
78-
});
77+
const processedColor = processColor(colorStop.color);
78+
if (processedColor == null) {
79+
// If a color is invalid, return an empty array and do not apply gradient. Same as web.
80+
return [];
81+
}
82+
if (positions != null && positions.length > 0) {
83+
for (const position of positions) {
84+
if (position.endsWith('%')) {
85+
processedColorStops.push({
86+
color: processedColor,
87+
position: parseFloat(position) / 100,
88+
});
89+
} else {
90+
// If a position is invalid, return an empty array and do not apply gradient. Same as web.
91+
return [];
92+
}
93+
}
94+
} else {
95+
processedColorStops.push({
96+
color: processedColor,
97+
position: null,
98+
});
99+
}
79100
}
80101
}
81102

@@ -169,47 +190,85 @@ function parseCSSLinearGradient(
169190
// If first part is not an angle/direction or a color stop, return an empty array and do not apply any gradient. Same as web.
170191
return [];
171192
}
172-
colorStopRegex.lastIndex = 0;
173193

194+
const colorStopsString = parts.join(',');
174195
const colorStops = [];
175-
const fullColorStopsStr = parts.join(',');
176-
let colorStopMatch;
177-
while ((colorStopMatch = colorStopRegex.exec(fullColorStopsStr))) {
178-
const [, color, position1, position2] = colorStopMatch;
179-
const processedColor = processColor(color.trim().toLowerCase());
180-
if (processedColor == null) {
181-
// If a color is invalid, return an empty array and do not apply any gradient. Same as web.
196+
// split by comma, but not if it's inside a parentheses. e.g. red, rgba(0, 0, 0, 0.5), green => ["red", "rgba(0, 0, 0, 0.5)", "green"]
197+
const stops = colorStopsString.split(/,(?![^(]*\))/);
198+
for (const stop of stops) {
199+
const trimmedStop = stop.trim().toLowerCase();
200+
// Match function like pattern or single words
201+
const colorStopParts = trimmedStop.match(/\S+\([^)]*\)|\S+/g);
202+
if (colorStopParts == null) {
203+
// If a color stop is invalid, return an empty array and do not apply any gradient. Same as web.
182204
return [];
183205
}
184-
185-
if (typeof position1 !== 'undefined') {
186-
if (position1.endsWith('%')) {
206+
// Case 1: [color, position, position]
207+
if (colorStopParts.length === 3) {
208+
const color = colorStopParts[0];
209+
const position1 = colorStopParts[1];
210+
const position2 = colorStopParts[2];
211+
const processedColor = processColor(color);
212+
if (processedColor == null) {
213+
// If a color is invalid, return an empty array and do not apply any gradient. Same as web.
214+
return [];
215+
}
216+
if (position1.endsWith('%') && position2.endsWith('%')) {
187217
colorStops.push({
188218
color: processedColor,
189219
position: parseFloat(position1) / 100,
190220
});
221+
colorStops.push({
222+
color: processedColor,
223+
position: parseFloat(position2) / 100,
224+
});
191225
} else {
192226
// If a position is invalid, return an empty array and do not apply any gradient. Same as web.
193227
return [];
194228
}
195-
} else {
196-
colorStops.push({
197-
color: processedColor,
198-
position: null,
199-
});
200229
}
201-
202-
if (typeof position2 !== 'undefined') {
203-
if (position2.endsWith('%')) {
230+
// Case 2: [color, position]
231+
else if (colorStopParts.length === 2) {
232+
const color = colorStopParts[0];
233+
const position = colorStopParts[1];
234+
const processedColor = processColor(color);
235+
if (processedColor == null) {
236+
// If a color is invalid, return an empty array and do not apply any gradient. Same as web.
237+
return [];
238+
}
239+
if (position.endsWith('%')) {
204240
colorStops.push({
205241
color: processedColor,
206-
position: parseFloat(position2) / 100,
242+
position: parseFloat(position) / 100,
207243
});
208244
} else {
209245
// If a position is invalid, return an empty array and do not apply any gradient. Same as web.
210246
return [];
211247
}
212248
}
249+
// Case 3: [color]
250+
// Case 4: [position] => transition hint syntax
251+
else if (colorStopParts.length === 1) {
252+
if (colorStopParts[0].endsWith('%')) {
253+
colorStops.push({
254+
color: null,
255+
position: parseFloat(colorStopParts[0]) / 100,
256+
});
257+
} else {
258+
const processedColor = processColor(colorStopParts[0]);
259+
if (processedColor == null) {
260+
// If a color is invalid, return an empty array and do not apply any gradient. Same as web.
261+
return [];
262+
}
263+
colorStops.push({
264+
color: processedColor,
265+
position: null,
266+
});
267+
}
268+
} else {
269+
// If a color stop is invalid, return an empty array and do not apply any gradient. Same as web.
270+
return [];
271+
}
213272
}
214273

215274
const fixedColorStops = getFixedColorStops(colorStops);
@@ -286,15 +345,15 @@ function getAngleInDegrees(angle?: string): ?number {
286345
// https://drafts.csswg.org/css-images-4/#color-stop-fixup
287346
function getFixedColorStops(
288347
colorStops: $ReadOnlyArray<{
289-
color: ProcessedColorValue,
348+
color: ColorStopColor,
290349
position: number | null,
291350
}>,
292351
): Array<{
293-
color: ProcessedColorValue,
352+
color: ColorStopColor,
294353
position: number,
295354
}> {
296355
let fixedColorStops: Array<{
297-
color: ProcessedColorValue,
356+
color: ColorStopColor,
298357
position: number,
299358
}> = [];
300359
let hasNullPositions = false;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8375,11 +8375,12 @@ exports[`public API should not change unintentionally Libraries/StyleSheet/proce
83758375
"type LinearGradientDirection =
83768376
| { type: \\"angle\\", value: number }
83778377
| { type: \\"keyword\\", value: string };
8378+
type ColorStopColor = ProcessedColorValue | null;
83788379
type ParsedGradientValue = {
83798380
type: \\"linearGradient\\",
83808381
direction: LinearGradientDirection,
83818382
colorStops: $ReadOnlyArray<{
8382-
color: ProcessedColorValue,
8383+
color: ColorStopColor,
83838384
position: number,
83848385
}>,
83858386
};

0 commit comments

Comments
 (0)