diff --git a/README.md b/README.md
index fe0c76d9..3427d0c7 100644
--- a/README.md
+++ b/README.md
@@ -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]
diff --git a/config/karma.config.js b/config/karma.config.js
index 1886c470..f52eb8f7 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -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,
diff --git a/docs/components/TextInput.md b/docs/components/TextInput.md
index 5d53f8f9..50da55ea 100644
--- a/docs/components/TextInput.md
+++ b/docs/components/TextInput.md
@@ -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 (
-
+
);
}
}
const styles = {
+ default: {
+ borderColor: 'gray',
+ borderWidth: '0 0 2px 0'
+ },
+ focused: {
+ borderColor: 'blue'
+ }
}
```
diff --git a/package.json b/package.json
index 215d66f5..22fb2227 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/TextInput/index.js b/src/components/TextInput/index.js
index af36be12..8774ccab 100644
--- a/src/components/TextInput/index.js
+++ b/src/components/TextInput/index.js
@@ -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 (
-
+ 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
+ ?
+ :
)
}
}
diff --git a/src/components/TextInput/index.spec.js b/src/components/TextInput/index.spec.js
index cd4d7ba6..1325adf0 100644
--- a/src/components/TextInput/index.spec.js
+++ b/src/components/TextInput/index.spec.js
@@ -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()
+ assert.equal(dom.getAttribute('autocomplete'), undefined)
+ // on
+ dom = utils.renderToDOM()
+ assert.equal(dom.getAttribute('autocomplete'), 'on')
+ })
+
+ test('prop "autoFocus"', () => {
+ // false
+ let dom = utils.renderToDOM()
+ assert.deepEqual(document.activeElement, document.body)
+ // true
+ dom = utils.renderToDOM()
+ assert.deepEqual(document.activeElement, dom)
+ })
+
+ test('prop "clearTextOnFocus"', () => {
+ const defaultValue = 'defaultValue'
+ utils.requiresFocus(() => {
+ // false
+ let dom = utils.renderAndInject()
+ dom.focus()
+ assert.equal(dom.value, defaultValue)
+ // true
+ dom = utils.renderAndInject()
+ dom.focus()
+ assert.equal(dom.value, '')
+ })
+ })
+
+ test('prop "defaultValue"', () => {
+ const defaultValue = 'defaultValue'
+ const result = utils.shallowRender()
+ assert.equal(result.props.defaultValue, defaultValue)
+ })
+
+ test('prop "editable"', () => {
+ // true
+ let dom = utils.renderToDOM()
+ assert.equal(dom.getAttribute('readonly'), undefined)
+ // false
+ dom = utils.renderToDOM()
+ assert.equal(dom.getAttribute('readonly'), '')
+ })
+
+ test('prop "keyboardType"', () => {
+ // default
+ let dom = utils.renderToDOM()
+ assert.equal(dom.getAttribute('type'), undefined)
+ dom = utils.renderToDOM()
+ assert.equal(dom.getAttribute('type'), undefined)
+ // email-address
+ dom = utils.renderToDOM()
+ assert.equal(dom.getAttribute('type'), 'email')
+ // numeric
+ dom = utils.renderToDOM()
+ assert.equal(dom.getAttribute('type'), 'number')
+ // phone-pad
+ dom = utils.renderToDOM()
+ assert.equal(dom.getAttribute('type'), 'tel')
+ // url
+ dom = utils.renderToDOM()
+ assert.equal(dom.getAttribute('type'), 'url')
+ })
+
+ test('prop "maxLength"', () => {
+ let dom = utils.renderToDOM()
+ assert.equal(dom.getAttribute('maxlength'), undefined)
+ dom = utils.renderToDOM()
+ 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(
+
+ )
+ 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()
+ assert.equal(dom.tagName, 'INPUT')
+ // true
+ dom = utils.renderToDOM()
+ assert.equal(dom.tagName, 'TEXTAREA')
+ })
+
+ test('prop "numberOfLines"', () => {
+ const style = { borderWidth: 0, fontSize: 20, lineHeight: 1 }
+ // missing multiline
+ let dom = utils.renderToDOM()
+ assert.equal(dom.tagName, 'INPUT')
+ // with multiline
+ dom = utils.renderAndInject()
+ 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()
+ ReactTestUtils.Simulate.blur(input)
+ function onBlur(e) {
+ assert.ok(e)
+ done()
+ }
+ })
+
+ test('prop "onChange"', (done) => {
+ const input = utils.renderToDOM()
+ ReactTestUtils.Simulate.change(input)
+ function onChange(e) {
+ assert.ok(e)
+ done()
+ }
+ })
+
+ test('prop "onChangeText"', (done) => {
+ const newText = 'newText'
+ const input = utils.renderToDOM()
+ ReactTestUtils.Simulate.change(input, { target: { value: newText } })
+ function onChangeText(text) {
+ assert.equal(text, newText)
+ done()
+ }
+ })
+
+ test('prop "onFocus"', (done) => {
+ const input = utils.renderToDOM()
+ ReactTestUtils.Simulate.focus(input)
+ function onFocus(e) {
+ assert.ok(e)
+ done()
+ }
+ })
+
+ test.skip('prop "onLayout"', () => {})
+
+ test('prop "onSelectionChange"', (done) => {
+ const input = utils.renderAndInject()
+ 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()
+ assert.equal(dom.getAttribute('type'), 'password')
+ // ignored for multiline
+ dom = utils.renderToDOM()
+ assert.equal(dom.getAttribute('type'), undefined)
+ })
+
+ test('prop "selectTextOnFocus"', () => {
+ const text = 'Text'
+ utils.requiresFocus(() => {
+ // false
+ let dom = utils.renderAndInject()
+ dom.focus()
+ assert.equal(dom.selectionEnd, 0)
+ assert.equal(dom.selectionStart, 0)
+ // true
+ dom = utils.renderAndInject()
+ 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()
+ assert.equal(result.props.value, value)
+ })
})
-*/
diff --git a/src/example.js b/src/example.js
index d60c13bc..1058459d 100644
--- a/src/example.js
+++ b/src/example.js
@@ -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) }}
/>
+
+
-
+
-
-
+
Touchable
)
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)