diff --git a/Examples/UIExplorer/KeyboardAvoidingViewExample.js b/Examples/UIExplorer/KeyboardAvoidingViewExample.js new file mode 100644 index 00000000000000..e23a984f19c371 --- /dev/null +++ b/Examples/UIExplorer/KeyboardAvoidingViewExample.js @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule KeyboardAvoidingViewExample + */ +'use strict'; + +const React = require('React'); +const ReactNative = require('react-native'); +const { + KeyboardAvoidingView, + Modal, + SegmentedControlIOS, + StyleSheet, + Text, + TextInput, + TouchableHighlight, + View, +} = ReactNative; + +const UIExplorerBlock = require('./UIExplorerBlock'); +const UIExplorerPage = require('./UIExplorerPage'); + +const KeyboardAvoidingViewExample = React.createClass({ + statics: { + title: '', + description: 'Base component for views that automatically adjust their height or position to move out of the way of the keyboard.', + }, + + getInitialState() { + return { + behavior: 'padding', + modalOpen: false, + }; + }, + + onSegmentChange(segment: String) { + this.setState({behavior: segment.toLowerCase()}); + }, + + renderExample() { + return ( + + + + + + + this.setState({modalOpen: false})} + style={styles.closeButton}> + Close + + + + this.setState({modalOpen: true})}> + Open Example + + + ); + }, + + render() { + return ( + + + {this.renderExample()} + + + ); + }, +}); + +const styles = StyleSheet.create({ + outerContainer: { + flex: 1, + }, + container: { + flex: 1, + justifyContent: 'center', + paddingHorizontal: 20, + paddingTop: 20, + }, + textInput: { + borderRadius: 5, + borderWidth: 1, + height: 44, + paddingHorizontal: 10, + }, + segment: { + marginBottom: 10, + }, + closeButton: { + position: 'absolute', + top: 30, + left: 10, + } +}); + +module.exports = KeyboardAvoidingViewExample; diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index 25279bdf6f157d..c4c7aecc95d7bc 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -1,4 +1,11 @@ /** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index 5aefa3288d2b1e..3ab425b24f6b1b 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -40,6 +40,10 @@ const ComponentExamples: Array = [ key: 'ImageExample', module: require('./ImageExample'), }, + { + key: 'KeyboardAvoidingViewExample', + module: require('./KeyboardAvoidingViewExample'), + }, { key: 'LayoutEventsExample', module: require('./LayoutEventsExample'), diff --git a/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/Libraries/Components/Keyboard/KeyboardAvoidingView.js new file mode 100644 index 00000000000000..c5c1d9ba0b9b34 --- /dev/null +++ b/Libraries/Components/Keyboard/KeyboardAvoidingView.js @@ -0,0 +1,189 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule KeyboardAvoidingView + * @flow + */ +'use strict'; + +const Keyboard = require('Keyboard'); +const LayoutAnimation = require('LayoutAnimation'); +const Platform = require('Platform'); +const PropTypes = require('ReactPropTypes'); +const React = require('React'); +const TimerMixin = require('react-timer-mixin'); +const View = require('View'); + +import type EmitterSubscription from 'EmitterSubscription'; + +type Rect = { + x: number; + y: number; + width: number; + height: number; +}; +type ScreenRect = { + screenX: number; + screenY: number; + width: number; + height: number; +}; +type KeyboardChangeEvent = { + startCoordinates?: ScreenRect; + endCoordinates: ScreenRect; + duration?: number; + easing?: string; +}; +type LayoutEvent = { + nativeEvent: { + layout: Rect; + } +}; + +const viewRef = 'VIEW'; + +const KeyboardAvoidingView = React.createClass({ + mixins: [TimerMixin], + + propTypes: { + ...View.propTypes, + behavior: PropTypes.oneOf(['height', 'position', 'padding']), + + /** + * This is the distance between the top of the user screen and the react native view, + * may be non-zero in some use cases. + */ + keyboardVerticalOffset: PropTypes.number.isRequired, + }, + + getDefaultProps() { + return { + keyboardVerticalOffset: 0, + }; + }, + + getInitialState() { + return { + bottom: 0, + }; + }, + + subscriptions: ([]: Array), + frame: (null: ?Rect), + + relativeKeyboardHeight(keyboardFrame: ScreenRect): number { + const frame = this.frame; + if (!frame) { + return 0; + } + + const y1 = Math.max(frame.y, keyboardFrame.screenY - this.props.keyboardVerticalOffset); + const y2 = Math.min(frame.y + frame.height, keyboardFrame.screenY + keyboardFrame.height - this.props.keyboardVerticalOffset); + return Math.max(y2 - y1, 0); + }, + + onKeyboardChange(event: ?KeyboardChangeEvent) { + if (!event) { + this.setState({bottom: 0}); + return; + } + + const {duration, easing, endCoordinates} = event; + const height = this.relativeKeyboardHeight(endCoordinates); + + if (duration && easing) { + LayoutAnimation.configureNext({ + duration: duration, + update: { + duration: duration, + type: LayoutAnimation.Types[easing] || 'keyboard', + }, + }); + } + this.setState({bottom: height}); + }, + + onLayout(event: LayoutEvent) { + this.frame = event.nativeEvent.layout; + }, + + componentWillUpdate(nextProps: Object, nextState: Object, nextContext?: Object): void { + if (nextState.bottom === this.state.bottom && + this.props.behavior === 'height' && + nextProps.behavior === 'height') { + // If the component rerenders without an internal state change, e.g. + // triggered by parent component re-rendering, no need for bottom to change. + nextState.bottom = 0; + } + }, + + componentWillMount() { + if (Platform.OS === 'ios') { + this.subscriptions = [ + Keyboard.addListener('keyboardWillChangeFrame', this.onKeyboardChange), + ]; + } else { + this.subscriptions = [ + Keyboard.addListener('keyboardDidHide', this.onKeyboardChange), + Keyboard.addListener('keyboardDidShow', this.onKeyboardChange), + ]; + } + }, + + componentWillUnmount() { + this.subscriptions.forEach((sub) => sub.remove()); + }, + + render(): ReactElement { + const {behavior, children, style, ...props} = this.props; + + switch (behavior) { + case 'height': + let heightStyle; + if (this.frame) { + // Note that we only apply a height change when there is keyboard present, + // i.e. this.state.bottom is greater than 0. If we remove that condition, + // this.frame.height will never go back to its original value. + // When height changes, we need to disable flex. + heightStyle = {height: this.frame.height - this.state.bottom, flex: 0}; + } + return ( + + {children} + + ); + + case 'position': + const positionStyle = {bottom: this.state.bottom}; + return ( + + + {children} + + + ); + + case 'padding': + const paddingStyle = {paddingBottom: this.state.bottom}; + return ( + + {children} + + ); + + default: + return ( + + {children} + + ); + } + }, +}); + +module.exports = KeyboardAvoidingView; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 713becd5dfaabf..c9129314cdc365 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -35,6 +35,7 @@ const ReactNative = { get Image() { return require('Image'); }, get ImageEditor() { return require('ImageEditor'); }, get ImageStore() { return require('ImageStore'); }, + get KeyboardAvoidingView() { return require('KeyboardAvoidingView'); }, get ListView() { return require('ListView'); }, get MapView() { return require('MapView'); }, get Modal() { return require('Modal'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 191d3e22284171..b63a57cb5e2878 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -33,6 +33,7 @@ var ReactNative = Object.assign(Object.create(require('ReactNative')), { Image: require('Image'), ImageEditor: require('ImageEditor'), ImageStore: require('ImageStore'), + KeyboardAvoidingView: require('KeyboardAvoidingView'), ListView: require('ListView'), MapView: require('MapView'), Modal: require('Modal'),