Skip to content

Commit

Permalink
RN: Switch KeyboardAvoidingView to Class Syntax
Browse files Browse the repository at this point in the history
Summary:
Refactors `KeyboardAvoidingView` by using class syntax and fixing all Flow errors.

Note that there's still a bunch of sketchy stuff going on in this component with mutated instance variables (that are used in `render`!) and unsafe lifecycle methods. But at least now it's a little bit less painful on the eyes.

Reviewed By: TheSavior

Differential Revision: D7987443

fbshipit-source-id: f5c27a9dd383c430d9a5a9dc0b6e10e2c4fe8dd9
  • Loading branch information
yungsters authored and facebook-github-bot committed May 15, 2018
1 parent 858f7f8 commit c017dcb
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 122 deletions.
23 changes: 13 additions & 10 deletions Libraries/Components/Keyboard/Keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,19 @@ type KeyboardEventName =
| 'keyboardWillChangeFrame'
| 'keyboardDidChangeFrame';

export type KeyboardEvent = {|
+duration?: number,
+easing?: string,
+endCoordinates: {|
+width: number,
+height: number,
+screenX: number,
+screenY: number,
|},
|};
type ScreenRect = $ReadOnly<{|
screenX: number,
screenY: number,
width: number,
height: number,
|}>;

export type KeyboardEvent = $ReadOnly<{|
duration?: number,
easing?: string,
endCoordinates: ScreenRect,
startCoordinates?: ScreenRect,
|}>;

type KeyboardEventListener = (e: KeyboardEvent) => void;

Expand Down
199 changes: 87 additions & 112 deletions Libraries/Components/Keyboard/KeyboardAvoidingView.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,97 +10,69 @@

'use strict';

const createReactClass = require('create-react-class');
const Keyboard = require('Keyboard');
const LayoutAnimation = require('LayoutAnimation');
const Platform = require('Platform');
const PropTypes = require('prop-types');
const React = require('React');
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error
* found when Flow v0.54 was deployed. To see the error delete this comment and
* run Flow. */
const TimerMixin = require('react-timer-mixin');
const StyleSheet = require('StyleSheet');
const View = require('View');
const ViewPropTypes = require('ViewPropTypes');

import type EmitterSubscription from 'EmitterSubscription';
import type {ViewStyleProp} from 'StyleSheet';
import type {ViewProps, ViewLayout, ViewLayoutEvent} from 'ViewPropTypes';

type ScreenRect = {
screenX: number,
screenY: number,
width: number,
height: number,
};
type KeyboardChangeEvent = {
startCoordinates?: ScreenRect,
endCoordinates: ScreenRect,
duration?: number,
easing?: string,
};
import type {KeyboardEvent} from 'Keyboard';

type Props = $ReadOnly<{|
...ViewProps,

/**
* Specify how to react to the presence of the keyboard.
*/
behavior?: ?('height' | 'position' | 'padding'),

/**
* Style of the content container when `behavior` is 'position'.
*/
contentContainerStyle?: ?ViewStyleProp,
enabled?: ?boolean,
keyboardVerticalOffset?: ?number,

/**
* Controls whether this `KeyboardAvoidingView` instance should take effect.
* This is useful when more than one is on the screen. Defaults to true.
*/
enabled: ?boolean,

/**
* Distance between the top of the user screen and the React Native view. This
* may be non-zero in some cases. Defaults to 0.
*/
keyboardVerticalOffset: number,
|}>;

type State = {|
bottom: number,
|};

const viewRef = 'VIEW';

/**
* This is a component to solve the common problem of views that need to move out of the way of the virtual keyboard.
* It can automatically adjust either its height, position or bottom padding based on the position of the keyboard.
* View that moves out of the way when the keyboard appears by automatically
* adjusting its height, position, or bottom padding.
*/
const KeyboardAvoidingView = ((createReactClass({
displayName: 'KeyboardAvoidingView',
mixins: [TimerMixin],

propTypes: {
...ViewPropTypes,
/**
* Specify how the `KeyboardAvoidingView` will react to the presence of
* the keyboard. It can adjust the height, position or bottom padding of the view
*/
behavior: PropTypes.oneOf(['height', 'position', 'padding']),

/**
* The style of the content container(View) when behavior is 'position'.
*/
contentContainerStyle: ViewPropTypes.style,

/**
* This is the distance between the top of the user screen and the react native view,
* may be non-zero in some use cases. The default value is 0.
*/
keyboardVerticalOffset: PropTypes.number.isRequired,
/**
* This is to allow us to manually control which KAV shuld take effect when
* having more than one KAV at the same screen
*/
enabled: PropTypes.bool.isRequired,
},

getDefaultProps() {
return {
enabled: true,
keyboardVerticalOffset: 0,
};
},

getInitialState() {
return {
bottom: 0,
};
},

subscriptions: ([]: Array<EmitterSubscription>),
frame: (null: ?ViewLayout),

_relativeKeyboardHeight(keyboardFrame: ScreenRect): number {
const frame = this.frame;
class KeyboardAvoidingView extends React.Component<Props, State> {
static defaultProps = {
enabled: true,
keyboardVerticalOffset: 0,
};

_frame: ?ViewLayout = null;
_subscriptions: Array<EmitterSubscription> = [];

state = {
bottom: 0,
};

_relativeKeyboardHeight(keyboardFrame): number {
const frame = this._frame;
if (!frame || !keyboardFrame) {
return 0;
}
Expand All @@ -110,10 +82,10 @@ const KeyboardAvoidingView = ((createReactClass({
// Calculate the displacement needed for the view such that it
// no longer overlaps with the keyboard
return Math.max(frame.y + frame.height - keyboardY, 0);
},
}

_onKeyboardChange(event: ?KeyboardChangeEvent) {
if (!event) {
_onKeyboardChange = (event: ?KeyboardEvent) => {
if (event == null) {
this.setState({bottom: 0});
return;
}
Expand All @@ -135,17 +107,13 @@ const KeyboardAvoidingView = ((createReactClass({
});
}
this.setState({bottom: height});
},
};

_onLayout(event: ViewLayoutEvent) {
this.frame = event.nativeEvent.layout;
},
_onLayout = (event: ViewLayoutEvent) => {
this._frame = event.nativeEvent.layout;
};

UNSAFE_componentWillUpdate(
nextProps: Object,
nextState: Object,
nextContext?: Object,
): void {
UNSAFE_componentWillUpdate(nextProps: Props, nextState: State): void {
if (
nextState.bottom === this.state.bottom &&
this.props.behavior === 'height' &&
Expand All @@ -155,74 +123,82 @@ const KeyboardAvoidingView = ((createReactClass({
// triggered by parent component re-rendering, no need for bottom to change.
nextState.bottom = 0;
}
},
}

UNSAFE_componentWillMount() {
componentDidMount(): void {
if (Platform.OS === 'ios') {
this.subscriptions = [
this._subscriptions = [
Keyboard.addListener('keyboardWillChangeFrame', this._onKeyboardChange),
];
} else {
this.subscriptions = [
this._subscriptions = [
Keyboard.addListener('keyboardDidHide', this._onKeyboardChange),
Keyboard.addListener('keyboardDidShow', this._onKeyboardChange),
];
}
},

componentWillUnmount() {
this.subscriptions.forEach(sub => sub.remove());
},

render(): React.Element<any> {
// $FlowFixMe(>=0.41.0)
const {behavior, children, style, ...props} = this.props;
const bottomHeight = this.props.enabled ? this.state.bottom : 0;
}

componentWillUnmount(): void {
this._subscriptions.forEach(subscription => {
subscription.remove();
});
}

render(): React.Node {
const {
behavior,
children,
contentContainerStyle,
enabled,
keyboardVerticalOffset, // eslint-disable-line no-unused-vars
style,
...props
} = this.props;
const bottomHeight = enabled ? this.state.bottom : 0;
switch (behavior) {
case 'height':
let heightStyle;
if (this.frame) {
if (this._frame != null) {
// 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 - bottomHeight, flex: 0};
heightStyle = {
height: this._frame.height - bottomHeight,
flex: 0,
};
}
return (
// $FlowFixMe - Typing ReactNativeComponent revealed errors
<View
ref={viewRef}
style={[style, heightStyle]}
style={StyleSheet.compose(style, heightStyle)}
onLayout={this._onLayout}
{...props}>
{children}
</View>
);

case 'position':
const positionStyle = {bottom: bottomHeight};
const {contentContainerStyle} = this.props;

return (
// $FlowFixMe - Typing ReactNativeComponent revealed errors
<View
ref={viewRef}
style={style}
onLayout={this._onLayout}
{...props}>
<View style={[contentContainerStyle, positionStyle]}>
<View
style={StyleSheet.compose(contentContainerStyle, {
bottom: bottomHeight,
})}>
{children}
</View>
</View>
);

case 'padding':
const paddingStyle = {paddingBottom: bottomHeight};
return (
// $FlowFixMe - Typing ReactNativeComponent revealed errors
<View
ref={viewRef}
style={[style, paddingStyle]}
style={StyleSheet.compose(style, {paddingBottom: bottomHeight})}
onLayout={this._onLayout}
{...props}>
{children}
Expand All @@ -231,7 +207,6 @@ const KeyboardAvoidingView = ((createReactClass({

default:
return (
// $FlowFixMe - Typing ReactNativeComponent revealed errors
<View
ref={viewRef}
onLayout={this._onLayout}
Expand All @@ -241,7 +216,7 @@ const KeyboardAvoidingView = ((createReactClass({
</View>
);
}
},
}): any): React.ComponentType<Props>);
}
}

module.exports = KeyboardAvoidingView;

0 comments on commit c017dcb

Please sign in to comment.