TextInput: props and tests

This commit is contained in:
Nicolas Gallagher
2015-09-20 15:43:52 -07:00
parent d6db206ec4
commit e727193809
8 changed files with 483 additions and 70 deletions

View File

@@ -4,7 +4,7 @@
[![npm version][npm-image]][npm-url]
The core [React Native][react-native-url] components adapted and expanded upon
for the web, backed by a precomputed CSS library. ~19KB minified and gzipped.
for the web, backed by a precomputed CSS library. ~21KB minified and gzipped.
* [Slack: reactiflux channel #react-native-web][slack-url]
* [Gitter: react-native-web][gitter-url]

View File

@@ -5,7 +5,7 @@ var webpackConfig = require('./webpack.config.base')
module.exports = function (config) {
config.set({
basePath: constants.ROOT_DIRECTORY,
browsers: [ process.env.TRAVIS ? 'Firefox' : 'Chrome' ],
browsers: process.env.TRAVIS ? [ 'Firefox' ] : [ 'Chrome' ],
browserNoActivityTimeout: 60000,
client: {
captureConsole: true,

View File

@@ -5,67 +5,130 @@ such as auto-complete, auto-focus, placeholder text, and event callbacks.
Note: some props are exclusive to or excluded from `multiline`.
Unsupported React Native props:
`autoCapitalize`,
`autoCorrect`,
`onEndEditing`,
`onSubmitEditing`,
`clearButtonMode` (ios),
`enablesReturnKeyAutomatically` (ios),
`returnKeyType` (ios),
`selectionState` (ios),
`textAlign` (android),
`textAlignVertical` (android),
`underlineColorAndroid` (android)
## Props
**autoComplete** bool
(web) **accessibilityLabel**: string
Defines the text label available to assistive technologies upon interaction
with the element. (This is implemented using `aria-label`.)
(web) **autoComplete**: bool = false
Indicates whether the value of the control can be automatically completed by the browser.
**autoFocus** bool
**autoFocus**: bool = false
If true, focuses the input on `componentDidMount`. Only the first form element
in a document with `autofocus` is focused. Default: `false`.
in a document with `autofocus` is focused.
**defaultValue** string
**clearTextOnFocus**: bool = false
If `true`, clears the text field automatically when focused.
**defaultValue**: string
Provides an initial value that will change when the user starts typing. Useful
for simple use-cases where you don't want to deal with listening to events and
updating the `value` prop to keep the controlled state in sync.
**editable** bool
**editable**: bool = true
If false, text is not editable. Default: `true`.
If `false`, text is not editable (i.e., read-only).
**keyboardType** oneOf('default', 'email', 'numeric', 'search', 'tel', 'url')
**keyboardType**: oneOf('default', 'email-address', 'numeric', 'phone-pad', 'url') = 'default'
Determines which keyboard to open, e.g. `email`. Default: `default`. (Not
available when `multiline` is `true`.)
Determines which keyboard to open.
**multiline** bool
(Not available when `multiline` is `true`.)
If true, the text input can be multiple lines. Default: `false`.
**maxLength**: number
**onBlur** function
Limits the maximum number of characters that can be entered.
(web) **maxNumberOfLines**: number = numberOfLines
Limits the maximum number of lines for a multiline `TextInput`.
(Requires `multiline` to be `true`.)
**multiline**: bool = false
If true, the text input can be multiple lines.
**numberOfLines**: number = 2
Sets the initial number of lines for a multiline `TextInput`.
(Requires `multiline` to be `true`.)
**onBlur**: function
Callback that is called when the text input is blurred.
**onChange** function
**onChange**: function
Callback that is called when the text input's text changes.
**onChangeText** function
**onChangeText**: function
Callback that is called when the text input's text changes. Changed text is
passed as an argument to the callback handler.
Callback that is called when the text input's text changes. The text is passed
as an argument to the callback handler.
**onFocus** function
**onFocus**: function
Callback that is called when the text input is focused.
**placeholder** string
**onLayout**: function
TODO
(web) **onSelectionChange**: function
Callback that is called when the text input's selection changes. The following
object is passed as an argument to the callback handler.
```js
{
selectionDirection,
selectionEnd,
selectionStart,
nativeEvent
}
```
**placeholder**: string
The string that will be rendered before text input has been entered.
**placeholderTextColor** string
**placeholderTextColor**: string
The text color of the placeholder string.
TODO. The text color of the placeholder string.
**secureTextEntry** bool
**secureTextEntry**: bool = false
If true, the text input obscures the text entered so that sensitive text like
passwords stay secure. Default: `false`. (Not available when `multiline` is `true`.)
passwords stay secure.
**style** style
(Not available when `multiline` is `true`.)
**selectTextOnFocus**: bool = false
If `true`, all text will automatically be selected on focus.
**style**: style
[View](View.md) style
@@ -81,31 +144,60 @@ passwords stay secure. Default: `false`. (Not available when `multiline` is `tru
+ `textDecoration`
+ `textTransform`
**testID** string
**testID**: string
Used to locate this view in end-to-end tests.
**value**: string
The value to show for the text input. `TextInput` is a controlled component,
which means the native `value` will be forced to match this prop if provided.
Read about how [React form
components](https://facebook.github.io/react/docs/forms.html) work. To prevent
user edits to the value set `editable={false}`.
## Examples
```js
import React, { TextInput } from 'react-native-web'
const { Component, PropTypes } = React
const { Component } = React
class AppTextInput extends Component {
static propTypes = {
constructor(props, context) {
super(props, context)
this.state = { isFocused: false }
}
static defaultProps = {
_onFocus(e) {
this.setState({ isFocused: true })
}
render() {
return (
<TextInput />
<TextInput
accessibilityLabel='Write a status update'
maxNumberOfLines={5}
multiline
numberOfLines={2}
onFocus={this._onFocus.bind(this)}
placeholder={`What's happening?`}
style={
...styles.default
...(this.state.isFocused && styles.focused)
}
/>
);
}
}
const styles = {
default: {
borderColor: 'gray',
borderWidth: '0 0 2px 0'
},
focused: {
borderColor: 'blue'
}
}
```

View File

@@ -18,7 +18,8 @@
"dependencies": {
"react": ">=0.13.3",
"react-swipeable": "^3.0.2",
"react-tappable": "^0.6.0"
"react-tappable": "^0.6.0",
"react-textarea-autosize": "^2.5.2"
},
"devDependencies": {
"autoprefixer-loader": "^3.1.0",

View File

@@ -1,6 +1,7 @@
import { pickProps } from '../../modules/filterObjectProps'
import CoreComponent from '../CoreComponent'
import React, { PropTypes } from 'react'
import TextareaAutosize from 'react-textarea-autosize'
import TextInputStylePropTypes from './TextInputStylePropTypes'
const textInputStyleKeys = Object.keys(TextInputStylePropTypes)
@@ -9,90 +10,166 @@ const styles = {
initial: {
appearance: 'none',
backgroundColor: 'transparent',
borderColor: 'black',
borderWidth: '1px',
boxSizing: 'border-box',
color: 'inherit',
font: 'inherit'
font: 'inherit',
padding: 0
}
}
class TextInput extends React.Component {
static propTypes = {
accessibilityLabel: PropTypes.string,
autoComplete: PropTypes.bool,
autoFocus: PropTypes.bool,
clearTextOnFocus: PropTypes.bool,
defaultValue: PropTypes.string,
editable: PropTypes.bool,
keyboardType: PropTypes.oneOf(['default', 'email', 'numeric', 'search', 'tel', 'url']),
keyboardType: PropTypes.oneOf(['default', 'email-address', 'numeric', 'phone-pad', 'url']),
maxLength: PropTypes.number,
maxNumberOfLines: PropTypes.number,
multiline: PropTypes.bool,
numberOfLines: PropTypes.number,
onBlur: PropTypes.func,
onChange: PropTypes.func,
onChangeText: PropTypes.func,
onFocus: PropTypes.func,
onSelectionChange: PropTypes.func,
placeholder: PropTypes.string,
placeholderTextColor: PropTypes.string,
secureTextEntry: PropTypes.bool,
selectTextOnFocus: PropTypes.bool,
style: PropTypes.shape(TextInputStylePropTypes),
testID: CoreComponent.propTypes.testID
testID: CoreComponent.propTypes.testID,
value: PropTypes.string
}
static stylePropTypes = TextInputStylePropTypes
static defaultProps = {
autoComplete: false,
autoFocus: false,
editable: true,
keyboardType: 'default',
multiline: false,
numberOfLines: 2,
secureTextEntry: false,
style: styles.initial
}
_onBlur(e) {
if (this.props.onBlur) this.props.onBlur(e)
const { onBlur } = this.props
if (onBlur) onBlur(e)
}
_onChange(e) {
if (this.props.onChangeText) this.props.onChangeText(e.target.value)
if (this.props.onChange) this.props.onChange(e)
const { onChange, onChangeText } = this.props
if (onChangeText) onChangeText(e.target.value)
if (onChange) onChange(e)
}
_onFocus(e) {
if (this.props.onFocus) this.props.onFocus(e)
const { clearTextOnFocus, onFocus, selectTextOnFocus } = this.props
const node = React.findDOMNode(this)
if (clearTextOnFocus) node.value = ''
if (selectTextOnFocus) node.select()
if (onFocus) onFocus(e)
}
_onSelectionChange(e) {
const { onSelectionChange } = this.props
const { selectionDirection, selectionEnd, selectionStart } = e.target
const event = {
selectionDirection,
selectionEnd,
selectionStart,
nativeEvent: e.nativeEvent
}
if (onSelectionChange) onSelectionChange(event)
}
render() {
const {
accessibilityLabel,
autoComplete,
autoFocus,
defaultValue,
editable,
keyboardType,
maxLength,
maxNumberOfLines,
multiline,
numberOfLines,
onBlur,
onChange,
onChangeText,
onSelectionChange,
placeholder,
secureTextEntry,
style,
testID
testID,
value
} = this.props
const resolvedStyle = pickProps(style, textInputStyleKeys)
const type = secureTextEntry && 'password' || (keyboardType === 'default' ? '' : keyboardType)
let type
return (
<CoreComponent
autoComplete={autoComplete}
autoFocus={autoFocus}
className={'TextInput'}
component={multiline ? 'textarea' : 'input'}
defaultValue={defaultValue || placeholder}
onBlur={this._onBlur.bind(this)}
onChange={this._onChange.bind(this)}
onFocus={this._onFocus.bind(this)}
readOnly={!editable}
style={{
...(styles.initial),
...resolvedStyle
}}
testID={testID}
type={multiline ? type : undefined}
/>
switch (keyboardType) {
case 'email-address':
type = 'email'
break
case 'numeric':
type = 'number'
break
case 'phone-pad':
type = 'tel'
break
case 'url':
type = 'url'
break
}
if (secureTextEntry) {
type = 'password'
}
const propsCommon = {
'aria-label': accessibilityLabel,
autoComplete: autoComplete && 'on',
autoFocus,
className: 'TextInput',
defaultValue,
maxLength,
onBlur: onBlur && this._onBlur.bind(this),
onChange: (onChange || onChangeText) && this._onChange.bind(this),
onFocus: this._onFocus.bind(this),
onSelect: onSelectionChange && this._onSelectionChange.bind(this),
placeholder,
readOnly: !editable,
style: {
...styles.initial,
...resolvedStyle
},
testID,
value
}
const propsMultiline = {
...propsCommon,
component: TextareaAutosize,
maxRows: maxNumberOfLines || numberOfLines,
minRows: numberOfLines
}
const propsSingleline = {
...propsCommon,
component: 'input',
type
}
return (multiline
? <CoreComponent {...propsMultiline} />
: <CoreComponent {...propsSingleline} />
)
}
}

View File

@@ -1,5 +1,4 @@
/*
import { assertProps, renderToDOM, shallowRender } from '../../modules/specHelpers'
import * as utils from '../../modules/specHelpers'
import assert from 'assert'
import React from 'react/addons'
@@ -7,7 +6,217 @@ import TextInput from '.'
const ReactTestUtils = React.addons.TestUtils
suite.skip('TextInput', () => {
test('prop "children"', () => {})
suite('TextInput', () => {
test('prop "accessibilityLabel"', () => {
utils.assertProps.accessibilityLabel(TextInput)
})
test('prop "autoComplete"', () => {
// off
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('autocomplete'), undefined)
// on
dom = utils.renderToDOM(<TextInput autoComplete />)
assert.equal(dom.getAttribute('autocomplete'), 'on')
})
test('prop "autoFocus"', () => {
// false
let dom = utils.renderToDOM(<TextInput />)
assert.deepEqual(document.activeElement, document.body)
// true
dom = utils.renderToDOM(<TextInput autoFocus />)
assert.deepEqual(document.activeElement, dom)
})
test('prop "clearTextOnFocus"', () => {
const defaultValue = 'defaultValue'
utils.requiresFocus(() => {
// false
let dom = utils.renderAndInject(<TextInput defaultValue={defaultValue} />)
dom.focus()
assert.equal(dom.value, defaultValue)
// true
dom = utils.renderAndInject(<TextInput clearTextOnFocus defaultValue={defaultValue} />)
dom.focus()
assert.equal(dom.value, '')
})
})
test('prop "defaultValue"', () => {
const defaultValue = 'defaultValue'
const result = utils.shallowRender(<TextInput defaultValue={defaultValue} />)
assert.equal(result.props.defaultValue, defaultValue)
})
test('prop "editable"', () => {
// true
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('readonly'), undefined)
// false
dom = utils.renderToDOM(<TextInput editable={false} />)
assert.equal(dom.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)
// email-address
dom = utils.renderToDOM(<TextInput keyboardType='email-address' />)
assert.equal(dom.getAttribute('type'), 'email')
// numeric
dom = utils.renderToDOM(<TextInput keyboardType='numeric' />)
assert.equal(dom.getAttribute('type'), 'number')
// phone-pad
dom = utils.renderToDOM(<TextInput keyboardType='phone-pad' />)
assert.equal(dom.getAttribute('type'), 'tel')
// url
dom = utils.renderToDOM(<TextInput keyboardType='url' />)
assert.equal(dom.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')
})
test('prop "maxNumberOfLines"', () => {
const style = { borderWidth: 0, fontSize: 20, lineHeight: 1 }
const value = (() => {
let str = ''
while (str.length < 100) str += 'x'
return str
}())
let dom = utils.renderAndInject(
<TextInput
maxNumberOfLines={3}
multiline
style={style}
value={value}
/>
)
const height = dom.getBoundingClientRect().height
// need a range because of cross-browser differences
assert.ok(height >= 60)
assert.ok(height <= 65)
})
test('prop "multiline"', () => {
// false
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.tagName, 'INPUT')
// true
dom = utils.renderToDOM(<TextInput multiline />)
assert.equal(dom.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')
// with multiline
dom = utils.renderAndInject(<TextInput multiline numberOfLines={2} style={style} />)
assert.equal(dom.tagName, 'TEXTAREA')
const height = dom.getBoundingClientRect().height
// need a range because of cross-browser differences
assert.ok(height >= 40)
assert.ok(height <= 45)
})
test('prop "onBlur"', (done) => {
const input = utils.renderToDOM(<TextInput onBlur={onBlur} />)
ReactTestUtils.Simulate.blur(input)
function onBlur(e) {
assert.ok(e)
done()
}
})
test('prop "onChange"', (done) => {
const input = utils.renderToDOM(<TextInput onChange={onChange} />)
ReactTestUtils.Simulate.change(input)
function onChange(e) {
assert.ok(e)
done()
}
})
test('prop "onChangeText"', (done) => {
const newText = 'newText'
const input = utils.renderToDOM(<TextInput onChangeText={onChangeText} />)
ReactTestUtils.Simulate.change(input, { target: { value: newText } })
function onChangeText(text) {
assert.equal(text, newText)
done()
}
})
test('prop "onFocus"', (done) => {
const input = utils.renderToDOM(<TextInput onFocus={onFocus} />)
ReactTestUtils.Simulate.focus(input)
function onFocus(e) {
assert.ok(e)
done()
}
})
test.skip('prop "onLayout"', () => {})
test('prop "onSelectionChange"', (done) => {
const input = utils.renderAndInject(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />)
ReactTestUtils.Simulate.select(input, { target: { selectionStart: 0, selectionEnd: 3 } })
function onSelectionChange(e) {
assert.equal(e.selectionEnd, 3)
assert.equal(e.selectionStart, 0)
done()
}
})
test.skip('prop "placeholder"', () => {})
test.skip('prop "placeholderTextColor"', () => {})
test('prop "secureTextEntry"', () => {
let dom = utils.renderToDOM(<TextInput secureTextEntry />)
assert.equal(dom.getAttribute('type'), 'password')
// ignored for multiline
dom = utils.renderToDOM(<TextInput multiline secureTextEntry />)
assert.equal(dom.getAttribute('type'), undefined)
})
test('prop "selectTextOnFocus"', () => {
const text = 'Text'
utils.requiresFocus(() => {
// false
let dom = utils.renderAndInject(<TextInput defaultValue={text} />)
dom.focus()
assert.equal(dom.selectionEnd, 0)
assert.equal(dom.selectionStart, 0)
// true
dom = utils.renderAndInject(<TextInput defaultValue={text} selectTextOnFocus />)
dom.focus()
assert.equal(dom.selectionEnd, 4)
assert.equal(dom.selectionStart, 0)
})
})
test('prop "style"', () => {
utils.assertProps.style(TextInput)
})
test('prop "testID"', () => {
utils.assertProps.testID(TextInput)
})
test('prop "value"', () => {
const value = 'value'
const result = utils.shallowRender(<TextInput value={value} />)
assert.equal(result.props.value, value)
})
})
*/

View File

@@ -132,13 +132,20 @@ class Example extends Component {
onChange={(e) => { console.log('TextInput.onChange', e) }}
onChangeText={(e) => { console.log('TextInput.onChangeText', e) }}
onFocus={(e) => { console.log('TextInput.onFocus', e) }}
onSelectionChange={(e) => { console.log('TextInput.onSelectionChange', e) }}
/>
<TextInput secureTextEntry />
<TextInput defaultValue='read only' editable={false} />
<TextInput keyboardType='email-address' />
<TextInput keyboardType='numeric' />
<TextInput keyboardType='tel' />
<TextInput keyboardType='phone-pad' />
<TextInput keyboardType='url' />
<TextInput keyboardType='search' />
<TextInput defaultValue='default value' multiline />
<TextInput
defaultValue='default value'
maxNumberOfLines={10}
multiline
numberOfLines={5}
/>
<Heading level='2' size='large'>Touchable</Heading>
<Touchable

View File

@@ -59,8 +59,8 @@ export const assertProps = {
const styleToMerge = { margin: '10' }
shallow = shallowRender(<Component {...props} style={styleToMerge} />)
assert.deepEqual(
shallow.props.style.margin,
styleToMerge.margin,
shallow.props.style,
{ ...Component.defaultProps.style, ...styleToMerge }
)
},
@@ -86,6 +86,33 @@ export function renderToDOM(element, container) {
return React.findDOMNode(result)
}
export function renderAndInject(element) {
const id = '_renderAndInject'
let div = document.getElementById(id)
if (!div) {
div = document.createElement('div')
div.id = id
document.body.appendChild(div)
} else {
div.innerHTML = ''
}
const result = render(element, div)
return React.findDOMNode(result)
}
export function requiresFocus(test, fallback) {
if (document.hasFocus && document.hasFocus()) {
test()
} else {
console.warn('Test was skipped as the document is not focused')
if (fallback) {
fallback()
}
}
}
export function shallowRender(component, context = {}) {
const shallowRenderer = React.addons.TestUtils.createRenderer()
shallowRenderer.render(component, context)