Compare commits

...

112 Commits

Author SHA1 Message Date
Nicolas Gallagher
791ede06dd 0.0.15 2016-02-18 23:04:35 -08:00
Nicolas Gallagher
0567232942 [fix] NetInfo export 2016-02-18 23:03:59 -08:00
Nicolas Gallagher
e5ecc26d21 0.0.14 2016-02-18 21:32:08 -08:00
Nicolas Gallagher
715c71b215 Refactor StyleSheet to support arrays 2016-02-18 16:45:23 -08:00
Nicolas Gallagher
f8554ecc1e Update README; add guides to docs 2016-02-17 00:49:18 -08:00
Nicolas Gallagher
3292ced765 Add new components
- ActivityIndicator
- Portal
- StaticContainer
- StaticRenderer
2016-02-17 00:20:02 -08:00
Nicolas Gallagher
1c7fb4cb45 Add APIs
- AppRegistry
- AppState
- AsyncStorage
- Dimensions
- NativeMethods
- NetInfo
- PixelRatio
- Platform
- UIManager
2016-02-16 23:54:06 -08:00
Nicolas Gallagher
60409bea18 BSD License; CONTRIBUTING.md 2016-02-16 23:43:41 -08:00
Nicolas Gallagher
5c74c0efb7 Move StyleSheet into 'apis' directory 2016-02-16 23:40:50 -08:00
Ben Alpert
a0187f9b1a Add semicolons to property initializers
According to the current proposal, semicolons are required. Babel 6.4
was updated to require them (see github.com/babel/babel/pull/3225). This
is required in order to fix the build -- master will be red on Travis as
soon as the build is rerun because it will pick up the latest Babel
parser.
2016-01-29 10:16:58 -08:00
Nicolas Gallagher
74ef265d83 [chore] update documentation 2015-12-30 14:10:22 -08:00
Nicolas Gallagher
97b3a91c0e [add] StyleSheet: immutable style in development
Fix #66
2015-12-30 14:10:15 -08:00
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
Nicolas Gallagher
a19b57df4d 0.0.7 2015-10-18 09:11:38 -07:00
Nicolas Gallagher
1c444569ae [change] React 0.14 support 2015-10-18 08:54:09 -07:00
Nicolas Gallagher
0b8c4b8746 [fix] link to API docs 2015-10-17 18:51:01 -07:00
Nicolas Gallagher
6772233837 [remove] Swipeable 2015-10-17 18:49:21 -07:00
Nicolas Gallagher
cd89f88d96 [add] StyleSheet API
Initial StyleSheet implementation for Web. Converts style object
declarations to "atomic" CSS rules.

Close gh-25
2015-10-17 17:52:01 -07:00
Nicolas Gallagher
b59bdb17b2 [change] Unit test setup and reporter 2015-10-17 17:51:54 -07:00
Nicolas Gallagher
2bfa579fe4 [remove] babel-plugin-typecheck 2015-10-17 17:51:30 -07:00
Nicolas Gallagher
f4c6e33b2f Fix #26 2015-10-01 15:18:28 -07:00
Nicolas Gallagher
39b273f9d8 Moves 2015-09-20 21:00:15 -07:00
Nicolas Gallagher
ec9985a3b3 0.0.6 2015-09-20 19:52:18 -07:00
Nicolas Gallagher
0aa29d8816 Image: add test for 'role=img' 2015-09-20 19:41:13 -07:00
Nicolas Gallagher
45777e0405 Add '_className' escape-hatch to Text and View 2015-09-20 19:31:59 -07:00
Nicolas Gallagher
84e06564d4 Accessibility fixes for Image and Text 2015-09-20 19:21:01 -07:00
Nicolas Gallagher
1d1b633317 npm-script: fix 'test' 2015-09-20 16:40:46 -07:00
Nicolas Gallagher
565ec2fee8 Docs: minor update 2015-09-20 15:50:22 -07:00
Nicolas Gallagher
33082f988e Text: add props 'accessibilityLabel' and 'accessible'
These properties are missing in React Native but important for
accessible web interfaces.
2015-09-20 15:48:09 -07:00
Nicolas Gallagher
a2fb65a79c Touchable: activeHighlight -> activeUnderlayColor 2015-09-20 15:47:35 -07:00
Nicolas Gallagher
e727193809 TextInput: props and tests 2015-09-20 15:43:52 -07:00
Nicolas Gallagher
d6db206ec4 Fix type of 'fontSize' style 2015-09-20 15:29:00 -07:00
Nicolas Gallagher
b3beea9bb3 Fix npm scripts 2015-09-20 15:25:03 -07:00
Nicolas Gallagher
ef4de789ae ScrollView: initial docs 2015-09-14 17:28:20 -07:00
Nicolas Gallagher
86037ab740 Add links to slack and gitter rooms 2015-09-14 11:51:44 -07:00
Nicolas Gallagher
6c20646f10 Touchable: add 'accessibilityRole' prop 2015-09-13 22:59:45 -07:00
Nicolas Gallagher
b5aa68dc7a specHelpers: accept props 2015-09-13 22:59:04 -07:00
Nicolas Gallagher
5668c40ee6 Don't strip testID's in production 2015-09-13 21:43:25 -07:00
Nicolas Gallagher
be86250ac6 Image: fix example code 2015-09-12 18:08:58 -07:00
Nicolas Gallagher
1e04dfc306 filterObjectProps: props -> propKeys 2015-09-12 18:08:21 -07:00
Nicolas Gallagher
283ab2fa2e Update dependencies 2015-09-12 18:07:22 -07:00
Nicolas Gallagher
09dd9a224a Remove type tests; fix code style 2015-09-11 21:28:03 -07:00
Nicolas Gallagher
c7524b7b6f Add Flow type checking; React >= 0.13 2015-09-11 21:17:48 -07:00
Nicolas Gallagher
5453c8843a Trivial edits 2015-09-09 00:48:18 -07:00
Nicolas Gallagher
e0f836ccb5 Minor example page update 2015-09-09 00:31:32 -07:00
Nicolas Gallagher
eada8e7fc7 Refactor dev workflow 2015-09-08 23:29:31 -07:00
Nicolas Gallagher
114fb5f8c7 LICENCE -> LICENSE 2015-09-08 22:44:27 -07:00
Nicolas Gallagher
4aa87c79fa Fix tests and code style 2015-09-08 14:21:17 -07:00
Nicolas Gallagher
9107fd3de9 Use more reliable npm badge host 2015-09-08 09:26:16 -07:00
Nicolas Gallagher
c72173ff88 Image: set 'resizeMode' default to 'stretch'
Fix #8
2015-09-08 00:29:53 -07:00
Nicolas Gallagher
edf0fda75a Docs: minor adjustments 2015-09-08 00:24:12 -07:00
Nicolas Gallagher
3b848fe378 View: additional accessibility props
* Add `accessibilityLiveRegion` for `aria-live` support.
* Add `accessibilityRole` for `role` support.

Fix #11
2015-09-08 00:09:09 -07:00
Nicolas Gallagher
7eff1a644e Spec helpers 2015-09-07 23:37:36 -07:00
Nicolas Gallagher
2750d70a93 Add support for 'accessible' prop 2015-09-07 22:09:16 -07:00
Nicolas Gallagher
65559f50e6 Image: fix ARIA role 2015-09-07 19:16:58 -07:00
Nicolas Gallagher
77fd21ea44 Touchable: add support for 'style' and keyboard 2015-09-07 14:40:37 -07:00
Nicolas Gallagher
38e4de76cd Documentation edits; transfer props 2015-09-07 13:14:09 -07:00
Nicolas Gallagher
0f42cd83e1 Component stylePropTypes keys as constants 2015-09-07 13:00:59 -07:00
Nicolas Gallagher
e6cbea82c4 Don't optimize published package 2015-09-07 12:59:16 -07:00
Nicolas Gallagher
6d4c9e881f Move CoreComponent module 2015-09-07 12:58:48 -07:00
135 changed files with 5612 additions and 2674 deletions

View File

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

View File

@@ -1,2 +0,0 @@
dist
docs

View File

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

View File

@@ -1,6 +1,10 @@
language: node_js
node_js:
- "0.12"
- "5"
- "4"
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
script:
- npm run lint
- npm test

View File

@@ -1,125 +1,78 @@
# Contributing to this project
# Contributing
The issue tracker is the preferred channel for [bug reports](#bugs),
[features requests](#features) and [submitting pull
requests](#pull-requests).
We are open to, and grateful for, any contributions made by the community.
<a name="bugs"></a>
## Bug reports
## Reporting Issues and Asking Questions
A bug is a _demonstrable problem_ that is caused by the code in the repository.
Good bug reports are extremely helpful - thank you!
Before opening an issue, please search the [issue
tracker](https://github.com/necolas/react-native-web/issues) to make sure your
issue hasn't already been reported.
Guidelines for bug reports:
## Development
1. **Use the GitHub issue search** &mdash; check if the issue has already been
reported.
Visit the [Issue tracker](https://github.com/necolas/react-native-web/issues)
to find a list of open issues that need attention.
2. **Check if the issue has been fixed** &mdash; try to reproduce it using the
latest `master` or development branch in the repository.
Fork, then clone the repo:
3. **Isolate the problem** &mdash; create a [reduced test
case](http://css-tricks.com/reduced-test-cases/) and a live example.
```
git clone https://github.com/your-username/react-native-web.git
```
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
experience the problem? What would you expect to be the outcome? All these
details really help!
Run the examples:
Example:
```
npm run examples
```
> Short and descriptive example bug report title
>
> A summary of the issue and the browser/OS environment in which it occurs. If
> suitable, include the steps required to reproduce the bug.
>
> 1. This is the first step
> 2. This is the second step
> 3. Further steps, etc.
>
> `<url>` - a link to the reduced test case
>
> Any other information you want to share that is relevant to the issue being
> reported. This might include the lines of code that you have identified as
> causing the bug, and potential solutions (and your opinions on their
> merits).
### Building
```
npm run build
```
<a name="features"></a>
## Feature requests
To create a UMD build:
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.
```
npm run build:umd
```
### Testing and Linting
<a name="pull-requests"></a>
## Pull requests
To run the tests:
Good pull requests - patches, improvements, new features - are a fantastic
help. Please keep them focused in scope and avoid containing unrelated commits.
```
npm run test
```
**Please ask first** before embarking on any significant pull request (e.g.
implementing new features or components, refactoring code), otherwise you risk
spending a lot of time working on something that the project's developers might
not want to merge into the project.
To continuously watch and run tests, run the following:
Development commands:
```
npm run test:watch
```
* `npm start` start the dev server and develop against live examples
* `npm run lint` run the linter
* `npm run specs` run the unit tests
* `npm run build` generate a build
To perform linting, run the following:
Please follow this process for submitting a patch:
```
npm run lint
```
1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork,
and configure the remotes:
### New Features
```bash
# Clone your fork of the repo into the current directory
git clone https://github.com/<your-username>/react-native-web
# Navigate to the newly cloned directory
cd react-native-web
# Assign the original repo to a remote called "upstream"
git remote add upstream https://github.com/necolas/react-native-web
```
Please open an issue with a proposal for a new feature or refactoring before
starting on the work. We don't want you to waste your efforts on a pull request
that we won't want to accept.
2. If you cloned a while ago, get the latest changes from upstream:
## Submitting Changes
```bash
git checkout master
git pull upstream master
```
* Open a new issue in the [Issue tracker](https://github.com/necolas/react-native-web/issues).
* Fork the repo.
* Create a new feature branch based off the `master` branch.
* Make sure all tests pass and there are no linting errors.
* Submit a pull request, referencing any issues it addresses.
3. Create a new topic branch (off the main project development branch) to
contain your feature, change, or fix:
Please try to keep your pull request focused in scope and avoid including unrelated commits.
```bash
git checkout -b <topic-branch-name>
```
After you have submitted your pull request, we'll try to get back to you as soon as possible. We may suggest some changes or improvements.
4. Commit your changes in logical chunks. Please adhere to these [git commit
message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
or your code is unlikely be merged into the main project. Use Git's
[interactive rebase](https://help.github.com/articles/interactive-rebase)
feature to tidy up your commits before making them public.
5. Locally merge (or rebase) the upstream development branch into your topic branch:
```bash
git pull [--rebase] upstream master
```
6. Push your topic branch up to your fork:
```bash
git push origin <topic-branch-name>
```
7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
with a clear title and description.
**IMPORTANT**: By submitting a patch, you agree to allow the project owner to
license your work under the same license as that used by the project.
Thank you for contributing!

21
LICENCE
View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Nicolas Gallagher
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

31
LICENSE Normal file
View File

@@ -0,0 +1,31 @@
BSD License
For React Native software
Copyright (c) 2015-present, Nicolas Gallagher. All rights reserved.
Copyright (c) 2015-present, Facebook, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name Facebook nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

230
README.md
View File

@@ -2,188 +2,114 @@
[![Build Status][travis-image]][travis-url]
[![npm version][npm-image]][npm-url]
![gzipped size](https://img.shields.io/badge/gzipped-~23k-blue.svg)
The core [React Native][react-native-url] components adapted and expanded upon
for the web, backed by a precomputed CSS library. ~19KB minified and gzipped.
[React Native][react-native-url] components and APIs for the Web.
## Table of contents
## Quick start
* [Install](#install)
* [Use](#use)
* [Components](#components)
* [Styling](#styling)
* [Contributing](#contributing)
* [Thanks](#thanks)
* [License](#license)
## Install
To install in your app:
```
npm install --save react react-native-web
npm install --save react@0.14 react-dom@0.14 react-native-web
```
## Use
Or [try it on CodePen](http://codepen.io/necolas/pen/PZzwBR).
React Native for Web exports its components and a reference to the `React`
installation. Styles are authored in JavaScript as plain objects.
Browser support: Chrome, Firefox, Safari >= 7, IE 10, Edge.
## Overview
This is a web implementation of React Native components and APIs. The React
Native components are good web application building blocks, and provide a common
foundation for component libraries.
For example, the [`View`](docs/apis/View.md) component makes it easy to build
common layouts with flexbox, such as stacked and nested boxes with margin and
padding. And the [`StyleSheet`](docs/guides/style.md) API converts styles
defined in JavaScript to "atomic" CSS.
## Example
More examples can be found in the [`examples` directory](examples).
```js
import React, { View } from 'react-native-web'
import React, { AppRegistry, Image, StyleSheet, Text, View } from 'react-native'
class MyComponent extends React.Component {
render() {
return (
<View style={styles.root} />
)
}
}
// Components
const Card = ({ children }) => <View style={styles.card}>{children}</View>
const Title = ({ children }) => <Text style={styles.title}>{children}</Text>
const Photo = ({ uri }) => <Image source={{ uri }} style={styles.image} />
const App = () => (
<Card>
<Title>App Card</Title>
<Photo uri="/some-photo.jpg" />
</Card>
)
const styles = {
root: {
borderColor: 'currentcolor'
borderWidth: '5px',
flexDirection: 'row'
height: '5em'
}
}
```
// App registration and rendering
AppRegistry.registerComponent('MyApp', () => App)
AppRegistry.runApplication('MyApp', { rootTag: document.getElementById('react-root') })
## Components
### [`Image`](docs/components/Image.md)
An accessibile image component with support for image resizing, default image,
and child content.
### [`ListView`](docs/components/ListView.md)
(TODO)
### [`ScrollView`](docs/components/ListView.md)
(TODO)
### [`Swipeable`](docs/components/Swipeable.md)
Touch bindings for swipe gestures.
### [`Text`](docs/components/Text.md)
Displays text as an inline block and supports basic press handling.
### [`TextInput`](docs/components/TextInput.md)
Accessible single- and multi-line text input via a keyboard. Supports features
### [`Touchable`](docs/components/Touchable.md)
Touch bindings for press and long press.
### [`View`](docs/components/View.md)
The fundamental UI building block: layout with flexbox, layout and positioning
styles, and accessibility controls.
## Styling
React Native for Web provides a mechanism to declare all your styles and layout
inline with the components that use them. The `View` component makes it easy to
build common layouts with flexbox, such as stacked and nested boxes with margin
and padding.
Styling is identical to using inline styles in React, but most inline styles
are converted to single-purpose classes. The current implementation includes
300+ precomputed CSS declarations (~4.5KB gzipped) that cover a large
proportion of common styles. A more sophisticated build-time implementation may
produce a slightly larger CSS file for large apps, and fall back to fewer
inline styles. Read more about the [styling
strategy](docs/style.md).
See this [guide to flexbox][flexbox-guide-url].
```js
import React, { Image, Text, View } from 'react-native-web'
class App extends React.Component {
render() {
return (
<View style={styles.row}>
<Image
source={{ uri: 'http://facebook.github.io/react/img/logo_og.png' }}
style={styles.image}
/>
<View style={styles.text}>
<Text style={styles.title}>
React Native Web
</Text>
<Text style={styles.subtitle}>
Build high quality web apps using React
</Text>
</View>
</View>
)
},
})
const styles = {
row: {
flexDirection: 'row',
margin: 40
},
image: {
height: 40,
marginRight: 10,
width: 40,
},
text: {
flex: 1,
// Styles
const styles = StyleSheet.create({
card: {
flexGrow: 1,
justifyContent: 'center'
},
title: {
fontSize: '1.25rem',
fontWeight: 'bold'
},
subtitle: {
fontSize: '1rem'
image: {
height: 40,
marginVertical: 10,
width: 40
}
}
})
```
Combine and override style objects:
## Documentation
```js
import baseStyle from './baseStyle'
Guides:
const buttonStyle = {
...baseStyle,
backgroundColor: '#333',
color: '#fff'
}
```
* [Accessibility](docs/guides/accessibility.md)
* [Client and server rendering](docs/guides/rendering.md)
* [Direct manipulation](docs/guides/direct-manipulation.md)
* [Known issues](docs/guides/known-issues.md)
* [React Native](docs/guides/react-native.md)
* [Style](docs/guides/style.md)
## Contributing
Exported modules:
Please read the [contribution guidelines][contributing-url]. Contributions are
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.
Thanks to [react-swipeable](https://github.com/dogfessional/react-swipeable/)
for the current implementation of `Swipeable`, and to
[react-tappable](https://github.com/JedWatson/react-tappable) for backing the
current implementation of `Touchable`.
* Components
* [`ActivityIndicator`](docs/components/ActivityIndicator.md)
* [`Image`](docs/components/Image.md)
* [`ListView`](docs/components/ListView.md)
* [`Portal`](docs/components/Portal.md)
* [`ScrollView`](docs/components/ScrollView.md)
* [`Text`](docs/components/Text.md)
* [`TextInput`](docs/components/TextInput.md)
* [`TouchableHighlight`](docs/components/TouchableHighlight.md)
* [`TouchableOpacity`](docs/components/TouchableOpacity.md)
* [`TouchableWithoutFeedback`](docs/components/TouchableWithoutFeedback.md)
* [`View`](docs/components/View.md)
* APIs
* [`AppRegistry`](docs/apis/AppRegistry.md)
* [`AppState`](docs/apis/AppState.md)
* [`AsyncStorage`](docs/apis/AsyncStorage.md)
* [`Dimensions`](docs/apis/Dimensions.md)
* [`NativeMethods`](docs/apis/NativeMethods.md)
* [`NetInfo`](docs/apis/NetInfo.md)
* [`PixelRatio`](docs/apis/PixelRatio.md)
* [`Platform`](docs/apis/Platform.md)
* [`StyleSheet`](docs/apis/StyleSheet.md)
## License
Copyright (c) 2015 Nicolas Gallagher. Released under the [MIT
license](http://www.opensource.org/licenses/mit-license.php).
React Native for Web is [BSD licensed](LICENSE).
[contributing-url]: https://github.com/necolas/react-native-web/blob/master/CONTRIBUTING.md
[flexbox-guide-url]: https://css-tricks.com/snippets/css/a-guide-to-flexbox/
[npm-image]: https://img.shields.io/npm/v/react-native-web.svg
[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/
[travis-image]: https://travis-ci.org/necolas/react-native-web.svg?branch=master

11
config/constants.js Normal file
View File

@@ -0,0 +1,11 @@
var path = require('path')
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, 'tests.webpack.js')
}

View File

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

View File

@@ -1,23 +0,0 @@
var autoprefixer = require('autoprefixer-core')
module.exports = {
module: {
loaders: [
{
test: /\.css$/,
loader: [
'style-loader',
'css-loader?module&localIdentName=[hash:base64:5]',
'!postcss-loader'
].join('!')
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: { cacheDirectory: true }
}
]
},
postcss: [ autoprefixer ]
}

View File

@@ -0,0 +1,37 @@
const constants = require('./constants')
const path = require('path')
const webpack = require('webpack')
module.exports = {
devServer: {
contentBase: constants.EXAMPLES_DIRECTORY
},
entry: {
example: constants.EXAMPLES_DIRECTORY
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: { cacheDirectory: true }
}
]
},
output: {
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()
],
resolve: {
alias: {
'react-native': path.join(__dirname, '../dist/react-native-web')
}
}
}

View File

@@ -1,12 +1,26 @@
var assign = require('object-assign')
var base = require('./webpack-base.config.js')
var constants = require('./constants')
var webpack = require('webpack')
var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin
var plugins = []
if (process.env.NODE_ENV === 'production') {
plugins.push(
new UglifyJsPlugin({
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,
@@ -14,21 +28,5 @@ if (process.env.NODE_ENV === 'production') {
warnings: true
}
})
)
]
}
module.exports = assign({}, base, {
entry: {
main: './src/index'
},
externals: [{
react: true
}],
output: {
filename: 'react-native-web.js',
library: 'ReactNativeWeb',
libraryTarget: 'commonjs2',
path: './dist'
},
plugins: plugins
})

60
docs/apis/AppRegistry.md Normal file
View File

@@ -0,0 +1,60 @@
# AppRegistry
`AppRegistry` is the control point for registering, running, prerendering, and
unmounting all apps. App root components should register themselves with
`AppRegistry.registerComponent`. Apps can be run by invoking
`AppRegistry.runApplication`, and prerendered by invoking
`AppRegistry.prerenderApplication` (see the [client and server rendering
guide](../guides/rendering.md) for more details).
To "stop" an application when a view should be destroyed, call
`AppRegistry.unmountApplicationComponentAtRootTag` with the tag that was passed
into `runApplication`. These should always be used as a pair.
## Methods
(web) static **prerenderApplication**(appKey:string, appParameters: object)
Renders the given application to an HTML string. Use this for server-side
rendering. Return object is of type `{ html: string; style: string; }`, where
`html` the prerendered HTML, and `style` is the prerendered style sheet.
static **registerConfig**(config: Array<AppConfig>)
Registry multiple applications. `AppConfig` is of type `{ appKey: string;
component: ComponentProvider; run?: Function }`.
static **registerComponent**(appKey: string, getComponentFunc: ComponentProvider)
Register a component provider under the given `appKey`.
static **registerRunnable**(appKey: string, run: Function)
Register a custom render function for an application. The function will receive
the `appParameters` passed to `runApplication`.
static **getAppKeys**()
Returns all registered app keys.
static **runApplication**(appKey: string, appParameters?: object)
Runs the application that was registered under `appKey`. The `appParameters`
must include the `rootTag` into which the application is rendered, and
optionally any `initialProps`.
static **unmountApplicationComponentAtRootTag**(rootTag: HTMLElement)
To "stop" an application when a view should be destroyed, call
`AppRegistry.unmountApplicationComponentAtRootTag` with the tag that was passed
into `runApplication`
## Example
```
AppRegistry.registerComponent('MyApp', () => AppComponent)
AppRegistry.runApplication('MyApp', {
initialProps: {},
rootTag: document.getElementById('react-root')
})
```

61
docs/apis/AppState.md Normal file
View File

@@ -0,0 +1,61 @@
## AppState
`AppState` can tell you if the app is in the foreground or background, and
notify you when the state changes.
States
* `active` - The app is running in the foreground
* `background` - The app is running in the background (i.e., the user has not focused the app's tab).
## Properties
static **currentState**
Returns the current state of the app: `active` or `background`.
## Methods
static **addEventListener**(type: string, handler: Function)
Add a handler to `AppState` changes by listening to the `change` event type and
providing the `handler`. The handler is called with the app state value.
static **removeEventListener**(type: string, handler: Function)
Remove a handler by passing the change event `type` and the `handler`.
## Examples
To see the current state, you can check `AppState.currentState`, which will be
kept up-to-date. This example will only ever appear to say "Current state is:
active" because the app is only visible to the user when in the `active` state,
and the null state will happen only momentarily.
```js
class Example extends React.Component {
constructor(props) {
super(props)
this.state = { currentAppState: AppState.currentState }
this._handleAppStateChange = this._handleAppStateChange.bind(this)
}
componentDidMount() {
AppState.addEventListener('change', this._handleAppStateChange);
}
componentWillUnmount() {
AppState.removeEventListener('change', this._handleAppStateChange);
}
_handleAppStateChange(currentAppState) {
this.setState({ currentAppState });
}
render() {
return (
<Text>Current state is: {this.state.currentAppState}</Text>
)
}
}
```

71
docs/apis/AsyncStorage.md Normal file
View File

@@ -0,0 +1,71 @@
# AsyncStorage
`AsyncStorage` is a simple, asynchronous, persistent, key-value storage system
that is global to the domain. It's a facade over, and should be used instead of
`window.localStorage` to provide an asynchronous API and multi functions. Each
method returns a `Promise` object.
It is recommended that you use an abstraction on top of `AsyncStorage` instead
of `AsyncStorage` directly for anything more than light usage since it operates
globally.
The batched functions are useful for executing a lot of operations at once,
allowing for optimizations to provide the convenience of a single promise after
all operations are complete.
## Methods
static **clear**()
Erases all AsyncStorage. You probably don't want to call this - use
`removeItem` or `multiRemove` to clear only your own keys instead. Returns a
Promise object.
static **getAllKeys**()
Gets all known keys. Returns a Promise object.
static **getItem**(key: string)
Fetches the value of the given key. Returns a Promise object.
static **mergeItem**(key: string, value: string)
Merges existing value with input value, assuming they are stringified JSON.
Returns a Promise object.
static **multiGet**(keys: Array<string>)
`multiGet` results in an array of key-value pair arrays that matches the input
format of `multiSet`. Returns a Promise object.
```js
multiGet(['k1', 'k2']) -> [['k1', 'val1'], ['k2', 'val2']]
```
static **multiMerge**(keyValuePairs: Array<Array<string>>)
multiMerge takes an array of key-value array pairs that match the output of
`multiGet`. It merges existing values with input values, assuming they are
stringified JSON. Returns a Promise object.
static **multiRemove**(keys: Array<string>)
Delete all the keys in the keys array. Returns a Promise object.
static **multiSet**(keyValuePairs: Array<Array<string>>)
`multiSet` takes an array of key-value array pairs that match the output of
`multiGet`. Returns a Promise object.
```js
multiSet([['k1', 'val1'], ['k2', 'val2']]);
```
static **removeItem**(key: string)
Removes the value of the given key. Returns a Promise object.
static **setItem**(key: string, value: string)
Sets the value of the given key. Returns a Promise object.

13
docs/apis/Dimensions.md Normal file
View File

@@ -0,0 +1,13 @@
# Dimensions
Note: dimensions may change (e.g due to device rotation) so any rendering logic
or styles that depend on these constants should try to call this function on
every render, rather than caching the value.
## Methods
static **get**(dimension: string)
Get a dimension (e.g., `"window"` or `"screen"`).
Example: `const { height, width } = Dimensions.get('window')`

View File

@@ -0,0 +1,42 @@
# NativeMethods
React Native for Web provides several methods to directly access the underlying
DOM node. This can be useful in cases when you want to focus a view or measure
its on-screen dimensions, for example.
The methods described are available on most of the default components provided
by React Native for Web. Note, however, that they are *not* available on the
composite components that you define in your own app. For more information, see
[Direct Manipulation](../guides/direct-manipulation.md).
## Methods
**blur**()
Removes focus from an input or view. This is the opposite of `focus()`.
**focus**()
Requests focus for the given input or view. The exact behavior triggered will
depend the type of view.
**measure**(callback: (x, y, width, height, pageX, pageY) => void)
For a given view, `measure` determines the offset relative to the parent view,
width, height, and the offset relative to the viewport. Returns the values via
an async callback.
Note that these measurements are not available until after the rendering has
been completed.
**measureLayout**(relativeToNativeNode: DOMNode, onSuccess: (x, y, width, height) => void)
Like `measure`, but measures the view relative to another view, specified as
`relativeToNativeNode`. This means that the returned `x`, `y` are relative to
the origin `x`, `y` of the ancestor view.
**setNativeProps**(nativeProps: Object)
This function sends props straight to the underlying DOM node. See the [direct
manipulation](../guides/direct-manipulation.md) guide for cases where
`setNativeProps` should be used.

77
docs/apis/NetInfo.md Normal file
View File

@@ -0,0 +1,77 @@
# NetInfo
`NetInfo` asynchronously determines the online/offline status of the
application.
Connection types:
* `bluetooth` - The user agent is using a Bluetooth connection.
* `cellular` - The user agent is using a cellular connection (e.g., EDGE, HSPA, LTE, etc.).
* `ethernet` - The user agent is using an Ethernet connection.
* `mixed` - The user agent is using multiple connection types.
* `none` - The user agent will not contact the network (offline).
* `other` - The user agent is using a connection type that is not one of enumerated connection types.
* `unknown` - The user agent has established a network connection, but is unable to determine what is the underlying connection technology.
* `wifi` - The user agent is using a Wi-Fi connection.
* `wimax` - The user agent is using a WiMAX connection.
## Methods
Note that support for retrieving the connection type depends upon browswer
support (and is limited to mobile browsers). It will default to `unknown` when
support is missing.
static **addEventListener**(eventName: ChangeEventName, handler: Function)
static **fetch**(): Promise
static **removeEventListener**(eventName: ChangeEventName, handler: Function)
## Properties
**isConnected**
Available on all user agents. Asynchronously fetch a boolean to determine
internet connectivity.
**isConnected.addEventListener**(eventName: ChangeEventName, handler: Function)
**isConnected.fetch**(): Promise
**isConnected.removeEventListener**(eventName: ChangeEventName, handler: Function)
## Examples
Fetching the connection type:
```
NetInfo.fetch().then((connectionType) => {
console.log('Connection type:', connectionType);
});
```
Subscribing to changes in the connection type:
```js
const handleConnectivityTypeChange = (connectionType) => {
console.log('Current connection type:', connectionType);
}
NetInfo.addEventListener('change', handleConnectivityTypeChange);
```
Fetching the connection status:
```js
NetInfo.isConnected.fetch().then((isConnected) => {
console.log('Connection status:', (isConnected ? 'online' : 'offline'));
});
```
Subscribing to changes in the connection status:
```js
const handleConnectivityStatusChange = (isConnected) => {
console.log('Current connection status:', (isConnected ? 'online' : 'offline'));
}
NetInfo.isConnected.addEventListener('change', handleConnectivityStatusChange);
```

51
docs/apis/PixelRatio.md Normal file
View File

@@ -0,0 +1,51 @@
# PixelRatio
`PixelRatio` gives access to the device pixel density.
## Methods
static **get**()
Returns the device pixel density. Some examples:
* PixelRatio.get() === 1
* mdpi Android devices (160 dpi)
* PixelRatio.get() === 1.5
* hdpi Android devices (240 dpi)
* PixelRatio.get() === 2
* iPhone 4, 4S
* iPhone 5, 5c, 5s
* iPhone 6
* xhdpi Android devices (320 dpi)
* PixelRatio.get() === 3
* iPhone 6 plus
* xxhdpi Android devices (480 dpi)
* PixelRatio.get() === 3.5
* Nexus 6
static **getPixelSizeForLayoutSize**(layoutSize: number)
Converts a layout size (dp) to pixel size (px). Guaranteed to return an integer
number.
static **roundToNearestPixel**(layoutSize: number)
Rounds a layout size (dp) to the nearest layout size that corresponds to an
integer number of pixels. For example, on a device with a PixelRatio of 3,
`PixelRatio.roundToNearestPixel(8.4)` = `8.33`, which corresponds to exactly
`(8.33 * 3)` = `25` pixels.
## Examples
Fetching a correctly sized image. You should get a higher resolution image if
you are on a high pixel density device. A good rule of thumb is to multiply the
size of the image you display by the pixel ratio.
```js
const image = getImage({
width: PixelRatio.getPixelSizeForLayoutSize(200),
height: PixelRatio.getPixelSizeForLayoutSize(100),
});
<Image source={image} style={{ width: 200, height: 100 }} />
```

28
docs/apis/Platform.md Normal file
View File

@@ -0,0 +1,28 @@
# Platform
Detect what is the platform in which the app is running. This piece of
functionality can be useful when only small parts of a component are platform
specific.
## Properties
**OS**: string
`Platform.OS` will be `web` when running in a Web browser.
**userAgent**: string
On Web, the `Platform` module can be also be used to detect the browser
`userAgent`.
## Examples
```js
const styles = StyleSheet.create({
height: (Platform.OS === 'web') ? 200 : 100,
});
if (Platform.userAgent.includes('Android')) {
console.log('Running on Android!');
}
```

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

@@ -0,0 +1,57 @@
# StyleSheet
The `StyleSheet` abstraction converts predefined styles to (vendor-prefixed)
CSS without requiring a compile-time step. Some styles cannot be resolved
outside of the render loop and are applied as inline styles. Read more about to
[how style your application](docs/guides/style).
## Methods
**create**(obj: {[key: string]: any})
Each key of the object passed to `create` must define a style object.
## Example
```js
const styles = StyleSheet.create({
container: {
borderRadius: 4,
borderWidth: 0.5,
borderColor: '#d6d7da',
},
title: {
fontSize: 19,
fontWeight: 'bold',
},
activeTitle: {
color: 'red',
}
})
```
Use styles:
```js
<View style={styles.container}>
<Text
style={[
styles.title,
this.props.isActive && styles.activeTitle
]}
/>
</View>
```
Or:
```js
<View style={styles.container}>
<Text
style={{
...styles.title,
...(this.props.isActive && styles.activeTitle)
}}
/>
</View>
```

View File

@@ -0,0 +1,69 @@
# ActivityIndicator
## Props
[...View props](./View.md)
**animating**: bool = true
Whether to show the indicator (true, the default) or hide it (false).
**color**: string = #999999
The foreground color of the spinner (default is gray).
**hidesWhenStopped**: bool = true
Whether the indicator should hide when not animating (true by default).
**size**: oneOf('small, 'large')
Size of the indicator. Small has a height of `20`, large has a height of `36`.
## Examples
```js
import React, { ActivityIndicator, Component, StyleSheet, View } from 'react-native'
class ToggleAnimatingActivityIndicator extends Component {
constructor(props) {
super(props)
this.state = { animating: true }
}
componentDidMount: function() {
this.setToggleTimeout();
}
render() {
return (
<ActivityIndicator
animating={this.state.animating}
size="large"
style={[styles.centering, { height: 80 }]}
/>
);
}
_setToggleTimeout() {
setTimeout(() => {
this.setState({ animating: !this.state.animating })
this._setToggleTimeout()
}, 1200)
}
})
const styles = StyleSheet.create({
centering: {
alignItems: 'center',
justifyContent: 'center'
},
gray: {
backgroundColor: '#cccccc'
},
horizontal: {
flexDirection: 'row',
justifyContent: 'space-around'
}
})
```

View File

@@ -3,60 +3,72 @@
An accessibile image component with support for image resizing, default image,
and child content.
Unsupported React Native props:
`capInsets` (ios),
`onProgress` (ios)
## Props
**accessibilityLabel** string
**accessibilityLabel**: string
The text that's read by the screen reader when the user interacts with the image.
The text that's read by a screenreader when someone interacts with the image.
**children** any
**accessible**: bool
When `false`, the view is hidden from screenreaders. Default: `true`.
**children**: any
Content to display over the image.
**defaultSource** { uri: string }
**defaultSource**: { uri: string }
An image to display as a placeholder while downloading the final image off the network.
**onError** function
**onError**: function
Invoked on load error with `{nativeEvent: {error}}`.
**onLoad** function
**onLayout**: function
TODO
**onLoad**: function
Invoked when load completes successfully.
**onLoadEnd** function
**onLoadEnd**: function
Invoked when load either succeeds or fails,
**onLoadStart** function
**onLoadStart**: function
Invoked on load start.
**resizeMode** oneOf('clip', 'contain', 'cover', 'stretch')
**resizeMode**: oneOf('contain', 'cover', 'none', 'stretch') = 'stretch'
Determines how to resize the image when the frame doesn't match the raw image
dimensions. Default: `cover`.
dimensions.
**source** { uri: string }
**source**: { uri: string }
`uri` is a string representing the resource identifier for the image, which
could be an http address or a base64 encoded image.
**style** style
**style**: style
[View](View.md) style
+ ...[View#style](View.md)
Defaults:
```js
{
alignSelf: 'flex-start',
backgroundColor: 'lightGray'
backgroundColor: 'transparent'
}
```
**testID** string
**testID**: string
Used to locate a view in end-to-end tests.
@@ -64,14 +76,12 @@ 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'
const { Component, PropTypes } = React;
class Avatar extends Component {
export default class ImageExample extends Component {
constructor(props, context) {
super(props, context)
this.state = { isLoaded: false }
this.state = { loading: true }
}
static propTypes = {
@@ -86,12 +96,12 @@ class Avatar extends Component {
_onLoad(e) {
console.log('Avatar.onLoad', e)
this.setState({ isLoaded: true })
this.setState({ loading: false })
}
render() {
const { size, testID, user } = this.props
const { isLoaded } = this.state
const loadingStyle = this.state.loading ? { styles.loading } : { }
return (
<Image
@@ -100,33 +110,36 @@ class Avatar extends Component {
onLoad={this._onLoad.bind(this)}
resizeMode='cover'
source={{ uri: user.avatarUrl }}
style={ ...style.base, ...style[size], ...style[isLoaded] }
style={[
styles.base,
styles[size],
loadingStyle
]}
/>
)
}
}
const style = {
const styles = StyleSheet.create({
base: {
borderColor: 'white',
borderRadius: '5px',
borderWidth: '5px',
opacity: 0.5,
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
}
isLoaded: {
opacity: 1
}
}
})
```

View File

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

67
docs/components/Portal.md Normal file
View File

@@ -0,0 +1,67 @@
# Portal
`Portal` is used to render modal content on top of everything else in the
application. It passes modal views all the way up to the root element created
by `AppRegistry.runApplication`.
There can only be one `Portal` instance rendered in an application, and this
instance is controlled by React Native for Web.
## Methods
static **allocateTag**()
Creates a new unique tag for the modal that your component is rendering. A
good place to allocate a tag is in `componentWillMount`. Returns a string. See
`showModal` and `closeModal`.
static **closeModal**(tag: string)
Remove a modal from the collection of modals to be rendered. The `tag` must
exactly match the tag previous passed to `showModal` to identify the React
component.
static **getOpenModals**()
Get an array of all the open modals, as identified by their tag string.
static **showModal**(tag: string, component: any)
Render a new modal. The `tag` must be unique as it is used to identify the
React component to render. This same tag can later be used in `closeModal`.
## Examples
```js
import React, { Portal, Text, Touchable } from 'react-native'
export default class PortalExample extends Component {
componentWillMount() {
this._portalTag = Portal.allocateTag()
}
render() {
return (
<Touchable onPress={this._handlePortalOpen.bind(this)}>
<Text>Open portal</Text>
</Touchable>
)
}
_handlePortalClose(e) {
Portal.closeModal(this._portalTag)
}
_handlePortalOpen(e) {
Portal.showModal(this._portalTag, this._renderPortalContent())
}
_renderPortalContent() {
return (
<Touchable onPress={this._handlePortalClose.bind(this)}>
<Text>Close portal</Text>
</Touchable>
)
}
}
```

View File

@@ -1,40 +1,83 @@
# ScrollView
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
**children** any
**children**: any
Content to display over the image.
Child content.
**style** style
**contentContainerStyle**: style
+ `property` type
These styles will be applied to the scroll view content container which wraps
all of the child views.
Defaults:
**horizontal**: bool = false
```js
{
}
```
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.
**scrollEnabled**: bool = true
When false, the content does not scroll.
**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. The
default value is `0`, which means the scroll event will be sent only once each
time the view is scrolled.
**style**: style
+ ...[View#style](View.md)
## Examples
```js
import React, { ScrollView } from 'react-native-web'
import React, { Component, ScrollView, StyleSheet } from 'react-native'
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,93 +0,0 @@
# Swipeable
## Props
**delta** number
Number of pixels that must be swiped before events are dispatched. Default: `10`.
**flickThreshold** number
The velocity threshold at which a swipe is considered a flick. Default: `0.6`.
**onSwiped** function
(SyntheticTouchEvent, deltaX, deltaY, isFlick) => swipeHandler
Called once a swipe has ended.
**onSwipedDown** function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called once a swipe-down has ended.
**onSwipedLeft** function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called once a swipe-left has ended.
**onSwipedUp** function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called once a swipe-up has ended.
**onSwipedRight** function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called once a swipe-right has ended.
**onSwipingDown** function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called while a swipe-down is in progress.
**onSwipingLeft** function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called while a swipe-left is in progress.
**onSwipingRight** function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called while a swipe-right is in progress.
**onSwipingUp** function
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
Called while a swipe-up is in progress.
## Examples
```js
import React, { Swipeable } from 'react-native-web'
const { Component, PropTypes } = React;
class Example extends Component {
static propTypes = {
}
static defaultProps = {
}
_onSwiped(event, x, y, isFlick) {
}
render() {
return (
<Swipeable
onSwiped={this._onSwiped.bind(this)}
/>
)
}
}
```

View File

@@ -1,75 +1,95 @@
# 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:
`allowFontScaling` (ios),
`suppressHighlighting` (ios)
## Props
**children** any
NOTE: `Text` will transfer all other props to the rendered HTML element.
Child content
(web) **accessibilityLabel**: string
**component** function, string
Defines the text available to assistive technologies upon interaction with the
element. (This is implemented using `aria-label`.)
Default is `span`.
(web) **accessibilityRole**: oneOf(roles)
**numberOfLines** number
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)).
Truncates the text with an ellipsis after this many lines.
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.
**onPress** function
(web) **accessible**: bool = true
When `false`, the text is hidden from assistive technologies. (This is
implemented using `aria-hidden`.)
**children**: any
Child content.
**numberOfLines**: number
Truncates the text with an ellipsis after this many lines. Currently only supports `1`.
**onPress**: function
This function is called on press.
**style** style
**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
**testID**: string
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'
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'
}
};
render() {
const { color, size, style, weight, ...other } = this.props;
@@ -77,32 +97,32 @@ class PrettyText extends Component {
return (
<Text
...other
style={{
...style,
...localStyle.color[color],
...localStyle.size[size],
...localStyle.weight[weight]
}}
style={[
style,
colorStyles[color],
sizeStyles[size],
weightStyles[weight]
]}
/>
);
}
}
const localStyle = {
color: {
white: { color: 'white' },
gray: { color: 'gray' },
red: { color: 'red' }
},
size: {
small: { fontSize: '0.85rem', padding: '0.5rem' },
normal: { fontSize: '1rem', padding: '0.75rem' },
large: { fontSize: '1.5rem', padding: '1rem' }
},
weight: {
light: { fontWeight: '300' },
normal: { fontWeight: '400' },
bold: { fontWeight: '700' }
}
}
const colorStyles = StyleSheet.create({
white: { color: 'white' },
gray: { color: 'gray' },
red: { color: 'red' }
})
const sizeStyles = StyleSheet.create({
small: { fontSize: '0.85rem', padding: '0.5rem' },
normal: { fontSize: '1rem', padding: '0.75rem' },
large: { fontSize: '1.5rem', padding: '1rem' }
})
const weightStyles = StyleSheet.create({
light: { fontWeight: '300' },
normal: { fontWeight: '400' },
bold: { fontWeight: '700' }
})
```

View File

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

View File

@@ -1,54 +1,95 @@
# Touchable
A wrapper for making views respond to mouse, keyboard, and touch presses. On
press in, the touchable area can display a highlight color, and the opacity of
the wrapped view can be decreased.
This component combines the various `Touchable*` components from React Native.
Unsupported React Native props:
`accessibilityComponentType` (android) use `accessibilityRole`,
`accessibilityTraits` (ios) use `accessibilityRole`,
`onHideUnderlay` use `onPressOut`,
`onShowUnderlay` use `onPressIn`,
`underlayColor` use `activeUnderlayColor`
## Props
**activeHighlight** string
**accessibilityLabel**: string
Sets the color of the background highlight when `onPressIn` is called. The
highlight is removed when `onPressOut` is called. Default: `transparent`.
Overrides the text that's read by the screen reader when the user interacts
with the element.
**activeOpacity** number
(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
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.
**accessible**: bool = true
When `false`, the view is hidden from screenreaders.
**activeOpacity**: number = 0.8
Sets the opacity of the child view when `onPressIn` is called. The opacity is
reset when `onPressOut` is called. Default: `1`.
reset when `onPressOut` is called.
**component** function or string
(web) **activeUnderlayColor**: string = 'black'
The backing component. Default: `div`.
Sets the color of the background highlight when `onPressIn` is called. The
highlight is removed when `onPressOut` is called.
**delayLongPress** number
**children**: element
Delay in ms, from `onPressIn`, before `onLongPress` is called. Default: `1000`.
A single child element.
**delayPressIn** number (TODO)
**delayLongPress**: number = 500
Delay in ms, from the start of the touch, before `onPressIn` is called. Default: `0`.
Delay in ms, from `onPressIn`, before `onLongPress` is called.
**delayPressOut** number (TODO)
**delayPressIn**: number = 0
Delay in ms, from the release of the touch, before `onPressOut` is called. Default: `0`.
(TODO)
**onLongPress** function
Delay in ms, from the start of the touch, before `onPressIn` is called.
**onPress** function
**delayPressOut**: number = 100
**onPressIn** function
(TODO)
**onPressOut** function
Delay in ms, from the release of the touch, before `onPressOut` is called.
**onLayout**: function
(TODO)
**onLongPress**: function
**onPress**: function
**onPressIn**: function
**onPressOut**: function
**style**: style
+ ...[View#style](View.md)
## Examples
```js
import React, { Touchable } from 'react-native-web'
import React, { Component, PropTypes, Touchable } from 'react-native'
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

@@ -1,24 +1,68 @@
# View
`View` is the fundamental UI building block. It is a component that supports
style, layout with flexbox, and accessibility controls. It can be nested
style, layout with flexbox, and accessibility controls. It can be nested
inside another `View` and has 0-to-many children of any type.
Unsupported React Native props:
`accessibilityComponentType` (android) use `accessibilityRole`,
`accessibilityTraits` (ios) use `accessibilityRole`,
`collapsable` (android),
`importantForAccessibility` (android),
`needsOffscreenAlphaCompositing` (android),
`onAccessibilityTap`,
`onMagicTap`,
`onMoveShouldSetResponder`,
`onResponder*`,
`onStartShouldSetResponder`,
`onStartShouldSetResponderCapture`
`removeClippedSubviews` (ios),
`renderToHardwareTextureAndroid` (android),
`shouldRasterizeIOS` (ios)
## Props
**accessibilityLabel** string
NOTE: `View` will transfer all other props to the rendered HTML element.
Overrides the text that's read by the screen reader when the user interacts
with the element. This is implemented using `aria-label`.
**accessibilityLabel**: string
**component** function, string
Defines the text available to assistive technologies upon interaction with the
element. (This is implemented using `aria-label`.)
Default is `div`.
**accessibilityLiveRegion**: oneOf('assertive', 'off', 'polite') = 'off'
**pointerEvents** oneOf('auto', 'box-only', 'box-none', 'none')
Indicates to assistive technologies whether to notify the user when the view
changes. The values of this attribute are expressed in degrees of importance.
When regions are specified as `polite` (recommended), updates take low
priority. When regions are specified as `assertive`, assistive technologies
will interrupt and immediately notify the user. (This is implemented using
[`aria-live`](http://www.w3.org/TR/wai-aria/states_and_properties#aria-live).)
We deviate from the CSS spec by supporting additional `pointerEvents` modes,
therefore `pointerEvents` is excluded from `style`.
(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.
**accessible**: bool = true
When `false`, the view is hidden from assistive technologies. (This is
implemented using `aria-hidden`.)
**onLayout**: function
(TODO)
**pointerEvents**: oneOf('auto', 'box-only', 'box-none', 'none') = 'auto'
Configure the `pointerEvents` of the view. The enhanced `pointerEvents` modes
provided are not part of the CSS spec, therefore, `pointerEvents` is excluded
from `style`.
`box-none` is the equivalent of:
@@ -34,7 +78,7 @@ therefore `pointerEvents` is excluded from `style`.
.box-only * { pointer-events: none }
```
**style** style
**style**: style
+ `alignContent`
+ `alignItems`
@@ -55,6 +99,8 @@ therefore `pointerEvents` is excluded from `style`.
+ `bottom`
+ `boxShadow`
+ `boxSizing`
+ `cursor`
+ `flex` (number)
+ `flexBasis`
+ `flexDirection`
+ `flexGrow`
@@ -63,7 +109,13 @@ therefore `pointerEvents` is excluded from `style`.
+ `height`
+ `justifyContent`
+ `left`
+ `margin`
+ `margin` (single value)
+ `marginBottom`
+ `marginHorizontal`
+ `marginLeft`
+ `marginRight`
+ `marginTop`
+ `marginVertical`
+ `maxHeight`
+ `maxWidth`
+ `minHeight`
@@ -73,7 +125,13 @@ therefore `pointerEvents` is excluded from `style`.
+ `overflow`
+ `overflowX`
+ `overflowY`
+ `padding`
+ `padding` (single value)
+ `paddingBottom`
+ `paddingHorizontal`
+ `paddingLeft`
+ `paddingRight`
+ `paddingTop`
+ `paddingVertical`
+ `position`
+ `right`
+ `top`
@@ -103,18 +161,16 @@ Default:
(See [facebook/css-layout](https://github.com/facebook/css-layout)).
**testID** string
**testID**: string
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'
const { Component, PropTypes } = React
class Example extends Component {
export default class ViewExample extends Component {
render() {
return (
<View style={styles.row}>
@@ -128,14 +184,12 @@ class Example extends Component {
}
}
const styles = {
const styles = StyleSheet.create({
row: {
flexDirection: 'row'
},
cell: {
flexGrow: 1
}
}
export default Example
})
```

View File

@@ -0,0 +1,33 @@
# 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 type of the
rendered HTML element. The `accessibilityRole` prop is used to infer an
[analogous HTML element][html-aria-url] to use in addition to the resulting
ARIA `role`, 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' />`.
Other ARIA properties should be set via [direct
manipulation](./direct-manipulation.md).
[aria-in-html-url]: https://w3c.github.io/aria-in-html/
[html-accessibility-url]: http://www.html5accessibility.com/
[html-aria-url]: http://www.w3.org/TR/html-aria/

View File

@@ -0,0 +1,115 @@
# Direct manipulation
It is sometimes necessary to make changes directly to a component without using
state/props to trigger a re-render of the entire subtree in the browser, this
is done by directly modifying a DOM node. `setNativeProps` is the React Native
equivalent to setting properties directly on a DOM node. Use direct
manipulation when frequent re-rendering creates a performance bottleneck Direct
manipulation will not be a tool that you reach for frequently.
## `setNativeProps` and `shouldComponentUpdate`
`setNativeProps` is imperative and stores state in the native layer (DOM,
UIView, etc.) and not within your React components, which makes your code more
difficult to reason about. Before you use it, try to solve your problem with
`setState` and `shouldComponentUpdate`.
## Avoiding conflicts with the render function
If you update a property that is also managed by the render function, you might
end up with some unpredictable and confusing bugs because anytime the component
re-renders and that property changes, whatever value was previously set from
`setNativeProps` will be completely ignored and overridden.
## Why use `setNativeProps` on Web?
Using `setNativeProps` in web-specific code is required when making changes to
`className` or `style`, as these properties are controlled by React Native for
Web and setting them directly may cause unintended rendering issues.
```js
setOpacityTo(value) {
this._childElement.setNativeProps({
style: { opacity: value }
})
}
```
## Composite components and `setNativeProps`
Composite components are not backed by a DOM node, so you cannot call
`setNativeProps` on them. Consider this example:
```js
const MyButton = (props) => (
<View>
<Text>{props.label}</Text>
</View>
)
const App = () => (
<TouchableOpacity>
<MyButton label="Press me!" />
</TouchableOpacity>
)
```
If you run this you will immediately see this error: `Touchable` child must
either be native or forward `setNativeProps` to a native component. This occurs
because `MyButton` isn't directly backed by a native view whose opacity should
be set. You can think about it like this: if you define a component with
`React.Component/createClass` you would not expect to be able to set a style
prop on it and have that work - you would need to pass the style prop down to a
child, unless you are wrapping a native component. Similarly, we are going to
forward `setNativeProps` to a native-backed child component.
## Forward `setNativeProps` to a child
All we need to do is provide a `setNativeProps` method on our component that
calls `setNativeProps` on the appropriate child with the given arguments.
```js
class MyButton extends React.Component {
setNativeProps(nativeProps) {
this._root.setNativeProps(nativeProps)
}
render() {
return (
<View ref={component => this._root = component}>
<Text>{this.props.label}</Text>
</View>
)
}
}
```
You can now use `MyButton` inside of `TouchableOpacity`!
## `setNativeProps` to clear `TextInput` value
Another very common use case of `setNativeProps` is to clear the value of a
`TextInput`. For example, the following code demonstrates clearing the input
when you tap a button:
```js
class App extends React.Component {
_handlePress() {
this._textInput.setNativeProps({ text: '' })
}
render() {
return (
<View style={styles.container}>
<TextInput
ref={component => this._textInput = component}
style={styles.textInput}
/>
<TouchableOpacity onPress={this._handlePress.bind(this)}>
<Text>Clear text</Text>
</TouchableOpacity>
</View>
)
}
}
```

View File

@@ -0,0 +1,28 @@
# Known issues
## Missing modules and Views
This is an initial release of React Native for Web, therefore, not all of the
views present on iOS/Android are released on Web. We are very much interested in
the community's feedback on the next set of modules and views.
Not all the modules or views for iOS/Android can be implemented on Web. In some
cases it will be necessary to use a Web counterpart.
## Missing component props
There are properties that do not work across all platforms. All web-specific
props are annotated with `(web)` in the documentaiton.
## Platform parity
There are some known issues in React Native where APIs could be made more
consistent between platforms. For example, React Native for Web includes
`ActivityIndicator` and a horizontal `ProgressBar`.
Other parts of React Native, such as the `Animated` and `PanResponder` APIs,
are highly complex and have not yet been ported to React Native for Web. Given
the difficulties keeping these APIs in sync with React Native, we'd prefer the
APIs to be published as separate npm packages. If not, we will consider a web
implementation, possibly using the [Web Animations
API/polyfill](https://github.com/web-animations/web-animations-js)

View File

@@ -0,0 +1,40 @@
# React Native
This is an experimental feature to support: using community-developed React
Native components on the Web; and rendering React Native apps to Web.
Use a module loader that supports package aliases (e.g., webpack), and alias
`react-native` to `react-native-web`.
```js
// webpack.config.js
module.exports = {
resolve: {
alias: {
'react-native': 'react-native-web'
}
}
}
```
Web-specific implementations can use the `*.web.js` naming pattern, which
webpack will resolve.
Minor platform differences can use the `Platform` module.
```js
import { AppRegistry, Platform, StyleSheet } from 'react-native'
const styles = StyleSheet.create({
height: (Platform.OS === 'web') ? 200 : 100
})
AppRegistry.registerComponent('MyApp', () => MyApp)
if (Platform.OS === 'web') {
AppRegistry.runApplication('MyApp', {
rootTag: document.getElementById('react-root')
});
}
```

57
docs/guides/rendering.md Normal file
View File

@@ -0,0 +1,57 @@
# Client and Server rendering
## Client-side rendering
```js
// client.js
import React, { AppRegistry } from 'react-native'
import MyApp from './MyApp'
// register the app
AppRegistry.registerApp('MyApp', () => MyApp)
// mount the app within the `rootTag` and run it
AppRegistry.runApplication('MyApp', { initialProps, rootTag: document.getElementById('react-root') })
// DOM render
React.render(<div />, document.getElementById('sidebar-app'))
// Server render
React.renderToString(<div />)
```
## Server-side rendering
Pre-rendering React apps on the server is a key feature for Web applications.
React Native for Web extends `AppRegistry` to provide support for server-side
rendering.
```js
// server.js
import React, { AppRegistry } from 'react-native'
import MyApp from './MyApp'
// register the app
AppRegistry.registerApp('MyApp', () => MyApp)
// prerender the app
const { html, style } = AppRegistry.prerenderApplication('MyApp', { initialProps })
// construct full page markup
const HtmlShell = (html, style) => (
<html>
<head>
<meta charSet="utf-8" />
<meta content="initial-scale=1,width=device-width" name="viewport" />
{style}
</head>
<body>
<div id="react-root" dangerouslySetInnerHTML={{ __html: html }} />
</body>
</html>
)
React.renderToStaticMarkup(<HtmlShell html={html} style={style} />)
```

225
docs/guides/style.md Normal file
View File

@@ -0,0 +1,225 @@
# Style
React Native for Web relies on JavaScript to define styles for your
application. Along with a novel JS-to-CSS conversion strategy, this allows you
to avoid issues arising from the [7 deadly sins of
CSS](https://speakerdeck.com/vjeux/react-css-in-js):
1. Global namespace
2. Dependency hell
3. No dead code elimination
4. No code minification
5. No sharing of constants
6. Non-deterministic resolution
7. Lack of isolation
## Defining styles
Styles should be defined outside of the component:
```js
class Example extends React.Component {}
const styles = StyleSheet.create({
heading: {
color: 'gray',
fontSize: '2rem'
},
text: {
color: 'gray',
fontSize: '1.25rem'
}
})
```
Using `StyleSheet.create` is optional but provides some key advantages: styles
are immutable in development, styles are converted to CSS rather than applied
as inline styles, and styles are only created once for the application and not
on every render.
The attribute names and values are a subset of CSS. See the `style`
documentation of individual components.
## Using styles
All the React Native components accept a `style` attribute.
```js
<Text style={styles.text} />
<View style={styles.view} />
```
A common pattern is to conditionally add style based on a condition:
```js
// either
<View style={[
styles.base,
this.state.active && styles.active
]} />
// or
<View style={{
...styles.base,
...(this.state.active && styles.active)
}} />
```
## Composing styles
In order to let a call site customize the style of your component children, you
can pass styles around. Use `View.propTypes.style` and `Text.propTypes.style` in
order to make sure only valid styles are being passed.
```js
class List extends React.Component {
static propTypes = {
style: View.propTypes.style,
elementStyle: View.propTypes.style,
}
render() {
return (
<View style={this.props.style}>
{elements.map((element) =>
<View style={[ styles.element, this.props.elementStyle ]} />
)}
</View>
);
}
}
```
In another file:
```js
<List style={styles.list} elementStyle={styles.listElement} />
```
You also have much greater control over how styles are composed when compared
to using class names. For example, you may choose to accept a limited subset
of style props in the component's API, and control when they are applied:
```js
class List extends React.Component {
static propTypes = {
children: React.PropTypes.any,
// limit which styles are accepted
style: React.PropTypes.shape({
borderColor: View.propTypes.borderColor,
borderWidth: View.propTypes.borderWidth
})
}
render() {
return (
<View
children={children}
style={[
this.props.style,
// override border-color when scrolling
isScrolling && { borderColor: 'transparent' }
]}
/>
)
}
}
```
## Media Queries
`StyleSheet.create` is a way of defining the styles your application requires;
it does not concern itself with _where_ or _when_ those styles are applied to
elements.
There are various React libraries wrapping JavaScript Media Query API's, 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 and pseudo-elements
Pseudo-classes like `:hover` and `:focus` can be implemented with the events
(e.g. `onFocus`). Pseudo-elements are not supported; elements should be used
instead.
## How it works
Every call to `StyleSheet.create` extracts the unique _declarations_ and
converts them to a unique CSS rule. This is sometimes referred to as "atomic
CSS". All the core components map their `style` property-value pairs to the
corresponding `className`'s.
By doing this, the total size of the generated CSS is determined by the
total number of unique declarations (rather than the total number of rules in
the application), making it viable to inline the style sheet when pre-rendering
on the server. Styles are updated if new module bundle are loaded asynchronously.
JavaScript definition:
```js
const styles = StyleSheet.create({
heading: {
color: 'gray',
fontSize: '2rem'
},
text: {
color: 'gray',
fontSize: '1.25rem'
}
})
```
CSS output:
```css
._s1 { color: gray; }
._s2 { font-size: 2rem; }
._s3 { font-size: 1.25rem; }
```
Rendered HTML:
```html
<span className="_s1 _s2">Heading</span>
<span className="_s1 _s3">Text</span>
```
### Reset
You **do not** need to include a CSS reset or
[normalize.css](https://necolas.github.io/normalize.css/).
React Native for Web includes a very small CSS reset taken from normalize.css.
It removes unwanted User Agent styles from (pseudo-)elements beyond the reach
of React (e.g., `html`, `body`) or inline styles (e.g., `::-moz-focus-inner`).
```css
html {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
ol,
ul,
li {
list-style:none
}
```

View File

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

View File

@@ -1,19 +0,0 @@
module.exports = {
entry: {
example: './example.js'
},
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: { cacheDirectory: true }
}
]
},
output: {
filename: 'example.js',
path: '../dist'
}
}

View File

@@ -1,62 +1,43 @@
import React, { Image, Swipeable, Text, TextInput, Touchable, View } from '../dist/react-native-web'
import GridView from './GridView'
import Heading from './Heading'
import MediaQueryWidget from './MediaQueryWidget'
import React, { Image, StyleSheet, ScrollView, Text, TextInput, TouchableHighlight, View } from 'react-native'
const { Component, PropTypes } = React
class Heading extends Component {
static propTypes = {
children: Text.propTypes.children,
level: PropTypes.oneOf(['1', '2', '3']),
size: PropTypes.oneOf(['xlarge', 'large', 'normal'])
}
static defaultProps = {
level: '1',
size: 'normal'
}
render() {
const { children, level, size } = this.props
return (
<Text
children={children}
component={`h${level}`}
style={headingStyles.size[size]}
/>
)
}
}
const headingStyles = {
size: {
xlarge: {
fontSize: '2rem',
marginBottom: '1em'
},
large: {
fontSize: '1.5rem',
marginBottom: '1em',
marginTop: '1em'
},
normal: {
fontSize: '1.25rem',
marginBottom: '0.5em',
marginTop: '0.5em'
}
}
}
class Example extends Component {
export default class App extends React.Component {
static propTypes = {
mediaQuery: React.PropTypes.object,
style: View.propTypes.style
}
render() {
return (
<View style={styles.root}>
<Heading level='1' size='xlarge'>React Native for Web: examples</Heading>
constructor(...args) {
super(...args)
this.state = {
scrollEnabled: true
}
}
<Heading level='2' size='large'>Image</Heading>
render() {
const { mediaQuery } = this.props
const finalRootStyles = [
rootStyles.common,
mediaQuery.small.matches && rootStyles.mqSmall,
mediaQuery.large.matches && rootStyles.mqLarge
]
return (
<ScrollView accessibilityRole='main'>
<View style={finalRootStyles}>
<Heading size='xlarge'>React Native for Web</Heading>
<Text>React Native Web takes the core components from <Text
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>
<MediaQueryWidget mediaQuery={mediaQuery} />
<Heading size='large'>Image</Heading>
<Image
accessibilityLabel='accessible image'
children={<Text>Inner content</Text>}
@@ -79,22 +60,7 @@ class Example extends Component {
testID='Example.image'
/>
<Heading level='2' size='large'>Swipeable</Heading>
<Swipeable
onSwiped={(e) => { console.log('Swipeable.onSwiped', e) }}
testID={'Example.swipeable'}
>
<View
style={{
backgroundColor: 'black',
alignSelf: 'center',
width: '200px',
height: '200px'
}}
/>
</Swipeable>
<Heading level='2' size='large'>Text</Heading>
<Heading size='large'>Text</Heading>
<Text
onPress={(e) => { console.log('Text.onPress', e) }}
testID={'Example.text'}
@@ -119,23 +85,31 @@ class Example extends Component {
hendrerit consequat.
</Text>
<Heading level='2' size='large'>TextInput</Heading>
<Heading size='large'>TextInput</Heading>
<TextInput
keyboardType='default'
onBlur={(e) => { console.log('TextInput.onBlur', e) }}
onChange={(e) => { console.log('TextInput.onChange', e) }}
onChangeText={(e) => { console.log('TextInput.onChangeText', e) }}
onFocus={(e) => { console.log('TextInput.onFocus', e) }}
onSelectionChange={(e) => { console.log('TextInput.onSelectionChange', e) }}
/>
<TextInput secureTextEntry={true} />
<TextInput secureTextEntry />
<TextInput defaultValue='read only' editable={false} />
<TextInput keyboardType='email-address' placeholder='you@domain.com' placeholderTextColor='red' />
<TextInput keyboardType='numeric' />
<TextInput keyboardType='tel' />
<TextInput keyboardType='url' />
<TextInput keyboardType='search' />
<TextInput defaultValue='default value' multiline />
<TextInput keyboardType='phone-pad' />
<TextInput defaultValue='https://delete-me' keyboardType='url' placeholder='https://www.some-website.com' selectTextOnFocus />
<TextInput
defaultValue='default value'
maxNumberOfLines={10}
multiline
numberOfLines={5}
/>
<Heading level='2' size='large'>Touchable</Heading>
<Touchable
<Heading size='large'>Touchable</Heading>
<TouchableHighlight
accessibilityLabel={'Touchable element'}
activeHighlight='lightblue'
activeOpacity={0.8}
onLongPress={(e) => { console.log('Touchable.onLongPress', e) }}
@@ -146,10 +120,10 @@ class Example extends Component {
<View style={styles.touchableArea}>
<Text>Touchable area (press, long press)</Text>
</View>
</Touchable>
</TouchableHighlight>
<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 (
@@ -160,7 +134,7 @@ class Example extends Component {
})}
</View>
<Heading level='3'>Row layout</Heading>
<Heading>Row layout</Heading>
<View style={styles.row}>
{[ 1, 2, 3, 4, 5, 6 ].map((item, i) => {
return (
@@ -171,13 +145,13 @@ class Example extends Component {
})}
</View>
<Heading level='3'>pointerEvents</Heading>
<View style={styles.row}>
<Heading>pointerEvents</Heading>
<GridView alley='10px'>
{['box-none', 'box-only', 'none'].map((value, i) => {
return (
<View
accessibilityRole='link'
children={value}
component='a'
href='https://google.com'
key={i}
pointerEvents={value}
@@ -185,18 +159,73 @@ class Example extends Component {
/>
)
})}
</GridView>
<Heading size='large'>ScrollView</Heading>
<label>
<input
checked={this.state.scrollEnabled}
onChange={() => this.setState({
scrollEnabled: !this.state.scrollEnabled
})}
type='checkbox'
/> Enable scroll
</label>
<Heading>Default layout</Heading>
<View style={styles.scrollViewContainer}>
<ScrollView
contentContainerStyle={styles.scrollViewContentContainerStyle}
onScroll={e => console.log('ScrollView.onScroll', e)}
scrollEnabled={this.state.scrollEnabled}
scrollEventThrottle={1} // 1 event per second
style={styles.scrollViewStyle}
>
{Array.from({ length: 50 }).map((item, i) => (
<View key={i} style={styles.box}>
<Text>{i}</Text>
</View>
))}
</ScrollView>
</View>
</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>
</ScrollView>
)
}
}
const styles = {
root: {
fontFamily: 'sans-serif',
maxWidth: '600px',
margin: '0 auto'
const rootStyles = StyleSheet.create({
common: {
marginVertical: 0,
marginHorizontal: 'auto'
},
mqSmall: {
maxWidth: '400px'
},
mqLarge: {
maxWidth: '600px'
}
})
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
flexWrap: 'wrap'
@@ -205,7 +234,10 @@ const styles = {
alignItems: 'center',
flexGrow: 1,
justifyContent: 'center',
borderWidth: '1px'
borderWidth: 1
},
horizontalBox: {
width: '50px'
},
boxFull: {
width: '100%'
@@ -213,7 +245,7 @@ const styles = {
pointerEventsBox: {
alignItems: 'center',
borderWidth: '1px',
flexGrow: '1',
flexGrow: 1,
height: '100px',
justifyContent: 'center'
},
@@ -222,7 +254,14 @@ const styles = {
borderWidth: 1,
height: '200px',
justifyContent: 'center'
},
scrollViewContainer: {
height: '200px'
},
scrollViewStyle: {
borderWidth: '1px'
},
scrollViewContentContainerStyle: {
padding: '10px'
}
}
React.render(<Example />, document.getElementById('react-root'))
})

View File

@@ -0,0 +1,65 @@
import React, { Component, PropTypes, StyleSheet, View } from 'react-native'
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>
)
}
}
const styles = StyleSheet.create({
root: {
overflow: 'hidden'
},
contentContainer: {
flexDirection: 'row',
flexGrow: 1
},
// distribute all space (rather than extra space)
column: {
flexBasis: '0%'
}
})

View File

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

View File

@@ -0,0 +1,39 @@
import React, { StyleSheet, Text, View } from 'react-native'
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 style={styles.text}>{`"${active.alias}"`} {active.mql && active.mql.media}</Text>
</View>
)
}
const styles = StyleSheet.create({
root: {
alignItems: 'center',
borderWidth: 1,
marginVertical: 10,
padding: 10
},
heading: {
fontWeight: 'bold',
padding: 5,
textAlign: 'center'
},
text: {
textAlign: 'center'
}
})
export default MediaQueryWidget

View File

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

23
examples/index.js Normal file
View File

@@ -0,0 +1,23 @@
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, { AppRegistry } from 'react-native'
const mediaQueries = {
small: '(min-width: 300px)',
medium: '(min-width: 400px)',
large: '(min-width: 500px)'
}
const ResponsiveApp = matchMedia()(App)
const WrappedApp = () => (
<MediaProvider getMedia={createGetter(mediaQueries)} listener={createListener(mediaQueries)}>
<ResponsiveApp />
</MediaProvider>
)
AppRegistry.registerComponent('Example', () => WrappedApp)
AppRegistry.runApplication('Example', {
rootTag: document.getElementById('react-root')
})

View File

@@ -1,57 +1,76 @@
{
"name": "react-native-web",
"version": "0.0.5",
"version": "0.0.15",
"description": "React Native for Web",
"main": "dist/react-native-web.js",
"main": "dist/index.js",
"files": [
"dist"
],
"scripts": {
"prepublish": "NODE_ENV=production npm run build",
"build": "rm -rf ./dist && webpack --config config/webpack.config.js --sort-assets-by --progress",
"example": "cd example && webpack --config webpack.config.js",
"lint": "eslint .",
"specs": "NODE_ENV=test karma start config/karma.config.js",
"specs:watch": "npm run specs -- --no-single-run",
"start": "webpack-dev-server --config config/webpack.config.js --inline --hot --colors --quiet",
"test": "npm run specs && npm run lint"
"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 examples src",
"prepublish": "npm run build && npm run build:umd",
"test": "karma start config/karma.config.js",
"test:watch": "npm run test:unit -- --no-single-run"
},
"dependencies": {
"react": "^0.13.3",
"react-swipeable": "^3.0.2",
"react-tappable": "^0.6.0"
"exenv": "^1.2.0",
"inline-style-prefixer": "^0.5.3",
"invariant": "^2.2.0",
"lodash.debounce": "^3.1.1",
"react-tappable": "^0.7.1",
"react-textarea-autosize": "^3.1.0"
},
"devDependencies": {
"autoprefixer-core": "^5.2.1",
"babel-core": "^5.8.23",
"babel-eslint": "^4.1.1",
"babel-loader": "^5.3.2",
"babel-runtime": "^5.8.20",
"css-loader": "^0.17.0",
"eslint": "^1.3.1",
"eslint-config-standard": "^4.3.1",
"eslint-config-standard-react": "^1.0.4",
"eslint-plugin-react": "^3.3.1",
"eslint-plugin-standard": "^1.3.0",
"extract-text-webpack-plugin": "^0.8.2",
"karma": "^0.13.9",
"karma-chrome-launcher": "^0.2.0",
"karma-firefox-launcher": "^0.1.6",
"karma-mocha": "^0.2.0",
"karma-sourcemap-loader": "^0.3.5",
"babel-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",
"postcss-loader": "^0.4.4",
"style-loader": "^0.12.3",
"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"
},
"peerDependencies": {
"react": "^0.14.3",
"react-dom": "^0.14.3"
},
"author": "Nicolas Gallagher",
"license": "MIT",
"license": "BSD-3-Clause",
"repository": {
"type": "git",
"url": "git://github.com/necolas/react-native-web.git"
}
},
"tags": [
"react"
],
"keywords": [
"react",
"react-component",
"react-native",
"web"
]
}

View File

@@ -0,0 +1,23 @@
/* 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)
})
})
})

View File

@@ -0,0 +1,47 @@
import Portal from '../../components/Portal'
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import StyleSheet from '../StyleSheet'
import View from '../../components/View'
export default class ReactNativeApp extends Component {
static propTypes = {
initialProps: PropTypes.object,
rootComponent: PropTypes.any.isRequired,
rootTag: PropTypes.any
};
constructor(props, context) {
super(props, context)
this._handleModalVisibilityChange = this._handleModalVisibilityChange.bind(this)
}
_handleModalVisibilityChange(modalVisible) {
ReactDOM.findDOMNode(this._root).setAttribute('aria-hidden', `${modalVisible}`)
}
render() {
const { initialProps, rootComponent: RootComponent, rootTag } = this.props
return (
<View style={styles.appContainer}>
<RootComponent {...initialProps} ref={(c) => { this._root = c }} rootTag={rootTag} />
<Portal onModalVisibilityChanged={this._handleModalVisibilityChange} />
</View>
)
}
}
const styles = StyleSheet.create({
/**
* Ensure that the application covers the whole screen. This prevents the
* Portal content from being clipped.
*/
appContainer: {
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0
}
})

View File

@@ -0,0 +1,90 @@
/**
* Copyright (c) 2015-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import { Component } from 'react'
import invariant from 'invariant'
import ReactDOM from 'react-dom'
import renderApplication, { prerenderApplication } from './renderApplication'
const runnables = {}
type ComponentProvider = () => Component<any, any, any>
type AppConfig = {
appKey: string;
component?: ComponentProvider;
run?: Function;
};
/**
* `AppRegistry` is the JS entry point to running all React Native apps.
*/
export default class AppRegistry {
static getAppKeys(): Array<string> {
return Object.keys(runnables)
}
static prerenderApplication(appKey: string, appParameters?: Object): string {
invariant(
runnables[appKey] && runnables[appKey].prerender,
`Application ${appKey} has not been registered. ` +
`This is either due to an import error during initialization or failure to call AppRegistry.registerComponent.`
)
return runnables[appKey].prerender(appParameters)
}
static registerComponent(appKey: string, getComponentFunc: ComponentProvider): string {
runnables[appKey] = {
run: ({ initialProps, rootTag }) => renderApplication(getComponentFunc(), initialProps, rootTag),
prerender: ({ initialProps } = {}) => prerenderApplication(getComponentFunc(), initialProps)
}
return appKey
}
static registerConfig(config: Array<AppConfig>) {
config.forEach(({ appKey, component, run }) => {
if (run) {
AppRegistry.registerRunnable(appKey, run)
} else {
invariant(component, 'No component provider passed in')
AppRegistry.registerComponent(appKey, component)
}
})
}
// TODO: fix style sheet creation when using this method
static registerRunnable(appKey: string, run: Function): string {
runnables[appKey] = { run }
return appKey
}
static runApplication(appKey: string, appParameters?: Object): void {
const isDevelopment = process.env.NODE_ENV === 'development'
const params = { ...appParameters }
params.rootTag = `#${params.rootTag.id}`
console.log(
`Running application "${appKey}" with appParams: ${JSON.stringify(params)}. ` +
`development-level warnings are ${isDevelopment ? 'ON' : 'OFF'}, ` +
`performance optimizations are ${isDevelopment ? 'OFF' : 'ON'}`
)
invariant(
runnables[appKey] && runnables[appKey].run,
`Application "${appKey}" has not been registered. ` +
`This is either due to an import error during initialization or failure to call AppRegistry.registerComponent.`
)
runnables[appKey].run(appParameters)
}
static unmountApplicationComponentAtRootTag(rootTag) {
ReactDOM.unmountComponentAtNode(rootTag)
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2015-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import invariant from 'invariant'
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import ReactDOMServer from 'react-dom/server'
import ReactNativeApp from './ReactNativeApp'
import StyleSheet from '../../apis/StyleSheet'
const renderStyleSheetToString = () => `<style id="${StyleSheet.elementId}">${StyleSheet._renderToString()}</style>`
export default function renderApplication(RootComponent: Component, initialProps: Object, rootTag: any) {
invariant(rootTag, 'Expect to have a valid rootTag, instead got ', rootTag)
// insert style sheet if needed
const styleElement = document.getElementById(StyleSheet.elementId)
if (!styleElement) { rootTag.insertAdjacentHTML('beforebegin', renderStyleSheetToString()) }
const component = (
<ReactNativeApp
initialProps={initialProps}
rootComponent={RootComponent}
rootTag={rootTag}
/>
)
ReactDOM.render(component, rootTag)
}
export function prerenderApplication(RootComponent: Component, initialProps: Object): string {
const component = (
<ReactNativeApp
initialProps={initialProps}
rootComponent={RootComponent}
/>
)
const html = ReactDOMServer.renderToString(component)
const style = renderStyleSheetToString()
return { html, style }
}

View File

@@ -0,0 +1,5 @@
/* eslint-env mocha */
suite('apis/AppState', () => {
test.skip('NO TEST COVERAGE', () => {})
})

View File

@@ -0,0 +1,29 @@
import invariant from 'invariant'
const listeners = {}
const eventTypes = [ 'change' ]
export default class AppState {
static get currentState() {
switch (document.visibilityState) {
case 'hidden':
case 'prerender':
case 'unloaded':
return 'background'
default:
return 'active'
}
}
static addEventListener(type: string, handler: Function) {
listeners[handler] = () => handler(AppState.currentState)
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
document.addEventListener('visibilitychange', listeners[handler], false)
}
static removeEventListener(type: string, handler: Function) {
invariant(eventTypes.indexOf(type) !== -1, 'Trying to remove listener for unknown event: "%s"', type)
document.removeEventListener('visibilitychange', listeners[handler], false)
delete listeners[handler]
}
}

View File

@@ -0,0 +1,5 @@
/* eslint-env mocha */
suite('apis/AsyncStorage', () => {
test.skip('NO TEST COVERAGE', () => {})
})

View File

@@ -0,0 +1,159 @@
/**
* Copyright (c) 2015-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*/
const mergeLocalStorageItem = (key, value) => {
const oldValue = window.localStorage.getItem(key)
const oldObject = JSON.parse(oldValue)
const newObject = JSON.parse(value)
const nextValue = JSON.stringify({ ...oldObject, ...newObject })
window.localStorage.setItem(key, nextValue)
}
export default class AsyncStorage {
/**
* Erases *all* AsyncStorage for the domain.
*/
static clear() {
return new Promise((resolve, reject) => {
try {
window.localStorage.clear()
resolve(null)
} catch (err) {
reject(err)
}
})
}
/**
* Gets *all* keys known to the app, for all callers, libraries, etc.
*/
static getAllKeys() {
return new Promise((resolve, reject) => {
try {
const numberOfKeys = window.localStorage.length
const keys = []
for (let i = 0; i < numberOfKeys; i += 1) {
const key = window.localStorage.key(i)
keys.push(key)
}
resolve(keys)
} catch (err) {
reject(err)
}
})
}
/**
* Fetches `key` value.
*/
static getItem(key: string) {
return new Promise((resolve, reject) => {
try {
const value = window.localStorage.getItem(key)
resolve(value)
} catch (err) {
reject(err)
}
})
}
/**
* Merges existing value with input value, assuming they are stringified JSON.
*/
static mergeItem(key: string, value: string) {
return new Promise((resolve, reject) => {
try {
mergeLocalStorageItem(key, value)
resolve(null)
} catch (err) {
reject(err)
}
})
}
/**
* multiGet resolves to an array of key-value pair arrays that matches the
* input format of multiSet.
*
* multiGet(['k1', 'k2']) -> [['k1', 'val1'], ['k2', 'val2']]
*/
static multiGet(keys: Array<string>) {
const promises = keys.map((key) => AsyncStorage.getItem(key))
return Promise.all(promises).then(
(result) => Promise.resolve(result.map((value, i) => [ keys[i], value ])),
(error) => Promise.reject(error)
)
}
/**
* Takes an array of key-value array pairs and merges them with existing
* values, assuming they are stringified JSON.
*
* multiMerge([['k1', 'val1'], ['k2', 'val2']])
*/
static multiMerge(keyValuePairs: Array<Array<string>>) {
const promises = keyValuePairs.map((item) => AsyncStorage.mergeItem(item[0], item[1]))
return Promise.all(promises).then(
() => Promise.resolve(null),
(error) => Promise.reject(error)
)
}
/**
* Delete all the keys in the `keys` array.
*/
static multiRemove(keys: Array<string>) {
const promises = keys.map((key) => AsyncStorage.removeItem(key))
return Promise.all(promises).then(
() => Promise.resolve(null),
(error) => Promise.reject(error)
)
}
/**
* Takes an array of key-value array pairs.
* multiSet([['k1', 'val1'], ['k2', 'val2']])
*/
static multiSet(keyValuePairs: Array<Array<string>>) {
const promises = keyValuePairs.map((item) => AsyncStorage.setItem(item[0], item[1]))
return Promise.all(promises).then(
() => Promise.resolve(null),
(error) => Promise.reject(error)
)
}
/**
* Removes a `key`
*/
static removeItem(key: string) {
return new Promise((resolve, reject) => {
try {
window.localStorage.removeItem(key)
resolve(null)
} catch (err) {
reject(err)
}
})
}
/**
* Sets `value` for `key`.
*/
static setItem(key: string, value: string) {
return new Promise((resolve, reject) => {
try {
window.localStorage.setItem(key, value)
resolve(null)
} catch (err) {
reject(err)
}
})
}
}

View File

@@ -0,0 +1,5 @@
/* eslint-env mocha */
suite('apis/Dimensions', () => {
test.skip('NO TEST COVERAGE', () => {})
})

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2015-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import invariant from 'invariant'
const dimensions = {
screen: {
fontScale: 1,
get height() { return window.screen.height },
scale: window.devicePixelRatio || 1,
get width() { return window.screen.width }
},
window: {
fontScale: 1,
get height() { return document.documentElement.clientHeight },
scale: window.devicePixelRatio || 1,
get width() { return document.documentElement.clientWidth }
}
}
export default class Dimensions {
static get(dimension: string): Object {
invariant(dimensions[dimension], 'No dimension set for key ' + dimension)
return dimensions[dimension]
}
}

View File

@@ -0,0 +1,5 @@
/* eslint-env mocha */
suite('apis/NetInfo', () => {
test.skip('NO TEST COVERAGE', () => {})
})

79
src/apis/NetInfo/index.js Normal file
View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2015-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import invariant from 'invariant'
const connection = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection
const eventTypes = [ 'change' ]
/**
* Navigator online: https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine
* Network Connection API: https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation
*/
const NetInfo = {
addEventListener(type: string, handler: Function): { remove: () => void } {
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
if (!connection) {
console.error('Network Connection API is not supported. Not listening for connection type changes.')
return {
remove: () => {}
}
}
connection.addEventListener(type, handler)
return {
remove: () => NetInfo.removeEventListener(type, handler)
}
},
removeEventListener(type: string, handler: Function): void {
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
if (!connection) { return }
connection.removeEventListener(type, handler)
},
fetch(): Promise {
return new Promise((resolve, reject) => {
try {
resolve(connection.type)
} catch (err) {
resolve('unknown')
}
})
},
isConnected: {
addEventListener(type: string, handler: Function): { remove: () => void } {
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
window.addEventListener('online', handler.bind(true), false)
window.addEventListener('offline', handler.bind(false), false)
return {
remove: () => NetInfo.isConnected.removeEventListener(type, handler)
}
},
removeEventListener(type: string, handler: Function): void {
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
window.removeEventListener('online', handler.bind(true), false)
window.removeEventListener('offline', handler.bind(false), false)
},
fetch(): Promise {
return new Promise((resolve, reject) => {
try {
resolve(window.navigator.onLine)
} catch (err) {
resolve(true)
}
})
}
}
}
export default NetInfo

View File

@@ -0,0 +1,5 @@
/* eslint-env mocha */
suite('apis/PixelRatio', () => {
test.skip('NO TEST COVERAGE', () => {})
})

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2015-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import Dimensions from '../Dimensions'
/**
* PixelRatio gives access to the device pixel density.
*/
export default class PixelRatio {
/**
* Returns the device pixel density.
*/
static get(): number {
return Dimensions.get('window').scale
}
/**
* No equivalent for Web
*/
static getFontScale(): number {
return Dimensions.get('window').fontScale || PixelRatio.get()
}
/**
* Converts a layout size (dp) to pixel size (px).
* Guaranteed to return an integer number.
*/
static getPixelSizeForLayoutSize(layoutSize: number): number {
return Math.round(layoutSize * PixelRatio.get())
}
/**
* Rounds a layout size (dp) to the nearest layout size that corresponds to
* an integer number of pixels. For example, on a device with a PixelRatio
* of 3, `PixelRatio.roundToNearestPixel(8.4) = 8.33`, which corresponds to
* exactly (8.33 * 3) = 25 pixels.
*/
static roundToNearestPixel(layoutSize: number): number {
const ratio = PixelRatio.get()
return Math.round(layoutSize * ratio) / ratio
}
}

View File

@@ -0,0 +1,8 @@
import { canUseDOM } from 'exenv'
const Platform = {
OS: 'web',
userAgent: canUseDOM ? window.navigator.userAgent : ''
}
export default Platform

View File

@@ -0,0 +1,25 @@
import { PropTypes } from 'react'
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
const numberOrString = PropTypes.oneOfType([ PropTypes.number, PropTypes.string ])
const BorderStylePropType = PropTypes.oneOf([ 'solid', 'dotted', 'dashed' ])
const BorderPropTypes = {
borderColor: ColorPropType,
borderTopColor: ColorPropType,
borderRightColor: ColorPropType,
borderBottomColor: ColorPropType,
borderLeftColor: ColorPropType,
borderRadius: numberOrString,
borderTopLeftRadius: numberOrString,
borderTopRightRadius: numberOrString,
borderBottomLeftRadius: numberOrString,
borderBottomRightRadius: numberOrString,
borderStyle: BorderStylePropType,
borderTopStyle: BorderStylePropType,
borderRightStyle: BorderStylePropType,
borderBottomStyle: BorderStylePropType,
borderLeftStyle: BorderStylePropType
}
export default BorderPropTypes

View File

@@ -0,0 +1,5 @@
import { PropTypes } from 'react'
const ColorPropType = PropTypes.string
export default ColorPropType

View File

@@ -0,0 +1,54 @@
import { PropTypes } from 'react'
const { number, oneOf, oneOfType, string } = PropTypes
const numberOrString = oneOfType([ number, string ])
const LayoutPropTypes = {
// box model
borderWidth: numberOrString,
borderBottomWidth: numberOrString,
borderLeftWidth: numberOrString,
borderRightWidth: numberOrString,
borderTopWidth: numberOrString,
boxSizing: string,
height: numberOrString,
margin: numberOrString,
marginBottom: numberOrString,
marginHorizontal: numberOrString,
marginLeft: numberOrString,
marginRight: numberOrString,
marginTop: numberOrString,
marginVertical: numberOrString,
maxHeight: numberOrString,
maxWidth: numberOrString,
minHeight: numberOrString,
minWidth: numberOrString,
padding: numberOrString,
paddingBottom: numberOrString,
paddingHorizontal: numberOrString,
paddingLeft: numberOrString,
paddingRight: numberOrString,
paddingTop: numberOrString,
paddingVertical: numberOrString,
width: numberOrString,
// flexbox
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' ]),
flex: number,
flexBasis: string,
flexDirection: oneOf([ 'column', 'column-reverse', 'row', 'row-reverse' ]),
flexGrow: number,
flexShrink: number,
flexWrap: oneOf([ 'nowrap', 'wrap', 'wrap-reverse' ]),
justifyContent: oneOf([ 'center', 'flex-end', 'flex-start', 'space-around', 'space-between' ]),
order: number,
// position
bottom: numberOrString,
left: numberOrString,
position: oneOf([ 'absolute', 'fixed', 'relative', 'static' ]),
right: numberOrString,
top: numberOrString
}
export default LayoutPropTypes

View File

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

View File

@@ -0,0 +1,22 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import createStrictShapeTypeChecker from './createStrictShapeTypeChecker'
import flattenStyle from './flattenStyle'
export default function StyleSheetPropType(shape) {
const shapePropType = createStrictShapeTypeChecker(shape)
return function (props, propName, componentName, location?) {
let newProps = props
if (props[propName]) {
// Just make a dummy prop object with only the flattened style
newProps = {}
newProps[propName] = flattenStyle(props[propName])
}
return shapePropType(newProps, propName, componentName, location)
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import flattenStyle from './flattenStyle'
import prefixer from './prefixer'
export default class StyleSheetRegistry {
static registerStyle(style: Object, store): number {
if (process.env.NODE_ENV !== 'production') {
Object.freeze(style)
}
const normalizedStyle = flattenStyle(style)
Object.keys(normalizedStyle).forEach((prop) => {
// add each declaration to the store
store.set(prop, normalizedStyle[prop])
})
}
static getStyleAsNativeProps(style, store) {
let _className
let _style = {}
const classList = []
const normalizedStyle = flattenStyle(style)
for (const prop in normalizedStyle) {
let styleClass = store.get(prop, normalizedStyle[prop])
if (styleClass) {
classList.push(styleClass)
} else {
_style[prop] = normalizedStyle[prop]
}
}
_className = classList.join(' ')
_style = prefixer.prefix(_style)
return { className: _className, style: _style }
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
* @flow
*/
import { PropTypes } from 'react'
import ImageStylePropTypes from '../../components/Image/ImageStylePropTypes'
import TextStylePropTypes from '../../components/Text/TextStylePropTypes'
import ViewStylePropTypes from '../../components/View/ViewStylePropTypes'
import invariant from 'invariant'
export default class StyleSheetValidation {
static validateStyleProp(prop, style, caller) {
if (process.env.NODE_ENV !== 'production') {
if (allStylePropTypes[prop] === undefined) {
const message1 = `"${prop}" is not a valid style property.`
const message2 = '\nValid style props: ' + JSON.stringify(Object.keys(allStylePropTypes).sort(), null, ' ')
styleError(message1, style, caller, message2)
}
const error = allStylePropTypes[prop](style, prop, caller, 'prop')
if (error) {
styleError(error.message, style, caller)
}
}
}
static validateStyle(name, styles) {
if (process.env.NODE_ENV !== 'production') {
for (const prop in styles[name]) {
StyleSheetValidation.validateStyleProp(prop, styles[name], 'StyleSheet ' + name)
}
}
}
static addValidStylePropTypes(stylePropTypes) {
for (const key in stylePropTypes) {
allStylePropTypes[key] = stylePropTypes[key]
}
}
}
const styleError = (message1, style, caller, message2) => {
invariant(
false,
message1 + '\n' + (caller || '<<unknown>>') + ': ' +
JSON.stringify(style, null, ' ') + (message2 || '')
)
}
const allStylePropTypes = {}
StyleSheetValidation.addValidStylePropTypes(ImageStylePropTypes)
StyleSheetValidation.addValidStylePropTypes(TextStylePropTypes)
StyleSheetValidation.addValidStylePropTypes(ViewStylePropTypes)
StyleSheetValidation.addValidStylePropTypes({
appearance: PropTypes.string,
clear: PropTypes.string,
cursor: PropTypes.string,
display: PropTypes.string,
direction: PropTypes.string, /* @private */
float: PropTypes.oneOf([ 'left', 'none', 'right' ]),
font: PropTypes.string, /* @private */
listStyle: PropTypes.string,
verticalAlign: PropTypes.string
})

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import { PropTypes } from 'react'
const ArrayOfNumberPropType = PropTypes.arrayOf(PropTypes.number)
const numberOrString = PropTypes.oneOfType([ PropTypes.number, PropTypes.string ])
const TransformMatrixPropType = function (
props : Object,
propName : string,
componentName : string
) : ?Error {
if (props.transform && props.transformMatrix) {
return new Error(
'transformMatrix and transform styles cannot be used on the same ' +
'component'
)
}
return ArrayOfNumberPropType(props, propName, componentName)
}
const TransformPropTypes = {
transform: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.shape({ perspective: numberOrString }),
PropTypes.shape({ rotate: numberOrString }),
PropTypes.shape({ rotateX: numberOrString }),
PropTypes.shape({ rotateY: numberOrString }),
PropTypes.shape({ rotateZ: numberOrString }),
PropTypes.shape({ scale: numberOrString }),
PropTypes.shape({ scaleX: numberOrString }),
PropTypes.shape({ scaleY: numberOrString }),
PropTypes.shape({ skewX: numberOrString }),
PropTypes.shape({ skewY: numberOrString }),
PropTypes.shape({ translateX: numberOrString }),
PropTypes.shape({ translateY: numberOrString })
])
),
transformMatrix: TransformMatrixPropType
}
export default TransformPropTypes

View File

@@ -0,0 +1,116 @@
/* eslint-env mocha */
import assert from 'assert'
import Store from '../Store'
suite('apis/StyleSheet/Store', () => {
suite('the constructor', () => {
test('initialState', () => {
const initialState = { classNames: { 'alignItems:center': '__classname__' } }
const store = new Store(initialState)
assert.deepEqual(store._classNames['alignItems:center'], '__classname__')
})
})
suite('#get', () => {
test('returns a declaration-specific className', () => {
const initialState = {
classNames: {
'alignItems:center': '__expected__',
'alignItems:flex-start': '__error__'
}
}
const store = new Store(initialState)
assert.deepEqual(store.get('alignItems', 'center'), '__expected__')
})
})
suite('#set', () => {
test('stores declarations', () => {
const store = new Store()
store.set('alignItems', 'center')
store.set('flexGrow', 0)
store.set('flexGrow', 1)
store.set('flexGrow', 2)
assert.deepEqual(store._declarations, {
alignItems: [ 'center' ],
flexGrow: [ 0, 1, 2 ]
})
})
test('human-readable classNames', () => {
const store = new Store()
store.set('alignItems', 'center')
store.set('flexGrow', 0)
store.set('flexGrow', 1)
store.set('flexGrow', 2)
assert.deepEqual(store._classNames, {
'alignItems:center': 'alignItems:center',
'flexGrow:0': 'flexGrow:0',
'flexGrow:1': 'flexGrow:1',
'flexGrow:2': 'flexGrow:2'
})
})
test('obfuscated classNames', () => {
const store = new Store({}, { obfuscateClassNames: true })
store.set('alignItems', 'center')
store.set('flexGrow', 0)
store.set('flexGrow', 1)
store.set('flexGrow', 2)
assert.deepEqual(store._classNames, {
'alignItems:center': '_s_1',
'flexGrow:0': '_s_2',
'flexGrow:1': '_s_3',
'flexGrow:2': '_s_4'
})
})
test('replaces space characters', () => {
const store = new Store()
store.set('margin', '0 auto')
assert.equal(store.get('margin', '0 auto'), 'margin\:0-auto')
})
})
suite('#toString', () => {
test('human-readable style sheet', () => {
const store = new Store()
store.set('alignItems', 'center')
store.set('backgroundColor', 'rgba(0,0,0,0)')
store.set('color', '#fff')
store.set('fontFamily', '"Helvetica Neue", Arial, sans-serif')
store.set('marginBottom', '0px')
store.set('width', '100%')
const expected = '/* 6 unique declarations */\n' +
'.alignItems\\:center{align-items:center;}\n' +
'.backgroundColor\\:rgba\\(0\\,0\\,0\\,0\\){background-color:rgba(0,0,0,0);}\n' +
'.color\\:\\#fff{color:#fff;}\n' +
'.fontFamily\\:\\"Helvetica-Neue\\"\\,-Arial\\,-sans-serif{font-family:"Helvetica Neue", Arial, sans-serif;}\n' +
'.marginBottom\\:0px{margin-bottom:0px;}\n' +
'.width\\:100\\%{width:100%;}'
assert.equal(store.toString(), expected)
})
test('obfuscated style sheet', () => {
const store = new Store({}, { obfuscateClassNames: true })
store.set('alignItems', 'center')
store.set('marginBottom', '0px')
store.set('margin', '1px')
store.set('margin', '2px')
store.set('margin', '3px')
const expected = '/* 5 unique declarations */\n' +
'._s_1{align-items:center;}\n' +
'._s_3{margin:1px;}\n' +
'._s_4{margin:2px;}\n' +
'._s_5{margin:3px;}\n' +
'._s_2{margin-bottom:0px;}'
assert.equal(store.toString(), expected)
})
})
})

View File

@@ -0,0 +1,45 @@
/* eslint-env mocha */
import assert from 'assert'
import expandStyle from '../expandStyle'
suite('apis/StyleSheet/expandStyle', () => {
test('style resolution', () => {
const initial = {
borderTopWidth: 1,
borderWidth: 2,
marginTop: 50,
marginVertical: 25,
margin: 10
}
const expected = {
borderTopWidth: '1px',
borderLeftWidth: '2px',
borderRightWidth: '2px',
borderBottomWidth: '2px',
marginTop: '50px',
marginBottom: '25px',
marginLeft: '10px',
marginRight: '10px'
}
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

@@ -0,0 +1,51 @@
/* eslint-env mocha */
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
import assert from 'assert'
import flattenStyle from '../flattenStyle'
suite('flattenStyle', () => {
test('should merge style objects', () => {
const style1 = {opacity: 1}
const style2 = {order: 2}
const flatStyle = flattenStyle([style1, style2])
assert.equal(flatStyle.opacity, 1)
assert.equal(flatStyle.order, 2)
})
test('should override style properties', () => {
const style1 = {backgroundColor: '#000', order: 1}
const style2 = {backgroundColor: '#023c69', order: null}
const flatStyle = flattenStyle([style1, style2])
assert.equal(flatStyle.backgroundColor, '#023c69')
assert.strictEqual(flatStyle.order, null)
})
test('should overwrite properties with `undefined`', () => {
const style1 = {backgroundColor: '#000'}
const style2 = {backgroundColor: undefined}
const flatStyle = flattenStyle([style1, style2])
assert.strictEqual(flatStyle.backgroundColor, undefined)
})
test('should not fail on falsy values', () => {
assert.doesNotThrow(() => flattenStyle([null, false, undefined]))
})
test('should recursively flatten arrays', () => {
const style1 = {order: 2}
const style2 = {opacity: 1}
const style3 = {order: 3}
const flatStyle = flattenStyle([null, [], [style1, style2], style3])
assert.equal(flatStyle.order, 3)
assert.equal(flatStyle.opacity, 1)
})
})

View File

@@ -0,0 +1,16 @@
/* eslint-env mocha */
import assert from 'assert'
import hyphenate from '../hyphenate'
suite('apis/StyleSheet/hyphenate', () => {
test('style property', () => {
assert.equal(hyphenate('alignItems'), 'align-items')
assert.equal(hyphenate('color'), 'color')
})
test('vendor prefixed style property', () => {
assert.equal(hyphenate('MozTransition'), '-moz-transition')
assert.equal(hyphenate('msTransition'), '-ms-transition')
assert.equal(hyphenate('WebkitTransition'), '-webkit-transition')
})
})

View File

@@ -0,0 +1,66 @@
/* eslint-env mocha */
import { resetCSS, predefinedCSS } from '../predefs'
import assert from 'assert'
import StyleSheet from '..'
const styles = { root: { borderWidth: 1 } }
suite('apis/StyleSheet', () => {
setup(() => {
StyleSheet._destroy()
})
suite('create', () => {
const div = document.createElement('div')
setup(() => {
document.body.appendChild(div)
StyleSheet.create(styles)
div.innerHTML = `<style id='${StyleSheet.elementId}'>${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(StyleSheet.elementId).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 = { style: styles.root }
const expected = { 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

@@ -0,0 +1,13 @@
/* eslint-env mocha */
import assert from 'assert'
import normalizeValue from '../normalizeValue'
suite('apis/StyleSheet/normalizeValue', () => {
test('normalizes property values requiring units', () => {
assert.deepEqual(normalizeValue('margin', 0), '0px')
})
test('ignores unitless property values', () => {
assert.deepEqual(normalizeValue('flexGrow', 1), 1)
})
})

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
import ReactPropTypeLocationNames from 'react/lib/ReactPropTypeLocationNames'
import invariant from 'invariant'
export default function createStrictShapeTypeChecker(shapeTypes) {
function checkType(isRequired, props, propName, componentName, location?) {
if (!props[propName]) {
if (isRequired) {
invariant(
false,
`Required object \`${propName}\` was not specified in ` +
`\`${componentName}\`.`
)
}
return
}
const propValue = props[propName]
const propType = typeof propValue
const locationName = location && ReactPropTypeLocationNames[location] || '(unknown)'
if (propType !== 'object') {
invariant(
false,
`Invalid ${locationName} \`${propName}\` of type \`${propType}\` ` +
`supplied to \`${componentName}\`, expected \`object\`.`
)
}
// We need to check all keys in case some are required but missing from
// props.
const allKeys = { ...props[propName], ...shapeTypes }
for (const key in allKeys) {
const checker = shapeTypes[key]
if (!checker) {
invariant(
false,
`Invalid props.${propName} key \`${key}\` supplied to \`${componentName}\`.` +
`\nBad object: ` + JSON.stringify(props[propName], null, ' ') +
`\nValid keys: ` + JSON.stringify(Object.keys(shapeTypes), null, ' ')
)
}
const error = checker(propValue, key, componentName, location)
if (error) {
invariant(
false,
error.message + `\nBad object: ` + JSON.stringify(props[propName], null, ' ')
)
}
}
}
function chainedCheckType(
props: {[key: string]: any},
propName: string,
componentName: string,
location?: string
): ?Error {
return checkType(false, props, propName, componentName, location)
}
chainedCheckType.isRequired = checkType.bind(null, true)
return chainedCheckType
}

View File

@@ -0,0 +1,59 @@
import normalizeValue from './normalizeValue'
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 = normalizeValue(key, 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

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import invariant from 'invariant'
import expandStyle from './expandStyle'
export default function flattenStyle(style): ?Object {
if (!style) {
return undefined
}
invariant(style !== true, 'style may be false but not true')
if (!Array.isArray(style)) {
// we must expand styles during the flattening because expanded styles
// override shorthands
return expandStyle(style)
}
const result = {}
for (let i = 0; i < style.length; ++i) {
const computedStyle = flattenStyle(style[i])
if (computedStyle) {
for (const key in computedStyle) {
result[key] = computedStyle[key]
}
}
}
return result
}

View File

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

View File

@@ -0,0 +1,75 @@
import { resetCSS, predefinedCSS, predefinedClassNames } from './predefs'
import flattenStyle from './flattenStyle'
import Store from './Store'
import StyleSheetRegistry from './StyleSheetRegistry'
import StyleSheetValidation from './StyleSheetValidation'
const ELEMENT_ID = 'react-stylesheet'
let isRendered = false
let lastStyleSheet = ''
/**
* Initialize the store with pointer-event styles mapping to our custom pointer
* event classes
*/
const initialState = { classNames: predefinedClassNames }
const options = { obfuscateClassNames: !(process.env.NODE_ENV !== 'production') }
const createStore = () => new Store(initialState, options)
let store = createStore()
/**
* 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}`
}
const create = (styles: Object): Object => {
for (const key in styles) {
StyleSheetValidation.validateStyle(key, styles)
StyleSheetRegistry.registerStyle(styles[key], store)
}
// update the style sheet in place
if (isRendered) {
const stylesheet = document.getElementById(ELEMENT_ID)
if (stylesheet) {
const newStyleSheet = _renderToString()
if (lastStyleSheet !== newStyleSheet) {
stylesheet.textContent = newStyleSheet
lastStyleSheet = newStyleSheet
}
} else if (process.env.NODE_ENV !== 'production') {
console.error('ReactNative: cannot find "react-stylesheet" element')
}
}
return styles
}
/**
* Accepts React props and converts inline styles to single purpose classes
* where possible.
*/
const resolve = ({ style = {} }) => {
return StyleSheetRegistry.getStyleAsNativeProps(style, store)
}
export default {
_destroy,
_renderToString,
create,
elementId: ELEMENT_ID,
flatten: flattenStyle,
resolve
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,100 @@
/* eslint-env mocha */
import assert from 'assert'
import UIManager from '..'
const createNode = (style = {}) => {
const root = document.createElement('div')
Object.keys(style).forEach((prop) => {
root.style[prop] = style[prop]
})
return root
}
let defaultBodyMargin
suite('apis/UIManager', () => {
setup(() => {
// remove default body margin so we can predict the measured offsets
defaultBodyMargin = document.body.style.margin
document.body.style.margin = 0
})
teardown(() => {
document.body.style.margin = defaultBodyMargin
})
suite('measure', () => {
test('provides correct layout to callback', () => {
const node = createNode({ height: '5000px', left: '100px', position: 'relative', top: '100px', width: '5000px' })
document.body.appendChild(node)
UIManager.measure(node, (x, y, width, height, pageX, pageY) => {
assert.equal(x, 100)
assert.equal(y, 100)
assert.equal(width, 5000)
assert.equal(height, 5000)
assert.equal(pageX, 100)
assert.equal(pageY, 100)
})
// test values account for scroll position
window.scrollTo(200, 200)
UIManager.measure(node, (x, y, width, height, pageX, pageY) => {
assert.equal(x, 100)
assert.equal(y, 100)
assert.equal(width, 5000)
assert.equal(height, 5000)
assert.equal(pageX, -100)
assert.equal(pageY, -100)
})
document.body.removeChild(node)
})
})
suite('measureLayout', () => {
test('provides correct layout to onSuccess callback', () => {
const node = createNode({ height: '10px', width: '10px' })
const middle = createNode({ padding: '20px' })
const context = createNode({ padding: '20px' })
middle.appendChild(node)
context.appendChild(middle)
document.body.appendChild(context)
UIManager.measureLayout(node, context, () => {}, (x, y, width, height) => {
assert.equal(x, 40)
assert.equal(y, 40)
assert.equal(width, 10)
assert.equal(height, 10)
})
document.body.removeChild(context)
})
})
suite('updateView', () => {
test('adds new className to existing className', () => {
const node = createNode()
node.className = 'existing'
const props = { className: 'extra' }
UIManager.updateView(node, props)
assert.equal(node.getAttribute('class'), 'existing extra')
})
test('adds new style to existing style', () => {
const node = createNode({ color: 'red' })
const props = { style: { opacity: 0 } }
UIManager.updateView(node, props)
assert.equal(node.getAttribute('style'), 'color: red; opacity: 0;')
})
test('sets attribute values', () => {
const node = createNode()
const props = { 'aria-level': '4', 'data-of-type': 'string' }
UIManager.updateView(node, props)
assert.equal(node.getAttribute('aria-level'), '4')
assert.equal(node.getAttribute('data-of-type'), 'string')
})
})
})

View File

@@ -0,0 +1,35 @@
import CSSPropertyOperations from 'react/lib/CSSPropertyOperations'
const measureAll = (node, callback, relativeToNativeNode) => {
const { height, left, top, width } = node.getBoundingClientRect()
const relativeNode = relativeToNativeNode || node.parentNode
const relativeRect = relativeNode.getBoundingClientRect()
const x = left - relativeRect.left
const y = top - relativeRect.top
callback(x, y, width, height, left, top)
}
const UIManager = {
measure(node, callback) {
measureAll(node, callback)
},
measureLayout(node, relativeToNativeNode, onFail, onSuccess) {
measureAll(node, (x, y, width, height) => onSuccess(x, y, width, height), relativeToNativeNode)
},
updateView(node, props) {
for (const prop in props) {
const value = props[prop]
if (prop === 'style') {
CSSPropertyOperations.setValueForStyles(node, value)
} else if (prop === 'className') {
node.classList.add(value)
} else {
node.setAttribute(prop, value)
}
}
}
}
export default UIManager

View File

@@ -0,0 +1,5 @@
/* eslint-env mocha */
suite('components/ActivityIndicator', () => {
test.skip('NO TEST COVERAGE', () => {})
})

View File

@@ -0,0 +1,105 @@
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import StyleSheet from '../../apis/StyleSheet'
import View from '../View'
const GRAY = '#999999'
const animationEffectTimingProperties = {
direction: 'alternate',
duration: 700,
easing: 'ease-in-out',
fill: 'forwards',
iterations: Infinity
}
const keyframeEffects = [
{ transform: 'scale(1)', opacity: 1.0 },
{ transform: 'scale(0.95)', opacity: 0.5 }
]
export default class ActivityIndicator extends Component {
static propTypes = {
animating: PropTypes.bool,
color: PropTypes.string,
hidesWhenStopped: PropTypes.bool,
size: PropTypes.oneOf(['small', 'large']),
style: View.propTypes.style
};
static defaultProps = {
animating: true,
color: GRAY,
hidesWhenStopped: true,
size: 'small',
style: {}
};
componentDidMount() {
if (document.documentElement.animate) {
this._player = ReactDOM.findDOMNode(this._indicatorRef).animate(keyframeEffects, animationEffectTimingProperties)
}
if (this.props.animating) {
this._player.play()
} else {
this._player.cancel()
}
}
componentDidUpdate() {
if (this.props.animating) {
this._player.play()
} else {
this._player.cancel()
}
}
render() {
const {
animating,
color,
hidesWhenStopped,
size,
style,
...other
} = this.props
return (
<View {...other} style={[ styles.container, style ]}>
<View
ref={(c) => { this._indicatorRef = c }}
style={[
indicatorStyles[size],
hidesWhenStopped && !animating && styles.hidesWhenStopped,
{ borderColor: color }
]}
/>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center'
},
hidesWhenStopped: {
visibility: 'hidden'
}
})
const indicatorStyles = StyleSheet.create({
small: {
borderRadius: 100,
borderWidth: 3,
width: 20,
height: 20
},
large: {
borderRadius: 100,
borderWidth: 4,
width: 36,
height: 36
}
})

View File

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

View File

@@ -1,3 +1,64 @@
import CoreComponent from './modules/CoreComponent'
import React, { Component, PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
export default CoreComponent
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'
}
export default class CoreComponent extends Component {
static propTypes = {
accessibilityLabel: PropTypes.string,
accessibilityLiveRegion: PropTypes.oneOf([ 'assertive', 'off', 'polite' ]),
accessibilityRole: PropTypes.string,
accessible: PropTypes.bool,
component: PropTypes.oneOfType([ PropTypes.func, PropTypes.string ]),
style: PropTypes.oneOfType([ PropTypes.array, PropTypes.object ]),
testID: PropTypes.string,
type: PropTypes.string
};
static defaultProps = {
accessible: true,
component: 'div'
};
render() {
const {
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,42 +0,0 @@
import React, { PropTypes } from 'react'
import restyle from './restyle'
import stylePropTypes from './stylePropTypes'
class CoreComponent extends React.Component {
static propTypes = {
className: PropTypes.string,
component: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string
]),
style: PropTypes.object,
testID: PropTypes.string
}
static stylePropTypes = stylePropTypes;
static defaultProps = {
className: '',
component: 'div'
}
render() {
const {
className,
component: Component,
style,
testID,
...other
} = this.props
return (
<Component
{...other}
{...restyle({ className, style })}
data-testid={process.env.NODE_ENV === 'production' ? null : testID}
/>
)
}
}
export default CoreComponent

View File

@@ -1,47 +0,0 @@
export default function prefixStyles(style) {
if (style.hasOwnProperty('flexBasis')) {
style = {
WebkitFlexBasis: style.flexBasis,
msFlexBasis: style.flexBasis,
...style
}
}
if (style.hasOwnProperty('flexGrow')) {
style = {
WebkitBoxFlex: style.flexGrow,
WebkitFlexGrow: style.flexGrow,
msFlexPositive: style.flexGrow,
...style
}
}
if (style.hasOwnProperty('flexShrink')) {
style = {
WebkitFlexShrink: style.flexShrink,
msFlexNegative: style.flexShrink,
...style
}
}
// NOTE: adding `;` to the string value prevents React from automatically
// adding a `px` suffix to the unitless value
if (style.hasOwnProperty('order')) {
style = {
WebkitBoxOrdinalGroup: `${parseInt(style.order, 10) + 1};`,
WebkitOrder: `${style.order}`,
msFlexOrder: `${style.order}`,
...style
}
}
if (style.hasOwnProperty('transform')) {
style = {
WebkitTransform: style.transform,
msTransform: style.transform,
...style
}
}
return style
}

View File

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

View File

@@ -1,104 +0,0 @@
import { PropTypes } from 'react'
const numberOrString = PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
])
const { string } = PropTypes
export default {
alignContent: string,
alignItems: string,
alignSelf: string,
backfaceVisibility: string,
// background
backgroundAttachment: string,
backgroundClip: string,
backgroundColor: string,
backgroundImage: string,
backgroundOrigin: string,
backgroundPosition: string,
backgroundRepeat: string,
backgroundSize: string,
// border color
borderColor: numberOrString,
borderBottomColor: numberOrString,
borderLeftColor: numberOrString,
borderRightColor: numberOrString,
borderTopColor: numberOrString,
// border-radius
borderRadius: numberOrString,
borderTopLeftRadius: numberOrString,
borderTopRightRadius: numberOrString,
borderBottomLeftRadius: numberOrString,
borderBottomRightRadius: numberOrString,
// border style
borderStyle: numberOrString,
borderBottomStyle: numberOrString,
borderLeftStyle: numberOrString,
borderRightStyle: numberOrString,
borderTopStyle: numberOrString,
// border width
borderWidth: numberOrString,
borderBottomWidth: numberOrString,
borderLeftWidth: numberOrString,
borderRightWidth: numberOrString,
borderTopWidth: numberOrString,
bottom: numberOrString,
boxSizing: string,
clear: string,
color: string,
direction: string,
display: string,
flexBasis: string,
flexDirection: string,
flexGrow: numberOrString,
flexShrink: numberOrString,
flexWrap: string,
float: string,
font: string,
fontFamily: string,
fontSize: string,
fontStyle: string,
fontWeight: string,
height: numberOrString,
justifyContent: string,
left: numberOrString,
letterSpacing: string,
lineHeight: numberOrString,
// margin
margin: numberOrString,
marginBottom: numberOrString,
marginLeft: numberOrString,
marginRight: numberOrString,
marginTop: numberOrString,
// min/max
maxHeight: numberOrString,
maxWidth: numberOrString,
minHeight: numberOrString,
minWidth: numberOrString,
opacity: numberOrString,
order: numberOrString,
overflow: string,
overflowX: string,
overflowY: string,
// padding
padding: numberOrString,
paddingBottom: numberOrString,
paddingLeft: numberOrString,
paddingRight: numberOrString,
paddingTop: numberOrString,
position: string,
right: numberOrString,
textAlign: string,
textDecoration: string,
textTransform: string,
top: numberOrString,
userSelect: string,
visibility: string,
whiteSpace: string,
width: numberOrString,
wordWrap: string,
zIndex: numberOrString
}

View File

@@ -1,4 +1,23 @@
import View from '../View'
import { PropTypes } from 'react'
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
import LayoutPropTypes from '../../apis/StyleSheet/LayoutPropTypes'
import TransformPropTypes from '../../apis/StyleSheet/TransformPropTypes'
const hiddenOrVisible = PropTypes.oneOf([ 'hidden', 'visible' ])
export default {
...(View.stylePropTypes)
...LayoutPropTypes,
...TransformPropTypes,
backfaceVisibility: hiddenOrVisible,
backgroundColor: ColorPropType,
/**
* @platform web
*/
boxShadow: PropTypes.string,
opacity: PropTypes.number,
overflow: hiddenOrVisible,
/**
* @platform web
*/
visibility: hiddenOrVisible
}

View File

@@ -0,0 +1,73 @@
/* eslint-env mocha */
import * as utils from '../../../modules/specHelpers'
import assert from 'assert'
import React from 'react'
import Image from '../'
suite('components/Image', () => {
test('default accessibility', () => {
const dom = utils.renderToDOM(<Image />)
assert.equal(dom.getAttribute('role'), 'img')
})
test('prop "accessibilityLabel"', () => {
const accessibilityLabel = 'accessibilityLabel'
const result = utils.shallowRender(<Image accessibilityLabel={accessibilityLabel} />)
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
})
test('prop "accessible"', () => {
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 = utils.renderToDOM(<Image defaultSource={defaultSource} />)
const backgroundImage = dom.style.backgroundImage
assert(backgroundImage.indexOf(defaultSource.uri) > -1)
})
test('prop "onError"', function (done) {
this.timeout(5000)
utils.render(<Image
onError={onError}
source={{ uri: 'https://google.com/favicon.icox' }}
/>)
function onError(e) {
assert.equal(e.nativeEvent.type, 'error')
done()
}
})
test('prop "onLoad"', function (done) {
this.timeout(5000)
utils.render(<Image
onLoad={onLoad}
source={{ uri: 'https://google.com/favicon.ico' }}
/>)
function onLoad(e) {
assert.equal(e.nativeEvent.type, 'load')
done()
}
})
test('prop "onLoadEnd"')
test('prop "onLoadStart"')
test('prop "resizeMode"')
test('prop "source"')
test('prop "testID"', () => {
const testID = 'testID'
const result = utils.shallowRender(<Image testID={testID} />)
assert.equal(result.props.testID, testID)
})
})

View File

@@ -1,8 +1,9 @@
/* global window */
import { pickProps } from '../../modules/filterObjectProps'
import StyleSheet from '../../apis/StyleSheet'
import CoreComponent from '../CoreComponent'
import ImageStylePropTypes from './ImageStylePropTypes'
import React, { PropTypes } from 'react'
import React, { Component, PropTypes } from 'react'
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
import View from '../View'
const STATUS_ERRORED = 'ERRORED'
@@ -11,82 +12,38 @@ const STATUS_LOADING = 'LOADING'
const STATUS_PENDING = 'PENDING'
const STATUS_IDLE = 'IDLE'
const styles = {
initial: {
alignSelf: 'flex-start',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundSize: 'cover'
},
img: {
borderWidth: 0,
height: 'auto',
maxHeight: '100%',
maxWidth: '100%',
opacity: 0
},
children: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0
},
resizeMode: {
clip: {
backgroundSize: 'auto'
},
contain: {
backgroundSize: 'contain'
},
cover: {
backgroundSize: 'cover'
},
stretch: {
backgroundSize: '100% 100%'
}
}
}
class Image extends React.Component {
export default class Image extends 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,
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
accessible: CoreComponent.propTypes.accessible,
children: PropTypes.any,
defaultSource: PropTypes.object,
onError: PropTypes.func,
onLoad: PropTypes.func,
onLoadEnd: PropTypes.func,
onLoadStart: PropTypes.func,
resizeMode: PropTypes.oneOf(['clip', 'contain', 'cover', 'stretch']),
resizeMode: PropTypes.oneOf(['contain', 'cover', 'none', 'stretch']),
source: PropTypes.object,
style: PropTypes.shape(ImageStylePropTypes),
style: StyleSheetPropType(ImageStylePropTypes),
testID: CoreComponent.propTypes.testID
}
static stylePropTypes = ImageStylePropTypes
};
static defaultProps = {
accessible: true,
defaultSource: {},
resizeMode: 'cover',
source: {},
style: styles.initial
}
_cancelEvent(event) {
event.preventDefault()
event.stopPropagation()
}
source: {}
};
_createImageLoader() {
const { source } = this.props
@@ -101,8 +58,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
}
}
@@ -113,8 +70,8 @@ class Image extends React.Component {
this._destroyImageLoader()
this.setState({ status: STATUS_ERRORED })
if (onError) onError(event)
this._onLoadEnd()
if (onError) onError(event)
}
_onLoad(e) {
@@ -165,6 +122,7 @@ class Image extends React.Component {
render() {
const {
accessibilityLabel,
accessible,
children,
defaultSource,
resizeMode,
@@ -176,7 +134,6 @@ class Image extends React.Component {
const isLoaded = this.state.status === STATUS_LOADED
const defaultImage = defaultSource.uri || null
const displayImage = !isLoaded ? defaultImage : source.uri
const resolvedStyle = pickProps(style, Object.keys(ImageStylePropTypes))
const backgroundImage = displayImage ? `url("${displayImage}")` : null
/**
@@ -189,21 +146,17 @@ class Image extends React.Component {
return (
<View
accessibilityLabel={accessibilityLabel}
aria-role='img'
className={'Image'}
component='div'
style={{
...(styles.initial),
...resolvedStyle,
...(backgroundImage && { backgroundImage }),
...(styles.resizeMode[resizeMode])
}}
accessibilityRole='img'
accessible={accessible}
style={[
styles.initial,
style,
backgroundImage && { backgroundImage },
resizeModeStyles[resizeMode]
]}
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}
@@ -212,4 +165,41 @@ class Image extends React.Component {
}
}
export default Image
const styles = StyleSheet.create({
initial: {
alignSelf: 'flex-start',
backgroundColor: 'transparent',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover'
},
img: {
borderWidth: 0,
height: 'auto',
maxHeight: '100%',
maxWidth: '100%',
opacity: 0
},
children: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0
}
})
const resizeModeStyles = StyleSheet.create({
contain: {
backgroundSize: 'contain'
},
cover: {
backgroundSize: 'cover'
},
none: {
backgroundSize: 'auto'
},
stretch: {
backgroundSize: '100% 100%'
}
})

View File

@@ -1,109 +0,0 @@
import assert from 'assert'
import React from 'react/addons'
import Image from '.'
import View from '../View'
const ReactTestUtils = React.addons.TestUtils
function shallowRender(component, context = {}) {
const shallowRenderer = React.addons.TestUtils.createRenderer()
shallowRenderer.render(component, context)
return shallowRenderer.getRenderOutput()
}
function render(component, node) {
return node ? React.render(component, node) : ReactTestUtils.renderIntoDocument(component)
}
function getImageDOM(props) {
const result = ReactTestUtils.renderIntoDocument(<Image {...props} />)
return React.findDOMNode(result)
}
suite('Image', () => {
test('defaults', () => {
const result = shallowRender(<Image />)
assert.equal(result.type, View)
})
test('prop "accessibilityLabel"', () => {
const accessibilityLabel = 'accessibilityLabel'
const element = getImageDOM()
const elementHasLabel = getImageDOM({ accessibilityLabel })
assert.equal(element.getAttribute('aria-label'), null)
assert.equal(elementHasLabel.getAttribute('aria-label'), accessibilityLabel)
})
test.skip('prop "children"', () => { })
test('prop "defaultSource"', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' }
const elementHasdefaultSource = getImageDOM({ defaultSource })
const backgroundImage = elementHasdefaultSource.style.backgroundImage
assert(backgroundImage.indexOf(defaultSource.uri) > -1)
})
test('prop "onError"', function (done) {
this.timeout(5000)
function onError(e) {
assert.equal(e.nativeEvent.type, 'error')
done()
}
render(<Image
onError={onError}
source={{ uri: 'https://google.com/favicon.icox' }}
/>)
})
test('prop "onLoad"', function (done) {
this.timeout(5000)
function onLoad(e) {
assert.equal(e.nativeEvent.type, 'load')
done()
}
render(<Image
onLoad={onLoad}
source={{ uri: 'https://google.com/favicon.ico' }}
/>)
})
test.skip('prop "onLoadEnd"', () => { })
test.skip('prop "onLoadStart"', () => { })
test.skip('prop "resizeMode"', () => { })
test.skip('prop "source"', () => { })
test('prop "style"', () => {
const initial = shallowRender(<Image />)
assert.deepEqual(
initial.props.style,
Image.defaultProps.style
)
const unsupported = shallowRender(<Image style={{ unsupported: 'true' }} />)
assert.deepEqual(
unsupported.props.style.unsupported,
null,
'unsupported "style" properties must not be transferred'
)
})
test('prop "testID"', () => {
const testID = 'Example.image'
const elementHasTestID = getImageDOM({ testID })
assert.equal(
elementHasTestID.getAttribute('data-testid'),
testID
)
})
})

Some files were not shown because too many files have changed in this diff Show More