Compare commits

..

134 Commits

Author SHA1 Message Date
Nicolas Gallagher
640e41dc34 0.0.35 2016-07-11 19:03:33 -07:00
Andrew Palm
c609a6ff2b Fix TouchableWithoutFeedback propTypes (#164) 2016-07-11 19:02:39 -07:00
Nicolas Gallagher
294d94d869 0.0.34 2016-07-11 00:02:15 -07:00
Nicolas Gallagher
179d624917 [change] don't use invariant in StyleSheet validation 2016-07-11 00:01:29 -07:00
Nicolas Gallagher
61860b6d49 0.0.33 2016-07-10 22:20:03 -07:00
Nicolas Gallagher
597fcc65e8 [add] initial 'onLayout' support
Add initial support for 'onLayout' when components mount and update.

Ref #60
2016-07-10 22:15:51 -07:00
Nicolas Gallagher
5e1e0ec8e5 [fix] update Touchables 2016-07-10 22:15:24 -07:00
Nicolas Gallagher
0ac243038f remove Portal docs 2016-07-10 22:13:14 -07:00
Nicolas Gallagher
c9d68fe93e Resolve React@15.2.0 unknown props warnings 2016-07-10 18:32:02 -07:00
Nicolas Gallagher
77f72aa129 [change] StyleSheet: news APIs and refactor
This fixes several issues with 'StyleSheet' and simplifies the
implementation.

1. The generated style sheet could render after an apps existing style
sheets, potentially overwriting certain 'html' and 'body' styles. To fix
this, the style sheet is now rendered first in the document head.

2. 'StyleSheet' didn't make it easy to render app shells on the server.
The prerendered style sheet would contain classnames that didn't apply
to the client-generated style sheet (in part because the class names
were not generated as a hash of the declaration). When the client
initialized, server-rendered parts of the page could become unstyled. To
fix this 'StyleSheet' uses inline styles by default and a few predefined
CSS rules where inline styles are not possible.

3. Even with the strategy of mapping declarations to unique CSS rules,
very large apps can produce very large style sheets. For example,
twitter.com would produce a gzipped style sheet ~30 KB. Issues related
to this are also alleviated by using inline styles.

4. 'StyleSheet' didn't really work unless you rendered an app using
'AppRegistry'. To fix this, 'StyleSheet' now handles injection of the
DOM style sheet.

Using inline styles doesn't appear to have any serious performance
problems compared to using single classes (ref #110).

Fix #90
Fix #106
2016-07-10 18:31:12 -07:00
Nicolas Gallagher
216885406f [fix] TouchableHighlight inactive styles 2016-07-10 17:42:23 -07:00
Nicolas Gallagher
f15bf2664a fix View propTypes 2016-07-10 14:23:05 -07:00
Nicolas Gallagher
79998e0acc move 'normalizeNativeEvent' and 'injectResponderEventPlugin' 2016-07-09 11:17:05 -07:00
Nicolas Gallagher
44fc48f7a0 use 'normalizeValue' in 'processTransform' 2016-07-07 22:24:05 -07:00
Nicolas Gallagher
37f2d78f34 Minor tweaks 2016-07-07 22:22:37 -07:00
Nicolas Gallagher
1dc769bfb1 move propTypes and normalizeColor 2016-07-07 22:14:08 -07:00
Nicolas Gallagher
4b3cb41107 rename createNativeComponent to createReactDOMComponent 2016-07-07 21:21:45 -07:00
Nicolas Gallagher
ed2cbfd5d3 [fix] React Native styles -> React DOM styles
Add 'createReactStyleObject' to transform a React Native style object
into a React DOM-compatible style object. This is also needed to ensure
that 'setNativeProps' works as expected.
2016-07-06 19:49:55 -07:00
Nicolas Gallagher
8c4b5b68c3 Fix eslint error 2016-07-06 18:57:51 -07:00
Nicolas Gallagher
3564bbf840 0.0.32 2016-07-06 18:50:25 -07:00
Nicolas Gallagher
297b2e5afb [fix] support for Animated transform styles (part 2)
Only add 'px' to numeric translate values
2016-07-06 18:48:53 -07:00
Nicolas Gallagher
215697234e 0.0.31 2016-07-06 18:33:12 -07:00
Nicolas Gallagher
9efa7e94bd [fix] support for Animated transform styles
Animated uses 'setNativeProps' to update styles. This mutates the DOM
without using React. But the code path was not adding 'px' units to
transform values and browsers were ignoring the style.

Fix #129
2016-07-06 17:16:55 -07:00
Nicolas Gallagher
c44da41497 0.0.30 2016-07-06 15:30:40 -07:00
Nicolas Gallagher
331c92fb3a Use enzyme for all React component tests 2016-07-06 15:26:32 -07:00
Nicolas Gallagher
26758e905c [fix] TextInput alignment of small inner 'input'
Fix #139
2016-07-06 15:25:00 -07:00
Nicolas Gallagher
a15b15c55d Use babel-preset-react-native 2016-07-06 10:27:43 -07:00
Nicolas Gallagher
f0202dbe61 Remove use of decorator syntax 2016-07-06 10:11:03 -07:00
Nicolas Gallagher
d4d67dafc0 [fix] StyleSheet expansion of shortform properties
The previous implementation relied on a buggy sorting strategy. It could
result in shortform properties replacing the values set for longform
properties. This patch avoids expanding shorthand values to a longform
property if it declared in the original style object.

Fix #141
2016-07-05 19:18:11 -07:00
Nicolas Gallagher
579bdeb8a5 [fix] setNativeProps on TextInput 2016-07-05 18:52:44 -07:00
Nicolas Gallagher
7132a18440 Remove unused import 2016-07-05 18:48:49 -07:00
Nicolas Gallagher
18881b1edb [fix] support border styles on Image
Fix #128
2016-07-05 13:57:42 -07:00
Nicolas Gallagher
4d1e7d8c0b 0.0.29 2016-07-05 13:50:25 -07:00
Nicolas Gallagher
a7158aeb6f [change] remove Portal component
Portal was undocumented and has been removed from React Native.

Fix #149
2016-07-05 13:49:37 -07:00
Nicolas Gallagher
03d413bca4 0.0.28 2016-07-05 11:33:29 -07:00
IjzerenHein
aef5efbad3 [add] basic ListView component
Close #87
2016-07-05 11:33:02 -07:00
Nicolas Gallagher
8fb8645723 Use 'enzyme' for 'View' tests 2016-07-05 11:33:02 -07:00
Cesar Andreu
d69406b4b1 Add an API to wrap and initialize animated (#159) 2016-07-03 11:00:50 -07:00
Nicolas Gallagher
2c2a96a183 update rendering docs 2016-06-29 17:42:06 -07:00
Nicolas Gallagher
b4a3053b5b fix README install command 2016-06-29 17:00:50 -07:00
Nicolas Gallagher
24836afd6a 0.0.26 2016-06-28 16:38:31 -07:00
Nicolas Gallagher
c46f242f6b [add] ReactDOM server API to ReactNative API 2016-06-28 16:38:21 -07:00
Nicolas Gallagher
1940868065 [fix] TextInput support for Text styles
Fix #81
Fix #133
2016-06-28 15:55:27 -07:00
Nicolas Gallagher
65a9317756 [fix] TextInput placeholder layout and focus
Fix the layout of placeholder text and shift focus to the DOM input when
`TextInput` is clicked or pressed.

Thanks to @tuckerconnelly and @Dremora.

Fix #138
Fix #119
Close #137
2016-06-28 15:04:38 -07:00
Nicolas Gallagher
3da05c48b0 [fix] support 'onClick' prop in 'View' 2016-06-28 15:04:17 -07:00
Nicolas Gallagher
f33312a4dd [change] Use animatedjs/animated
Depend on 'animatedjs/animated' for the Animation implementation. This
patch also replaces 'es6-set' with a small shim to reduce impact on
production bundle size.

Fix #95
2016-06-23 15:10:43 -07:00
Nicolas Gallagher
4516c72296 Use enzyme for Image tests 2016-06-23 13:52:08 -07:00
Nicolas Gallagher
7f94c4bf06 Install enzyme
Fix #83
2016-06-23 13:50:06 -07:00
Nicolas Gallagher
37781171aa [add] more ReactNative exports 2016-06-23 10:16:45 -07:00
Nicolas Gallagher
22f45e350b [fix] React@15: remove inline-style fallback values
React 15 has no way to handle fallback CSS values (for example, vendor
prefixed 'display:flex' values) in inline styles. This patch drops all
fallback values for inline styles at the cost of regressing browser
support (those without standard flexbox support will not layout React
Native components correctly).

Fix #131
2016-06-22 16:13:48 -07:00
Nicolas Gallagher
af40f98f23 [fix] AppState event handler registration
Fix #151
2016-06-22 15:41:35 -07:00
Nicolas Gallagher
eca2f69593 Remove a React error from test report 2016-06-21 14:59:45 -07:00
Nicolas Gallagher
d03d89ac71 [fix] CoreComponent -> createNativeComponent
'CoreComponent' creates new component instances and clutters the React
component tree during debugging. This patch converts 'CoreComponent' to
a simple function that creates a native web element.

This patch also includes a fix for use of the 'flexShrink' style on
'View'.

Fix #140
2016-06-21 14:59:38 -07:00
Nicolas Gallagher
393a6ef835 [fix] don't use 'bind' in JSX props 2016-06-20 11:31:38 -07:00
Nicolas Gallagher
36e89d5275 [fix] installation on Windows
Fix #114
2016-06-20 11:22:42 -07:00
Nicolas Gallagher
d53d1e6e56 Add link to react-native-web-starter 2016-06-20 11:09:50 -07:00
Nicolas Gallagher
2cb68a45be [add] Platform.select 2016-06-18 16:43:22 -07:00
Nicolas Gallagher
b56b8e494a Update various packages (inc. babel and eslint) 2016-06-14 16:05:30 -07:00
Nicolas Gallagher
60ad0e9ec5 Further fixes to examples following react@15 update 2016-06-14 15:56:10 -07:00
Nicolas Gallagher
f2ea7c089c [change] separate the React and React Native APIs
Fix #136
2016-06-14 13:47:47 -07:00
Nicolas Gallagher
a3b59ed2b4 [fix] Touchable with React@15
Fix #123
2016-06-14 13:47:39 -07:00
Nicolas Gallagher
a378d3cce2 [change] update to React@15 2016-06-14 13:04:30 -07:00
Nicolas Gallagher
462f9793ea Fix code style issue 2016-06-13 15:05:03 -07:00
Monir Abu Hilal
ae38bb538c Do not treat lineHeight as a unitless numbers
Match the behavior of react-native for iOS and Android

The browser treats the 'line-height' CSS property as an 'em' value,
while react-native treats it as pixel unit (or device unit, which should
be 'px' for the web), this issue is causing the 'TextInput' component to
be sized incorrectly.

Close #142
2016-06-13 12:04:53 -07:00
Nicolas Gallagher
93d1488cc7 Fix README link to View 2016-06-13 11:59:24 -07:00
Nicolas Gallagher
a16e542bd8 [fix] don't replace 'className' value 2016-06-13 11:58:05 -07:00
Nicolas Gallagher
62cd335788 [fix] TouchableHighlight default underlay style 2016-06-13 11:57:02 -07:00
Nicolas Gallagher
288e14cd70 Remove MediaQueryWidget from examples 2016-06-13 11:56:15 -07:00
Nicolas Gallagher
71cfd23624 0.0.25 2016-04-29 12:49:13 -07:00
Nicolas Gallagher
77b8e4a1fc [fix] pin inline-style-prefix-all
Version 1.1.0 contains a breaking change
2016-04-29 12:48:40 -07:00
Nicolas Gallagher
9543a79c3f 0.0.24 2016-04-20 11:38:38 -07:00
Nicolas Gallagher
e3eea6e132 [fix] TouchableHighlight
The fix in 97c0a31ce6 was incomplete due
to state key not being renamed.
2016-04-20 11:37:15 -07:00
Nicolas Gallagher
4d3418a968 0.0.23 2016-04-19 17:11:26 -07:00
Nicolas Gallagher
ea9bc734f1 [fix] TouchableWithoutFeedback
Fix #127
2016-04-19 17:10:50 -07:00
Nicolas Gallagher
e03af435ac 0.0.22 2016-04-18 16:55:57 -07:00
Nicolas Gallagher
97c0a31ce6 [fix] TouchableHighlight default underlayColor 2016-04-18 16:46:09 -07:00
Nicolas Gallagher
25d11ded46 [fix] NetInfo event handlers 2016-04-18 16:38:09 -07:00
Nicolas Gallagher
6a73d77030 Fix build 2016-03-24 17:01:38 -07:00
Nicolas Gallagher
0b63ba4e89 0.0.21 2016-03-24 11:50:01 -07:00
Nicolas Gallagher
51109d0768 [fix] update inline-style-prefix-all
inline-style-prefix-all@1.0.4 doesn't depend on `Set` anymore
2016-03-24 11:49:22 -07:00
Nicolas Gallagher
ac04ecd69e Update Dimensions when window resizes 2016-03-24 11:44:02 -07:00
Nicolas Gallagher
1a670ba6a7 Fix UMD bundle
Include React and ReactDOM in the UMD bundle. The library occupies the
`React` global.

Fix #105
2016-03-22 18:39:24 -07:00
Nicolas Gallagher
7a16d5711c 0.0.20 2016-03-20 12:19:40 -07:00
Nicolas Gallagher
9dde70fff5 Update documentation 2016-03-20 12:19:29 -07:00
Nicolas Gallagher
203980ab66 [fix] fbjs version compatible with React Native
React Native 0.21 currently uses fbjs@0.6.x, and React Native 0.22 will
use fbjs@0.7.x.

Fix #103
2016-03-20 12:11:31 -07:00
Nicolas Gallagher
924dc36d4a [fix] refactor StyleSheet
**Problem**

StyleSheet's implementation was overly complex. It required
`flattenStyle` to use `expandStyle`, and couldn't support mapping React
Native style props to CSS properties without also exposing those CSS
properties in the API.

**Response**

- `flattenStyle` is concerned only with flattening style objects.

- `StyleSheetRegistry` is responsible for registering styles, mapping
  the React Native style prop to DOM props, and generating the CSS for
  the backing style element.

- `StyleSheetRegistry` uses a simpler approach to caching styles and
  generating style sheet strings. It also drops the unobfuscated class
  names from development mode, as the React Dev Tools can provide a
  better debugging experience (pending a fix to allow props/styles to be
  changed from the dev tools).

- `StyleSheet` will fall back to inline styles if it doesn't think a
  style sheet has been rendered into the document. The relationship is
  currently only implicit. This should be revisited.

- `StyleSheet` exports `renderToString` as part of the documented API.

- Fix processing of `transformMatrix` and add tests for
  `processTransform`.

- Fix `input[type=search]` rendering in Safari by using `display:none`
  on its pseudo-elements.

- Add support for `textDecorationLine` and `textAlignVertical`.

- Note the `View` hack to conditionally apply the `flex-shrink:0` reset
  from css-layout. This is required because React Native's approach to
  resolving `style` is to give precendence to long-hand styles
  (e.g., `flexShrink`) over short-hand styles (e.g., `flex`). This means
  the `View` reset overrides any `flex:1` declaration. To get around
  this, `flexShrink` is only set in `View` if `flex` is not set.
2016-03-20 12:09:04 -07:00
Nicolas Gallagher
9b2421cdfa [fix] Server-side rendering
`AppRegistry.prerenderApplication` now returns a style element for use
in app shells.

Guard use of `window` in APIs and Event plugin.

Fix #107
Fix #108
2016-03-20 11:43:13 -07:00
Nicolas Gallagher
36ea662402 0.0.19 2016-03-16 10:19:17 -07:00
Nicolas Gallagher
69962ae815 [fix] StyleSheet: add hairlineWidth
Fix #99
2016-03-16 10:03:37 -07:00
Nicolas Gallagher
62d1a0f83d Add Game2048 example 2016-03-16 00:55:04 -07:00
Nicolas Gallagher
910286303a Add TicTacToe example 2016-03-16 00:48:41 -07:00
Nicolas Gallagher
706fa887e6 [fix] remove invariant error from Portal 2016-03-16 00:48:41 -07:00
Nicolas Gallagher
c589d79035 Reorganize karma and webpack configs 2016-03-16 00:48:37 -07:00
Nicolas Gallagher
83e4c68461 [fix] TouchableHighlight inactive state
TouchableHighlight didn't preserve its original 'backgroundColor' after
pressOut. This was caused by the inactive background style (transparent)
being applied as an inline style, and so not merging with the original
prop style. The patch sets inactive 'backgroundColor' to 'null' so as to
remove the inline style and render the backgroundColor from props.

Fix #98
2016-03-16 00:36:06 -07:00
Nicolas Gallagher
54597edbaf 0.0.18 2016-03-15 17:27:42 -07:00
Nicolas Gallagher
fc31287566 [fix] remove -webkit-tap-highlight-color 2016-03-15 17:26:29 -07:00
Nicolas Gallagher
21cc8f47ba Update documentation 2016-03-15 17:14:44 -07:00
Nicolas Gallagher
bf7beb4102 Update dependencies 2016-03-15 14:25:27 -07:00
Nicolas Gallagher
127d103c0a Fix lint issues 2016-03-15 14:19:46 -07:00
Nicolas Gallagher
ae6132af56 Update library exports 2016-03-15 14:19:29 -07:00
Nicolas Gallagher
3c4d7655db [fix] Touchable: adapt RN touchables for Web 2016-03-15 14:18:08 -07:00
Nicolas Gallagher
190966f411 [fix] ScrollView: based on ScrollResponder 2016-03-15 14:07:45 -07:00
Nicolas Gallagher
8d5ecb84d5 [add] ScrollResponder: adjust to work on Web 2016-03-15 13:49:03 -07:00
Nicolas Gallagher
b4a9177ce3 [add] dismissKeyboard module 2016-03-15 13:44:50 -07:00
Nicolas Gallagher
ad4a6c5be7 [fix] View: guard against browser flexbox bugs
There are various flexbox bugs that can cause views to grow beyond the
vertical or horizontal bounds of their container. The width and height
styles avoid most of them manifesting.
2016-03-15 13:43:25 -07:00
Nicolas Gallagher
5f795dfc6c Clean up View's event normalization code 2016-03-15 13:43:11 -07:00
Nicolas Gallagher
949cb75894 [add] TextInput: 'clear' method on instance 2016-03-15 13:38:54 -07:00
Nicolas Gallagher
2e1914080f [add] TextInputState 2016-03-15 13:38:28 -07:00
Nicolas Gallagher
49e9e0ab5b [add] StyleSheet: new prop types 2016-03-15 13:32:52 -07:00
Nicolas Gallagher
ee4c544957 [fix] StyleSheet: support for 'transform' and 'transformMatrix'
Fix #46
2016-03-15 13:32:02 -07:00
Nicolas Gallagher
56549cf794 [add] NativeMethodsMixin: 'measureInWindow' support 2016-03-15 13:24:36 -07:00
Nicolas Gallagher
e6811b2134 [fix] ResponderEventPlugin: touch or mouse dependencies
**Problem**

Browsers tend to fire both touch and mouse events when touch is
supported. This causes touchables to fire responder callbacks twice.
For example, 'TouchableOpacity' will animate the opacity twice in
response to a touch.

**Response**

If a browser supports touch, don't include the mouse events as
dependencies of the responder events. This is not ideal, as some devices
support both touch and mouse interactions. This will need to be
revisited.
2016-03-15 13:19:43 -07:00
Nicolas Gallagher
d8b7dcc60f Add 'locationX/Y' to recorded Responder events 2016-03-15 13:19:24 -07:00
Nicolas Gallagher
62a08f09ab [fix] ResponderEventPlugin: add missing 'responderRelease' dependencies 2016-03-15 13:17:25 -07:00
Nicolas Gallagher
3e7cd1a001 [add] UIManager: 'blur', 'focus' and 'measureInWindow' 2016-03-15 13:14:06 -07:00
Nicolas Gallagher
8441755d61 [fix] 'window' value in 'Dimension' 2016-03-15 11:35:36 -07:00
Nicolas Gallagher
ba9fa2a7a0 [add] upstream 'merge' module 2016-03-15 11:34:09 -07:00
Nicolas Gallagher
e26edfb9ea Move NativeMethodsDecorator 2016-03-14 23:25:58 -07:00
Nicolas Gallagher
9a8a9ad209 Use 'module.exports' over 'export default'
The use of CommonJS require in RN modules makes it simpler to use
CommonJS exports everywhere.
2016-03-14 23:21:12 -07:00
Nicolas Gallagher
efccbe41bb 0.0.17 2016-03-12 08:00:02 -08:00
IjzerenHein
f6f8d30aba [fix] PanResponder improvements + mouse support
- Adds `locationX` and `locationY` to touch events
- Adds `timestamp` to the `touches` and `touchesChanged` data
- Add mouse event support

Close #94
2016-03-12 07:53:24 -08:00
Nicolas Gallagher
6d7d98c149 [add] support directly requiring image assets
Thanks to IjzerenHein <hrutjes@gmail.com>. See #84
2016-03-08 18:03:45 -08:00
IjzerenHein
77d201988d Add .editorconfig 2016-03-08 09:40:28 -08:00
Nicolas Gallagher
b3d7332ddd Update 'known issues' guide 2016-03-08 09:33:56 -08:00
IjzerenHein
651d519500 [add] NativeModules shim
The `NativeModules` namespace is import by some libraries, which will
throw an error if missing.

Close #86
2016-03-08 09:32:14 -08:00
Hein Rutjes
06d8614519 Merge pull request #1 from necolas/master
Update from original master
2016-03-08 10:57:42 +01:00
Nicolas Gallagher
3eced7e842 Update documentation 2016-03-08 00:52:13 -08:00
rofrischmann
e627e0cd77 Use inline-style-prefix-all for vendor prefixes
Fix #74
Close #82
2016-03-08 00:31:43 -08:00
Nicolas Gallagher
7ab33727c4 Minor test clean up 2016-03-08 00:21:47 -08:00
Nicolas Gallagher
7295a8fee8 Add tests for Image resizeMode 2016-03-08 00:21:35 -08:00
IjzerenHein
9a09456532 [add] Image: support resizeMode style 2016-03-08 00:19:56 -08:00
Nicolas Gallagher
4cd38552cd Fix the 'test:watch' npm-script 2016-03-07 21:18:30 -08:00
Neil Kistner
b0f35f6c66 Add syntax highlighting to docs where missing 2016-03-07 20:43:56 -08:00
Nicolas Gallagher
2dff45b561 Add link to Game2048 example 2016-03-07 20:43:22 -08:00
142 changed files with 5760 additions and 4748 deletions

View File

@@ -1,10 +1,5 @@
{
"presets": [
"es2015",
"stage-1",
"react"
],
"plugins": [
"transform-decorators-legacy"
"react-native"
]
}

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
# EditorConfig: http://editorconfig.org
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2

View File

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

View File

@@ -2,32 +2,41 @@
[![Build Status][travis-image]][travis-url]
[![npm version][npm-image]][npm-url]
![gzipped size](https://img.shields.io/badge/gzipped-~23k-blue.svg)
![gzipped size](https://img.shields.io/badge/gzipped-~41.0k-blue.svg)
[React Native][react-native-url] components and APIs for the Web.
Browser support: Chrome, Firefox, Safari >= 7, IE 10, Edge.
## Overview
"React Native for Web" is a project to bring React Native's building blocks and
touch handling to the Web.
React Native provides a foundational layer to support interoperable,
zero-configuration React component development. This is missing from React's
web ecosystem where OSS components rely on inline styles (usually without
vendor prefixes), or require build tool configuration. This project allows
components built upon React Native to be run on the Web, and it manages all
component styling out-of-the-box.
For example, the [`View`](docs/components/View.md) component makes it easy to build
cross-browser 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 into "Atomic CSS".
## Quick start
To install in your app:
```
npm install --save react@0.14 react-dom@0.14 react-native-web
npm install --save react react-native-web
```
Read the [Client and Server rendering](docs/guides/rendering.md) guide.
## 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.
You can also bootstrap a standard React Native project structure for web by
using [react-native-web-starter](https://github.com/grabcode/react-native-web-starter).
## Examples
@@ -35,11 +44,13 @@ Demos:
* [React Native for Web: Playground](http://codepen.io/necolas/pen/PZzwBR).
* [TicTacToe](http://codepen.io/necolas/full/eJaLZd/)
* [2048](http://codepen.io/necolas/full/wMVvxj/)
Example:
Sample:
```js
import React, { AppRegistry, Image, StyleSheet, Text, View } from 'react-native'
import React from 'react'
import { AppRegistry, Image, StyleSheet, Text, View } from 'react-native'
// Components
const Card = ({ children }) => <View style={styles.card}>{children}</View>
@@ -52,10 +63,6 @@ const App = () => (
</Card>
)
// App registration and rendering
AppRegistry.registerComponent('MyApp', () => App)
AppRegistry.runApplication('MyApp', { rootTag: document.getElementById('react-root') })
// Styles
const styles = StyleSheet.create({
card: {
@@ -72,6 +79,10 @@ const styles = StyleSheet.create({
width: 40
}
})
// App registration and rendering
AppRegistry.registerComponent('MyApp', () => App)
AppRegistry.runApplication('MyApp', { rootTag: document.getElementById('react-root') })
```
## Documentation
@@ -91,12 +102,11 @@ Exported modules:
* [`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)
* [`TouchableHighlight`](http://facebook.github.io/react-native/releases/0.22/docs/touchablehighlight.html) (mirrors React Native)
* [`TouchableOpacity`](http://facebook.github.io/react-native/releases/0.22/docs/touchableopacity.html) (mirrors React Native)
* [`TouchableWithoutFeedback`](docs/components/TouchableWithoutFeedback.md)
* [`View`](docs/components/View.md)
* APIs

View File

@@ -1,11 +0,0 @@
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

@@ -16,8 +16,11 @@ into `runApplication`. These should always be used as a pair.
(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.
rendering. Return object is of type `{ html: string; style: string;
styleElement: ReactComponent }`. `html` is the prerendered HTML, `style` is the
prerendered style sheet, and `styleElement` is a React Component. It's
recommended that you use `styleElement` to render the style sheet in an app
shell.
static **registerConfig**(config: Array<AppConfig>)
@@ -51,7 +54,7 @@ into `runApplication`
## Example
```
```js
AppRegistry.registerComponent('MyApp', () => AppComponent)
AppRegistry.runApplication('MyApp', {
initialProps: {},

View File

@@ -37,7 +37,6 @@ class Example extends React.Component {
constructor(props) {
super(props)
this.state = { currentAppState: AppState.currentState }
this._handleAppStateChange = this._handleAppStateChange.bind(this)
}
componentDidMount() {
@@ -48,7 +47,7 @@ class Example extends React.Component {
AppState.removeEventListener('change', this._handleAppStateChange);
}
_handleAppStateChange(currentAppState) {
_handleAppStateChange = (currentAppState) => {
this.setState({ currentAppState });
}

View File

@@ -44,7 +44,7 @@ internet connectivity.
Fetching the connection type:
```
```js
NetInfo.fetch().then((connectionType) => {
console.log('Connection type:', connectionType);
});

View File

@@ -10,19 +10,36 @@ specific.
`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
import { Platform } from 'react-native';
const styles = StyleSheet.create({
height: (Platform.OS === 'web') ? 200 : 100,
});
if (Platform.userAgent.includes('Android')) {
console.log('Running on Android!');
}
```
## Methods
**select**: any
`Platform.select` takes an object containing `Platform.OS` as keys and returns
the value for the platform you are currently running on.
```js
import { Platform } from 'react-native';
const containerStyles = {
flex: 1,
...Platform.select({
android: {
backgroundColor: 'blue'
},
ios: {
backgroundColor: 'red'
},
web: {
backgroundColor: 'green'
}
})
});
```

View File

@@ -11,9 +11,56 @@ outside of the render loop and are applied as inline styles. Read more about to
Each key of the object passed to `create` must define a style object.
**flatten**: function
Flattens an array of styles into a single style object.
**render**: function
Returns a React `<style>` element for use in server-side rendering.
## Properties
**absoluteFill**: number
A very common pattern is to create overlays with position absolute and zero positioning,
so `absoluteFill` can be used for convenience and to reduce duplication of these repeated
styles.
```js
<View style={StyleSheet.absoluteFill} />
```
**absoluteFillObject**: object
Sometimes you may want `absoluteFill` but with a couple tweaks - `absoluteFillObject` can be
used to create a customized entry in a `StyleSheet`, e.g.:
```js
const styles = StyleSheet.create({
wrapper: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'transparent',
top: 10
}
})
```
**hairlineWidth**: number
## Example
```js
<View style={styles.container}>
<Text
children={'Title text'}
style={[
styles.title,
this.props.isActive && styles.activeTitle
]}
/>
</View>
const styles = StyleSheet.create({
container: {
borderRadius: 4,
@@ -29,29 +76,3 @@ const styles = StyleSheet.create({
}
})
```
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

@@ -23,7 +23,8 @@ 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'
import React, { Component } from 'react'
import { ActivityIndicator, StyleSheet, View } from 'react-native'
class ToggleAnimatingActivityIndicator extends Component {
constructor(props) {

View File

@@ -31,7 +31,8 @@ Invoked on load error with `{nativeEvent: {error}}`.
**onLayout**: function
TODO
Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width,
height } } }`, where `x` and `y` are the offsets from the parent node.
**onLoad**: function
@@ -57,26 +58,29 @@ could be an http address or a base64 encoded image.
**style**: style
+ ...[View#style](View.md)
Defaults:
```js
{
alignSelf: 'flex-start',
backgroundColor: 'transparent'
}
```
+ ...[View#style](./View.md)
+ `resizeMode`
**testID**: string
Used to locate a view in end-to-end tests.
## Properties
static **resizeMode**: Object
Example usage:
```
<Image resizeMode={Image.resizeMode.contain} />
```
## Examples
```js
import placeholderAvatar from './placeholderAvatar.png'
import React, { Component, Image, PropTypes, StyleSheet } from 'react-native'
import React, { Component } from 'react'
import { Image, PropTypes, StyleSheet } from 'react-native'
export default class ImageExample extends Component {
constructor(props, context) {

View File

@@ -4,6 +4,8 @@ TODO
## Props
[...ScrollView props](./ScrollView.md)
**children**: any
Content to display over the image.
@@ -15,7 +17,8 @@ Content to display over the image.
## Examples
```js
import React, { Component, ListView, PropTypes } from 'react-native'
import React, { Component, PropTypes } from 'react'
import { ListView } from 'react-native'
export default class ListViewExample extends Component {
static propTypes = {}

View File

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

View File

@@ -1,13 +1,13 @@
# 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.
A scrollable `View` that provides itegration with the touch-locking "responder"
system. `ScrollView`'s must have a bounded height: either set the height of the
view directly (discouraged) or make sure all parent views have bounded height
(e.g., transfer `{ flex: 1}` down the view stack).
## Props
**children**: any
Child content.
[...View props](./View.md)
**contentContainerStyle**: style
@@ -19,11 +19,32 @@ all of the child views.
When true, the scroll view's children are arranged horizontally in a row
instead of vertically in a column.
**keyboardDismissMode**: oneOf('none', 'on-drag') = 'none'
Determines whether the keyboard gets dismissed in response to a scroll drag.
* `none` (the default), drags do not dismiss the keyboard.
* `on-drag`, the keyboard is dismissed when a drag begins.
* `interactive` (not supported on web; same as `none`)
**onContentSizeChange**: function
Called when scrollable content view of the `ScrollView` changes. It's
implemented using the `onLayout` handler attached to the content container
which this `ScrollView` renders.
**onScroll**: function
Fires at most once per frame during scrolling. The frequency of the events can
be contolled using the `scrollEventThrottle` prop.
**refreshControl**: element
TODO
A [RefreshControl](../RefreshControl) component, used to provide
pull-to-refresh functionality for the `ScrollView`.
**scrollEnabled**: bool = true
When false, the content does not scroll.
@@ -36,21 +57,39 @@ 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
## Instance methods
+ ...[View#style](View.md)
**getInnerViewNode()**: any
Returns a reference to the underlying content container DOM node within the `ScrollView`.
**getScrollableNode()**: any
Returns a reference to the underlying scrollable DOM node.
**getScrollResponder()**: Component
Returns a reference to the underlying scroll responder, which supports
operations like `scrollTo`. All `ScrollView`-like components should implement
this method so that they can be composed while providing access to the
underlying scroll responder's methods.
**scrollTo(options: { x: number = 0; y: number = 0; animated: boolean = true })**
Scrolls to a given `x`, `y` offset (animation is not currently supported).
## Examples
```js
import React, { Component, ScrollView, StyleSheet } from 'react-native'
import React, { Component } from 'react'
import { 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 }))
items: Array.from(new Array(20)).map((_, i) => ({ id: i }))
}
}

View File

@@ -45,6 +45,11 @@ Child content.
Truncates the text with an ellipsis after this many lines. Currently only supports `1`.
**onLayout**: function
Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width,
height } } }`, where `x` and `y` are the offsets from the parent node.
**onPress**: function
This function is called on press.
@@ -60,7 +65,8 @@ This function is called on press.
+ `letterSpacing`
+ `lineHeight`
+ `textAlign`
+ `textDecoration`
+ `textAlignVertical`
+ `textDecorationLine`
+ `textShadow`
+ `textTransform`
+ `whiteSpace`
@@ -74,7 +80,8 @@ Used to locate this view in end-to-end tests.
## Examples
```js
import React, { Component, PropTypes, StyleSheet, Text } from 'react-native'
import React, { Component, PropTypes } from 'react'
import { StyleSheet, Text } from 'react-native'
export default class PrettyText extends Component {
static propTypes = {

View File

@@ -14,16 +14,11 @@ Unsupported React Native props:
`enablesReturnKeyAutomatically` (ios),
`returnKeyType` (ios),
`selectionState` (ios),
`textAlign` (android),
`textAlignVertical` (android),
`underlineColorAndroid` (android)
## Props
(web) **accessibilityLabel**: string
Defines the text label available to assistive technologies upon interaction
with the element. (This is implemented using `aria-label`.)
[...View props](./View.md)
(web) **autoComplete**: bool = false
@@ -92,10 +87,6 @@ as an argument to the callback handler.
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
@@ -132,7 +123,7 @@ If `true`, all text will automatically be selected on focus.
**style**: style
+ ...[Text#style](Text.md)
+ ...[Text#style](./Text.md)
+ `outline`
**testID**: string
@@ -147,10 +138,25 @@ 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}`.
## Instance methods
**blur()**
Blur the underlying DOM input.
**clear()**
Clear the text from the underlying DOM input.
**focus()**
Focus the underlying DOM input.
## Examples
```js
import React, { Component, StyleSheet, TextInput } from 'react-native'
import React, { Component } from 'react'
import { StyleSheet, TextInput } from 'react-native'
export default class TextInputExample extends Component {
constructor(props, context) {

View File

@@ -1,100 +0,0 @@
# 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

@@ -0,0 +1,73 @@
# TouchableWithoutFeedback
Do not use unless you have a very good reason. All the elements that respond to
press should have a visual feedback when touched. This is one of the primary
reason a "web" app doesn't feel "native".
**NOTE: `TouchableWithoutFeedback` supports only one child**. If you wish to have
several child components, wrap them in a View.
## Props
[...View props](./View.md)
**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
**accessible**: bool = true
When `false`, the view is hidden from screenreaders.
**children**: View
**delayLongPress**: number
Delay in ms, from `onPressIn`, before `onLongPress` is called.
**delayPressIn**: number
Delay in ms, from the start of the touch, before `onPressIn` is called.
**delayPressOut**: number
Delay in ms, from the release of the touch, before `onPressOut` is called.
**disabled**: bool
If true, disable all interactions for this component.
**hitSlop**: `{top: number, left: number, bottom: number, right: number}`
This defines how far your touch can start away from the button. This is added
to `pressRetentionOffset` when moving off of the button. **NOTE**: The touch
area never extends past the parent view bounds and the z-index of sibling views
always takes precedence if a touch hits two overlapping views.
**onLayout**: function
Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width,
height } } }`, where `x` and `y` are the offsets from the parent node.
**onLongPress**: function
**onPress**: function
Called when the touch is released, but not if cancelled (e.g. by a scroll that steals the responder lock).
**onPressIn**: function
**onPressOut**: function
**pressRetentionOffset**: `{top: number, left: number, bottom: number, right: number}`
When the scroll view is disabled, this defines how far your touch may move off
of the button, before deactivating the button. Once deactivated, try moving it
back and you'll see that the button is once again reactivated! Move it back and
forth several times while the scroll view is disabled. Ensure you pass in a
constant to reduce memory allocations.

View File

@@ -4,21 +4,13 @@
style, layout with flexbox, and accessibility controls. It can be nested
inside another `View` and has 0-to-many children of any type.
Also, refer to React Native's documentation about the [Gesture Responder
System](http://facebook.github.io/react-native/releases/0.22/docs/gesture-responder-system.html).
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)
`hitSlop`,
`onMagicTap`
## Props
@@ -56,7 +48,31 @@ implemented using `aria-hidden`.)
**onLayout**: function
(TODO)
Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width,
height } } }`, where `x` and `y` are the offsets from the parent node.
**onMoveShouldSetResponder**: function
**onMoveShouldSetResponderCapture**: function
**onResponderGrant**: function
For most touch interactions, you'll simply want to wrap your component in
`TouchableHighlight` or `TouchableOpacity`.
**onResponderMove**: function
**onResponderReject**: function
**onResponderRelease**: function
**onResponderTerminate**: function
**onResponderTerminationRequest**: function
**onStartShouldSetResponder**: function
**onStartShouldSetResponderCapture**: function
**pointerEvents**: oneOf('auto', 'box-only', 'box-none', 'none') = 'auto'
@@ -136,6 +152,7 @@ from `style`.
+ `right`
+ `top`
+ `transform`
+ `transformMatrix`
+ `userSelect`
+ `visibility`
+ `width`
@@ -168,7 +185,8 @@ Used to locate this view in end-to-end tests.
## Examples
```js
import React, { Component, PropTypes, StyleSheet, View } from 'react-native'
import React, { Component, PropTypes } from 'react'
import { StyleSheet, View } from 'react-native'
export default class ViewExample extends Component {
render() {

View File

@@ -7,22 +7,17 @@ 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.
cases it will be necessary to use a Web counterpart or to guard the use of a
module with `Platform.OS` (e.g. `NativeModules`)
## Missing component props
There are properties that do not work across all platforms. All web-specific
props are annotated with `(web)` in the documentaiton.
props are annotated with `(web)` in the documentation.
## 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)
`ActivityIndicator` and a horizontal `ProgressBar` for Web use, in anticipation
of the convergence between the iOS and Android components in React Native.

View File

@@ -1,8 +1,5 @@
# 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`.
@@ -10,6 +7,7 @@ Use a module loader that supports package aliases (e.g., webpack), and alias
// webpack.config.js
module.exports = {
// ...
resolve: {
alias: {
'react-native': 'react-native-web'
@@ -18,17 +16,31 @@ module.exports = {
}
```
Web-specific implementations can use the `*.web.js` naming pattern, which
webpack will resolve.
## Image assets
In order to require image assets (e.g. `require('assets/myimage.png')`), add
the `url-loader` to the webpack config:
```js
// webpack.config.js
module.exports = {
// ...
module: {
loaders: {
test: /\.(gif|jpe?g|png|svg)$/,
loader: 'url-loader',
query: { name: '[name].[hash:16].[ext]' }
}
}
```
## Web-specific code
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
})
import { AppRegistry, Platform } from 'react-native'
AppRegistry.registerComponent('MyApp', () => MyApp)
@@ -38,3 +50,24 @@ if (Platform.OS === 'web') {
});
}
```
More substantial Web-specific implementation code should be written in files
with the extension `.web.js`, which webpack will automatically resolve.
## Optimizations
Production builds can benefit from dead-code elimination by defining the
following variables:
```js
// webpack.config.js
module.exports = {
// ...
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
}
}
```

View File

@@ -18,56 +18,41 @@ module.exports = {
## Client-side rendering
Rendering without using the `AppRegistry`:
```js
// client.js
import React from 'react'
import ReactNative from 'react-native'
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') })
// component that renders the app
const AppHeaderContainer = (props) => { /* ... */ }
// DOM render
React.render(<div />, document.getElementById('sidebar-app'))
ReactNative.render(<AppHeaderContainer />, document.getElementById('react-app-header'))
// Server render
React.renderToString(<div />)
ReactNative.renderToString(<AppHeaderContainer />)
ReactNative.renderToStaticMarkup(<AppHeaderContainer />)
```
## 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.
Rendering using the `AppRegistry`:
```js
// server.js
import React from 'react'
import ReactNative, { AppRegistry } from 'react-native'
import React, { AppRegistry } from 'react-native'
import MyApp from './MyApp'
// component that renders the app
const AppContainer = (props) => { /* ... */ }
// register the app
AppRegistry.registerApp('MyApp', () => MyApp)
AppRegistry.registerComponent('App', () => AppContainer)
// DOM render
AppRegistry.runApplication('App', {
initialProps: {},
rootTag: document.getElementById('react-app')
})
// 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} />)
const { html, styleElement } = AppRegistry.prerenderApplication('App', { initialProps })
```

View File

@@ -174,16 +174,16 @@ const styles = StyleSheet.create({
CSS output:
```css
._s1 { color: gray; }
._s2 { font-size: 2rem; }
._s3 { font-size: 1.25rem; }
.__style1 { color: gray; }
.__style2 { font-size: 2rem; }
.__style3 { font-size: 1.25rem; }
```
Rendered HTML:
```html
<span className="_s1 _s2">Heading</span>
<span className="_s1 _s3">Text</span>
<span className="__style1 __style2">Heading</span>
<span className="__style1 __style3">Text</span>
```
### Reset
@@ -200,6 +200,7 @@ html {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color:rgba(0,0,0,0)
}
body {
@@ -214,12 +215,6 @@ input::-moz-focus-inner {
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
ol,
ul,
li {
list-style:none
display: none;
}
```

325
examples/2048/Game2048.js Normal file
View File

@@ -0,0 +1,325 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* 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 NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK 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.
*
* @providesModule Game2048
* @flow
*/
'use strict';
var React = require('react');
var ReactNative = require('react-native');
var {
Animated,
AppRegistry,
StyleSheet,
Text,
TouchableBounce,
View,
} = ReactNative;
var GameBoard = require('./GameBoard');
var BOARD_PADDING = 3;
var CELL_MARGIN = 4;
var CELL_SIZE = 60;
class Cell extends React.Component {
render() {
return <View style={styles.cell} />;
}
}
class Board extends React.Component {
render() {
return (
<View style={styles.board}>
<View style={styles.row}><Cell/><Cell/><Cell/><Cell/></View>
<View style={styles.row}><Cell/><Cell/><Cell/><Cell/></View>
<View style={styles.row}><Cell/><Cell/><Cell/><Cell/></View>
<View style={styles.row}><Cell/><Cell/><Cell/><Cell/></View>
{this.props.children}
</View>
);
}
}
class Tile extends React.Component {
state: any;
static _getPosition(index): number {
return BOARD_PADDING + (index * (CELL_SIZE + CELL_MARGIN * 2) + CELL_MARGIN);
}
constructor(props: {}) {
super(props);
var tile = this.props.tile;
this.state = {
opacity: new Animated.Value(0),
top: new Animated.Value(Tile._getPosition(tile.toRow())),
left: new Animated.Value(Tile._getPosition(tile.toColumn())),
};
}
calculateOffset(): {top: number; left: number; opacity: number} {
var tile = this.props.tile;
var offset = {
top: this.state.top,
left: this.state.left,
opacity: this.state.opacity,
};
if (tile.isNew()) {
Animated.timing(this.state.opacity, {
duration: 100,
toValue: 1,
}).start();
} else {
Animated.parallel([
Animated.timing(offset.top, {
duration: 100,
toValue: Tile._getPosition(tile.toRow()),
}),
Animated.timing(offset.left, {
duration: 100,
toValue: Tile._getPosition(tile.toColumn()),
}),
]).start();
}
return offset;
}
render() {
var tile = this.props.tile;
var tileStyles = [
styles.tile,
styles['tile' + tile.value],
this.calculateOffset(),
];
var textStyles = [
styles.value,
tile.value > 4 && styles.whiteText,
tile.value > 100 && styles.threeDigits,
tile.value > 1000 && styles.fourDigits,
];
return (
<Animated.View style={tileStyles}>
<Text style={textStyles}>{tile.value}</Text>
</Animated.View>
);
}
}
class GameEndOverlay extends React.Component {
render() {
var board = this.props.board;
if (!board.hasWon() && !board.hasLost()) {
return <View/>;
}
var message = board.hasWon() ?
'Good Job!' : 'Game Over';
return (
<View style={styles.overlay}>
<Text style={styles.overlayMessage}>{message}</Text>
<TouchableBounce onPress={this.props.onRestart} style={styles.tryAgain}>
<Text style={styles.tryAgainText}>Try Again?</Text>
</TouchableBounce>
</View>
);
}
}
class Game2048 extends React.Component {
startX: number;
startY: number;
state: any;
constructor(props: {}) {
super(props);
this.state = {
board: new GameBoard(),
};
this.startX = 0;
this.startY = 0;
}
restartGame() {
this.setState({board: new GameBoard()});
}
handleTouchStart(event: Object) {
if (this.state.board.hasWon()) {
return;
}
this.startX = event.nativeEvent.pageX;
this.startY = event.nativeEvent.pageY;
}
handleTouchEnd(event: Object) {
if (this.state.board.hasWon()) {
return;
}
var deltaX = event.nativeEvent.pageX - this.startX;
var deltaY = event.nativeEvent.pageY - this.startY;
var direction = -1;
if (Math.abs(deltaX) > 3 * Math.abs(deltaY) && Math.abs(deltaX) > 30) {
direction = deltaX > 0 ? 2 : 0;
} else if (Math.abs(deltaY) > 3 * Math.abs(deltaX) && Math.abs(deltaY) > 30) {
direction = deltaY > 0 ? 3 : 1;
}
if (direction !== -1) {
this.setState({board: this.state.board.move(direction)});
}
}
render() {
var tiles = this.state.board.tiles
.filter((tile) => tile.value)
.map((tile) => <Tile ref={tile.id} key={tile.id} tile={tile} />);
return (
<View
style={styles.container}
onTouchStart={(event) => this.handleTouchStart(event)}
onTouchEnd={(event) => this.handleTouchEnd(event)}>
<Board>
{tiles}
</Board>
<GameEndOverlay board={this.state.board} onRestart={() => this.restartGame()} />
</View>
);
}
}
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
board: {
padding: BOARD_PADDING,
backgroundColor: '#bbaaaa',
borderRadius: 5,
},
overlay: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(221, 221, 221, 0.5)',
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
},
overlayMessage: {
fontSize: 40,
marginBottom: 20,
},
tryAgain: {
backgroundColor: '#887761',
padding: 20,
borderRadius: 5,
},
tryAgainText: {
color: '#ffffff',
fontSize: 20,
fontWeight: '500',
},
cell: {
width: CELL_SIZE,
height: CELL_SIZE,
borderRadius: 5,
backgroundColor: '#ddccbb',
margin: CELL_MARGIN,
},
row: {
flexDirection: 'row',
},
tile: {
position: 'absolute',
width: CELL_SIZE,
height: CELL_SIZE,
backgroundColor: '#ddccbb',
borderRadius: 5,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
value: {
fontSize: 24,
color: '#776666',
fontFamily: 'Verdana',
fontWeight: '500',
},
tile2: {
backgroundColor: '#eeeeee',
},
tile4: {
backgroundColor: '#eeeecc',
},
tile8: {
backgroundColor: '#ffbb87',
},
tile16: {
backgroundColor: '#ff9966',
},
tile32: {
backgroundColor: '#ff7755',
},
tile64: {
backgroundColor: '#ff5533',
},
tile128: {
backgroundColor: '#eecc77',
},
tile256: {
backgroundColor: '#eecc66',
},
tile512: {
backgroundColor: '#eecc55',
},
tile1024: {
backgroundColor: '#eecc33',
},
tile2048: {
backgroundColor: '#eecc22',
},
whiteText: {
color: '#ffffff',
},
threeDigits: {
fontSize: 20,
},
fourDigits: {
fontSize: 18,
},
});
AppRegistry.registerComponent('Game2048', () => Game2048);
module.exports = Game2048;

201
examples/2048/GameBoard.js Normal file
View File

@@ -0,0 +1,201 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* 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 NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK 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.
*
* @providesModule GameBoard
* @flow
*/
'use strict';
// NB: Taken straight from: https://github.com/IvanVergiliev/2048-react/blob/master/src/board.js
// with no modification except to format it for CommonJS and fix lint/flow errors
var rotateLeft = function (matrix) {
var rows = matrix.length;
var columns = matrix[0].length;
var res = [];
for (var row = 0; row < rows; ++row) {
res.push([]);
for (var column = 0; column < columns; ++column) {
res[row][column] = matrix[column][columns - row - 1];
}
}
return res;
};
var Tile = function (value?: number, row?: number, column?: number) {
this.value = value || 0;
this.row = row || -1;
this.column = column || -1;
this.oldRow = -1;
this.oldColumn = -1;
this.markForDeletion = false;
this.mergedInto = null;
this.id = Tile.id++;
};
Tile.id = 0;
Tile.prototype.moveTo = function (row, column) {
this.oldRow = this.row;
this.oldColumn = this.column;
this.row = row;
this.column = column;
};
Tile.prototype.isNew = function () {
return this.oldRow === -1 && !this.mergedInto;
};
Tile.prototype.hasMoved = function () {
return (this.fromRow() !== -1 && (this.fromRow() !== this.toRow() || this.fromColumn() !== this.toColumn())) ||
this.mergedInto;
};
Tile.prototype.fromRow = function () {
return this.mergedInto ? this.row : this.oldRow;
};
Tile.prototype.fromColumn = function () {
return this.mergedInto ? this.column : this.oldColumn;
};
Tile.prototype.toRow = function () {
return this.mergedInto ? this.mergedInto.row : this.row;
};
Tile.prototype.toColumn = function () {
return this.mergedInto ? this.mergedInto.column : this.column;
};
var Board = function () {
this.tiles = [];
this.cells = [];
for (var i = 0; i < Board.size; ++i) {
this.cells[i] = [this.addTile(), this.addTile(), this.addTile(), this.addTile()];
}
this.addRandomTile();
this.setPositions();
this.won = false;
};
Board.prototype.addTile = function () {
var res = new Tile();
Tile.apply(res, arguments);
this.tiles.push(res);
return res;
};
Board.size = 4;
Board.prototype.moveLeft = function () {
var hasChanged = false;
for (var row = 0; row < Board.size; ++row) {
var currentRow = this.cells[row].filter(function (tile) { return tile.value !== 0; });
var resultRow = [];
for (var target = 0; target < Board.size; ++target) {
var targetTile = currentRow.length ? currentRow.shift() : this.addTile();
if (currentRow.length > 0 && currentRow[0].value === targetTile.value) {
var tile1 = targetTile;
targetTile = this.addTile(targetTile.value);
tile1.mergedInto = targetTile;
var tile2 = currentRow.shift();
tile2.mergedInto = targetTile;
targetTile.value += tile2.value;
}
resultRow[target] = targetTile;
this.won = this.won || (targetTile.value === 2048);
hasChanged = hasChanged || (targetTile.value !== this.cells[row][target].value);
}
this.cells[row] = resultRow;
}
return hasChanged;
};
Board.prototype.setPositions = function () {
this.cells.forEach(function (row, rowIndex) {
row.forEach(function (tile, columnIndex) {
tile.oldRow = tile.row;
tile.oldColumn = tile.column;
tile.row = rowIndex;
tile.column = columnIndex;
tile.markForDeletion = false;
});
});
};
Board.fourProbability = 0.1;
Board.prototype.addRandomTile = function () {
var emptyCells = [];
for (var r = 0; r < Board.size; ++r) {
for (var c = 0; c < Board.size; ++c) {
if (this.cells[r][c].value === 0) {
emptyCells.push({r: r, c: c});
}
}
}
var index = Math.floor(Math.random() * emptyCells.length);
var cell = emptyCells[index];
var newValue = Math.random() < Board.fourProbability ? 4 : 2;
this.cells[cell.r][cell.c] = this.addTile(newValue);
};
Board.prototype.move = function (direction) {
// 0 -> left, 1 -> up, 2 -> right, 3 -> down
this.clearOldTiles();
for (var i = 0; i < direction; ++i) {
this.cells = rotateLeft(this.cells);
}
var hasChanged = this.moveLeft();
for (var i = direction; i < 4; ++i) {
this.cells = rotateLeft(this.cells);
}
if (hasChanged) {
this.addRandomTile();
}
this.setPositions();
return this;
};
Board.prototype.clearOldTiles = function () {
this.tiles = this.tiles.filter(function (tile) { return tile.markForDeletion === false; });
this.tiles.forEach(function (tile) { tile.markForDeletion = true; });
};
Board.prototype.hasWon = function () {
return this.won;
};
Board.deltaX = [-1, 0, 1, 0];
Board.deltaY = [0, -1, 0, 1];
Board.prototype.hasLost = function () {
var canMove = false;
for (var row = 0; row < Board.size; ++row) {
for (var column = 0; column < Board.size; ++column) {
canMove = canMove || (this.cells[row][column].value === 0);
for (var dir = 0; dir < 4; ++dir) {
var newRow = row + Board.deltaX[dir];
var newColumn = column + Board.deltaY[dir];
if (newRow < 0 || newRow >= Board.size || newColumn < 0 || newColumn >= Board.size) {
continue;
}
canMove = canMove || (this.cells[row][column].value === this.cells[newRow][newColumn].value);
}
}
}
return !canMove;
};
module.exports = Board;

View File

@@ -0,0 +1,322 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* 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 NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK 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.
*
* @providesModule TicTacToeApp
* @flow
*/
'use strict';
var React = require('react');
var ReactNative = require('react-native');
var {
AppRegistry,
StyleSheet,
Text,
TouchableHighlight,
View,
} = ReactNative;
class Board {
grid: Array<Array<number>>;
turn: number;
constructor() {
var size = 3;
var grid = Array(size);
for (var i = 0; i < size; i++) {
var row = Array(size);
for (var j = 0; j < size; j++) {
row[j] = 0;
}
grid[i] = row;
}
this.grid = grid;
this.turn = 1;
}
mark(row: number, col: number, player: number): Board {
this.grid[row][col] = player;
return this;
}
hasMark(row: number, col: number): boolean {
return this.grid[row][col] !== 0;
}
winner(): ?number {
for (var i = 0; i < 3; i++) {
if (this.grid[i][0] !== 0 && this.grid[i][0] === this.grid[i][1] &&
this.grid[i][0] === this.grid[i][2]) {
return this.grid[i][0];
}
}
for (var i = 0; i < 3; i++) {
if (this.grid[0][i] !== 0 && this.grid[0][i] === this.grid[1][i] &&
this.grid[0][i] === this.grid[2][i]) {
return this.grid[0][i];
}
}
if (this.grid[0][0] !== 0 && this.grid[0][0] === this.grid[1][1] &&
this.grid[0][0] === this.grid[2][2]) {
return this.grid[0][0];
}
if (this.grid[0][2] !== 0 && this.grid[0][2] === this.grid[1][1] &&
this.grid[0][2] === this.grid[2][0]) {
return this.grid[0][2];
}
return null;
}
tie(): boolean {
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
if (this.grid[i][j] === 0) {
return false;
}
}
}
return this.winner() === null;
}
}
var Cell = React.createClass({
cellStyle() {
switch (this.props.player) {
case 1:
return styles.cellX;
case 2:
return styles.cellO;
default:
return null;
}
},
textStyle() {
switch (this.props.player) {
case 1:
return styles.cellTextX;
case 2:
return styles.cellTextO;
default:
return {};
}
},
textContents() {
switch (this.props.player) {
case 1:
return 'X';
case 2:
return 'O';
default:
return '';
}
},
render() {
return (
<TouchableHighlight
onPress={this.props.onPress}
underlayColor="transparent"
activeOpacity={0.5}>
<View style={[styles.cell, this.cellStyle()]}>
<Text style={[styles.cellText, this.textStyle()]}>
{this.textContents()}
</Text>
</View>
</TouchableHighlight>
);
}
});
var GameEndOverlay = React.createClass({
render() {
var board = this.props.board;
var tie = board.tie();
var winner = board.winner();
if (!winner && !tie) {
return <View />;
}
var message;
if (tie) {
message = 'It\'s a tie!';
} else {
message = (winner === 1 ? 'X' : 'O') + ' wins!';
}
return (
<View style={styles.overlay}>
<Text style={styles.overlayMessage}>{message}</Text>
<TouchableHighlight
onPress={this.props.onRestart}
underlayColor="transparent"
activeOpacity={0.5}>
<View style={styles.newGame}>
<Text style={styles.newGameText}>New Game</Text>
</View>
</TouchableHighlight>
</View>
);
}
});
var TicTacToeApp = React.createClass({
getInitialState() {
return { board: new Board(), player: 1 };
},
restartGame() {
this.setState(this.getInitialState());
},
nextPlayer(): number {
return this.state.player === 1 ? 2 : 1;
},
handleCellPress(row: number, col: number) {
if (this.state.board.hasMark(row, col)) {
return;
}
this.setState({
board: this.state.board.mark(row, col, this.state.player),
player: this.nextPlayer(),
});
},
render() {
var rows = this.state.board.grid.map((cells, row) =>
<View key={'row' + row} style={styles.row}>
{cells.map((player, col) =>
<Cell
key={'cell' + col}
player={player}
onPress={this.handleCellPress.bind(this, row, col)}
/>
)}
</View>
);
return (
<View style={styles.container}>
<Text style={styles.title}>EXTREME T3</Text>
<View style={styles.board}>
{rows}
</View>
<GameEndOverlay
board={this.state.board}
onRestart={this.restartGame}
/>
</View>
);
}
});
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'white'
},
title: {
fontFamily: 'Chalkduster',
fontSize: 39,
marginBottom: 20,
},
board: {
padding: 5,
backgroundColor: '#47525d',
borderRadius: 10,
},
row: {
flexDirection: 'row',
},
// CELL
cell: {
width: 80,
height: 80,
borderRadius: 5,
backgroundColor: '#7b8994',
margin: 5,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
cellX: {
backgroundColor: '#72d0eb',
},
cellO: {
backgroundColor: '#7ebd26',
},
// CELL TEXT
cellText: {
borderRadius: 5,
fontSize: 50,
fontFamily: 'AvenirNext-Bold',
},
cellTextX: {
color: '#19a9e5',
},
cellTextO: {
color: '#b9dc2f',
},
// GAME OVER
overlay: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(221, 221, 221, 0.5)',
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
},
overlayMessage: {
fontSize: 40,
marginBottom: 20,
marginLeft: 20,
marginRight: 20,
fontFamily: 'AvenirNext-DemiBold',
textAlign: 'center',
},
newGame: {
backgroundColor: '#887765',
padding: 20,
borderRadius: 5,
},
newGameText: {
color: 'white',
fontSize: 20,
fontFamily: 'AvenirNext-DemiBold',
},
});
AppRegistry.registerComponent('TicTacToeApp', () => TicTacToeApp);
module.exports = TicTacToeApp;

View File

@@ -1,27 +1,23 @@
import GridView from './GridView'
import Heading from './Heading'
import MediaQueryWidget from './MediaQueryWidget'
import React, { Image, StyleSheet, ScrollView, Text, TextInput, TouchableHighlight, View } from 'react-native'
import React from 'react'
import { 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)
constructor(props) {
super(props)
this.state = {
scrollEnabled: true
}
}
render() {
const { mediaQuery } = this.props
const finalRootStyles = [
rootStyles.common,
mediaQuery.small.matches && rootStyles.mqSmall,
mediaQuery.large.matches && rootStyles.mqLarge
rootStyles.common
]
return (
@@ -35,10 +31,9 @@ export default class App extends React.Component {
scroll views from which more complex components and apps can be
constructed.</Text>
<MediaQueryWidget mediaQuery={mediaQuery} />
<Heading size='large'>Image</Heading>
<Image
onLayout={(e) => { console.log(e.nativeEvent.layout) }}
accessibilityLabel='accessible image'
children={<Text>Inner content</Text>}
defaultSource={{
@@ -63,6 +58,7 @@ export default class App extends React.Component {
<Heading size='large'>Text</Heading>
<Text
onPress={(e) => { console.log('Text.onPress', e) }}
onLayout={(e) => { console.log(e.nativeEvent.layout) }}
testID={'Example.text'}
>
PRESS ME.
@@ -96,7 +92,10 @@ export default class App extends React.Component {
/>
<TextInput secureTextEntry />
<TextInput defaultValue='read only' editable={false} />
<TextInput keyboardType='email-address' placeholder='you@domain.com' placeholderTextColor='red' />
<TextInput
style={{ flex:1, height: 60, padding: 20, fontSize: 20, textAlign: 'center' }}
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 />

View File

@@ -1,4 +1,5 @@
import React, { Component, PropTypes, StyleSheet, View } from 'react-native'
import React, { Component, PropTypes } from 'react'
import { StyleSheet, View } from 'react-native'
export default class GridView extends Component {
static propTypes = {
@@ -12,31 +13,31 @@ export default class GridView extends Component {
}
static defaultProps = {
alley: '0',
gutter: '0'
alley: '0px',
gutter: '0px'
}
render() {
const { alley, children, gutter, style, ...other } = this.props
const rootStyle = {
...style,
...styles.root
}
const rootStyle = [
style,
styles.root
]
const contentContainerStyle = {
...styles.contentContainer,
marginHorizontal: `calc(-0.5 * ${alley})`,
paddingHorizontal: `${gutter}`
}
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})`
}
style: [
child.props.style,
styles.column,
{ marginHorizontal: `calc(0.5 * ${alley})` }
]
})
})

View File

@@ -1,10 +1,11 @@
import React, { StyleSheet, Text } from 'react-native'
import React from 'react'
import { StyleSheet, Text } from 'react-native'
const Heading = ({ children, size = 'normal' }) => (
<Text
accessibilityRole='heading'
children={children}
style={{ ...styles.root, ...sizeStyles[size] }}
style={[ styles.root, sizeStyles[size] ]}
/>
)

View File

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

View File

@@ -3,4 +3,4 @@
<title>React Native for Web</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="react-root"></div>
<script src="/examples.js"></script>
<script src="/bundle.js"></script>

View File

@@ -1,23 +1,10 @@
import { MediaProvider, matchMedia } from 'react-media-queries'
import React from 'react'
import ReactNative, { AppRegistry } from 'react-native'
import App from './components/App'
import createGetter from 'react-media-queries/lib/createMediaQueryGetter'
import createListener from 'react-media-queries/lib/createMediaQueryListener'
import React, { AppRegistry } from 'react-native'
import Game2048 from './2048/Game2048'
import TicTacToeApp from './TicTacToe/TicTacToe'
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')
})
const rootTag = document.getElementById('react-root')
AppRegistry.registerComponent('App', () => App)
AppRegistry.runApplication('App', { rootTag })
// ReactNative.render(<App />, rootTag)

View File

@@ -1,13 +1,14 @@
const constants = require('./constants')
const path = require('path')
const webpack = require('webpack')
const EXAMPLES_DIRECTORY = __dirname
module.exports = {
devServer: {
contentBase: constants.EXAMPLES_DIRECTORY
contentBase: EXAMPLES_DIRECTORY
},
entry: {
example: constants.EXAMPLES_DIRECTORY
example: EXAMPLES_DIRECTORY
},
module: {
loaders: [
@@ -20,13 +21,18 @@ module.exports = {
]
},
output: {
filename: 'examples.js'
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
}),
new webpack.optimize.DedupePlugin(),
// https://github.com/animatedjs/animated/issues/40
new webpack.NormalModuleReplacementPlugin(
/es6-set/,
path.join(__dirname, '../src/modules/polyfills/Set.js')
),
new webpack.optimize.OccurenceOrderPlugin()
],
resolve: {

View File

@@ -1,9 +1,9 @@
var constants = require('./constants')
var webpack = require('webpack')
const webpack = require('webpack')
const testEntry = 'tests.webpack.js'
module.exports = function (config) {
config.set({
basePath: constants.ROOT_DIRECTORY,
browsers: process.env.TRAVIS ? [ 'Firefox' ] : [ 'Chrome' ],
browserNoActivityTimeout: 60000,
client: {
@@ -12,24 +12,31 @@ module.exports = function (config) {
useIframe: true
},
files: [
constants.TEST_ENTRY
testEntry
],
frameworks: [ 'mocha' ],
plugins: [
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-mocha',
'karma-mocha-reporter',
'karma-sourcemap-loader',
'karma-spec-reporter',
'karma-webpack'
],
preprocessors: {
[constants.TEST_ENTRY]: [ 'webpack', 'sourcemap' ]
[testEntry]: [ 'webpack', 'sourcemap' ]
},
reporters: process.env.TRAVIS ? [ 'dots' ] : [ 'spec' ],
reporters: process.env.TRAVIS ? [ 'dots' ] : [ 'mocha' ],
singleRun: true,
webpack: {
devtool: 'inline-source-map',
// required by 'enzyme'
externals: {
'cheerio': 'window',
'react/addons': true,
'react/lib/ExecutionEnvironment': true,
'react/lib/ReactContext': true
},
module: {
loaders: [
{

View File

@@ -1,62 +1,61 @@
{
"name": "react-native-web",
"version": "0.0.16",
"version": "0.0.35",
"description": "React Native for Web",
"main": "dist/index.js",
"files": [
"dist"
],
"scripts": {
"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",
"build": "del ./dist && mkdir dist && babel src -d dist --ignore **/__tests__",
"build:umd": "webpack --config webpack.config.js --sort-assets-by --progress",
"examples": "webpack-dev-server --config examples/webpack.config.js --inline --hot --colors --quiet",
"lint": "eslint 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"
"test": "karma start karma.config.js",
"test:watch": "npm run test -- --no-single-run"
},
"dependencies": {
"fbjs": "^0.7.2",
"inline-style-prefixer": "^0.6.7",
"lodash.debounce": "^3.1.1",
"react-tappable": "^0.7.1",
"react-textarea-autosize": "^3.1.0"
"animated": "^0.1.3",
"babel-runtime": "^6.9.2",
"fbjs": "^0.8.1",
"inline-style-prefixer": "^2.0.0",
"lodash": "^4.13.1",
"react-dom": "^15.1.0",
"react-textarea-autosize": "^4.0.2",
"react-timer-mixin": "^0.13.3"
},
"devDependencies": {
"babel-cli": "^6.3.17",
"babel-core": "^6.3.13",
"babel-eslint": "^4.1.6",
"babel-loader": "^6.2.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"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",
"babel-cli": "^6.10.1",
"babel-core": "^6.10.4",
"babel-eslint": "^6.1.0",
"babel-loader": "^6.2.4",
"babel-preset-react-native": "^1.9.0",
"del-cli": "^0.2.0",
"enzyme": "^2.3.0",
"eslint": "^2.12.0",
"eslint-config-standard": "^5.3.1",
"eslint-config-standard-react": "^2.4.0",
"eslint-plugin-promise": "^1.3.2",
"eslint-plugin-react": "^5.1.1",
"eslint-plugin-standard": "^1.3.2",
"karma": "^0.13.22",
"karma-browserstack-launcher": "^1.0.1",
"karma-chrome-launcher": "^1.0.1",
"karma-firefox-launcher": "^1.0.0",
"karma-mocha": "^1.1.1",
"karma-mocha-reporter": "^2.0.4",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^1.7.0",
"mocha": "^2.3.4",
"mocha": "^2.5.3",
"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"
"react": "^15.2.0",
"react-addons-test-utils": "^15.2.0",
"webpack": "^1.13.1",
"webpack-dev-server": "^1.14.1"
},
"peerDependencies": {
"react": "^0.14.3",
"react-dom": "^0.14.3"
"react": "^15.1.0"
},
"author": "Nicolas Gallagher",
"license": "BSD-3-Clause",

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,288 +0,0 @@
/* eslint-disable */
/**
* 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.
*
* @providesModule Interpolation
* @flow
*/
/* eslint no-bitwise: 0 */
'use strict';
/* @edit start */
var normalizeColor = require('../StyleSheet/normalizeColor');
var invariant = require('fbjs/lib/invariant');
/* @edit end */
type ExtrapolateType = 'extend' | 'identity' | 'clamp';
export type InterpolationConfigType = {
inputRange: Array<number>;
outputRange: (Array<number> | Array<string>);
easing?: ((input: number) => number);
extrapolate?: ExtrapolateType;
extrapolateLeft?: ExtrapolateType;
extrapolateRight?: ExtrapolateType;
};
var linear = (t) => t;
/**
* Very handy helper to map input ranges to output ranges with an easing
* function and custom behavior outside of the ranges.
*/
class Interpolation {
static create(config: InterpolationConfigType): (input: number) => number | string {
if (config.outputRange && typeof config.outputRange[0] === 'string') {
return createInterpolationFromStringOutputRange(config);
}
var outputRange: Array<number> = (config.outputRange: any);
checkInfiniteRange('outputRange', outputRange);
var inputRange = config.inputRange;
checkInfiniteRange('inputRange', inputRange);
checkValidInputRange(inputRange);
invariant(
inputRange.length === outputRange.length,
'inputRange (' + inputRange.length + ') and outputRange (' +
outputRange.length + ') must have the same length'
);
var easing = config.easing || linear;
var extrapolateLeft: ExtrapolateType = 'extend';
if (config.extrapolateLeft !== undefined) {
extrapolateLeft = config.extrapolateLeft;
} else if (config.extrapolate !== undefined) {
extrapolateLeft = config.extrapolate;
}
var extrapolateRight: ExtrapolateType = 'extend';
if (config.extrapolateRight !== undefined) {
extrapolateRight = config.extrapolateRight;
} else if (config.extrapolate !== undefined) {
extrapolateRight = config.extrapolate;
}
return (input) => {
invariant(
typeof input === 'number',
'Cannot interpolation an input which is not a number'
);
var range = findRange(input, inputRange);
return interpolate(
input,
inputRange[range],
inputRange[range + 1],
outputRange[range],
outputRange[range + 1],
easing,
extrapolateLeft,
extrapolateRight,
);
};
}
}
function interpolate(
input: number,
inputMin: number,
inputMax: number,
outputMin: number,
outputMax: number,
easing: ((input: number) => number),
extrapolateLeft: ExtrapolateType,
extrapolateRight: ExtrapolateType,
) {
var result = input;
// Extrapolate
if (result < inputMin) {
if (extrapolateLeft === 'identity') {
return result;
} else if (extrapolateLeft === 'clamp') {
result = inputMin;
} else if (extrapolateLeft === 'extend') {
// noop
}
}
if (result > inputMax) {
if (extrapolateRight === 'identity') {
return result;
} else if (extrapolateRight === 'clamp') {
result = inputMax;
} else if (extrapolateRight === 'extend') {
// noop
}
}
if (outputMin === outputMax) {
return outputMin;
}
if (inputMin === inputMax) {
if (input <= inputMin) {
return outputMin;
}
return outputMax;
}
// Input Range
if (inputMin === -Infinity) {
result = -result;
} else if (inputMax === Infinity) {
result = result - inputMin;
} else {
result = (result - inputMin) / (inputMax - inputMin);
}
// Easing
result = easing(result);
// Output Range
if (outputMin === -Infinity) {
result = -result;
} else if (outputMax === Infinity) {
result = result + outputMin;
} else {
result = result * (outputMax - outputMin) + outputMin;
}
return result;
}
function colorToRgba(input: string): string {
var int32Color = normalizeColor(input);
if (int32Color === null) {
return input;
}
int32Color = int32Color || 0; // $FlowIssue
var r = (int32Color & 0xff000000) >>> 24;
var g = (int32Color & 0x00ff0000) >>> 16;
var b = (int32Color & 0x0000ff00) >>> 8;
var a = (int32Color & 0x000000ff) / 255;
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
var stringShapeRegex = /[0-9\.-]+/g;
/**
* Supports string shapes by extracting numbers so new values can be computed,
* and recombines those values into new strings of the same shape. Supports
* things like:
*
* rgba(123, 42, 99, 0.36) // colors
* -45deg // values with units
*/
function createInterpolationFromStringOutputRange(
config: InterpolationConfigType,
): (input: number) => string {
var outputRange: Array<string> = (config.outputRange: any);
invariant(outputRange.length >= 2, 'Bad output range');
outputRange = outputRange.map(colorToRgba);
checkPattern(outputRange);
// ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
// ->
// [
// [0, 50],
// [100, 150],
// [200, 250],
// [0, 0.5],
// ]
/* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to
* guard against this possibility.
*/
var outputRanges = outputRange[0].match(stringShapeRegex).map(() => []);
outputRange.forEach(value => {
/* $FlowFixMe(>=0.18.0): `value.match()` can return `null`. Need to guard
* against this possibility.
*/
value.match(stringShapeRegex).forEach((number, i) => {
outputRanges[i].push(+number);
});
});
/* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to
* guard against this possibility.
*/
var interpolations = outputRange[0].match(stringShapeRegex).map((value, i) => {
return Interpolation.create({
...config,
outputRange: outputRanges[i],
});
});
return (input) => {
var i = 0;
// 'rgba(0, 100, 200, 0)'
// ->
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
return outputRange[0].replace(stringShapeRegex, () => {
return String(interpolations[i++](input));
});
};
}
function checkPattern(arr: Array<string>) {
var pattern = arr[0].replace(stringShapeRegex, '');
for (var i = 1; i < arr.length; ++i) {
invariant(
pattern === arr[i].replace(stringShapeRegex, ''),
'invalid pattern ' + arr[0] + ' and ' + arr[i],
);
}
}
function findRange(input: number, inputRange: Array<number>) {
for (var i = 1; i < inputRange.length - 1; ++i) {
if (inputRange[i] >= input) {
break;
}
}
return i - 1;
}
function checkValidInputRange(arr: Array<number>) {
invariant(arr.length >= 2, 'inputRange must have at least 2 elements');
for (var i = 1; i < arr.length; ++i) {
invariant(
arr[i] >= arr[i - 1],
/* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
* one or both of the operands may be something that doesn't cleanly
* convert to a string, like undefined, null, and object, etc. If you really
* mean this implicit string conversion, you can do something like
* String(myThing)
*/
'inputRange must be monotonically increasing ' + arr
);
}
}
function checkInfiniteRange(name: string, arr: Array<number>) {
invariant(arr.length >= 2, name + ' must have at least 2 elements');
invariant(
arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity,
/* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
* one or both of the operands may be something that doesn't cleanly convert
* to a string, like undefined, null, and object, etc. If you really mean
* this implicit string conversion, you can do something like
* String(myThing)
*/
name + 'cannot be ]-infinity;+infinity[ ' + arr
);
}
module.exports = Interpolation;

View File

@@ -1,103 +0,0 @@
/* eslint-disable */
/**
* 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.
*
* @providesModule SpringConfig
* @flow
*/
'use strict';
type SpringConfigType = {
tension: number,
friction: number,
};
function tensionFromOrigamiValue(oValue) {
return (oValue - 30) * 3.62 + 194;
}
function frictionFromOrigamiValue(oValue) {
return (oValue - 8) * 3 + 25;
}
function fromOrigamiTensionAndFriction(
tension: number,
friction: number,
): SpringConfigType {
return {
tension: tensionFromOrigamiValue(tension),
friction: frictionFromOrigamiValue(friction)
};
}
function fromBouncinessAndSpeed(
bounciness: number,
speed: number,
): SpringConfigType {
function normalize(value, startValue, endValue) {
return (value - startValue) / (endValue - startValue);
}
function projectNormal(n, start, end) {
return start + (n * (end - start));
}
function linearInterpolation(t, start, end) {
return t * end + (1 - t) * start;
}
function quadraticOutInterpolation(t, start, end) {
return linearInterpolation(2 * t - t * t, start, end);
}
function b3Friction1(x) {
return (0.0007 * Math.pow(x, 3)) -
(0.031 * Math.pow(x, 2)) + 0.64 * x + 1.28;
}
function b3Friction2(x) {
return (0.000044 * Math.pow(x, 3)) -
(0.006 * Math.pow(x, 2)) + 0.36 * x + 2;
}
function b3Friction3(x) {
return (0.00000045 * Math.pow(x, 3)) -
(0.000332 * Math.pow(x, 2)) + 0.1078 * x + 5.84;
}
function b3Nobounce(tension) {
if (tension <= 18) {
return b3Friction1(tension);
} else if (tension > 18 && tension <= 44) {
return b3Friction2(tension);
} else {
return b3Friction3(tension);
}
}
var b = normalize(bounciness / 1.7, 0, 20);
b = projectNormal(b, 0, 0.8);
var s = normalize(speed / 1.7, 0, 20);
var bouncyTension = projectNormal(s, 0.5, 200);
var bouncyFriction = quadraticOutInterpolation(
b,
b3Nobounce(bouncyTension),
0.01
);
return {
tension: tensionFromOrigamiValue(bouncyTension),
friction: frictionFromOrigamiValue(bouncyFriction)
};
}
module.exports = {
fromOrigamiTensionAndFriction,
fromBouncinessAndSpeed,
};

View File

@@ -1,19 +1,14 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import AnimatedImplementation from './AnimatedImplementation'
import Animated from 'animated'
import StyleSheet from '../StyleSheet'
import Image from '../../components/Image'
import Text from '../../components/Text'
import View from '../../components/View'
export default {
...AnimatedImplementation,
View: AnimatedImplementation.createAnimatedComponent(View),
Text: AnimatedImplementation.createAnimatedComponent(Text),
Image: AnimatedImplementation.createAnimatedComponent(Image)
Animated.inject.FlattenStyle(StyleSheet.flatten)
module.exports = {
...Animated,
Image: Animated.createAnimatedComponent(Image),
Text: Animated.createAnimatedComponent(Text),
View: Animated.createAnimatedComponent(View)
}

View File

@@ -1,15 +0,0 @@
function SetPolyfill() {
this._cache = []
}
SetPolyfill.prototype.add = function (e) {
if (this._cache.indexOf(e) === -1) {
this._cache.push(e)
}
}
SetPolyfill.prototype.forEach = function (cb) {
this._cache.forEach(cb)
}
export default SetPolyfill

View File

@@ -1,32 +1,20 @@
import Portal from '../../components/Portal'
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import StyleSheet from '../StyleSheet'
import View from '../../components/View'
export default class ReactNativeApp extends Component {
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} />
<RootComponent {...initialProps} rootTag={rootTag} />
</View>
)
}
@@ -34,8 +22,7 @@ export default class ReactNativeApp extends Component {
const styles = StyleSheet.create({
/**
* Ensure that the application covers the whole screen. This prevents the
* Portal content from being clipped.
* Ensure that the application covers the whole screen.
*/
appContainer: {
position: 'absolute',
@@ -45,3 +32,5 @@ const styles = StyleSheet.create({
bottom: 0
}
})
module.exports = ReactNativeApp

View File

@@ -0,0 +1,16 @@
/* eslint-env mocha */
import assert from 'assert'
import { prerenderApplication } from '../renderApplication'
import React from 'react'
const component = () => <div />
suite('apis/AppRegistry/renderApplication', () => {
test('prerenderApplication', () => {
const { html, styleElement } = prerenderApplication(component, {})
assert.ok(html.indexOf('<div ') > -1)
assert.equal(styleElement.type, 'style')
})
})

View File

@@ -24,7 +24,7 @@ type AppConfig = {
/**
* `AppRegistry` is the JS entry point to running all React Native apps.
*/
export default class AppRegistry {
class AppRegistry {
static getAppKeys(): Array<string> {
return Object.keys(runnables)
}
@@ -33,7 +33,7 @@ export default class AppRegistry {
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.`
'This is either due to an import error during initialization or failure to call AppRegistry.registerComponent.'
)
return runnables[appKey].prerender(appParameters)
@@ -78,7 +78,7 @@ export default class AppRegistry {
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.`
'This is either due to an import error during initialization or failure to call AppRegistry.registerComponent.'
)
runnables[appKey].run(appParameters)
@@ -88,3 +88,5 @@ export default class AppRegistry {
ReactDOM.unmountComponentAtNode(rootTag)
}
}
module.exports = AppRegistry

View File

@@ -13,15 +13,9 @@ 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}
@@ -40,6 +34,6 @@ export function prerenderApplication(RootComponent: Component, initialProps: Obj
/>
)
const html = ReactDOMServer.renderToString(component)
const style = renderStyleSheetToString()
return { html, style }
const styleElement = StyleSheet.render()
return { html, styleElement }
}

View File

@@ -1,5 +1,31 @@
/* eslint-env mocha */
import AppState from '..'
import assert from 'assert'
suite('apis/AppState', () => {
test.skip('NO TEST COVERAGE', () => {})
const handler = () => {}
teardown(() => {
try { AppState.removeEventListener('change', handler) } catch (e) {}
})
suite('addEventListener', () => {
test('throws if the provided "eventType" is not supported', () => {
assert.throws(() => AppState.addEventListener('foo', handler))
assert.doesNotThrow(() => AppState.addEventListener('change', handler))
})
})
suite('removeEventListener', () => {
test('throws if the handler is not registered', () => {
assert.throws(() => AppState.removeEventListener('change', handler))
})
test('throws if the provided "eventType" is not supported', () => {
AppState.addEventListener('change', handler)
assert.throws(() => AppState.removeEventListener('foo', handler))
assert.doesNotThrow(() => AppState.removeEventListener('change', handler))
})
})
})

View File

@@ -1,29 +1,54 @@
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
import findIndex from 'lodash/findIndex'
import invariant from 'fbjs/lib/invariant'
const listeners = {}
const eventTypes = [ 'change' ]
const EVENT_TYPES = [ 'change' ]
const VISIBILITY_CHANGE_EVENT = 'visibilitychange'
const AppStates = {
BACKGROUND: 'background',
ACTIVE: 'active'
}
const listeners = []
class AppState {
static isSupported = ExecutionEnvironment.canUseDOM && document.visibilityState
export default class AppState {
static get currentState() {
if (!AppState.isSupported) {
return AppState.ACTIVE
}
switch (document.visibilityState) {
case 'hidden':
case 'prerender':
case 'unloaded':
return 'background'
return AppStates.BACKGROUND
default:
return 'active'
return AppStates.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)
if (AppState.isSupported) {
invariant(EVENT_TYPES.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
const callback = () => handler(AppState.currentState)
listeners.push([ handler, callback ])
document.addEventListener(VISIBILITY_CHANGE_EVENT, callback, 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]
if (AppState.isSupported) {
invariant(EVENT_TYPES.indexOf(type) !== -1, 'Trying to remove listener for unknown event: "%s"', type)
const listenerIndex = findIndex(listeners, (pair) => pair[0] === handler)
invariant(listenerIndex !== -1, 'Trying to remove AppState listener for unregistered handler')
const callback = listeners[listenerIndex][1]
document.removeEventListener(VISIBILITY_CHANGE_EVENT, callback, false)
listeners.splice(listenerIndex, 1)
}
}
}
module.exports = AppState

View File

@@ -12,7 +12,7 @@ const mergeLocalStorageItem = (key, value) => {
window.localStorage.setItem(key, nextValue)
}
export default class AsyncStorage {
class AsyncStorage {
/**
* Erases *all* AsyncStorage for the domain.
*/
@@ -157,3 +157,5 @@ export default class AsyncStorage {
})
}
}
module.exports = AsyncStorage

View File

@@ -6,26 +6,38 @@
* @flow
*/
import debounce from 'lodash/debounce'
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
import invariant from 'fbjs/lib/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 }
}
}
const win = ExecutionEnvironment.canUseDOM ? window : { screen: {} }
export default class Dimensions {
const dimensions = {}
class Dimensions {
static get(dimension: string): Object {
invariant(dimensions[dimension], 'No dimension set for key ' + dimension)
return dimensions[dimension]
}
static set(): void {
dimensions.window = {
fontScale: 1,
height: win.innerHeight,
scale: win.devicePixelRatio || 1,
width: win.innerWidth
}
dimensions.screen = {
fontScale: 1,
height: win.screen.height,
scale: win.devicePixelRatio || 1,
width: win.screen.width
}
}
}
Dimensions.set()
ExecutionEnvironment.canUseDOM && window.addEventListener('resize', debounce(Dimensions.set, 50))
module.exports = Dimensions

View File

@@ -1,83 +0,0 @@
/* eslint-disable */
/**
* https://github.com/arian/cubic-bezier
*
* MIT License
*
* Copyright (c) 2013 Arian Stolwijk
*
* 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.
*
* @providesModule bezier
* @nolint
*/
module.exports = function(x1, y1, x2, y2, epsilon){
var curveX = function(t){
var v = 1 - t;
return 3 * v * v * t * x1 + 3 * v * t * t * x2 + t * t * t;
};
var curveY = function(t){
var v = 1 - t;
return 3 * v * v * t * y1 + 3 * v * t * t * y2 + t * t * t;
};
var derivativeCurveX = function(t){
var v = 1 - t;
return 3 * (2 * (t - 1) * t + v * v) * x1 + 3 * (-t * t * t + 2 * v * t) * x2;
};
return function(t){
var x = t, t0, t1, t2, x2, d2, i;
// First try a few iterations of Newton's method -- normally very fast.
for (t2 = x, i = 0; i < 8; i++){
x2 = curveX(t2) - x;
if (Math.abs(x2) < epsilon) { return curveY(t2); }
d2 = derivativeCurveX(t2);
if (Math.abs(d2) < 1e-6) { break; }
t2 = t2 - x2 / d2;
}
t0 = 0;
t1 = 1;
t2 = x;
if (t2 < t0) { return curveY(t0); }
if (t2 > t1) { return curveY(t1); }
// Fallback to the bisection method for reliability.
while (t0 < t1){
x2 = curveX(t2);
if (Math.abs(x2 - x) < epsilon) { return curveY(t2); }
if (x > x2) { t0 = t2; }
else { t1 = t2; }
t2 = (t1 - t0) * 0.5 + t0;
}
// Failure
return curveY(t2);
};
};

View File

@@ -1,155 +0,0 @@
/* eslint-disable */
/**
* 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.
*
* @providesModule Easing
* @flow
*/
'use strict';
var _bezier = require('./bezier');
/**
* This class implements common easing functions. The math is pretty obscure,
* but this cool website has nice visual illustrations of what they represent:
* http://xaedes.de/dev/transitions/
*/
class Easing {
static step0(n) {
return n > 0 ? 1 : 0;
}
static step1(n) {
return n >= 1 ? 1 : 0;
}
static linear(t) {
return t;
}
static ease(t: number): number {
return ease(t);
}
static quad(t) {
return t * t;
}
static cubic(t) {
return t * t * t;
}
static poly(n) {
return (t) => Math.pow(t, n);
}
static sin(t) {
return 1 - Math.cos(t * Math.PI / 2);
}
static circle(t) {
return 1 - Math.sqrt(1 - t * t);
}
static exp(t) {
return Math.pow(2, 10 * (t - 1));
}
/**
* A simple elastic interaction, similar to a spring. Default bounciness
* is 1, which overshoots a little bit once. 0 bounciness doesn't overshoot
* at all, and bounciness of N > 1 will overshoot about N times.
*
* Wolfram Plots:
*
* http://tiny.cc/elastic_b_1 (default bounciness = 1)
* http://tiny.cc/elastic_b_3 (bounciness = 3)
*/
static elastic(bounciness: number = 1): (t: number) => number {
var p = bounciness * Math.PI;
return (t) => 1 - Math.pow(Math.cos(t * Math.PI / 2), 3) * Math.cos(t * p);
}
static back(s: number): (t: number) => number {
if (s === undefined) {
s = 1.70158;
}
return (t) => t * t * ((s + 1) * t - s);
}
static bounce(t: number): number {
if (t < 1 / 2.75) {
return 7.5625 * t * t;
}
if (t < 2 / 2.75) {
t -= 1.5 / 2.75;
return 7.5625 * t * t + 0.75;
}
if (t < 2.5 / 2.75) {
t -= 2.25 / 2.75;
return 7.5625 * t * t + 0.9375;
}
t -= 2.625 / 2.75;
return 7.5625 * t * t + 0.984375;
}
static bezier(
x1: number,
y1: number,
x2: number,
y2: number,
epsilon?: ?number,
): (t: number) => number {
if (epsilon === undefined) {
// epsilon determines the precision of the solved values
// a good approximation is:
var duration = 500; // duration of animation in milliseconds.
epsilon = (1000 / 60 / duration) / 4;
}
return _bezier(x1, y1, x2, y2, epsilon);
}
static in(
easing: (t: number) => number,
): (t: number) => number {
return easing;
}
/**
* Runs an easing function backwards.
*/
static out(
easing: (t: number) => number,
): (t: number) => number {
return (t) => 1 - easing(1 - t);
}
/**
* Makes any easing function symmetrical.
*/
static inOut(
easing: (t: number) => number,
): (t: number) => number {
return (t) => {
if (t < 0.5) {
return easing(t * 2) / 2;
}
return 1 - easing((1 - t) * 2) / 2;
};
}
}
var ease = Easing.bezier(0.42, 0, 1, 1);
module.exports = Easing;

View File

@@ -45,4 +45,4 @@ const InteractionManager = {
addListener: () => {}
}
export default InteractionManager
module.exports = InteractionManager

View File

@@ -6,9 +6,15 @@
* @flow
*/
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
import invariant from 'fbjs/lib/invariant'
const connection = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection
const connection = ExecutionEnvironment.canUseDOM && (
window.navigator.connection ||
window.navigator.mozConnection ||
window.navigator.webkitConnection
)
const eventTypes = [ 'change' ]
/**
@@ -50,8 +56,8 @@ const NetInfo = {
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)
window.addEventListener('online', handler.bind(null, true), false)
window.addEventListener('offline', handler.bind(null, false), false)
return {
remove: () => NetInfo.isConnected.removeEventListener(type, handler)
@@ -60,8 +66,8 @@ const NetInfo = {
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)
window.removeEventListener('online', handler.bind(null, true), false)
window.removeEventListener('offline', handler.bind(null, false), false)
},
fetch(): Promise {
@@ -76,4 +82,4 @@ const NetInfo = {
}
}
export default NetInfo
module.exports = NetInfo

View File

@@ -6,6 +6,7 @@
"use strict";
import normalizeNativeEvent from '../../modules/normalizeNativeEvent';
var TouchHistoryMath = require('./TouchHistoryMath');
var currentCentroidXOfTouchesChangedAfter =
@@ -287,21 +288,26 @@ var PanResponder = {
var panHandlers = {
onStartShouldSetResponder: function(e) {
return config.onStartShouldSetPanResponder === undefined ? false :
config.onStartShouldSetPanResponder(e, gestureState);
config.onStartShouldSetPanResponder(normalizeEvent(e), gestureState);
},
onMoveShouldSetResponder: function(e) {
return config.onMoveShouldSetPanResponder === undefined ? false :
config.onMoveShouldSetPanResponder(e, gestureState);
config.onMoveShouldSetPanResponder(normalizeEvent(e), gestureState);
},
onStartShouldSetResponderCapture: function(e) {
// TODO: Actually, we should reinitialize the state any time
// touches.length increases from 0 active to > 0 active.
if (e.nativeEvent.touches.length === 1) {
if (e.nativeEvent.touches) {
if (e.nativeEvent.touches.length === 1) {
PanResponder._initializeGestureState(gestureState);
}
}
else if (e.nativeEvent.type === 'mousedown') {
PanResponder._initializeGestureState(gestureState);
}
gestureState.numberActiveTouches = e.touchHistory.numberActiveTouches;
return config.onStartShouldSetPanResponderCapture !== undefined ?
config.onStartShouldSetPanResponderCapture(e, gestureState) : false;
config.onStartShouldSetPanResponderCapture(normalizeEvent(e), gestureState) : false;
},
onMoveShouldSetResponderCapture: function(e) {
@@ -314,7 +320,7 @@ var PanResponder = {
}
PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
return config.onMoveShouldSetPanResponderCapture ?
config.onMoveShouldSetPanResponderCapture(e, gestureState) : false;
config.onMoveShouldSetPanResponderCapture(normalizeEvent(e), gestureState) : false;
},
onResponderGrant: function(e) {
@@ -322,25 +328,25 @@ var PanResponder = {
gestureState.y0 = currentCentroidY(e.touchHistory);
gestureState.dx = 0;
gestureState.dy = 0;
config.onPanResponderGrant && config.onPanResponderGrant(e, gestureState);
config.onPanResponderGrant && config.onPanResponderGrant(normalizeEvent(e), gestureState);
// TODO: t7467124 investigate if this can be removed
return config.onShouldBlockNativeResponder === undefined ? true :
config.onShouldBlockNativeResponder();
},
onResponderReject: function(e) {
config.onPanResponderReject && config.onPanResponderReject(e, gestureState);
config.onPanResponderReject && config.onPanResponderReject(normalizeEvent(e), gestureState);
},
onResponderRelease: function(e) {
config.onPanResponderRelease && config.onPanResponderRelease(e, gestureState);
config.onPanResponderRelease && config.onPanResponderRelease(normalizeEvent(e), gestureState);
PanResponder._initializeGestureState(gestureState);
},
onResponderStart: function(e) {
var touchHistory = e.touchHistory;
gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
config.onPanResponderStart && config.onPanResponderStart(e, gestureState);
config.onPanResponderStart && config.onPanResponderStart(normalizeEvent(e), gestureState);
},
onResponderMove: function(e) {
@@ -353,13 +359,13 @@ var PanResponder = {
// Filter out any touch moves past the first one - we would have
// already processed multi-touch geometry during the first event.
PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
config.onPanResponderMove && config.onPanResponderMove(e, gestureState);
config.onPanResponderMove && config.onPanResponderMove(normalizeEvent(e), gestureState);
},
onResponderEnd: function(e) {
var touchHistory = e.touchHistory;
gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
config.onPanResponderEnd && config.onPanResponderEnd(e, gestureState);
config.onPanResponderEnd && config.onPanResponderEnd(normalizeEvent(e), gestureState);
},
onResponderTerminate: function(e) {
@@ -370,11 +376,17 @@ var PanResponder = {
onResponderTerminationRequest: function(e) {
return config.onPanResponderTerminationRequest === undefined ? true :
config.onPanResponderTerminationRequest(e, gestureState);
config.onPanResponderTerminationRequest(normalizeEvent(e), gestureState);
},
};
return {panHandlers: panHandlers};
},
};
function normalizeEvent(e) {
const normalizedEvent = Object.create(e);
normalizedEvent.nativeEvent = normalizeNativeEvent(e.nativeEvent, e.type);
return normalizedEvent;
}
module.exports = PanResponder;

View File

@@ -11,7 +11,7 @@ import Dimensions from '../Dimensions'
/**
* PixelRatio gives access to the device pixel density.
*/
export default class PixelRatio {
class PixelRatio {
/**
* Returns the device pixel density.
*/
@@ -45,3 +45,5 @@ export default class PixelRatio {
return Math.round(layoutSize * ratio) / ratio
}
}
module.exports = PixelRatio

View File

@@ -1,8 +1,6 @@
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'
const Platform = {
OS: 'web',
userAgent: canUseDOM ? window.navigator.userAgent : ''
select: (obj: Object) => obj.web
}
export default Platform
module.exports = Platform

View File

@@ -1,97 +0,0 @@
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({ [property]: value })
const cssDeclarations = Object.keys(declarations).reduce((str, prop) => {
const value = declarations[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

@@ -1,46 +0,0 @@
/**
* 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(_style)
return { className: _className, style: _style }
}
}

View File

@@ -10,19 +10,20 @@ 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 'fbjs/lib/invariant'
import warning from 'fbjs/lib/warning'
export default class StyleSheetValidation {
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)
} else {
const error = allStylePropTypes[prop](style, prop, caller, 'prop')
if (error) {
styleError(error.message, style, caller)
}
}
}
}
@@ -43,7 +44,7 @@ export default class StyleSheetValidation {
}
const styleError = (message1, style, caller, message2) => {
invariant(
warning(
false,
message1 + '\n' + (caller || '<<unknown>>') + ': ' +
JSON.stringify(style, null, ' ') + (message2 || '')
@@ -63,6 +64,7 @@ StyleSheetValidation.addValidStylePropTypes({
direction: PropTypes.string, /* @private */
float: PropTypes.oneOf([ 'left', 'none', 'right' ]),
font: PropTypes.string, /* @private */
listStyle: PropTypes.string,
verticalAlign: PropTypes.string
listStyle: PropTypes.string
})
module.exports = StyleSheetValidation

View File

@@ -1,47 +0,0 @@
/**
* 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

@@ -1,117 +0,0 @@
/* eslint-env mocha */
import assert from 'assert'
import Store from '../Store'
suite('apis/StyleSheet/Store', () => {
suite('the constructor', () => {
test('initialState', () => {
const initialState = { classNames: { 'textAlign:center': '__classname__' } }
const store = new Store(initialState)
assert.deepEqual(store._classNames['textAlign:center'], '__classname__')
})
})
suite('#get', () => {
test('returns a declaration-specific className', () => {
const initialState = {
classNames: {
'textAlign:center': '__expected__',
'textAlign:left': '__error__'
}
}
const store = new Store(initialState)
assert.deepEqual(store.get('textAlign', 'center'), '__expected__')
})
})
suite('#set', () => {
test('stores declarations', () => {
const store = new Store()
store.set('textAlign', 'center')
store.set('marginTop', 0)
store.set('marginTop', 1)
store.set('marginTop', 2)
assert.deepEqual(store._declarations, {
textAlign: [ 'center' ],
marginTop: [ 0, 1, 2 ]
})
})
test('human-readable classNames', () => {
const store = new Store()
store.set('textAlign', 'center')
store.set('marginTop', 0)
store.set('marginTop', 1)
store.set('marginTop', 2)
assert.deepEqual(store._classNames, {
'textAlign:center': 'textAlign:center',
'marginTop:0': 'marginTop:0',
'marginTop:1': 'marginTop:1',
'marginTop:2': 'marginTop:2'
})
})
test('obfuscated classNames', () => {
const store = new Store({}, { obfuscateClassNames: true })
store.set('textAlign', 'center')
store.set('marginTop', 0)
store.set('marginTop', 1)
store.set('marginTop', 2)
assert.deepEqual(store._classNames, {
'textAlign:center': '_s_1',
'marginTop:0': '_s_2',
'marginTop:1': '_s_3',
'marginTop:2': '_s_4'
})
})
test('replaces space characters', () => {
const store = new Store()
store.set('backgroundPosition', 'top left')
assert.equal(store.get('backgroundPosition', 'top left'), 'backgroundPosition\:top-left')
})
})
suite('#toString', () => {
test('human-readable style sheet', () => {
const store = new Store()
store.set('textAlign', '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%')
console.log(store.toString())
const expected = '/* 6 unique declarations */\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' +
'.textAlign\\:center{text-align:center;}\n' +
'.width\\:100\\%{width:100%;}'
assert.equal(store.toString(), expected)
})
test('obfuscated style sheet', () => {
const store = new Store({}, { obfuscateClassNames: true })
store.set('textAlign', 'center')
store.set('marginBottom', '0px')
store.set('margin', '1px')
store.set('margin', '2px')
store.set('margin', '3px')
const expected = '/* 5 unique declarations */\n' +
'._s_3{margin:1px;}\n' +
'._s_4{margin:2px;}\n' +
'._s_5{margin:3px;}\n' +
'._s_2{margin-bottom:0px;}\n' +
'._s_1{text-align:center;}'
assert.equal(store.toString(), expected)
})
})
})

View File

@@ -0,0 +1,13 @@
/* eslint-env mocha */
import assert from 'assert'
import createReactStyleObject from '../createReactStyleObject'
suite('apis/StyleSheet/createReactStyleObject', () => {
test('converts ReactNative style to ReactDOM style', () => {
const reactNativeStyle = { display: 'flex', marginVertical: 0, opacity: 0 }
const expectedStyle = { display: 'flex', marginTop: '0px', marginBottom: '0px', opacity: 0 }
assert.deepEqual(createReactStyleObject(reactNativeStyle), expectedStyle)
})
})

View File

@@ -4,20 +4,29 @@ import assert from 'assert'
import expandStyle from '../expandStyle'
suite('apis/StyleSheet/expandStyle', () => {
test('style resolution', () => {
test('shortform -> longform', () => {
const initial = {
borderTopWidth: 1,
borderWidth: 2,
borderStyle: 'solid',
boxSizing: 'border-box',
borderBottomColor: 'white',
borderBottomWidth: 1,
borderWidth: 0,
marginTop: 50,
marginVertical: 25,
margin: 10
}
const expected = {
borderTopWidth: '1px',
borderLeftWidth: '2px',
borderRightWidth: '2px',
borderBottomWidth: '2px',
borderBottomStyle: 'solid',
borderLeftStyle: 'solid',
borderRightStyle: 'solid',
boxSizing: 'border-box',
borderBottomColor: 'white',
borderTopStyle: 'solid',
borderTopWidth: '0px',
borderLeftWidth: '0px',
borderRightWidth: '0px',
borderBottomWidth: '1px',
marginTop: '50px',
marginBottom: '25px',
marginLeft: '10px',
@@ -27,6 +36,18 @@ suite('apis/StyleSheet/expandStyle', () => {
assert.deepEqual(expandStyle(initial), expected)
})
test('textAlignVertical', () => {
const initial = {
textAlignVertical: 'center'
}
const expected = {
verticalAlign: 'middle'
}
assert.deepEqual(expandStyle(initial), expected)
})
test('flex', () => {
const value = 10

View File

@@ -1,16 +0,0 @@
/* 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

@@ -1,66 +1,71 @@
/* eslint-env mocha */
import { resetCSS, predefinedCSS } from '../predefs'
import assert from 'assert'
import { defaultStyles } from '../predefs'
import isPlainObject from 'lodash/isPlainObject'
import StyleSheet from '..'
const styles = { root: { borderWidth: 1 } }
suite('apis/StyleSheet', () => {
setup(() => {
StyleSheet._destroy()
StyleSheet._reset()
})
test('absoluteFill', () => {
assert(Number.isInteger(StyleSheet.absoluteFill) === true)
})
test('absoluteFillObject', () => {
assert.ok(isPlainObject(StyleSheet.absoluteFillObject) === true)
})
suite('create', () => {
const div = document.createElement('div')
setup(() => {
document.body.appendChild(div)
StyleSheet.create(styles)
div.innerHTML = `<style id='${StyleSheet.elementId}'>${StyleSheet._renderToString()}</style>`
test('replaces styles with numbers', () => {
const style = StyleSheet.create({ root: { opacity: 1 } })
assert(Number.isInteger(style.root) === true)
})
teardown(() => {
document.body.removeChild(div)
})
test('returns styles object', () => {
assert.equal(StyleSheet.create(styles), styles)
})
test('updates already-rendered style sheet', () => {
test('renders a style sheet in the browser', () => {
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;}`
document.getElementById('__react-native-style').textContent,
defaultStyles
)
})
})
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('flatten', () => {
assert(typeof StyleSheet.flatten === 'function')
})
test('_renderToString', () => {
StyleSheet.create(styles)
test('hairlineWidth', () => {
assert(Number.isInteger(StyleSheet.hairlineWidth) === true)
})
test('render', () => {
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;}`
StyleSheet.render().props.dangerouslySetInnerHTML.__html,
defaultStyles
)
})
test('resolve', () => {
assert.deepEqual(
StyleSheet.resolve({
className: 'test',
style: {
display: 'flex',
opacity: 1,
pointerEvents: 'box-none'
}
}),
{
className: 'test __style_df __style_pebn',
style: {
display: 'flex',
opacity: 1,
pointerEvents: 'box-none'
}
}
)
})
})

View File

@@ -9,5 +9,6 @@ suite('apis/StyleSheet/normalizeValue', () => {
})
test('ignores unitless property values', () => {
assert.deepEqual(normalizeValue('flexGrow', 1), 1)
assert.deepEqual(normalizeValue('scale', 2), 2)
})
})

View File

@@ -0,0 +1,32 @@
/* eslint-env mocha */
import assert from 'assert'
import processTransform from '../processTransform'
suite('apis/StyleSheet/processTransform', () => {
test('transform', () => {
const style = {
transform: [
{ scaleX: 20 },
{ translateX: 20 },
{ rotate: '20deg' }
]
}
assert.deepEqual(
processTransform(style),
{ transform: 'scaleX(20) translateX(20px) rotate(20deg)' }
)
})
test('transformMatrix', () => {
const style = {
transformMatrix: [ 1, 2, 3, 4, 5, 6 ]
}
assert.deepEqual(
processTransform(style),
{ transform: 'matrix3d(1,2,3,4,5,6)' }
)
})
})

View File

@@ -0,0 +1,22 @@
import expandStyle from './expandStyle'
import flattenStyle from '../../modules/flattenStyle'
import prefixAll from 'inline-style-prefixer/static'
import processTransform from './processTransform'
const addVendorPrefixes = (style) => {
let prefixedStyles = prefixAll(style)
// React@15 removed undocumented support for fallback values in
// inline-styles. Revert array values to the standard CSS value
for (const prop in prefixedStyles) {
const value = prefixedStyles[prop]
if (Array.isArray(value)) {
prefixedStyles[prop] = value[value.length - 1]
}
}
return prefixedStyles
}
const _createReactDOMStyleObject = (reactNativeStyle) => processTransform(expandStyle(flattenStyle(reactNativeStyle)))
const createReactDOMStyleObject = (reactNativeStyle) => addVendorPrefixes(_createReactDOMStyleObject(reactNativeStyle))
module.exports = createReactDOMStyleObject

View File

@@ -1,6 +1,18 @@
/**
* The browser implements the CSS cascade, where the order of properties is a
* factor in determining which styles to paint. React Native is different in
* giving precedence to the more specific styles. For example, the value of
* `paddingTop` takes precedence over that of `padding`.
*
* This module creates mutally exclusive style declarations by expanding all of
* React Native's supported shortform properties (e.g. `padding`) to their
* longfrom equivalents.
*/
import normalizeValue from './normalizeValue'
const styleShortHands = {
const emptyObject = {}
const styleShortFormProperties = {
borderColor: [ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor' ],
borderRadius: [ 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius' ],
borderStyle: [ 'borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle' ],
@@ -8,52 +20,54 @@ const styleShortHands = {
margin: [ 'marginTop', 'marginRight', 'marginBottom', 'marginLeft' ],
marginHorizontal: [ 'marginRight', 'marginLeft' ],
marginVertical: [ 'marginTop', 'marginBottom' ],
overflow: [ 'overflowX', 'overflowY' ],
padding: [ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft' ],
paddingHorizontal: [ 'paddingRight', 'paddingLeft' ],
paddingVertical: [ 'paddingTop', 'paddingBottom' ],
textDecorationLine: [ 'textDecoration' ],
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
const alphaSort = (arr) => arr.sort((a, b) => {
if (a < b) { return -1 }
if (a > b) { return 1 }
return 0
})
/**
* Expand the shorthand properties to isolate every declaration from the others.
*/
const expandStyle = (style) => {
const propsArray = Object.keys(style)
const sortedProps = sortProps(propsArray)
const createStyleReducer = (originalStyle) => {
const originalStyleProps = Object.keys(originalStyle)
return sortedProps.reduce((resolvedStyle, key) => {
const expandedProps = styleShortHands[key]
const value = normalizeValue(key, style[key])
return (style, prop) => {
const value = normalizeValue(prop, originalStyle[prop])
const longFormProperties = styleShortFormProperties[prop]
if (expandedProps) {
expandedProps.forEach((prop, i) => {
resolvedStyle[expandedProps[i]] = value
// React Native treats `flex:1` like `flex:1 1 auto`
if (prop === 'flex') {
style.flexGrow = value
style.flexShrink = 1
style.flexBasis = 'auto'
// React Native accepts 'center' as a value
} else if (prop === 'textAlignVertical') {
style.verticalAlign = (value === 'center' ? 'middle' : value)
} else if (longFormProperties) {
longFormProperties.forEach((longForm, i) => {
// the value of any longform property in the original styles takes
// precedence over the shortform's value
if (originalStyleProps.indexOf(longForm) === -1) {
style[longForm] = value
}
})
} else if (key === 'flex') {
resolvedStyle.flexGrow = value
resolvedStyle.flexShrink = 1
resolvedStyle.flexBasis = 'auto'
} else {
resolvedStyle[key] = value
style[prop] = value
}
return resolvedStyle
}, {})
return style
}
}
export default expandStyle
const expandStyle = (style = emptyObject) => {
const sortedStyleProps = alphaSort(Object.keys(style))
const styleReducer = createStyleReducer(style)
return sortedStyleProps.reduce(styleReducer, {})
}
module.exports = expandStyle

View File

@@ -1,34 +0,0 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import invariant from 'fbjs/lib/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

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

View File

@@ -1,75 +1,76 @@
import { resetCSS, predefinedCSS, predefinedClassNames } from './predefs'
import flattenStyle from './flattenStyle'
import Store from './Store'
import StyleSheetRegistry from './StyleSheetRegistry'
import createReactStyleObject from './createReactStyleObject'
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
import flattenStyle from '../../modules/flattenStyle'
import React from 'react'
import ReactNativePropRegistry from '../../modules/ReactNativePropRegistry'
import StyleSheetValidation from './StyleSheetValidation'
import { defaultStyles, mapStyleToClassName } from './predefs'
const ELEMENT_ID = 'react-stylesheet'
let isRendered = false
let lastStyleSheet = ''
let styleElement
const STYLE_SHEET_ID = '__react-native-style'
/**
* 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()
const _injectStyleSheet = () => {
// check if the server rendered the style sheet
styleElement = document.getElementById(STYLE_SHEET_ID)
// if not, inject the style sheet
if (!styleElement) { document.head.insertAdjacentHTML('afterbegin', renderToString()) }
isRendered = true
}
/**
* Destroy existing styles
*/
const _destroy = () => {
store = createStore()
const _reset = () => {
if (styleElement) { document.head.removeChild(styleElement) }
styleElement = null
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 absoluteFillObject = { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0 }
const absoluteFill = ReactNativePropRegistry.register(absoluteFillObject)
const create = (styles: Object): Object => {
for (const key in styles) {
StyleSheetValidation.validateStyle(key, styles)
StyleSheetRegistry.registerStyle(styles[key], store)
if (!isRendered && ExecutionEnvironment.canUseDOM) {
_injectStyleSheet()
}
// 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')
const result = {}
for (let key in styles) {
StyleSheetValidation.validateStyle(key, styles)
result[key] = ReactNativePropRegistry.register(styles[key])
}
return result
}
const render = () => <style dangerouslySetInnerHTML={{ __html: defaultStyles }} id={STYLE_SHEET_ID} />
const renderToString = () => `<style id="${STYLE_SHEET_ID}">${defaultStyles}</style>`
/**
* Accepts React props and converts style declarations to classNames when necessary
*/
const resolve = (props) => {
let className = props.className || ''
let style = createReactStyleObject(props.style)
for (const prop in style) {
const value = style[prop]
const replacementClassName = mapStyleToClassName(prop, value)
if (replacementClassName) {
className += ` ${replacementClassName}`
// delete style[prop]
}
}
return styles
return { className, style }
}
/**
* 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,
module.exports = {
_reset,
absoluteFill,
absoluteFillObject,
create,
elementId: ELEMENT_ID,
hairlineWidth: 1,
flatten: flattenStyle,
/* @platform web */
render,
/* @platform web */
resolve
}

View File

@@ -9,7 +9,6 @@ const unitlessNumbers = {
flexNegative: true,
fontWeight: true,
lineClamp: true,
lineHeight: true,
opacity: true,
order: true,
orphans: true,
@@ -20,7 +19,12 @@ const unitlessNumbers = {
fillOpacity: true,
strokeDashoffset: true,
strokeOpacity: true,
strokeWidth: true
strokeWidth: true,
// transform types
scale: true,
scaleX: true,
scaleY: true,
scaleZ: true
}
const normalizeValue = (property, value) => {
@@ -30,4 +34,4 @@ const normalizeValue = (property, value) => {
return value
}
export default normalizeValue
module.exports = normalizeValue

View File

@@ -1,25 +1,38 @@
/**
* 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}`
const DISPLAY_FLEX_CLASSNAME = '__style_df'
const POINTER_EVENTS_AUTO_CLASSNAME = '__style_pea'
const POINTER_EVENTS_BOX_NONE_CLASSNAME = '__style_pebn'
const POINTER_EVENTS_BOX_ONLY_CLASSNAME = '__style_pebo'
const POINTER_EVENTS_NONE_CLASSNAME = '__style_pen'
/**
* 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'
const styleAsClassName = {
display: {
'flex': DISPLAY_FLEX_CLASSNAME
},
pointerEvents: {
'auto': POINTER_EVENTS_AUTO_CLASSNAME,
'box-none': POINTER_EVENTS_BOX_NONE_CLASSNAME,
'box-only': POINTER_EVENTS_BOX_ONLY_CLASSNAME,
'none': POINTER_EVENTS_NONE_CLASSNAME
}
}
export const mapStyleToClassName = (prop, value) => {
return styleAsClassName[prop] && styleAsClassName[prop][value]
}
// reset unwanted styles beyond the control of React inline styles
const resetCSS =
'/* React Native */\n' +
'html {font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}\n' +
'body {margin:0}\n' +
'button::-moz-focus-inner, input::-moz-focus-inner {border:0;padding:0}\n' +
'input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {display:none}'
const helperCSS =
// vendor prefix 'display:flex' until React supports fallback values for inline styles
`.${DISPLAY_FLEX_CLASSNAME} {display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex}\n` +
// implement React Native's pointer event values
`.${POINTER_EVENTS_AUTO_CLASSNAME}, .${POINTER_EVENTS_BOX_ONLY_CLASSNAME}, .${POINTER_EVENTS_BOX_NONE_CLASSNAME} * {pointer-events:auto}\n` +
`.${POINTER_EVENTS_NONE_CLASSNAME}, .${POINTER_EVENTS_BOX_ONLY_CLASSNAME} *, .${POINTER_EVENTS_NONE_CLASSNAME} {pointer-events:none}`
export const defaultStyles = `${resetCSS}\n${helperCSS}`

View File

@@ -1,3 +0,0 @@
import Prefixer from 'inline-style-prefixer'
const prefixer = Prefixer.prefixAll
export default prefixer

View File

@@ -0,0 +1,29 @@
import normalizeValue from './normalizeValue'
// { scale: 2 } => 'scale(2)'
// { translateX: 20 } => 'translateX(20px)'
const mapTransform = (transform) => {
const type = Object.keys(transform)[0]
const value = normalizeValue(type, transform[type])
return `${type}(${value})`
}
// [1,2,3,4,5,6] => 'matrix3d(1,2,3,4,5,6)'
const convertTransformMatrix = (transformMatrix) => {
var matrix = transformMatrix.join(',')
return `matrix3d(${matrix})`
}
const processTransform = (style) => {
if (style) {
if (style.transform) {
style.transform = style.transform.map(mapTransform).join(' ')
} else if (style.transformMatrix) {
style.transform = convertTransformMatrix(style.transformMatrix)
delete style.transformMatrix
}
}
return style
}
module.exports = processTransform

View File

@@ -73,23 +73,63 @@ suite('apis/UIManager', () => {
})
})
suite('measureInWindow', () => {
test('provides correct layout to 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.measureInWindow(node, (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 componentStub = {
_reactInternalInstance: {
_currentElement: { _owner: {} },
_debugID: 1
}
}
test('add new className to existing className', () => {
const node = createNode()
node.className = 'existing'
const props = { className: 'extra' }
UIManager.updateView(node, props)
UIManager.updateView(node, props, componentStub)
assert.equal(node.getAttribute('class'), 'existing extra')
})
test('adds new style to existing style', () => {
test('adds correct DOM styles 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;')
const props = { style: { marginVertical: 0, opacity: 0 } }
UIManager.updateView(node, props, componentStub)
assert.equal(node.getAttribute('style'), 'color: red; margin-top: 0px; margin-bottom: 0px; opacity: 0;')
})
test('sets attribute values', () => {
test('replaces input and textarea text', () => {
const node = createNode()
node.value = 'initial'
const textProp = { text: 'expected-text' }
const valueProp = { value: 'expected-value' }
UIManager.updateView(node, textProp)
assert.equal(node.value, 'expected-text')
UIManager.updateView(node, valueProp)
assert.equal(node.value, 'expected-value')
})
test('sets other attribute values', () => {
const node = createNode()
const props = { 'aria-level': '4', 'data-of-type': 'string' }
UIManager.updateView(node, props)

View File

@@ -1,6 +1,7 @@
import createReactStyleObject from '../StyleSheet/createReactStyleObject'
import CSSPropertyOperations from 'react/lib/CSSPropertyOperations'
const measureAll = (node, callback, relativeToNativeNode) => {
const _measureLayout = (node, relativeToNativeNode, callback) => {
const relativeNode = relativeToNativeNode || node.parentNode
const relativeRect = relativeNode.getBoundingClientRect()
const { height, left, top, width } = node.getBoundingClientRect()
@@ -10,26 +11,59 @@ const measureAll = (node, callback, relativeToNativeNode) => {
}
const UIManager = {
blur(node) {
try { node.blur() } catch (err) {}
},
focus(node) {
try { node.focus() } catch (err) {}
},
measure(node, callback) {
measureAll(node, callback)
_measureLayout(node, null, callback)
},
measureInWindow(node, callback) {
const { height, left, top, width } = node.getBoundingClientRect()
callback(left, top, width, height)
},
measureLayout(node, relativeToNativeNode, onFail, onSuccess) {
measureAll(node, (x, y, width, height) => onSuccess(x, y, width, height), relativeToNativeNode)
const relativeTo = relativeToNativeNode || node.parentNode
_measureLayout(node, relativeTo, onSuccess)
},
updateView(node, props) {
updateView(node, props, component /* only needed to surpress React errors in development */) {
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)
switch (prop) {
case 'style':
// convert styles to DOM-styles
CSSPropertyOperations.setValueForStyles(
node,
createReactStyleObject(value),
component._reactInternalInstance
)
break
case 'class':
case 'className': {
const nativeProp = 'class'
// prevent class names managed by React Native from being replaced
const className = node.getAttribute(nativeProp) + ' ' + value
node.setAttribute(nativeProp, className)
break
}
case 'text':
case 'value':
// native platforms use `text` prop to replace text input value
node.value = value
break
default:
node.setAttribute(prop, value)
}
}
}
}
export default UIManager
module.exports = UIManager

View File

@@ -1,4 +1,4 @@
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
import applyNativeMethods from '../../modules/applyNativeMethods'
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import StyleSheet from '../../apis/StyleSheet'
@@ -19,8 +19,7 @@ const keyframeEffects = [
{ transform: 'scale(0.95)', opacity: 0.5 }
]
@NativeMethodsDecorator
export default class ActivityIndicator extends Component {
class ActivityIndicator extends Component {
static propTypes = {
animating: PropTypes.bool,
color: PropTypes.string,
@@ -61,7 +60,7 @@ export default class ActivityIndicator extends Component {
return (
<View {...other} style={[ styles.container, style ]}>
<View
ref={(c) => { this._indicatorRef = c }}
ref={this._createIndicatorRef}
style={[
indicatorStyles[size],
hidesWhenStopped && !animating && styles.hidesWhenStopped,
@@ -72,6 +71,10 @@ export default class ActivityIndicator extends Component {
)
}
_createIndicatorRef = (component) => {
this._indicatorRef = component
}
_manageAnimation() {
if (this._player) {
if (this.props.animating) {
@@ -83,6 +86,8 @@ export default class ActivityIndicator extends Component {
}
}
applyNativeMethods(ActivityIndicator)
const styles = StyleSheet.create({
container: {
alignItems: 'center',
@@ -107,3 +112,5 @@ const indicatorStyles = StyleSheet.create({
height: 36
}
})
module.exports = ActivityIndicator

View File

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

View File

@@ -1,66 +0,0 @@
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
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'
}
@NativeMethodsDecorator
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

@@ -7,4 +7,4 @@ const ImageResizeMode = keyMirror({
stretch: null
})
export default ImageResizeMode
module.exports = ImageResizeMode

View File

@@ -1,15 +1,19 @@
import { PropTypes } from 'react'
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
import LayoutPropTypes from '../../apis/StyleSheet/LayoutPropTypes'
import TransformPropTypes from '../../apis/StyleSheet/TransformPropTypes'
import BorderPropTypes from '../../propTypes/BorderPropTypes'
import ColorPropType from '../../propTypes/ColorPropType'
import LayoutPropTypes from '../../propTypes/LayoutPropTypes'
import TransformPropTypes from '../../propTypes/TransformPropTypes'
import ImageResizeMode from './ImageResizeMode'
const hiddenOrVisible = PropTypes.oneOf([ 'hidden', 'visible' ])
export default {
module.exports = {
...BorderPropTypes,
...LayoutPropTypes,
...TransformPropTypes,
backfaceVisibility: hiddenOrVisible,
backgroundColor: ColorPropType,
resizeMode: PropTypes.oneOf(Object.keys(ImageResizeMode)),
/**
* @platform web
*/

View File

@@ -1,44 +1,56 @@
/* eslint-env mocha */
import * as utils from '../../../modules/specHelpers'
import { mount, shallow } from 'enzyme'
import assert from 'assert'
import React from 'react'
import StyleSheet from '../../../apis/StyleSheet'
import Image from '../'
suite('components/Image', () => {
test('default accessibility', () => {
const dom = utils.renderToDOM(<Image />)
assert.equal(dom.getAttribute('role'), 'img')
test('sets correct accessibility role"', () => {
const image = shallow(<Image />)
assert.equal(image.prop('accessibilityRole'), 'img')
})
test('prop "accessibilityLabel"', () => {
const accessibilityLabel = 'accessibilityLabel'
const result = utils.shallowRender(<Image accessibilityLabel={accessibilityLabel} />)
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
const image = shallow(<Image accessibilityLabel={accessibilityLabel} />)
assert.equal(image.prop('accessibilityLabel'), accessibilityLabel)
})
test('prop "accessible"', () => {
const accessible = false
const result = utils.shallowRender(<Image accessible={accessible} />)
assert.equal(result.props.accessible, accessible)
const image = shallow(<Image accessible={accessible} />)
assert.equal(image.prop('accessible'), accessible)
})
test('prop "children"')
test('prop "children"', () => {
const children = <div className='unique' />
const wrapper = shallow(<Image>{children}</Image>)
assert.equal(wrapper.contains(children), true)
})
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)
suite('prop "defaultSource"', () => {
test('sets background image when value is an object', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' }
const image = shallow(<Image defaultSource={defaultSource} />)
const backgroundImage = StyleSheet.flatten(image.prop('style')).backgroundImage
assert(backgroundImage.indexOf(defaultSource.uri) > -1)
})
test('sets background image when value is a string', () => {
// emulate require-ed asset
const defaultSource = 'https://google.com/favicon.ico'
const image = shallow(<Image defaultSource={defaultSource} />)
const backgroundImage = StyleSheet.flatten(image.prop('style')).backgroundImage
assert(backgroundImage.indexOf(defaultSource) > -1)
})
})
test('prop "onError"', function (done) {
this.timeout(5000)
utils.render(<Image
onError={onError}
source={{ uri: 'https://google.com/favicon.icox' }}
/>)
mount(<Image onError={onError} source={{ uri: 'https://google.com/favicon.icox' }} />)
function onError(e) {
assert.equal(e.nativeEvent.type, 'error')
done()
@@ -47,27 +59,104 @@ suite('components/Image', () => {
test('prop "onLoad"', function (done) {
this.timeout(5000)
utils.render(<Image
onLoad={onLoad}
source={{ uri: 'https://google.com/favicon.ico' }}
/>)
const image = mount(<Image onLoad={onLoad} source={{ uri: 'https://google.com/favicon.ico' }} />)
function onLoad(e) {
assert.equal(e.nativeEvent.type, 'load')
const backgroundImage = StyleSheet.flatten(image.ref('root').prop('style')).backgroundImage
assert.notDeepEqual(backgroundImage, undefined)
done()
}
})
test('prop "onLoadEnd"')
test('prop "onLoadEnd"', function (done) {
this.timeout(5000)
const image = mount(<Image onLoadEnd={onLoadEnd} source={{ uri: 'https://google.com/favicon.ico' }} />)
function onLoadEnd() {
assert.ok(true)
const backgroundImage = StyleSheet.flatten(image.ref('root').prop('style')).backgroundImage
assert.notDeepEqual(backgroundImage, undefined)
done()
}
})
test('prop "onLoadStart"')
test('prop "onLoadStart"', function (done) {
this.timeout(5000)
mount(<Image onLoadStart={onLoadStart} source={{ uri: 'https://google.com/favicon.ico' }} />)
function onLoadStart() {
assert.ok(true)
done()
}
})
test('prop "resizeMode"')
suite('prop "resizeMode"', () => {
const getBackgroundSize = (image) => StyleSheet.flatten(image.prop('style')).backgroundSize
test('prop "source"')
test('value "contain"', () => {
const image = shallow(<Image resizeMode={Image.resizeMode.contain} />)
assert.equal(getBackgroundSize(image), 'contain')
})
test('value "cover"', () => {
const image = shallow(<Image resizeMode={Image.resizeMode.cover} />)
assert.equal(getBackgroundSize(image), 'cover')
})
test('value "none"', () => {
const image = shallow(<Image resizeMode={Image.resizeMode.none} />)
assert.equal(getBackgroundSize(image), 'auto')
})
test('value "stretch"', () => {
const image = shallow(<Image resizeMode={Image.resizeMode.stretch} />)
assert.equal(getBackgroundSize(image), '100% 100%')
})
test('no value', () => {
const image = shallow(<Image />)
assert.equal(getBackgroundSize(image), 'cover')
})
})
suite('prop "source"', function () {
this.timeout(5000)
test('sets background image when value is an object', (done) => {
const source = { uri: 'https://google.com/favicon.ico' }
mount(<Image onLoad={onLoad} source={source} />)
function onLoad(e) {
const src = e.nativeEvent.target.src
assert.equal(src, source.uri)
done()
}
})
test('sets background image when value is a string', (done) => {
// emulate require-ed asset
const source = 'https://google.com/favicon.ico'
mount(<Image onLoad={onLoad} source={source} />)
function onLoad(e) {
const src = e.nativeEvent.target.src
assert.equal(src, source)
done()
}
})
})
suite('prop "style"', () => {
test('converts "resizeMode" property', () => {
const image = shallow(<Image style={{ resizeMode: Image.resizeMode.contain }} />)
assert.equal(StyleSheet.flatten(image.prop('style')).backgroundSize, 'contain')
})
test('removes "resizeMode" property', () => {
const image = shallow(<Image style={{ resizeMode: Image.resizeMode.contain }} />)
assert.equal(StyleSheet.flatten(image.prop('style')).resizeMode, undefined)
})
})
test('prop "testID"', () => {
const testID = 'testID'
const result = utils.shallowRender(<Image testID={testID} />)
assert.equal(result.props.testID, testID)
const image = shallow(<Image testID={testID} />)
assert.equal(image.prop('testID'), testID)
})
})

View File

@@ -1,11 +1,12 @@
/* global window */
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
import StyleSheet from '../../apis/StyleSheet'
import CoreComponent from '../CoreComponent'
import applyNativeMethods from '../../modules/applyNativeMethods'
import createReactDOMComponent from '../../modules/createReactDOMComponent'
import ImageResizeMode from './ImageResizeMode'
import ImageStylePropTypes from './ImageStylePropTypes'
import resolveAssetSource from './resolveAssetSource'
import React, { Component, PropTypes } from 'react'
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
import StyleSheet from '../../apis/StyleSheet'
import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
import View from '../View'
const STATUS_ERRORED = 'ERRORED'
@@ -14,50 +15,128 @@ const STATUS_LOADING = 'LOADING'
const STATUS_PENDING = 'PENDING'
const STATUS_IDLE = 'IDLE'
@NativeMethodsDecorator
export default class Image extends Component {
const ImageSourcePropType = PropTypes.oneOfType([
PropTypes.shape({
uri: PropTypes.string.isRequired
}),
PropTypes.string
])
class Image extends Component {
static displayName = 'Image'
static propTypes = {
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
accessible: CoreComponent.propTypes.accessible,
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
accessible: createReactDOMComponent.propTypes.accessible,
children: PropTypes.any,
defaultSource: PropTypes.object,
defaultSource: ImageSourcePropType,
onError: PropTypes.func,
onLayout: PropTypes.func,
onLoad: PropTypes.func,
onLoadEnd: PropTypes.func,
onLoadStart: PropTypes.func,
resizeMode: PropTypes.oneOf(['contain', 'cover', 'none', 'stretch']),
source: PropTypes.object,
source: ImageSourcePropType,
style: StyleSheetPropType(ImageStylePropTypes),
testID: CoreComponent.propTypes.testID
testID: createReactDOMComponent.propTypes.testID
};
static defaultProps = {
accessible: true,
defaultSource: {},
resizeMode: 'cover',
source: {}
style: {}
};
static resizeMode = ImageResizeMode;
constructor(props, context) {
super(props, context)
const { uri } = props.source
// state
const uri = resolveAssetSource(props.source)
this.state = { status: uri ? STATUS_PENDING : STATUS_IDLE }
// autobinding
this._onError = this._onError.bind(this)
this._onLoad = this._onLoad.bind(this)
}
componentDidMount() {
if (this.state.status === STATUS_PENDING) {
this._createImageLoader()
}
}
componentDidUpdate() {
if (this.state.status === STATUS_PENDING && !this.image) {
this._createImageLoader()
}
}
componentWillReceiveProps(nextProps) {
const nextUri = resolveAssetSource(nextProps.source)
if (resolveAssetSource(this.props.source) !== nextUri) {
this.setState({
status: nextUri ? STATUS_PENDING : STATUS_IDLE
})
}
}
componentWillUnmount() {
this._destroyImageLoader()
}
render() {
const {
accessibilityLabel,
accessible,
children,
defaultSource,
onLayout,
source,
testID
} = this.props
const isLoaded = this.state.status === STATUS_LOADED
const displayImage = resolveAssetSource(!isLoaded ? defaultSource : source)
const backgroundImage = displayImage ? `url("${displayImage}")` : null
const style = StyleSheet.flatten(this.props.style)
const resizeMode = this.props.resizeMode || style.resizeMode || ImageResizeMode.cover
// remove resizeMode style, as it is not supported by View
delete style.resizeMode
/**
* 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}
onLayout={onLayout}
ref='root'
style={[
styles.initial,
style,
backgroundImage && { backgroundImage },
resizeModeStyles[resizeMode]
]}
testID={testID}
>
{createReactDOMComponent({ component: 'img', src: displayImage, style: styles.img })}
{children ? (
<View children={children} pointerEvents='box-none' style={styles.children} />
) : null}
</View>
)
}
_createImageLoader() {
const { source } = this.props
const uri = resolveAssetSource(this.props.source)
this._destroyImageLoader()
this.image = new window.Image()
this.image.onerror = this._onError
this.image.onload = this._onLoad
this.image.src = source.uri
this.image.src = uri
this._onLoadStart()
}
@@ -69,7 +148,7 @@ export default class Image extends Component {
}
}
_onError(e) {
_onError = (e) => {
const { onError } = this.props
const event = { nativeEvent: e }
@@ -79,7 +158,7 @@ export default class Image extends Component {
if (onError) onError(event)
}
_onLoad(e) {
_onLoad = (e) => {
const { onLoad } = this.props
const event = { nativeEvent: e }
@@ -99,77 +178,10 @@ export default class Image extends Component {
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>
)
}
}
applyNativeMethods(Image)
const styles = StyleSheet.create({
initial: {
alignSelf: 'flex-start',
@@ -208,3 +220,5 @@ const resizeModeStyles = StyleSheet.create({
backgroundSize: '100% 100%'
}
})
module.exports = Image

View File

@@ -0,0 +1,5 @@
function resolveAssetSource(source) {
return ((typeof source === 'object') ? source.uri : source) || null
}
module.exports = resolveAssetSource

View File

@@ -0,0 +1,408 @@
/* eslint-disable */
/**
* Copyright (c) 2015, Facebook, Inc. All rights reserved.
*
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
* all intellectual property and other proprietary rights, in and to the React
* Native CustomComponents software (the "Software"). Subject to your
* compliance with these terms, you are hereby granted a non-exclusive,
* worldwide, royalty-free copyright license to (1) use and copy the Software;
* and (2) reproduce and distribute the Software as part of your own software
* ("Your Software"). Facebook reserves all rights not expressly granted to
* you in this license agreement.
*
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "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 FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
* EMPLOYEES 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 THE SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @providesModule ListViewDataSource
* @typechecks
* @flow
*/
'use strict';
var invariant = require('fbjs/lib/invariant');
var isEmpty = require('fbjs/lib/isEmpty');
var warning = require('fbjs/lib/warning');
function defaultGetRowData(
dataBlob: any,
sectionID: number | string,
rowID: number | string
): any {
return dataBlob[sectionID][rowID];
}
function defaultGetSectionHeaderData(
dataBlob: any,
sectionID: number | string
): any {
return dataBlob[sectionID];
}
type differType = (data1: any, data2: any) => bool;
type ParamType = {
rowHasChanged: differType;
getRowData: ?typeof defaultGetRowData;
sectionHeaderHasChanged: ?differType;
getSectionHeaderData: ?typeof defaultGetSectionHeaderData;
}
/**
* Provides efficient data processing and access to the
* `ListView` component. A `ListViewDataSource` is created with functions for
* extracting data from the input blob, and comparing elements (with default
* implementations for convenience). The input blob can be as simple as an
* array of strings, or an object with rows nested inside section objects.
*
* To update the data in the datasource, use `cloneWithRows` (or
* `cloneWithRowsAndSections` if you care about sections). The data in the
* data source is immutable, so you can't modify it directly. The clone methods
* suck in the new data and compute a diff for each row so ListView knows
* whether to re-render it or not.
*
* In this example, a component receives data in chunks, handled by
* `_onDataArrived`, which concats the new data onto the old data and updates the
* data source. We use `concat` to create a new array - mutating `this._data`,
* e.g. with `this._data.push(newRowData)`, would be an error. `_rowHasChanged`
* understands the shape of the row data and knows how to efficiently compare
* it.
*
* ```
* getInitialState: function() {
* var ds = new ListViewDataSource({rowHasChanged: this._rowHasChanged});
* return {ds};
* },
* _onDataArrived(newData) {
* this._data = this._data.concat(newData);
* this.setState({
* ds: this.state.ds.cloneWithRows(this._data)
* });
* }
* ```
*/
class ListViewDataSource {
/**
* You can provide custom extraction and `hasChanged` functions for section
* headers and rows. If absent, data will be extracted with the
* `defaultGetRowData` and `defaultGetSectionHeaderData` functions.
*
* The default extractor expects data of one of the following forms:
*
* { sectionID_1: { rowID_1: <rowData1>, ... }, ... }
*
* or
*
* { sectionID_1: [ <rowData1>, <rowData2>, ... ], ... }
*
* or
*
* [ [ <rowData1>, <rowData2>, ... ], ... ]
*
* The constructor takes in a params argument that can contain any of the
* following:
*
* - getRowData(dataBlob, sectionID, rowID);
* - getSectionHeaderData(dataBlob, sectionID);
* - rowHasChanged(prevRowData, nextRowData);
* - sectionHeaderHasChanged(prevSectionData, nextSectionData);
*/
constructor(params: ParamType) {
invariant(
params && typeof params.rowHasChanged === 'function',
'Must provide a rowHasChanged function.'
);
this._rowHasChanged = params.rowHasChanged;
this._getRowData = params.getRowData || defaultGetRowData;
this._sectionHeaderHasChanged = params.sectionHeaderHasChanged;
this._getSectionHeaderData =
params.getSectionHeaderData || defaultGetSectionHeaderData;
this._dataBlob = null;
this._dirtyRows = [];
this._dirtySections = [];
this._cachedRowCount = 0;
// These two private variables are accessed by outsiders because ListView
// uses them to iterate over the data in this class.
this.rowIdentities = [];
this.sectionIdentities = [];
}
/**
* Clones this `ListViewDataSource` with the specified `dataBlob` and
* `rowIdentities`. The `dataBlob` is just an arbitrary blob of data. At
* construction an extractor to get the interesting information was defined
* (or the default was used).
*
* The `rowIdentities` is is a 2D array of identifiers for rows.
* ie. [['a1', 'a2'], ['b1', 'b2', 'b3'], ...]. If not provided, it's
* assumed that the keys of the section data are the row identities.
*
* Note: This function does NOT clone the data in this data source. It simply
* passes the functions defined at construction to a new data source with
* the data specified. If you wish to maintain the existing data you must
* handle merging of old and new data separately and then pass that into
* this function as the `dataBlob`.
*/
cloneWithRows(
dataBlob: Array<any> | {[key: string]: any},
rowIdentities: ?Array<string>
): ListViewDataSource {
var rowIds = rowIdentities ? [rowIdentities] : null;
if (!this._sectionHeaderHasChanged) {
this._sectionHeaderHasChanged = () => false;
}
return this.cloneWithRowsAndSections({s1: dataBlob}, ['s1'], rowIds);
}
/**
* This performs the same function as the `cloneWithRows` function but here
* you also specify what your `sectionIdentities` are. If you don't care
* about sections you should safely be able to use `cloneWithRows`.
*
* `sectionIdentities` is an array of identifiers for sections.
* ie. ['s1', 's2', ...]. If not provided, it's assumed that the
* keys of dataBlob are the section identities.
*
* Note: this returns a new object!
*/
cloneWithRowsAndSections(
dataBlob: any,
sectionIdentities: ?Array<string>,
rowIdentities: ?Array<Array<string>>
): ListViewDataSource {
invariant(
typeof this._sectionHeaderHasChanged === 'function',
'Must provide a sectionHeaderHasChanged function with section data.'
);
var newSource = new ListViewDataSource({
getRowData: this._getRowData,
getSectionHeaderData: this._getSectionHeaderData,
rowHasChanged: this._rowHasChanged,
sectionHeaderHasChanged: this._sectionHeaderHasChanged,
});
newSource._dataBlob = dataBlob;
if (sectionIdentities) {
newSource.sectionIdentities = sectionIdentities;
} else {
newSource.sectionIdentities = Object.keys(dataBlob);
}
if (rowIdentities) {
newSource.rowIdentities = rowIdentities;
} else {
newSource.rowIdentities = [];
newSource.sectionIdentities.forEach((sectionID) => {
newSource.rowIdentities.push(Object.keys(dataBlob[sectionID]));
});
}
newSource._cachedRowCount = countRows(newSource.rowIdentities);
newSource._calculateDirtyArrays(
this._dataBlob,
this.sectionIdentities,
this.rowIdentities
);
return newSource;
}
getRowCount(): number {
return this._cachedRowCount;
}
/**
* Returns if the row is dirtied and needs to be rerendered
*/
rowShouldUpdate(sectionIndex: number, rowIndex: number): bool {
var needsUpdate = this._dirtyRows[sectionIndex][rowIndex];
warning(needsUpdate !== undefined,
'missing dirtyBit for section, row: ' + sectionIndex + ', ' + rowIndex);
return needsUpdate;
}
/**
* Gets the data required to render the row.
*/
getRowData(sectionIndex: number, rowIndex: number): any {
var sectionID = this.sectionIdentities[sectionIndex];
var rowID = this.rowIdentities[sectionIndex][rowIndex];
warning(
sectionID !== undefined && rowID !== undefined,
'rendering invalid section, row: ' + sectionIndex + ', ' + rowIndex
);
return this._getRowData(this._dataBlob, sectionID, rowID);
}
/**
* Gets the rowID at index provided if the dataSource arrays were flattened,
* or null of out of range indexes.
*/
getRowIDForFlatIndex(index: number): ?string {
var accessIndex = index;
for (var ii = 0; ii < this.sectionIdentities.length; ii++) {
if (accessIndex >= this.rowIdentities[ii].length) {
accessIndex -= this.rowIdentities[ii].length;
} else {
return this.rowIdentities[ii][accessIndex];
}
}
return null;
}
/**
* Gets the sectionID at index provided if the dataSource arrays were flattened,
* or null for out of range indexes.
*/
getSectionIDForFlatIndex(index: number): ?string {
var accessIndex = index;
for (var ii = 0; ii < this.sectionIdentities.length; ii++) {
if (accessIndex >= this.rowIdentities[ii].length) {
accessIndex -= this.rowIdentities[ii].length;
} else {
return this.sectionIdentities[ii];
}
}
return null;
}
/**
* Returns an array containing the number of rows in each section
*/
getSectionLengths(): Array<number> {
var results = [];
for (var ii = 0; ii < this.sectionIdentities.length; ii++) {
results.push(this.rowIdentities[ii].length);
}
return results;
}
/**
* Returns if the section header is dirtied and needs to be rerendered
*/
sectionHeaderShouldUpdate(sectionIndex: number): bool {
var needsUpdate = this._dirtySections[sectionIndex];
warning(needsUpdate !== undefined,
'missing dirtyBit for section: ' + sectionIndex);
return needsUpdate;
}
/**
* Gets the data required to render the section header
*/
getSectionHeaderData(sectionIndex: number): any {
if (!this._getSectionHeaderData) {
return null;
}
var sectionID = this.sectionIdentities[sectionIndex];
warning(sectionID !== undefined,
'renderSection called on invalid section: ' + sectionIndex);
return this._getSectionHeaderData(this._dataBlob, sectionID);
}
/**
* Private members and methods.
*/
_getRowData: typeof defaultGetRowData;
_getSectionHeaderData: typeof defaultGetSectionHeaderData;
_rowHasChanged: differType;
_sectionHeaderHasChanged: ?differType;
_dataBlob: any;
_dirtyRows: Array<Array<bool>>;
_dirtySections: Array<bool>;
_cachedRowCount: number;
// These two 'protected' variables are accessed by ListView to iterate over
// the data in this class.
rowIdentities: Array<Array<string>>;
sectionIdentities: Array<string>;
_calculateDirtyArrays(
prevDataBlob: any,
prevSectionIDs: Array<string>,
prevRowIDs: Array<Array<string>>
): void {
// construct a hashmap of the existing (old) id arrays
var prevSectionsHash = keyedDictionaryFromArray(prevSectionIDs);
var prevRowsHash = {};
for (var ii = 0; ii < prevRowIDs.length; ii++) {
var sectionID = prevSectionIDs[ii];
warning(
!prevRowsHash[sectionID],
'SectionID appears more than once: ' + sectionID
);
prevRowsHash[sectionID] = keyedDictionaryFromArray(prevRowIDs[ii]);
}
// compare the 2 identity array and get the dirtied rows
this._dirtySections = [];
this._dirtyRows = [];
var dirty;
for (var sIndex = 0; sIndex < this.sectionIdentities.length; sIndex++) {
var sectionID = this.sectionIdentities[sIndex];
// dirty if the sectionHeader is new or _sectionHasChanged is true
dirty = !prevSectionsHash[sectionID];
var sectionHeaderHasChanged = this._sectionHeaderHasChanged;
if (!dirty && sectionHeaderHasChanged) {
dirty = sectionHeaderHasChanged(
this._getSectionHeaderData(prevDataBlob, sectionID),
this._getSectionHeaderData(this._dataBlob, sectionID)
);
}
this._dirtySections.push(!!dirty);
this._dirtyRows[sIndex] = [];
for (var rIndex = 0; rIndex < this.rowIdentities[sIndex].length; rIndex++) {
var rowID = this.rowIdentities[sIndex][rIndex];
// dirty if the section is new, row is new or _rowHasChanged is true
dirty =
!prevSectionsHash[sectionID] ||
!prevRowsHash[sectionID][rowID] ||
this._rowHasChanged(
this._getRowData(prevDataBlob, sectionID, rowID),
this._getRowData(this._dataBlob, sectionID, rowID)
);
this._dirtyRows[sIndex].push(!!dirty);
}
}
}
}
function countRows(allRowIDs) {
var totalRows = 0;
for (var sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) {
var rowIDs = allRowIDs[sectionIdx];
totalRows += rowIDs.length;
}
return totalRows;
}
function keyedDictionaryFromArray(arr) {
if (isEmpty(arr)) {
return {};
}
var result = {};
for (var ii = 0; ii < arr.length; ii++) {
var key = arr[ii];
warning(!result[key], 'Value appears more than once in array: ' + key);
result[key] = true;
}
return result;
}
module.exports = ListViewDataSource;

View File

@@ -0,0 +1,22 @@
import { PropTypes } from 'react'
import ScrollView from '../ScrollView'
import ListViewDataSource from './ListViewDataSource'
export default {
...ScrollView.propTypes,
dataSource: PropTypes.instanceOf(ListViewDataSource).isRequired,
renderSeparator: PropTypes.func,
renderRow: PropTypes.func.isRequired,
initialListSize: PropTypes.number,
onEndReached: PropTypes.func,
onEndReachedThreshold: PropTypes.number,
pageSize: PropTypes.number,
renderFooter: PropTypes.func,
renderHeader: PropTypes.func,
renderSectionHeader: PropTypes.func,
renderScrollComponent: PropTypes.func.isRequired,
scrollRenderAheadDistance: PropTypes.number,
onChangeVisibleRows: PropTypes.func,
removeClippedSubviews: PropTypes.bool,
stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number)
}

View File

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

View File

@@ -1,21 +1,104 @@
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
import React, { Component, PropTypes } from 'react'
import applyNativeMethods from '../../modules/applyNativeMethods'
import React, { Component } from 'react'
import ScrollView from '../ScrollView'
import ListViewDataSource from './ListViewDataSource'
import ListViewPropTypes from './ListViewPropTypes'
@NativeMethodsDecorator
export default class ListView extends Component {
static propTypes = {
children: PropTypes.any,
style: ScrollView.propTypes.style
};
const SCROLLVIEW_REF = 'listviewscroll'
class ListView extends Component {
static propTypes = ListViewPropTypes;
static defaultProps = {
style: {}
initialListSize: 10,
pageSize: 1,
renderScrollComponent: (props) => <ScrollView {...props} />,
scrollRenderAheadDistance: 1000,
onEndReachedThreshold: 1000,
stickyHeaderIndices: []
};
static DataSource = ListViewDataSource;
constructor(props) {
super(props)
this.state = {
curRenderedRowsCount: this.props.initialListSize,
highlightedRow: {}
}
this.onRowHighlighted = (sectionId, rowId) => this._onRowHighlighted(sectionId, rowId)
}
getScrollResponder() {
return this.refs[SCROLLVIEW_REF] && this.refs[SCROLLVIEW_REF].getScrollResponder()
}
scrollTo(...args) {
return this.refs[SCROLLVIEW_REF] && this.refs[SCROLLVIEW_REF].scrollTo(...args)
}
setNativeProps(props) {
return this.refs[SCROLLVIEW_REF] && this.refs[SCROLLVIEW_REF].setNativeProps(props)
}
_onRowHighlighted(sectionId, rowId) {
this.setState({highlightedRow: {sectionId, rowId}})
}
render() {
return (
<ScrollView {...this.props} />
)
const dataSource = this.props.dataSource
const header = this.props.renderHeader ? this.props.renderHeader() : undefined
const footer = this.props.renderFooter ? this.props.renderFooter() : undefined
// render sections and rows
const children = []
const sections = dataSource.rowIdentities
const renderRow = this.props.renderRow
const renderSectionHeader = this.props.renderSectionHeader
const renderSeparator = this.props.renderSeparator
for (let sectionIdx = 0, sectionCnt = sections.length; sectionIdx < sectionCnt; sectionIdx++) {
const rows = sections[sectionIdx]
const sectionId = dataSource.sectionIdentities[sectionIdx]
// render optional section header
if (renderSectionHeader) {
const section = dataSource.getSectionHeaderData(sectionIdx)
const key = 's_' + sectionId
const child = <div key={key}>{renderSectionHeader(section, sectionId)}</div>
children.push(child)
}
// render rows
for (let rowIdx = 0, rowCnt = rows.length; rowIdx < rowCnt; rowIdx++) {
const rowId = rows[rowIdx]
const row = dataSource.getRowData(sectionIdx, rowIdx)
const key = 'r_' + sectionId + '_' + rowId
const child = <div key={key}>{renderRow(row, sectionId, rowId, this.onRowHighlighted)}</div>
children.push(child)
// render optional separator
if (renderSeparator && ((rowIdx !== rows.length - 1) || (sectionIdx === sections.length - 1))) {
const adjacentRowHighlighted =
this.state.highlightedRow.sectionID === sectionId && (
this.state.highlightedRow.rowID === rowId ||
this.state.highlightedRow.rowID === rows[rowIdx + 1])
const separator = renderSeparator(sectionId, rowId, adjacentRowHighlighted)
children.push(separator)
}
}
}
const {
renderScrollComponent,
...props
} = this.props
return React.cloneElement(renderScrollComponent(props), {
ref: SCROLLVIEW_REF
}, header, children, footer)
}
}
applyNativeMethods(ListView)
module.exports = ListView

View File

@@ -1,156 +0,0 @@
/**
* Copyright 2015-present, Nicolas Gallagher
* Copyright 2004-present, Facebook Inc.
* All Rights Reserved.
*
* @flow
*/
import invariant from 'fbjs/lib/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
}
})

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import debounce from 'lodash/debounce'
import React, { Component, PropTypes } from 'react'
import View from '../View'
/**
* Encapsulates the Web-specific scroll throttling and disabling logic
*/
export default class ScrollViewBase extends Component {
static propTypes = {
...View.propTypes,
onMomentumScrollBegin: PropTypes.func,
onMomentumScrollEnd: PropTypes.func,
onScroll: PropTypes.func,
onScrollBeginDrag: PropTypes.func,
onScrollEndDrag: PropTypes.func,
onTouchMove: PropTypes.func,
onWheel: PropTypes.func,
scrollEnabled: PropTypes.bool,
scrollEventThrottle: PropTypes.number
};
static defaultProps = {
scrollEnabled: true
};
constructor(props) {
super(props)
this._debouncedOnScrollEnd = debounce(this._handleScrollEnd, 100)
this._state = { isScrolling: false }
}
_handlePreventableScrollEvent = (handler) => {
return (e) => {
if (!this.props.scrollEnabled) {
e.preventDefault()
} else {
if (handler) handler(e)
}
}
}
_handleScroll = (e) => {
const { scrollEventThrottle } = this.props
// A scroll happened, so the scroll bumps the debounce.
this._debouncedOnScrollEnd(e)
if (this._state.isScrolling) {
// Scroll last tick may have changed, check if we need to notify
if (this._shouldEmitScrollEvent(this._state.scrollLastTick, scrollEventThrottle)) {
this._handleScrollTick(e)
}
} else {
// Weren't scrolling, so we must have just started
this._handleScrollStart(e)
}
}
_handleScrollStart(e) {
this._state.isScrolling = true
this._state.scrollLastTick = Date.now()
}
_handleScrollTick(e) {
const { onScroll } = this.props
this._state.scrollLastTick = Date.now()
if (onScroll) onScroll(e)
}
_handleScrollEnd(e) {
const { onScroll } = this.props
this._state.isScrolling = false
if (onScroll) onScroll(e)
}
_shouldEmitScrollEvent(lastTick, eventThrottle) {
const timeSinceLastTick = Date.now() - lastTick
return (eventThrottle > 0 && timeSinceLastTick >= (1000 / eventThrottle))
}
render() {
const {
onMomentumScrollBegin, onMomentumScrollEnd, onScrollBeginDrag, onScrollEndDrag, scrollEnabled, scrollEventThrottle, // eslint-disable-line
...other
} = this.props
return (
<View
{...other}
onScroll={this._handleScroll}
onTouchMove={this._handlePreventableScrollEvent(this.props.onTouchMove)}
onWheel={this._handlePreventableScrollEvent(this.props.onWheel)}
/>
)
}
}

View File

@@ -1,130 +1,233 @@
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
import debounce from 'lodash.debounce'
import React, { Component, PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
import View from '../View'
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
@NativeMethodsDecorator
export default class ScrollView extends Component {
static propTypes = {
children: PropTypes.any,
contentContainerStyle: View.propTypes.style,
import dismissKeyboard from '../../modules/dismissKeyboard'
import invariant from 'fbjs/lib/invariant'
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ScrollResponder from '../../modules/ScrollResponder'
import ScrollViewBase from './ScrollViewBase'
import StyleSheet from '../../apis/StyleSheet'
import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
import View from '../View'
import ViewStylePropTypes from '../View/ViewStylePropTypes'
const INNERVIEW = 'InnerScrollView'
const SCROLLVIEW = 'ScrollView'
const ScrollView = React.createClass({
propTypes: {
...View.propTypes,
children: View.propTypes.children,
contentContainerStyle: StyleSheetPropType(ViewStylePropTypes),
horizontal: PropTypes.bool,
keyboardDismissMode: PropTypes.oneOf([ 'none', 'interactive', 'on-drag' ]),
onContentSizeChange: PropTypes.func,
onScroll: PropTypes.func,
refreshControl: PropTypes.element,
scrollEnabled: PropTypes.bool,
scrollEventThrottle: PropTypes.number,
style: View.propTypes.style
};
style: StyleSheetPropType(ViewStylePropTypes)
},
static defaultProps = {
contentContainerStyle: {},
horizontal: false,
scrollEnabled: true,
scrollEventThrottle: 0,
style: {}
};
mixins: [ScrollResponder.Mixin],
constructor(...args) {
super(...args)
this._debouncedOnScrollEnd = debounce(this._onScrollEnd, 100)
this.state = {
isScrolling: false
}
}
getInitialState() {
return this.scrollResponderMixinGetInitialState()
},
_onScroll(e) {
const { scrollEventThrottle } = this.props
const { isScrolling, scrollLastTick } = this.state
setNativeProps(props: Object) {
this.refs[SCROLLVIEW].setNativeProps(props)
},
// A scroll happened, so the scroll bumps the debounce.
this._debouncedOnScrollEnd(e)
/**
* Returns a reference to the underlying scroll responder, which supports
* operations like `scrollTo`. All ScrollView-like components should
* implement this method so that they can be composed while providing access
* to the underlying scroll responder's methods.
*/
getScrollResponder(): Component {
return this
},
if (isScrolling) {
// Scroll last tick may have changed, check if we need to notify
if (this._shouldEmitScrollEvent(scrollLastTick, scrollEventThrottle)) {
this._onScrollTick(e)
}
getScrollableNode(): any {
return ReactDOM.findDOMNode(this.refs[SCROLLVIEW])
},
getInnerViewNode(): any {
return ReactDOM.findDOMNode(this.refs[INNERVIEW])
},
/**
* Scrolls to a given x, y offset, either immediately or with a smooth animation.
* Syntax:
*
* scrollTo(options: {x: number = 0; y: number = 0; animated: boolean = true})
*
* Note: The weird argument signature is due to the fact that, for historical reasons,
* the function also accepts separate arguments as as alternative to the options object.
* This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
*/
scrollTo(
y?: number | { x?: number, y?: number, animated?: boolean },
x?: number,
animated?: boolean
) {
if (typeof y === 'number') {
console.warn('`scrollTo(y, x, animated)` is deprecated. Use `scrollTo({x: 5, y: 5, animated: true})` instead.')
} else {
// Weren't scrolling, so we must have just started
this._onScrollStart(e)
({x, y, animated} = y || {})
}
}
_onScrollStart() {
this.setState({
isScrolling: true,
scrollLastTick: Date.now()
})
}
this.getScrollResponder().scrollResponderScrollTo({x: x || 0, y: y || 0, animated: animated !== false})
},
_onScrollTick(e) {
const { onScroll } = this.props
this.setState({
scrollLastTick: Date.now()
})
if (onScroll) onScroll(e)
}
/**
* Deprecated, do not use.
*/
scrollWithoutAnimationTo(y: number = 0, x: number = 0) {
console.warn('`scrollWithoutAnimationTo` is deprecated. Use `scrollTo` instead')
this.scrollTo({x, y, animated: false})
},
_onScrollEnd(e) {
const { onScroll } = this.props
this.setState({
isScrolling: false
})
if (onScroll) onScroll(e)
}
handleScroll(e: Object) {
if (process.env.NODE_ENV !== 'production') {
if (this.props.onScroll && !this.props.scrollEventThrottle) {
console.log(
'You specified `onScroll` on a <ScrollView> but not ' +
'`scrollEventThrottle`. You will only receive one event. ' +
'Using `16` you get all the events but be aware that it may ' +
'cause frame drops, use a bigger number if you don\'t need as ' +
'much precision.'
)
}
}
_shouldEmitScrollEvent(lastTick, eventThrottle) {
const timeSinceLastTick = Date.now() - lastTick
return (eventThrottle > 0 && timeSinceLastTick >= (1000 / eventThrottle))
}
if (this.props.keyboardDismissMode === 'on-drag') {
dismissKeyboard()
}
_maybePreventScroll(e) {
const { scrollEnabled } = this.props
if (!scrollEnabled) e.preventDefault()
}
this.scrollResponderHandleScroll(e)
},
_handleContentOnLayout(e: Object) {
const { width, height } = e.nativeEvent.layout
this.props.onContentSizeChange && this.props.onContentSizeChange(width, height)
},
render() {
const {
children,
contentContainerStyle,
horizontal,
style
keyboardDismissMode, // eslint-disable-line
onContentSizeChange,
onScroll, // eslint-disable-line
refreshControl,
...other
} = this.props
return (
if (process.env.NODE_ENV !== 'production' && this.props.style) {
const style = StyleSheet.flatten(this.props.style)
const childLayoutProps = ['alignItems', 'justifyContent'].filter((prop) => style && style[prop] !== undefined)
invariant(
childLayoutProps.length === 0,
'ScrollView child layout (' + JSON.stringify(childLayoutProps) +
') must be applied through the contentContainerStyle prop.'
)
}
let contentSizeChangeProps = {}
if (onContentSizeChange) {
contentSizeChangeProps = {
onLayout: this._handleContentOnLayout
}
}
const contentContainer = (
<View
onScroll={(e) => this._onScroll(e)}
onTouchMove={(e) => this._maybePreventScroll(e)}
onWheel={(e) => this._maybePreventScroll(e)}
{...contentSizeChangeProps}
children={this.props.children}
collapsable={false}
ref={INNERVIEW}
style={[
styles.initial,
style
styles.contentContainer,
horizontal && styles.contentContainerHorizontal,
contentContainerStyle
]}
>
{children ? (
<View
children={children}
style={[
styles.initialContentContainer,
contentContainerStyle,
horizontal && styles.row
]}
/>
) : null}
</View>
/>
)
const props = {
...other,
style: [
styles.base,
horizontal && styles.baseHorizontal,
this.props.style
],
onTouchStart: this.scrollResponderHandleTouchStart,
onTouchMove: this.scrollResponderHandleTouchMove,
onTouchEnd: this.scrollResponderHandleTouchEnd,
onScrollBeginDrag: this.scrollResponderHandleScrollBeginDrag,
onScrollEndDrag: this.scrollResponderHandleScrollEndDrag,
onMomentumScrollBegin: this.scrollResponderHandleMomentumScrollBegin,
onMomentumScrollEnd: this.scrollResponderHandleMomentumScrollEnd,
onStartShouldSetResponder: this.scrollResponderHandleStartShouldSetResponder,
onStartShouldSetResponderCapture: this.scrollResponderHandleStartShouldSetResponderCapture,
onScrollShouldSetResponder: this.scrollResponderHandleScrollShouldSetResponder,
onScroll: this.handleScroll,
onResponderGrant: this.scrollResponderHandleResponderGrant,
onResponderTerminationRequest: this.scrollResponderHandleTerminationRequest,
onResponderTerminate: this.scrollResponderHandleTerminate,
onResponderRelease: this.scrollResponderHandleResponderRelease,
onResponderReject: this.scrollResponderHandleResponderReject
}
const ScrollViewClass = ScrollViewBase
invariant(
ScrollViewClass !== undefined,
'ScrollViewClass must not be undefined'
)
if (refreshControl) {
return React.cloneElement(
refreshControl,
{ style: props.style },
<ScrollViewClass {...props} ref={SCROLLVIEW} style={styles.base}>
{contentContainer}
</ScrollViewClass>
)
}
return (
<ScrollViewClass {...props} ref={SCROLLVIEW} style={props.style}>
{contentContainer}
</ScrollViewClass>
)
}
}
})
const styles = StyleSheet.create({
initial: {
base: {
flex: 1,
overflow: 'auto'
overflowX: 'hidden',
overflowY: 'auto'
},
initialContentContainer: {
baseHorizontal: {
overflowX: 'auto',
overflowY: 'hidden'
},
contentContainer: {
flex: 1
},
row: {
contentContainerHorizontal: {
flexDirection: 'row'
}
})
module.exports = ScrollView

View File

@@ -23,7 +23,7 @@ import React, { Component, PropTypes } from 'react'
* Typically, you will not need to use this component and should opt for normal
* React reconciliation.
*/
export default class StaticContainer extends Component {
class StaticContainer extends Component {
static propTypes = {
children: PropTypes.any.isRequired,
shouldUpdate: PropTypes.bool.isRequired
@@ -38,3 +38,5 @@ export default class StaticContainer extends Component {
return (child === null || child === false) ? null : React.Children.only(child)
}
}
module.exports = StaticContainer

View File

@@ -22,7 +22,7 @@ import { Component, PropTypes } from 'react'
* React reconciliation.
*/
export default class StaticRenderer extends Component {
class StaticRenderer extends Component {
static propTypes = {
render: PropTypes.func.isRequired,
shouldUpdate: PropTypes.bool.isRequired
@@ -36,3 +36,5 @@ export default class StaticRenderer extends Component {
return this.props.render()
}
}
module.exports = StaticRenderer

View File

@@ -1,11 +1,11 @@
import { PropTypes } from 'react'
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
import ColorPropType from '../../propTypes/ColorPropType'
import ViewStylePropTypes from '../View/ViewStylePropTypes'
const { number, oneOf, oneOfType, string } = PropTypes
const numberOrString = oneOfType([ number, string ])
export default {
module.exports = {
...ViewStylePropTypes,
color: ColorPropType,
fontFamily: string,
@@ -15,14 +15,17 @@ export default {
letterSpacing: numberOrString,
lineHeight: numberOrString,
textAlign: oneOf([ 'center', 'inherit', 'justify', 'justify-all', 'left', 'right' ]),
/**
* @platform web
*/
textDecoration: string,
textAlignVertical: oneOf([ 'auto', 'bottom', 'center', 'top' ]),
textDecorationLine: string,
/* @platform web */
textOverflow: string,
/* @platform web */
textShadow: string,
/* @platform web */
textTransform: oneOf([ 'capitalize', 'lowercase', 'none', 'uppercase' ]),
/* @platform web */
whiteSpace: string,
/* @platform web */
wordWrap: string,
writingDirection: string
writingDirection: oneOf([ 'auto', 'ltr', 'rtl' ])
}

View File

@@ -1,51 +1,34 @@
/* eslint-env mocha */
import * as utils from '../../../modules/specHelpers'
import assert from 'assert'
import React from 'react'
import ReactTestUtils from 'react-addons-test-utils'
import Text from '../'
import { mount, shallow } from 'enzyme'
suite('components/Text', () => {
test('prop "accessibilityLabel"', () => {
const accessibilityLabel = 'accessibilityLabel'
const result = utils.shallowRender(<Text accessibilityLabel={accessibilityLabel} />)
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
})
test('prop "accessibilityRole"', () => {
const accessibilityRole = 'accessibilityRole'
const result = utils.shallowRender(<Text accessibilityRole={accessibilityRole} />)
assert.equal(result.props.accessibilityRole, accessibilityRole)
})
test('prop "accessible"', () => {
const accessible = false
const result = utils.shallowRender(<Text accessible={accessible} />)
assert.equal(result.props.accessible, accessible)
})
test('prop "children"', () => {
const children = 'children'
const result = utils.shallowRender(<Text>{children}</Text>)
assert.equal(result.props.children, children)
const text = shallow(<Text>{children}</Text>)
assert.equal(text.prop('children'), children)
})
test('prop "numberOfLines"')
test('prop "onLayout"', (done) => {
mount(<Text onLayout={onLayout} />)
function onLayout(e) {
const { layout } = e.nativeEvent
assert.deepEqual(layout, { x: 0, y: 0, width: 0, height: 0 })
done()
}
})
test('prop "onPress"', (done) => {
const dom = utils.renderToDOM(<Text onPress={onPress} />)
ReactTestUtils.Simulate.click(dom)
const text = mount(<Text onPress={onPress} />)
text.simulate('click')
function onPress(e) {
assert.ok(e.nativeEvent)
done()
}
})
test('prop "testID"', () => {
const testID = 'testID'
const result = utils.shallowRender(<Text testID={testID} />)
assert.equal(result.props.testID, testID)
})
})

View File

@@ -1,54 +1,58 @@
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
import CoreComponent from '../CoreComponent'
import React, { Component, PropTypes } from 'react'
import applyLayout from '../../modules/applyLayout'
import applyNativeMethods from '../../modules/applyNativeMethods'
import createReactDOMComponent from '../../modules/createReactDOMComponent'
import { Component, PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
import TextStylePropTypes from './TextStylePropTypes'
@NativeMethodsDecorator
export default class Text extends Component {
class Text extends Component {
static displayName = 'Text'
static propTypes = {
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
accessibilityRole: CoreComponent.propTypes.accessibilityRole,
accessible: CoreComponent.propTypes.accessible,
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
accessibilityRole: createReactDOMComponent.propTypes.accessibilityRole,
accessible: createReactDOMComponent.propTypes.accessible,
children: PropTypes.any,
numberOfLines: PropTypes.number,
onLayout: PropTypes.func,
onPress: PropTypes.func,
style: StyleSheetPropType(TextStylePropTypes),
testID: CoreComponent.propTypes.testID
testID: createReactDOMComponent.propTypes.testID
};
static defaultProps = {
accessible: true
};
_onPress(e) {
if (this.props.onPress) this.props.onPress(e)
}
render() {
const {
numberOfLines,
onPress,
onLayout, // eslint-disable-line
onPress, // eslint-disable-line
style,
...other
} = this.props
return (
<CoreComponent
{...other}
component='span'
onClick={this._onPress.bind(this)}
style={[
styles.initial,
style,
numberOfLines === 1 && styles.singleLineStyle
]}
/>
)
return createReactDOMComponent({
...other,
component: 'span',
onClick: this._onPress,
style: [
styles.initial,
style,
numberOfLines === 1 && styles.singleLineStyle
]
})
}
_onPress = (e) => {
if (this.props.onPress) this.props.onPress(e)
}
}
applyLayout(applyNativeMethods(Text))
const styles = StyleSheet.create({
initial: {
color: 'inherit',
@@ -56,7 +60,7 @@ const styles = StyleSheet.create({
font: 'inherit',
margin: 0,
padding: 0,
textDecoration: 'none',
textDecorationLine: 'none',
wordWrap: 'break-word'
},
singleLineStyle: {
@@ -66,3 +70,5 @@ const styles = StyleSheet.create({
whiteSpace: 'nowrap'
}
})
module.exports = Text

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import UIManager from '../../apis/UIManager'
/**
* This class is responsible for coordinating the "focused"
* state for TextInputs. All calls relating to the keyboard
* should be funneled through here
*/
const TextInputState = {
/**
* Internal state
*/
_currentlyFocusedNode: (null: ?Object),
/**
* Returns the ID of the currently focused text field, if one exists
* If no text field is focused it returns null
*/
currentlyFocusedField(): ?Object {
return this._currentlyFocusedNode
},
/**
* @param {Object} TextInputID id of the text field to focus
* Focuses the specified text field
* noop if the text field was already focused
*/
focusTextInput(textFieldNode: ?Object) {
if (this._currentlyFocusedNode !== textFieldNode && textFieldNode !== null) {
this._currentlyFocusedNode = textFieldNode
UIManager.focus(textFieldNode)
}
},
/**
* @param {Object} textFieldNode id of the text field to focus
* Unfocuses the specified text field
* noop if it wasn't focused
*/
blurTextInput(textFieldNode: ?Object) {
if (this._currentlyFocusedNode === textFieldNode && textFieldNode !== null) {
this._currentlyFocusedNode = null
UIManager.blur(textFieldNode)
}
}
}
module.exports = TextInputState

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