Skip to content

Commit

Permalink
Color transition hint
Browse files Browse the repository at this point in the history
  • Loading branch information
intergalacticspacehighway committed Dec 26, 2024
1 parent 974fdf9 commit e73db98
Show file tree
Hide file tree
Showing 7 changed files with 418 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ export type GradientValue = {
// Angle or direction enums
direction?: string | undefined;
colorStops: ReadonlyArray<{
color: ColorValue;
color: ColorValue | null;
positions?: ReadonlyArray<string[]> | undefined;
}>;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -697,4 +697,34 @@ describe('processBackgroundImage', () => {
});
}
});

it('should process color transition hint in object style', () => {
const input = [
{
type: 'linearGradient',
direction: 'To Bottom',
colorStops: [{color: 'red'}, {positions: ['20%']}, {color: 'blue'}],
},
];
const result = processBackgroundImage(input);
expect(result[0].type).toBe('linearGradient');
expect(result[0].direction).toEqual({type: 'angle', value: 180});
expect(result[0].colorStops).toEqual([
{color: processColor('red'), position: 0},
{color: null, position: 0.2},
{color: processColor('blue'), position: 1},
]);
});

it('should process color transition hint', () => {
const input = 'linear-gradient(red, 40%, blue)';
const result = processBackgroundImage(input);
expect(result[0].type).toBe('linearGradient');
expect(result[0].direction).toEqual({type: 'angle', value: 180});
expect(result[0].colorStops).toEqual([
{color: processColor('red'), position: 0},
{color: null, position: 0.4},
{color: processColor('blue'), position: 1},
]);
});
});
149 changes: 104 additions & 45 deletions packages/react-native/Libraries/StyleSheet/processBackgroundImage.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ type LinearGradientDirection =
| {type: 'angle', value: number}
| {type: 'keyword', value: string};

type ColorStopColor = ProcessedColorValue | null;

type ParsedGradientValue = {
type: 'linearGradient',
direction: LinearGradientDirection,
colorStops: $ReadOnlyArray<{
color: ProcessedColorValue,
color: ColorStopColor,
position: number,
}>,
};
Expand All @@ -49,33 +51,52 @@ export default function processBackgroundImage(
} else if (Array.isArray(backgroundImage)) {
for (const bgImage of backgroundImage) {
const processedColorStops: Array<{
color: ProcessedColorValue,
color: ColorStopColor,
position: number | null,
}> = [];
for (let index = 0; index < bgImage.colorStops.length; index++) {
const colorStop = bgImage.colorStops[index];
const processedColor = processColor(colorStop.color);
if (processedColor == null) {
// If a color is invalid, return an empty array and do not apply gradient. Same as web.
return [];
}
if (colorStop.positions != null && colorStop.positions.length > 0) {
for (const position of colorStop.positions) {
if (position.endsWith('%')) {
processedColorStops.push({
color: processedColor,
position: parseFloat(position) / 100,
});
} else {
// If a position is invalid, return an empty array and do not apply gradient. Same as web.
return [];
}
const positions = colorStop.positions;
// Color transition hint syntax (red, 20%, blue)
if (
colorStop.color == null &&
Array.isArray(positions) &&
positions.length === 1
) {
const position = positions[0];
if (typeof position === 'string' && position.endsWith('%')) {
processedColorStops.push({
color: null,
position: parseFloat(position) / 100,
});
} else {
// If a position is invalid, return an empty array and do not apply gradient. Same as web.
return [];
}
} else {
processedColorStops.push({
color: processedColor,
position: null,
});
const processedColor = processColor(colorStop.color);
if (processedColor == null) {
// If a color is invalid, return an empty array and do not apply gradient. Same as web.
return [];
}
if (positions != null && positions.length > 0) {
for (const position of positions) {
if (position.endsWith('%')) {
processedColorStops.push({
color: processedColor,
position: parseFloat(position) / 100,
});
} else {
// If a position is invalid, return an empty array and do not apply gradient. Same as web.
return [];
}
}
} else {
processedColorStops.push({
color: processedColor,
position: null,
});
}
}
}

Expand Down Expand Up @@ -169,47 +190,85 @@ function parseCSSLinearGradient(
// 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.
return [];
}
colorStopRegex.lastIndex = 0;

const colorStopsString = parts.join(',');
const colorStops = [];
const fullColorStopsStr = parts.join(',');
let colorStopMatch;
while ((colorStopMatch = colorStopRegex.exec(fullColorStopsStr))) {
const [, color, position1, position2] = colorStopMatch;
const processedColor = processColor(color.trim().toLowerCase());
if (processedColor == null) {
// If a color is invalid, return an empty array and do not apply any gradient. Same as web.
// 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"]
const stops = colorStopsString.split(/,(?![^(]*\))/);
for (const stop of stops) {
const trimmedStop = stop.trim().toLowerCase();
// Match function like pattern or single words
const parts = trimmedStop.match(/\S+\([^)]*\)|\S+/g);

Check warning on line 201 in packages/react-native/Libraries/StyleSheet/processBackgroundImage.js

View workflow job for this annotation

GitHub Actions / test_js (20)

'parts' is already declared in the upper scope on line 157 column 11

Check warning on line 201 in packages/react-native/Libraries/StyleSheet/processBackgroundImage.js

View workflow job for this annotation

GitHub Actions / test_js (18)

'parts' is already declared in the upper scope on line 157 column 11
if (parts == null) {
// If a color stop is invalid, return an empty array and do not apply any gradient. Same as web.
return [];
}

if (typeof position1 !== 'undefined') {
if (position1.endsWith('%')) {
// Case 1: [color, position, position]
if (parts.length === 3) {
const color = parts[0];
const position1 = parts[1];
const position2 = parts[2];
const processedColor = processColor(color);
if (processedColor == null) {
// If a color is invalid, return an empty array and do not apply any gradient. Same as web.
return [];
}
if (position1.endsWith('%') && position2.endsWith('%')) {
colorStops.push({
color: processedColor,
position: parseFloat(position1) / 100,
});
colorStops.push({
color: processedColor,
position: parseFloat(position2) / 100,
});
} else {
// If a position is invalid, return an empty array and do not apply any gradient. Same as web.
return [];
}
} else {
colorStops.push({
color: processedColor,
position: null,
});
}

if (typeof position2 !== 'undefined') {
if (position2.endsWith('%')) {
// Case 2: [color, position]
else if (parts.length === 2) {
const color = parts[0];
const position = parts[1];
const processedColor = processColor(color);
if (processedColor == null) {
// If a color is invalid, return an empty array and do not apply any gradient. Same as web.
return [];
}
if (position.endsWith('%')) {
colorStops.push({
color: processedColor,
position: parseFloat(position2) / 100,
position: parseFloat(position) / 100,
});
} else {
// If a position is invalid, return an empty array and do not apply any gradient. Same as web.
return [];
}
}
// Case 3: [color]
// Case 4: [position] => transition hint syntax
else if (parts.length === 1) {
if (parts[0].endsWith('%')) {
colorStops.push({
color: null,
position: parseFloat(parts[0]) / 100,
});
} else {
const processedColor = processColor(parts[0]);
if (processedColor == null) {
// If a color is invalid, return an empty array and do not apply any gradient. Same as web.
return [];
}
colorStops.push({
color: processedColor,
position: null,
});
}
} else {
// If a color stop is invalid, return an empty array and do not apply any gradient. Same as web.
return [];
}
}

const fixedColorStops = getFixedColorStops(colorStops);
Expand Down Expand Up @@ -286,15 +345,15 @@ function getAngleInDegrees(angle?: string): ?number {
// https://drafts.csswg.org/css-images-4/#color-stop-fixup
function getFixedColorStops(
colorStops: $ReadOnlyArray<{
color: ProcessedColorValue,
color: ColorStopColor,
position: number | null,
}>,
): Array<{
color: ProcessedColorValue,
color: ColorStopColor,
position: number,
}> {
let fixedColorStops: Array<{
color: ProcessedColorValue,
color: ColorStopColor,
position: number,
}> = [];
let hasNullPositions = false;
Expand Down
Loading

0 comments on commit e73db98

Please sign in to comment.