Skip to content

Commit ebb5403

Browse files
implement radial gradient
1 parent 5349b7c commit ebb5403

File tree

22 files changed

+2522
-659
lines changed

22 files changed

+2522
-659
lines changed

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

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ export type DropShadowValue = {
723723
color?: ____ColorValue_Internal,
724724
};
725725

726-
export type GradientValue = {
726+
type LinearGradientValue = {
727727
type: 'linearGradient',
728728
// Angle or direction enums
729729
direction?: string,
@@ -733,6 +733,46 @@ export type GradientValue = {
733733
}>,
734734
};
735735

736+
type RadialExtent =
737+
| 'closest-corner'
738+
| 'closest-side'
739+
| 'farthest-corner'
740+
| 'farthest-side';
741+
type RadialGradientPosition =
742+
| {
743+
top: number | string,
744+
left: number | string,
745+
}
746+
| {
747+
top: number | string,
748+
right: number | string,
749+
}
750+
| {
751+
bottom: number | string,
752+
left: number | string,
753+
}
754+
| {
755+
bottom: number | string,
756+
right: number | string,
757+
};
758+
type RadialGradientValue = {
759+
type: 'radialGradient',
760+
shape: 'circle' | 'ellipse',
761+
size:
762+
| RadialExtent
763+
| {
764+
x: number | string,
765+
y: number | string,
766+
},
767+
position: RadialGradientPosition,
768+
colorStops: $ReadOnlyArray<{
769+
color: ____ColorValue_Internal,
770+
positions?: $ReadOnlyArray<string>,
771+
}>,
772+
};
773+
774+
export type BackgroundImageValue = LinearGradientValue | RadialGradientValue;
775+
736776
export type BoxShadowValue = {
737777
offsetX: number | string,
738778
offsetY: number | string,
@@ -806,7 +846,7 @@ export type ____ViewStyle_InternalBase = $ReadOnly<{
806846
boxShadow?: $ReadOnlyArray<BoxShadowValue> | string,
807847
filter?: $ReadOnlyArray<FilterFunction> | string,
808848
mixBlendMode?: ____BlendMode_Internal,
809-
experimental_backgroundImage?: $ReadOnlyArray<GradientValue> | string,
849+
experimental_backgroundImage?: $ReadOnlyArray<BackgroundImageValue> | string,
810850
isolation?: 'auto' | 'isolate',
811851
}>;
812852

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

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,4 +891,242 @@ describe('processBackgroundImage', () => {
891891
{color: processColor('blue'), position: 40},
892892
]);
893893
});
894+
895+
it('should process radial gradient object syntax', () => {
896+
const input = [
897+
{
898+
type: 'radialGradient',
899+
shape: 'circle',
900+
colorStops: [
901+
{color: 'red', positions: ['10%', 20]},
902+
{color: 'blue', positions: ['30%', 40]},
903+
],
904+
position: {top: '50%', left: '50%'},
905+
size: 'closest-side',
906+
},
907+
{
908+
type: 'radialGradient',
909+
shape: 'circle',
910+
colorStops: [
911+
{color: 'red', positions: ['10%', 20]},
912+
{color: 'blue', positions: ['30%', 40]},
913+
],
914+
position: {top: '50%', left: 50},
915+
size: {
916+
x: 100,
917+
y: 100,
918+
},
919+
},
920+
];
921+
const result = processBackgroundImage(input);
922+
expect(result[0].colorStops).toEqual([
923+
{color: processColor('red'), position: '10%'},
924+
{color: processColor('red'), position: 20},
925+
{color: processColor('blue'), position: '30%'},
926+
{color: processColor('blue'), position: 40},
927+
]);
928+
expect(result[0].position).toEqual({top: '50%', left: '50%'});
929+
expect(result[0].size).toEqual('closest-side');
930+
expect(result[0].shape).toEqual('circle');
931+
expect(result[0].type).toEqual('radialGradient');
932+
933+
expect(result[1].size).toEqual({
934+
x: 100,
935+
y: 100,
936+
});
937+
});
938+
939+
it('should process radial gradient without shape, size and position', () => {
940+
const input = 'radial-gradient(red, blue)';
941+
const result = processBackgroundImage(input);
942+
expect(result[0].colorStops).toEqual([
943+
{color: processColor('red'), position: null},
944+
{color: processColor('blue'), position: null},
945+
]);
946+
expect(result[0].position).toEqual({top: '50%', left: '50%'});
947+
expect(result[0].size).toEqual('farthest-corner');
948+
expect(result[0].shape).toEqual('ellipse');
949+
expect(result[0].type).toEqual('radialGradient');
950+
});
951+
952+
it('should process radial gradient with shape circle', () => {
953+
const input = 'radial-gradient(circle, red, blue)';
954+
const result = processBackgroundImage(input);
955+
expect(result[0].colorStops).toEqual([
956+
{color: processColor('red'), position: null},
957+
{color: processColor('blue'), position: null},
958+
]);
959+
expect(result[0].position).toEqual({top: '50%', left: '50%'});
960+
expect(result[0].size).toEqual('farthest-corner');
961+
expect(result[0].shape).toEqual('circle');
962+
expect(result[0].type).toEqual('radialGradient');
963+
});
964+
965+
it('should infer radial gradient circle shape from single length', () => {
966+
const input = 'radial-gradient(100px, red, blue)';
967+
const result = processBackgroundImage(input);
968+
expect(result[0].size).toEqual({x: 100, y: 100});
969+
expect(result[0].shape).toEqual('circle');
970+
});
971+
972+
it('should infer radial gradient ellipse shape from double length', () => {
973+
const input = 'radial-gradient(100px 50px, red, blue)';
974+
const result = processBackgroundImage(input);
975+
expect(result[0].size).toEqual({x: 100, y: 50});
976+
expect(result[0].shape).toEqual('ellipse');
977+
});
978+
979+
it('should handle radial gradient with explicit shape with size', () => {
980+
const input = 'radial-gradient(circle 100px at center, red, blue 80%)';
981+
const result = processBackgroundImage(input);
982+
expect(result[0].colorStops).toEqual([
983+
{color: processColor('red'), position: null},
984+
{color: processColor('blue'), position: '80%'},
985+
]);
986+
expect(result[0].position).toEqual({top: '50%', left: '50%'});
987+
expect(result[0].size).toEqual({x: 100, y: 100});
988+
expect(result[0].shape).toEqual('circle');
989+
expect(result[0].type).toEqual('radialGradient');
990+
});
991+
992+
// 1. position syntax: [ left | center | right | top | bottom | <length-percentage> ]
993+
it('should handle radial gradient position length syntax', () => {
994+
const input = 'radial-gradient(circle at 20px, red, blue)';
995+
const result = processBackgroundImage(input);
996+
expect(result[0].position).toEqual({left: 20, top: '50%'});
997+
});
998+
999+
// 2. position syntax: [ left | center | right ] && [ top | center | bottom ]
1000+
it('should handle radial gradient position keywords syntax', () => {
1001+
const input = 'radial-gradient(circle at left top, red, blue)';
1002+
const input1 = 'radial-gradient(circle at top left, red, blue)';
1003+
const result = processBackgroundImage(input);
1004+
const result1 = processBackgroundImage(input1);
1005+
expect(result[0].position).toEqual({left: '0%', top: '0%'});
1006+
expect(result1[0].position).toEqual({left: '0%', top: '0%'});
1007+
});
1008+
1009+
// 3. position syntax: [ left | center | right | <length-percentage> ] [ top | center | bottom | <length-percentage> ]
1010+
it('should handle left position top position syntax', () => {
1011+
const input = 'radial-gradient(circle at left 20px, red, blue)';
1012+
const input1 = 'radial-gradient(circle at 20px 20px, red, blue)';
1013+
const input2 = 'radial-gradient(circle at right 50px, red, blue)';
1014+
const result = processBackgroundImage(input);
1015+
const result1 = processBackgroundImage(input1);
1016+
const result2 = processBackgroundImage(input2);
1017+
expect(result[0].position).toEqual({left: '0%', top: 20});
1018+
expect(result1[0].position).toEqual({left: 20, top: 20});
1019+
expect(result2[0].position).toEqual({left: '100%', top: 50});
1020+
});
1021+
1022+
// 4. position syntax: [ [ left | right ] <length-percentage> ] && [ [ top | bottom ] <length-percentage> ]
1023+
it('should handle left position top position syntax', () => {
1024+
const input = 'radial-gradient(at top 0% right 10%, red, blue)';
1025+
const result = processBackgroundImage(input);
1026+
expect(result[0].position).toEqual({right: '10%', top: '0%'});
1027+
});
1028+
1029+
it('should handle color stops with transition hints in radial gradients', () => {
1030+
const input =
1031+
'radial-gradient(circle, red 0%, 25%, blue 50%, 75%, green 100%)';
1032+
const result = processBackgroundImage(input);
1033+
expect(result[0].colorStops).toEqual([
1034+
{color: processColor('red'), position: '0%'},
1035+
{color: null, position: '25%'},
1036+
{color: processColor('blue'), position: '50%'},
1037+
{color: null, position: '75%'},
1038+
{color: processColor('green'), position: '100%'},
1039+
]);
1040+
expect(result[0].shape).toEqual('circle');
1041+
expect(result[0].type).toEqual('radialGradient');
1042+
});
1043+
1044+
it('should process multiple gradients with both radial and linear', () => {
1045+
const input = `
1046+
radial-gradient(circle at top left, red, blue),
1047+
linear-gradient(to bottom, green, yellow)
1048+
`;
1049+
const result = processBackgroundImage(input);
1050+
expect(result).toHaveLength(2);
1051+
expect(result[0].type).toEqual('radialGradient');
1052+
expect(result[0].shape).toEqual('circle');
1053+
expect(result[0].position).toEqual({top: '0%', left: '0%'});
1054+
expect(result[0].colorStops).toEqual([
1055+
{color: processColor('red'), position: null},
1056+
{color: processColor('blue'), position: null},
1057+
]);
1058+
expect(result[1].type).toEqual('linearGradient');
1059+
expect(result[1].direction).toEqual({
1060+
type: 'angle',
1061+
value: 180,
1062+
});
1063+
expect(result[1].colorStops).toEqual([
1064+
{color: processColor('green'), position: null},
1065+
{color: processColor('yellow'), position: null},
1066+
]);
1067+
});
1068+
1069+
it('should handle mixed case in radial gradient syntax', () => {
1070+
const input = 'RaDiAl-GrAdIeNt(CiRcLe ClOsEsT-sIdE aT cEnTeR, rEd, bLuE)';
1071+
const result = processBackgroundImage(input);
1072+
expect(result[0].colorStops).toEqual([
1073+
{color: processColor('red'), position: null},
1074+
{color: processColor('blue'), position: null},
1075+
]);
1076+
expect(result[0].position).toEqual({top: '50%', left: '50%'});
1077+
expect(result[0].size).toEqual('closest-side');
1078+
expect(result[0].shape).toEqual('circle');
1079+
expect(result[0].type).toEqual('radialGradient');
1080+
});
1081+
1082+
it('should handle whitespace variations in radial gradients', () => {
1083+
const input =
1084+
'radial-gradient( circle farthest-corner at 25% 75% , red 0% , blue 100% )';
1085+
const result = processBackgroundImage(input);
1086+
expect(result[0].colorStops).toEqual([
1087+
{color: processColor('red'), position: '0%'},
1088+
{color: processColor('blue'), position: '100%'},
1089+
]);
1090+
expect(result[0].position).toEqual({left: '25%', top: '75%'});
1091+
expect(result[0].size).toEqual('farthest-corner');
1092+
expect(result[0].shape).toEqual('circle');
1093+
expect(result[0].type).toEqual('radialGradient');
1094+
});
1095+
1096+
it('should handle position keywords in different orders', () => {
1097+
const input1 = 'radial-gradient(circle at top left, red, blue)';
1098+
const input2 = 'radial-gradient(circle at left top, red, blue)';
1099+
const result1 = processBackgroundImage(input1);
1100+
const result2 = processBackgroundImage(input2);
1101+
1102+
expect(result1[0].position).toEqual({top: '0%', left: '0%'});
1103+
expect(result2[0].position).toEqual({top: '0%', left: '0%'});
1104+
});
1105+
1106+
it('should handle invalid radial gradient syntax', () => {
1107+
const input = 'radial-gradient(circle at top leftt, red, blue, green)';
1108+
const input1 = 'radial-gradient(circle at, red, blue, green)';
1109+
const input2 = 'radial-gradient(ellipse 100px, red, blue, green)';
1110+
const input3 =
1111+
'radial-gradient(ellipse at top 20% top 50%, red, blue, green)';
1112+
const result = processBackgroundImage(input);
1113+
const result1 = processBackgroundImage(input1);
1114+
const result2 = processBackgroundImage(input2);
1115+
const result3 = processBackgroundImage(input3);
1116+
expect(result).toEqual([]);
1117+
expect(result1).toEqual([]);
1118+
expect(result2).toEqual([]);
1119+
expect(result3).toEqual([]);
1120+
});
1121+
1122+
it('should handle multiple color stops with radial gradient syntax', () => {
1123+
const input = 'radial-gradient(red 0%, yellow 30%, green 60%, blue 100%)';
1124+
const result = processBackgroundImage(input);
1125+
expect(result[0].colorStops).toEqual([
1126+
{color: processColor('red'), position: '0%'},
1127+
{color: processColor('yellow'), position: '30%'},
1128+
{color: processColor('green'), position: '60%'},
1129+
{color: processColor('blue'), position: '100%'},
1130+
]);
1131+
});
8941132
});

0 commit comments

Comments
 (0)