Skip to content

Commit 3c443c3

Browse files
Linear gradient with px (#3)
* linear gradient px support * fix test cases * handle invalid transition hint * fix multiple transition hint * final fixes
1 parent 6913cdb commit 3c443c3

File tree

8 files changed

+659
-414
lines changed

8 files changed

+659
-414
lines changed

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

Lines changed: 167 additions & 123 deletions
Large diffs are not rendered by default.

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

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

25-
// null color stops indicate that the transition hint syntax is used. e.g. red, 20%, blue
25+
// null color indicate that the transition hint syntax is used. e.g. red, 20%, blue
2626
type ColorStopColor = ProcessedColorValue | null;
27+
// percentage or pixel value
28+
type ColorStopPosition = number | string | null;
2729

2830
type ParsedGradientValue = {
2931
type: 'linearGradient',
3032
direction: LinearGradientDirection,
3133
colorStops: $ReadOnlyArray<{
3234
color: ColorStopColor,
33-
position: number,
35+
position: ColorStopPosition,
3436
}>,
3537
};
3638

@@ -53,7 +55,7 @@ export default function processBackgroundImage(
5355
for (const bgImage of backgroundImage) {
5456
const processedColorStops: Array<{
5557
color: ColorStopColor,
56-
position: number | null,
58+
position: ColorStopPosition,
5759
}> = [];
5860
for (let index = 0; index < bgImage.colorStops.length; index++) {
5961
const colorStop = bgImage.colorStops[index];
@@ -65,10 +67,13 @@ export default function processBackgroundImage(
6567
positions.length === 1
6668
) {
6769
const position = positions[0];
68-
if (typeof position === 'string' && position.endsWith('%')) {
70+
if (
71+
typeof position === 'number' ||
72+
(typeof position === 'string' && position.endsWith('%'))
73+
) {
6974
processedColorStops.push({
7075
color: null,
71-
position: parseFloat(position) / 100,
76+
position,
7277
});
7378
} else {
7479
// If a position is invalid, return an empty array and do not apply gradient. Same as web.
@@ -82,10 +87,13 @@ export default function processBackgroundImage(
8287
}
8388
if (positions != null && positions.length > 0) {
8489
for (const position of positions) {
85-
if (position.endsWith('%')) {
90+
if (
91+
typeof position === 'number' ||
92+
(typeof position === 'string' && position.endsWith('%'))
93+
) {
8694
processedColorStops.push({
8795
color: processedColor,
88-
position: parseFloat(position) / 100,
96+
position,
8997
});
9098
} else {
9199
// If a position is invalid, return an empty array and do not apply gradient. Same as web.
@@ -131,12 +139,10 @@ export default function processBackgroundImage(
131139
}
132140
}
133141

134-
const fixedColorStops = getFixedColorStops(processedColorStops);
135-
136142
result = result.concat({
137143
type: 'linearGradient',
138144
direction,
139-
colorStops: fixedColorStops,
145+
colorStops: processedColorStops,
140146
});
141147
}
142148
}
@@ -186,8 +192,9 @@ function parseCSSLinearGradient(
186192
const colorStops = [];
187193
// 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"]
188194
const stops = colorStopsString.split(/,(?![^(]*\))/);
189-
let lastStop = null;
190-
for (const stop of stops) {
195+
let prevStop = null;
196+
for (let i = 0; i < stops.length; i++) {
197+
const stop = stops[i];
191198
const trimmedStop = stop.trim().toLowerCase();
192199
// Match function like pattern or single words
193200
const colorStopParts = trimmedStop.match(/\S+\([^)]*\)|\S+/g);
@@ -198,61 +205,65 @@ function parseCSSLinearGradient(
198205
// Case 1: [color, position, position]
199206
if (colorStopParts.length === 3) {
200207
const color = colorStopParts[0];
201-
const position1 = colorStopParts[1];
202-
const position2 = colorStopParts[2];
208+
const position1 = getPositionFromCSSValue(colorStopParts[1]);
209+
const position2 = getPositionFromCSSValue(colorStopParts[2]);
203210
const processedColor = processColor(color);
204211
if (processedColor == null) {
205212
// If a color is invalid, return an empty array and do not apply any gradient. Same as web.
206213
return [];
207214
}
208-
if (position1.endsWith('%') && position2.endsWith('%')) {
209-
colorStops.push({
210-
color: processedColor,
211-
position: parseFloat(position1) / 100,
212-
});
213-
colorStops.push({
214-
color: processedColor,
215-
position: parseFloat(position2) / 100,
216-
});
217-
} else {
215+
216+
if (position1 == null || position2 == null) {
218217
// If a position is invalid, return an empty array and do not apply any gradient. Same as web.
219218
return [];
220219
}
220+
221+
colorStops.push({
222+
color: processedColor,
223+
position: position1,
224+
});
225+
colorStops.push({
226+
color: processedColor,
227+
position: position2,
228+
});
221229
}
222230
// Case 2: [color, position]
223231
else if (colorStopParts.length === 2) {
224232
const color = colorStopParts[0];
225-
const position = colorStopParts[1];
233+
const position = getPositionFromCSSValue(colorStopParts[1]);
226234
const processedColor = processColor(color);
227235
if (processedColor == null) {
228236
// If a color is invalid, return an empty array and do not apply any gradient. Same as web.
229237
return [];
230238
}
231-
if (position.endsWith('%')) {
232-
colorStops.push({
233-
color: processedColor,
234-
position: parseFloat(position) / 100,
235-
});
236-
} else {
239+
if (position == null) {
237240
// If a position is invalid, return an empty array and do not apply any gradient. Same as web.
238241
return [];
239242
}
243+
colorStops.push({
244+
color: processedColor,
245+
position,
246+
});
240247
}
241248
// Case 3: [color]
242249
// Case 4: [position] => transition hint syntax
243250
else if (colorStopParts.length === 1) {
244-
if (colorStopParts[0].endsWith('%')) {
251+
const position = getPositionFromCSSValue(colorStopParts[0]);
252+
if (position != null) {
253+
// handle invalid transition hint syntax. transition hint syntax must have color before and after the position. e.g. red, 20%, blue
245254
if (
246-
lastStop != null &&
247-
lastStop.length === 1 &&
248-
lastStop[0].endsWith('%')
255+
(prevStop != null &&
256+
prevStop.length === 1 &&
257+
getPositionFromCSSValue(prevStop[0]) != null) ||
258+
i === stops.length - 1 ||
259+
i === 0
249260
) {
250261
// If the last stop is a transition hint syntax, return an empty array and do not apply any gradient. Same as web.
251262
return [];
252263
}
253264
colorStops.push({
254265
color: null,
255-
position: parseFloat(colorStopParts[0]) / 100,
266+
position,
256267
});
257268
} else {
258269
const processedColor = processColor(colorStopParts[0]);
@@ -269,15 +280,13 @@ function parseCSSLinearGradient(
269280
// If a color stop is invalid, return an empty array and do not apply any gradient. Same as web.
270281
return [];
271282
}
272-
lastStop = colorStopParts;
283+
prevStop = colorStopParts;
273284
}
274285

275-
const fixedColorStops = getFixedColorStops(colorStops);
276-
277286
gradients.push({
278287
type: 'linearGradient',
279288
direction,
280-
colorStops: fixedColorStops,
289+
colorStops,
281290
});
282291
}
283292

@@ -343,79 +352,12 @@ function getAngleInDegrees(angle?: string): ?number {
343352
}
344353
}
345354

346-
// https://drafts.csswg.org/css-images-4/#color-stop-fixup
347-
function getFixedColorStops(
348-
colorStops: $ReadOnlyArray<{
349-
color: ColorStopColor,
350-
position: number | null,
351-
}>,
352-
): Array<{
353-
color: ColorStopColor,
354-
position: number,
355-
}> {
356-
let fixedColorStops: Array<{
357-
color: ColorStopColor,
358-
position: number,
359-
}> = [];
360-
let hasNullPositions = false;
361-
let maxPositionSoFar = colorStops[0].position ?? 0;
362-
for (let i = 0; i < colorStops.length; i++) {
363-
const colorStop = colorStops[i];
364-
let newPosition = colorStop.position;
365-
if (newPosition === null) {
366-
// Step 1:
367-
// If the first color stop does not have a position,
368-
// set its position to 0%. If the last color stop does not have a position,
369-
// set its position to 100%.
370-
if (i === 0) {
371-
newPosition = 0;
372-
} else if (i === colorStops.length - 1) {
373-
newPosition = 1;
374-
}
375-
}
376-
// Step 2:
377-
// If a color stop or transition hint has a position
378-
// that is less than the specified position of any color stop or transition hint
379-
// before it in the list, set its position to be equal to the
380-
// largest specified position of any color stop or transition hint before it.
381-
if (newPosition !== null) {
382-
newPosition = Math.max(newPosition, maxPositionSoFar);
383-
fixedColorStops[i] = {
384-
color: colorStop.color,
385-
position: newPosition,
386-
};
387-
maxPositionSoFar = newPosition;
388-
} else {
389-
hasNullPositions = true;
390-
}
355+
function getPositionFromCSSValue(position: string) {
356+
if (position.endsWith('px')) {
357+
return parseFloat(position);
391358
}
392359

393-
// Step 3:
394-
// If any color stop still does not have a position,
395-
// then, for each run of adjacent color stops without positions,
396-
// set their positions so that they are evenly spaced between the preceding and
397-
// following color stops with positions.
398-
if (hasNullPositions) {
399-
let lastDefinedIndex = 0;
400-
for (let i = 1; i < fixedColorStops.length; i++) {
401-
if (fixedColorStops[i] !== undefined) {
402-
const unpositionedStops = i - lastDefinedIndex - 1;
403-
if (unpositionedStops > 0) {
404-
const startPosition = fixedColorStops[lastDefinedIndex].position;
405-
const endPosition = fixedColorStops[i].position;
406-
const increment =
407-
(endPosition - startPosition) / (unpositionedStops + 1);
408-
for (let j = 1; j <= unpositionedStops; j++) {
409-
fixedColorStops[lastDefinedIndex + j] = {
410-
color: colorStops[lastDefinedIndex + j].color,
411-
position: startPosition + increment * j,
412-
};
413-
}
414-
}
415-
lastDefinedIndex = i;
416-
}
417-
}
360+
if (position.endsWith('%')) {
361+
return position;
418362
}
419-
420-
return fixedColorStops;
421363
}

0 commit comments

Comments
 (0)