[change] TextInput: implement placeholder and placeholderTextColor

Without access to the Shadow DOM pseudo-elements, the placeholder
behaviour needs to be reimplemented.

Update to match React Native's modification to `TextInput` to include
all `View` props and use the `Text` style props.

Fix #12
Fix #48
This commit is contained in:
Nicolas Gallagher
2015-12-20 02:52:20 -08:00
parent 3d1ad50a58
commit 0ab984f507
7 changed files with 174 additions and 131 deletions
+13 -16
View File
@@ -48,9 +48,10 @@ updating the `value` prop to keep the controlled state in sync.
If `false`, text is not editable (i.e., read-only).
**keyboardType**: oneOf('default', 'email-address', 'numeric', 'phone-pad', 'url') = 'default'
**keyboardType**: oneOf('default', 'email-address', 'numeric', 'phone-pad', 'search', 'url', 'web-search') = 'default'
Determines which keyboard to open.
Determines which keyboard to open. (NOTE: Safari iOS requires an ancestral
`<form action>` element to display the `search` keyboard).
(Not available when `multiline` is `true`.)
@@ -111,11 +112,12 @@ object is passed as an argument to the callback handler.
**placeholder**: string
The string that will be rendered before text input has been entered.
The string that will be rendered in an empty `TextInput` before text has been
entered.
**placeholderTextColor**: string
TODO. The text color of the placeholder string.
The text color of the placeholder string.
**secureTextEntry**: bool = false
@@ -130,18 +132,8 @@ If `true`, all text will automatically be selected on focus.
**style**: style
+ ...[View#style](View.md)
+ `color`
+ `direction`
+ `fontFamily`
+ `fontSize`
+ `fontStyle`
+ `fontWeight`
+ `letterSpacing`
+ `lineHeight`
+ `textAlign`
+ `textDecoration`
+ `textTransform`
+ ...[Text#style](Text.md)
+ `outline`
**testID**: string
@@ -166,6 +158,10 @@ export default class TextInputExample extends Component {
this.state = { isFocused: false }
}
_onBlur(e) {
this.setState({ isFocused: false })
}
_onFocus(e) {
this.setState({ isFocused: true })
}
@@ -177,6 +173,7 @@ export default class TextInputExample extends Component {
maxNumberOfLines={5}
multiline
numberOfLines={2}
onBlur={this._onBlur.bind(this)}
onFocus={this._onFocus.bind(this)}
placeholder={`What's happening?`}
style={{
+2 -2
View File
@@ -95,10 +95,10 @@ export default class App extends React.Component {
/>
<TextInput secureTextEntry />
<TextInput defaultValue='read only' editable={false} />
<TextInput keyboardType='email-address' />
<TextInput keyboardType='email-address' placeholder='you@domain.com' placeholderTextColor='red' />
<TextInput keyboardType='numeric' />
<TextInput keyboardType='phone-pad' />
<TextInput keyboardType='url' selectTextOnFocus />
<TextInput defaultValue='https://delete-me' keyboardType='url' placeholder='https://www.some-website.com' selectTextOnFocus />
<TextInput
defaultValue='default value'
maxNumberOfLines={10}
+1 -1
View File
@@ -28,7 +28,7 @@ const MediaQueryWidget = ({ mediaQuery = {} }) => {
return (
<View style={styles.root}>
<Text style={styles.heading}>Active Media Query</Text>
<Text>{`"${active.alias}"`} {active.mql.media}</Text>
<Text>{`"${active.alias}"`} {active.mql && active.mql.media}</Text>
</View>
)
}
@@ -1,20 +1,7 @@
import { pickProps } from '../../modules/filterObjectProps'
import View from '../View'
import CoreComponent from '../CoreComponent'
import React from 'react'
import Text from '../Text'
export default {
...(View.stylePropTypes),
...pickProps(CoreComponent.stylePropTypes, [
'color',
'direction',
'fontFamily',
'fontSize',
'fontStyle',
'fontWeight',
'letterSpacing',
'lineHeight',
'textAlign',
'textDecoration',
'textTransform'
])
...Text.stylePropTypes,
outline: React.PropTypes.string
}
@@ -7,6 +7,10 @@ import ReactTestUtils from 'react-addons-test-utils'
import TextInput from '../'
const findInput = (dom) => dom.querySelector('input, textarea')
const findShallowInput = (vdom) => vdom.props.children.props.children[0]
const findShallowPlaceholder = (vdom) => vdom.props.children.props.children[1]
suite('components/TextInput', () => {
test('prop "accessibilityLabel"', () => {
const accessibilityLabel = 'accessibilityLabel'
@@ -16,123 +20,120 @@ suite('components/TextInput', () => {
test('prop "autoComplete"', () => {
// off
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('autocomplete'), undefined)
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('autocomplete'), undefined)
// on
dom = utils.renderToDOM(<TextInput autoComplete />)
assert.equal(dom.getAttribute('autocomplete'), 'on')
input = findInput(utils.renderToDOM(<TextInput autoComplete />))
assert.equal(input.getAttribute('autocomplete'), 'on')
})
test('prop "autoFocus"', () => {
// false
let dom = utils.renderToDOM(<TextInput />)
let input = findInput(utils.renderToDOM(<TextInput />))
assert.deepEqual(document.activeElement, document.body)
// true
dom = utils.renderToDOM(<TextInput autoFocus />)
assert.deepEqual(document.activeElement, dom)
input = findInput(utils.renderToDOM(<TextInput autoFocus />))
assert.deepEqual(document.activeElement, input)
})
utils.testIfDocumentFocused('prop "clearTextOnFocus"', () => {
const defaultValue = 'defaultValue'
// false
let dom = utils.renderAndInject(<TextInput defaultValue={defaultValue} />)
dom.focus()
assert.equal(dom.value, defaultValue)
let input = findInput(utils.renderAndInject(<TextInput defaultValue={defaultValue} />))
input.focus()
assert.equal(input.value, defaultValue)
// true
dom = utils.renderAndInject(<TextInput clearTextOnFocus defaultValue={defaultValue} />)
dom.focus()
assert.equal(dom.value, '')
input = findInput(utils.renderAndInject(<TextInput clearTextOnFocus defaultValue={defaultValue} />))
input.focus()
assert.equal(input.value, '')
})
test('prop "defaultValue"', () => {
const defaultValue = 'defaultValue'
const result = utils.shallowRender(<TextInput defaultValue={defaultValue} />)
assert.equal(result.props.defaultValue, defaultValue)
const input = findShallowInput(utils.shallowRender(<TextInput defaultValue={defaultValue} />))
assert.equal(input.props.defaultValue, defaultValue)
})
test('prop "editable"', () => {
// true
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('readonly'), undefined)
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('readonly'), undefined)
// false
dom = utils.renderToDOM(<TextInput editable={false} />)
assert.equal(dom.getAttribute('readonly'), '')
input = findInput(utils.renderToDOM(<TextInput editable={false} />))
assert.equal(input.getAttribute('readonly'), '')
})
test('prop "keyboardType"', () => {
// default
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('type'), undefined)
dom = utils.renderToDOM(<TextInput keyboardType='default' />)
assert.equal(dom.getAttribute('type'), undefined)
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('type'), undefined)
input = findInput(utils.renderToDOM(<TextInput keyboardType='default' />))
assert.equal(input.getAttribute('type'), undefined)
// email-address
dom = utils.renderToDOM(<TextInput keyboardType='email-address' />)
assert.equal(dom.getAttribute('type'), 'email')
input = findInput(utils.renderToDOM(<TextInput keyboardType='email-address' />))
assert.equal(input.getAttribute('type'), 'email')
// numeric
dom = utils.renderToDOM(<TextInput keyboardType='numeric' />)
assert.equal(dom.getAttribute('type'), 'number')
input = findInput(utils.renderToDOM(<TextInput keyboardType='numeric' />))
assert.equal(input.getAttribute('type'), 'number')
// phone-pad
dom = utils.renderToDOM(<TextInput keyboardType='phone-pad' />)
assert.equal(dom.getAttribute('type'), 'tel')
input = findInput(utils.renderToDOM(<TextInput keyboardType='phone-pad' />))
assert.equal(input.getAttribute('type'), 'tel')
// url
dom = utils.renderToDOM(<TextInput keyboardType='url' />)
assert.equal(dom.getAttribute('type'), 'url')
input = findInput(utils.renderToDOM(<TextInput keyboardType='url' />))
assert.equal(input.getAttribute('type'), 'url')
})
test('prop "maxLength"', () => {
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('maxlength'), undefined)
dom = utils.renderToDOM(<TextInput maxLength={10} />)
assert.equal(dom.getAttribute('maxlength'), '10')
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('maxlength'), undefined)
input = findInput(utils.renderToDOM(<TextInput maxLength={10} />))
assert.equal(input.getAttribute('maxlength'), '10')
})
test('prop "maxNumberOfLines"', () => {
const style = { borderWidth: 0, fontSize: 20, lineHeight: 1 }
const generateValue = () => {
let str = ''
while (str.length < 100) str += 'x'
return str
}
let dom = utils.renderAndInject(
let input = findInput(utils.renderAndInject(
<TextInput
maxNumberOfLines={3}
multiline
style={style}
value={generateValue()}
/>
)
const height = dom.getBoundingClientRect().height
))
const height = input.getBoundingClientRect().height
// need a range because of cross-browser differences
assert.ok(height >= 60, height)
assert.ok(height <= 66, height)
assert.ok(height >= 42, height)
assert.ok(height <= 48, height)
})
test('prop "multiline"', () => {
// false
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.tagName, 'INPUT')
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.tagName, 'INPUT')
// true
dom = utils.renderToDOM(<TextInput multiline />)
assert.equal(dom.tagName, 'TEXTAREA')
input = findInput(utils.renderToDOM(<TextInput multiline />))
assert.equal(input.tagName, 'TEXTAREA')
})
test('prop "numberOfLines"', () => {
const style = { borderWidth: 0, fontSize: 20, lineHeight: 1 }
// missing multiline
let dom = utils.renderToDOM(<TextInput numberOfLines={2} />)
assert.equal(dom.tagName, 'INPUT')
let input = findInput(utils.renderToDOM(<TextInput numberOfLines={2} />))
assert.equal(input.tagName, 'INPUT')
// with multiline
dom = utils.renderAndInject(<TextInput multiline numberOfLines={2} style={style} />)
assert.equal(dom.tagName, 'TEXTAREA')
const height = dom.getBoundingClientRect().height
input = findInput(utils.renderAndInject(<TextInput multiline numberOfLines={2} />))
assert.equal(input.tagName, 'TEXTAREA')
const height = input.getBoundingClientRect().height
// need a range because of cross-browser differences
assert.ok(height >= 40)
assert.ok(height <= 46)
assert.ok(height >= 30, height)
assert.ok(height <= 36, height)
})
test('prop "onBlur"', (done) => {
const input = utils.renderToDOM(<TextInput onBlur={onBlur} />)
const input = findInput(utils.renderToDOM(<TextInput onBlur={onBlur} />))
ReactTestUtils.Simulate.blur(input)
function onBlur(e) {
assert.ok(e)
@@ -141,7 +142,7 @@ suite('components/TextInput', () => {
})
test('prop "onChange"', (done) => {
const input = utils.renderToDOM(<TextInput onChange={onChange} />)
const input = findInput(utils.renderToDOM(<TextInput onChange={onChange} />))
ReactTestUtils.Simulate.change(input)
function onChange(e) {
assert.ok(e)
@@ -151,7 +152,7 @@ suite('components/TextInput', () => {
test('prop "onChangeText"', (done) => {
const newText = 'newText'
const input = utils.renderToDOM(<TextInput onChangeText={onChangeText} />)
const input = findInput(utils.renderToDOM(<TextInput onChangeText={onChangeText} />))
ReactTestUtils.Simulate.change(input, { target: { value: newText } })
function onChangeText(text) {
assert.equal(text, newText)
@@ -160,7 +161,7 @@ suite('components/TextInput', () => {
})
test('prop "onFocus"', (done) => {
const input = utils.renderToDOM(<TextInput onFocus={onFocus} />)
const input = findInput(utils.renderToDOM(<TextInput onFocus={onFocus} />))
ReactTestUtils.Simulate.focus(input)
function onFocus(e) {
assert.ok(e)
@@ -171,7 +172,7 @@ suite('components/TextInput', () => {
test('prop "onLayout"')
test('prop "onSelectionChange"', (done) => {
const input = utils.renderAndInject(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />)
const input = findInput(utils.renderAndInject(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />))
ReactTestUtils.Simulate.select(input, { target: { selectionStart: 0, selectionEnd: 3 } })
function onSelectionChange(e) {
assert.equal(e.selectionEnd, 3)
@@ -180,30 +181,42 @@ suite('components/TextInput', () => {
}
})
test('prop "placeholder"')
test('prop "placeholder"', () => {
const placeholder = 'placeholder'
const result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} />))
assert.equal(result.props.children, placeholder)
})
test('prop "placeholderTextColor"')
test('prop "placeholderTextColor"', () => {
const placeholder = 'placeholder'
let result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} />))
assert.equal(result.props.style.color, 'darkgray')
result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} placeholderTextColor='red' />))
assert.equal(result.props.style.color, 'red')
})
test('prop "secureTextEntry"', () => {
let dom = utils.renderToDOM(<TextInput secureTextEntry />)
assert.equal(dom.getAttribute('type'), 'password')
let input = findInput(utils.renderToDOM(<TextInput secureTextEntry />))
assert.equal(input.getAttribute('type'), 'password')
// ignored for multiline
dom = utils.renderToDOM(<TextInput multiline secureTextEntry />)
assert.equal(dom.getAttribute('type'), undefined)
input = findInput(utils.renderToDOM(<TextInput multiline secureTextEntry />))
assert.equal(input.getAttribute('type'), undefined)
})
utils.testIfDocumentFocused('prop "selectTextOnFocus"', () => {
const text = 'Text'
// false
let dom = utils.renderAndInject(<TextInput defaultValue={text} />)
dom.focus()
assert.equal(dom.selectionEnd, 0)
assert.equal(dom.selectionStart, 0)
let input = findInput(utils.renderAndInject(<TextInput defaultValue={text} />))
input.focus()
assert.equal(input.selectionEnd, 0)
assert.equal(input.selectionStart, 0)
// true
dom = utils.renderAndInject(<TextInput defaultValue={text} selectTextOnFocus />)
dom.focus()
assert.equal(dom.selectionEnd, 4)
assert.equal(dom.selectionStart, 0)
input = findInput(utils.renderAndInject(<TextInput defaultValue={text} selectTextOnFocus />))
input.focus()
assert.equal(input.selectionEnd, 4)
assert.equal(input.selectionStart, 0)
})
test('prop "style"', () => {
@@ -218,7 +231,7 @@ suite('components/TextInput', () => {
test('prop "value"', () => {
const value = 'value'
const result = utils.shallowRender(<TextInput value={value} />)
assert.equal(result.props.value, value)
const input = findShallowInput(utils.shallowRender(<TextInput value={value} />))
assert.equal(input.props.value, value)
})
})
+65 -20
View File
@@ -3,27 +3,50 @@ import CoreComponent from '../CoreComponent'
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import StyleSheet from '../../modules/StyleSheet'
import Text from '../Text'
import TextareaAutosize from 'react-textarea-autosize'
import TextInputStylePropTypes from './TextInputStylePropTypes'
import View from '../View'
const textInputStyleKeys = Object.keys(TextInputStylePropTypes)
const styles = StyleSheet.create({
initial: {
...View.defaultProps.style,
borderColor: 'black',
borderWidth: 1
},
input: {
appearance: 'none',
backgroundColor: 'transparent',
borderColor: 'black',
borderWidth: '1px',
borderWidth: 0,
boxSizing: 'border-box',
color: 'inherit',
flexGrow: 1,
font: 'inherit',
padding: 0
padding: 0,
zIndex: 1
},
placeholder: {
bottom: 0,
color: 'darkgray',
left: 0,
overflow: 'hidden',
position: 'absolute',
right: 0,
top: 0,
whiteSpace: 'pre'
}
})
class TextInput extends React.Component {
constructor(props, context) {
super(props, context)
this.state = { showPlaceholder: !props.value && !props.defaultValue }
}
static propTypes = {
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
...View.propTypes,
autoComplete: PropTypes.bool,
autoFocus: PropTypes.bool,
clearTextOnFocus: PropTypes.bool,
@@ -61,20 +84,26 @@ class TextInput extends React.Component {
_onBlur(e) {
const { onBlur } = this.props
const value = e.target.value
this.setState({ showPlaceholder: value === '' })
if (onBlur) onBlur(e)
}
_onChange(e) {
const { onChange, onChangeText } = this.props
if (onChangeText) onChangeText(e.target.value)
const value = e.target.value
this.setState({ showPlaceholder: value === '' })
if (onChangeText) onChangeText(value)
if (onChange) onChange(e)
}
_onFocus(e) {
const { clearTextOnFocus, onFocus, selectTextOnFocus } = this.props
const node = ReactDOM.findDOMNode(this)
const node = ReactDOM.findDOMNode(this.refs.input)
const value = e.target.value
if (clearTextOnFocus) node.value = ''
if (selectTextOnFocus) node.select()
this.setState({ showPlaceholder: value === '' })
if (onFocus) onFocus(e)
}
@@ -92,7 +121,9 @@ class TextInput extends React.Component {
render() {
const {
/* eslint-disable react/prop-types */
accessibilityLabel,
/* eslint-enable react/prop-types */
autoComplete,
autoFocus,
defaultValue,
@@ -102,11 +133,9 @@ class TextInput extends React.Component {
maxNumberOfLines,
multiline,
numberOfLines,
onBlur,
onChange,
onChangeText,
onSelectionChange,
placeholder,
placeholderTextColor,
secureTextEntry,
style,
testID,
@@ -126,6 +155,10 @@ class TextInput extends React.Component {
case 'phone-pad':
type = 'tel'
break
case 'search':
case 'web-search':
type = 'search'
break
case 'url':
type = 'url'
break
@@ -136,23 +169,16 @@ class TextInput extends React.Component {
}
const propsCommon = {
accessibilityLabel,
autoComplete: autoComplete && 'on',
autoFocus,
className: 'TextInput',
defaultValue,
maxLength,
onBlur: onBlur && this._onBlur.bind(this),
onChange: (onChange || onChangeText) && this._onChange.bind(this),
onBlur: this._onBlur.bind(this),
onChange: this._onChange.bind(this),
onFocus: this._onFocus.bind(this),
onSelect: onSelectionChange && this._onSelectionChange.bind(this),
placeholder,
readOnly: !editable,
style: {
...styles.initial,
...resolvedStyle
},
testID,
style: { ...styles.input, outline: style.outline },
value
}
@@ -172,7 +198,26 @@ class TextInput extends React.Component {
const props = multiline ? propsMultiline : propsSingleline
return (
<CoreComponent {...props} />
<CoreComponent
accessibilityLabel={accessibilityLabel}
className='TextInput'
style={{
...styles.initial,
...resolvedStyle
}}
testID={testID}
>
<View style={{ flexGrow: 1 }}>
<CoreComponent {...props} ref='input' />
{placeholder && this.state.showPlaceholder && <Text
pointerEvents='none'
style={{
...styles.placeholder,
...(placeholderTextColor && { color: placeholderTextColor })
}}
>{placeholder}</Text>}
</View>
</CoreComponent>
)
}
}
+1
View File
@@ -80,6 +80,7 @@ export default {
minWidth: numberOrString,
opacity: numberOrString,
order: numberOrString,
outline: string,
overflow: string,
overflowX: string,
overflowY: string,