Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: linear gradient transition hint syntax #46219

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 colorStopParts = trimmedStop.match(/\S+\([^)]*\)|\S+/g);
if (colorStopParts == 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 (colorStopParts.length === 3) {
const color = colorStopParts[0];
const position1 = colorStopParts[1];
const position2 = colorStopParts[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 (colorStopParts.length === 2) {
const color = colorStopParts[0];
const position = colorStopParts[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 (colorStopParts.length === 1) {
if (colorStopParts[0].endsWith('%')) {
colorStops.push({
color: null,
position: parseFloat(colorStopParts[0]) / 100,
});
} else {
const processedColor = processColor(colorStopParts[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
Original file line number Diff line number Diff line change
Expand Up @@ -8375,11 +8375,12 @@ exports[`public API should not change unintentionally Libraries/StyleSheet/proce
"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 Down
Loading
Loading