Compare commits

...

128 Commits

Author SHA1 Message Date
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
Nicolas Gallagher
b9c8a560a0 0.0.5 2015-09-07 10:16:22 -07:00
Nicolas Gallagher
357201e843 Fix lint errors 2015-09-07 10:12:42 -07:00
Nicolas Gallagher
0ab345a490 Fix tests for Firefox 2015-09-07 10:00:06 -07:00
Nicolas Gallagher
2e087c10ad Add karma-firefox-launcher 2015-09-07 09:51:09 -07:00
Nicolas Gallagher
a7c1e105c6 Fix links to View doc 2015-09-07 09:49:38 -07:00
Nicolas Gallagher
969b57de53 Update examples 2015-09-07 09:45:16 -07:00
Nicolas Gallagher
50771fc5cd Misc fixes 2015-09-07 09:45:10 -07:00
Nicolas Gallagher
cd215a916d Remove legacy react-native-web-style module 2015-09-07 09:44:18 -07:00
Nicolas Gallagher
1668dc4635 Add placeholder ListView and ScrollView 2015-09-07 09:43:53 -07:00
Nicolas Gallagher
f0b6576e80 Add new Swipeable component 2015-09-07 09:43:19 -07:00
Nicolas Gallagher
7551596fa5 Add early Touchable implementation 2015-09-07 09:42:52 -07:00
Nicolas Gallagher
90e015112a Update Image props and implementation 2015-09-07 09:42:23 -07:00
Nicolas Gallagher
cfdbd351f0 Update TextInput props and implementation 2015-09-07 09:42:02 -07:00
Nicolas Gallagher
08013daa23 Update Text props and implementation 2015-09-07 09:41:37 -07:00
Nicolas Gallagher
e7a5d56058 Create CoreComponent; update View implementation 2015-09-07 09:40:51 -07:00
Nicolas Gallagher
abf2c0307f Rewrite documentation 2015-09-07 09:38:25 -07:00
Nicolas Gallagher
6bb6a17046 Fix travis build 2015-09-04 22:12:59 -07:00
Nicolas Gallagher
b5c8af2694 Documentation fixes 2015-09-03 23:12:17 -07:00
146 changed files with 6391 additions and 2331 deletions

View File

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

View File

@@ -1,3 +0,0 @@
dist
docs
example

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,3 +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.

200
README.md
View File

@@ -1,157 +1,115 @@
# React Native for Web
[![Build Status][travis-image]][travis-url]
[![npm version][npm-image]][npm-url] (14 KB gzipped)
[![npm version][npm-image]][npm-url]
![gzipped size](https://img.shields.io/badge/gzipped-~22.6k-blue.svg)
The core [React Native][react-native-url] components built for the web, backed
by a precomputed CSS library.
[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
Partial implementations of…
* [`Image`](docs/components/Image.md)
* [`Text`](docs/components/Text.md)
* [`TextInput`](docs/components/TextInput.md)
* [`View`](docs/components/View.md)
## Styling
React Native 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/react-native-web-style/styling.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.
* 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]: 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,45 +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: [ '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,36 @@
var constants = require('./constants')
var 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': '../../src'
}
}
}

View File

@@ -1,23 +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
module.exports = assign({}, base, {
module.exports = {
entry: {
main: './src/index'
main: constants.DIST_DIRECTORY
},
externals: [{
react: true
'react': true,
'react-dom': true,
'react-dom/server': true
}],
output: {
filename: 'react-native-web.js',
library: 'ReactNativeWeb',
libraryTarget: 'commonjs2',
path: './dist'
libraryTarget: 'umd',
path: constants.DIST_DIRECTORY
},
plugins: [
new UglifyJsPlugin({
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,
@@ -26,4 +29,4 @@ module.exports = assign({}, base, {
}
})
]
})
}

View File

@@ -1,66 +0,0 @@
# View
`View` is a flexbox container and the fundamental building block for UI. It is
designed to be nested inside other `View`'s and to have 0-to-many children of
any type.
## PropTypes
All other props are transferred directly to the `element`.
+ `component`: `func` or `string` (default `"div"`)
+ `pointerEvents`: `oneOf('all', 'box-only', 'box-none', 'none')`
+ `style`: `ViewStylePropTypes`
## ViewStylePropTypes
+ BackgroundPropTypes
+ BorderThemePropTypes
+ LayoutPropTypes
+ `boxShadow`: `string`
+ `color`: `string`
+ `opacity`: `number`
## ViewStyleDefaultProps
Implements the default styles from
[facebook/css-layout](https://github.com/facebook/css-layout).
1. All the flex elements are oriented from top-to-bottom, left-to-right and do
not shrink. This is how things are laid out using the default CSS settings
and what you'd expect.
2. The most convenient way to express the relation between width and other
box-model properties.
3. Everything is `display:flex` by default. All the behaviors of `block` and
`inline-block` can be expressed in term of flex but not the opposite.
4. Everything is `position:relative`. This makes `position:absolute` target the
direct parent and not some parent which is either relative or absolute. If
you want to position an element relative to something else, you should move
it in the DOM instead of relying of CSS. It also makes `top`, `left`,
`right`, `bottom` do something when not specifying `position:absolute`.
```js
const ViewDefaultStyle = {
alignItems: 'stretch', // 1
borderWidth: 0,
borderStyle: 'solid',
boxSizing: 'border-box', // 2
display: 'flex', // 3
flexBasis: 'auto', // 1
flexDirection: 'column', // 1
flexShrink: 0, // 1
listStyle: 'none',
margin: 0,
padding: 0,
position: 'relative' // 4
};
```
## Examples
```js
// TODO
```

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

@@ -1,31 +1,92 @@
# Image spec
# Image
#### PropTypes
An accessibile image component with support for image resizing, default image,
and child content.
All other props are transferred directly to the `element`.
Unsupported React Native props:
`capInsets` (ios),
`onProgress` (ios)
+ `accessibilityLabel`: `string`
+ `async`: `bool` (TODO)
+ `className`: `string`
+ `source`: `string`
+ `style`: `ImageStylePropTypes`
## Props
#### ImageStylePropTypes
**accessibilityLabel**: string
+ `BackgroundPropTypes`
+ `BorderThemePropTypes`
+ `LayoutPropTypes`
+ `opacity`: `string`
The text that's read by a screenreader when someone interacts with the image.
#### Examples
**accessible**: bool
When `false`, the view is hidden from screenreaders. Default: `true`.
**children**: any
Content to display over the image.
**defaultSource**: { uri: string }
An image to display as a placeholder while downloading the final image off the network.
**onError**: function
Invoked on load error with `{nativeEvent: {error}}`.
**onLayout**: function
TODO
**onLoad**: function
Invoked when load completes successfully.
**onLoadEnd**: function
Invoked when load either succeeds or fails,
**onLoadStart**: function
Invoked on load start.
**resizeMode**: oneOf('contain', 'cover', 'none', 'stretch') = 'stretch'
Determines how to resize the image when the frame doesn't match the raw image
dimensions.
**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
+ ...[View#style](View.md)
Defaults:
```js
import {Image} from 'react-web-sdk';
import React, {PropTypes} from 'react';
{
alignSelf: 'flex-start',
backgroundColor: 'transparent'
}
```
**testID**: string
Used to locate a view in end-to-end tests.
## Examples
```js
import placeholderAvatar from './placeholderAvatar.png'
import React, { Component, Image, PropTypes, StyleSheet } from 'react-native'
export default class ImageExample extends Component {
constructor(props, context) {
super(props, context)
this.state = { loading: true }
}
class Avatar extends React.Component {
static propTypes = {
size: PropTypes.oneOf(['small', 'normal', 'large']),
testID: Image.propTypes.testID,
user: PropTypes.object
}
@@ -33,34 +94,52 @@ class Avatar extends React.Component {
size: 'normal'
}
_onLoad(e) {
console.log('Avatar.onLoad', e)
this.setState({ loading: false })
}
render() {
const { size, testID, user } = this.props
const loadingStyle = this.state.loading ? { styles.loading } : { }
return (
<Image
accessibilityLabel={`${user.name}'s profile picture`}
source={user.avatarUrl}
style={ ...style.base, ...style[this.props.size] }
defaultSource={{ uri: placeholderAvatar }}
onLoad={this._onLoad.bind(this)}
resizeMode='cover'
source={{ uri: user.avatarUrl }}
style={[
styles.base,
styles[size],
loadingStyle
]}
/>
);
)
}
}
const style = {
const styles = StyleSheet.create({
base: {
borderColor: 'white',
borderRadius: '5px',
borderWidth: '5px'
borderRadius: 5,
borderWidth: 5
},
loading: {
opacity: 0.5
},
small: {
height: '32px',
width: '32px'
height: 32,
width: 32
},
normal: {
height: '48px',
width: '48px'
height: 48,
width: 48
},
large: {
height: '64px',
width: '32px'
height: 64,
width: 64
}
}
})
```

View File

@@ -0,0 +1,31 @@
# ListView
TODO
## Props
**children**: any
Content to display over the image.
**style**: style
+ ...[View#style](View.md)
## Examples
```js
import React, { Component, ListView, PropTypes } from 'react-native'
export default class ListViewExample extends Component {
static propTypes = {}
static defaultProps = {}
render() {
return (
<ListView />
)
}
}
```

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

@@ -0,0 +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
Child content.
**contentContainerStyle**: style
These styles will be applied to the scroll view content container which wraps
all of the child views.
**horizontal**: bool = false
When true, the scroll view's children are arranged horizontally in a row
instead of vertically in a column.
**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, { Component, ScrollView, StyleSheet } from 'react-native'
import Item from './Item'
export default class ScrollViewExample extends Component {
constructor(props, context) {
super(props, context)
this.state = {
items: Array.from({ length: 20 }).map((_, i) => ({ id: i }))
}
}
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,37 +1,95 @@
# Text spec
# Text
Text layout and styles.
`Text` is component for displaying text. It supports style, basic touch
handling, and inherits typographic styles from ancestor elements.
#### PropTypes
The `Text` is unique relative to layout: child elements use text layout
(`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.
All other props are transferred directly to the `element`.
Unsupported React Native props:
`allowFontScaling` (ios),
`suppressHighlighting` (ios)
+ `element`: `func` or `string` (default `"div"`)
+ `style`: `TextStylePropTypes`
## Props
#### TextStylePropTypes
NOTE: `Text` will transfer all other props to the rendered HTML element.
+ ViewStylePropTypes
+ TypographicPropTypes
(web) **accessibilityLabel**: string
Defines the text available to assistive technologies upon interaction with the
element. (This is implemented using `aria-label`.)
(web) **accessibilityRole**: oneOf(roles)
Allows assistive technologies to present and support interaction with the view
in a manner that is consistent with user expectations for similar views of that
type. For example, marking a touchable view with an `accessibilityRole` of
`button`. (This is implemented using [ARIA roles](http://www.w3.org/TR/wai-aria/roles#role_definitions)).
Note: Avoid changing `accessibilityRole` values over time or after user
actions. Generally, accessibility APIs do not provide a means of notifying
assistive technologies of a `role` value change.
(web) **accessible**: bool = true
When `false`, the text is hidden from assistive technologies. (This is
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
+ ...[View#style](View.md)
+ `color`
+ `fontFamily`
+ `fontSize`
+ `fontStyle`
+ `fontWeight`
+ `letterSpacing`
+ `lineHeight`
+ `textAlign`
+ `textDecoration`
+ `textShadow`
+ `textTransform`
+ `whiteSpace`
+ `wordWrap`
+ `writingDirection`
**testID**: string
Used to locate this view in end-to-end tests.
## Examples
```js
import {Text} from 'react-web-sdk';
import React, {PropTypes} from 'react';
import React, { Component, PropTypes, StyleSheet, Text } from 'react-native'
class PrettyText extends React.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;
@@ -39,32 +97,32 @@ class PrettyText extends React.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

@@ -1,15 +1,197 @@
# TextInput spec
# TextInput
Text input layout and styles.
Accessible single- and multi-line text input via a keyboard. Supports features
such as auto-complete, auto-focus, placeholder text, and event callbacks.
#### PropTypes
Note: some props are exclusive to or excluded from `multiline`.
All other props are transferred directly to the `element`.
Unsupported React Native props:
`autoCapitalize`,
`autoCorrect`,
`onEndEditing`,
`onSubmitEditing`,
`clearButtonMode` (ios),
`enablesReturnKeyAutomatically` (ios),
`returnKeyType` (ios),
`selectionState` (ios),
`textAlign` (android),
`textAlignVertical` (android),
`underlineColorAndroid` (android)
+ `component`: `func` or `string` (default `"div"`)
+ `style`: `TextStylePropTypes`
## Props
#### TextStylePropTypes
(web) **accessibilityLabel**: string
+ ViewStylePropTypes
+ TypographicPropTypes
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 = false
If true, focuses the input on `componentDidMount`. Only the first form element
in a document with `autofocus` is focused.
**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 = true
If `false`, text is not editable (i.e., read-only).
**keyboardType**: oneOf('default', 'email-address', 'numeric', 'phone-pad', 'search', 'url', 'web-search') = 'default'
Determines which keyboard to open. (NOTE: Safari iOS requires an ancestral
`<form action>` element to display the `search` keyboard).
(Not available when `multiline` is `true`.)
**maxLength**: number
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
Callback that is called when the text input's text changes.
**onChangeText**: function
Callback that is called when the text input's text changes. The text is passed
as an argument to the callback handler.
**onFocus**: function
Callback that is called when the text input is focused.
**onLayout**: function
TODO
(web) **onSelectionChange**: function
Callback that is called when the text input's selection changes. The following
object is passed as an argument to the callback handler.
```js
{
selectionDirection,
selectionEnd,
selectionStart,
nativeEvent
}
```
**placeholder**: string
The string that will be rendered in an empty `TextInput` before text has been
entered.
**placeholderTextColor**: string
The text color of the placeholder string.
**secureTextEntry**: bool = false
If true, the text input obscures the text entered so that sensitive text like
passwords stay secure.
(Not available when `multiline` is `true`.)
**selectTextOnFocus**: bool = false
If `true`, all text will automatically be selected on focus.
**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, { Component, StyleSheet, TextInput } from 'react-native'
export default class TextInputExample extends Component {
constructor(props, context) {
super(props, context)
this.state = { isFocused: false }
}
_onBlur(e) {
this.setState({ isFocused: false })
}
_onFocus(e) {
this.setState({ isFocused: true })
}
render() {
return (
<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 = StyleSheet.create({
default: {
borderColor: 'gray',
borderBottomWidth: 2
},
focused: {
borderColor: 'blue'
}
})
```

View File

@@ -0,0 +1,100 @@
# 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
**accessibilityLabel**: string
Overrides the text that's read by the screen reader when the user interacts
with the element.
(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.
(web) **activeUnderlayColor**: string = 'black'
Sets the color of the background highlight when `onPressIn` is called. The
highlight is removed when `onPressOut` is called.
**children**: element
A single child element.
**delayLongPress**: number = 500
Delay in ms, from `onPressIn`, before `onLongPress` is called.
**delayPressIn**: number = 0
(TODO)
Delay in ms, from the start of the touch, before `onPressIn` is called.
**delayPressOut**: number = 100
(TODO)
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, { Component, PropTypes, Touchable } from 'react-native'
export default class Example extends Component {
static propTypes = {}
static defaultProps = {}
render() {
return (
<Touchable />
)
}
}
```

View File

@@ -1,66 +1,195 @@
# View spec
# View
`View` is a flexbox container and the fundamental building block for UI. It is
designed to be nested inside other `View`'s and to have 0-to-many children of
any type.
`View` is the fundamental UI building block. It is a component that supports
style, layout with flexbox, and accessibility controls. It can be nested
inside another `View` and has 0-to-many children of any type.
## PropTypes
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)
All other props are transferred directly to the `element`.
## Props
+ `element`: `func` or `string` (default `"div"`)
+ `pointerEvents`: `oneOf('all', 'box-only', 'box-none', 'none')`
+ `style`: `ViewStylePropTypes`
NOTE: `View` will transfer all other props to the rendered HTML element.
## ViewStylePropTypes
**accessibilityLabel**: string
+ BackgroundPropTypes
+ BorderThemePropTypes
+ LayoutPropTypes
+ `boxShadow`: `string`
+ `color`: `string`
+ `opacity`: `number`
Defines the text available to assistive technologies upon interaction with the
element. (This is implemented using `aria-label`.)
## ViewStyleDefaultProps
**accessibilityLiveRegion**: oneOf('assertive', 'off', 'polite') = 'off'
Implements the default styles from
[facebook/css-layout](https://github.com/facebook/css-layout).
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).)
1. All the flex elements are oriented from top-to-bottom, left-to-right and do
not shrink. This is how things are laid out using the default CSS settings
and what you'd expect.
(web) **accessibilityRole**: oneOf(roles)
2. The most convenient way to express the relation between width and other
box-model properties.
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)).
3. Everything is `display:flex` by default. All the behaviors of `block` and
`inline-block` can be expressed in term of flex but not the opposite.
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.
4. Everything is `position:relative`. This makes `position:absolute` target the
direct parent and not some parent which is either relative or absolute. If
you want to position an element relative to something else, you should move
it in the DOM instead of relying of CSS. It also makes `top`, `left`,
`right`, `bottom` do something when not specifying `position:absolute`.
**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:
```css
.box-none { pointer-events: none }
.box-none * { pointer-events: auto }
```
`box-only` is the equivalent of:
```css
.box-only { pointer-events: auto }
.box-only * { pointer-events: none }
```
**style**: style
+ `alignContent`
+ `alignItems`
+ `alignSelf`
+ `backfaceVisibility`
+ `backgroundAttachment`
+ `backgroundClip`
+ `backgroundColor`
+ `backgroundImage`
+ `backgroundOrigin`
+ `backgroundPosition`
+ `backgroundRepeat`
+ `backgroundSize`
+ `borderColor`
+ `borderRadius`
+ `borderStyle`
+ `borderWidth`
+ `bottom`
+ `boxShadow`
+ `boxSizing`
+ `cursor`
+ `flex` (number)
+ `flexBasis`
+ `flexDirection`
+ `flexGrow`
+ `flexShrink`
+ `flexWrap`
+ `height`
+ `justifyContent`
+ `left`
+ `margin` (single value)
+ `marginBottom`
+ `marginHorizontal`
+ `marginLeft`
+ `marginRight`
+ `marginTop`
+ `marginVertical`
+ `maxHeight`
+ `maxWidth`
+ `minHeight`
+ `minWidth`
+ `opacity`
+ `order`
+ `overflow`
+ `overflowX`
+ `overflowY`
+ `padding` (single value)
+ `paddingBottom`
+ `paddingHorizontal`
+ `paddingLeft`
+ `paddingRight`
+ `paddingTop`
+ `paddingVertical`
+ `position`
+ `right`
+ `top`
+ `transform`
+ `userSelect`
+ `visibility`
+ `width`
+ `zIndex`
Default:
```js
const ViewStyleDefaultProps = {
alignItems: 'stretch', // 1
{
alignItems: 'stretch',
borderWidth: 0,
borderStyle: 'solid',
boxSizing: 'border-box', // 2
display: 'flex', // 3
flexBasis: 'auto', // 1
flexDirection: 'column', // 1
flexShrink: 0, // 1
listStyle: 'none',
boxSizing: 'border-box',
display: 'flex',
flexBasis: 'auto',
flexDirection: 'column',
flexShrink: 0,
margin: 0,
padding: 0,
position: 'relative' // 4
position: 'relative'
};
```
(See [facebook/css-layout](https://github.com/facebook/css-layout)).
**testID**: string
Used to locate this view in end-to-end tests.
## Examples
```js
// TODO
import React, { Component, PropTypes, StyleSheet, View } from 'react-native'
export default class ViewExample extends Component {
render() {
return (
<View style={styles.row}>
{
['1', '2', '3', '4', '5'].map((value, i) => {
return <View children={value} key={i} style={styles.cell} />
})
}
</View>
);
}
}
const styles = StyleSheet.create({
row: {
flexDirection: 'row'
},
cell: {
flexGrow: 1
}
})
```

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,89 +0,0 @@
## StyleProp spec
### Background
+ `backgroundColor`: `string`
+ `backgroundImage`: `string`
+ `backgroundPosition`: `string`
+ `backgroundRepeat`: `string`
+ `backgroundSize`: `string`
### BorderTheme
+ `borderColor`: `string`
+ `borderTopColor`: `string`
+ `borderRightColor`: `string`
+ `borderBottomColor`: `string`
+ `borderLeftColor`: `string`
+ `borderStyle`: `string`
+ `borderRadius`: `number` or `string`
+ `borderTopLeftRadius`: `number` or `string`
+ `borderTopRightRadius`: `number` or `string`
+ `borderBottomLeftRadius`: `number` or `string`
+ `borderBottomRightRadius`: `number` or `string`
### BoxModel
+ `borderWidth`: `number` or `string`
+ `borderTopWidth`: `number` or `string`
+ `borderRightWidth`: `number` or `string`
+ `borderBottomWidth`: `number` or `string`
+ `borderLeftWidth`: `number` or `string`
+ `boxSizing`: `oneOf('border-box', 'content-box')`
+ `display`: `oneOf('block', 'flex', 'inline', 'inline-block', 'inline-flex')`
+ `height`: `number` or `string`
+ `margin`: `number` or `string`
+ `marginTop`: `number` or `string`
+ `marginRight`: `number` or `string`
+ `marginBottom`: `number` or `string`
+ `marginLeft`: `number` or `string`
+ `padding`: `number` or `string`
+ `paddingTop`: `number` or `string`
+ `paddingRight`: `number` or `string`
+ `paddingBottom`: `number` or `string`
+ `paddingLeft`: `number` or `string`
+ `width`: `number` or `string`
### Flexbox
* `alignContent`: `oneOf('center', 'flex-end', 'flex-start', 'stretch', 'space-around', 'space-between')`
* `alignItems`: `oneOf('baseline', 'center', 'flex-end', 'flex-start', 'stretch')`
* `alignSelf`: `oneOf('auto', 'baseline', 'center', 'flex-end', 'flex-start', 'stretch')`
* `flex`: `string`
* `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`
### Layout
* BoxModel
* Flexbox
* Position
### Position
* `position`: `oneOf('absolute', 'fixed', 'relative')`
* `bottom`: `number` or `string`
* `left`: `number` or `string`
* `right`: `number` or `string`
* `top`: `number` or `string`
* `zIndex`: `number`
### Typographic
* `direction`: `oneOf('auto', 'ltr', 'rtl')`
* `fontFamily`: `string`
* `fontSize`: `string`
* `fontWeight`: `oneOf('100', '200', '300', '400', '500', '600', '700', '800', '900', 'bold', 'normal')`
* `fontStyle`: `oneOf('normal', 'italic')`
* `letterSpacing`: `string`
* `lineHeight`: `number` or `string`
* `textAlign`: `oneOf('auto', 'left', 'right', 'center')`
* `textDecoration`: `oneOf('none', 'underline')`
* `textTransform`: `oneOf('capitalize', 'lowercase', 'none', 'uppercase')`
* `wordWrap`: `oneOf('break-word', 'normal')`

View File

@@ -1,125 +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 Web SDK (in a divergence from React Native) represents style using plain
JS objects:
```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
One strategy for converting styles from JS to CSS is to map style objects to
CSS rules. Another strategy is to map declarations to declarataions.
![](../static/styling-strategy.png)
Mapping entire `style` objects to CSS rules can lead to increasingly large CSS
files. Each new component adds new rules to the stylesheet.
React Web SDK includes a proof-of-concept for the strategy of automatically
mapping unique declarations to declarations, via a unique selector for each
declaration. This strategy results in smaller CSS files because an application
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. But 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 SDK's
`TextInput` component (see React Native's API).

View File

@@ -1,52 +0,0 @@
import React, { Image, Text, TextInput, View } from '../dist/react-native-web';
class Example extends React.Component {
render() {
return (
<View>
<View style={styles.grid}>
{[1,2,3,4,5,6].map((item, i) => {
return (
<View key={i} style={{ ...styles.box, ...(item === 6 && styles.boxFull) }}>
<Text style={{ fontSize: '2rem' }}>{item}</Text>
</View>
);
})}
</View>
<View style={{
alignItems: 'center',
borderWidth: '1px',
justifyContent: 'center',
marginTop: '10px',
height: '200px'
}}>
<Text>This should be centered</Text>
</View>
<TextInput type="text" autoFocus />
<TextInput multiline defaultValue="default value" />
</View>
);
}
}
const styles = {
grid: {
flexDirection: 'row',
flexWrap: 'wrap'
},
box: {
alignItems: 'center',
backgroundColor: 'lightblue',
flexGrow: 1,
justifyContent: 'center',
borderColor: 'blue',
borderWidth: '5px'
},
boxFull: {
width: '100%'
}
}
React.render(<Example />, document.getElementById('react-root'));

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'
}
};

267
examples/components/App.js Normal file
View File

@@ -0,0 +1,267 @@
import GridView from './GridView'
import Heading from './Heading'
import MediaQueryWidget from './MediaQueryWidget'
import React, { Image, StyleSheet, ScrollView, Text, TextInput, TouchableHighlight, View } from 'react-native'
export default class App extends React.Component {
static propTypes = {
mediaQuery: React.PropTypes.object,
style: View.propTypes.style
}
constructor(...args) {
super(...args)
this.state = {
scrollEnabled: true
}
}
render() {
const { mediaQuery } = this.props
const 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>}
defaultSource={{
uri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAIAAAAP3aGbAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wkGESkdPWMDggAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAD5UlEQVR42u3UMQ0AAAgEMcC/x7eCCgaSVsIN10kK4IORADAsAMMCDAvAsAAMCzAsAMMCMCzAsAAMC8CwAMMCMCwAwwIMC8CwAAwLMCwAwwIwLMCwAAwLwLAAwwIwLADDAgwLwLAAwwIwLADDAgwLwLAADAswLADDAjAswLAADAvAsADDAjAsAMMCDAvAsAAMCzAsAMMCMCzAsAAMC8CwAMMCMCwAwwIMC8CwAMMCMCwAwwIMC8CwAAwLMCwAwwIwLMCwAAwLwLAAwwIwLADDAgwLwLAADAswLADDAjAswLAADAvAsADDAjAsAMMCDAvAsADDAjAsAMMCDAvAsAAMCzAsAMMCMCzAsAAMC8CwAMMCMCwAwwIMC8CwAAwLMCwAwwIwLMCwAAwLwLAAwwIwLADDAgwLwLAAwwIwLADDAgwLwLAADAswLADDAjAswLAADAvAsADDAjAsAMMCDAvAsAAMCzAsAMMCMCzAsAAMC8CwAMMCMCwAwwIMC8CwAMMCMCwAwwIMC8CwAAwLMCwAwwIwLMCwAAwLwLAAwwIwLADDAgwLwLAADAswLADDAjAswLAADAvAsADDAjAsAMMCDAvAsADDAjAsAMMCDAvAsAAMCzAsAMMCMCzAsAAMC8CwAMMCMCwAwwIMC8CwAAwLMCwAwwIwLMCwAAwLwLAAwwIwLADDAgwLwLAAwwIwLADDAgwLwLAADAswLADDAjAswLAADAvAsADDAjAsAMMCDAvAsAAMCzAsAMMCMCzAsAAMC8CwAMMCMCwAwwIMC8CwAMMCMCwAwwIMC8CwAAwLMCwAwwIwLMCwAAwLwLAAwwIwLADDAgwLwLAADAswLADDAjAswLAADAvAsADDAjAswLAkAAwLwLAAwwIwLADDAgwLwLAADAswLADDAjAswLAADAvAsADDAjAsAMMCDAvAsAAMCzAsAMMCMCzAsAAMC8CwAMMCMCzAsAAMC8CwAMMCMCwAwwIMC8CwAAwLMCwAwwIwLMCwAAwLwLAAwwIwLADDAgwLwLAADAswLADDAjAswLAADAvAsADDAjAswLAADAvAsADDAjAsAMMCDAvAsAAMCzAsAMMCMCzAsAAMC8CwAMMCMCwAwwIMC8CwAAwLMCwAwwIwLMCwAAwLwLAAwwIwLMCwAAwLwLAAwwIwLADDAgwLwLAADAswLADDAjAswLAADAvAsADDAjAsAMMCDAvAsAAMCzAsAMMCMCzAsAAMC8CwAMMCMCzAsAAMC8CwAMMCMCwAwwIMC8CwAAwLMCwAwwIwLMCwAAwLwLAAwwIwLADDAgwLwLAADAswLADDAjAswLAALi04UQW9HF910gAAAABJRU5ErkJggg=='
}}
onError={(e) => { console.log('Image.onError', e) }}
onLoad={(e) => { console.log('Image.onLoad', e) }}
onLoadEnd={() => { console.log('Image.onLoadEnd') }}
onLoadStart={() => { console.log('Image.onLoadStart') }}
resizeMode={'contain'}
source={{
height: 400,
uri: 'http://facebook.github.io/react/img/logo_og.png',
width: 400
}}
style={{
borderWidth: '5px'
}}
testID='Example.image'
/>
<Heading size='large'>Text</Heading>
<Text
onPress={(e) => { console.log('Text.onPress', e) }}
testID={'Example.text'}
>
PRESS ME.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vel
lectus urna. Aliquam vitae justo porttitor, aliquam erat nec,
venenatis diam. Vivamus facilisis augue non urna mattis ultricies.
Suspendisse et vulputate enim, a maximus nulla. Vivamus imperdiet
hendrerit consequat. Aliquam lorem quam, elementum eget ex nec,
ultrices porttitor nibh. Nulla pellentesque urna leo, a aliquet elit
rhoncus a. Aenean ultricies, nunc a interdum dictum, dui odio
scelerisque mauris, a fringilla elit ligula vel sem. Sed vel aliquet
ipsum, sed rhoncus velit. Vivamus commodo pretium libero id placerat.
</Text>
<Text numberOfLines={1}>
TRUNCATED after 1 line.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vel
lectus urna. Aliquam vitae justo porttitor, aliquam erat nec,
venenatis diam. Vivamus facilisis augue non urna mattis ultricies.
Suspendisse et vulputate enim, a maximus nulla. Vivamus imperdiet
hendrerit consequat.
</Text>
<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 />
<TextInput defaultValue='read only' editable={false} />
<TextInput keyboardType='email-address' placeholder='you@domain.com' placeholderTextColor='red' />
<TextInput keyboardType='numeric' />
<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 size='large'>Touchable</Heading>
<TouchableHighlight
accessibilityLabel={'Touchable element'}
activeHighlight='lightblue'
activeOpacity={0.8}
onLongPress={(e) => { console.log('Touchable.onLongPress', e) }}
onPress={(e) => { console.log('Touchable.onPress', e) }}
onPressIn={(e) => { console.log('Touchable.onPressIn', e) }}
onPressOut={(e) => { console.log('Touchable.onPressOut', e) }}
>
<View style={styles.touchableArea}>
<Text>Touchable area (press, long press)</Text>
</View>
</TouchableHighlight>
<Heading size='large'>View</Heading>
<Heading>Default layout</Heading>
<View>
{[ 1, 2, 3, 4, 5, 6 ].map((item, i) => {
return (
<View key={i} style={styles.box}>
<Text>{item}</Text>
</View>
)
})}
</View>
<Heading>Row layout</Heading>
<View style={styles.row}>
{[ 1, 2, 3, 4, 5, 6 ].map((item, i) => {
return (
<View key={i} style={styles.box}>
<Text>{item}</Text>
</View>
)
})}
</View>
<Heading>pointerEvents</Heading>
<GridView alley='10px'>
{['box-none', 'box-only', 'none'].map((value, i) => {
return (
<View
accessibilityRole='link'
children={value}
href='https://google.com'
key={i}
pointerEvents={value}
style={styles.pointerEventsBox}
/>
)
})}
</GridView>
<Heading size='large'>ScrollView</Heading>
<label>
<input
checked={this.state.scrollEnabled}
onChange={() => this.setState({
scrollEnabled: !this.state.scrollEnabled
})}
type='checkbox'
/> Enable scroll
</label>
<Heading>Default layout</Heading>
<View style={styles.scrollViewContainer}>
<ScrollView
contentContainerStyle={styles.scrollViewContentContainerStyle}
onScroll={e => console.log('ScrollView.onScroll', e)}
scrollEnabled={this.state.scrollEnabled}
scrollEventThrottle={1} // 1 event per second
style={styles.scrollViewStyle}
>
{Array.from({ length: 50 }).map((item, i) => (
<View key={i} style={styles.box}>
<Text>{i}</Text>
</View>
))}
</ScrollView>
</View>
<Heading>Horizontal layout</Heading>
<View style={styles.scrollViewContainer}>
<ScrollView
contentContainerStyle={styles.scrollViewContentContainerStyle}
horizontal
onScroll={e => console.log('ScrollView.onScroll', e)}
scrollEnabled={this.state.scrollEnabled}
scrollEventThrottle={1} // 1 event per second
style={styles.scrollViewStyle}
>
{Array.from({ length: 50 }).map((item, i) => (
<View key={i} style={[ styles.box, styles.horizontalBox ]}>
<Text>{i}</Text>
</View>
))}
</ScrollView>
</View>
</View>
</ScrollView>
)
}
}
const rootStyles = StyleSheet.create({
common: {
marginVertical: 0,
marginHorizontal: 'auto'
},
mqSmall: {
maxWidth: '400px'
},
mqLarge: {
maxWidth: '600px'
}
})
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
flexWrap: 'wrap'
},
box: {
alignItems: 'center',
flexGrow: 1,
justifyContent: 'center',
borderWidth: 1
},
horizontalBox: {
width: '50px'
},
boxFull: {
width: '100%'
},
pointerEventsBox: {
alignItems: 'center',
borderWidth: '1px',
flexGrow: 1,
height: '100px',
justifyContent: 'center'
},
touchableArea: {
alignItems: 'center',
borderWidth: 1,
height: '200px',
justifyContent: 'center'
},
scrollViewContainer: {
height: '200px'
},
scrollViewStyle: {
borderWidth: '1px'
},
scrollViewContentContainerStyle: {
padding: '10px'
}
})

View File

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

View File

@@ -0,0 +1,34 @@
import React, { StyleSheet, Text } from '../../src'
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'
}
})
const Heading = ({ children, size = 'normal' }) => (
<Text
accessibilityRole='heading'
children={children}
style={{ ...styles.root, ...sizeStyles[size] }}
/>
)
export default Heading

View File

@@ -0,0 +1,39 @@
import React, { StyleSheet, Text, View } from '../../src'
const styles = StyleSheet.create({
root: {
alignItems: 'center',
borderWidth: 1,
marginVertical: 10,
padding: 10
},
heading: {
fontWeight: 'bold',
padding: 5,
textAlign: 'center'
},
text: {
textAlign: 'center'
}
})
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>
)
}
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 '../src'
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,53 +1,76 @@
{
"name": "react-native-web",
"version": "0.0.4",
"version": "0.0.14",
"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",
"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"
"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-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 '../../apis/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', () => {})
})

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

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

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

@@ -0,0 +1,64 @@
import React, { Component, PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
const roleComponents = {
article: 'article',
banner: 'header',
button: 'button',
complementary: 'aside',
contentinfo: 'footer',
form: 'form',
heading: 'h1',
link: 'a',
list: 'ul',
listitem: 'li',
main: 'main',
navigation: 'nav',
region: 'section'
}
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

@@ -0,0 +1,23 @@
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 {
...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

@@ -0,0 +1,205 @@
/* global window */
import StyleSheet from '../../apis/StyleSheet'
import CoreComponent from '../CoreComponent'
import ImageStylePropTypes from './ImageStylePropTypes'
import React, { Component, PropTypes } from 'react'
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
import View from '../View'
const STATUS_ERRORED = 'ERRORED'
const STATUS_LOADED = 'LOADED'
const STATUS_LOADING = 'LOADING'
const STATUS_PENDING = 'PENDING'
const STATUS_IDLE = 'IDLE'
export default class Image extends Component {
constructor(props, context) {
super(props, context)
const { uri } = props.source
// state
this.state = { status: uri ? STATUS_PENDING : STATUS_IDLE }
// autobinding
this._onError = this._onError.bind(this)
this._onLoad = this._onLoad.bind(this)
}
static propTypes = {
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(['contain', 'cover', 'none', 'stretch']),
source: PropTypes.object,
style: StyleSheetPropType(ImageStylePropTypes),
testID: CoreComponent.propTypes.testID
};
static defaultProps = {
accessible: true,
defaultSource: {},
resizeMode: 'cover',
source: {}
};
_createImageLoader() {
const { source } = this.props
this._destroyImageLoader()
this.image = new window.Image()
this.image.onerror = this._onError
this.image.onload = this._onLoad
this.image.src = source.uri
this._onLoadStart()
}
_destroyImageLoader() {
if (this.image) {
this.image.onerror = null
this.image.onload = null
this.image = null
}
}
_onError(e) {
const { onError } = this.props
const event = { nativeEvent: e }
this._destroyImageLoader()
this.setState({ status: STATUS_ERRORED })
this._onLoadEnd()
if (onError) onError(event)
}
_onLoad(e) {
const { onLoad } = this.props
const event = { nativeEvent: e }
this._destroyImageLoader()
this.setState({ status: STATUS_LOADED })
if (onLoad) onLoad(event)
this._onLoadEnd()
}
_onLoadEnd() {
const { onLoadEnd } = this.props
if (onLoadEnd) onLoadEnd()
}
_onLoadStart() {
const { onLoadStart } = this.props
this.setState({ status: STATUS_LOADING })
if (onLoadStart) onLoadStart()
}
componentDidMount() {
if (this.state.status === STATUS_PENDING) {
this._createImageLoader()
}
}
componentDidUpdate() {
if (this.state.status === STATUS_PENDING && !this.image) {
this._createImageLoader()
}
}
componentWillReceiveProps(nextProps) {
if (this.props.source.uri !== nextProps.source.uri) {
this.setState({
status: nextProps.source.uri ? STATUS_PENDING : STATUS_IDLE
})
}
}
componentWillUnmount() {
this._destroyImageLoader()
}
render() {
const {
accessibilityLabel,
accessible,
children,
defaultSource,
resizeMode,
source,
style,
testID
} = this.props
const isLoaded = this.state.status === STATUS_LOADED
const defaultImage = defaultSource.uri || null
const displayImage = !isLoaded ? defaultImage : source.uri
const backgroundImage = displayImage ? `url("${displayImage}")` : null
/**
* Image is a non-stretching View. The image is displayed as a background
* image to support `resizeMode`. The HTML image is hidden but used to
* provide the correct responsive image dimensions, and to support the
* image context menu. Child content is rendered into an element absolutely
* positioned over the image.
*/
return (
<View
accessibilityLabel={accessibilityLabel}
accessibilityRole='img'
accessible={accessible}
style={[
styles.initial,
style,
backgroundImage && { backgroundImage },
resizeModeStyles[resizeMode]
]}
testID={testID}
>
<img src={displayImage} style={styles.img} />
{children ? (
<View children={children} pointerEvents='box-none' style={styles.children} />
) : null}
</View>
)
}
}
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

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

View File

@@ -0,0 +1,19 @@
import React, { Component, PropTypes } from 'react'
import ScrollView from '../ScrollView'
export default class ListView extends Component {
static propTypes = {
children: PropTypes.any,
style: ScrollView.propTypes.style
};
static defaultProps = {
style: {}
};
render() {
return (
<ScrollView {...this.props} />
)
}
}

View File

@@ -0,0 +1,156 @@
/**
* Copyright 2015-present, Nicolas Gallagher
* Copyright 2004-present, Facebook Inc.
* All Rights Reserved.
*
* @flow
*/
import invariant from 'invariant'
import Platform from '../../apis/Platform'
import React, { Component, PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
import View from '../View'
let _portalRef: any
// unique identifiers for modals
let lastUsedTag = 0
/**
* A container that renders all the modals on top of everything else in the application.
*/
export default class Portal extends Component {
static propTypes = {
onModalVisibilityChanged: PropTypes.func.isRequired
};
/**
* Create a new unique tag.
*/
static allocateTag(): string {
return `__modal_${++lastUsedTag}`
}
/**
* Render a new modal.
*/
static showModal(tag: string, component: any) {
if (!_portalRef) {
console.error('Calling showModal but no "Portal" has been rendered.')
return
}
_portalRef._showModal(tag, component)
}
/**
* Remove a modal from the collection of modals to be rendered.
*/
static closeModal(tag: string) {
if (!_portalRef) {
console.error('Calling closeModal but no "Portal" has been rendered.')
return
}
_portalRef._closeModal(tag)
}
/**
* Get an array of all the open modals, as identified by their tag string.
*/
static getOpenModals(): Array<string> {
if (!_portalRef) {
console.error('Calling getOpenModals but no "Portal" has been rendered.')
return []
}
return _portalRef._getOpenModals()
}
static notifyAccessibilityService() {
if (!_portalRef) {
console.error('Calling closeModal but no "Portal" has been rendered.')
return
}
_portalRef._notifyAccessibilityService()
}
constructor(props) {
super(props)
this.state = { modals: {} }
this._closeModal = this._closeModal.bind(this)
this._getOpenModals = this._getOpenModals.bind(this)
this._showModal = this._showModal.bind(this)
}
render() {
invariant(
_portalRef === this || _portalRef === undefined,
'More than one Portal instance detected. Never use <Portal> in your code.'
)
_portalRef = this
if (!this.state.modals) { return null }
const modals = []
for (const tag in this.state.modals) {
modals.push(this.state.modals[tag])
}
if (modals.length === 0) { return null }
return (
<View style={styles.root}>
{modals}
</View>
)
}
_closeModal(tag: string) {
if (!this.state.modals.hasOwnProperty(tag)) {
return
}
// We are about to close last modal, so Portal will disappear.
// Let's enable accessibility for application view.
if (this._getOpenModals().length === 1) {
this.props.onModalVisibilityChanged(false)
}
// This way state is chained through multiple calls to
// _showModal, _closeModal correctly.
this.setState((state) => {
const modals = state.modals
delete modals[tag]
return { modals }
})
}
_getOpenModals(): Array<string> {
return Object.keys(this.state.modals)
}
_notifyAccessibilityService() {
if (Platform.OS === 'web') {
// We need to send accessibility event in a new batch, as otherwise
// TextViews have no text set at the moment of populating event.
}
}
_showModal(tag: string, component: any) {
// We are about to open first modal, so Portal will appear.
// Let's disable accessibility for background view on Android.
if (this._getOpenModals().length === 0) {
this.props.onModalVisibilityChanged(true)
}
// This way state is chained through multiple calls to
// _showModal, _closeModal correctly.
this.setState((state) => {
const modals = state.modals
modals[tag] = component
return { modals }
})
}
}
const styles = StyleSheet.create({
root: {
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0
}
})

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