├── .gitignore ├── jsconfig.json ├── lib ├── KeyboardAwareFlatList.js ├── KeyboardAwareScrollView.js ├── KeyboardAwareSectionList.js ├── KeyboardAwareInterface.js └── KeyboardAwareHOC.js ├── .github └── FUNDING.yml ├── index.js ├── package.json ├── .flowconfig ├── index.d.ts ├── README.md └── .eslintrc /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | AwesomeProject.xcodeproj 4 | AwesomeProjectTests 5 | index.ios.js 6 | iOS 7 | 8 | .idea/ 9 | .vscode/ -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true 5 | }, 6 | "exclude": [ 7 | "node_modules" 8 | ] 9 | } -------------------------------------------------------------------------------- /lib/KeyboardAwareFlatList.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { FlatList } from 'react-native' 4 | import listenToKeyboardEvents from './KeyboardAwareHOC' 5 | 6 | export default listenToKeyboardEvents(FlatList) 7 | -------------------------------------------------------------------------------- /lib/KeyboardAwareScrollView.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ScrollView } from 'react-native' 4 | import listenToKeyboardEvents from './KeyboardAwareHOC' 5 | 6 | export default listenToKeyboardEvents(ScrollView) 7 | -------------------------------------------------------------------------------- /lib/KeyboardAwareSectionList.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { SectionList } from 'react-native' 4 | import listenToKeyboardEvents from './KeyboardAwareHOC' 5 | 6 | export default listenToKeyboardEvents(SectionList) 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [codler] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import listenToKeyboardEvents from './lib/KeyboardAwareHOC' 4 | import KeyboardAwareScrollView from './lib/KeyboardAwareScrollView' 5 | import KeyboardAwareFlatList from './lib/KeyboardAwareFlatList' 6 | import KeyboardAwareSectionList from './lib/KeyboardAwareSectionList' 7 | 8 | export { 9 | listenToKeyboardEvents, 10 | KeyboardAwareFlatList, 11 | KeyboardAwareSectionList, 12 | KeyboardAwareScrollView 13 | } 14 | -------------------------------------------------------------------------------- /lib/KeyboardAwareInterface.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export interface KeyboardAwareInterface { 4 | getScrollResponder: () => void, 5 | scrollToPosition: (x: number, y: number, animated?: boolean) => void, 6 | scrollToEnd: (animated?: boolean) => void, 7 | scrollForExtraHeightOnAndroid: (extraHeight: number) => void, 8 | scrollToFocusedInput: ( 9 | reactNode: Object, 10 | extraHeight: number, 11 | keyboardOpeningTime: number 12 | ) => void 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codler/react-native-keyboard-aware-scroll-view", 3 | "version": "2.0.1", 4 | "description": "A React Native ScrollView component that resizes when the keyboard appears.", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "lint": "eslint lib", 9 | "test": "npm run lint", 10 | "flow": "flow check" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/codler/react-native-keyboard-aware-scroll-view.git" 15 | }, 16 | "tags": [ 17 | "react", 18 | "react-native", 19 | "react-component", 20 | "ios", 21 | "android" 22 | ], 23 | "keywords": [ 24 | "react", 25 | "react-native", 26 | "scrollview", 27 | "keyboard", 28 | "ios", 29 | "android", 30 | "react-component" 31 | ], 32 | "funding": { 33 | "type": "individual", 34 | "url": "https://github.com/sponsors/codler" 35 | }, 36 | "author": "Han Lin Yap (https://yap.nu)", 37 | "license": "ISC", 38 | "dependencies": { 39 | "react-native-iphone-x-helper": "^1.0.3" 40 | }, 41 | "peerDependencies": { 42 | "react-native": ">=0.65.1" 43 | }, 44 | "devDependencies": { 45 | "babel-eslint": "^10.0.2", 46 | "eslint": "^6.1.0", 47 | "eslint-plugin-flowtype": "^4.2.0", 48 | "eslint-plugin-react": "^7.14.3", 49 | "eslint-plugin-react-native": "^3.7.0", 50 | "flow-bin": "^0.105.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | # We fork some components by platform. 4 | .*/*.web.js 5 | .*/*.android.js 6 | 7 | # Some modules have their own node_modules with overlap 8 | .*/node_modules/node-haste/.* 9 | 10 | # Ugh 11 | .*/node_modules/babel.* 12 | .*/node_modules/babylon.* 13 | .*/node_modules/invariant.* 14 | 15 | # Ignore react and fbjs where there are overlaps, but don't ignore 16 | # anything that react-native relies on 17 | .*/node_modules/fbjs/lib/Map.js 18 | .*/node_modules/fbjs/lib/fetch.js 19 | .*/node_modules/fbjs/lib/ExecutionEnvironment.js 20 | .*/node_modules/fbjs/lib/ErrorUtils.js 21 | 22 | # Flow has a built-in definition for the 'react' module which we prefer to use 23 | # over the currently-untyped source 24 | .*/node_modules/react/react.js 25 | .*/node_modules/react/lib/React.js 26 | .*/node_modules/react/lib/ReactDOM.js 27 | 28 | .*/__mocks__/.* 29 | .*/__tests__/.* 30 | 31 | .*/commoner/test/source/widget/share.js 32 | 33 | # Ignore commoner tests 34 | .*/node_modules/commoner/test/.* 35 | 36 | # See https://github.com/facebook/flow/issues/442 37 | .*/react-tools/node_modules/commoner/lib/reader.js 38 | 39 | # Ignore jest 40 | .*/node_modules/jest-cli/.* 41 | 42 | # Ignore Website 43 | .*/website/.* 44 | 45 | # Ignore generators 46 | .*/local-cli/generator.* 47 | 48 | # Ignore BUCK generated folders 49 | .*\.buckd/ 50 | 51 | .*/node_modules/is-my-json-valid/test/.*\.json 52 | .*/node_modules/iconv-lite/encodings/tables/.*\.json 53 | .*/node_modules/y18n/test/.*\.json 54 | .*/node_modules/spdx-license-ids/spdx-license-ids.json 55 | .*/node_modules/spdx-exceptions/index.json 56 | .*/node_modules/resolve/test/subdirs/node_modules/a/b/c/x.json 57 | .*/node_modules/resolve/lib/core.json 58 | .*/node_modules/jsonparse/samplejson/.*\.json 59 | .*/node_modules/json5/test/.*\.json 60 | .*/node_modules/ua-parser-js/test/.*\.json 61 | .*/node_modules/builtin-modules/builtin-modules.json 62 | .*/node_modules/binary-extensions/binary-extensions.json 63 | .*/node_modules/url-regex/tlds.json 64 | .*/node_modules/joi/.*\.json 65 | .*/node_modules/isemail/.*\.json 66 | .*/node_modules/tr46/.*\.json 67 | 68 | 69 | [include] 70 | 71 | [libs] 72 | node_modules/react-native/Libraries/react-native/react-native-interface.js 73 | node_modules/react-native/flow 74 | flow/ 75 | 76 | [options] 77 | module.system=haste 78 | 79 | esproposal.class_static_fields=enable 80 | esproposal.class_instance_fields=enable 81 | 82 | munge_underscores=true 83 | 84 | module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' 85 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\)$' -> 'RelativeImageStub' 86 | 87 | suppress_type=$FlowIssue 88 | suppress_type=$FlowFixMe 89 | suppress_type=$FixMe 90 | 91 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(2[0-3]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 92 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-3]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 93 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 94 | 95 | [version] 96 | >=0.47.0 97 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for react-native-keyboard-aware-scroll-view 2 | // Project: https://github.com/APSL/react-native-keyboard-aware-scroll-view 3 | // Definitions by: Kyle Roach 4 | // TypeScript Version: 2.3.2 5 | 6 | import * as React from 'react' 7 | import { 8 | ScrollViewProps, 9 | FlatListProps, 10 | SectionListProps 11 | } from 'react-native' 12 | 13 | interface KeyboardAwareProps { 14 | /** 15 | * Catches the reference of the component. 16 | * 17 | * 18 | * @type {function} 19 | * @memberof KeyboardAwareProps 20 | */ 21 | innerRef?: (ref: JSX.Element) => void 22 | /** 23 | * Adds an extra offset that represents the TabBarIOS height. 24 | * 25 | * Default is false 26 | * @type {boolean} 27 | * @memberof KeyboardAwareProps 28 | */ 29 | viewIsInsideTabBar?: boolean 30 | 31 | /** 32 | * Coordinates that will be used to reset the scroll when the keyboard hides. 33 | * 34 | * @type {{ 35 | * x: number, 36 | * y: number 37 | * }} 38 | * @memberof KeyboardAwareProps 39 | */ 40 | resetScrollToCoords?: { 41 | x: number 42 | y: number 43 | } 44 | 45 | /** 46 | * Lets the user enable or disable automatic resetScrollToCoords 47 | * 48 | * @type {boolean} 49 | * @memberof KeyboardAwareProps 50 | */ 51 | enableResetScrollToCoords?: boolean 52 | 53 | /** 54 | * When focus in TextInput will scroll the position 55 | * 56 | * Default is true 57 | * 58 | * @type {boolean} 59 | * @memberof KeyboardAwareProps 60 | */ 61 | 62 | enableAutomaticScroll?: boolean 63 | /** 64 | * Enables keyboard aware settings for Android 65 | * 66 | * Default is false 67 | * 68 | * @type {boolean} 69 | * @memberof KeyboardAwareProps 70 | */ 71 | enableOnAndroid?: boolean 72 | 73 | /** 74 | * Adds an extra offset when focusing the TextInputs. 75 | * 76 | * Default is 75 77 | * @type {number} 78 | * @memberof KeyboardAwareProps 79 | */ 80 | extraHeight?: number 81 | 82 | /** 83 | * Adds an extra offset to the keyboard. 84 | * Useful if you want to stick elements above the keyboard. 85 | * 86 | * Default is 0 87 | * 88 | * @type {number} 89 | * @memberof KeyboardAwareProps 90 | */ 91 | extraScrollHeight?: number 92 | 93 | /** 94 | * Sets the delay time before scrolling to new position 95 | * 96 | * Default is 250 97 | * 98 | * @type {number} 99 | * @memberof KeyboardAwareProps 100 | */ 101 | keyboardOpeningTime?: number 102 | 103 | /** 104 | * Callback when the keyboard will show. 105 | * 106 | * @param frames Information about the keyboard frame and animation. 107 | */ 108 | onKeyboardWillShow?: (frames: Object) => void 109 | 110 | /** 111 | * Callback when the keyboard did show. 112 | * 113 | * @param frames Information about the keyboard frame and animation. 114 | */ 115 | onKeyboardDidShow?: (frames: Object) => void 116 | 117 | /** 118 | * Callback when the keyboard will hide. 119 | * 120 | * @param frames Information about the keyboard frame and animation. 121 | */ 122 | onKeyboardWillHide?: (frames: Object) => void 123 | 124 | /** 125 | * Callback when the keyboard did hide. 126 | * 127 | * @param frames Information about the keyboard frame and animation. 128 | */ 129 | onKeyboardDidHide?: (frames: Object) => void 130 | 131 | /** 132 | * Callback when the keyboard frame will change. 133 | * 134 | * @param frames Information about the keyboard frame and animation. 135 | */ 136 | onKeyboardWillChangeFrame?: (frames: Object) => void 137 | 138 | /** 139 | * Callback when the keyboard frame did change. 140 | * 141 | * @param frames Information about the keyboard frame and animation. 142 | */ 143 | onKeyboardDidChangeFrame?: (frames: Object) => void 144 | } 145 | 146 | interface KeyboardAwareScrollViewProps 147 | extends KeyboardAwareProps, 148 | ScrollViewProps {} 149 | interface KeyboardAwareFlatListProps 150 | extends KeyboardAwareProps, 151 | FlatListProps {} 152 | interface KeyboardAwareSectionListProps 153 | extends KeyboardAwareProps, 154 | SectionListProps {} 155 | 156 | interface KeyboardAwareState { 157 | keyboardSpace: number 158 | } 159 | 160 | declare class ScrollableComponent extends React.Component { 161 | getScrollResponder: () => void 162 | scrollToPosition: (x: number, y: number, animated?: boolean) => void 163 | scrollToEnd: (animated?: boolean) => void 164 | scrollForExtraHeightOnAndroid: (extraHeight: number) => void 165 | scrollToFocusedInput: ( 166 | reactNode: Object, 167 | extraHeight?: number, 168 | keyboardOpeningTime?: number 169 | ) => void 170 | } 171 | 172 | export class KeyboardAwareMixin {} 173 | export class KeyboardAwareScrollView extends ScrollableComponent< 174 | KeyboardAwareScrollViewProps, 175 | KeyboardAwareState 176 | > {} 177 | export class KeyboardAwareFlatList extends ScrollableComponent< 178 | KeyboardAwareFlatListProps, 179 | KeyboardAwareState 180 | > {} 181 | export class KeyboardAwareSectionList extends ScrollableComponent< 182 | KeyboardAwareSectionListProps, 183 | KeyboardAwareState 184 | > {} 185 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-keyboard-aware-scroll-view 2 | 3 |

4 | 5 | 6 | 7 |

8 | 9 | A ScrollView component that handles keyboard appearance and automatically scrolls to focused `TextInput`. 10 | 11 |

12 | Scroll demo 13 |

14 | 15 | ## Supported versions 16 | 17 | - `v2.0.0` requires `RN>=0.65.1` 18 | - `v1.0.0` requires `RN>=0.63.0` 19 | 20 | ## Installation 21 | 22 | Installation can be done through `npm`: 23 | 24 | ```shell 25 | npm i @codler/react-native-keyboard-aware-scroll-view --save 26 | ``` 27 | 28 | ## Usage 29 | 30 | You can use the `KeyboardAwareScrollView`, `KeyboardAwareSectionList` or the `KeyboardAwareFlatList` 31 | components. They accept `ScrollView`, `SectionList` and `FlatList` default props respectively and 32 | implement a custom high order component called `KeyboardAwareHOC` to handle keyboard appearance. 33 | The high order component is also available if you want to use it in any other component. 34 | 35 | Import `react-native-keyboard-aware-scroll-view` and wrap your content inside 36 | it: 37 | 38 | ```js 39 | import { KeyboardAwareScrollView } from '@codler/react-native-keyboard-aware-scroll-view' 40 | ``` 41 | 42 | ```jsx 43 | 44 | 45 | 46 | 47 | 48 | ``` 49 | 50 | ## Auto-scroll in `TextInput` fields 51 | 52 | As of `v0.1.0`, the component auto scrolls to the focused `TextInput` 😎. For versions `v0.0.7` and older you can do the following. 53 | 54 | ### Programatically scroll to any `TextInput` 55 | 56 | In order to scroll to any `TextInput` field, you can use the built-in method `scrollToFocusedInput`. Example: 57 | 58 | ```js 59 | _scrollToInput (reactNode: any) { 60 | // Add a 'scroll' ref to your ScrollView 61 | this.scroll.props.scrollToFocusedInput(reactNode) 62 | } 63 | ``` 64 | 65 | ```jsx 66 | { 68 | this.scroll = ref 69 | }}> 70 | 71 | { 73 | // `bind` the function if you're using ES6 classes 74 | this._scrollToInput(ReactNative.findNodeHandle(event.target)) 75 | }} 76 | /> 77 | 78 | 79 | ``` 80 | 81 | ### Programatically scroll to any position 82 | 83 | There's another built-in function that lets you programatically scroll to any position of the scroll view: 84 | 85 | ```js 86 | this.scroll.props.scrollToPosition(0, 0) 87 | ``` 88 | 89 | ## Register to keyboard events 90 | 91 | You can register to `ScrollViewResponder` events `onKeyboardWillShow` and `onKeyboardWillHide`: 92 | 93 | ```jsx 94 | { 96 | console.log('Keyboard event', frames) 97 | }}> 98 | 99 | 100 | 101 | 102 | ``` 103 | 104 | ## Android Support 105 | 106 | First, Android natively has this feature, you can easily enable it by setting `windowSoftInputMode` in `AndroidManifest.xml`. Check [here](https://developer.android.com/guide/topics/manifest/activity-element.html#wsoft). 107 | 108 | But if you want to use feature like `extraHeight`, you need to enable Android Support with the following steps: 109 | 110 | - Make sure you are using react-native `0.46` or above. 111 | - Set `windowSoftInputMode` to `adjustPan` in `AndroidManifest.xml`. 112 | - Set `enableOnAndroid` property to `true`. 113 | 114 | Android Support is not perfect, here is the supported list: 115 | 116 | | **Prop** | **Android Support** | 117 | | --------------------------- | ------------------- | 118 | | `viewIsInsideTabBar` | Yes | 119 | | `resetScrollToCoords` | Yes | 120 | | `enableAutomaticScroll` | Yes | 121 | | `extraHeight` | Yes | 122 | | `extraScrollHeight` | Yes | 123 | | `enableResetScrollToCoords` | Yes | 124 | | `keyboardOpeningTime` | No | 125 | 126 | ## API 127 | 128 | ### Props 129 | 130 | All the `ScrollView`/`FlatList` props will be passed. 131 | 132 | | **Prop** | **Type** | **Description** | 133 | | --------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------- | 134 | | `innerRef` | `Function` | Catch the reference of the component. | 135 | | `viewIsInsideTabBar` | `boolean` | Adds an extra offset that represents the `TabBarIOS` height. | 136 | | `resetScrollToCoords` | `Object: {x: number, y: number}` | Coordinates that will be used to reset the scroll when the keyboard hides. | 137 | | `enableAutomaticScroll` | `boolean` | When focus in `TextInput` will scroll the position, default is enabled. | 138 | | `extraHeight` | `number` | Adds an extra offset when focusing the `TextInput`s. | 139 | | `extraScrollHeight` | `number` | Adds an extra offset to the keyboard. Useful if you want to stick elements above the keyboard. | 140 | | `enableResetScrollToCoords` | `boolean` | Lets the user enable or disable automatic resetScrollToCoords. | 141 | | `keyboardOpeningTime` | `number` | Sets the delay time before scrolling to new position, default is 250 | 142 | | `enableOnAndroid` | `boolean` | Enable Android Support | 143 | 144 | ### Methods 145 | 146 | Use `innerRef` to get the component reference and use `this.scrollRef.props` to access these methods. 147 | 148 | | **Method** | **Parameter** | **Description** | 149 | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | 150 | | `getScrollResponder` | `void` | Get `ScrollResponder` | 151 | | `scrollToPosition` | `x: number, y: number, animated: bool = true` | Scroll to specific position with or without animation. | 152 | | `scrollToEnd` | `animated?: bool = true` | Scroll to end with or without animation. | 153 | | `scrollIntoView` | `element: React.Element<*>, options: { getScrollPosition: ?(parentLayout, childLayout, contentOffset) => { x: number, y: number, animated: boolean } }` | Scrolls an element inside a KeyboardAwareScrollView into view. | 154 | 155 | ### Using high order component 156 | 157 | Enabling any component to be keyboard-aware is very easy. Take a look at the code of `KeyboardAwareFlatList`: 158 | 159 | ```js 160 | /* @flow */ 161 | 162 | import { FlatList } from 'react-native' 163 | import listenToKeyboardEvents from './KeyboardAwareHOC' 164 | 165 | export default listenToKeyboardEvents(FlatList) 166 | ``` 167 | 168 | The HOC can also be configured. Sometimes it's more convenient to provide a static config than configuring the behavior with props. This HOC config can be overriden with props. 169 | 170 | ```js 171 | /* @flow */ 172 | 173 | import { FlatList } from 'react-native' 174 | import listenToKeyboardEvents from './KeyboardAwareHOC' 175 | 176 | const config = { 177 | enableOnAndroid: true, 178 | enableAutomaticScroll: true 179 | } 180 | 181 | export default listenToKeyboardEvents(config)(FlatList) 182 | ``` 183 | 184 | The available config options are: 185 | 186 | ```js 187 | { 188 | enableOnAndroid: boolean, 189 | contentContainerStyle: ?Object, 190 | enableAutomaticScroll: boolean, 191 | extraHeight: number, 192 | extraScrollHeight: number, 193 | enableResetScrollToCoords: boolean, 194 | keyboardOpeningTime: number, 195 | viewIsInsideTabBar: boolean, 196 | refPropName: string, 197 | extractNativeRef: Function 198 | } 199 | ``` 200 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaFeatures": { 5 | "jsx": true 6 | } 7 | }, 8 | "env": { 9 | "es6": true, 10 | "jasmine": true 11 | }, 12 | "plugins": ["react", "react-native", "flowtype"], 13 | // Map from global var to bool specifying if it can be redefined 14 | "globals": { 15 | "__DEV__": true, 16 | "__dirname": false, 17 | "__fbBatchedBridgeConfig": false, 18 | "alert": false, 19 | "cancelAnimationFrame": false, 20 | "cancelIdleCallback": false, 21 | "clearImmediate": true, 22 | "clearInterval": false, 23 | "clearTimeout": false, 24 | "console": false, 25 | "document": false, 26 | "escape": false, 27 | "Event": false, 28 | "EventTarget": false, 29 | "exports": false, 30 | "fetch": false, 31 | "FormData": false, 32 | "global": false, 33 | "Generator": true, 34 | "jest": false, 35 | "Map": true, 36 | "module": false, 37 | "navigator": false, 38 | "process": false, 39 | "Promise": true, 40 | "requestAnimationFrame": true, 41 | "requestIdleCallback": true, 42 | "require": false, 43 | "Set": true, 44 | "setImmediate": true, 45 | "setInterval": false, 46 | "setTimeout": false, 47 | "window": false, 48 | "XMLHttpRequest": false, 49 | "pit": false, 50 | "test": true, 51 | // Flow global types. 52 | "ReactComponent": false, 53 | "ReactClass": false, 54 | "ReactElement": false, 55 | "ReactPropsCheckType": false, 56 | "ReactPropsChainableTypeChecker": false, 57 | "ReactPropTypes": false, 58 | "SyntheticEvent": false, 59 | "$Either": false, 60 | "$All": false, 61 | "$ArrayBufferView": false, 62 | "$Tuple": false, 63 | "$Supertype": false, 64 | "$Subtype": false, 65 | "$Shape": false, 66 | "$Diff": false, 67 | "$Keys": false, 68 | "$Enum": false, 69 | "$Exports": false, 70 | "$FlowIssue": false, 71 | "$FlowFixMe": false, 72 | "$FixMe": false 73 | }, 74 | "rules": { 75 | "comma-dangle": 0, // disallow trailing commas in object literals 76 | "no-cond-assign": 1, // disallow assignment in conditional expressions 77 | "no-console": 0, // disallow use of console (off by default in the node environment) 78 | "no-const-assign": 2, // disallow assignment to const-declared variables 79 | "no-constant-condition": 0, // disallow use of constant expressions in conditions 80 | "no-control-regex": 1, // disallow control characters in regular expressions 81 | "no-debugger": 0, // disallow use of debugger 82 | "no-dupe-keys": 1, // disallow duplicate keys when creating object literals 83 | "no-empty": 0, // disallow empty statements 84 | "no-ex-assign": 1, // disallow assigning to the exception in a catch block 85 | "no-extra-boolean-cast": 1, // disallow double-negation boolean casts in a boolean context 86 | "no-extra-parens": 0, // disallow unnecessary parentheses (off by default) 87 | "no-extra-semi": 1, // disallow unnecessary semicolons 88 | "no-func-assign": 1, // disallow overwriting functions written as function declarations 89 | "no-inner-declarations": 0, // disallow function or variable declarations in nested blocks 90 | "no-invalid-regexp": 1, // disallow invalid regular expression strings in the RegExp constructor 91 | "no-negated-in-lhs": 1, // disallow negation of the left operand of an in expression 92 | "no-obj-calls": 1, // disallow the use of object properties of the global object (Math and JSON) as functions 93 | "no-regex-spaces": 1, // disallow multiple spaces in a regular expression literal 94 | "no-reserved-keys": 0, // disallow reserved words being used as object literal keys (off by default) 95 | "no-sparse-arrays": 1, // disallow sparse arrays 96 | "no-unreachable": 1, // disallow unreachable statements after a return, throw, continue, or break statement 97 | "use-isnan": 1, // disallow comparisons with the value NaN 98 | "valid-jsdoc": 0, // Ensure JSDoc comments are valid (off by default) 99 | "valid-typeof": 1, // Ensure that the results of typeof are compared against a valid string 100 | // Best Practices 101 | // These are rules designed to prevent you from making mistakes. They either prescribe a better way of doing something or help you avoid footguns. 102 | "block-scoped-var": 0, // treat var statements as if they were block scoped (off by default) 103 | "complexity": 0, // specify the maximum cyclomatic complexity allowed in a program (off by default) 104 | "consistent-return": 0, // require return statements to either always or never specify values 105 | "curly": 1, // specify curly brace conventions for all control statements 106 | "default-case": 0, // require default case in switch statements (off by default) 107 | "dot-notation": 1, // encourages use of dot notation whenever possible 108 | "eqeqeq": [1, "allow-null"], // require the use of === and !== 109 | "guard-for-in": 0, // make sure for-in loops have an if statement (off by default) 110 | "no-alert": 1, // disallow the use of alert, confirm, and prompt 111 | "no-caller": 1, // disallow use of arguments.caller or arguments.callee 112 | "no-div-regex": 1, // disallow division operators explicitly at beginning of regular expression (off by default) 113 | "no-else-return": 0, // disallow else after a return in an if (off by default) 114 | "no-eq-null": 0, // disallow comparisons to null without a type-checking operator (off by default) 115 | "no-eval": 1, // disallow use of eval() 116 | "no-extend-native": 1, // disallow adding to native types 117 | "no-extra-bind": 1, // disallow unnecessary function binding 118 | "no-fallthrough": 1, // disallow fallthrough of case statements 119 | "no-floating-decimal": 1, // disallow the use of leading or trailing decimal points in numeric literals (off by default) 120 | "no-implied-eval": 1, // disallow use of eval()-like methods 121 | "no-labels": 1, // disallow use of labeled statements 122 | "no-iterator": 1, // disallow usage of __iterator__ property 123 | "no-lone-blocks": 1, // disallow unnecessary nested blocks 124 | "no-loop-func": 0, // disallow creation of functions within loops 125 | "no-multi-str": 0, // disallow use of multiline strings 126 | "no-native-reassign": 0, // disallow reassignments of native objects 127 | "no-new": 1, // disallow use of new operator when not part of the assignment or comparison 128 | "no-new-func": 1, // disallow use of new operator for Function object 129 | "no-new-wrappers": 1, // disallows creating new instances of String,Number, and Boolean 130 | "no-octal": 1, // disallow use of octal literals 131 | "no-octal-escape": 1, // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251"; 132 | "no-proto": 1, // disallow usage of __proto__ property 133 | "no-redeclare": 0, // disallow declaring the same variable more then once 134 | "no-return-assign": 1, // disallow use of assignment in return statement 135 | "no-script-url": 1, // disallow use of javascript: urls. 136 | "no-self-compare": 1, // disallow comparisons where both sides are exactly the same (off by default) 137 | "no-sequences": 1, // disallow use of comma operator 138 | "no-unused-expressions": 0, // disallow usage of expressions in statement position 139 | "no-void": 1, // disallow use of void operator (off by default) 140 | "no-warning-comments": 0, // disallow usage of configurable warning terms in comments": 1, // e.g. TODO or FIXME (off by default) 141 | "no-with": 1, // disallow use of the with statement 142 | "radix": 1, // require use of the second argument for parseInt() (off by default) 143 | "semi-spacing": 1, // require a space after a semi-colon 144 | "vars-on-top": 0, // requires to declare all vars on top of their containing scope (off by default) 145 | "wrap-iife": 0, // require immediate function invocation to be wrapped in parentheses (off by default) 146 | "yoda": 1, // require or disallow Yoda conditions 147 | // Variables 148 | // These rules have to do with variable declarations. 149 | "no-catch-shadow": 1, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment) 150 | "no-delete-var": 1, // disallow deletion of variables 151 | "no-label-var": 1, // disallow labels that share a name with a variable 152 | "no-shadow": 1, // disallow declaration of variables already declared in the outer scope 153 | "no-shadow-restricted-names": 1, // disallow shadowing of names such as arguments 154 | "no-undef": 2, // disallow use of undeclared variables unless mentioned in a /*global */ block 155 | "no-undefined": 0, // disallow use of undefined variable (off by default) 156 | "no-undef-init": 1, // disallow use of undefined when initializing variables 157 | "no-unused-vars": [ 158 | 1, 159 | { 160 | "vars": "all", 161 | "args": "none" 162 | } 163 | ], // disallow declaration of variables that are not used in the code 164 | "no-use-before-define": 0, // disallow use of variables before they are defined 165 | // Node.js 166 | // These rules are specific to JavaScript running on Node.js. 167 | "handle-callback-err": 1, // enforces error handling in callbacks (off by default) (on by default in the node environment) 168 | "no-mixed-requires": 1, // disallow mixing regular variable and require declarations (off by default) (on by default in the node environment) 169 | "no-new-require": 1, // disallow use of new operator with the require function (off by default) (on by default in the node environment) 170 | "no-path-concat": 1, // disallow string concatenation with __dirname and __filename (off by default) (on by default in the node environment) 171 | "no-process-exit": 0, // disallow process.exit() (on by default in the node environment) 172 | "no-restricted-modules": 1, // restrict usage of specified node modules (off by default) 173 | "no-sync": 0, // disallow use of synchronous methods (off by default) 174 | // Stylistic Issues 175 | // These rules are purely matters of style and are quite subjective. 176 | "key-spacing": 0, 177 | "keyword-spacing": 1, // enforce spacing before and after keywords 178 | "jsx-quotes": [1, "prefer-single"], 179 | "comma-spacing": 0, 180 | "no-multi-spaces": 0, 181 | "brace-style": 0, // enforce one true brace style (off by default) 182 | "camelcase": 0, // require camel case names 183 | "consistent-this": [1, "self"], // enforces consistent naming when capturing the current execution context (off by default) 184 | "eol-last": 1, // enforce newline at the end of file, with no multiple empty lines 185 | "func-names": 0, // require function expressions to have a name (off by default) 186 | "func-style": 0, // enforces use of function declarations or expressions (off by default) 187 | "new-cap": 0, // require a capital letter for constructors 188 | "new-parens": 1, // disallow the omission of parentheses when invoking a constructor with no arguments 189 | "no-nested-ternary": 0, // disallow nested ternary expressions (off by default) 190 | "no-array-constructor": 1, // disallow use of the Array constructor 191 | "no-lonely-if": 0, // disallow if as the only statement in an else block (off by default) 192 | "no-new-object": 1, // disallow use of the Object constructor 193 | "no-spaced-func": 1, // disallow space between function identifier and application 194 | "no-ternary": 0, // disallow the use of ternary operators (off by default) 195 | "no-trailing-spaces": 1, // disallow trailing whitespace at the end of lines 196 | "no-underscore-dangle": 0, // disallow dangling underscores in identifiers 197 | "no-mixed-spaces-and-tabs": 1, // disallow mixed spaces and tabs for indentation 198 | "quotes": [1, "single", "avoid-escape"], // specify whether double or single quotes should be used 199 | "quote-props": 0, // require quotes around object literal property names (off by default) 200 | "semi": ["error", "never"], // require or disallow use of semicolons instead of ASI 201 | "sort-vars": 0, // sort variables within the same declaration block (off by default) 202 | "space-in-brackets": 0, // require or disallow spaces inside brackets (off by default) 203 | "space-in-parens": 0, // require or disallow spaces inside parentheses (off by default) 204 | "space-infix-ops": 1, // require spaces around operators 205 | "space-unary-ops": [ 206 | 1, 207 | { 208 | "words": true, 209 | "nonwords": false 210 | } 211 | ], // require or disallow spaces before/after unary operators (words on by default, nonwords off by default) 212 | "max-nested-callbacks": 0, // specify the maximum depth callbacks can be nested (off by default) 213 | "one-var": 0, // allow just one var statement per function (off by default) 214 | "wrap-regex": 0, // require regex literals to be wrapped in parentheses (off by default) 215 | // Legacy 216 | // The following rules are included for compatibility with JSHint and JSLint. While the names of the rules may not match up with the JSHint/JSLint counterpart, the functionality is the same. 217 | "max-depth": 0, // specify the maximum depth that blocks can be nested (off by default) 218 | "max-len": 0, // specify the maximum length of a line in your program (off by default) 219 | "max-params": 0, // limits the number of parameters that can be used in the function declaration. (off by default) 220 | "max-statements": 0, // specify the maximum number of statement allowed in a function (off by default) 221 | "no-bitwise": 1, // disallow use of bitwise operators (off by default) 222 | "no-plusplus": 0, // disallow use of unary operators, ++ and -- (off by default) 223 | // React Plugin 224 | // The following rules are made available via `eslint-plugin-react`. 225 | "react/display-name": 0, 226 | "react/jsx-boolean-value": 0, 227 | "react/jsx-no-duplicate-props": 2, 228 | "react/jsx-no-undef": 1, 229 | "react/jsx-sort-props": 0, 230 | "react/jsx-uses-react": 1, 231 | "react/jsx-uses-vars": 1, 232 | "react/no-did-mount-set-state": 1, 233 | "react/no-did-update-set-state": 1, 234 | "react/no-multi-comp": 0, 235 | "react/no-string-refs": 1, 236 | "react/no-unknown-property": 0, 237 | "react/prop-types": 0, 238 | "react/react-in-jsx-scope": 1, 239 | "react/self-closing-comp": 1, 240 | "react/wrap-multilines": 0, 241 | // Flowtype Plugin 242 | "flowtype/boolean-style": [2, "boolean"], 243 | "flowtype/define-flow-type": 1, 244 | "flowtype/delimiter-dangle": [2, "never"], 245 | "flowtype/generic-spacing": [2, "never"], 246 | "flowtype/no-primitive-constructor-types": 2, 247 | "flowtype/no-weak-types": 0, 248 | "flowtype/object-type-delimiter": [2, "comma"], 249 | "flowtype/require-parameter-type": 2, 250 | "flowtype/require-return-type": 0, 251 | "flowtype/require-valid-file-annotation": 2, 252 | "flowtype/semi": [2, "never"], 253 | "flowtype/space-after-type-colon": [2, "always"], 254 | "flowtype/space-before-generic-bracket": [2, "never"], 255 | "flowtype/space-before-type-colon": [2, "never"], 256 | "flowtype/type-id-match": 0, 257 | "flowtype/union-intersection-spacing": [2, "always"], 258 | "flowtype/use-flow-type": 1, 259 | "flowtype/valid-syntax": 1 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /lib/KeyboardAwareHOC.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react' 4 | import { 5 | Keyboard, 6 | Platform, 7 | UIManager, 8 | TextInput, 9 | findNodeHandle, 10 | Animated 11 | } from 'react-native' 12 | import { isIphoneX } from 'react-native-iphone-x-helper' 13 | import type { KeyboardAwareInterface } from './KeyboardAwareInterface' 14 | 15 | const _KAM_DEFAULT_TAB_BAR_HEIGHT: number = isIphoneX() ? 83 : 49 16 | const _KAM_KEYBOARD_OPENING_TIME: number = 250 17 | const _KAM_EXTRA_HEIGHT: number = 75 18 | 19 | const supportedKeyboardEvents = [ 20 | 'keyboardWillShow', 21 | 'keyboardDidShow', 22 | 'keyboardWillHide', 23 | 'keyboardDidHide', 24 | 'keyboardWillChangeFrame', 25 | 'keyboardDidChangeFrame' 26 | ] 27 | const keyboardEventToCallbackName = (eventName: string) => 28 | 'on' + eventName[0].toUpperCase() + eventName.substring(1) 29 | const keyboardAwareHOCTypeEvents = supportedKeyboardEvents.reduce( 30 | (acc: Object, eventName: string) => ({ 31 | ...acc, 32 | [keyboardEventToCallbackName(eventName)]: Function 33 | }), 34 | {} 35 | ) 36 | 37 | export type KeyboardAwareHOCProps = { 38 | viewIsInsideTabBar?: boolean, 39 | resetScrollToCoords?: { 40 | x: number, 41 | y: number 42 | }, 43 | enableResetScrollToCoords?: boolean, 44 | enableAutomaticScroll?: boolean, 45 | extraHeight?: number, 46 | extraScrollHeight?: number, 47 | keyboardOpeningTime?: number, 48 | onScroll?: Function, 49 | update?: Function, 50 | contentContainerStyle?: any, 51 | enableOnAndroid?: boolean, 52 | innerRef?: Function, 53 | ...keyboardAwareHOCTypeEvents 54 | } 55 | export type KeyboardAwareHOCState = { 56 | keyboardSpace: number 57 | } 58 | 59 | export type ElementLayout = { 60 | x: number, 61 | y: number, 62 | width: number, 63 | height: number 64 | } 65 | 66 | export type ContentOffset = { 67 | x: number, 68 | y: number 69 | } 70 | 71 | export type ScrollPosition = { 72 | x: number, 73 | y: number, 74 | animated: boolean 75 | } 76 | 77 | export type ScrollIntoViewOptions = ?{ 78 | getScrollPosition?: ( 79 | parentLayout: ElementLayout, 80 | childLayout: ElementLayout, 81 | contentOffset: ContentOffset 82 | ) => ScrollPosition 83 | } 84 | 85 | export type KeyboardAwareHOCOptions = ?{ 86 | enableOnAndroid: boolean, 87 | contentContainerStyle: ?Object, 88 | enableAutomaticScroll: boolean, 89 | extraHeight: number, 90 | extraScrollHeight: number, 91 | enableResetScrollToCoords: boolean, 92 | keyboardOpeningTime: number, 93 | viewIsInsideTabBar: boolean, 94 | refPropName: string, 95 | extractNativeRef: Function 96 | } 97 | 98 | function getDisplayName(WrappedComponent: React$Component) { 99 | return ( 100 | (WrappedComponent && 101 | (WrappedComponent.displayName || WrappedComponent.name)) || 102 | 'Component' 103 | ) 104 | } 105 | 106 | const ScrollIntoViewDefaultOptions: KeyboardAwareHOCOptions = { 107 | enableOnAndroid: false, 108 | contentContainerStyle: undefined, 109 | enableAutomaticScroll: true, 110 | extraHeight: _KAM_EXTRA_HEIGHT, 111 | extraScrollHeight: 0, 112 | enableResetScrollToCoords: true, 113 | keyboardOpeningTime: _KAM_KEYBOARD_OPENING_TIME, 114 | viewIsInsideTabBar: false, 115 | 116 | // The ref prop name that will be passed to the wrapped component to obtain a ref 117 | // If your ScrollView is already wrapped, maybe the wrapper permit to get a ref 118 | // For example, with glamorous-native ScrollView, you should use "innerRef" 119 | refPropName: 'ref', 120 | // Sometimes the ref you get is a ref to a wrapped view (ex: Animated.ScrollView) 121 | // We need access to the imperative API of a real native ScrollView so we need extraction logic 122 | extractNativeRef: (ref: Object) => { 123 | // getNode() permit to support Animated.ScrollView automatically 124 | // see https://github.com/facebook/react-native/issues/19650 125 | // see https://stackoverflow.com/questions/42051368/scrollto-is-undefined-on-animated-scrollview/48786374 126 | // see https://github.com/facebook/react-native/commit/66e72bb4e00aafbcb9f450ed5db261d98f99f82a 127 | return ref 128 | } 129 | } 130 | 131 | function KeyboardAwareHOC( 132 | ScrollableComponent: React$Component, 133 | userOptions: KeyboardAwareHOCOptions = {} 134 | ) { 135 | const hocOptions: KeyboardAwareHOCOptions = { 136 | ...ScrollIntoViewDefaultOptions, 137 | ...userOptions 138 | } 139 | 140 | return class 141 | extends React.Component 142 | implements KeyboardAwareInterface { 143 | _rnkasv_keyboardView: any 144 | keyboardWillShowEvent: ?Function 145 | keyboardWillHideEvent: ?Function 146 | position: ContentOffset 147 | defaultResetScrollToCoords: ?{ x: number, y: number } 148 | mountedComponent: boolean 149 | handleOnScroll: Function 150 | state: KeyboardAwareHOCState 151 | static displayName = `KeyboardAware${getDisplayName(ScrollableComponent)}` 152 | 153 | // HOC options are used to init default props, so that these options can be overriden with component props 154 | static defaultProps = { 155 | enableAutomaticScroll: hocOptions.enableAutomaticScroll, 156 | extraHeight: hocOptions.extraHeight, 157 | extraScrollHeight: hocOptions.extraScrollHeight, 158 | enableResetScrollToCoords: hocOptions.enableResetScrollToCoords, 159 | keyboardOpeningTime: hocOptions.keyboardOpeningTime, 160 | viewIsInsideTabBar: hocOptions.viewIsInsideTabBar, 161 | enableOnAndroid: hocOptions.enableOnAndroid 162 | } 163 | 164 | constructor(props: KeyboardAwareHOCProps) { 165 | super(props) 166 | this.keyboardWillShowEvent = undefined 167 | this.keyboardWillHideEvent = undefined 168 | this.callbacks = {} 169 | this.position = { x: 0, y: 0 } 170 | this.defaultResetScrollToCoords = null 171 | const keyboardSpace: number = props.viewIsInsideTabBar 172 | ? _KAM_DEFAULT_TAB_BAR_HEIGHT 173 | : 0 174 | this.state = { keyboardSpace } 175 | } 176 | 177 | componentDidMount() { 178 | this.mountedComponent = true 179 | // Keyboard events 180 | if (Platform.OS === 'ios') { 181 | this.keyboardWillShowEvent = Keyboard.addListener( 182 | 'keyboardWillShow', 183 | this._updateKeyboardSpace 184 | ) 185 | this.keyboardWillHideEvent = Keyboard.addListener( 186 | 'keyboardWillHide', 187 | this._resetKeyboardSpace 188 | ) 189 | } else if (Platform.OS === 'android' && this.props.enableOnAndroid) { 190 | this.keyboardWillShowEvent = Keyboard.addListener( 191 | 'keyboardDidShow', 192 | this._updateKeyboardSpace 193 | ) 194 | this.keyboardWillHideEvent = Keyboard.addListener( 195 | 'keyboardDidHide', 196 | this._resetKeyboardSpace 197 | ) 198 | } 199 | 200 | supportedKeyboardEvents.forEach((eventName: string) => { 201 | const callbackName = keyboardEventToCallbackName(eventName) 202 | if (this.props[callbackName]) { 203 | this.callbacks[eventName] = Keyboard.addListener( 204 | eventName, 205 | this.props[callbackName] 206 | ) 207 | } 208 | }) 209 | } 210 | 211 | componentDidUpdate(prevProps: KeyboardAwareHOCProps) { 212 | if (this.props.viewIsInsideTabBar !== prevProps.viewIsInsideTabBar) { 213 | const keyboardSpace: number = this.props.viewIsInsideTabBar 214 | ? _KAM_DEFAULT_TAB_BAR_HEIGHT 215 | : 0 216 | if (this.state.keyboardSpace !== keyboardSpace) { 217 | this.setState({ keyboardSpace }) 218 | } 219 | } 220 | } 221 | 222 | componentWillUnmount() { 223 | this.mountedComponent = false 224 | this.keyboardWillShowEvent && this.keyboardWillShowEvent.remove() 225 | this.keyboardWillHideEvent && this.keyboardWillHideEvent.remove() 226 | Object.values(this.callbacks).forEach((callback: Object) => 227 | callback.remove() 228 | ) 229 | } 230 | 231 | getScrollResponder = () => { 232 | return ( 233 | this._rnkasv_keyboardView && 234 | this._rnkasv_keyboardView.getScrollResponder && 235 | this._rnkasv_keyboardView.getScrollResponder() 236 | ) 237 | } 238 | 239 | scrollToPosition = (x: number, y: number, animated: boolean = true) => { 240 | const responder = this.getScrollResponder() 241 | responder && responder.scrollTo && responder.scrollTo({ x, y, animated }) 242 | } 243 | 244 | scrollToEnd = (animated?: boolean = true) => { 245 | const responder = this.getScrollResponder() 246 | responder && responder.scrollToEnd && responder.scrollToEnd({ animated }) 247 | } 248 | 249 | scrollForExtraHeightOnAndroid = (extraHeight: number) => { 250 | this.scrollToPosition(0, this.position.y + extraHeight, true) 251 | } 252 | 253 | /** 254 | * @param keyboardOpeningTime: takes a different keyboardOpeningTime in consideration. 255 | * @param extraHeight: takes an extra height in consideration. 256 | */ 257 | scrollToFocusedInput = ( 258 | reactNode: any, 259 | extraHeight?: number, 260 | keyboardOpeningTime?: number 261 | ) => { 262 | if (extraHeight === undefined) { 263 | extraHeight = this.props.extraHeight || 0 264 | } 265 | if (keyboardOpeningTime === undefined) { 266 | keyboardOpeningTime = this.props.keyboardOpeningTime || 0 267 | } 268 | setTimeout(() => { 269 | if (!this.mountedComponent) { 270 | return 271 | } 272 | const responder = this.getScrollResponder() 273 | responder && 274 | responder.scrollResponderScrollNativeHandleToKeyboard( 275 | reactNode, 276 | extraHeight, 277 | true 278 | ) 279 | }, keyboardOpeningTime) 280 | } 281 | 282 | scrollIntoView = async ( 283 | element: React.Element<*>, 284 | options: ScrollIntoViewOptions = {} 285 | ) => { 286 | if (!this._rnkasv_keyboardView || !element) { 287 | return 288 | } 289 | 290 | const [parentLayout, childLayout] = await Promise.all([ 291 | this._measureElement(this._rnkasv_keyboardView), 292 | this._measureElement(element) 293 | ]) 294 | 295 | const getScrollPosition = 296 | options.getScrollPosition || this._defaultGetScrollPosition 297 | const { x, y, animated } = getScrollPosition( 298 | parentLayout, 299 | childLayout, 300 | this.position 301 | ) 302 | this.scrollToPosition(x, y, animated) 303 | } 304 | 305 | _defaultGetScrollPosition = ( 306 | parentLayout: ElementLayout, 307 | childLayout: ElementLayout, 308 | contentOffset: ContentOffset 309 | ): ScrollPosition => { 310 | return { 311 | x: 0, 312 | y: Math.max(0, childLayout.y - parentLayout.y + contentOffset.y), 313 | animated: true 314 | } 315 | } 316 | 317 | _measureElement = (element: React.Element<*>): Promise => { 318 | const node = findNodeHandle(element) 319 | return new Promise((resolve: ElementLayout => void) => { 320 | UIManager.measureInWindow( 321 | node, 322 | (x: number, y: number, width: number, height: number) => { 323 | resolve({ x, y, width, height }) 324 | } 325 | ) 326 | }) 327 | } 328 | 329 | // Keyboard actions 330 | _updateKeyboardSpace = (frames: Object) => { 331 | // Automatically scroll to focused TextInput 332 | if (this.props.enableAutomaticScroll) { 333 | let keyboardSpace: number = 334 | frames.endCoordinates.height + this.props.extraScrollHeight 335 | if (this.props.viewIsInsideTabBar) { 336 | keyboardSpace -= _KAM_DEFAULT_TAB_BAR_HEIGHT 337 | } 338 | this.setState({ keyboardSpace }) 339 | const currentlyFocusedField = findNodeHandle(TextInput.State.currentlyFocusedInput()) 340 | const responder = this.getScrollResponder() 341 | if (!currentlyFocusedField || !responder) { 342 | return 343 | } 344 | UIManager.viewIsDescendantOf( 345 | currentlyFocusedField, 346 | responder.getInnerViewNode(), 347 | (isAncestor: boolean) => { 348 | if (isAncestor) { 349 | // Check if the TextInput will be hidden by the keyboard 350 | UIManager.measureInWindow( 351 | currentlyFocusedField, 352 | (x: number, y: number, width: number, height: number) => { 353 | const textInputBottomPosition = y + height 354 | const keyboardPosition = frames.endCoordinates.screenY 355 | const totalExtraHeight = 356 | this.props.extraScrollHeight + this.props.extraHeight 357 | if (Platform.OS === 'ios') { 358 | if ( 359 | textInputBottomPosition > 360 | keyboardPosition - totalExtraHeight 361 | ) { 362 | this._scrollToFocusedInputWithNodeHandle( 363 | currentlyFocusedField 364 | ) 365 | } 366 | } else { 367 | // On android, the system would scroll the text input just 368 | // above the keyboard so we just neet to scroll the extra 369 | // height part 370 | if (textInputBottomPosition > keyboardPosition) { 371 | // Since the system already scrolled the whole view up 372 | // we should reduce that amount 373 | keyboardSpace = 374 | keyboardSpace - 375 | (textInputBottomPosition - keyboardPosition) 376 | this.setState({ keyboardSpace }) 377 | this.scrollForExtraHeightOnAndroid(totalExtraHeight) 378 | } else if ( 379 | textInputBottomPosition > 380 | keyboardPosition - totalExtraHeight 381 | ) { 382 | this.scrollForExtraHeightOnAndroid( 383 | totalExtraHeight - 384 | (keyboardPosition - textInputBottomPosition) 385 | ) 386 | } 387 | } 388 | } 389 | ) 390 | } 391 | } 392 | ) 393 | } 394 | if (!this.props.resetScrollToCoords) { 395 | if (!this.defaultResetScrollToCoords) { 396 | this.defaultResetScrollToCoords = this.position 397 | } 398 | } 399 | } 400 | 401 | _resetKeyboardSpace = () => { 402 | const keyboardSpace: number = this.props.viewIsInsideTabBar 403 | ? _KAM_DEFAULT_TAB_BAR_HEIGHT 404 | : 0 405 | this.setState({ keyboardSpace }) 406 | // Reset scroll position after keyboard dismissal 407 | if (this.props.enableResetScrollToCoords === false) { 408 | this.defaultResetScrollToCoords = null 409 | return 410 | } else if (this.props.resetScrollToCoords) { 411 | this.scrollToPosition( 412 | this.props.resetScrollToCoords.x, 413 | this.props.resetScrollToCoords.y, 414 | true 415 | ) 416 | } else { 417 | if (this.defaultResetScrollToCoords) { 418 | this.scrollToPosition( 419 | this.defaultResetScrollToCoords.x, 420 | this.defaultResetScrollToCoords.y, 421 | true 422 | ) 423 | this.defaultResetScrollToCoords = null 424 | } else { 425 | this.scrollToPosition(0, 0, true) 426 | } 427 | } 428 | } 429 | 430 | _scrollToFocusedInputWithNodeHandle = ( 431 | nodeID: number, 432 | extraHeight?: number, 433 | keyboardOpeningTime?: number 434 | ) => { 435 | if (extraHeight === undefined) { 436 | extraHeight = this.props.extraHeight 437 | } 438 | const reactNode = findNodeHandle(nodeID) 439 | this.scrollToFocusedInput( 440 | reactNode, 441 | extraHeight + this.props.extraScrollHeight, 442 | keyboardOpeningTime !== undefined 443 | ? keyboardOpeningTime 444 | : this.props.keyboardOpeningTime || 0 445 | ) 446 | } 447 | 448 | _handleOnScroll = ( 449 | e: SyntheticEvent<*> & { nativeEvent: { contentOffset: number } } 450 | ) => { 451 | this.position = e.nativeEvent.contentOffset 452 | } 453 | 454 | _handleRef = (ref: React.Component<*>) => { 455 | this._rnkasv_keyboardView = ref ? hocOptions.extractNativeRef(ref) : ref 456 | if (this.props.innerRef) { 457 | this.props.innerRef(this._rnkasv_keyboardView) 458 | } 459 | } 460 | 461 | update = () => { 462 | const currentlyFocusedField = findNodeHandle(TextInput.State.currentlyFocusedInput()) 463 | const responder = this.getScrollResponder() 464 | 465 | if (!currentlyFocusedField || !responder) { 466 | return 467 | } 468 | 469 | this._scrollToFocusedInputWithNodeHandle(currentlyFocusedField) 470 | } 471 | 472 | render() { 473 | const { enableOnAndroid, contentContainerStyle, onScroll } = this.props 474 | let newContentContainerStyle 475 | if (Platform.OS === 'android' && enableOnAndroid) { 476 | newContentContainerStyle = [].concat(contentContainerStyle).concat({ 477 | paddingBottom: 478 | ((contentContainerStyle || {}).paddingBottom || 0) + 479 | this.state.keyboardSpace 480 | }) 481 | } 482 | const refProps = { [hocOptions.refPropName]: this._handleRef } 483 | return ( 484 | 507 | ) 508 | } 509 | } 510 | } 511 | 512 | // Allow to pass options, without breaking change, and curried for composition 513 | // listenToKeyboardEvents(ScrollView); 514 | // listenToKeyboardEvents(options)(Comp); 515 | const listenToKeyboardEvents = (configOrComp: any) => { 516 | if (typeof configOrComp === 'object' && !configOrComp.displayName) { 517 | return (Comp: Function) => KeyboardAwareHOC(Comp, configOrComp) 518 | } else { 519 | return KeyboardAwareHOC(configOrComp) 520 | } 521 | } 522 | 523 | export default listenToKeyboardEvents 524 | --------------------------------------------------------------------------------