diff --git a/example/src/PickerExample.js b/example/src/PickerExample.js
deleted file mode 100644
index b64471102..000000000
--- a/example/src/PickerExample.js
+++ /dev/null
@@ -1,225 +0,0 @@
-import * as React from "react";
-import { Picker, withTheme } from "@draftbit/ui";
-import Section, { Container } from "./Section";
-
-const OPTIONS = [
- { value: "AudiValue", label: "Audi" },
- { value: "BMWValue", label: "BMW" },
- { value: "CadillacValue", label: "Cadillac" },
- { value: "DodgeValue", label: "Dodge" },
-];
-
-function PickerExample({ theme }) {
- const [value, setValue] = React.useState("Audi");
- const [value2, setValue2] = React.useState("Audi");
- const [value3, setValue3] = React.useState(1);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default withTheme(PickerExample);
diff --git a/example/src/PickerExample.tsx b/example/src/PickerExample.tsx
new file mode 100644
index 000000000..f53082367
--- /dev/null
+++ b/example/src/PickerExample.tsx
@@ -0,0 +1,157 @@
+import * as React from "react";
+import { Picker, MultiSelectPicker, PickerItem, withTheme } from "@draftbit/ui";
+import Section, { Container } from "./Section";
+
+const OPTIONS = [
+ { value: "AudiValue", label: "Audi" },
+ { value: "BMWValue", label: "BMW" },
+ {
+ value: "CadillacValue",
+ label: "Cadillac",
+ },
+ { value: "DodgeValue", label: "Dodge" },
+ { value: "KiaValue", label: "Kia" },
+ { value: "HyundaiValue", label: "Hyundai" },
+];
+
+function PickerExample() {
+ const [value1, setValue] = React.useState("Audi");
+ const [value3, setValue3] = React.useState(1);
+ const [value4, setValue4] = React.useState<(string | number)[]>([]);
+
+ return (
+
+ {/* Dropdown and Multiselect Pickers placed outside Section to be able to draw over sibling components */}
+
+ setValue(value.toString())}
+ style={{ marginBottom: 20 }}
+ />
+
+
+ setValue(value.toString())}
+ style={{ marginBottom: 20 }}
+ selectedIconColor="white"
+ >
+
+
+
+
+ setValue4(value)}
+ style={{ marginBottom: 20 }}
+ />
+
+
+ setValue(value.toString())}
+ />
+
+
+
+ setValue(value.toString())}
+ />
+
+
+
+ setValue(value.toString())}
+ />
+
+
+
+ setValue(value.toString())}
+ leftIconName={"AntDesign/caretleft"}
+ leftIconMode="outset"
+ />
+
+
+
+ setValue(value.toString())}
+ />
+
+
+
+ setValue(value.toString())}
+ />
+
+
+
+ setValue3(value as number)}
+ />
+
+
+ );
+}
+
+export default withTheme(PickerExample);
diff --git a/packages/core/package.json b/packages/core/package.json
index 8a7c957a1..022aeb329 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -52,11 +52,13 @@
"date-fns": "^2.16.1",
"dateformat": "^3.0.3",
"expo-av": "~13.2.1",
+ "lodash.isequal": "^4.5.0",
"lodash.isnumber": "^3.0.3",
"lodash.omit": "^4.5.0",
"lodash.tonumber": "^4.0.3",
"react-native-confirmation-code-field": "^7.3.1",
"react-native-deck-swiper": "^2.0.12",
+ "react-native-dropdown-picker": "^5.4.6",
"react-native-gesture-handler": "~2.9.0",
"react-native-markdown-display": "^7.0.0-alpha.2",
"react-native-modal-datetime-picker": "^13.0.0",
diff --git a/packages/core/src/__tests__/components/Picker.test.tsx b/packages/core/src/__tests__/components/Picker.test.tsx
new file mode 100644
index 000000000..b9d5a9843
--- /dev/null
+++ b/packages/core/src/__tests__/components/Picker.test.tsx
@@ -0,0 +1,124 @@
+import * as React from "react";
+import { fireEvent, render, screen } from "@testing-library/react-native";
+import { Picker, PickerItem } from "../../components/Picker";
+import Provider from "../../Provider";
+import DefaultTheme from "../../styles/DefaultTheme";
+import { act } from "react-test-renderer";
+
+jest.mock("@react-native-picker/picker", () => {
+ const React = require("react");
+ const { View } = require("react-native");
+
+ class Picker extends React.Component {
+ render(): React.ReactNode {
+ return {this.props.children};
+ }
+ static Item({ testID, label }) {
+ return ;
+ }
+ }
+ return { Picker };
+});
+
+const mockRenderDropDownPickerComponent = jest.fn();
+
+jest.mock("react-native-dropdown-picker", () => {
+ const React = require("react");
+ const { View } = require("react-native");
+
+ const Picker: React.FC = (props) => {
+ mockRenderDropDownPickerComponent(props);
+ return ;
+ };
+
+ return Picker;
+});
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+const defaultPickerProps = {
+ theme: DefaultTheme as any,
+ Icon: () => <>>,
+ onValueChange: () => {},
+ options: [],
+};
+
+describe("Picker tests", () => {
+ test("should render native picker when mode is 'native'", () => {
+ render();
+
+ const nativePicker = screen.queryByTestId("native-picker");
+ expect(nativePicker).toBeTruthy();
+ });
+
+ test("should render dropdown picker when mode is 'dropdown'", () => {
+ render();
+
+ const dropdownPicker = screen.queryByTestId("dropdown-picker");
+ expect(dropdownPicker).toBeTruthy();
+ });
+
+ describe("Native Picker tests", () => {
+ test("should first option be selected when no placeholder is provided", () => {
+ const options = ["option1", "option2"];
+ const mockOnValueChange = jest.fn();
+
+ render(
+
+ );
+
+ expect(mockOnValueChange).toBeCalledWith(options[0]);
+ });
+
+ test("should placeholder be added as the first option when provided", async () => {
+ const options = ["option1", "option2"];
+ const placeholder = "test placeholder";
+
+ render(
+
+
+
+ );
+
+ await act(() => fireEvent.press(screen.getByTestId("native-picker"))); //To show the items
+
+ const pickerItems = screen.queryAllByTestId("native-picker-item");
+
+ expect(pickerItems.at(0)?.props.label).toBe(placeholder);
+ });
+ });
+ describe("Dropdown Picker tests", () => {
+ test("should PickerItem styles be passed into picker component style props", () => {
+ const textStyles = { color: "blue", fontSize: 20 };
+ const viewStyles = { backgroundColor: "red", paddingTop: 20 };
+
+ render(
+
+
+
+ );
+
+ expect(mockRenderDropDownPickerComponent).toBeCalledWith(
+ expect.objectContaining({
+ listItemLabelStyle: expect.arrayContaining([
+ expect.objectContaining(textStyles),
+ ]),
+ listItemContainerStyle: expect.objectContaining(viewStyles),
+ })
+ );
+ });
+ });
+});
diff --git a/packages/core/src/components/Picker/NativePicker.tsx b/packages/core/src/components/Picker/NativePicker.tsx
new file mode 100644
index 000000000..388498984
--- /dev/null
+++ b/packages/core/src/components/Picker/NativePicker.tsx
@@ -0,0 +1,158 @@
+import * as React from "react";
+import { StyleSheet, Platform, Keyboard } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { Picker as NativePickerComponent } from "@react-native-picker/picker";
+import Portal from "../Portal/Portal";
+import { Button } from "../Button";
+import { useDeepCompareMemo } from "../../utilities";
+import {
+ CommonPickerProps,
+ SinglePickerProps,
+ normalizeToPickerOptions,
+} from "./PickerCommon";
+import PickerInputContainer from "./PickerInputContainer";
+import { withTheme } from "../../theming";
+
+const isIos = Platform.OS === "ios";
+const isWeb = Platform.OS === "web";
+
+const NativePicker: React.FC = ({
+ options: optionsProp = [],
+ onValueChange,
+ Icon,
+ placeholder,
+ value,
+ autoDismissKeyboard = true,
+ theme,
+ disabled,
+ ...rest
+}) => {
+ const pickerRef = React.useRef>(null);
+
+ const [pickerVisible, setPickerVisible] = React.useState(false);
+
+ const options = useDeepCompareMemo(() => {
+ const normalizedOptions = normalizeToPickerOptions(optionsProp);
+
+ // Underlying Picker component defaults selection to first element when value is not provided (or undefined)
+ // Placholder must be the 1st option in order to allow selection of the 'actual' 1st option
+ if (placeholder) {
+ return [{ label: placeholder, value: placeholder }, ...normalizedOptions];
+ } else {
+ return normalizedOptions;
+ }
+ }, [placeholder, optionsProp]);
+
+ // When no placeholder is provided then first item should be marked selected to reflect underlying Picker internal state
+ if (!placeholder && options.length && !value && value !== options[0].value) {
+ onValueChange?.(options[0].value);
+ }
+
+ const renderNativePicker = () => (
+ {
+ if (newValue !== placeholder) {
+ onValueChange?.(newValue);
+ } else if (newValue === placeholder) {
+ onValueChange?.("");
+ }
+ }}
+ style={isIos ? styles.iosNativePicker : styles.nativePicker}
+ onBlur={() => setPickerVisible(false)}
+ >
+ {options.map((option) => (
+
+ ))}
+
+ );
+
+ const renderPicker = () => {
+ if (isIos) {
+ return (
+
+
+
+
+ );
+ } else {
+ return renderNativePicker();
+ }
+ };
+
+ React.useEffect(() => {
+ if (pickerVisible && pickerRef.current) {
+ pickerRef?.current?.focus();
+ }
+ }, [pickerVisible, pickerRef]);
+
+ React.useEffect(() => {
+ if (pickerVisible && autoDismissKeyboard) {
+ Keyboard.dismiss();
+ }
+ }, [pickerVisible, autoDismissKeyboard]);
+
+ return (
+ setPickerVisible(!pickerVisible)}
+ disabled={disabled}
+ {...rest}
+ >
+ {/* Web version is collapsed by default, always show to allow direct expand */}
+ {(pickerVisible || isWeb) && !disabled && renderPicker()}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ nativePicker: {
+ position: "absolute",
+ bottom: 0,
+ left: 0,
+ right: 0,
+ flexDirection: "row",
+ justifyContent: "center",
+ width: "100%",
+ backgroundColor: "white",
+ opacity: 0,
+ ...Platform.select({
+ web: {
+ height: "100%", //To have the element fill the height
+ },
+ }),
+ },
+ iosNativePicker: {
+ backgroundColor: "white",
+ },
+ iosPickerContent: {
+ width: "100%",
+ position: "absolute",
+ bottom: 0,
+ backgroundColor: "white",
+ },
+ iosButton: {
+ backgroundColor: "transparent",
+ borderWidth: 0,
+ },
+});
+
+export default withTheme(NativePicker);
diff --git a/packages/core/src/components/Picker/Picker.tsx b/packages/core/src/components/Picker/Picker.tsx
deleted file mode 100644
index 6783a0e50..000000000
--- a/packages/core/src/components/Picker/Picker.tsx
+++ /dev/null
@@ -1,509 +0,0 @@
-import * as React from "react";
-import {
- View,
- StyleSheet,
- Text,
- Platform,
- ViewStyle,
- StyleProp,
- Dimensions,
- Keyboard,
-} from "react-native";
-import { omit, pickBy, identity, isObject } from "lodash";
-import { SafeAreaView } from "react-native-safe-area-context";
-import { Picker as NativePicker } from "@react-native-picker/picker";
-
-import { withTheme } from "../../theming";
-import Portal from "../Portal/Portal";
-import Button from "../../deprecated-components/DeprecatedButton";
-import Touchable from "../Touchable";
-import type { Theme } from "../../styles/DefaultTheme";
-import type { IconSlot } from "../../interfaces/Icon";
-import {
- extractStyles,
- extractBorderAndMarginStyles,
- borderStyleNames,
- marginStyleNames,
-} from "../../utilities";
-
-export interface PickerOption {
- value: string;
- label: string;
-}
-
-export type PickerProps = {
- error?: any;
- placeholder?: string;
- disabled?: boolean;
- style?: StyleProp & { height?: number };
- value?: string;
- options: PickerOption[] | string[];
- onValueChange: (value: string, index: number) => void;
- defaultValue?: string;
- assistiveText?: string;
- label?: string;
- iconColor?: string;
- iconSize?: number;
- leftIconMode?: "inset" | "outset";
- leftIconName?: string;
- placeholderTextColor?: string;
- rightIconName?: string;
- type?: "solid" | "underline";
- autoDismissKeyboard?: boolean;
- theme: Theme;
- Icon: IconSlot["Icon"];
-};
-
-function normalizeOptions(options: PickerProps["options"]): PickerOption[] {
- if (options.length === 0) {
- return [];
- }
-
- if (typeof options[0] === ("string" || "number")) {
- return (options as string[]).map((option) => ({
- label: String(option),
- value: String(option),
- }));
- }
-
- if (
- isObject(options[0]) &&
- options[0].value !== null &&
- options[0].label !== null
- ) {
- return (options as PickerOption[]).map((option) => {
- return {
- label: String(option.label),
- value: String(option.value),
- };
- });
- }
-
- throw new Error(
- 'Picker options must be either an array of strings or array of { "label": string; "value": string; } objects.'
- );
-}
-
-const { width: deviceWidth, height: deviceHeight } = Dimensions.get("screen");
-const isIos = Platform.OS === "ios";
-const isWeb = Platform.OS === "web";
-
-const unstyledColor = "rgba(165, 173, 183, 1)";
-const disabledColor = "rgb(240, 240, 240)";
-const errorColor = "rgba(255, 69, 100, 1)";
-
-//Empty string for 'value' is treated as a non-value
-//reason: Draftbit uses empty string as initial value for string state*/
-const Picker: React.FC = ({
- error,
- options = [],
- onValueChange,
- defaultValue,
- Icon,
- style,
- placeholder,
- value,
- disabled = false,
- assistiveText,
- label,
- iconColor = unstyledColor,
- iconSize = 24,
- leftIconMode = "inset",
- leftIconName,
- placeholderTextColor = unstyledColor,
- rightIconName,
- type = "solid",
- autoDismissKeyboard = true,
-}) => {
- const androidPickerRef = React.useRef(undefined);
-
- const [internalValue, setInternalValue] = React.useState(
- value || defaultValue
- );
-
- const [pickerVisible, setPickerVisible] = React.useState(false);
-
- const togglePickerVisible = () => {
- setPickerVisible(!pickerVisible);
- };
-
- React.useEffect(() => {
- if (value != null && value !== "") {
- setInternalValue(value);
- } else if (value === "") {
- setInternalValue(undefined);
- }
- }, [value]);
-
- React.useEffect(() => {
- if (defaultValue != null && defaultValue !== "") {
- setInternalValue(defaultValue);
- }
- }, [defaultValue]);
-
- React.useEffect(() => {
- if (pickerVisible && androidPickerRef.current) {
- androidPickerRef?.current?.focus();
- }
- }, [pickerVisible, androidPickerRef]);
-
- React.useEffect(() => {
- if (pickerVisible && autoDismissKeyboard) {
- Keyboard.dismiss();
- }
- }, [pickerVisible, autoDismissKeyboard]);
-
- const normalizedOptions = React.useMemo(
- () => normalizeOptions(options),
- [options]
- );
-
- //Underlying Picker component defaults selection to first element when value is not provided (or undefined)
- //Placholder must be the 1st option in order to allow selection of the 'actual' 1st option
- const pickerOptions = React.useMemo(
- () =>
- placeholder
- ? [{ label: placeholder, value: placeholder }, ...normalizedOptions]
- : normalizedOptions,
- [placeholder, normalizedOptions]
- );
-
- //When no placeholder is provided then first item should be marked selected to reflect underlying Picker internal state
- if (
- !placeholder &&
- pickerOptions.length &&
- !internalValue &&
- internalValue !== pickerOptions[0].value //Prevent infinite state changes incase first value is falsy
- ) {
- onValueChange?.(pickerOptions[0].value, 0);
- setInternalValue(pickerOptions[0].value);
- }
-
- const { viewStyles, textStyles } = extractStyles(style);
-
- const additionalBorderStyles = ["backgroundColor"];
-
- const additionalMarginStyles = [
- "bottom",
- "height",
- "left",
- "maxHeight",
- "maxWidth",
- "minHeight",
- "minWidth",
- "overflow",
- "position",
- "right",
- "top",
- "width",
- "zIndex",
- ];
-
- const {
- borderStyles: extractedBorderStyles,
- marginStyles: extractedMarginStyles,
- } = extractBorderAndMarginStyles(
- viewStyles,
- additionalBorderStyles,
- additionalMarginStyles
- );
-
- const borderStyles = {
- ...{
- ...(type === "solid"
- ? {
- borderTopLeftRadius: 5,
- borderTopRightRadius: 5,
- borderBottomRightRadius: 5,
- borderBottomLeftRadius: 5,
- borderTopWidth: 1,
- borderRightWidth: 1,
- borderLeftWidth: 1,
- }
- : {}),
- borderBottomWidth: 1,
- borderColor: unstyledColor,
- borderStyle: "solid",
- },
- ...extractedBorderStyles,
- ...(error ? { borderColor: errorColor } : {}),
- ...(disabled
- ? { borderColor: "transparent", backgroundColor: disabledColor }
- : {}),
- };
-
- const marginStyles = {
- height: 60,
- ...extractedMarginStyles,
- };
-
- const stylesWithoutBordersAndMargins = omit(viewStyles, [
- ...borderStyleNames,
- ...marginStyleNames,
- ...additionalBorderStyles,
- ...additionalMarginStyles,
- ]);
-
- const selectedLabel =
- internalValue &&
- ((pickerOptions as unknown as PickerOption[]).find(
- (option) => option.value === internalValue
- )?.label ??
- internalValue);
-
- const labelText = label ? (
-
- {label}
-
- ) : null;
-
- const leftIconOutset = leftIconMode === "outset";
-
- const leftIcon = leftIconName ? (
-
- ) : null;
-
- const rightIcon = rightIconName ? (
-
- ) : null;
-
- const textAlign = textStyles?.textAlign;
-
- const calculateLeftPadding = () => {
- if (leftIconOutset) {
- if (textAlign === "center") {
- return iconSize - Math.abs(8 - iconSize);
- }
-
- return iconSize + 8;
- }
-
- return 0;
- };
-
- const assistiveTextLabel = assistiveText ? (
-
- {assistiveText}
-
- ) : null;
-
- const primaryTextStyle = {
- color: unstyledColor,
- fontSize: 14,
- ...pickBy(textStyles, identity),
- ...(placeholder === internalValue ? { color: placeholderTextColor } : {}),
- ...(disabled ? { color: unstyledColor } : {}),
- };
-
- const handleValueChange = (newValue: string, itemIndex: number) => {
- if (newValue !== "" && newValue !== placeholder) {
- onValueChange?.(newValue, itemIndex);
- setInternalValue(newValue);
- } else if (newValue === placeholder) {
- onValueChange?.("", 0);
- setInternalValue(undefined);
- }
- };
-
- return (
- /* marginsContainer */
-
- {/* touchableContainer */}
-
- {/* outsetContainer */}
-
- {leftIcon}
-
- {/* insetContainer */}
-
- {/* primaryTextContainer */}
-
- {labelText}
-
-
- {String(selectedLabel ?? placeholder)}
-
-
-
- {rightIcon}
-
-
- {assistiveTextLabel}
-
-
- {/* iosPicker */}
- {isIos && pickerVisible ? (
-
-
-
-
-
-
- {(pickerOptions as unknown as PickerOption[]).map((option) => (
-
- ))}
-
-
-
-
- ) : null}
-
- {/* nonIosPicker */}
- {/* Web version is collapsed by default, always show to allow direct expand */}
- {!isIos && (pickerVisible || isWeb) ? (
- setPickerVisible(false)}
- >
- {(pickerOptions as unknown as PickerOption[]).map((option) => (
-
- ))}
-
- ) : null}
-
- );
-};
-
-const styles = StyleSheet.create({
- marginsContainer: {
- alignSelf: "stretch",
- alignItems: "center",
- width: "100%",
- maxWidth: deviceWidth,
- },
- touchableContainer: {
- flex: 1,
- height: "100%",
- width: "100%",
- alignSelf: "stretch",
- alignItems: "center",
- },
- outsetContainer: {
- flex: 1,
- height: "100%",
- width: "100%",
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "space-between",
- },
- insetContainer: {
- flex: 1,
- height: "100%",
- width: "100%",
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "space-between",
- paddingLeft: 12,
- paddingRight: 12,
- },
- primaryTextContainer: {
- flex: 1,
- },
- iosPicker: {
- position: "absolute",
- bottom: 0,
- left: 0,
- right: 0,
- flexDirection: "row",
- justifyContent: "center",
- width: "100%",
- maxWidth: deviceWidth,
- maxHeight: deviceHeight,
- backgroundColor: "white",
- },
- iosPickerContent: {
- flexDirection: "column",
- width: "100%",
- maxWidth: deviceWidth,
- },
- iosButton: {
- alignSelf: "flex-end",
- },
- iosNativePicker: {
- backgroundColor: "white",
- },
- nonIosPicker: {
- opacity: 0,
- position: "absolute",
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- width: "100%",
- maxWidth: deviceWidth,
- maxHeight: deviceHeight,
- },
-});
-
-export default withTheme(Picker);
diff --git a/packages/core/src/components/Picker/PickerCommon.ts b/packages/core/src/components/Picker/PickerCommon.ts
new file mode 100644
index 000000000..8ec63ea26
--- /dev/null
+++ b/packages/core/src/components/Picker/PickerCommon.ts
@@ -0,0 +1,81 @@
+import { StyleProp, ViewStyle, TextStyle } from "react-native";
+import { IconSlot } from "../../interfaces/Icon";
+import { isObject } from "lodash";
+import { Theme } from "../../styles/DefaultTheme";
+
+export interface PickerOption {
+ value: string | number;
+ label: string | number;
+}
+
+export interface PickerInputContainerProps extends IconSlot {
+ error?: boolean;
+ placeholder?: string;
+ disabled?: boolean;
+ style?: StyleProp | TextStyle;
+ label?: string;
+ assistiveText?: string;
+ iconColor?: string;
+ iconSize?: number;
+ leftIconMode?: "inset" | "outset";
+ leftIconName?: string;
+ placeholderTextColor?: string;
+ rightIconName?: string;
+ type?: "solid" | "underline";
+}
+
+export interface CommonPickerProps extends PickerInputContainerProps {
+ options: PickerOption[] | string[] | number[];
+ autoDismissKeyboard?: boolean;
+ theme: Theme;
+}
+
+export interface SinglePickerProps {
+ value?: string | number;
+ onValueChange: (value: string | number) => void;
+}
+
+export interface MultiSelectPickerProps {
+ value?: (string | number)[];
+ onValueChange: (value: (string | number)[]) => void;
+}
+
+export interface CommonDropDownPickerProps extends CommonPickerProps {
+ selectedIconName?: string;
+ selectedIconColor?: string;
+ selectedIconSize?: number;
+ dropDownBackgroundColor?: string;
+ dropDownBorderColor?: string;
+ dropDownBorderWidth?: number;
+ dropDownBorderRadius?: number;
+}
+
+export function normalizeToPickerOptions(
+ options: PickerOption[] | string[] | number[]
+): PickerOption[] {
+ if (options.length === 0) {
+ return [];
+ }
+
+ const firstOption = options[0];
+
+ if (typeof firstOption === ("string" || "number")) {
+ return options.map((option) => ({
+ label: option as string | number,
+ value: option as string | number,
+ }));
+ }
+
+ if (isObject(firstOption) && firstOption.value && firstOption.label) {
+ return (options as PickerOption[]).map((option) => {
+ return {
+ label: option.label,
+ value: option.value,
+ };
+ });
+ }
+
+ throw new Error(
+ 'Picker options must be either an array of strings, numbers, or an array of { "label": string | number; "value": string | number; } objects.'
+ );
+}
diff --git a/packages/core/src/components/Picker/PickerComponent.android.tsx b/packages/core/src/components/Picker/PickerComponent.android.tsx
deleted file mode 100644
index 76f059406..000000000
--- a/packages/core/src/components/Picker/PickerComponent.android.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import * as React from "react";
-import { View, StyleSheet } from "react-native";
-import omit from "lodash.omit";
-import { withTheme } from "../../theming";
-import { Picker as NativePicker } from "@react-native-picker/picker";
-import { extractStyles } from "../../utilities";
-
-import TextField from "../TextField";
-import Touchable from "../Touchable";
-import { PickerComponentProps } from "./PickerTypes";
-
-const Picker: React.FC = ({
- style,
- options,
- placeholder,
- selectedValue,
- disabled = false,
- onValueChange: onValueChangeOverride = () => {},
- ...props
-}) => {
- const {
- viewStyles: {
- borderRadius, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderTopWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderRightWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderBottomWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderLeftWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderColor, // eslint-disable-line @typescript-eslint/no-unused-vars
- backgroundColor, // eslint-disable-line @typescript-eslint/no-unused-vars
- padding, // eslint-disable-line @typescript-eslint/no-unused-vars
- paddingTop, // eslint-disable-line @typescript-eslint/no-unused-vars
- paddingRight, // eslint-disable-line @typescript-eslint/no-unused-vars
- paddingBottom, // eslint-disable-line @typescript-eslint/no-unused-vars
- paddingLeft, // eslint-disable-line @typescript-eslint/no-unused-vars
- ...viewStyles
- },
- } = extractStyles(style);
-
- const textField = React.useRef(undefined);
-
- const onValueChange = (itemValue: string, itemIndex: number) => {
- toggleFocus();
- onValueChangeOverride(itemValue, itemIndex);
- };
-
- const toggleFocus = () => {
- if (!disabled) {
- // @ts-ignore
- textField.current.toggleFocus(); // cannot determine if method exists due to component being wrapped in a withTheme()
- }
- };
-
- const stylesWithoutMargin =
- style &&
- omit(StyleSheet.flatten(style), [
- "margin",
- "marginTop",
- "marginRight",
- "marginBottom",
- "marginLeft",
- ]);
-
- const selectedLabel =
- selectedValue &&
- (options.find((o) => o.value === selectedValue)?.label ?? selectedValue);
-
- return (
-
-
-
- {options.map((o) => (
-
- ))}
-
-
-
-
-
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- alignSelf: "stretch",
- },
-});
-
-export default withTheme(Picker);
diff --git a/packages/core/src/components/Picker/PickerComponent.ios.tsx b/packages/core/src/components/Picker/PickerComponent.ios.tsx
deleted file mode 100644
index ea73a2f8e..000000000
--- a/packages/core/src/components/Picker/PickerComponent.ios.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import * as React from "react";
-import { View, StyleSheet } from "react-native";
-import { SafeAreaView } from "react-native-safe-area-context";
-import omit from "lodash.omit";
-
-import { Picker as NativePicker } from "@react-native-picker/picker";
-
-import { withTheme } from "../../theming";
-
-import Portal from "../Portal/Portal";
-import Button from "../../deprecated-components/DeprecatedButton";
-import TextField from "../TextField";
-import Touchable from "../Touchable";
-import { PickerComponentProps } from "./PickerTypes";
-import { extractStyles } from "../../utilities";
-import type { IconSlot } from "../../interfaces/Icon";
-
-const Picker: React.FC = ({
- Icon,
- style,
- options,
- placeholder,
- selectedValue,
- disabled = false,
- onValueChange = () => {},
- theme: { colors },
- ...props
-}) => {
- const {
- viewStyles: {
- borderRadius, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderTopWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderRightWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderBottomWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderLeftWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderColor, // eslint-disable-line @typescript-eslint/no-unused-vars
- backgroundColor, // eslint-disable-line @typescript-eslint/no-unused-vars
- padding, // eslint-disable-line @typescript-eslint/no-unused-vars
- paddingTop, // eslint-disable-line @typescript-eslint/no-unused-vars
- paddingRight, // eslint-disable-line @typescript-eslint/no-unused-vars
- paddingBottom, // eslint-disable-line @typescript-eslint/no-unused-vars
- paddingLeft, // eslint-disable-line @typescript-eslint/no-unused-vars
- ...viewStyles
- },
- } = extractStyles(style);
-
- const textField = React.useRef(undefined);
- const [pickerVisible, setIsPickerVisible] = React.useState(false);
-
- const toggleVisibility = () => {
- setIsPickerVisible(!pickerVisible);
- // @ts-ignore
- textField.current.toggleFocus(); // cannot determine if method exists due to component being wrapped in a withTheme()
- };
-
- const stylesWithoutMargin =
- style &&
- omit(StyleSheet.flatten(style), [
- "margin",
- "marginTop",
- "marginRight",
- "marginBottom",
- "marginLeft",
- ]);
-
- const selectedLabel =
- selectedValue &&
- (options.find((o) => o.value === selectedValue)?.label ?? selectedValue);
-
- return (
-
-
-
-
- {pickerVisible && (
-
-
-
-
-
- {options.map((o: any) => (
-
- ))}
-
-
-
-
- )}
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- alignSelf: "stretch",
- },
- picker: {
- position: "absolute",
- bottom: 0,
- left: 0,
- right: 0,
- flexDirection: "row",
- justifyContent: "center",
- },
- pickerContainer: {
- backgroundColor: "white",
- flexDirection: "column",
- width: "100%",
- },
- closeButton: {
- alignSelf: "flex-end",
- },
-});
-
-export default withTheme(Picker);
diff --git a/packages/core/src/components/Picker/PickerComponent.web.tsx b/packages/core/src/components/Picker/PickerComponent.web.tsx
deleted file mode 100644
index 3f8fe369e..000000000
--- a/packages/core/src/components/Picker/PickerComponent.web.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import * as React from "react";
-import { View, StyleSheet } from "react-native";
-import { Picker as NativePicker } from "@react-native-picker/picker";
-import omit from "lodash.omit";
-import { withTheme } from "../../theming";
-import { extractStyles } from "../../utilities";
-
-import TextField from "../TextField";
-import Touchable from "../Touchable";
-import { PickerComponentProps } from "./PickerTypes";
-
-const Picker: React.FC = ({
- style,
- options,
- placeholder,
- selectedValue,
- disabled = false,
- onValueChange: onValueChangeOverride = () => {},
- ...props
-}) => {
- const {
- viewStyles: {
- borderRadius, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderTopWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderRightWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderBottomWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderLeftWidth, // eslint-disable-line @typescript-eslint/no-unused-vars
- borderColor, // eslint-disable-line @typescript-eslint/no-unused-vars
- backgroundColor, // eslint-disable-line @typescript-eslint/no-unused-vars
- padding, // eslint-disable-line @typescript-eslint/no-unused-vars
- paddingTop, // eslint-disable-line @typescript-eslint/no-unused-vars
- paddingRight, // eslint-disable-line @typescript-eslint/no-unused-vars
- paddingBottom, // eslint-disable-line @typescript-eslint/no-unused-vars
- paddingLeft, // eslint-disable-line @typescript-eslint/no-unused-vars
- ...viewStyles
- },
- } = extractStyles(style);
-
- const textField = React.useRef(undefined);
-
- const onValueChange = (itemValue: string, itemIndex: number) => {
- toggleFocus();
- onValueChangeOverride(itemValue, itemIndex);
- };
-
- const toggleFocus = () => {
- if (!disabled) {
- // @ts-ignore
- textField.current.toggleFocus(); // cannot determine if method exists due to component being wrapped in a withTheme()
- }
- };
-
- const stylesWithoutMargin =
- style &&
- omit(StyleSheet.flatten(style), [
- "margin",
- "marginTop",
- "marginRight",
- "marginBottom",
- "marginLeft",
- ]);
-
- const selectedLabel =
- selectedValue &&
- (options.find((o) => o.value === selectedValue)?.label ?? selectedValue);
-
- return (
-
-
-
- {options.map((o) => (
-
- ))}
-
-
-
-
-
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- alignSelf: "stretch",
- },
-});
-
-export default withTheme(Picker);
diff --git a/packages/core/src/components/Picker/PickerInputContainer.tsx b/packages/core/src/components/Picker/PickerInputContainer.tsx
new file mode 100644
index 000000000..09a25695f
--- /dev/null
+++ b/packages/core/src/components/Picker/PickerInputContainer.tsx
@@ -0,0 +1,89 @@
+import React from "react";
+import { View, StyleSheet } from "react-native";
+import omit from "lodash.omit";
+import {
+ extractSizeStyles,
+ extractPositionStyles,
+ extractFlexItemStyles,
+ extractBorderAndMarginStyles,
+} from "../../utilities";
+import TextField from "../TextField";
+import Touchable from "../Touchable";
+import {
+ PickerInputContainerProps as ExposedPickerInputContainerProps,
+ PickerOption,
+} from "./PickerCommon";
+
+interface PickerInputContainerProps extends ExposedPickerInputContainerProps {
+ selectedValue?: string | number | (string | number)[];
+ options: PickerOption[];
+ zIndex?: number;
+ onPress?: () => void;
+ testID?: string;
+}
+
+const PickerInputContainer: React.FC<
+ React.PropsWithChildren
+> = ({
+ options = [],
+ onPress,
+ Icon,
+ style,
+ selectedValue,
+ disabled = false,
+ zIndex,
+ children,
+ testID,
+ ...rest
+}) => {
+ const containerStyle = StyleSheet.flatten([
+ extractSizeStyles(style),
+ extractPositionStyles(style),
+ extractFlexItemStyles(style),
+ extractBorderAndMarginStyles(style).marginStyles,
+ ]);
+
+ const textFieldStyle = omit(
+ StyleSheet.flatten(style),
+ Object.keys(containerStyle)
+ );
+
+ let selectedLabel: string | number | undefined = "";
+ if (Array.isArray(selectedValue)) {
+ selectedLabel = selectedValue
+ .map(
+ (value) =>
+ options.find((option) => option.value === value)?.label.toString() ||
+ value
+ )
+ .join(", ");
+ } else {
+ selectedLabel =
+ options
+ .find((option) => option.value === selectedValue)
+ ?.label.toString() || selectedValue;
+ }
+
+ return (
+
+ {}}
+ value={selectedLabel?.toString()}
+ editable={false}
+ disabled={disabled}
+ style={textFieldStyle}
+ {...rest}
+ />
+
+ {children}
+
+ );
+};
+
+export default PickerInputContainer;
diff --git a/packages/core/src/components/Picker/PickerTypes.ts b/packages/core/src/components/Picker/PickerTypes.ts
deleted file mode 100644
index 43c696709..000000000
--- a/packages/core/src/components/Picker/PickerTypes.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { ViewStyle } from "react-native";
-import { StyleProp } from "react-native";
-import { Props as TextFieldProps } from "../TextField";
-
-export interface PickerOption {
- value: string;
- label: string;
-}
-
-export interface PickerComponentProps extends TextFieldProps {
- style?: StyleProp & { height?: number };
- options: PickerOption[];
- placeholder?: string;
- selectedValue: string;
- disabled?: boolean;
- onValueChange?: (value: string, index: number) => void;
- defaultValue?: string;
-}
diff --git a/packages/core/src/components/Picker/dropdown/DropDownPicker.tsx b/packages/core/src/components/Picker/dropdown/DropDownPicker.tsx
new file mode 100644
index 000000000..72f0d5cb1
--- /dev/null
+++ b/packages/core/src/components/Picker/dropdown/DropDownPicker.tsx
@@ -0,0 +1,148 @@
+import * as React from "react";
+import { Keyboard } from "react-native";
+import {
+ extractStyles,
+ flattenReactFragments,
+ useDeepCompareMemo,
+} from "../../../utilities";
+import {
+ CommonDropDownPickerProps,
+ MultiSelectPickerProps,
+ SinglePickerProps,
+ normalizeToPickerOptions,
+} from "../PickerCommon";
+import PickerInputContainer from "../PickerInputContainer";
+import DropDownPickerComponent from "react-native-dropdown-picker";
+import { withTheme } from "../../../theming";
+import PickerItem, { PickerItemProps } from "./PickerItem";
+
+const DropDownPicker: React.FC<
+ React.PropsWithChildren<
+ CommonDropDownPickerProps & (SinglePickerProps | MultiSelectPickerProps)
+ >
+> = ({
+ theme,
+ options: optionsProp = [],
+ onValueChange,
+ Icon,
+ placeholder,
+ value,
+ autoDismissKeyboard = true,
+ selectedIconName = "Feather/check",
+ selectedIconColor = theme.colors.strong,
+ selectedIconSize = 20,
+ dropDownBackgroundColor = theme.colors.background,
+ dropDownBorderColor = theme.colors.divider,
+ dropDownBorderWidth = 1,
+ dropDownBorderRadius = 8,
+ children: childrenProp,
+ ...rest
+}) => {
+ const [pickerVisible, setPickerVisible] = React.useState(false);
+ const [internalValue, setInternalValue] = React.useState<
+ string | number | (string | number)[]
+ >();
+
+ const isMultiSelect = Array.isArray(value);
+
+ const pickerItemProps: PickerItemProps = React.useMemo(() => {
+ const children = flattenReactFragments(
+ React.Children.toArray(childrenProp) as React.ReactElement[]
+ );
+
+ // Only the props of the first PickerItem are used, any others are ignored
+ const firstPickerItem = children.find((child) => child.type === PickerItem);
+
+ return firstPickerItem?.props || {};
+ }, [childrenProp]);
+
+ const { viewStyles: pickerItemViewStyles, textStyles: pickerItemTextStyles } =
+ extractStyles(pickerItemProps.style);
+
+ const options = useDeepCompareMemo(
+ () =>
+ normalizeToPickerOptions(optionsProp).map((option) => ({
+ label: option.label.toString(),
+ value: option.value,
+ })),
+ [optionsProp]
+ );
+
+ React.useEffect(() => {
+ onValueChange?.(
+ (isMultiSelect ? internalValue ?? [] : internalValue ?? "") as any // cannot determine if multiselect or not on compile time
+ );
+ // onValueChange excluded to prevent running on every re-render when using an anoymous function, which is the common case
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [internalValue]);
+
+ React.useEffect(() => {
+ if (pickerVisible && autoDismissKeyboard) {
+ Keyboard.dismiss();
+ }
+ }, [pickerVisible, autoDismissKeyboard]);
+
+ return (
+ setPickerVisible(!pickerVisible)}
+ zIndex={pickerVisible ? 100 : undefined} // Guarantees drop down is rendered above all sibling components
+ {...rest}
+ >
+ (
+
+ )}
+ />
+
+ );
+};
+
+export default withTheme(DropDownPicker);
diff --git a/packages/core/src/components/Picker/dropdown/MultiSelectPicker.tsx b/packages/core/src/components/Picker/dropdown/MultiSelectPicker.tsx
new file mode 100644
index 000000000..370a0ea98
--- /dev/null
+++ b/packages/core/src/components/Picker/dropdown/MultiSelectPicker.tsx
@@ -0,0 +1,16 @@
+import * as React from "react";
+import {
+ CommonDropDownPickerProps,
+ MultiSelectPickerProps,
+} from "../PickerCommon";
+import DropDownPicker from "./DropDownPicker";
+import { withTheme } from "../../../theming";
+
+const MultiSelectPicker: React.FC<
+ React.PropsWithChildren
+> = ({ value, ...rest }) => {
+ //@ts-ignore Ignore theme type issues
+ return ;
+};
+
+export default withTheme(MultiSelectPicker);
diff --git a/packages/core/src/components/Picker/dropdown/PickerItem.tsx b/packages/core/src/components/Picker/dropdown/PickerItem.tsx
new file mode 100644
index 000000000..b9bac3cf3
--- /dev/null
+++ b/packages/core/src/components/Picker/dropdown/PickerItem.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+import { StyleProp, ViewStyle, TextStyle } from "react-native";
+
+export interface PickerItemProps {
+ selectedTextSize?: number;
+ selectedTextColor?: string;
+ selectedBackgroundColor?: string;
+ style?: StyleProp;
+}
+
+/**
+ * Renders nothing, only serves as a container for the props
+ * Prop values are used by the DropDownPicker
+ */
+export const PickerItem: React.FC = () => {
+ return null;
+};
+
+export default PickerItem;
diff --git a/packages/core/src/components/Picker/index.tsx b/packages/core/src/components/Picker/index.tsx
new file mode 100644
index 000000000..f4b66d6be
--- /dev/null
+++ b/packages/core/src/components/Picker/index.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import { CommonDropDownPickerProps, SinglePickerProps } from "./PickerCommon";
+import NativePicker from "./NativePicker";
+import DropDownPicker from "./dropdown/DropDownPicker";
+import { withTheme } from "../../theming";
+
+interface PickerProps extends CommonDropDownPickerProps, SinglePickerProps {
+ mode?: "native" | "dropdown";
+}
+
+const SinglePicker: React.FC> = ({
+ mode = "native",
+ ...rest
+}) => {
+ switch (mode) {
+ case "native":
+ //@ts-ignore Ignore theme type issues
+ return ;
+ case "dropdown":
+ //@ts-ignore
+ return ;
+ }
+};
+
+export const Picker = withTheme(SinglePicker);
+export { default as MultiSelectPicker } from "./dropdown/MultiSelectPicker";
+export { default as PickerItem } from "./dropdown/PickerItem";
diff --git a/packages/core/src/components/TextField.tsx b/packages/core/src/components/TextField.tsx
index 1cd292124..f046663d5 100644
--- a/packages/core/src/components/TextField.tsx
+++ b/packages/core/src/components/TextField.tsx
@@ -38,12 +38,14 @@ export type Props = {
text: string | NativeSyntheticEvent
) => void;
rightIconName?: string;
+ iconColor?: string;
+ iconSize?: number;
assistiveText?: string;
multiline?: boolean;
numberOfLines: number;
underlineColor?: string;
activeBorderColor?: string;
- style?: StyleProp & { height?: number };
+ style?: StyleProp;
theme: Theme;
render?: (
props: TextInputProps & { ref: (c: NativeTextInput) => void }
@@ -236,11 +238,14 @@ class TextField extends React.Component {
Icon,
type = "underline",
disabled = false,
+ editable = true,
label,
error = false,
leftIconName,
leftIconMode,
rightIconName,
+ iconColor,
+ iconSize,
assistiveText,
underlineColor: underlineColorProp,
activeBorderColor: activeBorderColorProp,
@@ -278,7 +283,7 @@ class TextField extends React.Component {
} else {
activeColor = error ? colors.error : activeBorderColorProp;
placeholderColor = borderColor = colors.light;
- underlineColor = underlineColorProp;
+ underlineColor = underlineColorProp || colors.light;
backgroundColor = colors.background;
}
@@ -344,7 +349,9 @@ class TextField extends React.Component {
}
let leftIconColor;
- if (error) {
+ if (iconColor) {
+ leftIconColor = iconColor;
+ } else if (error) {
leftIconColor = colors.error;
} else if (this.state.focused) {
leftIconColor = colors.primary;
@@ -353,7 +360,7 @@ class TextField extends React.Component {
}
const leftIconProps = {
- size: 24,
+ size: iconSize || 24,
color: leftIconColor,
name: leftIconName || "",
};
@@ -438,7 +445,7 @@ class TextField extends React.Component {
borderLeftWidth,
borderColor: borderCol,
...styleProp
- } = StyleSheet.flatten(style || {}) as ViewStyle & { height?: number };
+ } = StyleSheet.flatten(style || {}) as ViewStyle;
return (
@@ -447,7 +454,7 @@ class TextField extends React.Component {
) : null}
{
? this.state.placeholder
: this.props.placeholder,
placeholderTextColor: placeholderColor,
- editable: !disabled,
+ editable: !disabled && editable,
+ disabled: disabled || !editable,
selectionColor: activeColor,
multiline,
numberOfLines,
@@ -578,8 +586,8 @@ class TextField extends React.Component {
{rightIconName ? (
) {
const {
@@ -261,3 +261,35 @@ export function flattenReactFragments(
return flattened;
}
+
+function useDeepCompareMemoize(value: any) {
+ const ref = React.useRef();
+
+ if (!isEqual(value, ref.current)) {
+ ref.current = value;
+ }
+
+ return ref.current;
+}
+
+/**
+ * useMemo counterpart that does a deep compare on the dependency list
+ */
+export function useDeepCompareMemo(
+ factory: () => T,
+ deps: React.DependencyList | undefined
+): T {
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ return React.useMemo(factory, deps?.map(useDeepCompareMemoize));
+}
+
+/**
+ * useMemo counterpart that does a deep compare on the dependency list
+ */
+export function useDeepCompareEffect(
+ effect: React.EffectCallback,
+ deps: React.DependencyList | undefined
+) {
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ return React.useEffect(effect, deps?.map(useDeepCompareMemoize));
+}
diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx
index 08c2acbe1..72a3d8463 100644
--- a/packages/ui/src/index.tsx
+++ b/packages/ui/src/index.tsx
@@ -66,6 +66,7 @@ export {
HStack,
VStack,
ZStack,
+ PickerItem,
} from "@draftbit/core";
/**
@@ -91,6 +92,7 @@ import {
FieldSearchBarFull as BaseFieldSearchBarFull,
IconButton as BaseIconButton,
Picker as BasePicker,
+ MultiSelectPicker as BaseMultiSelectPicker,
StarRating as BaseStarRating,
TextField as BaseTextField,
RadioButton as BaseRadioButton,
@@ -120,6 +122,7 @@ export const FieldSearchBarFull = injectIcon(BaseFieldSearchBarFull, Icon);
export const IconButton = injectIcon(BaseIconButton, Icon);
export const Link = injectIcon(BaseLink, Icon);
export const Picker = injectIcon(BasePicker, Icon);
+export const MultiSelectPicker = injectIcon(BaseMultiSelectPicker, Icon);
export const RadioButton = injectIcon(BaseRadioButton, Icon);
export const RadioButtonRow = injectIcon(BaseRadioButtonRow, Icon);
export const RowBodyIcon = injectIcon(BaseRowBodyIcon, Icon);
diff --git a/yarn.lock b/yarn.lock
index ffe12929a..b3143c9c8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15337,6 +15337,11 @@ react-native-deck-swiper@^2.0.12:
dependencies:
prop-types "15.5.10"
+react-native-dropdown-picker@^5.4.6:
+ version "5.4.6"
+ resolved "https://registry.yarnpkg.com/react-native-dropdown-picker/-/react-native-dropdown-picker-5.4.6.tgz#3736fc468de4a295e4df8d1f65ed2eadaf9b445f"
+ integrity sha512-T1XBHbE++M6aRU3wFYw3MvcOuabhWZ29RK/Ivdls2r1ZkZ62iEBZknLUPeVLMX3x6iUxj4Zgr3X2DGlEGXeHsA==
+
react-native-fit-image@^1.5.5:
version "1.5.5"
resolved "https://registry.yarnpkg.com/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz#c660d1ad74b9dcaa1cba27a0d9c23837e000226c"