react-native-ui-lib
Version:
[](https://travis-ci.org/wix/react-native-ui-lib) [](https://www.npmjs.com/package/react-native-ui-lib) [![NPM Down
445 lines (444 loc) • 17 kB
JavaScript
import React from "react";
import { TextInput as RNTextInput, StyleSheet, Animated } from "react-native";
import _ from "lodash";
import BaseInput from "./BaseInput";
import Text from "../text";
import { Colors, Typography } from "../../style";
import { Constants } from "../../helpers";
import { Modal } from "../../screensComponents";
import TextArea from "./TextArea";
import View from "../view";
const DEFAULT_COLOR_BY_STATE = {
default: Colors.dark40,
focus: Colors.blue30,
error: Colors.red30
};
const DEFAULT_UNDERLINE_COLOR_BY_STATE = {
default: Colors.dark70,
focus: Colors.blue30,
error: Colors.red30
};
/**
* @description: A wrapper for Text Input component with extra functionality like floating placeholder
* @extends: TextInput
* @extendslink: https://facebook.github.io/react-native/docs/textinput.html
* @modifiers: Typography
* @gif: https://media.giphy.com/media/xULW8su8Cs5Z9Fq4PS/giphy.gif, https://media.giphy.com/media/3ohc1dhDcLS9FvWLJu/giphy.gif, https://media.giphy.com/media/oNUSOxnHdMP5ZnKYsh/giphy.gif
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/InputsScreen.js
*/
export default class TextInput extends BaseInput {
constructor(props) {
super(props);
this.onChangeText = this.onChangeText.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onDoneEditingExpandableInput = this.onDoneEditingExpandableInput.bind(this);
this.updateFloatingPlaceholderState = this.updateFloatingPlaceholderState.bind(this);
this.toggleExpandableModal = this.toggleExpandableModal.bind(this);
this.shouldShowHelperText = this.shouldShowHelperText.bind(this);
this.state = {
value: props.value,
floatingPlaceholderState: new Animated.Value(this.hasText(props.value) || this.shouldShowHelperText() ? 1 : 0),
showExpandableModal: false
};
this.generatePropsWarnings(props);
}
componentWillReceiveProps(nextProps) {
if (nextProps.value !== this.props.value) {
this.setState({
value: nextProps.value
}, this.updateFloatingPlaceholderState);
}
}
componentDidMount() {
this.getHeight();
}
generatePropsWarnings(props) {
if (props.maxLength === 0) {
console.warn("Setting maxLength to zero will block typing in this input");
}
if (props.showCharacterCounter && !props.maxLength) {
console.warn("In order to use showCharacterCount please pass 'maxLength' prop");
}
}
generateStyles() {
this.styles = createStyles(this.props);
}
getStateColor(colorProp, isUnderline) {
const { focused } = this.state;
const { error } = this.props;
const colorByState = _.cloneDeep(isUnderline ? DEFAULT_UNDERLINE_COLOR_BY_STATE : DEFAULT_COLOR_BY_STATE);
if (colorProp) {
if (_.isString(colorProp)) {
// use given color for any state
return colorProp;
}
else if (_.isObject(colorProp)) {
// set given colors by states
_.merge(colorByState, colorProp);
}
}
// return the right color for the current state
let color = colorByState.default;
if (error && isUnderline) {
color = colorByState.error;
}
else if (focused) {
color = colorByState.focus;
}
return color;
}
getCharCount() {
const { value } = this.state;
return _.size(value);
}
isCounterLimit() {
const { maxLength } = this.props;
const counter = this.getCharCount();
return counter === 0 ? false : maxLength === counter;
}
hasText(value) {
return !_.isEmpty(value || this.state.value);
}
shouldShowHelperText() {
const { focused } = this.state;
const { helperText } = this.props;
return focused && helperText;
}
shouldFakePlaceholder() {
const { floatingPlaceholder, centered } = this.props;
return Boolean(floatingPlaceholder && !centered);
}
getHeight() {
const { multiline } = this.props;
if (!multiline) {
const typography = this.getTypography();
return typography.lineHeight;
}
return this.getLinesHeightLimit();
}
// numberOfLines support for both platforms
getLinesHeightLimit() {
const { multiline, numberOfLines } = this.props;
if (multiline && numberOfLines) {
const typography = this.getTypography();
return typography.lineHeight * numberOfLines;
}
}
renderPlaceholder() {
const { floatingPlaceholderState } = this.state;
const { centered, expandable, placeholder, placeholderTextColor, floatingPlaceholderColor, multiline } = this.props;
const typography = this.getTypography();
const floatingTypography = Typography.text90;
if (this.shouldFakePlaceholder()) {
return (<Animated.Text style={[
this.styles.floatingPlaceholder,
this.styles.placeholder,
typography,
centered && this.styles.placeholderCentered,
!centered && {
top: floatingPlaceholderState.interpolate({
inputRange: [0, 1],
outputRange: [multiline ? 30 : 28, multiline ? 7 : 0]
}),
fontSize: floatingPlaceholderState.interpolate({
inputRange: [0, 1],
outputRange: [typography.fontSize, floatingTypography.fontSize]
}),
color: floatingPlaceholderState.interpolate({
inputRange: [0, 1],
outputRange: [
placeholderTextColor,
this.getStateColor(floatingPlaceholderColor)
]
}),
lineHeight: this.hasText() || this.shouldShowHelperText()
? floatingTypography.lineHeight
: typography.lineHeight
}
]} onPress={() => expandable && this.toggleExpandableModal(true)}>
{placeholder}
</Animated.Text>);
}
}
renderTitle() {
const { floatingPlaceholder, title, titleColor } = this.props;
const color = this.getStateColor(titleColor);
if (!floatingPlaceholder && title) {
return <Text style={[{ color }, this.styles.title]}>{title}</Text>;
}
}
renderCharCounter() {
const { focused } = this.state;
const { maxLength, showCharacterCounter } = this.props;
if (maxLength && showCharacterCounter) {
const counter = this.getCharCount();
const color = this.isCounterLimit() && focused
? DEFAULT_COLOR_BY_STATE.error
: DEFAULT_COLOR_BY_STATE.default;
return (<Text style={[{ color }, this.styles.charCounter]}>
{counter} / {maxLength}
</Text>);
}
}
renderError() {
const { enableErrors, error } = this.props;
if (enableErrors) {
return <Text style={this.styles.errorMessage}>{error}</Text>;
}
}
renderExpandableModal() {
const { renderExpandable } = this.props;
const { showExpandableModal } = this.state;
if (_.isFunction(renderExpandable) && showExpandableModal) {
return renderExpandable(this.props, this.state);
}
return (<Modal animationType={"slide"} visible={showExpandableModal} onRequestClose={() => this.toggleExpandableModal(false)}>
<Modal.TopBar onCancel={() => this.toggleExpandableModal(false)} onDone={this.onDoneEditingExpandableInput}/>
<View style={this.styles.expandableModalContent}>
<TextArea ref={textarea => {
this.expandableInput = textarea;
}} {...this.props} value={this.state.value}/>
</View>
</Modal>);
}
renderExpandableInput() {
const { style, floatingPlaceholder, placeholder } = this.props;
const { value } = this.state;
const typography = this.getTypography();
const color = this.props.color || this.extractColorValue();
const minHeight = typography.lineHeight;
const shouldShowPlaceholder = _.isEmpty(value) && !floatingPlaceholder;
const inputStyle = [
this.styles.input,
typography,
color && { color },
style
];
return (<Text style={[
{ minHeight },
inputStyle,
shouldShowPlaceholder && this.styles.placeholder
]} numberOfLines={3} onPress={() => this.toggleExpandableModal(true)}>
{shouldShowPlaceholder ? placeholder : value}
</Text>);
}
renderTextInput() {
const { value } = this.state;
const color = this.props.color || this.extractColorValue();
const typography = this.getTypography();
const { style, placeholder, floatingPlaceholder, centered, multiline, numberOfLines, helperText, ...others } = this.props;
const inputStyle = [
this.styles.input,
typography,
color && { color },
// with the right flex on the tree hierarchy we might not need this
// {height: this.getHeight()},
style
];
const placeholderText = this.shouldFakePlaceholder()
? this.shouldShowHelperText()
? helperText
: undefined
: placeholder;
return (<RNTextInput {...others} value={value} placeholder={placeholderText} underlineColorAndroid="transparent" style={inputStyle} multiline={multiline} numberOfLines={numberOfLines} onChangeText={this.onChangeText} onChange={this.onChange} onFocus={this.onFocus} onBlur={this.onBlur} ref={input => {
this.input = input;
}}/>);
}
render() {
const { expandable, containerStyle, underlineColor } = this.props;
const underlineStateColor = this.getStateColor(underlineColor, true);
return (<View style={[this.styles.container, containerStyle]} collapsable={false}>
{this.renderTitle()}
<View style={[
this.styles.innerContainer,
{ borderColor: underlineStateColor }
]}>
{this.renderPlaceholder()}
{expandable ? this.renderExpandableInput() : this.renderTextInput()}
{this.renderExpandableModal()}
</View>
<View row>
<View flex>{this.renderError()}</View>
{this.renderCharCounter()}
</View>
</View>);
}
toggleExpandableModal(value) {
this.setState({ showExpandableModal: value });
}
updateFloatingPlaceholderState(withoutAnimation) {
if (withoutAnimation) {
this.state.floatingPlaceholderState.setValue(this.hasText() || this.shouldShowHelperText() ? 1 : 0);
}
else {
Animated.spring(this.state.floatingPlaceholderState, {
toValue: this.hasText() || this.shouldShowHelperText() ? 1 : 0,
duration: 150
}).start();
}
}
onDoneEditingExpandableInput() {
const expandableInputValue = _.get(this.expandableInput, "state.value");
this.setState({
value: expandableInputValue
});
this.state.floatingPlaceholderState.setValue(expandableInputValue ? 1 : 0);
_.invoke(this.props, "onChangeText", expandableInputValue);
this.toggleExpandableModal(false);
}
onChangeText(text) {
const { transformer } = this.props;
let transformedText = text;
if (_.isFunction(transformer)) {
transformedText = transformer(text);
}
_.invoke(this.props, "onChangeText", transformedText);
this.setState({
value: transformedText
}, this.updateFloatingPlaceholderState);
}
onFocus(...args) {
_.invoke(this.props, "onFocus", ...args);
this.setState({ focused: true }, this.updateFloatingPlaceholderState);
}
onBlur(...args) {
_.invoke(this.props, "onBlur", ...args);
this.setState({ focused: false }, this.updateFloatingPlaceholderState);
}
}
TextInput.displayName = "TextInput";
TextInput.propTypes = {
...RNTextInput.propTypes,
...BaseInput.propTypes,
/**
* should placeholder have floating behavior
*/
floatingPlaceholder: PropTypes.bool,
/**
* floating placeholder color as a string or object of states, ex. {default: 'black', error: 'red', focus: 'blue'}
*/
floatingPlaceholderColor: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
]),
/**
* This text will appear as a placeholder when the textInput becomes focused, only when passing floatingPlaceholder
* as well (NOT for expandable textInputs)
*/
helperText: PropTypes.string,
/**
* hide text input underline, by default false
*/
hideUnderline: PropTypes.bool,
/**
* underline color as a string or object of states, ex. {default: 'black', error: 'red', focus: 'blue'}
*/
underlineColor: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
/**
* should text input be align to center
*/
centered: PropTypes.bool,
/**
* input error message, should be empty if no error exists
*/
error: PropTypes.string,
/**
* should the input component support error messages
*/
enableErrors: PropTypes.bool,
/**
* should the input expand to another text area modal
*/
expandable: PropTypes.bool,
/**
* allow custom rendering of expandable content when clicking on the input (useful for pickers)
* accept props and state as params, ex. (props, state) => {...}
* use toggleExpandableModal(false) method to toggle off the expandable content
*/
renderExpandable: PropTypes.func,
/**
* transform function executed on value and return transformed value
*/
transformer: PropTypes.func,
/**
* Fixed title that will displayed above the input (note: floatingPlaceholder MUST be 'false')
*/
title: PropTypes.string,
/**
* The title's color as a string or object of states, ex. {default: 'black', error: 'red', focus: 'blue'}
*/
titleColor: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
/**
* should the input display a character counter (only when passing 'maxLength')
*/
showCharacterCounter: PropTypes.bool,
/**
* Use to identify the component in tests
*/
testId: PropTypes.string
};
TextInput.defaultProps = {
placeholderTextColor: DEFAULT_COLOR_BY_STATE.default,
enableErrors: true
};
function createStyles({ placeholderTextColor, hideUnderline, centered, floatingPlaceholder }) {
return StyleSheet.create({
container: {},
innerContainer: {
flexDirection: "row",
borderBottomWidth: hideUnderline ? 0 : 1,
borderColor: Colors.dark70,
justifyContent: centered ? "center" : undefined,
paddingTop: floatingPlaceholder ? 25 : undefined,
flexGrow: 1
},
focusedUnderline: {
borderColor: Colors.blue30
},
errorUnderline: {
borderColor: Colors.red30
},
input: {
flexGrow: 1,
marginBottom: hideUnderline ? undefined : Constants.isIOS ? 10 : 5,
padding: 0,
textAlign: centered ? "center" : undefined,
backgroundColor: "transparent"
},
floatingPlaceholder: {
position: "absolute"
},
placeholder: {
color: placeholderTextColor
},
placeholderCentered: {
left: 0,
right: 0,
textAlign: "center"
},
errorMessage: {
color: Colors.red30,
textAlign: centered ? "center" : undefined,
...Typography.text90,
// height: Typography.text90.lineHeight,
marginTop: 1
},
expandableModalContent: {
flex: 1,
paddingTop: 15,
paddingHorizontal: 20
},
title: {
top: 0,
...Typography.text90,
height: Typography.text90.lineHeight,
marginBottom: Constants.isIOS ? 5 : 4
},
charCounter: {
...Typography.text90,
height: Typography.text90.lineHeight,
marginTop: 1
}
});
}