Compare commits

...

50 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
Nicolas Gallagher
a19b57df4d 0.0.7 2015-10-18 09:11:38 -07:00
Nicolas Gallagher
1c444569ae [change] React 0.14 support 2015-10-18 08:54:09 -07:00
Nicolas Gallagher
0b8c4b8746 [fix] link to API docs 2015-10-17 18:51:01 -07:00
Nicolas Gallagher
6772233837 [remove] Swipeable 2015-10-17 18:49:21 -07:00
Nicolas Gallagher
cd89f88d96 [add] StyleSheet API
Initial StyleSheet implementation for Web. Converts style object
declarations to "atomic" CSS rules.

Close gh-25
2015-10-17 17:52:01 -07:00
Nicolas Gallagher
b59bdb17b2 [change] Unit test setup and reporter 2015-10-17 17:51:54 -07:00
Nicolas Gallagher
2bfa579fe4 [remove] babel-plugin-typecheck 2015-10-17 17:51:30 -07:00
Nicolas Gallagher
f4c6e33b2f Fix #26 2015-10-01 15:18:28 -07:00
Nicolas Gallagher
39b273f9d8 Moves 2015-09-20 21:00:15 -07:00
80 changed files with 2303 additions and 2011 deletions

View File

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

View File

@@ -3,9 +3,6 @@
"parser": "babel-eslint",
// based on https://github.com/feross/standard
"extends": [ "standard", "standard-react" ],
"env": {
"mocha": true
},
"rules": {
// overrides of the standard style
"space-before-function-paren": [ 2, { "anonymous": "always", "named": "never" } ],

View File

@@ -1,6 +1,6 @@
language: node_js
node_js:
- "0.12"
- "4.1"
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start

View File

@@ -68,9 +68,9 @@ not want to merge into the project.
Development commands:
* `npm run build` build the library
* `npm run dev` start the dev server and develop against live examples
* `npm run examples` start the dev server and develop against live examples
* `npm run lint` run the linter
* `npm run test:specs` run and watch the unit tests
* `npm run test` run the linter and unit tests
Please follow this process for submitting a patch:

255
README.md
View File

@@ -2,19 +2,21 @@
[![Build Status][travis-image]][travis-url]
[![npm version][npm-image]][npm-url]
![gzipped size](https://img.shields.io/badge/gzipped-~18.6k-blue.svg)
The core [React Native][react-native-url] components adapted and expanded upon
for the web, backed by a precomputed CSS library. ~21KB minified and gzipped.
[React Native][react-native-url] components and APIs for the Web.
* [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
* [Install](#install)
* [Use](#use)
* [Example](#example)
* [APIs](#apis)
* [Components](#components)
* [Styling](#styling)
* [Accessibility](#accessibility)
* [Contributing](#contributing)
* [Thanks](#thanks)
* [License](#license)
@@ -22,87 +24,26 @@ for the web, backed by a precomputed CSS library. ~21KB minified and gzipped.
## Install
```
npm install --save react react-native-web
npm install --save react react-dom react-native-web
```
## Use
## Example
React Native for Web exports its components and a reference to the `React`
installation. Styles are authored in JavaScript as plain objects.
installation. Styles are defined with, and used as JavaScript objects.
Component:
```js
import React, { View } from 'react-native-web'
import React, { Image, StyleSheet, Text, View } from 'react-native-web'
class MyComponent extends React.Component {
render() {
return (
<View style={styles.root} />
)
}
}
const Title = ({ children }) => <Text style={styles.title}>{children}</Text>
const styles = {
root: {
borderColor: 'currentcolor'
borderWidth: '5px',
flexDirection: 'row'
height: '5em'
}
}
```
## Components
### [`Image`](docs/components/Image.md)
An accessibile image component with support for image resizing, default image,
and child content.
### [`ListView`](docs/components/ListView.md)
(TODO)
### [`ScrollView`](docs/components/ListView.md)
(TODO)
### [`Swipeable`](docs/components/Swipeable.md)
Touch bindings for swipe gestures.
### [`Text`](docs/components/Text.md)
Displays text as an inline block and supports basic press handling.
### [`TextInput`](docs/components/TextInput.md)
Accessible single- and multi-line text input via a keyboard.
### [`Touchable`](docs/components/Touchable.md)
Touch bindings for press and long press.
### [`View`](docs/components/View.md)
The fundamental UI building block using flexbox for layout.
## Styling
React Native for Web provides a mechanism to declare all your styles in
JavaScript within your components. 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].
Authoring `style` is no different to the existing use of inline styles in
React, but most inline styles are converted to single-purpose class names. The
current implementation includes 300+ precomputed CSS declarations (~4.5KB
gzipped) that covers many common property-value pairs. A more sophisticated
build-time implementation may produce a slightly larger CSS file for large
apps, and fall back to fewer inline styles. Read more about the [styling
strategy](docs/style.md).
```js
import React, { Image, Text, View } from 'react-native-web'
const Summary = ({ children }) => (
<View style={styles.text}>
<Text style={styles.subtitle}>{children}</Text>
</View>
)
class App extends React.Component {
render() {
@@ -112,20 +53,14 @@ class App extends React.Component {
source={{ uri: 'http://facebook.github.io/react/img/logo_og.png' }}
style={styles.image}
/>
<View style={styles.text}>
<Text style={styles.title}>
React Native Web
</Text>
<Text style={styles.subtitle}>
Build high quality web apps using React
</Text>
</View>
<Title>React Native Web</Title>
<Summary>Build high quality web apps using React</Summary>
</View>
)
},
})
const styles = {
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
margin: 40
@@ -146,21 +81,140 @@ const styles = {
subtitle: {
fontSize: '1rem'
}
}
})
```
Combine and override style objects:
Pre-render styles on the server:
```js
import baseStyle from './baseStyle'
// server.js
import App from './components/App'
import React, { StyleSheet } from 'react-native-web'
const buttonStyle = {
...baseStyle,
backgroundColor: '#333',
color: '#fff'
}
const html = React.renderToString(<App />);
const css = StyleSheet.renderToString();
const Html = () => (
<html>
<head>
<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 }} />
</body>
</html>
)
```
Render styles on the client:
```js
// client.js
import App from './components/App'
import React, { StyleSheet } from 'react-native-web'
import ReactDOM from 'react-dom'
const reactRoot = document.getElementById('react-root')
const reactStyleSheet = document.getElementById('react-stylesheet')
ReactDOM.render(<App />, reactRoot)
reactStyleSheet.textContent = StyleSheet.renderToString()
```
## APIs
### [`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 targeting elements and
pseudo-elements beyond the reach of React inline styles.
## Components
### [`Image`](docs/components/Image.md)
An accessibile image component with support for image resizing, default image,
and child content.
### [`ListView`](docs/components/ListView.md)
(TODO)
### [`ScrollView`](docs/components/ScrollView.md)
A scrollable view with event throttling.
### [`Text`](docs/components/Text.md)
Displays text inline and supports basic press handling.
### [`TextInput`](docs/components/TextInput.md)
Accessible single- and multi-line text input via a keyboard.
### [`Touchable`](docs/components/Touchable.md)
Touch bindings for press and long press.
### [`View`](docs/components/View.md)
The fundamental UI building block using flexbox for layout.
## Styling
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].
### 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
Please read the [contribution guidelines][contributing-url]. Contributions are
@@ -169,24 +223,25 @@ 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-swipeable](https://github.com/dogfessional/react-swipeable/)
for the current implementation of `Swipeable`, and to
[react-tappable](https://github.com/JedWatson/react-tappable) for backing the
current implementation of `Touchable`.
Thanks to [react-tappable](https://github.com/JedWatson/react-tappable) for
backing the current implementation of `Touchable`.
## License
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,6 +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
ROOT_DIRECTORY: ROOT,
TEST_ENTRY: path.join(ROOT, 'tests.webpack.js')
}

View File

@@ -1,6 +1,5 @@
var assign = require('object-assign')
var constants = require('./constants')
var webpackConfig = require('./webpack.config.base')
var webpack = require('webpack')
module.exports = function (config) {
config.set({
@@ -9,38 +8,48 @@ module.exports = function (config) {
browserNoActivityTimeout: 60000,
client: {
captureConsole: true,
mocha: {
ui: 'tdd'
},
mocha: { ui: 'tdd' },
useIframe: true
},
files: [
'src/specs.context.js'
],
frameworks: [
'mocha'
constants.TEST_ENTRY
],
frameworks: [ 'mocha' ],
plugins: [
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-mocha',
'karma-sourcemap-loader',
'karma-spec-reporter',
'karma-webpack'
],
preprocessors: {
'src/specs.context.js': [ 'webpack', 'sourcemap' ]
[constants.TEST_ENTRY]: [ 'webpack', 'sourcemap' ]
},
reporters: [ 'dots' ],
reporters: process.env.TRAVIS ? [ 'dots' ] : [ 'spec' ],
singleRun: true,
webpack: assign({}, webpackConfig, { devtool: 'inline' }),
webpackMiddleware: {
stats: {
assetsSort: 'name',
colors: true,
children: false,
chunks: false,
modules: false
}
webpack: {
devtool: 'inline-source-map',
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel',
query: { cacheDirectory: true }
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('test')
}
})
]
},
webpackServer: {
noInfo: true
}
})
}

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: {
@@ -25,14 +31,6 @@ if (process.env.NODE_ENV === 'production') {
module.exports = {
module: {
loaders: [
{
test: /\.css$/,
loader: [
'style-loader',
'css-loader?module&localIdentName=[hash:base64:5]',
'autoprefixer-loader'
].join('!')
},
{
test: /\.jsx?$/,
exclude: /node_modules/,

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

@@ -7,7 +7,8 @@ module.exports = assign({}, base, {
main: constants.SRC_DIRECTORY
},
externals: [{
react: true
'react': true,
'react-dom': true
}],
output: {
filename: 'react-native-web.js',

165
docs/apis/StyleSheet.md Normal file
View File

@@ -0,0 +1,165 @@
# StyleSheet
React Native for Web will automatically vendor-prefix styles applied to the
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.
Create a new StyleSheet:
```js
const styles = StyleSheet.create({
container: {
borderRadius: 4,
borderWidth: 0.5,
borderColor: '#d6d7da',
},
title: {
fontSize: 19,
fontWeight: 'bold',
},
activeTitle: {
color: 'red',
},
})
```
Use styles:
```js
<View style={styles.container}>
<Text
style={{
...styles.title,
...(this.props.isActive && styles.activeTitle)
}}
/>
</View>
```
Render styles on the server or in the browser:
```js
StyleSheet.renderToString()
```
## Methods
**create**(obj: {[key: string]: any})
**destroy**()
Clears all style information.
**renderToString**()
Renders a CSS Style Sheet.
## About
### Strategy
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:
```js
<View style={styles.root}>...</View>
const styles = StyleSheet.create({
root: {
background: 'transparent',
display: 'flex',
flexGrow: 1,
justifyContent: 'center'
}
})
```
Yields (in development):
```html
<div className="background:transparent display:flex flexGrow:1 justifyContent:center">...</div>
```
And is backed by the following CSS:
```css
.background\:transparent {background:transparent;}
.display\:flex {display:flex;}
.flexGrow\:1 {flex-grow:1;}
.justifyContext\:center {justify-content:center;}
```
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
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.)
### Reset
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`).
```css
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;
}
```

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,95 +0,0 @@
# Swipeable
Unique to web.
## Props
**delta**: number = 10
Number of pixels that must be swiped before events are dispatched.
**flickThreshold**: number = 0.6
The velocity threshold at which a swipe is considered a flick.
**onSwiped**: function
(SyntheticTouchEvent, deltaX, deltaY, isFlick) => swipeHandler
Called once a swipe has ended.
**onSwipedDown**: function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called once a swipe-down has ended.
**onSwipedLeft**: function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called once a swipe-left has ended.
**onSwipedUp**: function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called once a swipe-up has ended.
**onSwipedRight**: function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called once a swipe-right has ended.
**onSwipingDown**: function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called while a swipe-down is in progress.
**onSwipingLeft**: function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called while a swipe-left is in progress.
**onSwipingRight**: function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called while a swipe-right is in progress.
**onSwipingUp**: function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called while a swipe-up is in progress.
## Examples
```js
import React, { Swipeable } from 'react-native-web'
const { Component, PropTypes } = React;
class Example extends Component {
static propTypes = {
}
static defaultProps = {
}
_onSwiped(event, x, y, isFlick) {
}
render() {
return (
<Swipeable
onSwiped={this._onSwiped.bind(this)}
/>
)
}
}
```

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,123 +0,0 @@
# Styling strategy
Using the `style` attribute would normally produce inline styles. There are
several existing approaches to using the `style` attribute, some of which
convert inline styles to static CSS:
[jsxstyle](https://github.com/petehunt/jsxstyle),
[react-free-style](https://github.com/blakeembrey/react-free-style/),
[react-inline](https://github.com/martinandert/react-inline),
[react-native](https://facebook.github.io/react-native/),
[react-style](https://github.com/js-next/react-style),
[stilr](https://github.com/kodyl/stilr).
## Style syntax: native vs proprietary data structure
React Native for Web diverges from React Native by using plain JS objects for
styles:
```js
<Text style={styles.root}>...</Text>
const styles = {
root: {
background: 'transparent',
display: 'flex',
flexGrow: 1,
justifyContent: 'center'
}
};
```
Most approaches to managing style in React introduce a proprietary data
structure, often via an implementation of `Stylesheet.create`.
```js
<Text style={styles.root}>...</Text>
const styles = Stylesheet.create({
root: {
background: 'transparent',
display: 'flex',
flexGrow: 1,
justifyContent: 'center'
}
});
```
## JS-to-CSS: conversion strategies
Mapping entire `style` objects to CSS rules can lead to increasingly large CSS
files. Each new component adds new rules to the stylesheet.
![](../static/styling-strategy.png)
One strategy for converting styles from JS to CSS is to map style objects to
CSS rules. Another strategy is to map declarations to declarations.
React Native for Web currently includes a proof-of-concept implementation of
the latter strategy. This results in smaller CSS files because all applications
has fewer unique declarations than total declarations. Creating a new component
with no new unique declarations results in no change to the CSS file.
For example:
```js
<Text style={styles.root}>...</Text>
const styles = {
root: {
background: 'transparent',
display: 'flex',
flexGrow: 1,
justifyContent: 'center'
}
};
```
Yields:
```html
<span className="_abcde _fghij _klmno _pqrst">...</span>
```
And is backed by:
```css
._abcde { background: transparent }
._fghij { display: flex }
._klmno { flex-grow: 1 }
._pqrst { justify-content: center }
```
The current implementation uses a precomputed CSS library of single-declaration
rules, with obfuscated selectors. This handles a signficant portion of possible
declarations. A build-time implementation would produce more accurate CSS
files and fall through to inline styles significantly less often.
(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
particular set of classes and features to learn. All of them require developers
to manually connect CSS classes for given styles.)
## Dynamic styles: use inline styles
Some styles cannot be resolved ahead of time and continue to rely on inline
styles:
```js
<View style={{ backgroundColor: (Math.random() > 0.5 ? 'red' : 'black') }}>...</Text>
```
## Media Queries, pseudo-classes, and pseudo-elements
Media Queries could be replaced with `mediaMatch`. This would have the added
benefit of co-locating breakpoint-specific DOM and style changes. Perhaps Media
Query data could be accessed on `this.content`?
Pseudo-classes like `:hover` and `:focus` can be handled with JavaScript.
Pseudo-elements should be avoided in general, but for particular cases like
`::placeholder` it might be necessary to reimplement it in the `TextInput`
component (see React Native's API).

View File

@@ -1,68 +1,42 @@
import React, { Image, Swipeable, Text, TextInput, Touchable, View } from '.'
import GridView from './GridView'
import Heading from './Heading'
import MediaQueryWidget from './MediaQueryWidget'
import React, { Image, StyleSheet, ScrollView, Text, TextInput, Touchable, View } from '../../src'
const { Component, PropTypes } = React
class Heading extends Component {
static propTypes = {
children: Text.propTypes.children,
level: PropTypes.oneOf(['1', '2', '3']),
size: PropTypes.oneOf(['xlarge', 'large', 'normal'])
}
static defaultProps = {
level: '1',
size: 'normal'
}
render() {
const { children, level, size } = this.props
return (
<Text
children={children}
component={`h${level}`}
style={headingStyles.size[size]}
/>
)
}
}
const headingStyles = {
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 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 and swipe handling, flexbox layout,
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>}
@@ -85,22 +59,7 @@ class Example extends Component {
testID='Example.image'
/>
<Heading level='2' size='large'>Swipeable</Heading>
<Swipeable
onSwiped={(e) => { console.log('Swipeable.onSwiped', e) }}
testID={'Example.swipeable'}
>
<View
style={{
backgroundColor: 'black',
alignSelf: 'center',
width: '200px',
height: '200px'
}}
/>
</Swipeable>
<Heading level='2' size='large'>Text</Heading>
<Heading size='large'>Text</Heading>
<Text
onPress={(e) => { console.log('Text.onPress', e) }}
testID={'Example.text'}
@@ -125,7 +84,7 @@ class Example extends 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) }}
@@ -136,10 +95,10 @@ class Example extends 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' />
<TextInput defaultValue='https://delete-me' keyboardType='url' placeholder='https://www.some-website.com' selectTextOnFocus />
<TextInput
defaultValue='default value'
maxNumberOfLines={10}
@@ -147,7 +106,7 @@ class Example extends Component {
numberOfLines={5}
/>
<Heading level='2' size='large'>Touchable</Heading>
<Heading size='large'>Touchable</Heading>
<Touchable
accessibilityLabel={'Touchable element'}
activeHighlight='lightblue'
@@ -162,8 +121,8 @@ class Example extends 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 (
@@ -174,7 +133,7 @@ class Example extends 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 (
@@ -185,13 +144,13 @@ class Example extends 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}
@@ -199,16 +158,70 @@ class Example extends 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>
)
}
}
const styles = {
const styles = StyleSheet.create({
root: {
maxWidth: '600px',
margin: '0 auto'
common: {
marginVertical: 0,
marginHorizontal: 'auto'
},
mqSmall: {
maxWidth: '400px'
},
mqLarge: {
maxWidth: '600px'
}
},
row: {
flexDirection: 'row',
@@ -218,7 +231,10 @@ const styles = {
alignItems: 'center',
flexGrow: 1,
justifyContent: 'center',
borderWidth: '1px'
borderWidth: 1
},
horizontalBox: {
width: '50px'
},
boxFull: {
width: '100%'
@@ -226,7 +242,7 @@ const styles = {
pointerEventsBox: {
alignItems: 'center',
borderWidth: '1px',
flexGrow: '1',
flexGrow: 1,
height: '100px',
justifyContent: 'center'
},
@@ -235,7 +251,14 @@ const styles = {
borderWidth: 1,
height: '200px',
justifyContent: 'center'
},
scrollViewContainer: {
height: '200px'
},
scrollViewStyle: {
borderWidth: '1px'
},
scrollViewContentContainerStyle: {
padding: '10px'
}
}
React.render(<Example />, document.getElementById('react-root'))
})

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,6 +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.6",
"version": "0.0.12",
"description": "React Native for Web",
"main": "dist/react-native-web.js",
"files": [
@@ -8,50 +8,63 @@
],
"scripts": {
"build": "rm -rf ./dist && webpack --config config/webpack.config.publish.js --sort-assets-by --progress",
"dev": "webpack-dev-server --config config/webpack.config.example.js --inline --colors --quiet",
"lint": "eslint config src",
"prepublish": "NODE_ENV=publish npm run build",
"test:specs": "NODE_ENV=test karma start config/karma.config.js",
"test:specs:watch": "npm run test:specs -- --no-single-run",
"test": "npm run test:specs && npm run lint"
"examples": "webpack-dev-server --config config/webpack.config.example.js --inline --hot --colors --quiet",
"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": {
"react": ">=0.13.3",
"react-swipeable": "^3.0.2",
"react-tappable": "^0.6.0",
"react-textarea-autosize": "^2.5.2"
"inline-style-prefixer": "^0.5.3",
"lodash.debounce": "^3.1.1",
"react-tappable": "^0.7.1",
"react-textarea-autosize": "^3.1.0"
},
"devDependencies": {
"autoprefixer-loader": "^3.1.0",
"babel-core": "^5.8.23",
"babel-eslint": "^4.1.1",
"babel-loader": "^5.3.2",
"babel-plugin-typecheck": "^1.2.0",
"babel-runtime": "^5.8.20",
"css-loader": "^0.18.0",
"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",
"extract-text-webpack-plugin": "^0.8.2",
"karma": "^0.13.9",
"karma-chrome-launcher": "^0.2.0",
"karma-firefox-launcher": "^0.1.6",
"karma-mocha": "^0.2.0",
"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",
"style-loader": "^0.12.3",
"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",
"repository": {
"type": "git",
"url": "git://github.com/necolas/react-native-web.git"
}
},
"tags": [
"react"
],
"keywords": [
"react",
"react-component",
"react-native",
"web"
]
}

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

@@ -1,39 +1,70 @@
import React, { PropTypes } from 'react'
import restyle from './modules/restyle'
import stylePropTypes from './modules/stylePropTypes'
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 stylePropTypes = stylePropTypes;
static defaultProps = {
className: '',
accessible: true,
component: 'div'
}
static stylePropTypes = StylePropTypes;
render() {
const {
className,
component: Component,
style,
accessibilityLabel,
accessibilityLiveRegion,
accessibilityRole,
accessible,
component,
testID,
type,
...other
} = this.props
const Component = roleComponents[accessibilityRole] || component
return (
<Component
{...other}
{...restyle({ className, style })}
{...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,47 +0,0 @@
export default function prefixStyles(style) {
if (style.hasOwnProperty('flexBasis')) {
style = {
WebkitFlexBasis: style.flexBasis,
msFlexBasis: style.flexBasis,
...style
}
}
if (style.hasOwnProperty('flexGrow')) {
style = {
WebkitBoxFlex: style.flexGrow,
WebkitFlexGrow: style.flexGrow,
msFlexPositive: style.flexGrow,
...style
}
}
if (style.hasOwnProperty('flexShrink')) {
style = {
WebkitFlexShrink: style.flexShrink,
msFlexNegative: style.flexShrink,
...style
}
}
// NOTE: adding `;` to the string value prevents React from automatically
// adding a `px` suffix to the unitless value
if (style.hasOwnProperty('order')) {
style = {
WebkitBoxOrdinalGroup: `${parseInt(style.order, 10) + 1};`,
WebkitOrder: `${style.order}`,
msFlexOrder: `${style.order}`,
...style
}
}
if (style.hasOwnProperty('transform')) {
style = {
WebkitTransform: style.transform,
msTransform: style.transform,
...style
}
}
return style
}

View File

@@ -1,40 +0,0 @@
import autoprefix from './autoprefix'
import styles from '../../../modules/styles'
/**
* Get the HTML class that corresponds to a style declaration
* @param prop {string} prop name
* @param style {Object} style
* @return {string} class name
*/
function getSinglePurposeClassName(prop, style) {
const className = `${prop}-${style[prop]}`
if (style.hasOwnProperty(prop) && styles[className]) {
return styles[className]
}
}
/**
* Replace inline styles with single purpose classes where possible
* @param props {Object} React Element properties
* @return {Object}
*/
export default function stylingStrategy(props) {
let className
let style = {}
const classList = [ props.className ]
for (const prop in props.style) {
const styleClass = getSinglePurposeClassName(prop, props.style)
if (styleClass) {
classList.push(styleClass)
} else {
style[prop] = props.style[prop]
}
}
className = classList.join(' ')
style = autoprefix(style)
return { className, style }
}

View File

@@ -1,73 +1,77 @@
import { assertProps, render, renderToDOM } from '../../modules/specHelpers'
/* eslint-env mocha */
import * as utils from '../../../modules/specHelpers'
import assert from 'assert'
import React from 'react/addons'
import React from 'react'
import Image from '.'
import Image from '../'
suite('Image', () => {
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.skip('prop "children"', () => { })
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)
utils.render(<Image
onError={onError}
source={{ uri: 'https://google.com/favicon.icox' }}
/>)
function onError(e) {
assert.equal(e.nativeEvent.type, 'error')
done()
}
render(<Image
onError={onError}
source={{ uri: 'https://google.com/favicon.icox' }}
/>)
})
test('prop "onLoad"', function (done) {
this.timeout(5000)
utils.render(<Image
onLoad={onLoad}
source={{ uri: 'https://google.com/favicon.ico' }}
/>)
function onLoad(e) {
assert.equal(e.nativeEvent.type, 'load')
done()
}
render(<Image
onLoad={onLoad}
source={{ uri: 'https://google.com/favicon.ico' }}
/>)
})
test.skip('prop "onLoadEnd"', () => { })
test('prop "onLoadEnd"')
test.skip('prop "onLoadStart"', () => { })
test('prop "onLoadStart"')
test.skip('prop "resizeMode"', () => { })
test('prop "resizeMode"')
test.skip('prop "source"', () => { })
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

@@ -1,5 +1,6 @@
/* global window */
import { pickProps } from '../../modules/filterObjectProps'
import StyleSheet from '../../modules/StyleSheet'
import CoreComponent from '../CoreComponent'
import ImageStylePropTypes from './ImageStylePropTypes'
import React, { PropTypes } from 'react'
@@ -13,13 +14,13 @@ const STATUS_IDLE = 'IDLE'
const imageStyleKeys = Object.keys(ImageStylePropTypes)
const styles = {
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,
@@ -49,23 +50,22 @@ const styles = {
backgroundSize: '100% 100%'
}
}
}
})
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,
@@ -83,7 +83,7 @@ class Image extends React.Component {
static defaultProps = {
accessible: true,
defaultSource: {},
resizeMode: 'stretch',
resizeMode: 'cover',
source: {},
style: styles.initial
}
@@ -101,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
}
}
@@ -123,8 +123,8 @@ class Image extends React.Component {
this._destroyImageLoader()
this.setState({ status: STATUS_LOADED })
this._onLoadEnd()
if (onLoad) onLoad(event)
this._onLoadEnd()
}
_onLoadEnd() {
@@ -193,12 +193,11 @@ class Image extends React.Component {
accessibilityLabel={accessibilityLabel}
accessibilityRole='img'
accessible={accessible}
component='div'
style={{
...(styles.initial),
...styles.initial,
...resolvedStyle,
...(backgroundImage && { backgroundImage }),
...(styles.resizeMode[resizeMode])
...styles.resizeMode[resizeMode]
}}
testID={testID}
>

View File

@@ -0,0 +1 @@
/* eslint-env mocha */

View File

@@ -1,18 +0,0 @@
/*
import assert from 'assert'
import React from 'react/addons'
import ListView from '.'
const ReactTestUtils = React.addons.TestUtils
function shallowRender(component, context = {}) {
const shallowRenderer = React.addons.TestUtils.createRenderer()
shallowRenderer.render(component, context)
return shallowRenderer.getRenderOutput()
}
suite.skip('ListView', () => {
test('prop "children"', () => {})
})
*/

View File

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

View File

@@ -0,0 +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,13 +0,0 @@
/*
import { assertProps, renderToDOM, shallowRender } from '../../modules/specHelpers'
import assert from 'assert'
import React from 'react/addons'
import ScrollView from '.'
const ReactTestUtils = React.addons.TestUtils
suite.skip('ScrollView', () => {
test('prop "children"', () => {})
})
*/

View File

@@ -1,2 +0,0 @@
import Swipeable from 'react-swipeable'
export default Swipeable

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

@@ -0,0 +1,55 @@
/* eslint-env mocha */
import * as utils from '../../../modules/specHelpers'
import assert from 'assert'
import React from 'react'
import ReactTestUtils from 'react-addons-test-utils'
import Text from '../'
suite('components/Text', () => {
test('prop "accessibilityLabel"', () => {
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"', () => {
const accessible = false
const result = utils.shallowRender(<Text accessible={accessible} />)
assert.equal(result.props.accessible, accessible)
})
test('prop "children"', () => {
const children = 'children'
const result = utils.shallowRender(<Text>{children}</Text>)
assert.equal(result.props.children, children)
})
test('prop "numberOfLines"')
test('prop "onPress"', (done) => {
const dom = utils.renderToDOM(<Text onPress={onPress} />)
ReactTestUtils.Simulate.click(dom)
function onPress(e) {
assert.ok(e.nativeEvent)
done()
}
})
test('prop "style"', () => {
utils.assertProps.style(Text)
})
test('prop "testID"', () => {
const testID = 'testID'
const result = utils.shallowRender(<Text testID={testID} />)
assert.equal(result.props.testID, testID)
})
})

View File

@@ -1,17 +1,19 @@
import { pickProps } from '../../modules/filterObjectProps'
import CoreComponent from '../CoreComponent'
import React, { PropTypes } from 'react'
import StyleSheet from '../../modules/StyleSheet'
import TextStylePropTypes from './TextStylePropTypes'
const textStyleKeys = Object.keys(TextStylePropTypes)
const styles = {
const styles = StyleSheet.create({
initial: {
color: 'inherit',
display: 'inline-block',
display: 'inline',
font: 'inherit',
margin: 0,
padding: 0,
textDecoration: 'none',
wordWrap: 'break-word'
},
singleLineStyle: {
@@ -20,15 +22,15 @@ const styles = {
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}
}
})
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),
@@ -40,7 +42,6 @@ class Text extends React.Component {
static defaultProps = {
_className: '',
accessible: true,
component: 'span',
style: styles.initial
}
@@ -51,14 +52,9 @@ class Text extends React.Component {
render() {
const {
_className,
accessibilityLabel,
accessible,
children,
component,
numberOfLines,
onPress,
style,
testID,
...other
} = this.props
@@ -68,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,49 +0,0 @@
import { assertProps, renderToDOM, shallowRender } from '../../modules/specHelpers'
import assert from 'assert'
import React from 'react/addons'
import Text from '.'
const ReactTestUtils = React.addons.TestUtils
suite('Text', () => {
test('prop "accessibilityLabel"', () => {
assertProps.accessibilityLabel(Text)
})
test('prop "accessible"', () => {
assertProps.accessible(Text)
})
test('prop "children"', () => {
const children = 'children'
const result = shallowRender(<Text>{children}</Text>)
assert.equal(result.props.children, children)
})
test('prop "component"', () => {
assertProps.component(Text, 'span')
})
test.skip('prop "numberOfLines"', () => {})
test('prop "onPress"', (done) => {
const dom = renderToDOM(<Text onPress={onPress} />)
ReactTestUtils.Simulate.click(dom)
function onPress(e) {
assert(true, 'the "onPress" callback was never called')
assert.ok(e.nativeEvent)
done()
}
})
test('prop "style"', () => {
assertProps.style(Text)
})
test('prop "testID"', () => {
assertProps.testID(Text)
})
})

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

@@ -0,0 +1,237 @@
/* eslint-env mocha */
import * as utils from '../../../modules/specHelpers'
import assert from 'assert'
import React from 'react'
import ReactTestUtils from 'react-addons-test-utils'
import TextInput from '../'
const findInput = (dom) => dom.querySelector('input, textarea')
const findShallowInput = (vdom) => vdom.props.children.props.children[0]
const findShallowPlaceholder = (vdom) => vdom.props.children.props.children[1]
suite('components/TextInput', () => {
test('prop "accessibilityLabel"', () => {
const accessibilityLabel = 'accessibilityLabel'
const result = utils.shallowRender(<TextInput accessibilityLabel={accessibilityLabel} />)
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
})
test('prop "autoComplete"', () => {
// off
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('autocomplete'), undefined)
// on
input = findInput(utils.renderToDOM(<TextInput autoComplete />))
assert.equal(input.getAttribute('autocomplete'), 'on')
})
test('prop "autoFocus"', () => {
// false
let input = findInput(utils.renderToDOM(<TextInput />))
assert.deepEqual(document.activeElement, document.body)
// true
input = findInput(utils.renderToDOM(<TextInput autoFocus />))
assert.deepEqual(document.activeElement, input)
})
utils.testIfDocumentFocused('prop "clearTextOnFocus"', () => {
const defaultValue = 'defaultValue'
// false
let input = findInput(utils.renderAndInject(<TextInput defaultValue={defaultValue} />))
input.focus()
assert.equal(input.value, defaultValue)
// true
input = findInput(utils.renderAndInject(<TextInput clearTextOnFocus defaultValue={defaultValue} />))
input.focus()
assert.equal(input.value, '')
})
test('prop "defaultValue"', () => {
const defaultValue = 'defaultValue'
const input = findShallowInput(utils.shallowRender(<TextInput defaultValue={defaultValue} />))
assert.equal(input.props.defaultValue, defaultValue)
})
test('prop "editable"', () => {
// true
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('readonly'), undefined)
// false
input = findInput(utils.renderToDOM(<TextInput editable={false} />))
assert.equal(input.getAttribute('readonly'), '')
})
test('prop "keyboardType"', () => {
// default
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
input = findInput(utils.renderToDOM(<TextInput keyboardType='email-address' />))
assert.equal(input.getAttribute('type'), 'email')
// numeric
input = findInput(utils.renderToDOM(<TextInput keyboardType='numeric' />))
assert.equal(input.getAttribute('type'), 'number')
// phone-pad
input = findInput(utils.renderToDOM(<TextInput keyboardType='phone-pad' />))
assert.equal(input.getAttribute('type'), 'tel')
// url
input = findInput(utils.renderToDOM(<TextInput keyboardType='url' />))
assert.equal(input.getAttribute('type'), 'url')
})
test('prop "maxLength"', () => {
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 generateValue = () => {
let str = ''
while (str.length < 100) str += 'x'
return str
}
let input = findInput(utils.renderAndInject(
<TextInput
maxNumberOfLines={3}
multiline
value={generateValue()}
/>
))
const height = input.getBoundingClientRect().height
// need a range because of cross-browser differences
assert.ok(height >= 42, height)
assert.ok(height <= 48, height)
})
test('prop "multiline"', () => {
// false
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.tagName, 'INPUT')
// true
input = findInput(utils.renderToDOM(<TextInput multiline />))
assert.equal(input.tagName, 'TEXTAREA')
})
test('prop "numberOfLines"', () => {
// missing multiline
let input = findInput(utils.renderToDOM(<TextInput numberOfLines={2} />))
assert.equal(input.tagName, 'INPUT')
// with multiline
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 >= 30, height)
assert.ok(height <= 36, height)
})
test('prop "onBlur"', (done) => {
const input = findInput(utils.renderToDOM(<TextInput onBlur={onBlur} />))
ReactTestUtils.Simulate.blur(input)
function onBlur(e) {
assert.ok(e)
done()
}
})
test('prop "onChange"', (done) => {
const input = findInput(utils.renderToDOM(<TextInput onChange={onChange} />))
ReactTestUtils.Simulate.change(input)
function onChange(e) {
assert.ok(e)
done()
}
})
test('prop "onChangeText"', (done) => {
const newText = 'newText'
const input = findInput(utils.renderToDOM(<TextInput onChangeText={onChangeText} />))
ReactTestUtils.Simulate.change(input, { target: { value: newText } })
function onChangeText(text) {
assert.equal(text, newText)
done()
}
})
test('prop "onFocus"', (done) => {
const input = findInput(utils.renderToDOM(<TextInput onFocus={onFocus} />))
ReactTestUtils.Simulate.focus(input)
function onFocus(e) {
assert.ok(e)
done()
}
})
test('prop "onLayout"')
test('prop "onSelectionChange"', (done) => {
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)
assert.equal(e.selectionStart, 0)
done()
}
})
test('prop "placeholder"', () => {
const placeholder = 'placeholder'
const result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} />))
assert.equal(result.props.children, placeholder)
})
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 input = findInput(utils.renderToDOM(<TextInput secureTextEntry />))
assert.equal(input.getAttribute('type'), 'password')
// ignored for multiline
input = findInput(utils.renderToDOM(<TextInput multiline secureTextEntry />))
assert.equal(input.getAttribute('type'), undefined)
})
utils.testIfDocumentFocused('prop "selectTextOnFocus"', () => {
const text = 'Text'
// false
let input = findInput(utils.renderAndInject(<TextInput defaultValue={text} />))
input.focus()
assert.equal(input.selectionEnd, 0)
assert.equal(input.selectionStart, 0)
// true
input = findInput(utils.renderAndInject(<TextInput defaultValue={text} selectTextOnFocus />))
input.focus()
assert.equal(input.selectionEnd, 4)
assert.equal(input.selectionStart, 0)
})
test('prop "style"', () => {
utils.assertProps.style(TextInput)
})
test('prop "testID"', () => {
const testID = 'testID'
const result = utils.shallowRender(<TextInput testID={testID} />)
assert.equal(result.props.testID, testID)
})
test('prop "value"', () => {
const value = 'value'
const input = findShallowInput(utils.shallowRender(<TextInput value={value} />))
assert.equal(input.props.value, value)
})
})

View File

@@ -1,27 +1,52 @@
import { pickProps } from '../../modules/filterObjectProps'
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 = {
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,
@@ -59,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 = React.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)
}
@@ -90,7 +121,9 @@ class TextInput extends React.Component {
render() {
const {
/* eslint-disable react/prop-types */
accessibilityLabel,
/* eslint-enable react/prop-types */
autoComplete,
autoFocus,
defaultValue,
@@ -100,11 +133,9 @@ class TextInput extends React.Component {
maxNumberOfLines,
multiline,
numberOfLines,
onBlur,
onChange,
onChangeText,
onSelectionChange,
placeholder,
placeholderTextColor,
secureTextEntry,
style,
testID,
@@ -124,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
@@ -134,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
}
@@ -167,9 +195,29 @@ class TextInput extends React.Component {
type
}
return (multiline
? <CoreComponent {...propsMultiline} />
: <CoreComponent {...propsSingleline} />
const props = multiline ? propsMultiline : propsSingleline
return (
<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,222 +0,0 @@
import * as utils from '../../modules/specHelpers'
import assert from 'assert'
import React from 'react/addons'
import TextInput from '.'
const ReactTestUtils = React.addons.TestUtils
suite('TextInput', () => {
test('prop "accessibilityLabel"', () => {
utils.assertProps.accessibilityLabel(TextInput)
})
test('prop "autoComplete"', () => {
// off
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('autocomplete'), undefined)
// on
dom = utils.renderToDOM(<TextInput autoComplete />)
assert.equal(dom.getAttribute('autocomplete'), 'on')
})
test('prop "autoFocus"', () => {
// false
let dom = utils.renderToDOM(<TextInput />)
assert.deepEqual(document.activeElement, document.body)
// true
dom = utils.renderToDOM(<TextInput autoFocus />)
assert.deepEqual(document.activeElement, dom)
})
test('prop "clearTextOnFocus"', () => {
const defaultValue = 'defaultValue'
utils.requiresFocus(() => {
// false
let dom = utils.renderAndInject(<TextInput defaultValue={defaultValue} />)
dom.focus()
assert.equal(dom.value, defaultValue)
// true
dom = utils.renderAndInject(<TextInput clearTextOnFocus defaultValue={defaultValue} />)
dom.focus()
assert.equal(dom.value, '')
})
})
test('prop "defaultValue"', () => {
const defaultValue = 'defaultValue'
const result = utils.shallowRender(<TextInput defaultValue={defaultValue} />)
assert.equal(result.props.defaultValue, defaultValue)
})
test('prop "editable"', () => {
// true
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('readonly'), undefined)
// false
dom = utils.renderToDOM(<TextInput editable={false} />)
assert.equal(dom.getAttribute('readonly'), '')
})
test('prop "keyboardType"', () => {
// default
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('type'), undefined)
dom = utils.renderToDOM(<TextInput keyboardType='default' />)
assert.equal(dom.getAttribute('type'), undefined)
// email-address
dom = utils.renderToDOM(<TextInput keyboardType='email-address' />)
assert.equal(dom.getAttribute('type'), 'email')
// numeric
dom = utils.renderToDOM(<TextInput keyboardType='numeric' />)
assert.equal(dom.getAttribute('type'), 'number')
// phone-pad
dom = utils.renderToDOM(<TextInput keyboardType='phone-pad' />)
assert.equal(dom.getAttribute('type'), 'tel')
// url
dom = utils.renderToDOM(<TextInput keyboardType='url' />)
assert.equal(dom.getAttribute('type'), 'url')
})
test('prop "maxLength"', () => {
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('maxlength'), undefined)
dom = utils.renderToDOM(<TextInput maxLength={10} />)
assert.equal(dom.getAttribute('maxlength'), '10')
})
test('prop "maxNumberOfLines"', () => {
const style = { borderWidth: 0, fontSize: 20, lineHeight: 1 }
const value = (() => {
let str = ''
while (str.length < 100) str += 'x'
return str
}())
let dom = utils.renderAndInject(
<TextInput
maxNumberOfLines={3}
multiline
style={style}
value={value}
/>
)
const height = dom.getBoundingClientRect().height
// need a range because of cross-browser differences
assert.ok(height >= 60, height)
assert.ok(height <= 66, height)
})
test('prop "multiline"', () => {
// false
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.tagName, 'INPUT')
// true
dom = utils.renderToDOM(<TextInput multiline />)
assert.equal(dom.tagName, 'TEXTAREA')
})
test('prop "numberOfLines"', () => {
const style = { borderWidth: 0, fontSize: 20, lineHeight: 1 }
// missing multiline
let dom = utils.renderToDOM(<TextInput numberOfLines={2} />)
assert.equal(dom.tagName, 'INPUT')
// with multiline
dom = utils.renderAndInject(<TextInput multiline numberOfLines={2} style={style} />)
assert.equal(dom.tagName, 'TEXTAREA')
const height = dom.getBoundingClientRect().height
// need a range because of cross-browser differences
assert.ok(height >= 40)
assert.ok(height <= 45)
})
test('prop "onBlur"', (done) => {
const input = utils.renderToDOM(<TextInput onBlur={onBlur} />)
ReactTestUtils.Simulate.blur(input)
function onBlur(e) {
assert.ok(e)
done()
}
})
test('prop "onChange"', (done) => {
const input = utils.renderToDOM(<TextInput onChange={onChange} />)
ReactTestUtils.Simulate.change(input)
function onChange(e) {
assert.ok(e)
done()
}
})
test('prop "onChangeText"', (done) => {
const newText = 'newText'
const input = utils.renderToDOM(<TextInput onChangeText={onChangeText} />)
ReactTestUtils.Simulate.change(input, { target: { value: newText } })
function onChangeText(text) {
assert.equal(text, newText)
done()
}
})
test('prop "onFocus"', (done) => {
const input = utils.renderToDOM(<TextInput onFocus={onFocus} />)
ReactTestUtils.Simulate.focus(input)
function onFocus(e) {
assert.ok(e)
done()
}
})
test.skip('prop "onLayout"', () => {})
test('prop "onSelectionChange"', (done) => {
const input = utils.renderAndInject(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />)
ReactTestUtils.Simulate.select(input, { target: { selectionStart: 0, selectionEnd: 3 } })
function onSelectionChange(e) {
assert.equal(e.selectionEnd, 3)
assert.equal(e.selectionStart, 0)
done()
}
})
test.skip('prop "placeholder"', () => {})
test.skip('prop "placeholderTextColor"', () => {})
test('prop "secureTextEntry"', () => {
let dom = utils.renderToDOM(<TextInput secureTextEntry />)
assert.equal(dom.getAttribute('type'), 'password')
// ignored for multiline
dom = utils.renderToDOM(<TextInput multiline secureTextEntry />)
assert.equal(dom.getAttribute('type'), undefined)
})
test('prop "selectTextOnFocus"', () => {
const text = 'Text'
utils.requiresFocus(() => {
// false
let dom = utils.renderAndInject(<TextInput defaultValue={text} />)
dom.focus()
assert.equal(dom.selectionEnd, 0)
assert.equal(dom.selectionStart, 0)
// true
dom = utils.renderAndInject(<TextInput defaultValue={text} selectTextOnFocus />)
dom.focus()
assert.equal(dom.selectionEnd, 4)
assert.equal(dom.selectionStart, 0)
})
})
test('prop "style"', () => {
utils.assertProps.style(TextInput)
})
test('prop "testID"', () => {
utils.assertProps.testID(TextInput)
})
test('prop "value"', () => {
const value = 'value'
const result = utils.shallowRender(<TextInput value={value} />)
assert.equal(result.props.value, value)
})
})

View File

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

View File

@@ -1,14 +1,15 @@
import React, { PropTypes } from 'react'
import Tappable from 'react-tappable'
import View from '../View'
import StyleSheet from '../../modules/StyleSheet'
const styles = {
const styles = StyleSheet.create({
initial: {
...View.defaultProps.style,
cursor: 'pointer',
userSelect: undefined
}
}
})
class Touchable extends React.Component {
constructor(props, context) {
@@ -24,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,
@@ -44,7 +45,6 @@ class Touchable extends React.Component {
accessibilityRole: 'button',
activeOpacity: 1,
activeUnderlayColor: 'transparent',
component: 'div',
delayLongPress: 1000,
delayPressIn: 0,
delayPressOut: 0,

View File

@@ -1,27 +0,0 @@
import { assertProps, shallowRender } from '../../modules/specHelpers'
import assert from 'assert'
import React from 'react/addons'
import Touchable from '.'
const children = <span style={{}}>children</span>
const requiredProps = { children }
suite('Touchable', () => {
test('prop "accessibilityLabel"', () => {
assertProps.accessibilityLabel(Touchable, requiredProps)
})
test('prop "accessibilityRole"', () => {
assertProps.accessibilityRole(Touchable, requiredProps)
})
test('prop "accessible"', () => {
assertProps.accessible(Touchable, requiredProps)
})
test('prop "children"', () => {
const result = shallowRender(<Touchable {...requiredProps} />)
assert.deepEqual(result.props.children, children)
})
})

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

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

View File

@@ -1,11 +1,12 @@
import { pickProps } from '../../modules/filterObjectProps'
import CoreComponent from '../CoreComponent'
import React, { PropTypes } from 'react'
import StyleSheet from '../../modules/StyleSheet'
import ViewStylePropTypes from './ViewStylePropTypes'
const viewStyleKeys = Object.keys(ViewStylePropTypes)
const styles = {
const styles = StyleSheet.create({
// https://github.com/facebook/css-layout#default-values
initial: {
alignItems: 'stretch',
@@ -20,23 +21,23 @@ const styles = {
margin: 0,
padding: 0,
position: 'relative',
textDecoration: 'none',
// button reset
backgroundColor: 'transparent',
color: 'inherit',
font: 'inherit',
textAlign: 'inherit'
}
}
})
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
@@ -47,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
@@ -71,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

@@ -1,46 +0,0 @@
import { assertProps, shallowRender } from '../../modules/specHelpers'
import assert from 'assert'
import React from 'react/addons'
import View from '.'
suite('View', () => {
test('prop "accessibilityLabel"', () => {
assertProps.accessibilityLabel(View)
})
test('prop "accessibilityLiveRegion"', () => {
assertProps.accessibilityLiveRegion(View)
})
test('prop "accessibilityRole"', () => {
assertProps.accessibilityRole(View)
})
test('prop "accessible"', () => {
assertProps.accessible(View)
})
test('prop "children"', () => {
const children = 'children'
const result = 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' />)
assert.equal(result.props.style.pointerEvents, 'box-only')
})
test('prop "style"', () => {
assertProps.style(View)
})
test('prop "testID"', () => {
assertProps.testID(View)
})
})

View File

@@ -1,24 +1,31 @@
import React from 'react'
import StyleSheet from './modules/StyleSheet'
// components
import Image from './components/Image'
import ListView from './components/ListView'
import ScrollView from './components/ScrollView'
import Swipeable from './components/Swipeable'
import Text from './components/Text'
import TextInput from './components/TextInput'
import Touchable from './components/Touchable'
import View from './components/View'
export default React
export {
const ReactNative = {
// components
Image,
ListView,
ScrollView,
Swipeable,
Text,
TextInput,
Touchable,
View
View,
// apis
StyleSheet,
// React
...React
}
module.exports = ReactNative

View File

@@ -1,16 +1,17 @@
import { PropTypes } from 'react'
const numberOrString = PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
])
const { string } = PropTypes
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,
@@ -20,40 +21,42 @@ export default {
backgroundPosition: string,
backgroundRepeat: string,
backgroundSize: string,
borderColor: numberOrString,
borderBottomColor: numberOrString,
borderLeftColor: numberOrString,
borderRightColor: numberOrString,
borderTopColor: numberOrString,
borderColor: string,
borderBottomColor: string,
borderLeftColor: string,
borderRightColor: string,
borderTopColor: string,
borderRadius: numberOrString,
borderTopLeftRadius: numberOrString,
borderTopRightRadius: numberOrString,
borderBottomLeftRadius: numberOrString,
borderBottomRightRadius: numberOrString,
borderStyle: numberOrString,
borderBottomStyle: numberOrString,
borderLeftStyle: numberOrString,
borderRightStyle: numberOrString,
borderTopStyle: numberOrString,
borderStyle: string,
borderBottomStyle: string,
borderLeftStyle: string,
borderRightStyle: string,
borderTopStyle: string,
borderWidth: numberOrString,
borderBottomWidth: numberOrString,
borderLeftWidth: numberOrString,
borderRightWidth: numberOrString,
borderTopWidth: numberOrString,
bottom: numberOrString,
boxShadow: string,
boxSizing: string,
clear: string,
color: string,
cursor: string,
direction: string,
display: 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,
@@ -63,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

@@ -0,0 +1,99 @@
import hyphenate from './hyphenate'
import normalizeValue from './normalizeValue'
import prefixer from './prefixer'
export default class Store {
constructor(
initialState:Object = {},
options:Object = { obfuscateClassNames: false }
) {
this._counter = 0
this._classNames = { ...initialState.classNames }
this._declarations = { ...initialState.declarations }
this._options = options
}
get(property, value) {
const normalizedValue = normalizeValue(property, value)
const key = this._getDeclarationKey(property, normalizedValue)
return this._classNames[key]
}
set(property, value) {
if (value != null) {
const normalizedValue = normalizeValue(property, value)
const values = this._getPropertyValues(property) || []
if (values.indexOf(normalizedValue) === -1) {
values.push(normalizedValue)
this._setClassName(property, normalizedValue)
this._setPropertyValues(property, values)
}
}
}
toString() {
const obfuscate = this._options.obfuscateClassNames
// sort the properties to ensure shorthands are first in the cascade
const properties = Object.keys(this._declarations).sort()
// transform the class name to a valid CSS selector
const getCssSelector = (property, value) => {
let className = this.get(property, value)
if (!obfuscate && className) {
className = className.replace(/[,":?.%\\$#]/g, '\\$&')
}
return className
}
// transform the declarations into CSS rules with vendor-prefixes
const buildCSSRules = (property, values) => {
return values.reduce((cssRules, value) => {
const declarations = prefixer.prefix({ [property]: value })
const cssDeclarations = Object.keys(declarations).reduce((str, prop) => {
str += `${hyphenate(prop)}:${value};`
return str
}, '')
const selector = getCssSelector(property, value)
cssRules += `\n.${selector}{${cssDeclarations}}`
return cssRules
}, '')
}
const css = properties.reduce((css, property) => {
const values = this._declarations[property]
css += buildCSSRules(property, values)
return css
}, '')
return (`/* ${this._counter} unique declarations */${css}`)
}
_getDeclarationKey(property, value) {
return `${property}:${value}`
}
_getPropertyValues(property) {
return this._declarations[property]
}
_setPropertyValues(property, values) {
this._declarations[property] = values.map(value => normalizeValue(property, value))
}
_setClassName(property, value) {
const key = this._getDeclarationKey(property, value)
const exists = !!this._classNames[key]
if (!exists) {
this._counter += 1
if (this._options.obfuscateClassNames) {
this._classNames[key] = `_s_${this._counter}`
} else {
const val = `${value}`.replace(/\s/g, '-')
this._classNames[key] = `${property}:${val}`
}
}
}
}

View File

@@ -0,0 +1,128 @@
/* eslint-env mocha */
import assert from 'assert'
import Store from '../Store'
suite('modules/StyleSheet/Store', () => {
suite('the constructor', () => {
test('initialState', () => {
const initialState = { classNames: { 'alignItems:center': '__classname__' } }
const store = new Store(initialState)
assert.deepEqual(store._classNames['alignItems:center'], '__classname__')
})
})
suite('#get', () => {
test('returns a declaration-specific className', () => {
const initialState = {
classNames: {
'alignItems:center': '__expected__',
'alignItems:flex-start': '__error__'
}
}
const store = new Store(initialState)
assert.deepEqual(store.get('alignItems', 'center'), '__expected__')
})
})
suite('#set', () => {
test('stores declarations', () => {
const store = new Store()
store.set('alignItems', 'center')
store.set('flexGrow', 0)
store.set('flexGrow', 1)
store.set('flexGrow', 2)
assert.deepEqual(store._declarations, {
alignItems: [ 'center' ],
flexGrow: [ 0, 1, 2 ]
})
})
test('human-readable classNames', () => {
const store = new Store()
store.set('alignItems', 'center')
store.set('flexGrow', 0)
store.set('flexGrow', 1)
store.set('flexGrow', 2)
assert.deepEqual(store._classNames, {
'alignItems:center': 'alignItems:center',
'flexGrow:0': 'flexGrow:0',
'flexGrow:1': 'flexGrow:1',
'flexGrow:2': 'flexGrow:2'
})
})
test('obfuscated classNames', () => {
const store = new Store({}, { obfuscateClassNames: true })
store.set('alignItems', 'center')
store.set('flexGrow', 0)
store.set('flexGrow', 1)
store.set('flexGrow', 2)
assert.deepEqual(store._classNames, {
'alignItems:center': '_s_1',
'flexGrow:0': '_s_2',
'flexGrow:1': '_s_3',
'flexGrow:2': '_s_4'
})
})
test('value normalization', () => {
const store = new Store()
store.set('flexGrow', 0)
store.set('margin', 0)
assert.deepEqual(store._declarations, {
flexGrow: [ 0 ],
margin: [ '0px' ]
})
assert.deepEqual(store._classNames, {
'flexGrow:0': 'flexGrow:0',
'margin:0px': 'margin:0px'
})
})
test('replaces space characters', () => {
const store = new Store()
store.set('margin', '0 auto')
assert.equal(store.get('margin', '0 auto'), 'margin\:0-auto')
})
})
suite('#toString', () => {
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('width', '100%')
const expected = '/* 5 unique declarations */\n' +
'.alignItems\\:center{align-items:center;}\n' +
'.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)
})
test('obfuscated style sheet', () => {
const store = new Store({}, { obfuscateClassNames: true })
store.set('alignItems', 'center')
store.set('marginBottom', 0)
store.set('margin', 1)
store.set('margin', 2)
store.set('margin', 3)
const expected = '/* 5 unique declarations */\n' +
'._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

@@ -0,0 +1,36 @@
/* eslint-env mocha */
import assert from 'assert'
import getStyleObjects from '../getStyleObjects'
const fixture = {
rule: {
margin: 0,
padding: 0
},
nested: {
auto: {
backgroundSize: 'auto'
},
contain: {
backgroundSize: 'contain'
}
},
position: {
left: { left: 0 },
right: { right: 0 }
}
}
suite('modules/StyleSheet/getStyleObjects', () => {
test('returns only style objects', () => {
const actual = getStyleObjects(fixture)
assert.deepEqual(actual, [
{ margin: 0, padding: 0 },
{ backgroundSize: 'auto' },
{ backgroundSize: 'contain' },
{ left: 0 },
{ right: 0 }
])
})
})

View File

@@ -0,0 +1,16 @@
/* eslint-env mocha */
import assert from 'assert'
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('MozTransition'), '-moz-transition')
assert.equal(hyphenate('msTransition'), '-ms-transition')
assert.equal(hyphenate('WebkitTransition'), '-webkit-transition')
})
})

View File

@@ -0,0 +1,37 @@
/* eslint-env mocha */
import { resetCSS, predefinedCSS } from '../predefs'
import assert from 'assert'
import StyleSheet from '..'
const styles = { root: { borderWidth: 1 } }
suite('modules/StyleSheet', () => {
setup(() => {
StyleSheet.destroy()
})
test('create', () => {
assert.equal(StyleSheet.create(styles), styles)
})
test('renderToString', () => {
StyleSheet.create(styles)
assert.equal(
StyleSheet.renderToString(),
`${resetCSS}\n${predefinedCSS}\n` +
`/* 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 borderTopWidth:1px borderRightWidth:1px borderBottomWidth:1px borderLeftWidth:1px', style: {} }
StyleSheet.create(styles)
assert.deepEqual(StyleSheet.resolve(props), expected)
})
})

View File

@@ -0,0 +1,15 @@
/* eslint-env mocha */
import assert from 'assert'
import isObject from '../isObject'
suite('modules/StyleSheet/isObject', () => {
test('returns "true" for objects', () => {
assert.ok(isObject({}) === true)
})
test('returns "false" for non-objects', () => {
assert.ok(isObject(function () {}) === false)
assert.ok(isObject([]) === false)
assert.ok(isObject('') === false)
})
})

View File

@@ -0,0 +1,29 @@
/* eslint-env mocha */
import assert from 'assert'
import isStyleObject from '../isStyleObject'
const styles = {
root: {
margin: 0
},
align: {
left: {
textAlign: 'left'
},
right: {
textAlign: 'right'
}
}
}
suite('modules/StyleSheet/isStyleObject', () => {
test('returns "false" for non-style objects', () => {
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,13 @@
/* eslint-env mocha */
import assert from 'assert'
import normalizeValue from '../normalizeValue'
suite('modules/StyleSheet/normalizeValue', () => {
test('normalizes property values requiring units', () => {
assert.deepEqual(normalizeValue('margin', 0), '0px')
})
test('ignores unitless property values', () => {
assert.deepEqual(normalizeValue('flexGrow', 1), 1)
})
})

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

@@ -0,0 +1,22 @@
import isObject from './isObject'
import isStyleObject from './isStyleObject'
/**
* Recursively check for objects that are style rules.
*/
const getStyleObjects = (styles: Object): Array => {
const keys = Object.keys(styles)
return keys.reduce((rules, key) => {
const possibleRule = styles[key]
if (isObject(possibleRule)) {
if (isStyleObject(possibleRule)) {
rules.push(possibleRule)
} else {
rules = rules.concat(getStyleObjects(possibleRule))
}
}
return rules
}, [])
}
export default getStyleObjects

View File

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

View File

@@ -0,0 +1,88 @@
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
* event classes
*/
const initialState = { classNames: predefinedClassNames }
const options = { obfuscateClassNames: process.env.NODE_ENV === 'production' }
const createStore = () => new Store(initialState, options)
let store = createStore()
/**
* Process all unique declarations
*/
const create = (styles: Object): Object => {
const rules = getStyleObjects(styles)
rules.forEach((rule) => {
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
}
/**
* Destroy existing styles
*/
const destroy = () => {
store = createStore()
}
/**
* Render the styles as a CSS style sheet
*/
const renderToString = () => {
const css = store.toString()
return `${resetCSS}\n${predefinedCSS}\n${css}`
}
/**
* Accepts React props and converts inline styles to single purpose classes
* where possible.
*/
const resolve = ({ className = '', style = {} }) => {
let _className
let _style = {}
const expandedStyle = expandStyle(style)
const classList = [ className ]
for (const prop in expandedStyle) {
if (!StylePropTypes[prop]) {
continue
}
let styleClass = store.get(prop, expandedStyle[prop])
if (styleClass) {
classList.push(styleClass)
} else {
_style[prop] = expandedStyle[prop]
}
}
_className = classList.join(' ')
_style = prefixer.prefix(_style)
return { className: _className, style: _style }
}
export default {
create,
destroy,
renderToString,
resolve
}

View File

@@ -0,0 +1,5 @@
const isObject = (obj) => {
return Object.prototype.toString.call(obj) === '[object Object]'
}
export default isObject

View File

@@ -0,0 +1,11 @@
import isObject from './isObject'
const isStyleObject = (obj) => {
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

@@ -0,0 +1,33 @@
const unitlessNumbers = {
boxFlex: true,
boxFlexGroup: true,
columnCount: true,
flex: true,
flexGrow: true,
flexPositive: true,
flexShrink: true,
flexNegative: true,
fontWeight: true,
lineClamp: true,
lineHeight: true,
opacity: true,
order: true,
orphans: true,
widows: true,
zIndex: true,
zoom: true,
// SVG-related
fillOpacity: true,
strokeDashoffset: true,
strokeOpacity: true,
strokeWidth: true
}
const normalizeValues = (property, value) => {
if (!unitlessNumbers[property] && typeof value === 'number') {
value = `${value}px`
}
return value
}
export default normalizeValues

View File

@@ -0,0 +1,25 @@
/**
* Reset unwanted styles beyond the control of React inline styles
*/
export const resetCSS =
`/* React Native Web */
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}
ol,ul,li {list-style:none}`
/**
* Custom pointer event styles
*/
export const predefinedCSS =
`/* pointer-events */
._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': '_s_pe-a',
'pointerEvents:box-none': '_s_pe-bn',
'pointerEvents:box-only': '_s_pe-bo',
'pointerEvents:none': '_s_pe-n'
}

View File

@@ -0,0 +1,3 @@
import Prefixer from 'inline-style-prefixer'
const prefixer = new Prefixer()
export default prefixer

View File

@@ -1,4 +1,6 @@
import { omitProps, pickProps } from '.'
/* eslint-env mocha */
import { omitProps, pickProps } from '..'
import assert from 'assert'
suite('pickProps', () => {

View File

@@ -1,47 +1,11 @@
import assert from 'assert'
import React from 'react/addons'
/* eslint-env mocha */
const ReactTestUtils = React.addons.TestUtils
import assert from 'assert'
import React from 'react'
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
@@ -65,30 +29,15 @@ 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)
}
}
export function render(element, container) {
return container
? React.render(element, container)
? ReactDOM.render(element, container)
: ReactTestUtils.renderIntoDocument(element)
}
export function renderToDOM(element, container) {
const result = render(element, container)
return React.findDOMNode(result)
}
export function renderAndInject(element) {
const id = '_renderAndInject'
let div = document.getElementById(id)
@@ -102,22 +51,24 @@ export function renderAndInject(element) {
}
const result = render(element, div)
return React.findDOMNode(result)
return ReactDOM.findDOMNode(result)
}
export function requiresFocus(test, fallback) {
if (document.hasFocus && document.hasFocus()) {
test()
} else {
console.warn('Test was skipped as the document is not focused')
if (fallback) {
fallback()
}
}
export function renderToDOM(element, container) {
const result = render(element, container)
return ReactDOM.findDOMNode(result)
}
export function shallowRender(component, context = {}) {
const shallowRenderer = React.addons.TestUtils.createRenderer()
const shallowRenderer = ReactTestUtils.createRenderer()
shallowRenderer.render(component, context)
return shallowRenderer.getRenderOutput()
}
export function testIfDocumentFocused(message, fn) {
if (document.hasFocus && document.hasFocus()) {
test(message, fn)
} else {
test.skip(`${message} WARNING: document is not focused`)
}
}

View File

@@ -1,2 +0,0 @@
import styles from './styles.css'
export default styles

View File

@@ -1,688 +0,0 @@
/* align-content */
.alignContent-center { align-content: center; }
.alignContent-flex-end { align-content: flex-end; }
.alignContent-flex-start { align-content: flex-start; }
.alignContent-stretch { align-content: stretch; }
.alignContent-space-around { align-content: space-around; }
.alignContent-space-between { align-content: space-between; }
/* align-items */
.alignItems-center { align-items: center; }
.alignItems-flex-end { align-items: flex-end; }
.alignItems-flex-start { align-items: flex-start; }
.alignItems-stretch { align-items: stretch; }
.alignItems-space-around { align-items: space-around; }
.alignItems-space-between { align-items: space-between; }
/* align-self */
.alignSelf-auto { align-self: auto; }
.alignSelf-baseline { align-self: baseline; }
.alignSelf-center { align-self: center; }
.alignSelf-flex-end { align-self: flex-end; }
.alignSelf-flex-start { align-self: flex-start; }
.alignSelf-stretch { align-self: stretch; }
/* appearance */
.appearance-none { appearance: none; }
/* background-attachment */
.backgroundAttachment-fixed { background-attachment: fixed; }
.backgroundAttachment-inherit { background-attachment: inherit; }
.backgroundAttachment-local { background-attachment: local; }
.backgroundAttachment-scroll { background-attachment: scroll; }
/* background-clip */
.backgroundClip-border-box { background-clip: border-box; }
.backgroundClip-content-box { background-clip: content-box; }
.backgroundClip-inherit { background-clip: inherit; }
.backgroundClip-padding-box { background-clip: padding-box; }
/* background-color */
.backgroundColor-\#000,
.backgroundColor-black { background-color: black; }
.backgroundColor-\#fff,
.backgroundColor-white { background-color: white; }
.backgroundColor-currentcolor,
.backgroundColor-currentColor { background-color: currentcolor; }
.backgroundColor-inherit { background-color: inherit; }
.backgroundColor-transparent { background-color: transparent; }
/* background-image */
.backgroundImage { background-image: none; }
/* background-origin */
.backgroundOrigin-border-box { background-clip: border-box; }
.backgroundOrigin-content-box { background-clip: content-box; }
.backgroundOrigin-inherit { background-clip: inherit; }
.backgroundOrigin-padding-box { background-clip: padding-box; }
/* background-position */
.backgroundPosition-bottom { background-position: bottom; }
.backgroundPosition-center { background-position: center; }
.backgroundPosition-left { background-position: left; }
.backgroundPosition-right { background-position: right; }
.backgroundPosition-top { background-position: top; }
/* background-repeat */
.backgroundRepeat-inherit { background-repeat: inherit; }
.backgroundRepeat-no-repeat { background-repeat: no-repeat; }
.backgroundRepeat-repeat { background-repeat: repeat; }
.backgroundRepeat-repeat-x { background-repeat: repeat-x; }
.backgroundRepeat-repeat-y { background-repeat: repeat-y; }
.backgroundRepeat-round { background-repeat: round; }
.backgroundRepeat-space { background-repeat: space; }
/* background-size */
.backgroundSize-auto { background-size: auto; }
.backgroundSize-contain { background-size: contain; }
.backgroundSize-cover { background-size: cover; }
.backgroundSize-inherit { background-size: inherit; }
/* border-color */
.borderColor-\#fff,
.borderColor-white { border-color: white; }
.borderColor-currentcolor { border-color: currentcolor; }
.borderColor-inherit { border-color: inherit; }
.borderColor-transparent { border-color: transparent; }
/* border-bottom-color */
.borderBottomColor-\#fff,
.borderBottomColor-white { border-bottom-color: white; }
.borderBottomColor-currentcolor { border-bottom-color: currentcolor; }
.borderBottomColor-inherit { border-bottom-color: inherit; }
.borderBottomColor-transparent { border-bottom-color: transparent; }
/* border-left-color */
.borderLeftColor-\#fff,
.borderLeftColor-white { border-left-color: white; }
.borderLeftColor-currentcolor { border-left-color: currentcolor; }
.borderLeftColor-inherit { border-left-color: inherit; }
.borderLeftColor-transparent { border-left-color: transparent; }
/* border-right-color */
.borderRightColor-\#fff,
.borderRightColor-white { border-right-color: white; }
.borderRightColor-currentcolor { border-right-color: currentcolor; }
.borderRightColor-inherit { border-right-color: inherit; }
.borderRightColor-transparent { border-right-color: transparent; }
/* border-top-color */
.borderTopColor-\#fff,
.borderTopColor-white { border-top-color: white; }
.borderTopColor-currentcolor { border-top-color: currentcolor; }
.borderTopColor-inherit { border-top-color: inherit; }
.borderTopColor-transparent { border-top-color: transparent; }
/* border-style */
.borderStyle-dashed { border-style: dashed; }
.borderStyle-dotted { border-style: dotted; }
.borderStyle-inherit { border-style: inherit; }
.borderStyle-none { border-style: none; }
.borderStyle-solid { border-style: solid; }
/* border-bottom-style */
.borderBottomStyle-dashed { border-bottom-style: dashed; }
.borderBottomStyle-dotted { border-bottom-style: dotted; }
.borderBottomStyle-inherit { border-bottom-style: inherit; }
.borderBottomStyle-none { border-bottom-style: none; }
.borderBottomStyle-solid { border-bottom-style: solid; }
/* border-left-style */
.borderLeftStyle-dashed { border-left-style: dashed; }
.borderLeftStyle-dotted { border-left-style: dotted; }
.borderLeftStyle-inherit { border-left-style: inherit; }
.borderLeftStyle-none { border-left-style: none; }
.borderLeftStyle-solid { border-left-style: solid; }
/* border-right-style */
.borderRightStyle-dashed { border-right-style: dashed; }
.borderRightStyle-dotted { border-right-style: dotted; }
.borderRightStyle-inherit { border-right-style: inherit; }
.borderRightStyle-none { border-right-style: none; }
.borderRightStyle-solid { border-right-style: solid; }
/* border-top-style */
.borderTopStyle-dashed { border-top-style: dashed; }
.borderTopStyle-dotted { border-top-style: dotted; }
.borderTopStyle-inherit { border-top-style: inherit; }
.borderTopStyle-none { border-top-style: none; }
.borderTopStyle-solid { border-top-style: solid; }
/* border-width */
.borderWidth-0 { border-width: 0; }
.borderWidth-1px { border-width: 1px; }
.borderWidth-2px { border-width: 2px; }
.borderWidth-3px { border-width: 3px; }
.borderWidth-4px { border-width: 4px; }
.borderWidth-5px { border-width: 5px; }
/* border-bottom-width */
.borderBottomWidth-0 { border-bottom-width: 0; }
.borderBottomWidth-1px { border-bottom-width: 1px; }
.borderBottomWidth-2px { border-bottom-width: 2px; }
.borderBottomWidth-3px { border-bottom-width: 3px; }
.borderBottomWidth-4px { border-bottom-width: 4px; }
.borderBottomWidth-5px { border-bottom-width: 5px; }
/* border-left-width */
.borderLeftWidth-0 { border-left-width: 0; }
.borderLeftWidth-1px { border-left-width: 1px; }
.borderLeftWidth-2px { border-left-width: 2px; }
.borderLeftWidth-3px { border-left-width: 3px; }
.borderLeftWidth-4px { border-left-width: 4px; }
.borderLeftWidth-5px { border-left-width: 5px; }
/* border-right-width */
.borderRightWidth-0 { border-right-width: 0; }
.borderRightWidth-1px { border-right-width: 1px; }
.borderRightWidth-2px { border-right-width: 2px; }
.borderRightWidth-3px { border-right-width: 3px; }
.borderRightWidth-4px { border-right-width: 4px; }
.borderRightWidth-5px { border-right-width: 5px; }
/* border-top-width */
.borderTopWidth-0 { border-top-width: 0; }
.borderTopWidth-1px { border-top-width: 1px; }
.borderTopWidth-2px { border-top-width: 2px; }
.borderTopWidth-3px { border-top-width: 3px; }
.borderTopWidth-4px { border-top-width: 4px; }
.borderTopWidth-5px { border-top-width: 5px; }
/* bottom */
.bottom-0 { bottom: 0; }
.bottom-50% { bottom: 50%; }
.bottom-100% { bottom: 100%; }
/* box-sizing */
.boxSizing-border-box { box-sizing: border-box; }
.boxSizing-content-box { box-sizing: content-box; }
.boxSizing-inherit { box-sizing: inherit; }
.boxSizing-padding-box { box-sizing: padding-box; }
/* clear */
.clear-both { clear: both; }
.clear-inherit { clear: inherit; }
.clear-left { clear: left; }
.clear-none { clear: none; }
.clear-right { clear: right; }
/* color */
.color-#000,
.color-black { color: black; }
.color-\#fff,
.color-white { color: white; }
.color-inherit { color: inherit; }
.color-transparent { color: transparent; }
/* cursor */
.cursor-default { cursor: default; }
.cursor-pointer { cursor: pointer; }
/* direction */
.direction-inherit { direction: inherit; }
.direction-ltr { direction: ltr; }
.direction-rtl { direction: rtl; }
/* display */
.display-block { display: block; }
.display-contents { display: contents; }
.display-flex { display: flex; }
.display-grid { display: grid; }
.display-inherit { display: inherit; }
.display-initial { display: initial; }
.display-inline { display: inline; }
.display-inline-block { display: inline-block; }
.display-inline-flex { display: inline-flex; }
.display-inline-grid { display: inline-grid; }
.display-inline-table { display: inline-table; }
.display-list-item { display: list-item; }
.display-none { display: none; }
.display-table { display: table; }
.display-table-cell { display: table-cell; }
.display-table-column { display: table-column; }
.display-table-column-group { display: table-column-group; }
.display-table-footer-group { display: table-footer-group; }
.display-table-header-group { display: table-header-group; }
.display-table-row { display: table-row; }
.display-table-row-group { display: table-row-group; }
.display-unset { display: unset; }
/* flex-basis */
.flexBasis-0 { flex-basis: 0%; }
.flexBasis-auto { flex-basis: auto; }
/* flex-direction */
.flexDirection-column { flex-direction: column; }
.flexDirection-column-reverse { flex-direction: column-reverse; }
.flexDirection-row { flex-direction: row; }
.flexDirection-row-reverse { flex-direction: row-reverse; }
/* flex-grow */
.flexGrow-0 { flex-grow: 0; }
.flexGrow-1 { flex-grow: 1; }
.flexGrow-2 { flex-grow: 2; }
.flexGrow-3 { flex-grow: 3; }
.flexGrow-4 { flex-grow: 4; }
.flexGrow-5 { flex-grow: 5; }
.flexGrow-6 { flex-grow: 6; }
.flexGrow-7 { flex-grow: 7; }
.flexGrow-8 { flex-grow: 8; }
.flexGrow-9 { flex-grow: 9; }
.flexGrow-10 { flex-grow: 10; }
.flexGrow-11 { flex-grow: 11; }
/* flex-shrink */
.flexShrink-0 { flex-shrink: 0; }
.flexShrink-1 { flex-shrink: 1; }
.flexShrink-2 { flex-shrink: 2; }
.flexShrink-3 { flex-shrink: 3; }
.flexShrink-4 { flex-shrink: 4; }
.flexShrink-5 { flex-shrink: 5; }
.flexShrink-6 { flex-shrink: 6; }
.flexShrink-7 { flex-shrink: 7; }
.flexShrink-8 { flex-shrink: 8; }
.flexShrink-9 { flex-shrink: 9; }
.flexShrink-10 { flex-shrink: 10; }
.flexShrink-11 { flex-shrink: 11; }
/* flex-wrap */
.flexWrap-nowrap { flex-wrap: nowrap; }
.flexWrap-wrap { flex-wrap: wrap; }
.flexWrap-wrap-reverse { flex-wrap: wrap-reverse; }
/* float */
.float-left { float: left; }
.float-none { float: none; }
.float-right { float: right; }
/* font */
.font-inherit { font: inherit; }
/* font-family */
.fontFamily-inherit { font-family: inherit; }
.fontFamily-monospace { font-family: monospace; }
.fontFamily-sans-serif { font-family: sans-serif; }
.fontFamily-serif { font-family: serif; }
/* font-size */
.fontSize-100\% { font-size: 100%; }
.fontSize-inherit { font-size: inherit; }
/* font-style */
.fontStyle-inherit { font-style: inherit; }
.fontStyle-italic { font-style: italic; }
.fontStyle-normal { font-style: normal; }
.fontStyle-oblique { font-style: oblique; }
/* font-weight */
.fontWeight-100 { font-weight: 100; }
.fontWeight-200 { font-weight: 200; }
.fontWeight-300 { font-weight: 300; }
.fontWeight-400 { font-weight: 400; }
.fontWeight-500 { font-weight: 500; }
.fontWeight-600 { font-weight: 600; }
.fontWeight-700 { font-weight: 700; }
.fontWeight-800 { font-weight: 800; }
.fontWeight-900 { font-weight: 900; }
.fontWeight-bold { font-weight: bold; }
.fontWeight-bolder { font-weight: bolder; }
.fontWeight-inherit { font-weight: inherit; }
.fontWeight-lighter { font-weight: lighter; }
.fontWeight-normal { font-weight: normal; }
/* height */
.height-0 { height: 0; }
.height-10\% { height: 10%; }
.height-12\.5\% { height: 12.5%; }
.height-20\% { height: 20%; }
.height-25\% { height: 25%; }
.height-30\% { height: 30%; }
.height-37\.5\% { height: 37.5%; }
.height-40\% { height: 40%; }
.height-50\% { height: 50%; }
.height-60\% { height: 60%; }
.height-62\.5\% { height: 62.5%; }
.height-70\% { height: 70%; }
.height-75\% { height: 75%; }
.height-80\% { height: 80%; }
.height-87\.5\% { height: 87.5%; }
.height-90\% { height: 90%; }
.height-100\% { height: 100%; }
/* justify-content */
.justifyContent-center { justify-content: center; }
.justifyContent-flex-end { justify-content: flex-end; }
.justifyContent-flex-start { justify-content: flex-start; }
.justifyContent-space-around { justify-content: space-around; }
.justifyContent-space-between { justify-content: space-between; }
/* left */
.left-0 { left: 0; }
.left-50% { left: 50%; }
.left-100% { left: 100%; }
/* line-height */
.lineHeight-inherit { line-height: inherit; }
.lineHeight-normal { line-height: normal; }
/* list-style */
.listStyle-none { list-style: none; }
/* margin */
.margin-0 { margin: 0; }
.margin-auto { margin: auto; }
/* margin-bottom */
.marginBottom-auto { margin-bottom: auto; }
.marginBottom-0 { margin-bottom: 0; }
.marginBottom-1em { margin-bottom: 1em; }
.marginBottom-1rem { margin-bottom: 1rem; }
/* margin-left */
.marginLeft-auto { margin-left: auto; }
.marginLeft-0 { margin-left: 0; }
.marginLeft-1em { margin-left: 1em; }
.marginLeft-1rem { margin-left: 1rem; }
/* margin-right */
.marginRight-auto { margin-right: auto; }
.marginRight-0 { margin-right: 0; }
.marginRight-1em { margin-right: 1em; }
.marginRight-1rem { margin-right: 1rem; }
/* margin-top */
.marginTop-auto { margin-top: auto; }
.marginTop-0 { margin-top: 0; }
.marginTop-1em { margin-top: 1em; }
.marginTop-1rem { margin-top: 1rem; }
/* max-height */
.maxHeight-100\% { max-height: 100%; }
/* max-width */
.maxWidth-100\% { max-width: 100%; }
/* min-height */
.minHeight-100\% { min-height: 100%; }
/* min-width */
.minWidth-100\% { min-width: 100%; }
/* opacity */
.opacity-0 { opacity: 0; }
.opacity-0\.1 { opacity: 0.1; }
.opacity-0\.2 { opacity: 0.2; }
.opacity-0\.25 { opacity: 0.25; }
.opacity-0\.3 { opacity: 0.3; }
.opacity-0\.4 { opacity: 0.4; }
.opacity-0\.5 { opacity: 0.5; }
.opacity-0\.6 { opacity: 0.6; }
.opacity-0\.7 { opacity: 0.7; }
.opacity-0\.75 { opacity: 0.75; }
.opacity-0\.8 { opacity: 0.8; }
.opacity-0\.9 { opacity: 0.9; }
.opacity-1 { opacity: 1; }
/* order */
.order--1 { order: -1; }
.order-1 { order: 1; }
.order-2 { order: 2; }
.order-3 { order: 3; }
.order-4 { order: 4; }
.order-5 { order: 5; }
.order-6 { order: 6; }
.order-7 { order: 7; }
.order-8 { order: 8; }
.order-9 { order: 9; }
.order-10 { order: 10; }
.order-11 { order: 11; }
/* overflow */
.overflow-auto { overflow: auto; }
.overflow-hidden { overflow: hidden; }
.overflow-inherit { overflow: inherit; }
.overflow-scroll { overflow: scroll; }
.overflow-visible { overflow: visible; }
/* overflow-x */
.overflowX-auto { overflow-x: auto; }
.overflowX-hidden { overflow-x: hidden; }
.overflowX-inherit { overflow-x: inherit; }
.overflowX-scroll { overflow-x: scroll; }
.overflowX-visible { overflow-x: visible; }
/* overflow-y */
.overflowY-auto { overflow-y: auto; }
.overflowY-hidden { overflow-y: hidden; }
.overflowY-inherit { overflow-y: inherit; }
.overflowY-scroll { overflow-y: scroll; }
.overflowY-visible { overflow-y: visible; }
/* padding */
.padding-0 { padding: 0; }
.padding-1em { padding: 1em; }
.padding-1rem { padding: 1rem; }
/* padding-bottom */
.paddingBottom-0 { padding-bottom: 0; }
.paddingBottom-1em { padding-bottom: 1em; }
.paddingBottom-1rem { padding-bottom: 1rem; }
/* padding-left */
.paddingLeft-0 { padding-left: 0; }
.paddingLeft-1em { padding-left: 1em; }
.paddingLeft-1rem { padding-left: 1rem; }
/* padding-right */
.paddingRight-0 { padding-right: 0; }
.paddingRight-1em { padding-right: 1em; }
.paddingRight-1rem { padding-right: 1rem; }
/* padding-top */
.paddingTop-0 { padding-top: 0; }
.paddingTop-1em { padding-top: 1em; }
.paddingTop-1rem { padding-top: 1rem; }
/* pointer-events */
.pointerEvents-auto { pointer-events: auto; }
.pointerEvents-none { pointer-events: none; }
.pointerEvents-box-none { pointer-events: none; }
.pointerEvents-box-none * { pointer-events: auto;}
.pointerEvents-box-only { pointer-events: auto; }
.pointerEvents-box-only * { pointer-events: none; }
/* position */
.position-absolute { position: absolute; }
.position-fixed { position: fixed; }
.position-relative { position: relative; }
/* right */
.right-0 { right: 0; }
.right-50% { right: 50%; }
.right-100% { right: 100%; }
/* text-align */
.textAlign-center { text-align: center; }
.textAlign-end { text-align: end; }
.textAlign-inherit { text-align: inherit; }
.textAlign-left { text-align: left; }
.textAlign-right { text-align: right; }
.textAlign-justify { text-align: justify; }
.textAlign-start { text-align: start; }
/* text-decoration */
.textDecoration-inherit { text-decoration: inherit; }
.textDecoration-none { text-decoration: none; }
.textDecoration-underline { text-decoration: underline; }
/* text-overflow */
.textOverflow-clip { text-overflow: clip; }
.textOverflow-ellipsis { text-overflow: ellipsis; }
/* text-rendering */
.textRendering-auto { text-rendering: auto; }
.textRendering-geometricPrecision { text-rendering: geometricPrecision; }
.textRendering-inherit { text-rendering: inherit; }
.textRendering-optimizeLegibility { text-rendering: optimizeLegibility; }
.textRendering-optimizeSpeed { text-rendering: optimizeSpeed; }
/* text-transform */
.textTransform-capitalize { text-transform: capitalize; }
.textTransform-lowercase { text-transform: lowercase; }
.textTransform-none { text-transform: none; }
.textTransform-uppercase { text-transform: uppercase; }
/* top */
.top-0 { top: 0; }
.top-50% { top: 50%; }
.top-100% { top: 100%; }
/* unicode-bidi */
.unicodeBidi-bidi-override { unicode-bidi: bidi-override; }
.unicodeBidi-embed { unicode-bidi: embed; }
.unicodeBidi-inherit { unicode-bidi: inherit; }
.unicodeBidi-isolate { unicode-bidi: isolate; }
.unicodeBidi-isolate-override { unicode-bidi: isolate-override; }
.unicodeBidi-normal { unicode-bidi: normal; }
.unicodeBidi-plaintext { unicode-bidi: plaintext; }
/* user-select */
.userSelect-all { user-select: all; }
.userSelect-inherit { user-select: inherit; }
.userSelect-none { user-select: none; }
.userSelect-text { user-select: text; }
/* visibility */
.visibility-collapse { visibility: collapse; }
.visibility-hidden { visibility: hidden; }
.visibility-inherit { visibility: inherit; }
.visibility-visible { visibility: visible; }
/* white-space */
.whiteSpace-normal { white-space: normal; }
.whiteSpace-nowrap { white-space: nowrap; }
.whiteSpace-pre { white-space: pre; }
.whiteSpace-pre-line { white-space: pre-line; }
.whiteSpace-pre-wrap { white-space: pre-wrap; }
/* width */
.width-0 { width: 0; }
.width-10\% { width: 10%; }
.width-12\.5\% { width: 12.5%; }
.width-20\% { width: 20%; }
.width-25\% { width: 25%; }
.width-30\% { width: 30%; }
.width-37\.5\% { width: 37.5%; }
.width-40\% { width: 40%; }
.width-50\% { width: 50%; }
.width-60\% { width: 60%; }
.width-62\.5\% { width: 62.5%; }
.width-70\% { width: 70%; }
.width-75\% { width: 75%; }
.width-80\% { width: 80%; }
.width-87\.5\% { width: 87.5%; }
.width-90\% { width: 90%; }
.width-100\% { width: 100%; }
/* word-wrap */
.wordWrap-break-word { word-wrap: break-word; }
.wordWrap-normal { word-wrap: normal; }
/* z-index */
.zIndex--1 { z-index: -1; }
.zIndex-0 { z-index: 0; }
.zIndex-1 { z-index: 1; }
.zIndex-2 { z-index: 2; }
.zIndex-3 { z-index: 3; }
.zIndex-4 { z-index: 4; }
.zIndex-5 { z-index: 5; }
.zIndex-6 { z-index: 6; }
.zIndex-7 { z-index: 7; }
.zIndex-8 { z-index: 8; }
.zIndex-9 { z-index: 9; }
.zIndex-10 { z-index: 10; }

View File

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