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 ( + + +