diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 12f5d8e20..000000000 --- a/.eslintignore +++ /dev/null @@ -1,16 +0,0 @@ -# don't ever lint node_modules -node_modules -package-lock.json -# don't lint build output -build -dist -# don't lint coverage output -coverage -# don't lint workflows etc -.github -.idea -.vscode -# unsupported file types -*.yaml -*.yml -*.md \ No newline at end of file diff --git a/.eslintrc.yaml b/.eslintrc.yaml deleted file mode 100644 index 6386cc9af..000000000 --- a/.eslintrc.yaml +++ /dev/null @@ -1,109 +0,0 @@ ---- -# docs at: https://eslint.org/docs/latest/use/configure/configuration-files -env: - browser: true - es2021: true - react-native/react-native: true -extends: - - airbnb - - airbnb-typescript - - airbnb/hooks - - eslint:recommended - - plugin:@typescript-eslint/recommended - - plugin:@typescript-eslint/stylistic - - plugin:eslint-comments/recommended - - plugin:import/errors - - plugin:import/recommended - - plugin:import/typescript - - plugin:import/warnings - - plugin:jsdoc/recommended - - plugin:json/recommended - - plugin:jsx-a11y/recommended - - plugin:react-hooks/recommended - - plugin:react-native/all - - plugin:react/recommended - - prettier -globals: - JSX: true -parser: '@typescript-eslint/parser' -parserOptions: - ecmaFeatures: - jsx: true - ecmaVersion: latest - project: ./tsconfig.json - sourceType: module -plugins: - - '@typescript-eslint' - - eslint-comments - - eslint-plugin-json - - import - - jsdoc - - jsx-a11y - - react - - react-hooks - - react-native -rules: - '@typescript-eslint/array-type': - - error - - default: 'generic' - readonly: 'generic' - '@typescript-eslint/explicit-function-return-type': error - '@typescript-eslint/explicit-module-boundary-types': error - '@typescript-eslint/no-explicit-any': - - error - - fixToUnknown: false - ignoreRestArgs: false - '@typescript-eslint/no-shadow': error - '@typescript-eslint/no-use-before-define': error - camelcase: error - comma-dangle: - - error - - always-multiline - comma-style: - - error - - last - import/extensions: - - error - - never - import/no-unresolved: error - jsdoc/check-indentation: error - jsdoc/no-bad-blocks: error - jsdoc/require-description: error - jsdoc/require-file-overview: error - jsdoc/require-throws: error - jsx-quotes: - - error - - prefer-single - linebreak-style: - - error - - unix - max-lines: - - error - - 300 - max-lines-per-function: - - warn - - max: 20 - no-console: error - no-duplicate-imports: error - no-multi-spaces: error - no-shadow: error - no-template-curly-in-string: error - no-trailing-spaces: error - no-undef: error - no-use-before-define: warn - react-native/no-inline-styles: warn - react/jsx-filename-extension: - - error - - extensions: - - .ts - - .tsx - - .js - - .jsx - react/prop-types: error - sort-imports: warn - sort-keys: - - error - - asc - - caseSensitive: true - minKeys: 2 - natural: true diff --git a/.prettierignore b/.prettierignore index 12f5d8e20..fc0af47e1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ package-lock.json # don't lint build output build dist +*.expo # don't lint coverage output coverage # don't lint workflows etc diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..7dfe0079a --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,109 @@ +import eslint from '@eslint/js'; +import prettier from 'eslint-config-prettier'; +import eslintComments from 'eslint-plugin-eslint-comments'; +import json from 'eslint-plugin-json'; +import jsxA11y from 'eslint-plugin-jsx-a11y'; +import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactNative from 'eslint-plugin-react-native'; +import simpleImportSort from 'eslint-plugin-simple-import-sort'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + eslint.configs.recommended, + tseslint.configs.recommended, + tseslint.configs.stylistic, + { + ignores: [ + 'node_modules', + 'package-lock.json', + 'build', + 'dist', + '*.expo', + 'coverage', + '.github', + '.idea', + '.vscode', + '*.yaml', + '*.yml', + '*.md', + ], + plugins: { + react, + 'react-hooks': reactHooks, + 'react-native': reactNative, + 'jsx-a11y': jsxA11y, + '@typescript-eslint': tseslint.plugin, + 'eslint-comments': eslintComments, + json, + simpleImportSort, + }, + languageOptions: { + parser: tseslint.parser, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 'latest', + project: './tsconfig.json', + sourceType: 'module', + }, + globals: { + JSX: true, + require: true, + module: true, + __dirname: true, + setTimeout: true, + }, + }, + rules: { + '@typescript-eslint/array-type': [ + 'error', + { + default: 'generic', + readonly: 'generic', + }, + ], + // '@typescript-eslint/explicit-function-return-type': 'error', + // '@typescript-eslint/explicit-module-boundary-types': 'error', + '@typescript-eslint/no-explicit-any': [ + 'error', + { + fixToUnknown: false, + ignoreRestArgs: false, + }, + ], + '@typescript-eslint/no-require-imports': 'off', // diabling until typescript rewrite + '@typescript-eslint/no-shadow': 'error', + '@typescript-eslint/no-empty-function': 'off', // disabling due to lots of functions defaulting to empt@typescript-eslint/no-empty-functiony + '@typescript-eslint/no-unused-vars': [ + 'error', + { + caughtErrors: 'none', + }, + ], + 'no-underscore-dangle': 'off', + 'comma-dangle': ['error', 'always-multiline'], + 'comma-style': ['error', 'last'], + 'jsx-quotes': ['error', 'prefer-single'], + 'linebreak-style': ['error', 'unix'], + 'no-console': 'error', + 'no-duplicate-imports': 'error', + 'no-multi-spaces': 'error', + 'no-shadow': 'error', + 'no-template-curly-in-string': 'error', + 'no-trailing-spaces': 'error', + 'no-undef': 'error', + 'react-native/no-inline-styles': 'warn', + 'react/jsx-filename-extension': [ + 'error', + { + extensions: ['.ts', '.tsx', '.js', '.jsx'], + }, + ], + 'simpleImportSort/imports': ['error'], + 'react/prop-types': 'off', // Disabling until typescript rewrite + }, + }, + prettier, +); diff --git a/examples/.gitignore b/examples/.gitignore index 05647d55c..161b2b53d 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -7,6 +7,7 @@ node_modules/ .expo/ dist/ web-build/ +ios # Native *.orig.* diff --git a/examples/App.tsx b/examples/App.tsx index 3773bc9f4..1ab21bffb 100644 --- a/examples/App.tsx +++ b/examples/App.tsx @@ -1,160 +1,152 @@ import React, { JSX } from 'react'; -import { StyleSheet, Text, View } from 'react-native'; -import DropDownPicker, { ItemType } from 'react-native-dropdown-picker'; -import JavascriptClassExample from './example-src-files/javascript-class-example'; -import JavascriptFunctionExample from './example-src-files/javascript-function-example'; -import TypescriptClassExample from './example-src-files/typescript-class-example'; -import TypescriptFunctionExample from './example-src-files/typescript-function-example'; +import { FlatList, StyleSheet, useColorScheme, View } from 'react-native'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; -enum ExampleComponent { - JavaScriptClassSingleValue, - JavaScriptClassMultiValue, - JavaScriptFunctionSingleValue, - JavaScriptFunctionMultiValue, - TypeScriptClassSingleValue, - TypeScriptClassMultiValue, - TypeScriptFunctionSingleValue, - TypeScriptFunctionMultiValue, -} +import DropDownPickerExample, { + ExampleProps, +} from './example-src-files/example'; -const styles = StyleSheet.create({ - container: { - flex: 1, - // backgroundColor: "#fff", - // alignItems: "center", - // justifyContent: "center", - flexDirection: 'column', - margin: 3, - marginTop: 20, - padding: 3, +const EXAMPLES: Array = [ + { + title: 'Default Example', + description: 'This is the default dropdown picker', }, -}); - -const EXAMPLE_COMPONENT_ITEMS: Array> = [ { - label: 'JavaScript; class component; single-item', - value: ExampleComponent.JavaScriptClassSingleValue, + title: 'Multiple Select', + description: 'Multiple select example', + dropdownProps: { multiple: true }, }, { - label: 'JavaScript; class component; multiple-item', - value: ExampleComponent.JavaScriptClassMultiValue, + title: 'Multiple Select Badge Mode', + description: 'Multiple select example - with badges', + dropdownProps: { multiple: true, mode: 'BADGE', showBadgeDot: false }, }, { - label: 'JavaScript; function component; single-item', - value: ExampleComponent.JavaScriptFunctionSingleValue, + title: 'Multiple Select Badge Mode with Dots', + description: 'Multiple select example - with badges and dots', + dropdownProps: { multiple: true, mode: 'BADGE', showBadgeDot: true }, }, { - label: 'JavaScript; function component; multiple-item', - value: ExampleComponent.JavaScriptFunctionMultiValue, + title: 'Customized Multiple Select Badge Mode', + description: 'Multiple select example - with badges', + dropdownProps: { + multiple: true, + mode: 'BADGE', + showBadgeDot: false, + badgeDotStyle: {}, + badgeColors: '#d5c4a1', // Badge Colors currentlly overwites badgeStyle background color + placeholderStyle: { color: '#83a598' }, + badgeStyle: { + // background: '#d5c4a1', + borderColor: '#282828', + borderWidth: 2, + borderStyle: 'solid', + }, + badgeTextStyle: { + color: '#282828', + }, + style: { + backgroundColor: '#fbf1c7', + borderColor: '#b16286', + }, + customItemContainerStyle: {}, + listItemContainerStyle: { + backgroundColor: '#fbf1c7', + borderColor: '#b16286', + }, + listItemLabelStyle: { + color: '#b16286', + }, + }, }, { - label: 'TypeScript; class component; single-item', - value: ExampleComponent.TypeScriptClassSingleValue, + title: 'Autoscroll Example', + description: 'This is the default dropdown picker - with autoscroll', + dropdownProps: { autoScroll: true }, }, { - label: 'TypeScript; class component; multiple-item', - value: ExampleComponent.TypeScriptClassMultiValue, + title: 'Searchable Example', + description: 'This is the default dropdown picker - with search', + dropdownProps: { searchable: true }, }, { - label: 'TypeScript; function component; single-item', - value: ExampleComponent.TypeScriptFunctionSingleValue, + title: 'Multiple Search Example', + description: 'This is the default dropdown picker - with search', + + dropdownProps: { multiple: true, searchable: true }, }, { - label: 'TypeScript; function component; multiple-item', - value: ExampleComponent.TypeScriptFunctionMultiValue, + title: 'Multiple Search Clear on Select Example', + description: 'This is the default dropdown picker - with search', + dropdownProps: { + multiple: true, + searchable: true, + clearSearchFieldOnSelect: true, + mode: 'BADGE', + }, + }, + { + title: 'Modal Example', + description: 'This is the default dropdown picker - with search', + dropdownProps: { multiple: true, listMode: 'MODAL' }, }, ]; -type Props = Record; - -interface State { - currentExample: ExampleComponent; - examplePickerOpen: boolean; - exampleComponents: Array>; -} - -export default class App extends React.Component { - constructor(props: Readonly) { - super(props); - this.state = { - currentExample: ExampleComponent.JavaScriptClassSingleValue, - exampleComponents: EXAMPLE_COMPONENT_ITEMS, - examplePickerOpen: false, - }; - - this.setOpen = this.setOpen.bind(this); - this.setCurrentExample = this.setCurrentExample.bind(this); - } - - private static getExample(egComponent: ExampleComponent): JSX.Element { - switch (egComponent) { - case ExampleComponent.JavaScriptClassSingleValue: - return ; - case ExampleComponent.JavaScriptClassMultiValue: - return ; - case ExampleComponent.JavaScriptFunctionSingleValue: - return ; - case ExampleComponent.JavaScriptFunctionMultiValue: - return ; - case ExampleComponent.TypeScriptClassSingleValue: - return ; - case ExampleComponent.TypeScriptClassMultiValue: - return ; - case ExampleComponent.TypeScriptFunctionSingleValue: - return ; - case ExampleComponent.TypeScriptFunctionMultiValue: - return ; - default: - throw new Error( - "couldn't match example component in getExample() in App.tsx. egComponent was: ", - egComponent, - ); - } - } - - setOpen(examplePickerOpen: boolean): void { - this.setState({ examplePickerOpen }); - } - - setCurrentExample( - callback: (prevState: ExampleComponent | null) => ExampleComponent | null, - ): void { - this.setState((state: Readonly) => ({ - currentExample: callback(state.currentExample), - })); - } - - // todo: fix picker items being under text - - render(): JSX.Element { - const { currentExample, examplePickerOpen, exampleComponents } = this.state; - - return ( - - - - Choose example: - - - - - - - - - - Example: - - - {App.getExample(currentExample)} - +export default function App(): JSX.Element { + const colorScheme = useColorScheme(); + const backgroundColor = colorScheme === 'dark' ? '#222' : '#fff'; + + const renderItem = ({ item }: { item: ExampleProps }) => ( + + ); + + return ( + + + example.title} + renderItem={renderItem} + contentContainerStyle={styles.container} + showsVerticalScrollIndicator={false} + CellRendererComponent={({ children }) => ( + // Remove flatlsit view that wraps children for dropdown zIndex support + <>{children} + )} + /> - ); - } + + ); } + +const styles = StyleSheet.create({ + page: { + flex: 1, + }, + container: { + position: 'relative', + flexDirection: 'column', + margin: 'auto', + zIndex: 1, + marginTop: 64, + marginBottom: 64, + padding: 3, + maxWidth: 600, + minWidth: 400, + }, + examplesContainer: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-start', + gap: 32, + }, + exampleCard: { + zIndex: 0, + borderRadius: 8, + marginBottom: 48, + }, +}); diff --git a/examples/babel.config.js b/examples/babel.config.js index 9d89e1311..d872de3f5 100644 --- a/examples/babel.config.js +++ b/examples/babel.config.js @@ -2,5 +2,6 @@ module.exports = function (api) { api.cache(true); return { presets: ['babel-preset-expo'], + plugins: ['react-native-reanimated/plugin'], }; }; diff --git a/examples/example-src-files/CodeSnippet.tsx b/examples/example-src-files/CodeSnippet.tsx new file mode 100644 index 000000000..c282bd18d --- /dev/null +++ b/examples/example-src-files/CodeSnippet.tsx @@ -0,0 +1,89 @@ + +import React, { JSX, useState } from 'react'; +import { + LayoutChangeEvent, + Pressable, + StyleSheet, + Text, + View, + useColorScheme, +} from 'react-native'; +import Animated, { + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated'; +// temporarily disabled react-native-syntax-highlighter as it is being buggy :( +// todo: resolve issues with react-syntax-highlighter +// import { ghcolors, tomorrow } from 'react-syntax-highlighter/styles/prism'; +// import SyntaxHighlighter from 'react-native-syntax-highlighter'; + +interface CodeSnippetProps { + code: string; +} + +export default function CodeSnippet({ code }: CodeSnippetProps): JSX.Element { + const colorScheme = useColorScheme(); + const backgroundColor = colorScheme === 'dark' ? '#333' : '#f5f5f5'; + const color = colorScheme === 'dark' ? '#fff' : '#222'; + // const syntaxStyle = colorScheme === 'dark' ? tomorrow : ghcolors; + + const [isExpanded, setIsExpanded] = useState(false); + const height = useSharedValue(0); + const contentHeight = useSharedValue(0); + + const animatedStyle = useAnimatedStyle(() => ({ + height: height.value, + })); + + const toggle = () => { + height.value = withTiming(isExpanded ? 0 : contentHeight.value, { + duration: 300, + }); + setIsExpanded(!isExpanded); + }; + + const onLayout = (event: LayoutChangeEvent) => { + contentHeight.value = event.nativeEvent.layout.height; + }; + + return ( + + + + {isExpanded ? 'Hide Code' : 'Show Code'} + + + + + + {/* */} + {code} + + {/* */} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + marginVertical: 10, + borderRadius: 5, + overflow: 'hidden', + }, + header: { + padding: 10, + }, + headerText: { + fontWeight: 'bold', + }, + codeContainer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + padding: 16 + }, +}); diff --git a/examples/example-src-files/example.tsx b/examples/example-src-files/example.tsx new file mode 100644 index 000000000..076a0b5a3 --- /dev/null +++ b/examples/example-src-files/example.tsx @@ -0,0 +1,187 @@ +import React, { JSX, useMemo, useState } from 'react'; +import { Button, StyleSheet, Text, useColorScheme, View } from 'react-native'; +import DropDownPicker, { + DropDownPickerIsMultipleProps, + DropDownPickerIsSingleProps, + ItemType, + ValueType, +} from 'react-native-dropdown-picker'; +import CodeSnippet from './CodeSnippet'; + +// Omit these types from the example props +type CommonOmitKeys = 'setValue' | 'value' | 'open' | 'items' | 'setOpen'; + +type SingleDropdownProps = Omit< + Partial>, + CommonOmitKeys +>; + +type MultipleDropdownProps = Omit< + Partial>, + CommonOmitKeys +>; + +export interface ExampleProps { + title: string; + description?: string; + code?: string; + placeholder?: string; + multipleText?: string; + items?: Array>; + dropdownProps?: MultipleDropdownProps | SingleDropdownProps; +} + +const DEFAULT_ITEMS = [ + { label: 'Apple', value: 'apple' }, + { label: 'Banana', value: 'banana' }, + { label: 'Nectarines', value: 'nectarines' }, + { label: 'Kiwis', value: 'kiwis' }, + { label: 'Raspberries', value: 'raspberries' }, + { label: 'Pears', value: 'pears' }, + { label: 'Guava', value: 'guava' }, + { label: 'Grapes', value: 'grapes' }, + { label: 'Manderins', value: 'manderins' }, + { label: 'Pineapple', value: 'pineapple' }, + { label: 'Dragon Fruit', value: 'dragon_fruit' }, + { label: 'Prickly Pear', value: 'prickly_pear' }, +]; + +const styles = StyleSheet.create({ + title: { + fontSize: 24, + marginBottom: 4, + fontWeight: 'bold', + }, + description: { + fontSize: 12, + marginBottom: 16, + }, + body: { + fontSize: 12, + marginBottom: 72, + }, + exampleContainer: { + display: 'flex', + flexDirection: 'column', + position: 'relative', + gap: 16, + }, + dropdownContainer: { + zIndex: 1, + }, +}); + +/** + * + * @param props + * @param props.multiple + */ +export default function DropDownPickerExample({ + title, + description, + dropdownProps, + placeholder = 'Choose a fruit', + multipleText = 'You have chosen {count} fruits.', + items = DEFAULT_ITEMS, +}: ExampleProps): JSX.Element { + const [open, setOpen] = useState(false); + const [singleValue, setSingleValue] = useState(null); + const [multiValue, setMultiValue] = useState | null>(null); + const colorScheme = useColorScheme(); + const color = colorScheme === 'dark' ? '#fff' : '#222'; + const theme = colorScheme === 'dark' ? 'DARK' : 'LIGHT'; + + const [_items, setItems] = useState>>(items); + const isMultiple = dropdownProps?.multiple; + + const RenderDropDown = () => { + if(isMultiple) { + const {..._dropdownProps} = dropdownProps; + delete _dropdownProps.multiple // remove multiple prop to hard set as true + return ( + + open={open} + value={multiValue} + items={_items} + setOpen={setOpen} + setValue={setMultiValue} + setItems={setItems} + theme={theme} + placeholder={placeholder} + multipleText={multipleText} + multiple + {..._dropdownProps as MultipleDropdownProps} + /> + ) + } else { + return ( + + open={open} + value={singleValue} + items={_items} + setOpen={setOpen} + setValue={setSingleValue} + setItems={setItems} + theme={theme} + placeholder={placeholder} + multiple={false} + {...dropdownProps as SingleDropdownProps} + /> + ) + } + } + + return ( + // eslint-disable-next-line react-native/no-inline-styles + + + {title} + {description && ( + {description} + )} + + + {RenderDropDown()} + + + + {isMultiple ? 'Fruits currently are: ' : 'Fruit currently is: '} + {isMultiple ? JSON.stringify(multiValue) : JSON.stringify(singleValue)} + + +