├── .babelrc ├── .codeclimate.yml ├── .commitlintrc.json ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml ├── mergify.yml └── workflows │ ├── build.yml │ ├── commitlint.yml │ ├── dependabot.yml │ ├── main.yml │ └── release-please.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .nvmrc ├── .prettierrc.json ├── .release-please-manifest.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ex-android.gif ├── ex-ios.gif ├── index.d.ts ├── package.json ├── release-please-config.json ├── src ├── index.js └── styles.js ├── test ├── setup.js └── test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react-native" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: '2' # required to adjust maintainability checks 2 | checks: 3 | argument-count: 4 | config: 5 | threshold: 4 6 | complex-logic: 7 | config: 8 | threshold: 4 9 | file-lines: 10 | config: 11 | threshold: 500 12 | method-complexity: 13 | config: 14 | threshold: 5 15 | method-count: 16 | config: 17 | threshold: 20 18 | method-lines: 19 | config: 20 | threshold: 50 21 | nested-control-flow: 22 | config: 23 | threshold: 4 24 | return-statements: 25 | config: 26 | threshold: 4 27 | similar-code: 28 | config: 29 | threshold: 75 # language-specific defaults. an override will affect all languages. 30 | identical-code: 31 | config: 32 | threshold: 50 # language-specific defaults. an override will affect all languages. 33 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["./node_modules/cz-ls-commits/commitlint"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /**/*.d.ts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ['plugin:react/recommended', 'airbnb', 'airbnb/hooks'], 7 | plugins: ['react', 'react-native', 'prettier'], 8 | overrides: [ 9 | { 10 | env: { 11 | node: true, 12 | }, 13 | files: ['.eslintrc.{js,cjs}'], 14 | parserOptions: { 15 | sourceType: 'script', 16 | }, 17 | }, 18 | ], 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | }, 23 | rules: { 24 | 'arrow-body-style': 0, 25 | 'comma-dangle': 0, 26 | 'default-param-last': 0, 27 | 'import/no-extraneous-dependencies': 0, 28 | 'import/prefer-default-export': 0, 29 | 'object-curly-newline': 0, 30 | 'operator-linebreak': 0, 31 | 'prettier/prettier': 'error', 32 | 'react/destructuring-assignment': 0, 33 | 'react/forbid-prop-types': 0, 34 | 'react/jsx-filename-extension': 0, 35 | 'react/jsx-props-no-spreading': 0, 36 | 'react/no-arrow-function-lifecycle': 0, 37 | 'react/no-unused-class-component-methods': 0, 38 | 'react/prop-types': 0, 39 | 'react/static-property-placement': 0, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Report a reproducible bug or regression. 4 | 5 | --- 6 | 7 | 8 | 9 | 10 | **Describe the bug**
11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce**
14 | Steps to reproduce the behavior: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior**
22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots**
25 | Add screenshots to help explain your problem. If screenshots aren't applicable to this issue, write "n/a". 26 | 27 | **Additional details**
28 | - Device: [e.g. iPhone6] 29 | - OS: [e.g. iOS8.1] 30 | - react-native-picker-select version: [e.g. 4.3.0] 31 | - react-native version: [e.g. 0.56] 32 | - expo sdk version: [e.g. 38 or n/a] 33 | 34 | **Reproduction and/or code sample**
35 | Provide a link to a reproduction of this issue on https://snack.expo.io **or an explanation why you can not**. Not including a snack link will result in a significant delay in our ability to address this issue. 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: ❓Questions about how to use this library? 4 | url: https://stackoverflow.com/questions/tagged/react-native-picker-select 5 | about: Please read the documentation thoroughly, search through existing issues, and then ask any lingering questions on StackOverflow. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | about: Suggest an idea for this component. 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional details**
17 | Add any other details or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' 9 | directory: '/' 10 | schedule: 11 | interval: weekly 12 | time: '10:00' 13 | timezone: America/New_York 14 | commit-message: 15 | prefix: 'build' 16 | prefix-development: 'build' 17 | include: 'scope' 18 | groups: 19 | prettier: 20 | patterns: 21 | - prettier 22 | - eslint-plugin-prettier 23 | 24 | - package-ecosystem: 'github-actions' 25 | directory: '/' 26 | schedule: 27 | interval: weekly 28 | time: '10:00' 29 | timezone: America/New_York 30 | -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge for Dependabot pull requests 3 | conditions: 4 | - author=dependabot[bot] 5 | - check-success=build 6 | - check-success=commitlint 7 | - 'title~=^build\(deps-dev\): bump ' 8 | actions: 9 | merge: 10 | method: squash 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v4 11 | 12 | - name: Use Node.js 13 | uses: actions/setup-node@v4 14 | with: 15 | cache: yarn 16 | node-version-file: .nvmrc 17 | 18 | - name: Install dependencies 19 | run: yarn 20 | 21 | - name: Run ESLint 22 | run: yarn lint 23 | 24 | - name: Setup Code Climate 25 | uses: remarkablemark/setup-codeclimate@v2 26 | 27 | - name: Run Tests and Upload Coverage 28 | run: | 29 | cc-test-reporter before-build 30 | yarn test:coverage 31 | cc-test-reporter after-build --exit-code $? 32 | env: 33 | CC_TEST_REPORTER_ID: 9b36beb22e04451e6414fcc28926f995c253d94877f616b50d192255196fbb68 34 | 35 | - name: Codecov 36 | uses: codecov/codecov-action@v5 37 | env: 38 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: commitlint 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | commitlint: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | 14 | - name: Use Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | cache: yarn 18 | node-version-file: .nvmrc 19 | 20 | - name: Install dependencies 21 | run: yarn 22 | 23 | - name: Lint commit message 24 | run: yarn commitlint --from=HEAD~1 25 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: dependabot 2 | on: pull_request_target 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: github.actor == 'dependabot[bot]' && contains(github.event.pull_request.title, 'deps-dev') 12 | steps: 13 | - name: Approve Dependabot PR 14 | run: gh pr review --approve ${{ github.event.pull_request.html_url }} 15 | env: 16 | GITHUB_TOKEN: ${{ github.token }} 17 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Autocloser 2 | on: [issues] 3 | jobs: 4 | autoclose: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Autoclose issues that did not follow issue template 8 | uses: roots/issue-closer@v1.2 9 | with: 10 | repo-token: ${{ secrets.GH_TOKEN }} 11 | issue-close-message: "@${issue.user.login} this issue was automatically closed because it did not follow one of the available issue templates. See [here](https://github.com/lawnstarter/react-native-picker-select/issues/new/choose) for available options." 12 | issue-pattern: ".*Additional detail*" 13 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | release-please: 9 | runs-on: ubuntu-latest 10 | outputs: 11 | release_created: ${{ steps.release.outputs.release_created }} 12 | 13 | steps: 14 | - name: Release Please 15 | uses: google-github-actions/release-please-action@v4 16 | id: release 17 | with: 18 | token: ${{ secrets.GH_TOKEN }} 19 | 20 | publish: 21 | needs: release-please 22 | runs-on: ubuntu-latest 23 | if: ${{ needs.release-please.outputs.release_created }} 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Use Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | registry-url: https://registry.npmjs.org 33 | 34 | - name: Install dependencies 35 | run: yarn 36 | 37 | - name: Publish 38 | run: npm publish 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | coverage/ 4 | .vscode/ 5 | .npm/ -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | yarn commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint 2 | yarn test:coverage 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "arrowParens": "always" 6 | } 7 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "9.3.1" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [9.3.1](https://github.com/lawnstarter/react-native-picker-select/compare/v9.3.0...v9.3.1) (2024-08-12) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * **types:** add prop testID to index.d.ts ([#605](https://github.com/lawnstarter/react-native-picker-select/issues/605)) ([3fbe1cf](https://github.com/lawnstarter/react-native-picker-select/commit/3fbe1cfa7681988bc6edb1f020be726c76269041)) 9 | 10 | ## [9.3.0](https://github.com/lawnstarter/react-native-picker-select/compare/v9.2.0...v9.3.0) (2024-08-12) 11 | 12 | 13 | ### Features 14 | 15 | * apply custom styling to active dropdown item ([#609](https://github.com/lawnstarter/react-native-picker-select/issues/609)) ([4626a4e](https://github.com/lawnstarter/react-native-picker-select/commit/4626a4e595c2143020771dbb2fc7838dee8daa83)), closes [#608](https://github.com/lawnstarter/react-native-picker-select/issues/608) 16 | 17 | ## [9.2.0](https://github.com/lawnstarter/react-native-picker-select/compare/v9.1.3...v9.2.0) (2024-07-26) 18 | 19 | 20 | ### Features 21 | 22 | * add prop dropdownItemStyle ([#600](https://github.com/lawnstarter/react-native-picker-select/issues/600)) ([b5f19ad](https://github.com/lawnstarter/react-native-picker-select/commit/b5f19ad02a7cb06d00dd681f54358d2a21712262)) 23 | 24 | ## [9.1.3](https://github.com/lawnstarter/react-native-picker-select/compare/v9.1.2...v9.1.3) (2024-04-01) 25 | 26 | 27 | ### Documentation 28 | 29 | * **readme:** update README.md ([#331](https://github.com/lawnstarter/react-native-picker-select/issues/331)) ([36bca74](https://github.com/lawnstarter/react-native-picker-select/commit/36bca74fecaa66bcf17b65a360e423db692c3f13)) 30 | 31 | ## [9.1.2](https://github.com/lawnstarter/react-native-picker-select/compare/v9.1.1...v9.1.2) (2024-04-01) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * **types:** missing type definition for 'donePressed' parameter in 'onClose' callback ([#545](https://github.com/lawnstarter/react-native-picker-select/issues/545)) ([d39f880](https://github.com/lawnstarter/react-native-picker-select/commit/d39f880d1d866a81a84b5b1dd70e00f6001a3572)) 37 | 38 | ## [9.1.1](https://github.com/lawnstarter/react-native-picker-select/compare/v9.1.0...v9.1.1) (2024-03-29) 39 | 40 | ### Build System 41 | 42 | - **package:** release 9.1.1 ([f7c7646](https://github.com/lawnstarter/react-native-picker-select/commit/f7c764608f58598422b92fee19d3a96e5124c508)) 43 | 44 | ## 9.1.0 45 | 46 | ##### Bugfix 47 | 48 | - Improve comparison in getSelectedItem (#543) 49 | 50 | ## 9.0.1 51 | 52 | ##### Bugfix 53 | 54 | - Correct types for PickerStyle interface (#528) 55 | - Fix Icon prop type (#529) 56 | 57 | ## 9.0.0 58 | 59 | ##### Breaking Changes 60 | 61 | - Moved `react-native-picker` to peerDependencies and upgraded to ^2.4.0 (#523) 62 | 63 | --- 64 | 65 | ## 8.1.0 66 | 67 | ##### New 68 | 69 | - Dark mode support (#513) 70 | - donePressed on onClose callback (#319) 71 | - testID available on each item (#498) 72 | 73 | ##### Bugfix 74 | 75 | - Fixed reliance on now-private dep (#513) 76 | 77 | --- 78 | 79 | ## 8.0.4 80 | 81 | ##### Bugfix 82 | 83 | - Moved dep to @react-native-picker/picker 84 | 85 | --- 86 | 87 | ## 8.0.3 88 | 89 | ##### Bugfix 90 | 91 | - Fix `Cannot update component inside function` error (#346) 92 | 93 | --- 94 | 95 | ## 8.0.2 96 | 97 | ##### Bugfix 98 | 99 | - Add `fixAndroidTouchableBug` prop (#354) 100 | 101 | --- 102 | 103 | ## 8.0.1 104 | 105 | ##### Bugfix 106 | 107 | - Locked @react-native-community/picker to 1.6.0 to fix Expo issues 108 | - Add togglePicker method to Picker component typescript definition (#360) 109 | - Fix wrong PickerProps import in index.d.ts (#352) 110 | - Fixed inputWeb to be a TextStyle, not ViewStyle (#365) 111 | 112 | --- 113 | 114 | ## 8.0.0 115 | 116 | ##### Breaking Changes 117 | 118 | - Now using [@react-native-community/picker](https://github.com/react-native-community/react-native-picker#readme) under the hood (#340). For that reason, this library now requires React Native 0.60 or above. If using Expo, SDK38 or above is required. 119 | - Replaced item prop `displayValue` with `inputLabel` (#336) 120 | 121 | ##### New 122 | 123 | - Added web support (#316) 124 | 125 | --- 126 | 127 | ## 7.0.0 128 | 129 | ##### Breaking Changes 130 | 131 | - Deprecated prop `hideDoneBar` has been removed 132 | - Deprecated prop `placeholderTextColor` has been removed 133 | - Type definitions rewritten (#305) 134 | 135 | ##### Chore 136 | 137 | - Remove deprecated ColorPropType 138 | 139 | --- 140 | 141 | ### 6.6.0 142 | 143 | ##### New 144 | 145 | - Updated touchables to all be all TouchableOpacity (with override props available) 146 | - Done text now animates on depress like native select dialog (#215) 147 | 148 | --- 149 | 150 | ### 6.5.1 151 | 152 | ##### Bugfix 153 | 154 | - Update iOS colors (#281) 155 | 156 | --- 157 | 158 | ### 6.5.0 159 | 160 | ##### New 161 | 162 | - If an item has the `displayValue` property set to true, the TextInput shows the item `value` instead of the item `label` (#279) 163 | 164 | --- 165 | 166 | ### 6.4.0 167 | 168 | ##### New 169 | 170 | - Opened up `onOpen` prop to now support Android when in headless or `useNativeAndroidPickerStyle={false}` mode 171 | 172 | --- 173 | 174 | ### 6.3.4 175 | 176 | ##### Bugfix 177 | 178 | - Fix for `onDonePress` regression (#236) 179 | - "Done" Text element now set to `allowFontScaling={false}` (#247) 180 | 181 | --- 182 | 183 | ### 6.3.3 184 | 185 | ##### Chore 186 | 187 | - Split off styles into separate file 188 | 189 | --- 190 | 191 | ### 6.3.2 192 | 193 | ##### Bugfix 194 | 195 | - Update typescript definition file to add `InputAccessoryView` 196 | 197 | --- 198 | 199 | ### 6.3.1 200 | 201 | ##### Bugfix 202 | 203 | - Fix Done button on iPad (#209) 204 | 205 | --- 206 | 207 | ### 6.3.0 208 | 209 | ##### New 210 | 211 | - Added a prop called `InputAccessoryView` to allow a custom component to replace the InputAccessoryView on iOS. View the [snack](https://snack.expo.io/@lfkwtz/react-native-picker-select) to see examples on how this can be customized. As a result of this change, the `hideDoneBar` prop has been deprecated. 212 | - iOS modal window now correctly resizes on orientation change 213 | - `defaultStyles` are now exported 214 | 215 | --- 216 | 217 | ### 6.2.0 218 | 219 | ##### New 220 | 221 | - Supports an empty `items` array (#161) 222 | 223 | --- 224 | 225 | ### 6.1.1 226 | 227 | ##### Bugfix 228 | 229 | - Replaced setTimeouts with callbacks for arrow buttons (#177) 230 | 231 | --- 232 | 233 | ### 6.1.0 234 | 235 | ##### New 236 | 237 | - Opened up `placeholder` on style object for modification (#119) (#155). The `placeholderTextColor` prop is now deprecated, as this style object allows for additional properties. 238 | 239 | --- 240 | 241 | ## 6.0.0 242 | 243 | #### Breaking Changes 244 | 245 | - In order to make this component less opinionated, especially in terms of style, we have removed the default dropdown arrow icon in leiu of a more flexible `Icon` prop which will render a component - allowing you to insert your own css, image, svg, or icon from any library of your choosing. Due to this change, the `noIcon` prop has been removed. To replicate the arrow from previous versions, see the [last example](example/example.js) / see the styling section in the README for more details. 246 | - In Android, we no longer insert a psuedo-underline by default - as the default input style in React Native sets the underline color to transparent since [this PR](https://github.com/facebook/react-native/commit/a3a98eb1c7fa0054a236d45421393874ce8ce558) - which landed in 0.56. You can add this back in fairly easily, either by using the `textInputProps` prop or by adding a border on one of the wrapping container elements - all depending on your personal usage of the component. 247 | - Some of the default styles of the iOS "Done bar" have been tweaked and streamlined 248 | - if using useNativeAndroidPickerStyle={false}, the outer container is now only `headlessAndroidContainer` without `viewContainer` wrapping it 249 | 250 | --- 251 | 252 | ### 5.2.5 253 | 254 | ##### Bugfix 255 | 256 | - Fix headless Android onValueChange trigger on render (#141) 257 | 258 | --- 259 | 260 | ### 5.2.4 261 | 262 | #### Bugfix 263 | 264 | - Fix TypeError (#139) 265 | 266 | --- 267 | 268 | ### 5.2.3 269 | 270 | ##### Bugfix 271 | 272 | - Fixes Android headless mode trigger area (#122) 273 | 274 | --- 275 | 276 | ### 5.2.2 277 | 278 | ##### Bugfix 279 | 280 | - Fixes unnecessary renders (#129) 281 | 282 | --- 283 | 284 | ### 5.2.1 285 | 286 | ##### Bugfix 287 | 288 | - Fixes keyboard not dismissing on iOS 289 | 290 | --- 291 | 292 | ### 5.2.0 293 | 294 | ##### New 295 | 296 | - Added `onOpen` and `onClose` callbacks (iOS only) 297 | 298 | --- 299 | 300 | ### 5.1.1 301 | 302 | ##### New 303 | 304 | - Opened up headlessAndroidPicker and chevronContainer on style object for modification 305 | 306 | --- 307 | 308 | ### 5.1.0 309 | 310 | ##### New 311 | 312 | - Added `useNativeAndroidPickerStyle` prop. See README for more details. 313 | 314 | ##### Bugfix 315 | 316 | - Fixed Android headless mode showing selected value outside of View (#83) 317 | 318 | --- 319 | 320 | ### 5.0.1 321 | 322 | ##### Bugfix 323 | 324 | - Fixed `TouchableWithoutFeedback` warning 325 | 326 | --- 327 | 328 | ## 5.0.0 329 | 330 | #### Breaking Changes 331 | 332 | - `styles.placeholderColor` has been replaced with `placeholderTextColor` 333 | - `mode` prop is now accessible via `pickerProps` 334 | - `animationType` prop is now accessible via `modalProps` (see warning in README) 335 | 336 | ##### New 337 | 338 | - Default placeholder now includes default `color` of #9EA0A4 339 | - `pickerProps`, `modalProps`, and `textInputProps` have been added (see README) 340 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to react-native-picker-select 2 | 3 | #### **Did you write a patch that fixes a bug?** 4 | 5 | - Ensure that you link your PR to an open issue. If one is not open, use the "Bug report" template to create one. 6 | 7 | #### **Do you intend to add a new feature or change an existing one?** 8 | 9 | - First create an issue using the "Feature request" template and make a note that you intend to make this change. 10 | 11 | - Before opening the PR, please wait for a response from our team. 12 | 13 | #### **Looking for inspiration?** 14 | 15 | - See the [Feature Requests](https://github.com/lawnstarter/react-native-picker-select/projects/1) board and feel free to submit a PR for anything in the "To do" column. 16 | 17 | - If you're interested in working on anything in the "Under consideration" column - please first respond to the issue with your ideas on how you'd implement the feature. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) LawnStarter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-picker-select 2 | 3 | [![npm version](https://badge.fury.io/js/react-native-picker-select.svg)](https://badge.fury.io/js/react-native-picker-select) 4 | [![npm downloads](https://img.shields.io/npm/dm/react-native-picker-select.svg?style=flat-square)](https://www.npmjs.com/package/react-native-picker-select) 5 | [![Test Coverage](https://api.codeclimate.com/v1/badges/095f5b1ee137705ed382/test_coverage)](https://codeclimate.com/github/lawnstarter/react-native-picker-select/test_coverage) 6 | [![build](https://github.com/lawnstarter/react-native-picker-select/actions/workflows/build.yml/badge.svg)](https://github.com/lawnstarter/react-native-picker-select/actions/workflows/build.yml) 7 | 8 | A Picker component for React Native which emulates the native ` interfaces for each platform", 5 | "license": "MIT", 6 | "author": "Michael Lefkowitz ", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/lawnstarter/react-native-picker-select.git" 10 | }, 11 | "main": "src/index.js", 12 | "keywords": [ 13 | "dropdown", 14 | "picker", 15 | "select", 16 | "react", 17 | "react-native", 18 | "react native", 19 | "expo", 20 | "items" 21 | ], 22 | "files": [ 23 | "index.d.ts", 24 | "/src" 25 | ], 26 | "dependencies": { 27 | "lodash.isequal": "^4.5.0", 28 | "lodash.isobject": "^3.0.2" 29 | }, 30 | "devDependencies": { 31 | "@commitlint/cli": "^19.2.1", 32 | "@react-native-picker/picker": "^2.4.0", 33 | "@types/react-native": "^0.60.22", 34 | "babel-jest": "^23.6.0", 35 | "babel-preset-react-native": "^4.0.1", 36 | "cz-ls-commits": "^1.1.0", 37 | "enzyme": "^3.7.0", 38 | "enzyme-adapter-react-16": "^1.7.0", 39 | "enzyme-to-json": "^3.3.5", 40 | "eslint": "^8.2.0", 41 | "eslint-config-airbnb": "^19.0.4", 42 | "eslint-plugin-import": "^2.25.3", 43 | "eslint-plugin-jsx-a11y": "^6.5.1", 44 | "eslint-plugin-prettier": "^5.1.3", 45 | "eslint-plugin-react": "^7.28.0", 46 | "eslint-plugin-react-hooks": "^4.3.0", 47 | "eslint-plugin-react-native": "^5.0.0", 48 | "husky": "^9.0.11", 49 | "jest": "^23.6.0", 50 | "lint-staged": "^16.0.0", 51 | "prettier": "^3.2.5", 52 | "prop-types": "^15.7.2", 53 | "react": "16.6.1", 54 | "react-dom": "^16.6.1", 55 | "react-native": "0.57.7", 56 | "react-test-renderer": "^19.0.0" 57 | }, 58 | "peerDependencies": { 59 | "@react-native-picker/picker": "^2.4.0" 60 | }, 61 | "scripts": { 62 | "test": "jest", 63 | "test:watch": "jest --watch", 64 | "test:coverage": "jest --coverage", 65 | "open:coverage": "open ./coverage/lcov-report/index.html", 66 | "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"", 67 | "lint:fix": "yarn lint --fix", 68 | "prepare": "husky" 69 | }, 70 | "jest": { 71 | "preset": "react-native", 72 | "setupFiles": [ 73 | "./test/setup.js" 74 | ], 75 | "snapshotSerializers": [ 76 | "enzyme-to-json/serializer" 77 | ], 78 | "transformIgnorePatterns": [ 79 | "node_modules/?!(@react-native-picker/picker)" 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "release-type": "node", 4 | "pull-request-title-pattern": "build(release): release ${version}", 5 | "packages": { 6 | ".": { 7 | "include-component-in-tag": false 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { Picker } from '@react-native-picker/picker'; 2 | import isEqual from 'lodash.isequal'; 3 | import isObject from 'lodash.isobject'; 4 | import PropTypes from 'prop-types'; 5 | import React, { PureComponent } from 'react'; 6 | import { Keyboard, Modal, Platform, Text, TextInput, TouchableOpacity, View } from 'react-native'; 7 | import { defaultStyles } from './styles'; 8 | 9 | export default class RNPickerSelect extends PureComponent { 10 | static propTypes = { 11 | onValueChange: PropTypes.func.isRequired, 12 | items: PropTypes.arrayOf( 13 | PropTypes.shape({ 14 | label: PropTypes.string.isRequired, 15 | value: PropTypes.any.isRequired, 16 | testID: PropTypes.string, 17 | inputLabel: PropTypes.string, 18 | key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 19 | color: PropTypes.string, 20 | }) 21 | ).isRequired, 22 | value: PropTypes.any, 23 | placeholder: PropTypes.shape({ 24 | label: PropTypes.string, 25 | value: PropTypes.any, 26 | key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 27 | color: PropTypes.string, 28 | }), 29 | disabled: PropTypes.bool, 30 | itemKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 31 | style: PropTypes.shape({}), 32 | children: PropTypes.any, 33 | onOpen: PropTypes.func, 34 | useNativeAndroidPickerStyle: PropTypes.bool, 35 | fixAndroidTouchableBug: PropTypes.bool, 36 | darkTheme: PropTypes.bool, 37 | 38 | // Custom Modal props (iOS only) 39 | doneText: PropTypes.string, 40 | onDonePress: PropTypes.func, 41 | onUpArrow: PropTypes.func, 42 | onDownArrow: PropTypes.func, 43 | onClose: PropTypes.func, 44 | 45 | // Modal props (iOS only) 46 | modalProps: PropTypes.shape({}), 47 | 48 | // TextInput props 49 | textInputProps: PropTypes.shape({}), 50 | 51 | // Picker props 52 | pickerProps: PropTypes.shape({}), 53 | 54 | // Touchable Done props (iOS only) 55 | touchableDoneProps: PropTypes.shape({}), 56 | 57 | // Touchable wrapper props 58 | touchableWrapperProps: PropTypes.shape({}), 59 | 60 | // Custom Icon 61 | Icon: PropTypes.func, 62 | InputAccessoryView: PropTypes.func, 63 | dropdownItemStyle: PropTypes.shape({}), 64 | activeItemStyle: PropTypes.shape({}), 65 | }; 66 | 67 | static defaultProps = { 68 | value: undefined, 69 | placeholder: { 70 | label: 'Select an item...', 71 | value: null, 72 | color: '#9EA0A4', 73 | }, 74 | disabled: false, 75 | itemKey: null, 76 | style: {}, 77 | children: null, 78 | useNativeAndroidPickerStyle: true, 79 | fixAndroidTouchableBug: false, 80 | doneText: 'Done', 81 | onDonePress: null, 82 | onUpArrow: null, 83 | onDownArrow: null, 84 | onOpen: null, 85 | onClose: null, 86 | modalProps: {}, 87 | textInputProps: {}, 88 | pickerProps: {}, 89 | touchableDoneProps: {}, 90 | touchableWrapperProps: {}, 91 | Icon: null, 92 | InputAccessoryView: null, 93 | darkTheme: false, 94 | dropdownItemStyle: {}, 95 | activeItemStyle: {}, 96 | }; 97 | 98 | static handlePlaceholder({ placeholder }) { 99 | if (isEqual(placeholder, {})) { 100 | return []; 101 | } 102 | return [placeholder]; 103 | } 104 | 105 | static getSelectedItem({ items, key, value }) { 106 | let idx = items.findIndex((item) => { 107 | if (item.key && key) { 108 | return isEqual(item.key, key); 109 | } 110 | if (isObject(item.value) || isObject(value)) { 111 | return isEqual(item.value, value); 112 | } 113 | // convert to string to make sure types match 114 | return isEqual(String(item.value), String(value)); 115 | }); 116 | if (idx === -1) { 117 | idx = 0; 118 | } 119 | return { 120 | selectedItem: items[idx] || {}, 121 | idx, 122 | }; 123 | } 124 | 125 | constructor(props) { 126 | super(props); 127 | 128 | const items = RNPickerSelect.handlePlaceholder({ 129 | placeholder: props.placeholder, 130 | }).concat(props.items); 131 | 132 | const { selectedItem } = RNPickerSelect.getSelectedItem({ 133 | items, 134 | key: props.itemKey, 135 | value: props.value, 136 | }); 137 | 138 | this.state = { 139 | items, 140 | selectedItem, 141 | showPicker: false, 142 | animationType: undefined, 143 | orientation: 'portrait', 144 | doneDepressed: false, 145 | }; 146 | 147 | this.onUpArrow = this.onUpArrow.bind(this); 148 | this.onDownArrow = this.onDownArrow.bind(this); 149 | this.onValueChange = this.onValueChange.bind(this); 150 | this.onOrientationChange = this.onOrientationChange.bind(this); 151 | this.setInputRef = this.setInputRef.bind(this); 152 | this.togglePicker = this.togglePicker.bind(this); 153 | this.renderInputAccessoryView = this.renderInputAccessoryView.bind(this); 154 | } 155 | 156 | componentDidUpdate = (prevProps, prevState) => { 157 | // update items if items or placeholder prop changes 158 | const items = RNPickerSelect.handlePlaceholder({ 159 | placeholder: this.props.placeholder, 160 | }).concat(this.props.items); 161 | const itemsChanged = !isEqual(prevState.items, items); 162 | 163 | // update selectedItem if value prop is defined and differs from currently selected item 164 | const { selectedItem, idx } = RNPickerSelect.getSelectedItem({ 165 | items, 166 | key: this.props.itemKey, 167 | value: this.props.value, 168 | }); 169 | const selectedItemChanged = 170 | !isEqual(this.props.value, undefined) && !isEqual(prevState.selectedItem, selectedItem); 171 | 172 | if (itemsChanged || selectedItemChanged) { 173 | this.props.onValueChange(selectedItem.value, idx); 174 | 175 | this.setState({ 176 | ...(itemsChanged ? { items } : {}), 177 | ...(selectedItemChanged ? { selectedItem } : {}), 178 | }); 179 | } 180 | }; 181 | 182 | onUpArrow() { 183 | const { onUpArrow } = this.props; 184 | 185 | this.togglePicker(false, onUpArrow); 186 | } 187 | 188 | onDownArrow() { 189 | const { onDownArrow } = this.props; 190 | 191 | this.togglePicker(false, onDownArrow); 192 | } 193 | 194 | onValueChange(value, index) { 195 | const { onValueChange } = this.props; 196 | 197 | onValueChange(value, index); 198 | 199 | this.setState((prevState) => { 200 | return { 201 | selectedItem: prevState.items[index], 202 | }; 203 | }); 204 | } 205 | 206 | onOrientationChange({ nativeEvent }) { 207 | this.setState({ 208 | orientation: nativeEvent.orientation, 209 | }); 210 | } 211 | 212 | setInputRef(ref) { 213 | this.inputRef = ref; 214 | } 215 | 216 | getPlaceholderStyle() { 217 | const { placeholder, style } = this.props; 218 | const { selectedItem } = this.state; 219 | 220 | if (!isEqual(placeholder, {}) && selectedItem.label === placeholder.label) { 221 | return { 222 | ...defaultStyles.placeholder, 223 | ...style.placeholder, 224 | }; 225 | } 226 | return {}; 227 | } 228 | 229 | isDarkTheme() { 230 | const { darkTheme } = this.props; 231 | 232 | return Platform.OS === 'ios' && darkTheme; 233 | } 234 | 235 | triggerOpenCloseCallbacks(donePressed) { 236 | const { onOpen, onClose } = this.props; 237 | const { showPicker } = this.state; 238 | 239 | if (!showPicker && onOpen) { 240 | onOpen(); 241 | } 242 | 243 | if (showPicker && onClose) { 244 | onClose(donePressed); 245 | } 246 | } 247 | 248 | togglePicker(animate = false, postToggleCallback, donePressed = false) { 249 | const { modalProps, disabled } = this.props; 250 | const { showPicker } = this.state; 251 | 252 | if (disabled) { 253 | return; 254 | } 255 | 256 | if (!showPicker) { 257 | Keyboard.dismiss(); 258 | } 259 | 260 | const animationType = 261 | modalProps && modalProps.animationType ? modalProps.animationType : 'slide'; 262 | 263 | this.triggerOpenCloseCallbacks(donePressed); 264 | 265 | this.setState( 266 | (prevState) => { 267 | return { 268 | animationType: animate ? animationType : undefined, 269 | showPicker: !prevState.showPicker, 270 | }; 271 | }, 272 | () => { 273 | if (postToggleCallback) { 274 | postToggleCallback(); 275 | } 276 | } 277 | ); 278 | } 279 | 280 | renderPickerItems() { 281 | const { items, selectedItem } = this.state; 282 | const defaultItemColor = this.isDarkTheme() ? '#fff' : undefined; 283 | 284 | const { dropdownItemStyle, activeItemStyle } = this.props; 285 | 286 | return items.map((item) => { 287 | return ( 288 | 296 | ); 297 | }); 298 | } 299 | 300 | renderInputAccessoryView() { 301 | const { 302 | InputAccessoryView, 303 | doneText, 304 | onUpArrow, 305 | onDownArrow, 306 | onDonePress, 307 | style, 308 | touchableDoneProps, 309 | } = this.props; 310 | 311 | const { doneDepressed } = this.state; 312 | 313 | if (InputAccessoryView) { 314 | return ; 315 | } 316 | 317 | return ( 318 | 326 | 327 | 331 | 341 | 342 | 346 | 356 | 357 | 358 | { 361 | this.togglePicker(true, onDonePress, true); 362 | }} 363 | onPressIn={() => { 364 | this.setState({ doneDepressed: true }); 365 | }} 366 | onPressOut={() => { 367 | this.setState({ doneDepressed: false }); 368 | }} 369 | hitSlop={{ 370 | top: 4, 371 | right: 4, 372 | bottom: 4, 373 | left: 4, 374 | }} 375 | {...touchableDoneProps} 376 | > 377 | 378 | 388 | {doneText} 389 | 390 | 391 | 392 | 393 | ); 394 | } 395 | 396 | renderIcon() { 397 | const { style, Icon } = this.props; 398 | 399 | if (!Icon) { 400 | return null; 401 | } 402 | 403 | return ( 404 | 405 | 406 | 407 | ); 408 | } 409 | 410 | renderTextInputOrChildren() { 411 | const { children, style, textInputProps } = this.props; 412 | const { selectedItem } = this.state; 413 | 414 | const containerStyle = 415 | Platform.OS === 'ios' ? style.inputIOSContainer : style.inputAndroidContainer; 416 | 417 | if (children) { 418 | return ( 419 | 420 | {children} 421 | 422 | ); 423 | } 424 | 425 | return ( 426 | 427 | 438 | {this.renderIcon()} 439 | 440 | ); 441 | } 442 | 443 | renderIOS() { 444 | const { style, modalProps, pickerProps, touchableWrapperProps } = this.props; 445 | const { animationType, orientation, selectedItem, showPicker } = this.state; 446 | 447 | return ( 448 | 449 | { 452 | this.togglePicker(true); 453 | }} 454 | activeOpacity={1} 455 | {...touchableWrapperProps} 456 | > 457 | {this.renderTextInputOrChildren()} 458 | 459 | 468 | { 472 | this.togglePicker(true); 473 | }} 474 | /> 475 | {this.renderInputAccessoryView()} 476 | 484 | 490 | {this.renderPickerItems()} 491 | 492 | 493 | 494 | 495 | ); 496 | } 497 | 498 | renderAndroidHeadless() { 499 | const { 500 | disabled, 501 | Icon, 502 | style, 503 | pickerProps, 504 | onOpen, 505 | touchableWrapperProps, 506 | fixAndroidTouchableBug, 507 | } = this.props; 508 | const { selectedItem } = this.state; 509 | 510 | const Component = fixAndroidTouchableBug ? View : TouchableOpacity; 511 | return ( 512 | 518 | 519 | {this.renderTextInputOrChildren()} 520 | 532 | {this.renderPickerItems()} 533 | 534 | 535 | 536 | ); 537 | } 538 | 539 | renderAndroidNativePickerStyle() { 540 | const { disabled, Icon, style, pickerProps } = this.props; 541 | const { selectedItem } = this.state; 542 | 543 | return ( 544 | 545 | 557 | {this.renderPickerItems()} 558 | 559 | {this.renderIcon()} 560 | 561 | ); 562 | } 563 | 564 | renderWeb() { 565 | const { disabled, style, pickerProps } = this.props; 566 | const { selectedItem } = this.state; 567 | 568 | return ( 569 | 570 | 578 | {this.renderPickerItems()} 579 | 580 | {this.renderIcon()} 581 | 582 | ); 583 | } 584 | 585 | render() { 586 | const { children, useNativeAndroidPickerStyle } = this.props; 587 | 588 | if (Platform.OS === 'ios') { 589 | return this.renderIOS(); 590 | } 591 | 592 | if (Platform.OS === 'web') { 593 | return this.renderWeb(); 594 | } 595 | 596 | if (children || !useNativeAndroidPickerStyle) { 597 | return this.renderAndroidHeadless(); 598 | } 599 | 600 | return this.renderAndroidNativePickerStyle(); 601 | } 602 | } 603 | 604 | export { defaultStyles }; 605 | -------------------------------------------------------------------------------- /src/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const defaultStyles = StyleSheet.create({ 4 | viewContainer: { 5 | alignSelf: 'stretch', 6 | }, 7 | iconContainer: { 8 | position: 'absolute', 9 | right: 0, 10 | }, 11 | modalViewTop: { 12 | flex: 1, 13 | }, 14 | modalViewMiddle: { 15 | height: 45, 16 | flexDirection: 'row', 17 | justifyContent: 'space-between', 18 | alignItems: 'center', 19 | paddingHorizontal: 10, 20 | backgroundColor: '#f8f8f8', 21 | borderTopWidth: 1, 22 | borderTopColor: '#dedede', 23 | zIndex: 2, 24 | }, 25 | modalViewMiddleDark: { 26 | backgroundColor: '#232323', 27 | borderTopColor: '#252525', 28 | }, 29 | chevronContainer: { 30 | flexDirection: 'row', 31 | }, 32 | chevron: { 33 | width: 15, 34 | height: 15, 35 | backgroundColor: 'transparent', 36 | borderColor: '#a1a1a1', 37 | borderTopWidth: 1.5, 38 | borderRightWidth: 1.5, 39 | }, 40 | chevronDark: { 41 | borderColor: '#707070', 42 | }, 43 | chevronUp: { 44 | marginLeft: 11, 45 | transform: [{ translateY: 4 }, { rotate: '-45deg' }], 46 | }, 47 | chevronDown: { 48 | marginLeft: 22, 49 | transform: [{ translateY: -5 }, { rotate: '135deg' }], 50 | }, 51 | chevronActive: { 52 | borderColor: '#007aff', 53 | }, 54 | done: { 55 | color: '#007aff', 56 | fontWeight: '600', 57 | fontSize: 17, 58 | paddingTop: 1, 59 | paddingRight: 11, 60 | }, 61 | doneDark: { 62 | color: '#fff', 63 | }, 64 | doneDepressed: { 65 | fontSize: 19, 66 | }, 67 | modalViewBottom: { 68 | justifyContent: 'center', 69 | backgroundColor: '#d0d4da', 70 | }, 71 | modalViewBottomDark: { 72 | backgroundColor: '#252525', 73 | }, 74 | placeholder: { 75 | color: '#c7c7cd', 76 | }, 77 | headlessAndroidPicker: { 78 | position: 'absolute', 79 | width: '100%', 80 | height: '100%', 81 | color: 'transparent', 82 | opacity: 0, 83 | }, 84 | }); 85 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import Enzyme, { shallow, render, mount } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | // React 16 Enzyme adapter 5 | Enzyme.configure({ adapter: new Adapter() }); 6 | 7 | // Make Enzyme functions available in all test files without importing 8 | global.shallow = shallow; 9 | global.render = render; 10 | global.mount = mount; 11 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Keyboard, Platform, View } from 'react-native'; 3 | import RNPickerSelect from '../src'; 4 | 5 | const selectItems = [ 6 | { 7 | label: 'Red', 8 | value: 'red', 9 | }, 10 | { 11 | label: 'Orange', 12 | value: 'orange', 13 | }, 14 | { 15 | label: 'Yellow', 16 | value: 'yellow', 17 | }, 18 | { 19 | label: 'Green', 20 | value: 'green', 21 | }, 22 | { 23 | label: 'Blue', 24 | value: 'blue', 25 | }, 26 | { 27 | label: 'Indigo', 28 | value: 'indigo', 29 | }, 30 | ]; 31 | 32 | const objectSelectItems = [ 33 | { 34 | label: 'Red', 35 | value: { label: 'red' }, 36 | }, 37 | { 38 | label: 'Orange', 39 | value: { label: 'orange' }, 40 | }, 41 | { 42 | label: 'Yellow', 43 | value: { label: 'yellow' }, 44 | }, 45 | { 46 | label: 'Green', 47 | value: { label: 'green' }, 48 | }, 49 | { 50 | label: 'Blue', 51 | value: { label: 'blue' }, 52 | }, 53 | { 54 | label: 'Indigo', 55 | value: { label: 'indigo' }, 56 | }, 57 | ]; 58 | 59 | const violet = { label: 'Violet', value: 'violet' }; 60 | 61 | const placeholder = { 62 | label: 'Select a color...', 63 | value: null, 64 | }; 65 | 66 | const noop = () => {}; 67 | 68 | describe('RNPickerSelect', () => { 69 | beforeEach(() => { 70 | jest.resetAllMocks(); 71 | jest.spyOn(Keyboard, 'dismiss'); 72 | }); 73 | 74 | describe('when provided an itemKey prop', () => { 75 | it('sets the selected item via key rather than value', () => { 76 | const items = [ 77 | { 78 | label: '+1 Canada', 79 | value: 1, 80 | key: 'canada', 81 | }, 82 | { 83 | label: '+1 USA', 84 | value: 1, 85 | key: 'usa', 86 | }, 87 | ]; 88 | 89 | const wrapper = shallow( 90 | 97 | ); 98 | 99 | expect(wrapper.state().selectedItem.key).toEqual('usa'); 100 | }); 101 | }); 102 | 103 | it('should set the selected value to state', () => { 104 | const wrapper = shallow( 105 | 106 | ); 107 | 108 | wrapper.find('[testID="ios_picker"]').props().onValueChange('orange', 2); 109 | wrapper.find('[testID="ios_picker"]').props().onValueChange('yellow', 3); 110 | expect(wrapper.state().selectedItem.value).toEqual('yellow'); 111 | }); 112 | 113 | it('should not return the default InputAccessoryView if custom component is passed in', () => { 114 | const wrapper = shallow( 115 | { 120 | return ; 121 | }} 122 | /> 123 | ); 124 | 125 | const input_accessory_view = wrapper.find('[testID="input_accessory_view"]'); 126 | const custom_input_accessory_view = wrapper.find('[testID="custom_input_accessory_view"]'); 127 | 128 | expect(input_accessory_view).toHaveLength(0); 129 | expect(custom_input_accessory_view).toHaveLength(1); 130 | }); 131 | 132 | it('should update the orientation state when onOrientationChange is called', () => { 133 | const wrapper = shallow(); 134 | 135 | expect(wrapper.state().orientation).toEqual('portrait'); 136 | 137 | wrapper.instance().onOrientationChange({ nativeEvent: { orientation: 'landscape' } }); 138 | 139 | expect(wrapper.state().orientation).toEqual('landscape'); 140 | }); 141 | 142 | it('should handle an empty items array', () => { 143 | const wrapper = shallow( 144 | 145 | ); 146 | 147 | expect(wrapper.state().items).toHaveLength(0); 148 | }); 149 | 150 | it('should return the expected option to a callback passed into onSelect', () => { 151 | const onValueChangeSpy = jest.fn(); 152 | const wrapper = shallow( 153 | 158 | ); 159 | 160 | wrapper.find('[testID="ios_picker"]').props().onValueChange('orange', 2); 161 | expect(onValueChangeSpy).toHaveBeenCalledWith('orange', 2); 162 | }); 163 | 164 | it('should return the expected option to a callback passed into onSelect when the value is an object', () => { 165 | const onValueChangeSpy = jest.fn(); 166 | const wrapper = shallow( 167 | 172 | ); 173 | 174 | wrapper.find('[testID="ios_picker"]').props().onValueChange(objectSelectItems[5].value, 5); 175 | expect(onValueChangeSpy).toHaveBeenCalledWith(objectSelectItems[5].value, 5); 176 | }); 177 | 178 | it('should show the picker when pressed', () => { 179 | const wrapper = shallow( 180 | 181 | ); 182 | 183 | const touchable = wrapper.find('TouchableOpacity').at(1); 184 | touchable.simulate('press'); 185 | expect(wrapper.state().showPicker).toEqual(true); 186 | }); 187 | 188 | it('should not show the picker when pressed if disabled', () => { 189 | const wrapper = shallow( 190 | 196 | ); 197 | 198 | const touchable = wrapper.find('TouchableOpacity').at(1); 199 | touchable.simulate('press'); 200 | expect(wrapper.state().showPicker).toEqual(false); 201 | }); 202 | 203 | it('should show the value "RED" in the TextInput instead of the label "Red" when the inputLabel is set', () => { 204 | const items = [ 205 | { 206 | label: 'Red', 207 | value: 'red', 208 | inputLabel: 'RED', 209 | }, 210 | { 211 | label: 'Orange', 212 | value: 'orange', 213 | }, 214 | ]; 215 | 216 | const wrapper = shallow( 217 | 218 | ); 219 | 220 | const textInput = wrapper.find('[testID="text_input"]'); 221 | 222 | expect(textInput.props().value).toEqual('RED'); 223 | }); 224 | 225 | it('should update the selected value when the `value` prop updates and call the onValueChange cb', () => { 226 | const onValueChangeSpy = jest.fn(); 227 | const wrapper = shallow( 228 | 234 | ); 235 | 236 | expect(wrapper.state().selectedItem.value).toEqual('red'); 237 | 238 | wrapper.setProps({ value: 'orange' }); 239 | expect(onValueChangeSpy).toBeCalledWith('orange', 1); 240 | expect(wrapper.state().selectedItem.value).toEqual('orange'); 241 | 242 | wrapper.setProps({ value: 'yellow' }); 243 | expect(onValueChangeSpy).toBeCalledWith('yellow', 2); 244 | expect(wrapper.state().selectedItem.value).toEqual('yellow'); 245 | }); 246 | 247 | it('should update the items when the `items` prop updates', () => { 248 | const wrapper = shallow( 249 | 250 | ); 251 | 252 | expect(wrapper.state().items).toEqual([placeholder].concat(selectItems)); 253 | 254 | const selectItemsPlusViolet = selectItems.concat([violet]); 255 | 256 | wrapper.setProps({ items: selectItemsPlusViolet }); 257 | expect(wrapper.state().items).toEqual([placeholder].concat(selectItemsPlusViolet)); 258 | }); 259 | 260 | it('should should handle having no placeholder', () => { 261 | const wrapper = shallow( 262 | 263 | ); 264 | 265 | expect(wrapper.state().items).toEqual(selectItems); 266 | }); 267 | 268 | it('should should show the icon container the Icon prop receives a component', () => { 269 | const wrapper = shallow( 270 | { 274 | return ; 275 | }} 276 | /> 277 | ); 278 | 279 | expect(wrapper.find('[testID="icon_container"]')).toHaveLength(1); 280 | }); 281 | 282 | it('should should not show the icon container when the Icon prop is empty', () => { 283 | const wrapper = shallow(); 284 | 285 | expect(wrapper.find('[testID="icon_container"]')).toHaveLength(0); 286 | }); 287 | 288 | it('should call Keyboard.dismiss when opened', () => { 289 | const wrapper = shallow(); 290 | 291 | const touchable = wrapper.find('[testID="ios_touchable_wrapper"]'); 292 | touchable.simulate('press'); 293 | 294 | expect(Keyboard.dismiss).toHaveBeenCalledTimes(1); 295 | }); 296 | 297 | it("should reset to the first item (typically the placeholder) if a value is passed in that doesn't exist in the `items` array", () => { 298 | const wrapper = shallow( 299 | 305 | ); 306 | 307 | wrapper.find('[testID="ios_picker"]').props().onValueChange('orange', 2); 308 | expect(wrapper.state().selectedItem.value).toEqual('orange'); 309 | wrapper.setProps({ value: 'violet' }); 310 | expect(wrapper.state().selectedItem).toEqual(placeholder); 311 | }); 312 | 313 | it('should set the selected value to state (Android)', () => { 314 | Platform.OS = 'android'; 315 | const wrapper = shallow(); 316 | 317 | wrapper.find('[testID="android_picker"]').props().onValueChange('orange', 2); 318 | expect(wrapper.state().selectedItem.value).toEqual('orange'); 319 | }); 320 | 321 | it('should render the headless component when a child is passed in (Android)', () => { 322 | Platform.OS = 'android'; 323 | const wrapper = shallow( 324 | 325 | 326 | 327 | ); 328 | 329 | const component = wrapper.find('[testID="android_picker_headless"]'); 330 | expect(component).toHaveLength(1); 331 | }); 332 | 333 | it('should set the selected value to state (Web)', () => { 334 | Platform.OS = 'web'; 335 | const wrapper = shallow(); 336 | 337 | wrapper.find('[testID="web_picker"]').props().onValueChange('orange', 2); 338 | expect(wrapper.state().selectedItem.value).toEqual('orange'); 339 | }); 340 | 341 | it('should call the onDonePress callback when set (iOS)', () => { 342 | Platform.OS = 'ios'; 343 | const onDonePressSpy = jest.fn(); 344 | const wrapper = shallow( 345 | 346 | ); 347 | 348 | wrapper.find('[testID="done_button"]').simulate('press'); 349 | 350 | expect(onDonePressSpy).toHaveBeenCalledWith(); 351 | expect(onDonePressSpy).toHaveBeenCalledTimes(1); 352 | }); 353 | 354 | it('should update the Done styling during a press (iOS)', () => { 355 | Platform.OS = 'ios'; 356 | const wrapper = shallow(); 357 | 358 | const done_button = wrapper.find('[testID="done_button"]'); 359 | 360 | done_button.simulate('pressIn'); 361 | expect(wrapper.state().doneDepressed).toEqual(true); 362 | 363 | done_button.simulate('pressOut'); 364 | expect(wrapper.state().doneDepressed).toEqual(false); 365 | }); 366 | 367 | it('should call the onShow callback when set (iOS)', () => { 368 | Platform.OS = 'ios'; 369 | const onShowSpy = jest.fn(); 370 | const wrapper = shallow( 371 | 378 | ); 379 | wrapper.find('[testID="ios_modal"]').props().onShow(); 380 | expect(onShowSpy).toHaveBeenCalledWith(); 381 | }); 382 | 383 | it('should call the onDismiss callback when set (iOS)', () => { 384 | Platform.OS = 'ios'; 385 | const onDismissSpy = jest.fn(); 386 | const wrapper = shallow( 387 | 394 | ); 395 | wrapper.find('[testID="ios_modal"]').props().onDismiss(); 396 | expect(onDismissSpy).toHaveBeenCalledWith(); 397 | }); 398 | 399 | it('should call the onOpen callback when set (iOS)', () => { 400 | Platform.OS = 'ios'; 401 | const onOpenSpy = jest.fn(); 402 | const wrapper = shallow( 403 | 404 | ); 405 | 406 | const touchable = wrapper.find('[testID="ios_touchable_wrapper"]'); 407 | touchable.simulate('press'); 408 | 409 | expect(onOpenSpy).toHaveBeenCalledWith(); 410 | }); 411 | 412 | it('should call the onOpen callback when set (Android)', () => { 413 | Platform.OS = 'android'; 414 | const onOpenSpy = jest.fn(); 415 | const wrapper = shallow( 416 | 422 | ); 423 | 424 | const touchable = wrapper.find('[testID="android_touchable_wrapper"]'); 425 | touchable.simulate('press'); 426 | 427 | expect(onOpenSpy).toHaveBeenCalledWith(); 428 | }); 429 | 430 | it('should use a View when fixAndroidTouchableBug=true (Android)', () => { 431 | Platform.OS = 'android'; 432 | const wrapper = shallow( 433 | 439 | ); 440 | 441 | const touchable = wrapper.find('[testID="android_touchable_wrapper"]'); 442 | 443 | expect(touchable.type().displayName).toEqual('View'); 444 | }); 445 | 446 | it('should call the onClose callback when set', () => { 447 | Platform.OS = 'ios'; 448 | const onCloseSpy = jest.fn(); 449 | const wrapper = shallow( 450 | 451 | ); 452 | 453 | const touchable = wrapper.find('[testID="done_button"]'); 454 | // Open 455 | touchable.simulate('press'); 456 | // Close 457 | touchable.simulate('press'); 458 | 459 | expect(onCloseSpy).toHaveBeenCalledWith(true); 460 | }); 461 | 462 | it('should close the modal when the empty area above the picker is tapped', () => { 463 | const wrapper = shallow(); 464 | 465 | jest.spyOn(wrapper.instance(), 'togglePicker'); 466 | 467 | const touchable = wrapper.find('[testID="ios_modal_top"]'); 468 | touchable.simulate('press'); 469 | 470 | expect(wrapper.instance().togglePicker).toHaveBeenCalledWith(true); 471 | }); 472 | 473 | it('should use the dark theme when `darkTheme` prop is provided on iOS', () => { 474 | Platform.OS = 'ios'; 475 | 476 | const wrapper = shallow( 477 | 478 | ); 479 | 480 | const input_accessory_view = wrapper.find('[testID="input_accessory_view"]'); 481 | const darkThemeStyle = input_accessory_view.get(0).props.style[1]; 482 | 483 | expect(darkThemeStyle).toHaveProperty('backgroundColor', '#232323'); 484 | }); 485 | 486 | // TODO - fix 487 | xdescribe('getDerivedStateFromProps', () => { 488 | it('should return null when nothing changes', () => { 489 | const nextProps = { 490 | placeholder, 491 | value: selectItems[0].value, 492 | onValueChange() {}, 493 | items: selectItems, 494 | }; 495 | const prevState = { 496 | items: [placeholder].concat(selectItems), 497 | selectedItem: selectItems[0], 498 | }; 499 | 500 | expect(RNPickerSelect.getDerivedStateFromProps(nextProps, prevState)).toEqual(null); 501 | }); 502 | 503 | it('should return a new items state when the items change', () => { 504 | const nextProps = { 505 | placeholder, 506 | value: selectItems[0].value, 507 | onValueChange() {}, 508 | items: selectItems.concat([violet]), 509 | }; 510 | const prevState = { 511 | items: [placeholder].concat(selectItems), 512 | selectedItem: selectItems[0], 513 | }; 514 | 515 | expect(RNPickerSelect.getDerivedStateFromProps(nextProps, prevState)).toEqual({ 516 | items: [placeholder].concat(selectItems).concat([violet]), 517 | }); 518 | }); 519 | 520 | it('should return a new items state when the placeholder changes', () => { 521 | const newPlaceholder = { 522 | label: 'Select a thing...', 523 | value: null, 524 | }; 525 | const nextProps = { 526 | placeholder: newPlaceholder, 527 | value: selectItems[0].value, 528 | onValueChange() {}, 529 | items: selectItems, 530 | }; 531 | const prevState = { 532 | items: [placeholder].concat(selectItems), 533 | selectedItem: selectItems[0], 534 | }; 535 | 536 | expect(RNPickerSelect.getDerivedStateFromProps(nextProps, prevState)).toEqual({ 537 | items: [newPlaceholder].concat(selectItems), 538 | }); 539 | }); 540 | 541 | it('should return a new selectedItem state when the value changes', () => { 542 | const nextProps = { 543 | placeholder, 544 | value: selectItems[1].value, 545 | onValueChange() {}, 546 | items: selectItems, 547 | }; 548 | const prevState = { 549 | items: [placeholder].concat(selectItems), 550 | selectedItem: selectItems[0], 551 | }; 552 | 553 | expect(RNPickerSelect.getDerivedStateFromProps(nextProps, prevState)).toEqual({ 554 | selectedItem: selectItems[1], 555 | }); 556 | }); 557 | }); 558 | 559 | it('should apply custom styles to dropdown items', () => { 560 | const customDropdownItemStyle = { 561 | backgroundColor: '#d0d4da', 562 | color: '#000', 563 | }; 564 | 565 | const wrapper = shallow( 566 | 572 | ); 573 | 574 | wrapper.find('[testID="ios_touchable_wrapper"]').simulate('press'); 575 | 576 | const pickerItems = wrapper.find('Picker').find('Picker.Item'); 577 | 578 | pickerItems.forEach((item) => { 579 | expect(item.props().style).toEqual(customDropdownItemStyle); 580 | }); 581 | }); 582 | 583 | it('should apply custom styles to the active dropdown item', () => { 584 | const customActiveItemStyle = { 585 | backgroundColor: '#d0d4da', 586 | color: '#000', 587 | }; 588 | 589 | const selectItems = [ 590 | { label: 'Item 1', value: 'item1', key: '1' }, 591 | { label: 'Item 2', value: 'item2', key: '2' }, 592 | ]; 593 | 594 | const placeholder = { label: 'Select an item...', value: null }; 595 | 596 | const wrapper = shallow( 597 | {}} 601 | activeItemStyle={customActiveItemStyle} 602 | value="item2" // Select "Item 2" 603 | /> 604 | ); 605 | 606 | // Open the picker 607 | wrapper.find('[testID="ios_touchable_wrapper"]').simulate('press'); 608 | 609 | // Find picker items 610 | const pickerItems = wrapper.find('Picker').find('PickerItem'); 611 | 612 | // Ensure picker items are found 613 | expect(pickerItems.length).toBeGreaterThan(0); 614 | 615 | // Check if the active item has the custom styles 616 | const activeItem = pickerItems.findWhere((item) => item.prop('value') === 'item2'); 617 | 618 | // Ensure activeItem is found 619 | expect(activeItem.exists()).toBe(true); 620 | 621 | // Check styles applied to the active item 622 | expect(activeItem.prop('style')).toEqual(customActiveItemStyle); 623 | }); 624 | 625 | }); 626 | --------------------------------------------------------------------------------