1import * as React from 'react';
2import { LayoutRectangle, View } from 'react-native';
3import normalizeColor from 'react-native-web/src/modules/normalizeColor';
4
5import { NativeLinearGradientPoint, NativeLinearGradientProps } from './NativeLinearGradient.types';
6
7export default function NativeLinearGradient({
8  colors,
9  locations,
10  startPoint,
11  endPoint,
12  ...props
13}: NativeLinearGradientProps): React.ReactElement {
14  const [layout, setLayout] = React.useState<LayoutRectangle | null>(null);
15  const [gradientColors, setGradientColors] = React.useState<string[]>([]);
16  const [pseudoAngle, setPseudoAngle] = React.useState<number>(0);
17
18  const { width = 1, height = 1 } = layout ?? {};
19  React.useEffect(() => {
20    const getControlPoints = (): NativeLinearGradientPoint[] => {
21      let correctedStartPoint: NativeLinearGradientPoint = [0, 0];
22      if (Array.isArray(startPoint)) {
23        correctedStartPoint = [
24          startPoint[0] != null ? startPoint[0] : 0.0,
25          startPoint[1] != null ? startPoint[1] : 0.0,
26        ];
27      }
28      let correctedEndPoint: NativeLinearGradientPoint = [0.0, 1.0];
29      if (Array.isArray(endPoint)) {
30        correctedEndPoint = [
31          endPoint[0] != null ? endPoint[0] : 0.0,
32          endPoint[1] != null ? endPoint[1] : 1.0,
33        ];
34      }
35      return [correctedStartPoint, correctedEndPoint];
36    };
37
38    const [start, end] = getControlPoints();
39    start[0] *= width;
40    end[0] *= width;
41    start[1] *= height;
42    end[1] *= height;
43    const py = end[1] - start[1];
44    const px = end[0] - start[0];
45
46    setPseudoAngle(90 + (Math.atan2(py, px) * 180) / Math.PI);
47  }, [width, height, startPoint, endPoint]);
48
49  React.useEffect(() => {
50    const nextGradientColors = colors.map((color: number, index: number): string => {
51      const hexColor = normalizeColor(color);
52      let output = hexColor;
53      if (locations && locations[index]) {
54        const location = Math.max(0, Math.min(1, locations[index]));
55        // Convert 0...1 to 0...100
56        const percentage = location * 100;
57        output += ` ${percentage}%`;
58      }
59      return output;
60    });
61
62    setGradientColors(nextGradientColors);
63  }, [colors, locations]);
64
65  const colorStyle = gradientColors.join(',');
66  const backgroundImage = `linear-gradient(${pseudoAngle}deg, ${colorStyle})`;
67  // TODO(Bacon): In the future we could consider adding `backgroundRepeat: "no-repeat"`. For more
68  // browser support.
69  return (
70    <View
71      {...props}
72      style={[
73        props.style,
74        // @ts-ignore: [ts] Property 'backgroundImage' does not exist on type 'ViewStyle'.
75        { backgroundImage },
76      ]}
77      onLayout={event => {
78        const { x, y, width, height } = event.nativeEvent.layout;
79        const oldLayout = layout ?? { x: 0, y: 0, width: 1, height: 1 };
80        // don't set new layout state unless the layout has actually changed
81        if (
82          x !== oldLayout.x ||
83          y !== oldLayout.y ||
84          width !== oldLayout.width ||
85          height !== oldLayout.height
86        ) {
87          setLayout({ x, y, width, height });
88        }
89
90        if (props.onLayout) {
91          props.onLayout(event);
92        }
93      }}
94    />
95  );
96}
97