Compare commits

..

53 Commits

Author SHA1 Message Date
Nicolas Gallagher
c65aa8a943 0.0.13 2015-12-27 12:05:35 +00:00
Nicolas Gallagher
0aa60a3c29 [fix] umd bundle 2015-12-27 12:04:40 +00:00
Nicolas Gallagher
8ac84f6da5 [change] StyleSheet: support code-splitting / export smaller API
Quick-fix for code-splitting support by updating the rendered style
sheet in place. Reduce the API to `create`, as the rest is now internal
to the framework.

Fix #34
2015-12-27 11:54:53 +00:00
Nicolas Gallagher
69166b1502 [fix] StyleSheet: support textAlign 'inherit' 2015-12-27 11:46:03 +00:00
Nicolas Gallagher
cc10de43eb [change] export or replace react-dom methods
This change adds the react-dom methods to the main export, since this is
a Web-only environment (React Native does something similar). It
augments the default render methods in order to move style sheet
management under the control of the library (necessary for
code-splitting support).

Relates to #52
2015-12-27 11:45:49 +00:00
Nicolas Gallagher
c850a5fa8c [add] CSS textShadow and reintroduce enums 2015-12-26 17:54:56 +00:00
Nicolas Gallagher
1efe5a533f [add] StyleSheet: support 'flex' style prop
Fix #63
2015-12-26 17:54:13 +00:00
Nicolas Gallagher
804132ce36 [fix] 'process.env.NODE_ENV' check
Use babel to transpile the source code without bundling it.
Use webpack to create a standalone, productionized UMD bundle.

Fix #50
2015-12-26 14:22:36 +00:00
Nicolas Gallagher
5335bcfd48 [chore] docs and formatting 2015-12-26 10:40:36 +00:00
Nicolas Gallagher
c0e7afc495 [change] Touchable: default prop values from RN Touchables 2015-12-22 00:15:48 +00:00
Nicolas Gallagher
fa88548c3c Update CONTRIBUTING and README with CodePen link 2015-12-21 23:50:58 +00:00
Nicolas Gallagher
39b2b2f979 Fix unreliable TextInput tests 2015-12-20 04:26:59 -08:00
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
61 changed files with 1573 additions and 757 deletions

View File

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

View File

@@ -1,25 +1,24 @@
# Contributing to this project
The issue tracker is the preferred channel for [bug reports](#bugs),
[features requests](#features) and [submitting pull
requests](#pull-requests).
[features requests](#features), and [submitting pull requests](#pull-requests).
<a name="bugs"></a>
## Bug reports
A bug is a _demonstrable problem_ that is caused by the code in the repository.
Good bug reports are extremely helpful - thank you!
Good bug reports are extremely helpful - thank you! You can compare the
behaviour against that expected with React Native by using the [React Native
Playground](https://rnplay.org/)
Guidelines for bug reports:
1. **Use the GitHub issue search** &mdash; check if the issue has already been
reported.
reported or fixed in `master`.
2. **Check if the issue has been fixed** &mdash; try to reproduce it using the
latest `master` or development branch in the repository.
3. **Isolate the problem** &mdash; create a [reduced test
case](http://css-tricks.com/reduced-test-cases/) and a live example.
2. **Isolate the problem** &mdash; create a [reduced test
case](http://css-tricks.com/reduced-test-cases/) using this
[codepen](https://codepen.io/necolas/pen/PZzwBR?editors=001).
A good bug report contains as much detail as possible. What is your
environment? What steps will reproduce the issue? What browser(s) and OS
@@ -49,9 +48,9 @@ Example:
## Feature requests
Feature requests are welcome. But take a moment to find out whether your idea
fits with the scope and aims of the project. It's up to *you* to make a strong
case to convince the project's developers of the merits of this feature. Please
provide as much detail and context as possible.
fits with the scope and aims of the project (i.e., is this for parity with
React Native? does it make sense on the Web?). Please provide as much detail
and context as you think is necessary to make your case.
<a name="pull-requests"></a>
@@ -70,7 +69,8 @@ Development commands:
* `npm run build` build the library
* `npm run examples` start the dev server and develop against live examples
* `npm run lint` run the linter
* `npm run test` run the linter and unit tests
* `npm run test:watch` run and watch the unit tests
* `npm test` run the linter and unit tests
Please follow this process for submitting a patch:

View File

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

View File

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

View File

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

View File

@@ -1,37 +0,0 @@
var webpack = require('webpack')
var DedupePlugin = webpack.optimize.DedupePlugin
var OccurenceOrderPlugin = webpack.optimize.OccurenceOrderPlugin
var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin
var plugins = [
new DedupePlugin(),
new OccurenceOrderPlugin()
]
if (process.env.NODE_ENV === 'production') {
plugins.push(
new UglifyJsPlugin({
compress: {
dead_code: true,
drop_console: true,
screw_ie8: true,
warnings: true
}
})
)
}
module.exports = {
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: { cacheDirectory: true }
}
]
},
plugins: plugins
}

View File

@@ -1,17 +1,31 @@
var assign = require('object-assign')
var base = require('./webpack.config.base')
var constants = require('./constants')
var path = require('path')
var webpack = require('webpack')
module.exports = assign({}, base, {
module.exports = {
devServer: {
contentBase: constants.SRC_DIRECTORY
contentBase: constants.EXAMPLES_DIRECTORY
},
entry: {
example: path.join(constants.SRC_DIRECTORY, 'example')
example: constants.EXAMPLES_DIRECTORY
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: { cacheDirectory: true }
}
]
},
output: {
filename: 'example.js',
path: constants.DIST_DIRECTORY
}
})
filename: 'examples.js'
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
}),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurenceOrderPlugin()
]
}

32
config/webpack.config.js Normal file
View File

@@ -0,0 +1,32 @@
var constants = require('./constants')
var webpack = require('webpack')
module.exports = {
entry: {
main: constants.DIST_DIRECTORY
},
externals: [{
'react': true,
'react-dom': true,
'react-dom/server': true
}],
output: {
filename: 'react-native-web.js',
library: 'ReactNativeWeb',
libraryTarget: 'umd',
path: constants.DIST_DIRECTORY
},
plugins: [
new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({
compress: {
dead_code: true,
drop_console: true,
screw_ie8: true,
warnings: true
}
})
]
}

View File

@@ -1,19 +0,0 @@
var assign = require('object-assign')
var base = require('./webpack.config.base')
var constants = require('./constants')
module.exports = assign({}, base, {
entry: {
main: constants.SRC_DIRECTORY
},
externals: [{
'react': true,
'react-dom': true
}],
output: {
filename: 'react-native-web.js',
library: 'ReactNativeWeb',
libraryTarget: 'commonjs2',
path: constants.DIST_DIRECTORY
}
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,11 @@
# Text
`Text` is component for displaying text. It supports style, basic touch
handling, and inherits typographic styles from ancestor elements. In a
divergence from React Native, components other than `Text` can be children of a
`Text` component.
handling, and inherits typographic styles from ancestor elements.
The `Text` is unique relative to layout: child elements use text layout
(`inline-block`) rather than flexbox layout. This means that elements inside of
a `Text` are not rectangles, as they wrap when reaching the edge of their
(`inline`) rather than flexbox layout. This means that elements inside of a
`Text` are not rectangles, as they wrap when reaching the edge of their
container.
Unsupported React Native props:
@@ -23,6 +21,17 @@ NOTE: `Text` will transfer all other props to the rendered HTML element.
Defines the text available to assistive technologies upon interaction with the
element. (This is implemented using `aria-label`.)
(web) **accessibilityRole**: oneOf(roles)
Allows assistive technologies to present and support interaction with the view
in a manner that is consistent with user expectations for similar views of that
type. For example, marking a touchable view with an `accessibilityRole` of
`button`. (This is implemented using [ARIA roles](http://www.w3.org/TR/wai-aria/roles#role_definitions)).
Note: Avoid changing `accessibilityRole` values over time or after user
actions. Generally, accessibility APIs do not provide a means of notifying
assistive technologies of a `role` value change.
(web) **accessible**: bool = true
When `false`, the text is hidden from assistive technologies. (This is
@@ -32,10 +41,6 @@ implemented using `aria-hidden`.)
Child content.
(web) **component**: function | string = 'span'
Backing component.
**numberOfLines**: number
Truncates the text with an ellipsis after this many lines. Currently only supports `1`.
@@ -46,22 +51,21 @@ 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`
+ `textShadow`
+ `textTransform`
+ `whiteSpace`
+ `wordWrap`
+ `writingDirection`
**testID**: string
@@ -70,18 +74,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 +99,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 +124,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

@@ -20,7 +20,7 @@ Unsupported React Native props:
Overrides the text that's read by the screen reader when the user interacts
with the element.
(web) **accessibilityRole**: oneOf(roles)
(web) **accessibilityRole**: oneOf(roles) = 'button'
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
@@ -35,12 +35,12 @@ assistive technologies of a `role` value change.
When `false`, the view is hidden from screenreaders.
**activeOpacity**: number = 1
**activeOpacity**: number = 0.8
Sets the opacity of the child view when `onPressIn` is called. The opacity is
reset when `onPressOut` is called.
(web) **activeUnderlayColor**: string = 'transparent'
(web) **activeUnderlayColor**: string = 'black'
Sets the color of the background highlight when `onPressIn` is called. The
highlight is removed when `onPressOut` is called.
@@ -49,7 +49,7 @@ highlight is removed when `onPressOut` is called.
A single child element.
**delayLongPress**: number = 1000
**delayLongPress**: number = 500
Delay in ms, from `onPressIn`, before `onLongPress` is called.
@@ -59,7 +59,7 @@ Delay in ms, from `onPressIn`, before `onLongPress` is called.
Delay in ms, from the start of the touch, before `onPressIn` is called.
**delayPressOut**: number = 0
**delayPressOut**: number = 100
(TODO)
@@ -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)
@@ -104,6 +100,7 @@ from `style`.
+ `boxShadow`
+ `boxSizing`
+ `cursor`
+ `flex` (number)
+ `flexBasis`
+ `flexDirection`
+ `flexGrow`
@@ -112,7 +109,13 @@ from `style`.
+ `height`
+ `justifyContent`
+ `left`
+ `margin`
+ `margin` (single value)
+ `marginBottom`
+ `marginHorizontal`
+ `marginLeft`
+ `marginRight`
+ `marginTop`
+ `marginVertical`
+ `maxHeight`
+ `maxWidth`
+ `minHeight`
@@ -122,7 +125,13 @@ from `style`.
+ `overflow`
+ `overflowX`
+ `overflowY`
+ `padding`
+ `padding` (single value)
+ `paddingBottom`
+ `paddingHorizontal`
+ `paddingLeft`
+ `paddingRight`
+ `paddingTop`
+ `paddingVertical`
+ `position`
+ `right`
+ `top`
@@ -159,11 +168,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 +184,12 @@ class Example extends Component {
}
}
const styles = {
const styles = StyleSheet.create({
row: {
flexDirection: 'row'
},
cell: {
flexGrow: 1
}
}
export default Example
})
```

View File

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

View File

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

View File

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

View File

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

6
examples/index.html Normal file
View File

@@ -0,0 +1,6 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>React Native for Web</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="react-root"></div>
<script src="/examples.js"></script>

19
examples/index.js Normal file
View File

@@ -0,0 +1,19 @@
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 from '../src'
const mediaQueries = {
small: '(min-width: 300px)',
medium: '(min-width: 400px)',
large: '(min-width: 500px)'
}
const ResponsiveApp = matchMedia()(App)
React.render(
<MediaProvider getMedia={createGetter(mediaQueries)} listener={createListener(mediaQueries)}>
<ResponsiveApp />
</MediaProvider>,
document.getElementById('react-root')
)

View File

@@ -1,51 +1,57 @@
{
"name": "react-native-web",
"version": "0.0.7",
"version": "0.0.13",
"description": "React Native for Web",
"main": "dist/react-native-web.js",
"main": "dist/index.js",
"files": [
"dist"
],
"scripts": {
"build": "rm -rf ./dist && webpack --config config/webpack.config.publish.js --sort-assets-by --progress",
"build": "rm -rf ./dist && mkdir dist && babel src -d dist --ignore **/__tests__,src/modules/specHelpers",
"build:umd": "webpack --config config/webpack.config.js --sort-assets-by --progress",
"examples": "webpack-dev-server --config config/webpack.config.example.js --inline --hot --colors --quiet",
"lint": "eslint config src",
"prepublish": "NODE_ENV=publish npm run build",
"lint": "eslint config examples src",
"prepublish": "npm run build && npm run build:umd",
"test": "npm run lint && npm run test:unit",
"test:unit": "karma start config/karma.config.js",
"test:watch": "npm run test:unit -- --no-single-run"
},
"dependencies": {
"inline-style-prefixer": "^0.3.3",
"inline-style-prefixer": "^0.5.3",
"lodash.debounce": "^3.1.1",
"react-tappable": "^0.7.1",
"react-textarea-autosize": "^3.0.0"
"react-textarea-autosize": "^3.1.0"
},
"devDependencies": {
"babel-core": "^5.8.23",
"babel-eslint": "^4.1.1",
"babel-loader": "^5.3.2",
"babel-runtime": "^5.8.20",
"eslint": "^1.3.1",
"eslint-config-standard": "^4.3.1",
"eslint-config-standard-react": "^1.0.4",
"eslint-plugin-react": "^3.3.1",
"eslint-plugin-standard": "^1.3.0",
"karma": "^0.13.9",
"karma-browserstack-launcher": "^0.1.5",
"karma-chrome-launcher": "^0.2.0",
"karma-firefox-launcher": "^0.1.6",
"karma-mocha": "^0.2.0",
"karma-mocha-reporter": "^1.1.1",
"karma-sourcemap-loader": "^0.3.5",
"babel-cli": "^6.3.17",
"babel-core": "^6.3.13",
"babel-eslint": "^4.1.6",
"babel-loader": "^6.2.0",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"babel-preset-stage-1": "^6.3.13",
"babel-runtime": "^6.3.19",
"eslint": "^1.10.3",
"eslint-config-standard": "^4.4.0",
"eslint-config-standard-react": "^1.2.1",
"eslint-plugin-react": "^3.13.1",
"eslint-plugin-standard": "^1.3.1",
"karma": "^0.13.16",
"karma-browserstack-launcher": "^0.1.8",
"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",
"object-assign": "^4.0.1",
"react": "^0.14.0",
"react-addons-test-utils": "^0.14.0",
"react-dom": "^0.14.0",
"webpack": "^1.12.1",
"webpack-dev-server": "^1.10.1"
"mocha": "^2.3.4",
"node-libs-browser": "^0.5.3",
"react": "^0.14.3",
"react-addons-test-utils": "^0.14.3",
"react-dom": "^0.14.3",
"react-media-queries": "^2.0.1",
"webpack": "^1.12.9",
"webpack-dev-server": "^1.14.0"
},
"author": "Nicolas Gallagher",
"license": "MIT",

View File

@@ -0,0 +1,53 @@
/* eslint-env mocha */
import assert from 'assert'
import React from '..'
suite('ReactNativeWeb', () => {
suite('exports', () => {
test('React', () => {
assert.ok(React)
})
test('ReactDOM methods', () => {
assert.ok(React.findDOMNode)
assert.ok(React.render)
assert.ok(React.unmountComponentAtNode)
})
test('ReactDOM/server methods', () => {
assert.ok(React.renderToString)
assert.ok(React.renderToStaticMarkup)
})
})
suite('render methods', () => {
const id = 'test'
let div
setup(() => {
div = document.createElement('div')
div.id = id
document.body.appendChild(div)
})
teardown(() => {
document.body.removeChild(div)
})
test('"render" creates style sheet', () => {
React.render(<div />, div)
assert.ok(document.getElementById('react-stylesheet'))
})
test('"renderToString" creates style sheet', () => {
const result = React.renderToString(<div />)
assert.ok(result.indexOf('react-stylesheet') > -1)
})
test('"renderToStaticMarkup" creates style sheet', () => {
const result = React.renderToStaticMarkup(<div />)
assert.ok(result.indexOf('react-stylesheet') > -1)
})
})
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,11 +3,12 @@ import ScrollView from '../ScrollView'
class ListView extends React.Component {
static propTypes = {
children: PropTypes.any
children: PropTypes.any,
style: PropTypes.style
}
static defaultProps = {
className: ''
style: {}
}
render() {

View File

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

View File

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

View File

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

View File

@@ -1,31 +1,23 @@
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',
'textShadow',
'textTransform',
'whiteSpace',
'wordWrap'
'wordWrap',
'writingDirection'
])
}

View File

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

View File

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

View File

@@ -1,20 +1,7 @@
import { pickProps } from '../../modules/filterObjectProps'
import View from '../View'
import CoreComponent from '../CoreComponent'
import React from 'react'
import Text from '../Text'
export default {
...(View.stylePropTypes),
...pickProps(CoreComponent.stylePropTypes, [
'color',
'direction',
'fontFamily',
'fontSize',
'fontStyle',
'fontWeight',
'letterSpacing',
'lineHeight',
'textAlign',
'textDecoration',
'textTransform'
])
...Text.stylePropTypes,
outline: React.PropTypes.string
}

View File

@@ -7,129 +7,134 @@ import ReactTestUtils from 'react-addons-test-utils'
import TextInput from '../'
const findInput = (dom) => dom.querySelector('input, textarea')
const findShallowInput = (vdom) => vdom.props.children.props.children[0]
const findShallowPlaceholder = (vdom) => vdom.props.children.props.children[1]
suite('components/TextInput', () => {
test('prop "accessibilityLabel"', () => {
utils.assertProps.accessibilityLabel(TextInput)
const accessibilityLabel = 'accessibilityLabel'
const result = utils.shallowRender(<TextInput accessibilityLabel={accessibilityLabel} />)
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
})
test('prop "autoComplete"', () => {
// off
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('autocomplete'), undefined)
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('autocomplete'), undefined)
// on
dom = utils.renderToDOM(<TextInput autoComplete />)
assert.equal(dom.getAttribute('autocomplete'), 'on')
input = findInput(utils.renderToDOM(<TextInput autoComplete />))
assert.equal(input.getAttribute('autocomplete'), 'on')
})
test('prop "autoFocus"', () => {
// false
let dom = utils.renderToDOM(<TextInput />)
let input = findInput(utils.renderToDOM(<TextInput />))
assert.deepEqual(document.activeElement, document.body)
// true
dom = utils.renderToDOM(<TextInput autoFocus />)
assert.deepEqual(document.activeElement, dom)
input = findInput(utils.renderToDOM(<TextInput autoFocus />))
assert.deepEqual(document.activeElement, input)
})
utils.testIfDocumentFocused('prop "clearTextOnFocus"', () => {
const defaultValue = 'defaultValue'
// false
let dom = utils.renderAndInject(<TextInput defaultValue={defaultValue} />)
dom.focus()
assert.equal(dom.value, defaultValue)
let input = findInput(utils.renderAndInject(<TextInput defaultValue={defaultValue} />))
input.focus()
assert.equal(input.value, defaultValue)
// true
dom = utils.renderAndInject(<TextInput clearTextOnFocus defaultValue={defaultValue} />)
dom.focus()
assert.equal(dom.value, '')
input = findInput(utils.renderAndInject(<TextInput clearTextOnFocus defaultValue={defaultValue} />))
input.focus()
assert.equal(input.value, '')
})
test('prop "defaultValue"', () => {
const defaultValue = 'defaultValue'
const result = utils.shallowRender(<TextInput defaultValue={defaultValue} />)
assert.equal(result.props.defaultValue, defaultValue)
const input = findShallowInput(utils.shallowRender(<TextInput defaultValue={defaultValue} />))
assert.equal(input.props.defaultValue, defaultValue)
})
test('prop "editable"', () => {
// true
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('readonly'), undefined)
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('readonly'), undefined)
// false
dom = utils.renderToDOM(<TextInput editable={false} />)
assert.equal(dom.getAttribute('readonly'), '')
input = findInput(utils.renderToDOM(<TextInput editable={false} />))
assert.equal(input.getAttribute('readonly'), '')
})
test('prop "keyboardType"', () => {
// default
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('type'), undefined)
dom = utils.renderToDOM(<TextInput keyboardType='default' />)
assert.equal(dom.getAttribute('type'), undefined)
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('type'), undefined)
input = findInput(utils.renderToDOM(<TextInput keyboardType='default' />))
assert.equal(input.getAttribute('type'), undefined)
// email-address
dom = utils.renderToDOM(<TextInput keyboardType='email-address' />)
assert.equal(dom.getAttribute('type'), 'email')
input = findInput(utils.renderToDOM(<TextInput keyboardType='email-address' />))
assert.equal(input.getAttribute('type'), 'email')
// numeric
dom = utils.renderToDOM(<TextInput keyboardType='numeric' />)
assert.equal(dom.getAttribute('type'), 'number')
input = findInput(utils.renderToDOM(<TextInput keyboardType='numeric' />))
assert.equal(input.getAttribute('type'), 'number')
// phone-pad
dom = utils.renderToDOM(<TextInput keyboardType='phone-pad' />)
assert.equal(dom.getAttribute('type'), 'tel')
input = findInput(utils.renderToDOM(<TextInput keyboardType='phone-pad' />))
assert.equal(input.getAttribute('type'), 'tel')
// url
dom = utils.renderToDOM(<TextInput keyboardType='url' />)
assert.equal(dom.getAttribute('type'), 'url')
input = findInput(utils.renderToDOM(<TextInput keyboardType='url' />))
assert.equal(input.getAttribute('type'), 'url')
})
test('prop "maxLength"', () => {
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.getAttribute('maxlength'), undefined)
dom = utils.renderToDOM(<TextInput maxLength={10} />)
assert.equal(dom.getAttribute('maxlength'), '10')
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('maxlength'), undefined)
input = findInput(utils.renderToDOM(<TextInput maxLength={10} />))
assert.equal(input.getAttribute('maxlength'), '10')
})
test('prop "maxNumberOfLines"', () => {
const style = { borderWidth: 0, fontSize: 20, lineHeight: 1 }
const value = (() => {
const generateValue = () => {
let str = ''
while (str.length < 100) str += 'x'
return str
}())
let dom = utils.renderAndInject(
}
const result = utils.shallowRender(
<TextInput
maxNumberOfLines={3}
multiline
style={style}
value={value}
value={generateValue()}
/>
)
const height = dom.getBoundingClientRect().height
// need a range because of cross-browser differences
assert.ok(height >= 60, height)
assert.ok(height <= 66, height)
assert.equal(findShallowInput(result).props.maxRows, 3)
})
test('prop "multiline"', () => {
// false
let dom = utils.renderToDOM(<TextInput />)
assert.equal(dom.tagName, 'INPUT')
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.tagName, 'INPUT')
// true
dom = utils.renderToDOM(<TextInput multiline />)
assert.equal(dom.tagName, 'TEXTAREA')
input = findInput(utils.renderToDOM(<TextInput multiline />))
assert.equal(input.tagName, 'TEXTAREA')
})
test('prop "numberOfLines"', () => {
const style = { borderWidth: 0, fontSize: 20, lineHeight: 1 }
// missing multiline
let dom = utils.renderToDOM(<TextInput numberOfLines={2} />)
assert.equal(dom.tagName, 'INPUT')
let input = findInput(utils.renderToDOM(<TextInput numberOfLines={2} />))
assert.equal(input.tagName, 'INPUT')
// with multiline
dom = utils.renderAndInject(<TextInput multiline numberOfLines={2} style={style} />)
assert.equal(dom.tagName, 'TEXTAREA')
const height = dom.getBoundingClientRect().height
// need a range because of cross-browser differences
assert.ok(height >= 40)
assert.ok(height <= 46)
input = findInput(utils.renderAndInject(<TextInput multiline numberOfLines={2} />))
assert.equal(input.tagName, 'TEXTAREA')
const result = utils.shallowRender(
<TextInput
multiline
numberOfLines={3}
/>
)
assert.equal(findShallowInput(result).props.minRows, 3)
})
test('prop "onBlur"', (done) => {
const input = utils.renderToDOM(<TextInput onBlur={onBlur} />)
const input = findInput(utils.renderToDOM(<TextInput onBlur={onBlur} />))
ReactTestUtils.Simulate.blur(input)
function onBlur(e) {
assert.ok(e)
@@ -138,7 +143,7 @@ suite('components/TextInput', () => {
})
test('prop "onChange"', (done) => {
const input = utils.renderToDOM(<TextInput onChange={onChange} />)
const input = findInput(utils.renderToDOM(<TextInput onChange={onChange} />))
ReactTestUtils.Simulate.change(input)
function onChange(e) {
assert.ok(e)
@@ -148,7 +153,7 @@ suite('components/TextInput', () => {
test('prop "onChangeText"', (done) => {
const newText = 'newText'
const input = utils.renderToDOM(<TextInput onChangeText={onChangeText} />)
const input = findInput(utils.renderToDOM(<TextInput onChangeText={onChangeText} />))
ReactTestUtils.Simulate.change(input, { target: { value: newText } })
function onChangeText(text) {
assert.equal(text, newText)
@@ -157,7 +162,7 @@ suite('components/TextInput', () => {
})
test('prop "onFocus"', (done) => {
const input = utils.renderToDOM(<TextInput onFocus={onFocus} />)
const input = findInput(utils.renderToDOM(<TextInput onFocus={onFocus} />))
ReactTestUtils.Simulate.focus(input)
function onFocus(e) {
assert.ok(e)
@@ -168,7 +173,7 @@ suite('components/TextInput', () => {
test('prop "onLayout"')
test('prop "onSelectionChange"', (done) => {
const input = utils.renderAndInject(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />)
const input = findInput(utils.renderAndInject(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />))
ReactTestUtils.Simulate.select(input, { target: { selectionStart: 0, selectionEnd: 3 } })
function onSelectionChange(e) {
assert.equal(e.selectionEnd, 3)
@@ -177,30 +182,42 @@ suite('components/TextInput', () => {
}
})
test('prop "placeholder"')
test('prop "placeholder"', () => {
const placeholder = 'placeholder'
const result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} />))
assert.equal(result.props.children, placeholder)
})
test('prop "placeholderTextColor"')
test('prop "placeholderTextColor"', () => {
const placeholder = 'placeholder'
let result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} />))
assert.equal(result.props.style.color, 'darkgray')
result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} placeholderTextColor='red' />))
assert.equal(result.props.style.color, 'red')
})
test('prop "secureTextEntry"', () => {
let dom = utils.renderToDOM(<TextInput secureTextEntry />)
assert.equal(dom.getAttribute('type'), 'password')
let input = findInput(utils.renderToDOM(<TextInput secureTextEntry />))
assert.equal(input.getAttribute('type'), 'password')
// ignored for multiline
dom = utils.renderToDOM(<TextInput multiline secureTextEntry />)
assert.equal(dom.getAttribute('type'), undefined)
input = findInput(utils.renderToDOM(<TextInput multiline secureTextEntry />))
assert.equal(input.getAttribute('type'), undefined)
})
utils.testIfDocumentFocused('prop "selectTextOnFocus"', () => {
const text = 'Text'
// false
let dom = utils.renderAndInject(<TextInput defaultValue={text} />)
dom.focus()
assert.equal(dom.selectionEnd, 0)
assert.equal(dom.selectionStart, 0)
let input = findInput(utils.renderAndInject(<TextInput defaultValue={text} />))
input.focus()
assert.equal(input.selectionEnd, 0)
assert.equal(input.selectionStart, 0)
// true
dom = utils.renderAndInject(<TextInput defaultValue={text} selectTextOnFocus />)
dom.focus()
assert.equal(dom.selectionEnd, 4)
assert.equal(dom.selectionStart, 0)
input = findInput(utils.renderAndInject(<TextInput defaultValue={text} selectTextOnFocus />))
input.focus()
assert.equal(input.selectionEnd, 4)
assert.equal(input.selectionStart, 0)
})
test('prop "style"', () => {
@@ -208,12 +225,14 @@ suite('components/TextInput', () => {
})
test('prop "testID"', () => {
utils.assertProps.testID(TextInput)
const testID = 'testID'
const result = utils.shallowRender(<TextInput testID={testID} />)
assert.equal(result.props.testID, testID)
})
test('prop "value"', () => {
const value = 'value'
const result = utils.shallowRender(<TextInput value={value} />)
assert.equal(result.props.value, value)
const input = findShallowInput(utils.shallowRender(<TextInput value={value} />))
assert.equal(input.props.value, value)
})
})

View File

@@ -3,27 +3,50 @@ import CoreComponent from '../CoreComponent'
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import StyleSheet from '../../modules/StyleSheet'
import Text from '../Text'
import TextareaAutosize from 'react-textarea-autosize'
import TextInputStylePropTypes from './TextInputStylePropTypes'
import View from '../View'
const textInputStyleKeys = Object.keys(TextInputStylePropTypes)
const styles = StyleSheet.create({
initial: {
...View.defaultProps.style,
borderColor: 'black',
borderWidth: 1
},
input: {
appearance: 'none',
backgroundColor: 'transparent',
borderColor: 'black',
borderWidth: '1px',
borderWidth: 0,
boxSizing: 'border-box',
color: 'inherit',
flexGrow: 1,
font: 'inherit',
padding: 0
padding: 0,
zIndex: 1
},
placeholder: {
bottom: 0,
color: 'darkgray',
left: 0,
overflow: 'hidden',
position: 'absolute',
right: 0,
top: 0,
whiteSpace: 'pre'
}
})
class TextInput extends React.Component {
constructor(props, context) {
super(props, context)
this.state = { showPlaceholder: !props.value && !props.defaultValue }
}
static propTypes = {
accessibilityLabel: PropTypes.string,
...View.propTypes,
autoComplete: PropTypes.bool,
autoFocus: PropTypes.bool,
clearTextOnFocus: PropTypes.bool,
@@ -61,20 +84,26 @@ class TextInput extends React.Component {
_onBlur(e) {
const { onBlur } = this.props
const value = e.target.value
this.setState({ showPlaceholder: value === '' })
if (onBlur) onBlur(e)
}
_onChange(e) {
const { onChange, onChangeText } = this.props
if (onChangeText) onChangeText(e.target.value)
const value = e.target.value
this.setState({ showPlaceholder: value === '' })
if (onChangeText) onChangeText(value)
if (onChange) onChange(e)
}
_onFocus(e) {
const { clearTextOnFocus, onFocus, selectTextOnFocus } = this.props
const node = ReactDOM.findDOMNode(this)
const node = ReactDOM.findDOMNode(this.refs.input)
const value = e.target.value
if (clearTextOnFocus) node.value = ''
if (selectTextOnFocus) node.select()
this.setState({ showPlaceholder: value === '' })
if (onFocus) onFocus(e)
}
@@ -92,7 +121,9 @@ class TextInput extends React.Component {
render() {
const {
/* eslint-disable react/prop-types */
accessibilityLabel,
/* eslint-enable react/prop-types */
autoComplete,
autoFocus,
defaultValue,
@@ -102,11 +133,9 @@ class TextInput extends React.Component {
maxNumberOfLines,
multiline,
numberOfLines,
onBlur,
onChange,
onChangeText,
onSelectionChange,
placeholder,
placeholderTextColor,
secureTextEntry,
style,
testID,
@@ -126,6 +155,10 @@ class TextInput extends React.Component {
case 'phone-pad':
type = 'tel'
break
case 'search':
case 'web-search':
type = 'search'
break
case 'url':
type = 'url'
break
@@ -136,23 +169,16 @@ class TextInput extends React.Component {
}
const propsCommon = {
'aria-label': accessibilityLabel,
autoComplete: autoComplete && 'on',
autoFocus,
className: 'TextInput',
defaultValue,
maxLength,
onBlur: onBlur && this._onBlur.bind(this),
onChange: (onChange || onChangeText) && this._onChange.bind(this),
onBlur: this._onBlur.bind(this),
onChange: this._onChange.bind(this),
onFocus: this._onFocus.bind(this),
onSelect: onSelectionChange && this._onSelectionChange.bind(this),
placeholder,
readOnly: !editable,
style: {
...styles.initial,
...resolvedStyle
},
testID,
style: { ...styles.input, outline: style.outline },
value
}
@@ -172,7 +198,26 @@ class TextInput extends React.Component {
const props = multiline ? propsMultiline : propsSingleline
return (
<CoreComponent {...props} />
<CoreComponent
accessibilityLabel={accessibilityLabel}
className='TextInput'
style={{
...styles.initial,
...resolvedStyle
}}
testID={testID}
>
<View style={{ flexGrow: 1 }}>
<CoreComponent {...props} ref='input' />
{placeholder && this.state.showPlaceholder && <Text
pointerEvents='none'
style={{
...styles.placeholder,
...(placeholderTextColor && { color: placeholderTextColor })
}}
>{placeholder}</Text>}
</View>
</CoreComponent>
)
}
}

View File

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

View File

@@ -25,9 +25,9 @@ class Touchable extends React.Component {
}
static propTypes = {
accessibilityLabel: PropTypes.string,
accessibilityRole: PropTypes.string,
accessible: PropTypes.bool,
accessibilityLabel: View.propTypes.accessibilityLabel,
accessibilityRole: View.propTypes.accessibilityRole,
accessible: View.propTypes.accessible,
activeOpacity: PropTypes.number,
activeUnderlayColor: PropTypes.string,
children: PropTypes.element,
@@ -43,12 +43,11 @@ class Touchable extends React.Component {
static defaultProps = {
accessibilityRole: 'button',
activeOpacity: 1,
activeUnderlayColor: 'transparent',
component: 'div',
delayLongPress: 1000,
activeOpacity: 0.8,
activeUnderlayColor: 'black',
delayLongPress: 500,
delayPressIn: 0,
delayPressOut: 0,
delayPressOut: 100,
style: styles.initial
}

View File

@@ -44,6 +44,7 @@ export default {
'boxShadow',
'boxSizing',
'cursor',
'flex',
'flexBasis',
'flexDirection',
'flexGrow',
@@ -54,6 +55,8 @@ export default {
'left',
// margin
'margin',
'marginHorizontal',
'marginVertical',
'marginBottom',
'marginLeft',
'marginRight',
@@ -70,6 +73,8 @@ export default {
'overflowY',
// padding
'padding',
'paddingHorizontal',
'paddingVertical',
'paddingBottom',
'paddingLeft',
'paddingRight',

View File

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

View File

@@ -21,6 +21,7 @@ const styles = StyleSheet.create({
margin: 0,
padding: 0,
position: 'relative',
textDecoration: 'none',
// button reset
backgroundColor: 'transparent',
color: 'inherit',
@@ -32,12 +33,11 @@ const styles = StyleSheet.create({
class View extends React.Component {
static propTypes = {
_className: PropTypes.string, // escape-hatch for code migrations
accessibilityLabel: PropTypes.string,
accessibilityLiveRegion: PropTypes.oneOf(['assertive', 'off', 'polite']),
accessibilityRole: PropTypes.string,
accessible: PropTypes.bool,
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
accessibilityLiveRegion: CoreComponent.propTypes.accessibilityLiveRegion,
accessibilityRole: CoreComponent.propTypes.accessibilityRole,
accessible: CoreComponent.propTypes.accessible,
children: PropTypes.any,
component: CoreComponent.propTypes.component,
pointerEvents: PropTypes.oneOf(['auto', 'box-none', 'box-only', 'none']),
style: PropTypes.shape(ViewStylePropTypes),
testID: CoreComponent.propTypes.testID
@@ -48,41 +48,30 @@ 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
const className = `View ${_className}`.trim()
const className = `${_className} View`.trim()
const pointerEventsStyle = pointerEvents && { pointerEvents }
const resolvedStyle = pickProps(style, viewStyleKeys)
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,9 +0,0 @@
<!DOCTYPE html>
<meta charset="utf-8">
<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>

View File

@@ -1,5 +1,8 @@
import React from 'react'
import ReactDOM from 'react-dom'
import ReactDOMServer from 'react-dom/server'
// api
import StyleSheet from './modules/StyleSheet'
// components
@@ -11,9 +14,33 @@ import TextInput from './components/TextInput'
import Touchable from './components/Touchable'
import View from './components/View'
export default React
const renderStyle = () => {
return `<style id='react-stylesheet'>${StyleSheet._renderToString()}</style>`
}
export {
const render = (element, container, callback) => {
const styleElement = document.getElementById('react-stylesheet')
if (!styleElement) {
const style = renderStyle()
container.insertAdjacentHTML('beforebegin', style)
}
return ReactDOM.render(element, container, callback)
}
const renderToString = (element) => {
const style = renderStyle()
const html = ReactDOMServer.renderToString(element)
return `${style}\n${html}`
}
const renderToStaticMarkup = (element) => {
const style = renderStyle()
const html = ReactDOMServer.renderToStaticMarkup(element)
return `${style}\n${html}`
}
const ReactNative = {
// apis
StyleSheet,
// components
@@ -23,5 +50,15 @@ export {
Text,
TextInput,
Touchable,
View
View,
// React
...React,
...ReactDOM,
...ReactDOMServer,
render,
renderToString,
renderToStaticMarkup
}
module.exports = ReactNative

View File

@@ -1,22 +1,28 @@
import { PropTypes } from 'react'
const { number, string } = PropTypes
const numberOrString = PropTypes.oneOfType([ number, string ])
const { number, oneOf, oneOfType, string } = PropTypes
const numberOrString = oneOfType([ number, string ])
/**
* Any properties marked @private are used internally in resets or property
* mappings.
*
* https://developer.mozilla.org/en-US/docs/Web/CSS/Reference
*/
export default {
alignContent: string,
alignItems: string,
alignSelf: string,
alignContent: oneOf([ 'center', 'flex-end', 'flex-start', 'space-around', 'space-between', 'stretch' ]),
alignItems: oneOf([ 'baseline', 'center', 'flex-end', 'flex-start', 'stretch' ]),
alignSelf: oneOf([ 'auto', 'baseline', 'center', 'flex-end', 'flex-start', 'stretch' ]),
appearance: string,
backfaceVisibility: string,
backgroundAttachment: string,
backgroundAttachment: oneOf([ 'fixed', 'local', 'scroll' ]),
backgroundClip: string,
backgroundColor: string,
backgroundImage: string,
backgroundOrigin: string,
backgroundOrigin: oneOf([ 'border-box', 'content-box', 'padding-box' ]),
backgroundPosition: string,
backgroundRepeat: string,
backgroundSize: string,
border: string,
borderColor: string,
borderBottomColor: string,
borderLeftColor: string,
@@ -38,58 +44,69 @@ export default {
borderRightWidth: numberOrString,
borderTopWidth: numberOrString,
bottom: numberOrString,
boxSizing: string,
boxShadow: string,
boxSizing: oneOf([ 'border-box', 'content-box' ]),
clear: string,
color: string,
cursor: string,
direction: string,
display: string,
flex: string,
direction: string, /* @private */
flex: number,
flexBasis: string,
flexDirection: string,
flexGrow: numberOrString,
flexShrink: numberOrString,
flexWrap: string,
float: string,
font: string,
flexDirection: oneOf([ 'column', 'column-reverse', 'row', 'row-reverse' ]),
flexGrow: number,
flexShrink: number,
flexWrap: oneOf([ 'nowrap', 'wrap', 'wrap-reverse' ]),
float: oneOf([ 'left', 'none', 'right' ]),
font: string, /* @private */
fontFamily: string,
fontSize: numberOrString,
fontStyle: string,
fontWeight: string,
height: numberOrString,
justifyContent: string,
justifyContent: oneOf([ 'center', 'flex-end', 'flex-start', 'space-around', 'space-between' ]),
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,
position: string,
paddingVertical: numberOrString,
position: oneOf([ 'absolute', 'fixed', 'relative', 'static' ]),
right: numberOrString,
textAlign: string,
textAlign: oneOf([ 'center', 'inherit', 'justify', 'justify-all', 'left', 'right' ]),
textDecoration: string,
textTransform: string,
textOverflow: string,
textShadow: string,
textTransform: oneOf([ 'capitalize', 'lowercase', 'none', 'uppercase' ]),
top: numberOrString,
userSelect: string,
visibility: string,
verticalAlign: string,
visibility: oneOf([ 'hidden', 'visible' ]),
whiteSpace: string,
width: numberOrString,
wordWrap: string,
writingDirection: string,
zIndex: numberOrString
}

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
/* eslint-env mocha */
import assert from 'assert'
import expandStyle from '../expandStyle'
suite('modules/StyleSheet/expandStyle', () => {
test('style resolution', () => {
const initial = {
borderTopWidth: 1,
borderWidth: 2,
marginTop: 50,
marginVertical: 25,
margin: 10
}
const expected = {
borderTopWidth: 1,
borderLeftWidth: 2,
borderRightWidth: 2,
borderBottomWidth: 2,
marginTop: 50,
marginBottom: 25,
marginLeft: 10,
marginRight: 10
}
assert.deepEqual(expandStyle(initial), expected)
})
test('flex', () => {
const value = 10
const initial = {
flex: value
}
const expected = {
flexGrow: value,
flexShrink: 1,
flexBasis: 'auto'
}
assert.deepEqual(expandStyle(initial), expected)
})
})

View File

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

View File

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

View File

@@ -4,31 +4,63 @@ import { resetCSS, predefinedCSS } from '../predefs'
import assert from 'assert'
import StyleSheet from '..'
const styles = { root: { border: 0 } }
const styles = { root: { borderWidth: 1 } }
suite('modules/StyleSheet', () => {
setup(() => {
StyleSheet.destroy()
StyleSheet._destroy()
})
test('create', () => {
assert.equal(StyleSheet.create(styles), styles)
})
suite('create', () => {
const div = document.createElement('div')
test('renderToString', () => {
StyleSheet.create(styles)
assert.equal(
StyleSheet.renderToString(),
`${resetCSS}\n${predefinedCSS}\n` +
`/* 1 unique declarations */\n` +
`.border\\:0px{border:0px;}`
)
setup(() => {
document.body.appendChild(div)
StyleSheet.create(styles)
div.innerHTML = `<style id='react-stylesheet'>${StyleSheet._renderToString()}</style>`
})
teardown(() => {
document.body.removeChild(div)
})
test('returns styles object', () => {
assert.equal(StyleSheet.create(styles), styles)
})
test('updates already-rendered style sheet', () => {
StyleSheet.create({ root: { color: 'red' } })
assert.equal(
document.getElementById('react-stylesheet').textContent,
`${resetCSS}\n${predefinedCSS}\n` +
`/* 5 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;}\n` +
`.color\\:red{color:red;}`
)
})
})
test('resolve', () => {
const props = { className: 'className', style: styles.root }
const expected = { className: 'className border:0px', style: {} }
const expected = { className: 'className borderTopWidth:1px borderRightWidth:1px borderBottomWidth:1px borderLeftWidth:1px', style: {} }
StyleSheet.create(styles)
assert.deepEqual(StyleSheet.resolve(props), expected)
})
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;}`
)
})
})

View File

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

View File

@@ -0,0 +1,56 @@
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 if (key === 'flex') {
resolvedStyle.flexGrow = value
resolvedStyle.flexShrink = 1
resolvedStyle.flexBasis = 'auto'
} else {
resolvedStyle[key] = value
}
return resolvedStyle
}, {})
}
export default expandStyle

View File

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

View File

@@ -1,7 +1,9 @@
import { resetCSS, predefinedCSS, predefinedClassNames } from './predefs'
import expandStyle from './expandStyle'
import getStyleObjects from './getStyleObjects'
import prefixer from './prefixer'
import Store from './Store'
import StylePropTypes from '../StylePropTypes'
/**
* Initialize the store with pointer-event styles mapping to our custom pointer
@@ -11,37 +13,58 @@ const initialState = { classNames: predefinedClassNames }
const options = { obfuscateClassNames: process.env.NODE_ENV === 'production' }
const createStore = () => new Store(initialState, options)
let store = createStore()
let isRendered = false
/**
* Destroy existing styles
*/
const _destroy = () => {
store = createStore()
isRendered = false
}
/**
* Render the styles as a CSS style sheet
*/
const _renderToString = () => {
const css = store.toString()
isRendered = true
return `${resetCSS}\n${predefinedCSS}\n${css}`
}
/**
* Process all unique declarations
*/
const create = (styles: Object): Object => {
const rules = getStyleObjects(styles)
rules.forEach((rule) => {
Object.keys(rule).forEach(property => {
const value = rule[property]
// add each declaration to the store
store.set(property, value)
const style = expandStyle(rule)
Object.keys(style).forEach((property) => {
if (!StylePropTypes[property]) {
console.error(`ReactNativeWeb: the style property "${property}" is not supported`)
} else {
const value = style[property]
// add each declaration to the store
store.set(property, value)
}
})
})
// update the style sheet in place
if (isRendered) {
const stylesheet = document.getElementById('react-stylesheet')
if (stylesheet) {
stylesheet.textContent = _renderToString()
} else if (process.env.NODE_ENV !== 'production') {
console.error('ReactNativeWeb: cannot find "react-stylesheet" element')
}
}
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.
@@ -49,15 +72,19 @@ const renderToString = () => {
const resolve = ({ className = '', style = {} }) => {
let _className
let _style = {}
const expandedStyle = expandStyle(style)
const classList = [ className ]
for (const prop in style) {
let styleClass = store.get(prop, style[prop])
for (const prop in expandedStyle) {
if (!StylePropTypes[prop]) {
continue
}
let styleClass = store.get(prop, expandedStyle[prop])
if (styleClass) {
classList.push(styleClass)
} else {
_style[prop] = style[prop]
_style[prop] = expandedStyle[prop]
}
}
@@ -68,8 +95,8 @@ const resolve = ({ className = '', style = {} }) => {
}
export default {
_destroy,
_renderToString,
create,
destroy,
renderToString,
resolve
}

View File

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

View File

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

View File

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

View File

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