[change] TextInput uses DOM elements

This patch changes TextInput to use DOM inputs directly, rather than
trying to reimplement 'placeholder'. Removes support for
'placeholderTextColor'.

Fix #54
Fix #224
Fix #229
Fix #235
Fix #253
This commit is contained in:
Nicolas Gallagher
2016-11-21 16:52:40 -08:00
parent 4005f9ddde
commit 89f5a13891
5 changed files with 57 additions and 181 deletions
+1 -7
View File
@@ -6,12 +6,10 @@ such as auto-complete, auto-focus, placeholder text, and event callbacks.
Note: some props are exclusive to or excluded from `multiline`. Note: some props are exclusive to or excluded from `multiline`.
Unsupported React Native props: Unsupported React Native props:
`autoCapitalize`,
`autoCorrect`,
`onEndEditing`, `onEndEditing`,
`onSubmitEditing`,
`clearButtonMode` (ios), `clearButtonMode` (ios),
`enablesReturnKeyAutomatically` (ios), `enablesReturnKeyAutomatically` (ios),
`placeholderTextColor`,
`returnKeyType` (ios), `returnKeyType` (ios),
`selectionState` (ios), `selectionState` (ios),
`underlineColorAndroid` (android) `underlineColorAndroid` (android)
@@ -128,10 +126,6 @@ Callback that is called when the keyboard's submit button is pressed.
The string that will be rendered in an empty `TextInput` before text has been The string that will be rendered in an empty `TextInput` before text has been
entered. entered.
**placeholderTextColor**: string
The text color of the placeholder string.
**secureTextEntry**: bool = false **secureTextEntry**: bool = false
If true, the text input obscures the text entered so that sensitive text like If true, the text input obscures the text entered so that sensitive text like
@@ -210,25 +210,16 @@ class TokenizedTextExample extends React.Component {
} }
parts.push(_text); parts.push(_text);
//highlight hashtags
parts = parts.map((text) => {
if (/^#/.test(text)) {
return <Text key={text} style={styles.hashtag}>{text}</Text>;
} else {
return text;
}
});
return ( return (
<View> <View>
<TextInput <TextInput
value={parts.join('')}
multiline={true} multiline={true}
style={styles.multiline} style={styles.multiline}
onChangeText={(text) => { onChangeText={(text) => {
this.setState({text}); this.setState({text});
}}> }}
<Text>{parts}</Text> />
</TextInput>
</View> </View>
); );
} }
@@ -279,7 +270,7 @@ class BlurOnSubmitExample extends React.Component {
<TextInput <TextInput
ref="5" ref="5"
style={styles.default} style={styles.default}
keyboardType="numbers-and-punctuation" keyboardType="numeric"
placeholder="blurOnSubmit = true" placeholder="blurOnSubmit = true"
returnKeyType="done" returnKeyType="done"
/> />
@@ -519,15 +510,15 @@ const examples = [
render: function() { render: function() {
var keyboardTypes = [ var keyboardTypes = [
'default', 'default',
'ascii-capable', //'ascii-capable',
'numbers-and-punctuation', //'numbers-and-punctuation',
'url', 'url',
'number-pad', 'number-pad',
'phone-pad', 'phone-pad',
'name-phone-pad', //'name-phone-pad',
'email-address', 'email-address',
'decimal-pad', //'decimal-pad',
'twitter', //'twitter',
'web-search', 'web-search',
'numeric', 'numeric',
]; ];
@@ -776,14 +767,6 @@ const examples = [
style={styles.multiline} style={styles.multiline}
dataDetectorTypes="phoneNumber" dataDetectorTypes="phoneNumber"
/> />
<TextInput
placeholder="multiline with children"
multiline={true}
enablesReturnKeyAutomatically={true}
returnKeyType="go"
style={styles.multiline}>
<View style={styles.multilineChild}/>
</TextInput>
</View> </View>
); );
} }
@@ -1,15 +1,12 @@
/* eslint-env jasmine, jest */ /* eslint-env jasmine, jest */
import React from 'react'; import React from 'react';
import StyleSheet from '../../../apis/StyleSheet';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import TextInput from '..'; import TextInput from '..';
import { mount, shallow } from 'enzyme'; import { mount, shallow } from 'enzyme';
const placeholderText = 'placeholderText';
const findNativeInput = (wrapper) => wrapper.find('input'); const findNativeInput = (wrapper) => wrapper.find('input');
const findNativeTextarea = (wrapper) => wrapper.find(TextareaAutosize); const findNativeTextarea = (wrapper) => wrapper.find(TextareaAutosize);
const findPlaceholder = (wrapper) => wrapper.find({ children: placeholderText });
const testIfDocumentIsFocused = (message, fn) => { const testIfDocumentIsFocused = (message, fn) => {
if (document.hasFocus && document.hasFocus()) { if (document.hasFocus && document.hasFocus()) {
@@ -184,24 +181,6 @@ describe('components/TextInput', () => {
} }
}); });
test('prop "placeholder"', () => {
let textInput = shallow(<TextInput />);
expect(findPlaceholder(textInput).length).toEqual(0);
textInput = shallow(<TextInput placeholder={placeholderText} />);
expect(findPlaceholder(textInput).length).toEqual(1);
});
test('prop "placeholderTextColor"', () => {
let placeholderElement = findPlaceholder(shallow(<TextInput placeholder={placeholderText} />));
expect(StyleSheet.flatten(placeholderElement.prop('style')).color).toEqual('darkgray');
placeholderElement = findPlaceholder(
shallow(<TextInput placeholder={placeholderText} placeholderTextColor='red' />)
);
expect(StyleSheet.flatten(placeholderElement.prop('style')).color).toEqual('red');
});
test('prop "secureTextEntry"', () => { test('prop "secureTextEntry"', () => {
let input = findNativeInput(shallow(<TextInput secureTextEntry />)); let input = findNativeInput(shallow(<TextInput secureTextEntry />));
expect(input.prop('type')).toEqual('password'); expect(input.prop('type')).toEqual('password');
@@ -224,20 +203,6 @@ describe('components/TextInput', () => {
// assert.equal(input.node.selectionStart, 0) // assert.equal(input.node.selectionStart, 0)
}); });
test('prop "style"', () => {
const styles = StyleSheet.create({
root: {
borderWidth: 1,
textAlign: 'center'
}
});
const textInput = shallow(<TextInput style={styles.root} />);
const input = findNativeInput(textInput);
const borderWidth = StyleSheet.flatten(textInput.prop('style')).borderWidth;
expect(borderWidth).toEqual(1);
expect(input.prop('style').textAlign).toEqual('center');
});
test('prop "value"', () => { test('prop "value"', () => {
const value = 'value'; const value = 'value';
const input = findNativeInput(shallow(<TextInput value={value} />)); const input = findNativeInput(shallow(<TextInput value={value} />));
+45 -111
View File
@@ -1,18 +1,14 @@
import applyLayout from '../../modules/applyLayout';
import applyNativeMethods from '../../modules/applyNativeMethods'; import applyNativeMethods from '../../modules/applyNativeMethods';
import createDOMElement from '../../modules/createDOMElement'; import createDOMElement from '../../modules/createDOMElement';
import findNodeHandle from '../../modules/findNodeHandle'; import findNodeHandle from '../../modules/findNodeHandle';
import omit from 'lodash/omit';
import pick from 'lodash/pick';
import StyleSheet from '../../apis/StyleSheet'; import StyleSheet from '../../apis/StyleSheet';
import Text from '../Text'; import Text from '../Text';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import TextInputState from './TextInputState'; import TextInputState from './TextInputState';
import UIManager from '../../apis/UIManager'; import UIManager from '../../apis/UIManager';
import View from '../View'; import View from '../View';
import ViewStylePropTypes from '../View/ViewStylePropTypes'; import { Component, PropTypes } from 'react';
import React, { Component, PropTypes } from 'react';
const viewStyleProps = Object.keys(ViewStylePropTypes);
/** /**
* React Native events differ from W3C events. * React Native events differ from W3C events.
@@ -25,7 +21,7 @@ const normalizeEventHandler = (handler) => (e) => {
}; };
/** /**
* Determins whether a 'selection' prop differs from a node's existing * Determines whether a 'selection' prop differs from a node's existing
* selection state. * selection state.
*/ */
const isSelectionStale = (node, selection) => { const isSelectionStale = (node, selection) => {
@@ -64,7 +60,7 @@ class TextInput extends Component {
defaultValue: PropTypes.string, defaultValue: PropTypes.string,
editable: PropTypes.bool, editable: PropTypes.bool,
keyboardType: PropTypes.oneOf([ keyboardType: PropTypes.oneOf([
'default', 'email-address', 'numeric', 'phone-pad', 'search', 'url', 'web-search' 'default', 'email-address', 'number-pad', 'numeric', 'phone-pad', 'search', 'url', 'web-search'
]), ]),
maxLength: PropTypes.number, maxLength: PropTypes.number,
maxNumberOfLines: PropTypes.number, maxNumberOfLines: PropTypes.number,
@@ -85,7 +81,6 @@ class TextInput extends Component {
end: PropTypes.number end: PropTypes.number
}), }),
style: Text.propTypes.style, style: Text.propTypes.style,
testID: Text.propTypes.testID,
value: PropTypes.string value: PropTypes.string
}; };
@@ -101,60 +96,63 @@ class TextInput extends Component {
style: {} style: {}
}; };
constructor(props, context) {
super(props, context);
this.state = { showPlaceholder: !props.value && !props.defaultValue };
}
blur() { blur() {
TextInputState.blurTextInput(findNodeHandle(this._inputRef)); TextInputState.blurTextInput(this._node);
} }
clear() { clear() {
this.setNativeProps({ text: '' }); this._node.value = '';
} }
focus() { focus() {
TextInputState.focusTextInput(findNodeHandle(this._inputRef)); TextInputState.focusTextInput(this._node);
} }
isFocused() { isFocused() {
return TextInputState.currentlyFocusedField() === findNodeHandle(this._inputRef); return TextInputState.currentlyFocusedField() === this._node;
} }
setNativeProps(props) { setNativeProps(props) {
UIManager.updateView(this._inputRef, props, this); UIManager.updateView(this._node, props, this);
} }
componentDidMount() { componentDidMount() {
setSelection(findNodeHandle(this._inputRef), this.props.selection); setSelection(this._node, this.props.selection);
} }
componentDidUpdate() { componentDidUpdate() {
setSelection(findNodeHandle(this._inputRef), this.props.selection); setSelection(this._node, this.props.selection);
} }
render() { render() {
const { const {
accessibilityLabel, // eslint-disable-line
autoCapitalize,
autoComplete,
autoCorrect, autoCorrect,
autoFocus,
defaultValue,
editable, editable,
keyboardType, keyboardType,
maxLength,
maxNumberOfLines, maxNumberOfLines,
multiline, multiline,
numberOfLines, numberOfLines,
onLayout,
placeholder,
placeholderTextColor,
secureTextEntry, secureTextEntry,
style, style,
testID, /* eslint-disable */
value blurOnSubmit,
clearTextOnFocus,
dataDetectorTypes,
enablesReturnKeyAutomatically,
keyboardAppearance,
onChangeText,
onContentSizeChange,
onEndEditing,
onLayout,
onSelectionChange,
onSubmitEditing,
placeholderTextColor,
returnKeyType,
selection,
selectionColor,
selectTextOnFocus,
/* eslint-enable */
...other
} = this.props; } = this.props;
let type; let type;
@@ -163,6 +161,7 @@ class TextInput extends Component {
case 'email-address': case 'email-address':
type = 'email'; type = 'email';
break; break;
case 'number-pad':
case 'numeric': case 'numeric':
type = 'number'; type = 'number';
break; break;
@@ -184,29 +183,22 @@ class TextInput extends Component {
type = 'password'; type = 'password';
} }
// In order to support 'Text' styles on 'TextInput', we split the 'Text' const component = multiline ? TextareaAutosize : 'input';
// and 'View' styles and apply them to the 'Text' and 'View' components
// used in the implementation
const flattenedStyle = StyleSheet.flatten(style);
const rootStyles = pick(flattenedStyle, viewStyleProps);
const textStyles = omit(flattenedStyle, viewStyleProps);
const props = { const props = {
autoCapitalize, ...other,
autoComplete,
autoCorrect: autoCorrect ? 'on' : 'off', autoCorrect: autoCorrect ? 'on' : 'off',
autoFocus,
defaultValue,
maxLength,
onBlur: normalizeEventHandler(this._handleBlur), onBlur: normalizeEventHandler(this._handleBlur),
onChange: normalizeEventHandler(this._handleChange), onChange: normalizeEventHandler(this._handleChange),
onFocus: normalizeEventHandler(this._handleFocus), onFocus: normalizeEventHandler(this._handleFocus),
onKeyPress: normalizeEventHandler(this._handleKeyPress), onKeyPress: normalizeEventHandler(this._handleKeyPress),
onSelect: normalizeEventHandler(this._handleSelectionChange), onSelect: normalizeEventHandler(this._handleSelectionChange),
readOnly: !editable, readOnly: !editable,
ref: this._setInputRef, ref: this._setNode,
style: [ styles.input, textStyles, { outline: style.outline } ], style: [
value styles.initial,
style
]
}; };
if (multiline) { if (multiline) {
@@ -216,66 +208,27 @@ class TextInput extends Component {
props.type = type; props.type = type;
} }
const component = multiline ? TextareaAutosize : 'input'; return createDOMElement(component, props);
const optionalPlaceholder = placeholder && this.state.showPlaceholder && (
<View pointerEvents='none' style={styles.placeholder}>
<Text
children={placeholder}
style={[
styles.placeholderText,
textStyles,
placeholderTextColor && { color: placeholderTextColor }
]}
/>
</View>
);
return (
<View
accessibilityLabel={accessibilityLabel}
onClick={this._handleClick}
onLayout={onLayout}
style={[ styles.initial, rootStyles ]}
testID={testID}
>
<View style={styles.wrapper}>
{createDOMElement(component, props)}
{optionalPlaceholder}
</View>
</View>
);
} }
_handleBlur = (e) => { _handleBlur = (e) => {
const { onBlur } = this.props; const { onBlur } = this.props;
const { text } = e.nativeEvent;
this.setState({ showPlaceholder: text === '' });
if (onBlur) { onBlur(e); } if (onBlur) { onBlur(e); }
} }
_handleChange = (e) => { _handleChange = (e) => {
const { onChange, onChangeText } = this.props; const { onChange, onChangeText } = this.props;
const { text } = e.nativeEvent; const { text } = e.nativeEvent;
this.setState({ showPlaceholder: text === '' });
if (onChange) { onChange(e); } if (onChange) { onChange(e); }
if (onChangeText) { onChangeText(text); } if (onChangeText) { onChangeText(text); }
} }
_handleClick = (e) => {
if (this.props.editable) {
this.focus();
}
}
_handleFocus = (e) => { _handleFocus = (e) => {
const { clearTextOnFocus, onFocus, selectTextOnFocus } = this.props; const { clearTextOnFocus, onFocus, selectTextOnFocus } = this.props;
const { text } = e.nativeEvent; const node = this._node;
const node = findNodeHandle(this._inputRef);
if (onFocus) { onFocus(e); } if (onFocus) { onFocus(e); }
if (clearTextOnFocus) { this.clear(); } if (clearTextOnFocus) { this.clear(); }
if (selectTextOnFocus) { node && node.select(); } if (selectTextOnFocus) { node && node.select(); }
this.setState({ showPlaceholder: text === '' });
} }
_handleKeyPress = (e) => { _handleKeyPress = (e) => {
@@ -303,43 +256,24 @@ class TextInput extends Component {
} }
} }
_setInputRef = (component) => { _setNode = (component) => {
this._inputRef = component; this._node = findNodeHandle(component);
} }
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
initial: { initial: {
borderColor: 'black'
},
wrapper: {
flex: 1
},
input: {
appearance: 'none', appearance: 'none',
backgroundColor: 'transparent', backgroundColor: 'transparent',
borderColor: 'black',
borderRadius: 0, borderRadius: 0,
borderWidth: 0, borderWidth: 0,
boxSizing: 'border-box', boxSizing: 'border-box',
color: 'inherit', color: 'inherit',
flex: 1, flex: 1,
font: 'inherit', font: 'inherit',
minHeight: '100%', // center small inputs (fix #139) padding: 0
padding: 0,
zIndex: 1
},
placeholder: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0
},
placeholderText: {
color: 'darkgray',
overflow: 'hidden',
whiteSpace: 'pre'
} }
}); });
module.exports = applyNativeMethods(TextInput); module.exports = applyLayout(applyNativeMethods(TextInput));
+2 -2
View File
@@ -12,13 +12,13 @@ const applyLayout = (Component) => {
const componentDidUpdate = Component.prototype.componentDidUpdate || emptyFunction; const componentDidUpdate = Component.prototype.componentDidUpdate || emptyFunction;
Component.prototype.componentDidMount = function () { Component.prototype.componentDidMount = function () {
componentDidMount(); componentDidMount.call(this);
this._layoutState = {}; this._layoutState = {};
this._handleLayout(); this._handleLayout();
}; };
Component.prototype.componentDidUpdate = function () { Component.prototype.componentDidUpdate = function () {
componentDidUpdate(); componentDidUpdate.call(this);
this._handleLayout(); this._handleLayout();
}; };