UNPKG

react-native-ui-lib

Version:

[![Build Status](https://travis-ci.org/wix/react-native-ui-lib.svg?branch=master)](https://travis-ci.org/wix/react-native-ui-lib) [![npm](https://img.shields.io/npm/v/react-native-ui-lib.svg)](https://www.npmjs.com/package/react-native-ui-lib) [![NPM Down

654 lines (594 loc) • 19.8 kB
import React from 'react'; import PropTypes from 'prop-types'; import {TextInput as RNTextInput, StyleSheet, Animated, TouchableOpacity} 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'; import Image from '../image'; 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, }; const LABEL_TYPOGRAPHY = Typography.text80; /** * @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 TextField extends BaseInput { static displayName = 'TextField'; static 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]), /** * the color of all text when the input is disabled (if undefined will not apply color) */ disabledColor: PropTypes.string, /** * 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, /** * Render custom expandable input (requires expandable to be true) */ renderExpandableInput: PropTypes.func, /** * 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, /** * The picker modal top bar props */ topBarProps: PropTypes.shape(Modal.TopBar.propTypes), /** * 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]), /** * Additional styles for the title (not including 'color') */ titleStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), /** * should the input display a character counter (only when passing 'maxLength') */ showCharacterCounter: PropTypes.bool, /** * should float the placeholer when focused (instead of when typing) */ floatOnFocus: PropTypes.bool, /** * should the errors be displayed at the top */ useTopErrors: PropTypes.bool, /** * Icon asset source for showing on the right side, appropriate for dropdown icon and such */ rightIconSource: PropTypes.oneOfType([PropTypes.object, PropTypes.number]), }; static defaultProps = { placeholderTextColor: DEFAULT_COLOR_BY_STATE.default, enableErrors: true, }; constructor(props) { super(props); this.updateFloatingPlaceholderState = this.updateFloatingPlaceholderState.bind(this); this.toggleExpandableModal = this.toggleExpandableModal.bind(this); this.state = { value: props.value, floatingPlaceholderState: new Animated.Value(this.shouldFloatPlacholder(props.value) ? 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(); } /** Actions */ 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.getThemeProps()); } toggleExpandableModal(value) { this.setState({showExpandableModal: value}); } updateFloatingPlaceholderState(withoutAnimation) { if (withoutAnimation) { this.state.floatingPlaceholderState.setValue(this.shouldFloatPlacholder() ? 1 : 0); } else { Animated.spring(this.state.floatingPlaceholderState, { toValue: this.shouldFloatPlacholder() ? 1 : 0, duration: 150, }).start(); } } getPlaceholderText() { const {placeholder, helperText} = this.props; const text = this.shouldFakePlaceholder() ? (this.shouldShowHelperText() ? helperText : ' ') : (this.shouldShowTopError() && this.shouldShowHelperText() ? helperText : placeholder); return text; } // getHeight() { // const {multiline, numberOfLines} = this.props; // const typography = this.getTypography(); // if (!multiline) { // return typography.lineHeight; // } // // numberOfLines support for both platforms // if (multiline && numberOfLines) { // return typography.lineHeight * numberOfLines; // } // } isDisabled() { return this.props.editable === false; } getStateColor(colorProp, isUnderline) { const {focused} = this.state; const {error, disabledColor} = this.getThemeProps(); const colorByState = _.cloneDeep(isUnderline ? DEFAULT_UNDERLINE_COLOR_BY_STATE : DEFAULT_COLOR_BY_STATE); if (this.isDisabled() && disabledColor) { return disabledColor; } 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; if (value) { return value.length; } return 0; } isCounterLimit() { const {maxLength} = this.getThemeProps(); 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; } shouldFloatOnFocus() { const {focused} = this.state; const {floatOnFocus} = this.getThemeProps(); return focused && floatOnFocus; } shouldFloatPlacholder(text) { return this.hasText(text) || this.shouldShowHelperText() || this.shouldFloatOnFocus(); } shouldFakePlaceholder() { const {floatingPlaceholder, centered} = this.getThemeProps(); return Boolean(floatingPlaceholder && !centered && !this.shouldShowTopError()); } shouldShowError() { const {enableErrors, error} = this.getThemeProps(); return enableErrors && error; } shouldShowTopError() { const {useTopErrors} = this.getThemeProps(); return this.shouldShowError() && useTopErrors; } /** Renders */ renderPlaceholder() { const {floatingPlaceholderState} = this.state; const { centered, expandable, placeholder, placeholderTextColor, floatingPlaceholderColor, multiline, } = this.getThemeProps(); const typography = this.getTypography(); const placeholderColor = this.getStateColor(placeholderTextColor); 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, LABEL_TYPOGRAPHY.fontSize], }), color: floatingPlaceholderState.interpolate({ inputRange: [0, 1], outputRange: [placeholderColor, this.getStateColor(floatingPlaceholderColor)], }), lineHeight: this.shouldFloatPlacholder() ? LABEL_TYPOGRAPHY.lineHeight : typography.lineHeight, }, ]} numberOfLines={1} onPress={() => expandable && this.toggleExpandableModal(true)} suppressHighlighting > {placeholder} </Animated.Text> ); } } renderTitle() { const {floatingPlaceholder, title, titleColor, titleStyle} = this.getThemeProps(); const color = this.getStateColor(titleColor); if (!floatingPlaceholder && title) { return ( <Text style={[{color}, this.styles.topLabel, this.styles.label, titleStyle]} > {title} </Text> ); } } renderCharCounter() { const {focused} = this.state; const {maxLength, showCharacterCounter, disabledColor} = this.getThemeProps(); if (maxLength && showCharacterCounter) { const counter = this.getCharCount(); const textColor = this.isCounterLimit() && focused ? DEFAULT_COLOR_BY_STATE.error : DEFAULT_COLOR_BY_STATE.default; const color = (this.isDisabled() && disabledColor) ? disabledColor : textColor; return ( <Text style={[{color}, this.styles.bottomLabel, this.styles.label]} > {counter} / {maxLength} </Text> ); } } renderError(visible) { const {enableErrors, error, useTopErrors} = this.getThemeProps(); const positionStyle = useTopErrors ? this.styles.topLabel : this.styles.bottomLabel; if (visible && enableErrors) { return ( <Text style={[this.styles.errorMessage, this.styles.label, positionStyle]}> {error} </Text> ); } } renderExpandableModal() { const {renderExpandable, topBarProps} = this.getThemeProps(); const {showExpandableModal} = this.state; if (_.isFunction(renderExpandable) && showExpandableModal) { return renderExpandable(this.getThemeProps(), this.state); } return ( <Modal animationType={'slide'} visible={showExpandableModal} onRequestClose={() => this.toggleExpandableModal(false)} > <Modal.TopBar {...topBarProps} 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, hideUnderline, renderExpandableInput, rightIconSource} = this.getThemeProps(); const {value} = this.state; const typography = this.getTypography(); const color = this.getStateColor(this.props.color || this.extractColorValue()); const minHeight = typography.lineHeight; const shouldShowPlaceholder = _.isEmpty(value) && !floatingPlaceholder; const inputStyle = [ this.styles.input, hideUnderline && this.styles.inputWithoutUnderline, typography, color && {color}, style, ]; if (_.isFunction(renderExpandableInput)) { return renderExpandableInput(this.getThemeProps()); } return ( <TouchableOpacity style={this.styles.expandableInput} activeOpacity={1} onPress={() => !this.isDisabled() && this.toggleExpandableModal(true)} > <Text style={[ {minHeight}, inputStyle, shouldShowPlaceholder && this.styles.placeholder, ]} numberOfLines={3} > {shouldShowPlaceholder ? placeholder : value} </Text> {rightIconSource && <Image pointerEvents="none" source={rightIconSource} />} </TouchableOpacity> ); } renderTextInput() { const {value} = this.state; // value set on state for floatingPlaceholder functionality const color = this.getStateColor(this.props.color || this.extractColorValue()); const typography = this.getTypography(); const { style, placeholder, placeholderTextColor, floatingPlaceholder, centered, multiline, hideUnderline, numberOfLines, helperText, ...others } = this.props; const inputStyle = [ this.styles.input, hideUnderline && this.styles.inputWithoutUnderline, typography, color && {color}, // with the right flex on the tree hierarchy we might not need this // {height: this.getHeight()}, style, ]; // HACK: passing whitespace instead of undefined. Issue fixed in RN56 const placeholderText = this.getPlaceholderText(); const placeholderColor = this.getStateColor(placeholderTextColor); return ( <RNTextInput {...others} value={value} placeholder={placeholderText} placeholderTextColor={placeholderColor} underlineColorAndroid="transparent" style={inputStyle} multiline={multiline} numberOfLines={numberOfLines} onKeyPress={this.onKeyPress} onChangeText={this.onChangeText} onChange={this.onChange} onFocus={this.onFocus} onBlur={this.onBlur} ref={(input) => { this.input = input; }} /> ); } getTopPaddings() { const {floatingPlaceholder} = this.props; return floatingPlaceholder ? (this.shouldShowTopError() ? undefined : 25) : undefined; } render() { const {expandable, containerStyle, underlineColor, useTopErrors, hideUnderline} = this.getThemeProps(); const underlineStateColor = this.getStateColor(underlineColor, true); return ( <View style={[this.styles.container, containerStyle]} collapsable={false}> <View> {this.shouldShowTopError() ? this.renderError(useTopErrors) : this.renderTitle()} </View> <View style={[ this.styles.innerContainer, hideUnderline && this.styles.innerContainerWithoutUnderline, {borderColor: underlineStateColor}, {paddingTop: this.getTopPaddings()}]} > {this.renderPlaceholder()} {expandable ? this.renderExpandableInput() : this.renderTextInput()} {expandable && this.renderExpandableModal()} </View> <View row> <View flex> {this.renderError(!useTopErrors)} </View> {this.renderCharCounter()} </View> </View> ); } /** Events */ 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); } onKeyPress = (event) => { this.lastKey = event.nativeEvent.key; _.invoke(this.props, 'onKeyPress', event); } onChangeText = (text) => { // when character count exceeds maxLength text will be empty string. // HACK: To avoid setting state value to '' we check the source of that deletion if (text === '' && this.lastKey && this.lastKey !== 'Backspace') { return; } 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); } } function createStyles({ placeholderTextColor, centered, }) { return StyleSheet.create({ container: { }, innerContainer: { flexDirection: 'row', borderBottomWidth: 1, borderColor: Colors.dark70, justifyContent: centered ? 'center' : undefined, flexGrow: 1, }, innerContainerWithoutUnderline: { borderBottomWidth: 0, }, input: { flexGrow: 1, marginBottom: Constants.isIOS ? 10 : 5, padding: 0, textAlign: centered ? 'center' : undefined, backgroundColor: 'transparent', }, expandableInput: { flexDirection: 'row', alignItems: 'center', flexGrow: 1, }, inputWithoutUnderline: { marginBottom: undefined, }, floatingPlaceholder: { position: 'absolute', width: '100%', backgroundColor: 'transparent', }, placeholder: { color: placeholderTextColor, }, placeholderCentered: { left: 0, right: 0, textAlign: 'center', }, errorMessage: { color: Colors.red30, textAlign: centered ? 'center' : undefined, }, expandableModalContent: { flex: 1, paddingTop: 15, paddingHorizontal: 20, }, topLabel: { marginBottom: Constants.isIOS ? 6 : 7, }, bottomLabel: { marginTop: 1, }, label: { ...LABEL_TYPOGRAPHY, height: LABEL_TYPOGRAPHY.lineHeight, }, }); }