Compare commits

..

41 Commits

Author SHA1 Message Date
Nicolas Gallagher
fd04d65b03 0.0.12 2015-12-20 03:24:57 -08:00
Nicolas Gallagher
0ab984f507 [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
2015-12-20 03:11:39 -08:00
Nicolas Gallagher
3d1ad50a58 Update documentation and examples 2015-12-19 10:59:22 -08:00
Nicolas Gallagher
92554321df [add] Text: support all ViewStylePropTypes 2015-12-19 10:51:29 -08:00
Nicolas Gallagher
1c9270c4ea [fix] ReactNative export pattern 2015-12-19 10:49:49 -08:00
Nicolas Gallagher
8a5f9cd7d9 0.0.11 2015-12-19 06:04:03 -08:00
Nicolas Gallagher
aac6b796b2 Replace 'EnvironmentPlugin' with 'DefinePlugin' 2015-12-19 05:47:20 -08:00
Nicolas Gallagher
c77ce19f1b [fix] StyleSheet: escaping of class names in CSS
Fix #47
2015-12-19 04:22:00 -08:00
Nicolas Gallagher
25b74d30c4 [fix] Text: style props 2015-12-19 03:57:04 -08:00
Nicolas Gallagher
4191d58694 Fix styles in 'GridView' example 2015-12-19 03:27:44 -08:00
Nicolas Gallagher
11b861ae64 [add] support for 'list' and 'listitem' accessibilityRole mapping
Fix #49
2015-12-19 03:25:40 -08:00
Nicolas Gallagher
68bf08112a [fix] StylePropTypes: add support for 'verticalAlign'
Fix #45
2015-12-19 03:11:55 -08:00
Nicolas Gallagher
b277b3e509 [fix] StylePropTypes: add support for 'boxShadow'
Fix #44
2015-12-19 03:11:12 -08:00
Nicolas Gallagher
c135dddbd1 [fix] StyleSheet: isStyleObject check 2015-12-14 15:28:40 -08:00
Nicolas Gallagher
ffc6368162 0.0.10 2015-12-13 12:49:48 -08:00
Nicolas Gallagher
501c19fe9b [fix] StyleSheet: shorthand properties
Expand shorthand properties to preserve the one-rule-to-one-style
isolation. Resolve styles like React Native - most specific comes last.
Add support for the hz and vt properties for margin and padding.

Fix #40
2015-12-13 12:48:26 -08:00
Kirill Korolyov
e1da11fa1d [change] transparent default Image backgroundColor 2015-12-07 13:33:16 -08:00
Nicolas Gallagher
b2a4d742a9 [chore] update dependencies 2015-12-01 16:01:51 -08:00
Nicolas Gallagher
8b965fdfa0 [chore] update development dependencies 2015-12-01 15:58:04 -08:00
Nicolas Gallagher
8cfef85934 [fix] Image tests 2015-12-01 15:50:38 -08:00
Nicolas Gallagher
6db24e9358 [fix] Image: default 'resizeMode' is 'cover'
Fix #41
2015-12-01 14:44:17 -08:00
Nicolas Gallagher
13e36bee65 0.0.9 2015-10-24 12:06:37 -07:00
Nicolas Gallagher
93e8e90a1a [fix] README docs 2015-10-24 11:54:31 -07:00
Tom Ashworth
894fd0362d [add] initial ScrollView
Supports the following props: `children`, `contentContainerStyle`,
`horizontal`, `onScroll`, `scrollEnabled`, `scrollEventThrottle`, and
`style`.

Fix #6
2015-10-24 11:35:30 -07:00
Nicolas Gallagher
a1664927ce [change] initial example with media queries 2015-10-21 18:00:10 -07:00
Nicolas Gallagher
ae2abc578a [change] update dependencies 2015-10-19 18:45:33 -07:00
Nicolas Gallagher
5f7b3f5fef [change] move examples 2015-10-19 18:32:38 -07:00
Nicolas Gallagher
75f653818a [change] move tests.webpack.js 2015-10-19 18:31:27 -07:00
Nicolas Gallagher
2c52d41b75 [fix] -ms- CSS vendor prefix 2015-10-19 16:02:41 -07:00
Nicolas Gallagher
83f749d983 [change] docs on MQs and a11y 2015-10-19 10:35:42 -07:00
Nicolas Gallagher
bf5046415c [fix] doc examples 2015-10-19 09:42:30 -07:00
Nicolas Gallagher
885d4586a9 [change] obfuscated selector prefix 2015-10-18 22:15:03 -07:00
Nicolas Gallagher
ea0a778ba3 [change] add more about accessibility to docs 2015-10-18 22:11:20 -07:00
Nicolas Gallagher
0a7eda2505 [fix] remove default link styles 2015-10-18 22:09:27 -07:00
Nicolas Gallagher
35385e7b69 [change] pointerEvents CSS 2015-10-18 22:08:36 -07:00
Nicolas Gallagher
3fd29697c0 [fix] initial 'display' value of 'Text'
Using `inline-block` breaks text overflow truncation in nested `Text`
components.

Fix gh-19
2015-10-18 18:08:39 -07:00
Nicolas Gallagher
a26033be2d 0.0.8 2015-10-18 17:56:05 -07:00
Nicolas Gallagher
fdb4ee4aae [change] StyleSheet docs 2015-10-18 17:49:35 -07:00
Nicolas Gallagher
08300f624f [change] remove 'component' prop; accessibility docs
- infer underlying HTML tag from 'accessibilityRole'
- move accessibility props to 'CoreComponent'
- remove the 'component' prop from exported Components

Fix gh-23
2015-10-18 15:24:59 -07:00
Nicolas Gallagher
7f5a2807e2 [fix] avoid eslint-plugin-react@3.6.0 bug 2015-10-18 11:23:29 -07:00
Nicolas Gallagher
292f045c52 [fix] README examples 2015-10-18 11:02:36 -07:00
55 changed files with 1281 additions and 587 deletions

View File

@@ -1,7 +1,7 @@
{
"optional": [
"es7.classProperties",
"runtime"
],
"stage": 1
"presets": [
"es2015",
"stage-1",
"react"
]
}

View File

@@ -2,11 +2,11 @@
[![Build Status][travis-image]][travis-url]
[![npm version][npm-image]][npm-url]
![gzipped size](https://img.shields.io/badge/gzipped-~18.6k-blue.svg)
[React Native][react-native-url] components and APIs for the Web.
~17.7 KB minified and gzipped.
* [Slack: reactiflux channel #react-native-web][slack-url]
* [Discord: #react-native-web on reactiflux][discord-url]
* [Gitter: react-native-web][gitter-url]
## Table of contents
@@ -16,6 +16,7 @@
* [APIs](#apis)
* [Components](#components)
* [Styling](#styling)
* [Accessibility](#accessibility)
* [Contributing](#contributing)
* [Thanks](#thanks)
* [License](#license)
@@ -96,7 +97,9 @@ const css = StyleSheet.renderToString();
const Html = () => (
<html>
<head>
<style id="react-stylesheet">{css}</style>
<meta charSet="utf-8" />
<meta content="initial-scale=1,width=device-width" name="viewport" />
<style id="react-stylesheet" dangerouslySetInnerHTML={{ __html: css } />
</head>
<body>
<div id="react-root" dangerouslySetInnerHTML={{ __html: html }} />
@@ -111,13 +114,13 @@ Render styles on the client:
// client.js
import App from './components/App'
import React, { StyleSheet } from 'react-native-web'
import ReactDOM from 'react-dom'
React.render(
<App />,
document.getElementById('react-root')
)
const reactRoot = document.getElementById('react-root')
const reactStyleSheet = document.getElementById('react-stylesheet')
document.getElementById('stylesheet').textContent = StyleSheet.renderToString()
ReactDOM.render(<App />, reactRoot)
reactStyleSheet.textContent = StyleSheet.renderToString()
```
## APIs
@@ -125,7 +128,8 @@ document.getElementById('stylesheet').textContent = StyleSheet.renderToString()
### [`StyleSheet`](docs/apis/StyleSheet.md)
StyleSheet is a style abstraction that transforms inline styles to CSS on the
client or the server. It provides a minimal CSS reset.
client or the server. It provides a minimal CSS reset targeting elements and
pseudo-elements beyond the reach of React inline styles.
## Components
@@ -138,13 +142,13 @@ and child content.
(TODO)
### [`ScrollView`](docs/components/ListView.md)
### [`ScrollView`](docs/components/ScrollView.md)
(TODO)
A scrollable view with event throttling.
### [`Text`](docs/components/Text.md)
Displays text as an inline block and supports basic press handling.
Displays text inline and supports basic press handling.
### [`TextInput`](docs/components/TextInput.md)
@@ -160,14 +164,56 @@ The fundamental UI building block using flexbox for layout.
## Styling
React Native for Web relies on styles being defined in JavaScript.
React Native for Web relies on styles being defined in JavaScript. Styling
components can be achieved with inline styles or the use of
[StyleSheet](docs/apis/StyleSheet.md).
The `View` component makes it easy to build common layouts with flexbox, such
as stacked and nested boxes with margin and padding. See this [guide to
flexbox][flexbox-guide-url].
Styling components can be achieved with inline styles or the use of
[StyleSheet](docs/apis/StyleSheet.md).
### Media Queries, pseudo-classes, and pseudo-elements
Changing styles and/or the render tree in response to device adaptation can be
controlled in JavaScript, e.g.,
[react-media-queries](https://github.com/bloodyowl/react-media-queries),
[media-query-fascade](https://github.com/tanem/media-query-facade), or
[react-responsive](https://github.com/contra/react-responsive). This has the
benefit of co-locating breakpoint-specific DOM and style changes.
Pseudo-classes like `:hover` and `:focus` can be implemented with the `onHover`
and `onFocus` events.
Pseudo-elements are not supported.
## Accessibility
On the Web, assistive technologies derive useful information about the
structure, purpose, and interactivity of apps from their [HTML
elements][html-accessibility-url], attributes, and [ARIA in
HTML][aria-in-html-url].
The most common and best supported accessibility features of the Web are
exposed as the props: `accessible`, `accessibilityLabel`,
`accessibilityLiveRegion`, and `accessibilityRole`.
React Native for Web does not provide a way to directly control the rendered
HTML element. The `accessibilityRole` prop is used to infer an [analogous HTML
element][html-aria-url] to use in addition, where possible. While this may
contradict some ARIA recommendations, it also helps avoid certain HTML5
conformance errors and accessibility anti-patterns (e.g., giving a `heading`
role to a `button` element).
For example:
* `<View accessibilityRole='article' />` => `<article role='article' />`.
* `<View accessibilityRole='banner' />` => `<header role='banner' />`.
* `<View accessibilityRole='button' />` => `<button type='button' role='button' />`.
* `<Text accessibilityRole='link' href='/' />` => `<a role='link' href='/' />`.
* `<View accessibilityRole='main' />` => `<main role='main' />`.
See the component documentation for more details.
## Contributing
@@ -177,7 +223,7 @@ welcome!
## Thanks
Thanks to current and past members of the React and React Native teams (in
particular Vjeux and Pete Hunt), and Tobias Koppers for Webpack and CSS loader.
particular Vjeux and Pete Hunt).
Thanks to [react-tappable](https://github.com/JedWatson/react-tappable) for
backing the current implementation of `Touchable`.
@@ -187,12 +233,15 @@ backing the current implementation of `Touchable`.
Copyright (c) 2015 Nicolas Gallagher. Released under the [MIT
license](http://www.opensource.org/licenses/mit-license.php).
[aria-in-html-url]: https://w3c.github.io/aria-in-html/
[contributing-url]: https://github.com/necolas/react-native-web/blob/master/CONTRIBUTING.md
[discord-url]: http://join.reactiflux.com
[flexbox-guide-url]: https://css-tricks.com/snippets/css/a-guide-to-flexbox/
[gitter-url]: https://gitter.im/necolas/react-native-web
[html-accessibility-url]: http://www.html5accessibility.com/
[html-aria-url]: http://www.w3.org/TR/html-aria/
[npm-image]: https://badge.fury.io/js/react-native-web.svg
[npm-url]: https://npmjs.org/package/react-native-web
[react-native-url]: https://facebook.github.io/react-native/
[slack-url]: https://reactiflux.slack.com/messages/react-native-web/
[travis-image]: https://travis-ci.org/necolas/react-native-web.svg?branch=master
[travis-url]: https://travis-ci.org/necolas/react-native-web

View File

@@ -4,7 +4,8 @@ var ROOT = path.join(__dirname, '..')
module.exports = {
DIST_DIRECTORY: path.join(ROOT, 'dist'),
EXAMPLES_DIRECTORY: path.join(ROOT, 'examples'),
SRC_DIRECTORY: path.join(ROOT, 'src'),
ROOT_DIRECTORY: ROOT,
TEST_ENTRY: path.join(ROOT, 'src/tests.webpack.js')
TEST_ENTRY: path.join(ROOT, 'tests.webpack.js')
}

View File

@@ -19,14 +19,14 @@ module.exports = function (config) {
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-mocha',
'karma-mocha-reporter',
'karma-sourcemap-loader',
'karma-spec-reporter',
'karma-webpack'
],
preprocessors: {
[constants.TEST_ENTRY]: [ 'webpack', 'sourcemap' ]
},
reporters: process.env.TRAVIS ? [ 'dots' ] : [ 'mocha' ],
reporters: process.env.TRAVIS ? [ 'dots' ] : [ 'spec' ],
singleRun: true,
webpack: {
devtool: 'inline-source-map',

View File

@@ -1,6 +1,7 @@
var webpack = require('webpack')
var DedupePlugin = webpack.optimize.DedupePlugin
var DefinePlugin = webpack.DefinePlugin
var OccurenceOrderPlugin = webpack.optimize.OccurenceOrderPlugin
var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin
@@ -10,6 +11,11 @@ var plugins = [
]
if (process.env.NODE_ENV === 'production') {
plugins.push(
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
)
plugins.push(
new UglifyJsPlugin({
compress: {

View File

@@ -5,13 +5,13 @@ var path = require('path')
module.exports = assign({}, base, {
devServer: {
contentBase: constants.SRC_DIRECTORY
contentBase: constants.EXAMPLES_DIRECTORY
},
entry: {
example: path.join(constants.SRC_DIRECTORY, 'example')
example: path.join(constants.EXAMPLES_DIRECTORY, 'index')
},
output: {
filename: 'example.js',
filename: 'examples.js',
path: constants.DIST_DIRECTORY
}
})

View File

@@ -1,21 +1,13 @@
# StyleSheet
React Native for Web will automatically vendor-prefix styles applied to the
libraries components. The `StyleSheet` abstraction converts predefined styles
library's components. The `StyleSheet` abstraction converts predefined styles
to CSS without a compile-time step. Some styles cannot be resolved outside of
the render loop and are applied as inline styles.
The `style`-to-`className` conversion strategy is optimized to minimize the
amount of CSS required. Unique declarations are defined using "atomic" CSS a
unique class name for a unique declaration.
React Native for Web includes a CSS reset to remove unwanted user agent styles
from elements and pseudo-elements beyond the reach of React (e.g., `html` and
`body`).
Create a new StyleSheet:
```
```js
const styles = StyleSheet.create({
container: {
borderRadius: 4,
@@ -55,17 +47,53 @@ StyleSheet.renderToString()
**create**(obj: {[key: string]: any})
**destroy**()
Clears all style information.
**renderToString**()
## Strategy
Renders a CSS Style Sheet.
Mapping entire `style` objects to CSS rules can lead to increasingly large CSS
files. Each new component adds new rules to the stylesheet.
## About
![](../static/styling-strategy.png)
### Strategy
React Native for Web uses an alternative strategy: mapping declarations to
declarations.
React Native for Web uses a `style`-to-`className` conversion strategy that is
designed to avoid issues arising from the [7 deadly sins of
CSS](https://speakerdeck.com/vjeux/react-css-in-js):
1. Global namespace
2. Dependency hell
3. Dead code elimination
4. Code minification
5. Sharing constants
6. Non-deterministic resolution
7. Breaking isolation
The strategy also minimizes the amount of generated CSS, making it more viable
to inline the style sheet when pre-rendering pages on the server. There is one
unique selector per unique style _declaration_.
```js
// definition
{
heading: {
color: 'gray',
fontSize: '2rem'
},
text: {
color: 'gray',
fontSize: '1.25rem'
}
}
// css
//
// .a { color: gray; }
// .b { font-size: 2rem; }
// .c { font-size: 1.25rem; }
```
For example:
@@ -102,16 +130,36 @@ In production the class names are obfuscated.
(CSS libraries like [Atomic CSS](http://acss.io/),
[Basscss](http://www.basscss.com/), [SUIT CSS](https://suitcss.github.io/), and
[tachyons](http://tachyons.io/) are attempts to limit style scope and limit
stylesheet growth in a similar way. But they're CSS utility libraries, each with a
style sheet growth in a similar way. But they're CSS utility libraries, each with a
particular set of classes and features to learn. All of them require developers
to manually connect CSS classes for given styles.)
## Media Queries, pseudo-classes, and pseudo-elements
### Reset
Media Queries in JavaScript can be used to modify the render tree and styles.
This has the benefit of co-locating breakpoint-specific DOM and style changes.
React Native for Web includes a very small CSS reset taken from
[normalize.css](https://necolas.github.io/normalize.css/). It removes unwanted
User Agent styles from (pseudo-)elements beyond the reach of React (e.g.,
`html`, `body`) or inline styles (e.g., `::-moz-focus-inner`).
Pseudo-classes like `:hover` and `:focus` can be replaced with JavaScript
events.
```css
html {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
Pseudo-elements are not supported.
body {
margin: 0;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
```

View File

@@ -57,14 +57,14 @@ could be an http address or a base64 encoded image.
**style**: style
[View](View.md) style
+ ...[View#style](View.md)
Defaults:
```js
{
alignSelf: 'flex-start',
backgroundColor: 'lightGray'
backgroundColor: 'transparent'
}
```
@@ -76,11 +76,9 @@ Used to locate a view in end-to-end tests.
```js
import placeholderAvatar from './placeholderAvatar.png'
import React, { Image } from 'react-native-web'
import React, { Component, Image, PropTypes, StyleSheet } from 'react-native-web'
const { Component, PropTypes } = React;
class Avatar extends Component {
export default class ImageExample extends Component {
constructor(props, context) {
super(props, context)
this.state = { loading: true }
@@ -112,32 +110,36 @@ class Avatar extends Component {
onLoad={this._onLoad.bind(this)}
resizeMode='cover'
source={{ uri: user.avatarUrl }}
style={{ ...styles.base, ...styles[size], ...loadingStyle }}
style={{
...styles.base,
...styles[size],
...loadingStyle
}}
/>
)
}
}
const styles = {
const styles = StyleSheet.create({
base: {
borderColor: 'white',
borderRadius: '5px',
borderWidth: '5px'
borderRadius: 5,
borderWidth: 5
},
loading: {
opacity: 0.5
},
small: {
height: '32px',
width: '32px'
height: 32,
width: 32
},
normal: {
height: '48px',
width: '48px'
height: 48,
width: 48
},
large: {
height: '64px',
width: '64px'
height: 64,
width: 64
}
}
})
```

View File

@@ -10,28 +10,17 @@ Content to display over the image.
**style**: style
+ `property` type
Defaults:
```js
{
}
```
+ ...[View#style](View.md)
## Examples
```js
import React, { ListView } from 'react-native-web'
import React, { Component, ListView, PropTypes } from 'react-native-web'
const { Component, PropTypes } = React;
export default class ListViewExample extends Component {
static propTypes = {}
class Example extends Component {
static propTypes = {
}
static defaultProps = {
}
static defaultProps = {}
render() {
return (

View File

@@ -1,6 +1,7 @@
# ScrollView
TODO
Scrollable `View` for use with bounded height, either by setting the height of
the view directly (discouraged) or by bounding the height of ancestor views.
## Props
@@ -11,49 +12,72 @@ Child content.
**contentContainerStyle**: style
These styles will be applied to the scroll view content container which wraps
all of the child views. Example:
all of the child views.
**horizontal**: bool = false
When true, the scroll view's children are arranged horizontally in a row instead of vertically in a column. Default: `false`.
When true, the scroll view's children are arranged horizontally in a row
instead of vertically in a column.
**onScroll**: function
Fires at most once per frame during scrolling. The frequency of the events can be contolled using the `scrollEventThrottle` prop.
Fires at most once per frame during scrolling. The frequency of the events can
be contolled using the `scrollEventThrottle` prop.
**scrollEnabled**: bool
**scrollEnabled**: bool = true
When false, the content does not scroll. Default: `true`.
When false, the content does not scroll.
**scrollEventThrottle**: number
**scrollEventThrottle**: number = 0
This controls how often the scroll event will be fired while scrolling (in
events per seconds). A higher number yields better accuracy for code that is
tracking the scroll position, but can lead to scroll performance problems.
Default: `0` (the scroll event will be sent only once each time the view is
scrolled.)
tracking the scroll position, but can lead to scroll performance problems. The
default value is `0`, which means the scroll event will be sent only once each
time the view is scrolled.
**style**: style
[View](View.md) style
+ ...[View#style](View.md)
## Examples
```js
import React, { ScrollView } from 'react-native-web'
import React, { Component, ScrollView, StyleSheet } from 'react-native-web'
import Item from './Item'
const { Component, PropTypes } = React;
class Example extends Component {
static propTypes = {
export default class ScrollViewExample extends Component {
constructor(props, context) {
super(props, context)
this.state = {
items: Array.from({ length: 20 }).map((_, i) => ({ id: i }))
}
}
static defaultProps = {
onScroll(e) {
console.log(e)
}
render() {
return (
<ScrollView
children={this.state.items.map((item) => <Item {...item} />)}
contentContainerStyle={styles.container}
horizontal
onScroll={(e) => this.onScroll(e)}
scrollEventThrottle={60}
style={styles.root}
/>
)
}
}
const styles = StyleSheet.create({
root: {
borderWidth: 1
},
container: {
padding: 10
}
})
```

View File

@@ -1,13 +1,11 @@
# Text
`Text` is component for displaying text. It supports style, basic touch
handling, and inherits typographic styles from ancestor elements. In a
divergence from React Native, components other than `Text` can be children of a
`Text` component.
handling, and inherits typographic styles from ancestor elements.
The `Text` is unique relative to layout: child elements use text layout
(`inline-block`) rather than flexbox layout. This means that elements inside of
a `Text` are not rectangles, as they wrap when reaching the edge of their
(`inline`) rather than flexbox layout. This means that elements inside of a
`Text` are not rectangles, as they wrap when reaching the edge of their
container.
Unsupported React Native props:
@@ -23,6 +21,17 @@ NOTE: `Text` will transfer all other props to the rendered HTML element.
Defines the text available to assistive technologies upon interaction with the
element. (This is implemented using `aria-label`.)
(web) **accessibilityRole**: oneOf(roles)
Allows assistive technologies to present and support interaction with the view
in a manner that is consistent with user expectations for similar views of that
type. For example, marking a touchable view with an `accessibilityRole` of
`button`. (This is implemented using [ARIA roles](http://www.w3.org/TR/wai-aria/roles#role_definitions)).
Note: Avoid changing `accessibilityRole` values over time or after user
actions. Generally, accessibility APIs do not provide a means of notifying
assistive technologies of a `role` value change.
(web) **accessible**: bool = true
When `false`, the text is hidden from assistive technologies. (This is
@@ -32,10 +41,6 @@ implemented using `aria-hidden`.)
Child content.
(web) **component**: function | string = 'span'
Backing component.
**numberOfLines**: number
Truncates the text with an ellipsis after this many lines. Currently only supports `1`.
@@ -46,22 +51,20 @@ This function is called on press.
**style**: style
+ `backgroundColor`
+ ...[View#style](View.md)
+ `color`
+ `direction`
+ `fontFamily`
+ `fontSize`
+ `fontStyle`
+ `fontWeight`
+ `letterSpacing`
+ `lineHeight`
+ `margin`
+ `padding`
+ `textAlign`
+ `textDecoration`
+ `textTransform`
+ `whiteSpace`
+ `wordWrap`
+ `writingDirection`
**testID**: string
@@ -70,18 +73,18 @@ Used to locate this view in end-to-end tests.
## Examples
```js
import React, { Text } from 'react-native-web'
import React, { Component, PropTypes, StyleSheet, Text } from 'react-native-web'
const { Component, PropTypes } = React
class PrettyText extends Component {
export default class PrettyText extends Component {
static propTypes = {
...Text.propTypes,
color: PropTypes.oneOf(['white', 'gray', 'red']),
size: PropTypes.oneOf(['small', 'normal', 'large']),
weight: PropTypes.oneOf(['light', 'normal', 'bold'])
}
static defaultProps = {
...Text.defaultProps,
color: 'gray',
size: 'normal',
weight: 'normal'
@@ -95,16 +98,16 @@ class PrettyText extends Component {
...other
style={{
...style,
...localStyle.color[color],
...localStyle.size[size],
...localStyle.weight[weight]
...styles.color[color],
...styles.size[size],
...styles.weight[weight]
}}
/>
);
}
}
const localStyle = {
const styles = StyleSheet.create({
color: {
white: { color: 'white' },
gray: { color: 'gray' },
@@ -120,5 +123,5 @@ const localStyle = {
normal: { fontWeight: '400' },
bold: { fontWeight: '700' }
}
}
})
```

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,19 +132,8 @@ If `true`, all text will automatically be selected on focus.
**style**: style
[View](View.md) style
+ `color`
+ `direction`
+ `fontFamily`
+ `fontSize`
+ `fontStyle`
+ `fontWeight`
+ `letterSpacing`
+ `lineHeight`
+ `textAlign`
+ `textDecoration`
+ `textTransform`
+ ...[Text#style](Text.md)
+ `outline`
**testID**: string
@@ -159,16 +150,18 @@ user edits to the value set `editable={false}`.
## Examples
```js
import React, { TextInput } from 'react-native-web'
import React, { Component, StyleSheet, TextInput } from 'react-native-web'
const { Component } = React
class AppTextInput extends Component {
export default class TextInputExample extends Component {
constructor(props, context) {
super(props, context)
this.state = { isFocused: false }
}
_onBlur(e) {
this.setState({ isFocused: false })
}
_onFocus(e) {
this.setState({ isFocused: true })
}
@@ -180,24 +173,25 @@ class AppTextInput extends Component {
maxNumberOfLines={5}
multiline
numberOfLines={2}
onBlur={this._onBlur.bind(this)}
onFocus={this._onFocus.bind(this)}
placeholder={`What's happening?`}
style={
style={{
...styles.default
...(this.state.isFocused && styles.focused)
}
}}
/>
);
}
}
const styles = {
const styles = StyleSheet.create({
default: {
borderColor: 'gray',
borderWidth: '0 0 2px 0'
borderBottomWidth: 2
},
focused: {
borderColor: 'blue'
}
}
})
```

View File

@@ -79,21 +79,17 @@ Delay in ms, from the release of the touch, before `onPressOut` is called.
**style**: style
[View](View.md) style
+ ...[View#style](View.md)
## Examples
```js
import React, { Touchable } from 'react-native-web'
import React, { Component, PropTypes, Touchable } from 'react-native-web'
const { Component, PropTypes } = React;
export default class Example extends Component {
static propTypes = {}
class Example extends Component {
static propTypes = {
}
static defaultProps = {
}
static defaultProps = {}
render() {
return (

View File

@@ -54,10 +54,6 @@ assistive technologies of a `role` value change.
When `false`, the view is hidden from assistive technologies. (This is
implemented using `aria-hidden`.)
(web) **component**: function | string = 'div'
The React Component for this view.
**onLayout**: function
(TODO)
@@ -113,6 +109,12 @@ from `style`.
+ `justifyContent`
+ `left`
+ `margin`
+ `marginBottom`
+ `marginHorizontal`
+ `marginLeft`
+ `marginRight`
+ `marginTop`
+ `marginVertical`
+ `maxHeight`
+ `maxWidth`
+ `minHeight`
@@ -123,6 +125,12 @@ from `style`.
+ `overflowX`
+ `overflowY`
+ `padding`
+ `paddingBottom`
+ `paddingHorizontal`
+ `paddingLeft`
+ `paddingRight`
+ `paddingTop`
+ `paddingVertical`
+ `position`
+ `right`
+ `top`
@@ -159,11 +167,9 @@ Used to locate this view in end-to-end tests.
## Examples
```js
import React, { View } from 'react-native-web'
import React, { Component, PropTypes, StyleSheet, View } from 'react-native-web'
const { Component, PropTypes } = React
class Example extends Component {
export default class ViewExample extends Component {
render() {
return (
<View style={styles.row}>
@@ -177,14 +183,12 @@ class Example extends Component {
}
}
const styles = {
const styles = StyleSheet.create({
row: {
flexDirection: 'row'
},
cell: {
flexGrow: 1
}
}
export default Example
})
```

View File

@@ -1,50 +1,42 @@
import React, { Image, StyleSheet, Text, TextInput, Touchable, View } from '.'
import ReactDOM from 'react-dom'
import GridView from './GridView'
import Heading from './Heading'
import MediaQueryWidget from './MediaQueryWidget'
import React, { Image, StyleSheet, ScrollView, Text, TextInput, Touchable, View } from '../../src'
const Heading = ({ children, level = '1', size = 'normal' }) => (
<Text
children={children}
component={`h${level}`}
style={headingStyles.size[size]}
/>
)
const headingStyles = StyleSheet.create({
size: {
xlarge: {
fontSize: '2rem',
marginBottom: '1em'
},
large: {
fontSize: '1.5rem',
marginBottom: '1em',
marginTop: '1em'
},
normal: {
fontSize: '1.25rem',
marginBottom: '0.5em',
marginTop: '0.5em'
}
}
})
class Example extends React.Component {
export default class App extends React.Component {
static propTypes = {
mediaQuery: React.PropTypes.object,
style: View.propTypes.style
}
constructor(...args) {
super(...args)
this.state = {
scrollEnabled: true
}
}
render() {
const { mediaQuery } = this.props
const rootStyles = {
...(styles.root.common),
...(mediaQuery.small.matches && styles.root.mqSmall),
...(mediaQuery.large.matches && styles.root.mqLarge)
}
return (
<View accessibilityRole='main' style={styles.root}>
<Heading level='1' size='xlarge'>React Native Web</Heading>
<View accessibilityRole='main' style={rootStyles}>
<Heading size='xlarge'>React Native for Web</Heading>
<Text>React Native Web takes the core components from <Text
component='a' href='https://facebook.github.io/react-native/'>React
accessibilityRole='link' href='https://facebook.github.io/react-native/'>React
Native</Text> and brings them to the web. These components provide
simple building blocks touch handling, flexbox layout,
scroll views from which more complex components and apps can be
constructed.</Text>
<Heading level='2' size='large'>Image</Heading>
<MediaQueryWidget mediaQuery={mediaQuery} />
<Heading size='large'>Image</Heading>
<Image
accessibilityLabel='accessible image'
children={<Text>Inner content</Text>}
@@ -67,7 +59,7 @@ class Example extends React.Component {
testID='Example.image'
/>
<Heading level='2' size='large'>Text</Heading>
<Heading size='large'>Text</Heading>
<Text
onPress={(e) => { console.log('Text.onPress', e) }}
testID={'Example.text'}
@@ -92,7 +84,7 @@ class Example extends React.Component {
hendrerit consequat.
</Text>
<Heading level='2' size='large'>TextInput</Heading>
<Heading size='large'>TextInput</Heading>
<TextInput
keyboardType='default'
onBlur={(e) => { console.log('TextInput.onBlur', e) }}
@@ -103,10 +95,10 @@ class Example 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}
@@ -114,7 +106,7 @@ class Example extends React.Component {
numberOfLines={5}
/>
<Heading level='2' size='large'>Touchable</Heading>
<Heading size='large'>Touchable</Heading>
<Touchable
accessibilityLabel={'Touchable element'}
activeHighlight='lightblue'
@@ -129,8 +121,8 @@ class Example extends React.Component {
</View>
</Touchable>
<Heading level='2' size='large'>View</Heading>
<Heading level='3'>Default layout</Heading>
<Heading size='large'>View</Heading>
<Heading>Default layout</Heading>
<View>
{[ 1, 2, 3, 4, 5, 6 ].map((item, i) => {
return (
@@ -141,7 +133,7 @@ class Example extends React.Component {
})}
</View>
<Heading level='3'>Row layout</Heading>
<Heading>Row layout</Heading>
<View style={styles.row}>
{[ 1, 2, 3, 4, 5, 6 ].map((item, i) => {
return (
@@ -152,13 +144,13 @@ class Example extends React.Component {
})}
</View>
<Heading level='3'>pointerEvents</Heading>
<View style={styles.row}>
<Heading>pointerEvents</Heading>
<GridView alley='10px'>
{['box-none', 'box-only', 'none'].map((value, i) => {
return (
<View
accessibilityRole='link'
children={value}
component='a'
href='https://google.com'
key={i}
pointerEvents={value}
@@ -166,6 +158,52 @@ class Example extends React.Component {
/>
)
})}
</GridView>
<Heading size='large'>ScrollView</Heading>
<label>
<input
checked={this.state.scrollEnabled}
onChange={() => this.setState({
scrollEnabled: !this.state.scrollEnabled
})}
type='checkbox'
/> Enable scroll
</label>
<Heading>Default layout</Heading>
<View style={styles.scrollViewContainer}>
<ScrollView
contentContainerStyle={styles.scrollViewContentContainerStyle}
onScroll={e => console.log('ScrollView.onScroll', e)}
scrollEnabled={this.state.scrollEnabled}
scrollEventThrottle={1} // 1 event per second
style={styles.scrollViewStyle}
>
{Array.from({ length: 50 }).map((item, i) => (
<View key={i} style={styles.box}>
<Text>{i}</Text>
</View>
))}
</ScrollView>
</View>
<Heading>Horizontal layout</Heading>
<View style={styles.scrollViewContainer}>
<ScrollView
contentContainerStyle={styles.scrollViewContentContainerStyle}
horizontal
onScroll={e => console.log('ScrollView.onScroll', e)}
scrollEnabled={this.state.scrollEnabled}
scrollEventThrottle={1} // 1 event per second
style={styles.scrollViewStyle}
>
{Array.from({ length: 50 }).map((item, i) => (
<View key={i} style={{...styles.box, ...styles.horizontalBox}}>
<Text>{i}</Text>
</View>
))}
</ScrollView>
</View>
</View>
)
@@ -174,8 +212,16 @@ class Example extends React.Component {
const styles = StyleSheet.create({
root: {
maxWidth: '600px',
margin: '0 auto'
common: {
marginVertical: 0,
marginHorizontal: 'auto'
},
mqSmall: {
maxWidth: '400px'
},
mqLarge: {
maxWidth: '600px'
}
},
row: {
flexDirection: 'row',
@@ -185,7 +231,10 @@ const styles = StyleSheet.create({
alignItems: 'center',
flexGrow: 1,
justifyContent: 'center',
borderWidth: '1px'
borderWidth: 1
},
horizontalBox: {
width: '50px'
},
boxFull: {
width: '100%'
@@ -202,9 +251,14 @@ const styles = StyleSheet.create({
borderWidth: 1,
height: '200px',
justifyContent: 'center'
},
scrollViewContainer: {
height: '200px'
},
scrollViewStyle: {
borderWidth: '1px'
},
scrollViewContentContainerStyle: {
padding: '10px'
}
})
ReactDOM.render(<Example />, document.getElementById('react-root'))
document.getElementById('react-stylesheet').textContent = StyleSheet.renderToString()

View File

@@ -0,0 +1,65 @@
import React, { Component, PropTypes, StyleSheet, View } from '../../src'
const styles = StyleSheet.create({
root: {
overflow: 'hidden'
},
contentContainer: {
flexDirection: 'row',
flexGrow: 1
},
// distribute all space (rather than extra space)
column: {
flexBasis: '0%'
}
})
export default class GridView extends Component {
static propTypes = {
alley: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.arrayOf(PropTypes.element)
]),
gutter: PropTypes.string,
style: PropTypes.object
}
static defaultProps = {
alley: '0',
gutter: '0'
}
render() {
const { alley, children, gutter, style, ...other } = this.props
const rootStyle = {
...style,
...styles.root
}
const contentContainerStyle = {
...styles.contentContainer,
marginHorizontal: `calc(-0.5 * ${alley})`,
paddingHorizontal: `${gutter}`
}
const newChildren = React.Children.map(children, (child) => {
return child && React.cloneElement(child, {
style: {
...child.props.style,
...styles.column,
marginHorizontal: `calc(0.5 * ${alley})`
}
})
})
return (
<View className='GridView' {...other} style={rootStyle}>
<View style={contentContainerStyle}>
{newChildren}
</View>
</View>
)
}
}

View File

@@ -0,0 +1,33 @@
import React, { StyleSheet, Text } from '../../src'
const styles = StyleSheet.create({
root: {
fontFamily: '"Helvetica Neue", arial, sans-serif'
},
size: {
xlarge: {
fontSize: '2rem',
marginBottom: '1em'
},
large: {
fontSize: '1.5rem',
marginBottom: '1em',
marginTop: '1em'
},
normal: {
fontSize: '1.25rem',
marginBottom: '0.5em',
marginTop: '0.5em'
}
}
})
const Heading = ({ children, size = 'normal' }) => (
<Text
accessibilityRole='heading'
children={children}
style={{ ...styles.root, ...styles.size[size] }}
/>
)
export default Heading

View File

@@ -0,0 +1,36 @@
import React, { StyleSheet, Text, View } from '../../src'
const styles = StyleSheet.create({
root: {
alignItems: 'center',
borderWidth: 1,
marginVertical: 10,
padding: 10,
textAlign: 'center'
},
heading: {
fontWeight: 'bold',
padding: 5
}
})
const MediaQueryWidget = ({ mediaQuery = {} }) => {
const active = Object.keys(mediaQuery).reduce((active, alias) => {
if (mediaQuery[alias].matches) {
active = {
alias,
mql: mediaQuery[alias]
}
}
return active
}, {})
return (
<View style={styles.root}>
<Text style={styles.heading}>Active Media Query</Text>
<Text>{`"${active.alias}"`} {active.mql && active.mql.media}</Text>
</View>
)
}
export default MediaQueryWidget

View File

@@ -3,7 +3,6 @@
<title>React Native for Web</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="The core React Native components adapted and expanded upon for the web">
<style>html { font-family: sans-serif; }</style>
<style id="react-stylesheet"></style>
<div id="react-root"></div>
<script src="/example.js"></script>
<script src="/examples.js"></script>

22
examples/index.js Normal file
View File

@@ -0,0 +1,22 @@
import { MediaProvider, matchMedia } from 'react-media-queries'
import App from './components/App'
import createGetter from 'react-media-queries/lib/createMediaQueryGetter'
import createListener from 'react-media-queries/lib/createMediaQueryListener'
import React, { StyleSheet } from '../src'
import ReactDOM from 'react-dom'
const mediaQueries = {
small: '(min-width: 300px)',
medium: '(min-width: 400px)',
large: '(min-width: 500px)'
}
const ResponsiveApp = matchMedia()(App)
ReactDOM.render(
<MediaProvider getMedia={createGetter(mediaQueries)} listener={createListener(mediaQueries)}>
<ResponsiveApp />
</MediaProvider>,
document.getElementById('react-root')
)
document.getElementById('react-stylesheet').textContent = StyleSheet.renderToString()

View File

@@ -1,6 +1,6 @@
{
"name": "react-native-web",
"version": "0.0.7",
"version": "0.0.12",
"description": "React Native for Web",
"main": "dist/react-native-web.js",
"files": [
@@ -9,43 +9,48 @@
"scripts": {
"build": "rm -rf ./dist && webpack --config config/webpack.config.publish.js --sort-assets-by --progress",
"examples": "webpack-dev-server --config config/webpack.config.example.js --inline --hot --colors --quiet",
"lint": "eslint config src",
"prepublish": "NODE_ENV=publish npm run build",
"lint": "eslint config examples src",
"prepublish": "npm run build",
"test": "npm run lint && npm run test:unit",
"test:unit": "karma start config/karma.config.js",
"test:watch": "npm run test:unit -- --no-single-run"
},
"dependencies": {
"inline-style-prefixer": "^0.3.3",
"inline-style-prefixer": "^0.5.3",
"lodash.debounce": "^3.1.1",
"react-tappable": "^0.7.1",
"react-textarea-autosize": "^3.0.0"
"react-textarea-autosize": "^3.1.0"
},
"devDependencies": {
"babel-core": "^5.8.23",
"babel-eslint": "^4.1.1",
"babel-loader": "^5.3.2",
"babel-runtime": "^5.8.20",
"eslint": "^1.3.1",
"eslint-config-standard": "^4.3.1",
"eslint-config-standard-react": "^1.0.4",
"eslint-plugin-react": "^3.3.1",
"eslint-plugin-standard": "^1.3.0",
"karma": "^0.13.9",
"karma-browserstack-launcher": "^0.1.5",
"karma-chrome-launcher": "^0.2.0",
"karma-firefox-launcher": "^0.1.6",
"karma-mocha": "^0.2.0",
"karma-mocha-reporter": "^1.1.1",
"karma-sourcemap-loader": "^0.3.5",
"babel-core": "^6.2.4",
"babel-eslint": "^4.1.6",
"babel-loader": "^6.2.0",
"babel-preset-es2015": "^6.2.4",
"babel-preset-react": "^6.2.4",
"babel-preset-stage-1": "^6.2.4",
"babel-runtime": "^6.2.4",
"eslint": "^1.10.3",
"eslint-config-standard": "^4.4.0",
"eslint-config-standard-react": "^1.2.1",
"eslint-plugin-react": "^3.11.2",
"eslint-plugin-standard": "^1.3.1",
"karma": "^0.13.15",
"karma-browserstack-launcher": "^0.1.7",
"karma-chrome-launcher": "^0.2.2",
"karma-firefox-launcher": "^0.1.7",
"karma-mocha": "^0.2.1",
"karma-sourcemap-loader": "^0.3.6",
"karma-spec-reporter": "0.0.23",
"karma-webpack": "^1.7.0",
"mocha": "^2.3.0",
"node-libs-browser": "^0.5.2",
"mocha": "^2.3.4",
"node-libs-browser": "^0.5.3",
"object-assign": "^4.0.1",
"react": "^0.14.0",
"react-addons-test-utils": "^0.14.0",
"react-dom": "^0.14.0",
"webpack": "^1.12.1",
"webpack-dev-server": "^1.10.1"
"react": "^0.14.3",
"react-addons-test-utils": "^0.14.3",
"react-dom": "^0.14.3",
"react-media-queries": "^2.0.1",
"webpack": "^1.12.9",
"webpack-dev-server": "^1.14.0"
},
"author": "Nicolas Gallagher",
"license": "MIT",

View File

@@ -0,0 +1,62 @@
/* eslint-env mocha */
import * as utils from '../../../modules/specHelpers'
import assert from 'assert'
import React from 'react'
import CoreComponent from '../'
suite('components/CoreComponent', () => {
test('prop "accessibilityLabel"', () => {
const accessibilityLabel = 'accessibilityLabel'
const dom = utils.renderToDOM(<CoreComponent accessibilityLabel={accessibilityLabel} />)
assert.equal(dom.getAttribute('aria-label'), accessibilityLabel)
})
test('prop "accessibilityLiveRegion"', () => {
const accessibilityLiveRegion = 'polite'
const dom = utils.renderToDOM(<CoreComponent accessibilityLiveRegion={accessibilityLiveRegion} />)
assert.equal(dom.getAttribute('aria-live'), accessibilityLiveRegion)
})
test('prop "accessibilityRole"', () => {
const accessibilityRole = 'banner'
let dom = utils.renderToDOM(<CoreComponent accessibilityRole={accessibilityRole} />)
assert.equal(dom.getAttribute('role'), accessibilityRole)
assert.equal((dom.tagName).toLowerCase(), 'header')
const button = 'button'
dom = utils.renderToDOM(<CoreComponent accessibilityRole={button} />)
assert.equal(dom.getAttribute('type'), button)
assert.equal((dom.tagName).toLowerCase(), button)
})
test('prop "accessible"', () => {
// accessible (implicit)
let dom = utils.renderToDOM(<CoreComponent />)
assert.equal(dom.getAttribute('aria-hidden'), null)
// accessible (explicit)
dom = utils.renderToDOM(<CoreComponent accessible />)
assert.equal(dom.getAttribute('aria-hidden'), null)
// not accessible
dom = utils.renderToDOM(<CoreComponent accessible={false} />)
assert.equal(dom.getAttribute('aria-hidden'), 'true')
})
test('prop "component"', () => {
const component = 'main'
const dom = utils.renderToDOM(<CoreComponent component={component} />)
const tagName = (dom.tagName).toLowerCase()
assert.equal(tagName, component)
})
test('prop "testID"', () => {
// no testID
let dom = utils.renderToDOM(<CoreComponent />)
assert.equal(dom.getAttribute('data-testid'), null)
// with testID
const testID = 'Example.testID'
dom = utils.renderToDOM(<CoreComponent testID={testID} />)
assert.equal(dom.getAttribute('data-testid'), testID)
})
})

View File

@@ -2,18 +2,40 @@ import React, { PropTypes } from 'react'
import StylePropTypes from '../../modules/StylePropTypes'
import StyleSheet from '../../modules/StyleSheet'
const roleComponents = {
article: 'article',
banner: 'header',
button: 'button',
complementary: 'aside',
contentinfo: 'footer',
form: 'form',
heading: 'h1',
link: 'a',
list: 'ul',
listitem: 'li',
main: 'main',
navigation: 'nav',
region: 'section'
}
class CoreComponent extends React.Component {
static propTypes = {
accessibilityLabel: PropTypes.string,
accessibilityLiveRegion: PropTypes.oneOf(['assertive', 'off', 'polite']),
accessibilityRole: PropTypes.string,
accessible: PropTypes.bool,
className: PropTypes.string,
component: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string
]),
style: PropTypes.object,
testID: PropTypes.string
testID: PropTypes.string,
type: PropTypes.string
}
static defaultProps = {
accessible: true,
component: 'div'
}
@@ -21,16 +43,28 @@ class CoreComponent extends React.Component {
render() {
const {
component: Component,
accessibilityLabel,
accessibilityLiveRegion,
accessibilityRole,
accessible,
component,
testID,
type,
...other
} = this.props
const Component = roleComponents[accessibilityRole] || component
return (
<Component
{...other}
{...StyleSheet.resolve(other)}
aria-hidden={accessible ? null : true}
aria-label={accessibilityLabel}
aria-live={accessibilityLiveRegion}
data-testid={testID}
role={accessibilityRole}
type={accessibilityRole === 'button' ? 'button' : type}
/>
)
}

View File

@@ -1,6 +1,6 @@
/* eslint-env mocha */
import { assertProps, render, renderToDOM } from '../../../modules/specHelpers'
import * as utils from '../../../modules/specHelpers'
import assert from 'assert'
import React from 'react'
@@ -8,30 +8,34 @@ import Image from '../'
suite('components/Image', () => {
test('default accessibility', () => {
const dom = renderToDOM(<Image />)
const dom = utils.renderToDOM(<Image />)
assert.equal(dom.getAttribute('role'), 'img')
})
test('prop "accessibilityLabel"', () => {
assertProps.accessibilityLabel(Image)
const accessibilityLabel = 'accessibilityLabel'
const result = utils.shallowRender(<Image accessibilityLabel={accessibilityLabel} />)
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
})
test('prop "accessible"', () => {
assertProps.accessible(Image)
const accessible = false
const result = utils.shallowRender(<Image accessible={accessible} />)
assert.equal(result.props.accessible, accessible)
})
test('prop "children"')
test('prop "defaultSource"', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' }
const dom = renderToDOM(<Image defaultSource={defaultSource} />)
const dom = utils.renderToDOM(<Image defaultSource={defaultSource} />)
const backgroundImage = dom.style.backgroundImage
assert(backgroundImage.indexOf(defaultSource.uri) > -1)
})
test('prop "onError"', function (done) {
this.timeout(5000)
render(<Image
utils.render(<Image
onError={onError}
source={{ uri: 'https://google.com/favicon.icox' }}
/>)
@@ -43,7 +47,7 @@ suite('components/Image', () => {
test('prop "onLoad"', function (done) {
this.timeout(5000)
render(<Image
utils.render(<Image
onLoad={onLoad}
source={{ uri: 'https://google.com/favicon.ico' }}
/>)
@@ -62,10 +66,12 @@ suite('components/Image', () => {
test('prop "source"')
test('prop "style"', () => {
assertProps.style(Image)
utils.assertProps.style(Image)
})
test('prop "testID"', () => {
assertProps.testID(Image)
const testID = 'testID'
const result = utils.shallowRender(<Image testID={testID} />)
assert.equal(result.props.testID, testID)
})
})

View File

@@ -17,10 +17,10 @@ const imageStyleKeys = Object.keys(ImageStylePropTypes)
const styles = StyleSheet.create({
initial: {
alignSelf: 'flex-start',
backgroundColor: 'lightgray',
backgroundColor: 'transparent',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundSize: '100% 100%'
backgroundSize: 'cover'
},
img: {
borderWidth: 0,
@@ -55,18 +55,17 @@ const styles = StyleSheet.create({
class Image extends React.Component {
constructor(props, context) {
super(props, context)
const { uri } = props.source
// state
this.state = { status: props.source.uri ? STATUS_PENDING : STATUS_IDLE }
this.state = { status: uri ? STATUS_PENDING : STATUS_IDLE }
// autobinding
this._onError = this._onError.bind(this)
this._onLoad = this._onLoad.bind(this)
}
static propTypes = {
accessibilityLabel: PropTypes.string,
accessible: PropTypes.bool,
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
accessible: CoreComponent.propTypes.accessible,
children: PropTypes.any,
defaultSource: PropTypes.object,
onError: PropTypes.func,
@@ -84,7 +83,7 @@ class Image extends React.Component {
static defaultProps = {
accessible: true,
defaultSource: {},
resizeMode: 'stretch',
resizeMode: 'cover',
source: {},
style: styles.initial
}
@@ -102,8 +101,8 @@ class Image extends React.Component {
_destroyImageLoader() {
if (this.image) {
this.image.onload = null
this.image.onerror = null
this.image.onload = null
this.image = null
}
}
@@ -124,8 +123,8 @@ class Image extends React.Component {
this._destroyImageLoader()
this.setState({ status: STATUS_LOADED })
this._onLoadEnd()
if (onLoad) onLoad(event)
this._onLoadEnd()
}
_onLoadEnd() {
@@ -194,7 +193,6 @@ class Image extends React.Component {
accessibilityLabel={accessibilityLabel}
accessibilityRole='img'
accessible={accessible}
component='div'
style={{
...styles.initial,
...resolvedStyle,

View File

@@ -0,0 +1,4 @@
import View from '../View'
export default {
...(View.stylePropTypes)
}

View File

@@ -1 +1,11 @@
/* eslint-env mocha */
import * as utils from '../../../modules/specHelpers'
import ScrollView from '../'
suite('components/ScrollView', () => {
test('prop "style"', () => {
utils.assertProps.style(ScrollView)
})
})

View File

@@ -1,18 +1,138 @@
import { pickProps } from '../../modules/filterObjectProps'
import debounce from 'lodash.debounce'
import React, { PropTypes } from 'react'
import ScrollViewStylePropTypes from './ScrollViewStylePropTypes'
import StyleSheet from '../../modules/StyleSheet'
import View from '../View'
const scrollViewStyleKeys = Object.keys(ScrollViewStylePropTypes)
const styles = StyleSheet.create({
initial: {
flexGrow: 1,
flexShrink: 1,
overflow: 'scroll'
},
initialContentContainer: {
flexGrow: 1,
flexShrink: 1
},
row: {
flexDirection: 'row'
}
})
class ScrollView extends React.Component {
static propTypes = {
children: PropTypes.any
children: PropTypes.any,
contentContainerStyle: PropTypes.shape(ScrollViewStylePropTypes),
horizontal: PropTypes.bool,
onScroll: PropTypes.func,
scrollEnabled: PropTypes.bool,
scrollEventThrottle: PropTypes.number,
style: PropTypes.shape(ScrollViewStylePropTypes)
}
static defaultProps = {
className: ''
contentContainerStyle: styles.initialContentContainer,
horizontal: false,
scrollEnabled: true,
scrollEventThrottle: 0,
style: styles.initial
}
constructor(...args) {
super(...args)
this._debouncedOnScrollEnd = debounce(this._onScrollEnd, 100)
this.state = {
isScrolling: false
}
}
_onScroll(e) {
const { scrollEventThrottle } = this.props
const { isScrolling, scrollLastTick } = this.state
// A scroll happened, so the scroll bumps the debounce.
this._debouncedOnScrollEnd(e)
if (isScrolling) {
// Scroll last tick may have changed, check if we need to notify
if (this._shouldEmitScrollEvent(scrollLastTick, scrollEventThrottle)) {
this._onScrollTick(e)
}
} else {
// Weren't scrolling, so we must have just started
this._onScrollStart(e)
}
}
_onScrollStart() {
this.setState({
isScrolling: true,
scrollLastTick: Date.now()
})
}
_onScrollTick(e) {
const { onScroll } = this.props
this.setState({
scrollLastTick: Date.now()
})
if (onScroll) onScroll(e)
}
_onScrollEnd(e) {
const { onScroll } = this.props
this.setState({
isScrolling: false
})
if (onScroll) onScroll(e)
}
_shouldEmitScrollEvent(lastTick, eventThrottle) {
const timeSinceLastTick = Date.now() - lastTick
return (eventThrottle > 0 && timeSinceLastTick >= (1000 / eventThrottle))
}
_maybePreventScroll(e) {
const { scrollEnabled } = this.props
if (!scrollEnabled) e.preventDefault()
}
render() {
const {
children,
contentContainerStyle,
horizontal,
style
} = this.props
const resolvedStyle = pickProps(style, scrollViewStyleKeys)
const resolvedContentContainerStyle = pickProps(contentContainerStyle, scrollViewStyleKeys)
return (
<View {...this.props} />
<View
_className='ScrollView'
onScroll={(e) => this._onScroll(e)}
onTouchMove={(e) => this._maybePreventScroll(e)}
onWheel={(e) => this._maybePreventScroll(e)}
style={{
...styles.initial,
...resolvedStyle
}}
>
{children ? (
<View
children={children}
style={{
...styles.initialContentContainer,
...resolvedContentContainerStyle,
...(horizontal && styles.row)
}}
/>
) : null}
</View>
)
}
}

View File

@@ -1,31 +1,22 @@
import { pickProps } from '../../modules/filterObjectProps'
import CoreComponent from '../CoreComponent'
import View from '../View'
export default {
...View.stylePropTypes,
...pickProps(CoreComponent.stylePropTypes, [
'backgroundColor',
'color',
'direction',
'font',
'fontFamily',
'fontSize',
'fontStyle',
'fontWeight',
'letterSpacing',
'lineHeight',
'margin',
'marginBottom',
'marginLeft',
'marginRight',
'marginTop',
'padding',
'paddingBottom',
'paddingLeft',
'paddingRight',
'paddingTop',
'textAlign',
'textDecoration',
'textTransform',
'whiteSpace',
'wordWrap'
'wordWrap',
'writingDirection'
])
}

View File

@@ -1,6 +1,6 @@
/* eslint-env mocha */
import { assertProps, renderToDOM, shallowRender } from '../../../modules/specHelpers'
import * as utils from '../../../modules/specHelpers'
import assert from 'assert'
import React from 'react'
import ReactTestUtils from 'react-addons-test-utils'
@@ -9,27 +9,33 @@ import Text from '../'
suite('components/Text', () => {
test('prop "accessibilityLabel"', () => {
assertProps.accessibilityLabel(Text)
const accessibilityLabel = 'accessibilityLabel'
const result = utils.shallowRender(<Text accessibilityLabel={accessibilityLabel} />)
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
})
test('prop "accessibilityRole"', () => {
const accessibilityRole = 'accessibilityRole'
const result = utils.shallowRender(<Text accessibilityRole={accessibilityRole} />)
assert.equal(result.props.accessibilityRole, accessibilityRole)
})
test('prop "accessible"', () => {
assertProps.accessible(Text)
const accessible = false
const result = utils.shallowRender(<Text accessible={accessible} />)
assert.equal(result.props.accessible, accessible)
})
test('prop "children"', () => {
const children = 'children'
const result = shallowRender(<Text>{children}</Text>)
const result = utils.shallowRender(<Text>{children}</Text>)
assert.equal(result.props.children, children)
})
test('prop "component"', () => {
assertProps.component(Text, 'span')
})
test('prop "numberOfLines"')
test('prop "onPress"', (done) => {
const dom = renderToDOM(<Text onPress={onPress} />)
const dom = utils.renderToDOM(<Text onPress={onPress} />)
ReactTestUtils.Simulate.click(dom)
function onPress(e) {
assert.ok(e.nativeEvent)
@@ -38,10 +44,12 @@ suite('components/Text', () => {
})
test('prop "style"', () => {
assertProps.style(Text)
utils.assertProps.style(Text)
})
test('prop "testID"', () => {
assertProps.testID(Text)
const testID = 'testID'
const result = utils.shallowRender(<Text testID={testID} />)
assert.equal(result.props.testID, testID)
})
})

View File

@@ -9,10 +9,11 @@ const textStyleKeys = Object.keys(TextStylePropTypes)
const styles = StyleSheet.create({
initial: {
color: 'inherit',
display: 'inline-block',
display: 'inline',
font: 'inherit',
margin: 0,
padding: 0,
textDecoration: 'none',
wordWrap: 'break-word'
},
singleLineStyle: {
@@ -26,10 +27,10 @@ const styles = StyleSheet.create({
class Text extends React.Component {
static propTypes = {
_className: PropTypes.string, // escape-hatch for code migrations
accessibilityLabel: PropTypes.string,
accessible: PropTypes.bool,
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
accessibilityRole: CoreComponent.propTypes.accessibilityRole,
accessible: CoreComponent.propTypes.accessible,
children: PropTypes.any,
component: CoreComponent.propTypes.component,
numberOfLines: PropTypes.number,
onPress: PropTypes.func,
style: PropTypes.shape(TextStylePropTypes),
@@ -41,7 +42,6 @@ class Text extends React.Component {
static defaultProps = {
_className: '',
accessible: true,
component: 'span',
style: styles.initial
}
@@ -52,14 +52,9 @@ class Text extends React.Component {
render() {
const {
_className,
accessibilityLabel,
accessible,
children,
component,
numberOfLines,
onPress,
style,
testID,
...other
} = this.props
@@ -69,18 +64,14 @@ class Text extends React.Component {
return (
<CoreComponent
{...other}
aria-hidden={accessible ? null : true}
aria-label={accessibilityLabel}
children={children}
className={className}
component={component}
component='span'
onClick={this._onPress.bind(this)}
style={{
...styles.initial,
...resolvedStyle,
...(numberOfLines === 1 && styles.singleLineStyle)
}}
testID={testID}
/>
)
}

View File

@@ -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
}

View File

@@ -7,129 +7,133 @@ 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"', () => {
utils.assertProps.accessibilityLabel(TextInput)
const accessibilityLabel = 'accessibilityLabel'
const result = utils.shallowRender(<TextInput accessibilityLabel={accessibilityLabel} />)
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
})
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 value = (() => {
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={value}
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)
@@ -138,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)
@@ -148,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)
@@ -157,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)
@@ -168,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)
@@ -177,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"', () => {
@@ -208,12 +224,14 @@ suite('components/TextInput', () => {
})
test('prop "testID"', () => {
utils.assertProps.testID(TextInput)
const testID = 'testID'
const result = utils.shallowRender(<TextInput testID={testID} />)
assert.equal(result.props.testID, testID)
})
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)
})
})

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: PropTypes.string,
...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 = {
'aria-label': 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>
)
}
}

View File

@@ -1,6 +1,6 @@
/* eslint-env mocha */
import { assertProps, shallowRender } from '../../../modules/specHelpers'
import * as utils from '../../../modules/specHelpers'
import assert from 'assert'
import React from 'react'
@@ -11,19 +11,25 @@ const requiredProps = { children }
suite('components/Touchable', () => {
test('prop "accessibilityLabel"', () => {
assertProps.accessibilityLabel(Touchable, requiredProps)
const accessibilityLabel = 'accessibilityLabel'
const result = utils.shallowRender(<Touchable {...requiredProps} accessibilityLabel={accessibilityLabel} />)
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
})
test('prop "accessibilityRole"', () => {
assertProps.accessibilityRole(Touchable, requiredProps)
const accessibilityRole = 'accessibilityRole'
const result = utils.shallowRender(<Touchable {...requiredProps} accessibilityRole={accessibilityRole} />)
assert.equal(result.props.accessibilityRole, accessibilityRole)
})
test('prop "accessible"', () => {
assertProps.accessible(Touchable, requiredProps)
const accessible = false
const result = utils.shallowRender(<Touchable {...requiredProps} accessible={accessible} />)
assert.equal(result.props.accessible, accessible)
})
test('prop "children"', () => {
const result = shallowRender(<Touchable {...requiredProps} />)
const result = utils.shallowRender(<Touchable {...requiredProps} />)
assert.deepEqual(result.props.children, children)
})
})

View File

@@ -25,9 +25,9 @@ class Touchable extends React.Component {
}
static propTypes = {
accessibilityLabel: PropTypes.string,
accessibilityRole: PropTypes.string,
accessible: PropTypes.bool,
accessibilityLabel: View.propTypes.accessibilityLabel,
accessibilityRole: View.propTypes.accessibilityRole,
accessible: View.propTypes.accessible,
activeOpacity: PropTypes.number,
activeUnderlayColor: PropTypes.string,
children: PropTypes.element,
@@ -45,7 +45,6 @@ class Touchable extends React.Component {
accessibilityRole: 'button',
activeOpacity: 1,
activeUnderlayColor: 'transparent',
component: 'div',
delayLongPress: 1000,
delayPressIn: 0,
delayPressOut: 0,

View File

@@ -54,6 +54,8 @@ export default {
'left',
// margin
'margin',
'marginHorizontal',
'marginVertical',
'marginBottom',
'marginLeft',
'marginRight',
@@ -70,6 +72,8 @@ export default {
'overflowY',
// padding
'padding',
'paddingHorizontal',
'paddingVertical',
'paddingBottom',
'paddingLeft',
'paddingRight',

View File

@@ -1,6 +1,6 @@
/* eslint-env mocha */
import { assertProps, shallowRender } from '../../../modules/specHelpers'
import * as utils from '../../../modules/specHelpers'
import assert from 'assert'
import React from 'react'
@@ -8,41 +8,47 @@ import View from '../'
suite('components/View', () => {
test('prop "accessibilityLabel"', () => {
assertProps.accessibilityLabel(View)
const accessibilityLabel = 'accessibilityLabel'
const result = utils.shallowRender(<View accessibilityLabel={accessibilityLabel} />)
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
})
test('prop "accessibilityLiveRegion"', () => {
assertProps.accessibilityLiveRegion(View)
const accessibilityLiveRegion = 'polite'
const result = utils.shallowRender(<View accessibilityLiveRegion={accessibilityLiveRegion} />)
assert.equal(result.props.accessibilityLiveRegion, accessibilityLiveRegion)
})
test('prop "accessibilityRole"', () => {
assertProps.accessibilityRole(View)
const accessibilityRole = 'accessibilityRole'
const result = utils.shallowRender(<View accessibilityRole={accessibilityRole} />)
assert.equal(result.props.accessibilityRole, accessibilityRole)
})
test('prop "accessible"', () => {
assertProps.accessible(View)
const accessible = false
const result = utils.shallowRender(<View accessible={accessible} />)
assert.equal(result.props.accessible, accessible)
})
test('prop "children"', () => {
const children = 'children'
const result = shallowRender(<View>{children}</View>)
const result = utils.shallowRender(<View>{children}</View>)
assert.equal(result.props.children, children)
})
test('prop "component"', () => {
assertProps.component(View)
})
test('prop "pointerEvents"', () => {
const result = shallowRender(<View pointerEvents='box-only' />)
const result = utils.shallowRender(<View pointerEvents='box-only' />)
assert.equal(result.props.style.pointerEvents, 'box-only')
})
test('prop "style"', () => {
assertProps.style(View)
utils.assertProps.style(View)
})
test('prop "testID"', () => {
assertProps.testID(View)
const testID = 'testID'
const result = utils.shallowRender(<View testID={testID} />)
assert.equal(result.props.testID, testID)
})
})

View File

@@ -21,6 +21,7 @@ const styles = StyleSheet.create({
margin: 0,
padding: 0,
position: 'relative',
textDecoration: 'none',
// button reset
backgroundColor: 'transparent',
color: 'inherit',
@@ -32,12 +33,11 @@ const styles = StyleSheet.create({
class View extends React.Component {
static propTypes = {
_className: PropTypes.string, // escape-hatch for code migrations
accessibilityLabel: PropTypes.string,
accessibilityLiveRegion: PropTypes.oneOf(['assertive', 'off', 'polite']),
accessibilityRole: PropTypes.string,
accessible: PropTypes.bool,
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
accessibilityLiveRegion: CoreComponent.propTypes.accessibilityLiveRegion,
accessibilityRole: CoreComponent.propTypes.accessibilityRole,
accessible: CoreComponent.propTypes.accessible,
children: PropTypes.any,
component: CoreComponent.propTypes.component,
pointerEvents: PropTypes.oneOf(['auto', 'box-none', 'box-only', 'none']),
style: PropTypes.shape(ViewStylePropTypes),
testID: CoreComponent.propTypes.testID
@@ -48,20 +48,14 @@ class View extends React.Component {
static defaultProps = {
_className: '',
accessible: true,
component: 'div',
style: styles.initial
}
render() {
const {
_className,
accessibilityLabel,
accessibilityLiveRegion,
accessibilityRole,
accessible,
pointerEvents,
style,
testID,
...other
} = this.props
@@ -72,17 +66,12 @@ class View extends React.Component {
return (
<CoreComponent
{...other}
aria-hidden={accessible ? null : true}
aria-label={accessibilityLabel}
aria-live={accessibilityLiveRegion}
className={className}
role={accessibilityRole}
style={{
...styles.initial,
...resolvedStyle,
...pointerEventsStyle
}}
testID={testID}
/>
)
}

View File

@@ -11,11 +11,7 @@ import TextInput from './components/TextInput'
import Touchable from './components/Touchable'
import View from './components/View'
export default React
export {
StyleSheet,
const ReactNative = {
// components
Image,
ListView,
@@ -23,5 +19,13 @@ export {
Text,
TextInput,
Touchable,
View
View,
// apis
StyleSheet,
// React
...React
}
module.exports = ReactNative

View File

@@ -3,10 +3,15 @@ import { PropTypes } from 'react'
const { number, string } = PropTypes
const numberOrString = PropTypes.oneOfType([ number, string ])
/**
* Any properties marked @private are used internally in resets or property
* mappings.
*/
export default {
alignContent: string,
alignItems: string,
alignSelf: string,
appearance: string,
backfaceVisibility: string,
backgroundAttachment: string,
backgroundClip: string,
@@ -16,7 +21,6 @@ export default {
backgroundPosition: string,
backgroundRepeat: string,
backgroundSize: string,
border: string,
borderColor: string,
borderBottomColor: string,
borderLeftColor: string,
@@ -38,20 +42,21 @@ export default {
borderRightWidth: numberOrString,
borderTopWidth: numberOrString,
bottom: numberOrString,
boxShadow: string,
boxSizing: string,
clear: string,
color: string,
cursor: string,
direction: string,
display: string,
flex: string,
direction: string, /* @private */
flex: string, /* @private */
flexBasis: string,
flexDirection: string,
flexGrow: numberOrString,
flexShrink: numberOrString,
flexWrap: string,
float: string,
font: string,
font: string, /* @private */
fontFamily: string,
fontSize: numberOrString,
fontStyle: string,
@@ -61,35 +66,44 @@ export default {
left: numberOrString,
letterSpacing: string,
lineHeight: numberOrString,
listStyle: string,
margin: numberOrString,
marginBottom: numberOrString,
marginHorizontal: numberOrString,
marginLeft: numberOrString,
marginRight: numberOrString,
marginTop: numberOrString,
marginVertical: numberOrString,
maxHeight: numberOrString,
maxWidth: numberOrString,
minHeight: numberOrString,
minWidth: numberOrString,
opacity: numberOrString,
order: numberOrString,
outline: string,
overflow: string,
overflowX: string,
overflowY: string,
padding: numberOrString,
paddingBottom: numberOrString,
paddingHorizontal: numberOrString,
paddingLeft: numberOrString,
paddingRight: numberOrString,
paddingTop: numberOrString,
paddingVertical: numberOrString,
position: string,
right: numberOrString,
textAlign: string,
textDecoration: string,
textOverflow: string,
textTransform: string,
top: numberOrString,
userSelect: string,
verticalAlign: string,
visibility: string,
whiteSpace: string,
width: numberOrString,
wordWrap: string,
writingDirection: string,
zIndex: numberOrString
}

View File

@@ -41,7 +41,7 @@ export default class Store {
const getCssSelector = (property, value) => {
let className = this.get(property, value)
if (!obfuscate && className) {
className = className.replace(/[:?.%\\$#]/g, '\\$&')
className = className.replace(/[,":?.%\\$#]/g, '\\$&')
}
return className
}
@@ -89,7 +89,7 @@ export default class Store {
if (!exists) {
this._counter += 1
if (this._options.obfuscateClassNames) {
this._classNames[key] = `_rn_${this._counter}`
this._classNames[key] = `_s_${this._counter}`
} else {
const val = `${value}`.replace(/\s/g, '-')
this._classNames[key] = `${property}:${val}`

View File

@@ -59,10 +59,10 @@ suite('modules/StyleSheet/Store', () => {
store.set('flexGrow', 1)
store.set('flexGrow', 2)
assert.deepEqual(store._classNames, {
'alignItems:center': '_rn_1',
'flexGrow:0': '_rn_2',
'flexGrow:1': '_rn_3',
'flexGrow:2': '_rn_4'
'alignItems:center': '_s_1',
'flexGrow:0': '_s_2',
'flexGrow:1': '_s_3',
'flexGrow:2': '_s_4'
})
})
@@ -82,8 +82,9 @@ suite('modules/StyleSheet/Store', () => {
test('replaces space characters', () => {
const store = new Store()
store.set('margin', '0 auto')
assert.deepEqual(store.get('margin', '0 auto'), 'margin:0-auto')
assert.equal(store.get('margin', '0 auto'), 'margin\:0-auto')
})
})
@@ -91,17 +92,17 @@ suite('modules/StyleSheet/Store', () => {
test('human-readable style sheet', () => {
const store = new Store()
store.set('alignItems', 'center')
store.set('color', '#fff')
store.set('fontFamily', '"Helvetica Neue", Arial, sans-serif')
store.set('marginBottom', 0)
store.set('margin', 1)
store.set('margin', 2)
store.set('margin', 3)
store.set('width', '100%')
const expected = '/* 5 unique declarations */\n' +
'.alignItems\\:center{align-items:center;}\n' +
'.margin\\:1px{margin:1px;}\n' +
'.margin\\:2px{margin:2px;}\n' +
'.margin\\:3px{margin:3px;}\n' +
'.marginBottom\\:0px{margin-bottom:0px;}'
'.color\\:\\#fff{color:#fff;}\n' +
'.fontFamily\\:\\"Helvetica-Neue\\"\\,-Arial\\,-sans-serif{font-family:"Helvetica Neue", Arial, sans-serif;}\n' +
'.marginBottom\\:0px{margin-bottom:0px;}\n' +
'.width\\:100\\%{width:100%;}'
assert.equal(store.toString(), expected)
})
@@ -115,11 +116,11 @@ suite('modules/StyleSheet/Store', () => {
store.set('margin', 3)
const expected = '/* 5 unique declarations */\n' +
'._rn_1{align-items:center;}\n' +
'._rn_3{margin:1px;}\n' +
'._rn_4{margin:2px;}\n' +
'._rn_5{margin:3px;}\n' +
'._rn_2{margin-bottom:0px;}'
'._s_1{align-items:center;}\n' +
'._s_3{margin:1px;}\n' +
'._s_4{margin:2px;}\n' +
'._s_5{margin:3px;}\n' +
'._s_2{margin-bottom:0px;}'
assert.equal(store.toString(), expected)
})

View File

@@ -0,0 +1,29 @@
/* eslint-env mocha */
import assert from 'assert'
import expandStyle from '../expandStyle'
suite('modules/StyleSheet/expandStyle', () => {
test('style property', () => {
const initial = {
borderTopWidth: 1,
borderWidth: 2,
marginTop: 50,
marginVertical: 25,
margin: 10
}
const expectedStyle = {
borderTopWidth: 1,
borderLeftWidth: 2,
borderRightWidth: 2,
borderBottomWidth: 2,
marginTop: 50,
marginBottom: 25,
marginLeft: 10,
marginRight: 10
}
assert.deepEqual(expandStyle(initial), expectedStyle)
})
})

View File

@@ -16,8 +16,9 @@ const fixture = {
backgroundSize: 'contain'
}
},
ignored: {
pading: 0
position: {
left: { left: 0 },
right: { right: 0 }
}
}
@@ -27,7 +28,9 @@ suite('modules/StyleSheet/getStyleObjects', () => {
assert.deepEqual(actual, [
{ margin: 0, padding: 0 },
{ backgroundSize: 'auto' },
{ backgroundSize: 'contain' }
{ backgroundSize: 'contain' },
{ left: 0 },
{ right: 0 }
])
})
})

View File

@@ -6,8 +6,11 @@ import hyphenate from '../hyphenate'
suite('modules/StyleSheet/hyphenate', () => {
test('style property', () => {
assert.equal(hyphenate('alignItems'), 'align-items')
assert.equal(hyphenate('color'), 'color')
})
test('vendor prefixed style property', () => {
assert.equal(hyphenate('WebkitAppearance'), '-webkit-appearance')
assert.equal(hyphenate('MozTransition'), '-moz-transition')
assert.equal(hyphenate('msTransition'), '-ms-transition')
assert.equal(hyphenate('WebkitTransition'), '-webkit-transition')
})
})

View File

@@ -4,7 +4,7 @@ import { resetCSS, predefinedCSS } from '../predefs'
import assert from 'assert'
import StyleSheet from '..'
const styles = { root: { border: 0 } }
const styles = { root: { borderWidth: 1 } }
suite('modules/StyleSheet', () => {
setup(() => {
@@ -20,14 +20,17 @@ suite('modules/StyleSheet', () => {
assert.equal(
StyleSheet.renderToString(),
`${resetCSS}\n${predefinedCSS}\n` +
`/* 1 unique declarations */\n` +
`.border\\:0px{border:0px;}`
`/* 4 unique declarations */\n` +
`.borderBottomWidth\\:1px{border-bottom-width:1px;}\n` +
`.borderLeftWidth\\:1px{border-left-width:1px;}\n` +
`.borderRightWidth\\:1px{border-right-width:1px;}\n` +
`.borderTopWidth\\:1px{border-top-width:1px;}`
)
})
test('resolve', () => {
const props = { className: 'className', style: styles.root }
const expected = { className: 'className border:0px', style: {} }
const expected = { className: 'className borderTopWidth:1px borderRightWidth:1px borderBottomWidth:1px borderLeftWidth:1px', style: {} }
StyleSheet.create(styles)
assert.deepEqual(StyleSheet.resolve(props), expected)
})

View File

@@ -3,14 +3,27 @@
import assert from 'assert'
import isStyleObject from '../isStyleObject'
const style = { margin: 0 }
const notStyle = { root: style }
const styles = {
root: {
margin: 0
},
align: {
left: {
textAlign: 'left'
},
right: {
textAlign: 'right'
}
}
}
suite('modules/StyleSheet/isStyleObject', () => {
test('returns "true" for style objects', () => {
assert.ok(isStyleObject(style) === true)
})
test('returns "false" for non-style objects', () => {
assert.ok(isStyleObject(notStyle) === false)
assert.ok(isStyleObject(styles) === false)
assert.ok(isStyleObject(styles.align) === false)
})
test('returns "true" for style objects', () => {
assert.ok(isStyleObject(styles.root) === true)
assert.ok(isStyleObject(styles.align.left) === true)
})
})

View File

@@ -0,0 +1,52 @@
const styleShortHands = {
borderColor: [ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor' ],
borderRadius: [ 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius' ],
borderStyle: [ 'borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle' ],
borderWidth: [ 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth' ],
margin: [ 'marginTop', 'marginRight', 'marginBottom', 'marginLeft' ],
marginHorizontal: [ 'marginRight', 'marginLeft' ],
marginVertical: [ 'marginTop', 'marginBottom' ],
padding: [ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft' ],
paddingHorizontal: [ 'paddingRight', 'paddingLeft' ],
paddingVertical: [ 'paddingTop', 'paddingBottom' ],
writingDirection: [ 'direction' ]
}
/**
* Alpha-sort properties, apart from shorthands which appear before the
* properties they expand into. This ensures that more specific styles override
* the shorthands, whatever the order in which they were originally declared.
*/
const sortProps = (propsArray) => propsArray.sort((a, b) => {
const expandedA = styleShortHands[a]
const expandedB = styleShortHands[b]
if (expandedA && expandedA.indexOf(b) > -1) {
return -1
} else if (expandedB && expandedB.indexOf(a) > -1) {
return 1
}
return a < b ? -1 : a > b ? 1 : 0
})
/**
* Expand the shorthand properties to isolate every declaration from the others.
*/
const expandStyle = (style) => {
const propsArray = Object.keys(style)
const sortedProps = sortProps(propsArray)
return sortedProps.reduce((resolvedStyle, key) => {
const expandedProps = styleShortHands[key]
const value = style[key]
if (expandedProps) {
expandedProps.forEach((prop, i) => {
resolvedStyle[expandedProps[i]] = value
})
} else {
resolvedStyle[key] = value
}
return resolvedStyle
}, {})
}
export default expandStyle

View File

@@ -1 +1 @@
export default (string) => string.replace(/([A-Z])/g, '-$1').toLowerCase()
export default (string) => (string.replace(/([A-Z])/g, '-$1').toLowerCase()).replace(/^ms-/, '-ms-')

View File

@@ -1,7 +1,9 @@
import { resetCSS, predefinedCSS, predefinedClassNames } from './predefs'
import expandStyle from './expandStyle'
import getStyleObjects from './getStyleObjects'
import prefixer from './prefixer'
import Store from './Store'
import StylePropTypes from '../StylePropTypes'
/**
* Initialize the store with pointer-event styles mapping to our custom pointer
@@ -17,11 +19,18 @@ let store = createStore()
*/
const create = (styles: Object): Object => {
const rules = getStyleObjects(styles)
rules.forEach((rule) => {
Object.keys(rule).forEach(property => {
const value = rule[property]
// add each declaration to the store
store.set(property, value)
const style = expandStyle(rule)
Object.keys(style).forEach((property) => {
if (!StylePropTypes[property]) {
console.error(`ReactNativeWeb: the style property "${property}" is not supported`)
} else {
const value = style[property]
// add each declaration to the store
store.set(property, value)
}
})
})
return styles
@@ -49,15 +58,19 @@ const renderToString = () => {
const resolve = ({ className = '', style = {} }) => {
let _className
let _style = {}
const expandedStyle = expandStyle(style)
const classList = [ className ]
for (const prop in style) {
let styleClass = store.get(prop, style[prop])
for (const prop in expandedStyle) {
if (!StylePropTypes[prop]) {
continue
}
let styleClass = store.get(prop, expandedStyle[prop])
if (styleClass) {
classList.push(styleClass)
} else {
_style[prop] = style[prop]
_style[prop] = expandedStyle[prop]
}
}

View File

@@ -1,9 +1,11 @@
import { pickProps } from '../filterObjectProps'
import StylePropTypes from '../StylePropTypes'
import isObject from './isObject'
const isStyleObject = (obj) => {
const declarations = pickProps(obj, Object.keys(StylePropTypes))
return Object.keys(declarations).length > 0
const values = Object.keys(obj).map((key) => obj[key])
for (let i = 0; i < values.length; i += 1) {
if (isObject(values[i])) { return false }
}
return true
}
export default isStyleObject

View File

@@ -6,23 +6,20 @@ export const resetCSS =
html {font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}
body {margin:0}
button::-moz-focus-inner, input::-moz-focus-inner {border:0;padding:0}
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {-webkit-appearance:none}`
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {-webkit-appearance:none}
ol,ul,li {list-style:none}`
/**
* Custom pointer event styles
*/
export const predefinedCSS =
`/* pointer-events */
._rn_pe-a {pointer-events:auto}
._rn_pe-bn {pointer-events:none}
._rn_pe-bn * {pointer-events:auto}
._rn_pe-bo {pointer-events:auto}
._rn_pe-bo * {pointer-events:none}
._rn_pe-n {pointer-events:none}`
._s_pe-a, ._s_pe-bo, ._s_pe-bn * {pointer-events:auto}
._s_pe-n, ._s_pe-bo *, ._s_pe-bn {pointer-events:none}`
export const predefinedClassNames = {
'pointerEvents:auto': '_rn_pe-a',
'pointerEvents:box-none': '_rn_pe-bn',
'pointerEvents:box-only': '_rn_pe-bo',
'pointerEvents:none': '_rn_pe-n'
'pointerEvents:auto': '_s_pe-a',
'pointerEvents:box-none': '_s_pe-bn',
'pointerEvents:box-only': '_s_pe-bo',
'pointerEvents:none': '_s_pe-n'
}

View File

@@ -6,44 +6,6 @@ import ReactDOM from 'react-dom'
import ReactTestUtils from 'react-addons-test-utils'
export const assertProps = {
accessibilityLabel: function (Component, props) {
// with label
const accessibilityLabel = 'accessibilityLabel'
const dom = renderToDOM(<Component {...props} accessibilityLabel={accessibilityLabel} />)
assert.equal(dom.getAttribute('aria-label'), accessibilityLabel)
},
accessibilityLiveRegion: function (Component, props) {
const accessibilityLiveRegion = 'polite'
const dom = renderToDOM(<Component {...props} accessibilityLiveRegion={accessibilityLiveRegion} />)
assert.equal(dom.getAttribute('aria-live'), accessibilityLiveRegion)
},
accessibilityRole: function (Component, props) {
const accessibilityRole = 'main'
const dom = renderToDOM(<Component {...props} accessibilityRole={accessibilityRole} />)
assert.equal(dom.getAttribute('role'), accessibilityRole)
},
accessible: function (Component, props) {
// accessible (implicit)
let dom = renderToDOM(<Component {...props} />)
assert.equal(dom.getAttribute('aria-hidden'), null)
// accessible (explicit)
dom = renderToDOM(<Component {...props} accessible />)
assert.equal(dom.getAttribute('aria-hidden'), null)
// not accessible
dom = renderToDOM(<Component {...props} accessible={false} />)
assert.equal(dom.getAttribute('aria-hidden'), 'true')
},
component: function (Component, props) {
const component = 'main'
const dom = renderToDOM(<Component {...props} component={component} />)
const tagName = (dom.tagName).toLowerCase()
assert.equal(tagName, component)
},
style: function (Component, props) {
let shallow
// default styles
@@ -67,16 +29,6 @@ export const assertProps = {
shallow.props.style,
{ ...Component.defaultProps.style, ...styleToMerge }
)
},
testID: function (Component, props) {
// no testID
let dom = renderToDOM(<Component {...props} />)
assert.equal(dom.getAttribute('data-testid'), null)
// with testID
const testID = 'Example.testID'
dom = renderToDOM(<Component {...props} testID={testID} />)
assert.equal(dom.getAttribute('data-testid'), testID)
}
}

View File

@@ -4,5 +4,5 @@
*
* See: https://github.com/webpack/docs/wiki/context
*/
var context = require.context('.', true, /-test\.js$/)
var context = require.context('./src', true, /-test\.js$/)
context.keys().forEach(context)