Compare commits

...

76 Commits

Author SHA1 Message Date
Nicolas Gallagher
a94367bdcb 0.0.68 2017-01-11 13:12:25 -08:00
Nicolas Gallagher
65febbbc52 [fix] error in setNativeProps when no style
Close #325
Close #327
2017-01-11 13:08:10 -08:00
Nicolas Gallagher
b14d2e5bd8 [fix] CSS pointer event selectors 2017-01-11 12:47:24 -08:00
Nicolas Gallagher
7c83ba162d 0.0.67 2017-01-08 18:40:02 -08:00
Nicolas Gallagher
3ffc005a7b [fix] setNativeProps resolving logic
Since styles are set using both class names and inline styles,
'setNativeProps' needs an additional resolving step that accounts for
the pre-existing state of RN-managed styles on the DOM node.

Fix #321
2017-01-08 18:25:39 -08:00
Nicolas Gallagher
50a70ad02f 0.0.66 2017-01-07 19:05:54 -08:00
Nicolas Gallagher
768e895701 [fix] View transforms; add perspective styles
Fixes a regression introduced by
5db300df35

The `perspective` function is distinct from the `perspective` property.
This patch reverts the regression and adds support for `perspective`,
`perspectiveOrigin`, and `transformOrigin`.

Fix #208
2017-01-07 19:02:57 -08:00
Nicolas Gallagher
af5fde994d Fix yarn.lock 2017-01-07 18:25:54 -08:00
Paul Le Cam
c3d0763944 [fix] ListView imports and 'getRowAndSectionCount'
Close #316
2017-01-07 18:22:47 -08:00
Nicolas Gallagher
0aba506725 Fix UIManager tests 2017-01-07 18:18:56 -08:00
Nicolas Gallagher
91032d8565 [change] allow 'display' in ViewStylePropTypes
Fix #296
2017-01-07 18:15:16 -08:00
Nicolas Gallagher
0696721488 Document transition and animation View styles 2017-01-07 18:12:26 -08:00
Nicolas Gallagher
fe18830ce6 [change] use classList in UIManager
Prevent setting the same class multiple times
2017-01-07 18:03:51 -08:00
Nicolas Gallagher
1b86d02300 [change] wrap layout measurement in 'asap' 2017-01-07 18:03:03 -08:00
Nicolas Gallagher
c56b472258 [change] depend on normalize-css-color
Fix #308
2017-01-07 18:00:17 -08:00
Nicolas Gallagher
b00132f007 [fix] TouchableOpacity transition duration 2017-01-07 17:50:59 -08:00
Nicolas Gallagher
8b8f8f0374 [fix] CSS properties that support unitless numbers 2017-01-05 14:47:10 -08:00
Nicolas Gallagher
8e94af34e1 Remove use of 'keyOf' 2017-01-05 14:47:10 -08:00
Nicolas Gallagher
7ffaf592d5 [fix] Text automatic writing direction detection 2017-01-05 14:47:08 -08:00
Nicolas Gallagher
a1017fa785 0.0.65 2017-01-04 18:25:39 -08:00
Nicolas Gallagher
5db300df35 [fix] transform perspective resolution 2017-01-04 18:04:15 -08:00
Nicolas Gallagher
214d862e61 [add] ScrollView to Animated 2017-01-04 18:04:05 -08:00
Nicolas Gallagher
4ef5453b33 0.0.64 2017-01-04 10:58:15 -08:00
Nicolas Gallagher
a27671d7cf [fix] passing on RN style props in createDOMElement
The 'createDOMElement' function wasn't pulling 'style' out of the
props. A change to the logic that sets DOM props meant that if
'StyleRegistry.resolve' didn't return a 'style' object, the React Native
styles would be passed through to the underlying DOM node.

Fix #315
2017-01-04 10:43:19 -08:00
Nicolas Gallagher
8d2a650670 [fix] StyleSheet selector escaping
Values that contain '*' (e.g. 'calc(2 * 2)') were not properly escaped,
resulting in broken selectors.
2017-01-03 14:25:39 -08:00
Nicolas Gallagher
4cc1c983e8 0.0.63 2017-01-02 23:44:10 -08:00
Nicolas Gallagher
37413fd55f Update Text snapshot 2017-01-02 23:43:39 -08:00
Nicolas Gallagher
07ff0ea104 Use setNativeProps to update ProgressBar 2017-01-02 23:28:14 -08:00
Nicolas Gallagher
1a87657500 [fix] Switch thumb positioning 2017-01-02 23:25:15 -08:00
Nicolas Gallagher
5e4c8e520a [fix] StyleSheet registry key check 2017-01-02 23:24:24 -08:00
Nicolas Gallagher
236121e32c Add unregistered styles benchmark 2017-01-02 22:46:38 -08:00
Nicolas Gallagher
39c76ca50c Minor StyleSheet/injector refactor; small fixes 2017-01-02 15:07:05 -08:00
Nicolas Gallagher
e65f91d849 [change] simplify CSS generation 2017-01-02 13:15:38 -08:00
Nicolas Gallagher
a535c558d8 [change] Move 'onResponderRelease' cancel to 'Touchable'
Moves a fix for double-firing Touchables into 'Touchable'
2017-01-02 13:12:13 -08:00
Nicolas Gallagher
a74be91b7c Minor documentation changes 2017-01-02 11:27:46 -08:00
Nicolas Gallagher
8eaaf28a32 0.0.62 2017-01-01 20:45:45 -08:00
Nicolas Gallagher
16d448dc5b Update various dependencies 2017-01-01 20:45:14 -08:00
Nicolas Gallagher
ea75cced13 [change] ProgressBar using CSS animations
Fix #299
2017-01-01 20:22:42 -08:00
Nicolas Gallagher
cfc56a1354 [change] ActivityIndicator using CSS animations
Ref #299
2017-01-01 19:48:50 -08:00
Nicolas Gallagher
b1cd92a65d [change] update Animated
Fix #309
2017-01-01 18:35:18 -08:00
Nicolas Gallagher
d87f71ebc1 [change] StyleSheet performance rewrite
Improves StyleSheet benchmark performance by 13x. Removes undocumented
`StyleSheet.resolve` API.

Typical mean (and first) task duration.

[benchmark] DeepTree: depth=3, breadth=10, wrap=4)
  -master  2809.76ms (3117.64ms)
  -patch     211.2ms  (364.28ms)

[benchmark] DeepTree: depth=5, breadth=3, wrap=1)
  -master   421.25ms (428.15ms)
  -patch     32.46ms  (47.36ms)

This patch adds memoization of DOM prop resolution (~3-4x faster), and
re-introduces a `className`-based styling strategy (~3-4x faster).
Styles map to "atomic css" rules.

Fix #307
2017-01-01 17:42:25 -08:00
Nicolas Gallagher
a2cafe56fc Add initial performance benchmarks
Simple render tree benchmarks originally developed by @lelandrichardson

Fix #306
2017-01-01 14:43:47 -08:00
Nicolas Gallagher
351c0ac3d4 Minor changes 2016-12-30 22:19:51 -08:00
Nicolas Gallagher
877c0d2818 Simplify StyleSheet's expandStyle 2016-12-30 22:14:39 -08:00
Nicolas Gallagher
3afc5d5de6 Rename style processors to resolvers 2016-12-29 19:17:12 -08:00
Nicolas Gallagher
edf3b9b7ff Move 'flattenStyle' into 'StyleSheet' 2016-12-29 16:31:14 -08:00
Nicolas Gallagher
518a85bf1b Rename 'createReactStyleObject' to 'createReactDOMStyle' 2016-12-29 16:26:31 -08:00
vaukalak
ba75acb66a [add] BackAndroid API stub 2016-12-27 18:14:31 +00:00
Nicolas Gallagher
bc68b0b6f4 0.0.61 2016-12-27 18:09:11 +00:00
Nicolas Gallagher
44ecbc072e [change] update React and Touchables
Update to React@15.4. The 'EventConstants' module no longer exports a
key-mirror, which was preventing the 'ResponderEventPlugin' from working
as it did with React@15.3.

Close #255
2016-12-27 17:57:27 +00:00
Nicolas Gallagher
4cf4905fc2 [change] add support for ShadowPropTypes
Fix #44
2016-12-26 13:57:19 +00:00
Nicolas Gallagher
509920be4b [add] Image 'prefetch' and 'getSize' statics
Fix #160
2016-12-26 13:31:40 +00:00
Gethin Webster
04e3c23e67 [fix] ListView updates rows when dataSource changes
Close #295
2016-12-23 12:51:40 +00:00
Nicolas Gallagher
32f454de66 [change] add Platform and Touchable to 'core' module 2016-12-23 12:45:54 +00:00
Nicolas Gallagher
1273bfc7cf Address avoidable object creation 2016-12-23 12:22:32 +00:00
Nicolas Gallagher
dc7f526f6b [fix] TextInput props
- Add missing 'onSubmitEditing' propType and test
- Add 'dir=auto' DOM attribute to allow browser to switch writing
  direction for RTL languages
2016-12-17 23:38:36 +00:00
Nicolas Gallagher
7cda89c5ce 0.0.60 2016-12-16 12:15:23 +00:00
Nicolas Gallagher
695eba45af [add] Clipboard API
Close #125
Fix #122
2016-12-16 11:59:22 +00:00
Nicolas Gallagher
92a2cb274a [fix] remove TextInput default flex value 2016-12-16 11:39:12 +00:00
Nicolas Gallagher
b1ca04d11e Rename I18nManager example 2016-12-16 11:35:11 +00:00
Nicolas Gallagher
22ab70ea6f 0.0.59 2016-12-14 17:42:15 +00:00
Gethin Webster
49f36d8eb1 Update to ListView functionality
Re-build ListView from the core react-native component, to get better
feature parity

Ensure lists with small initialListSize render correctly

Changes as requested via PR
2016-12-14 09:39:05 -08:00
Nicolas Gallagher
80ba119b83 Update install command
Close #285
Close #286
2016-12-14 17:05:15 +00:00
Nicolas Gallagher
c30b67f6db 0.0.57 2016-12-12 15:10:39 +00:00
Nicolas Gallagher
4580f93199 Use fbjs requestAnimationFrame in Image 2016-12-12 14:26:07 +00:00
Nicolas Gallagher
4c46126ffe [change] ScrollView event normalization 2016-12-12 14:21:33 +00:00
Calvin Chan
f8d5c15405 [add] Button component 2016-12-12 13:02:16 +00:00
Nicolas Gallagher
dc54e03625 [add] Linking API
Adds support for opening external URLs in a new tab/window. Includes
patches to 'Text' to improve accessibility and 'createDOMElement' to
improve external link security.

Fix #198
2016-12-12 11:45:30 +00:00
Nicolas Gallagher
4d5819ae28 [fix] RTL translateX; Switch transition 2016-12-08 19:40:34 -08:00
Nicolas Gallagher
5c482ef3be 0.0.56 2016-12-08 18:29:38 -08:00
Nicolas Gallagher
f51592f96e [change] TouchableOpacity without Animated
Fix #259
2016-12-08 18:22:25 -08:00
Nicolas Gallagher
6bffe1775f Fix lint error 2016-12-07 16:49:53 -08:00
Nicolas Gallagher
7e75d037f2 [fix] Image passes unknown props to underlying View
Fix #267
2016-12-07 16:37:24 -08:00
Nicolas Gallagher
7536508fe3 Update docs 2016-12-07 16:22:39 -08:00
Maxime Thirouin
945fff0015 Add source files to published package 2016-11-25 12:38:29 -08:00
Nicolas Gallagher
5032ed6fe1 Update AppRegistry docs 2016-11-25 12:36:05 -08:00
141 changed files with 5109 additions and 3608 deletions

View File

@@ -13,5 +13,6 @@ https://github.com/necolas/react-native-web/CONTRIBUTING.md
- [ ] includes documentation
- [ ] includes tests
- [ ] includes benchmark reports
- [ ] includes an interactive example
- [ ] includes screenshots/videos

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/dist
/dist-examples
/dist-performance
/node_modules

View File

@@ -5,5 +5,4 @@ before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
script:
- npm run lint
- npm test

View File

@@ -1,7 +1,5 @@
# Contributing
We are open to, and grateful for, any contributions made by the community.
## Reporting Issues and Asking Questions
Before opening an issue, please search the [issue
@@ -31,6 +29,12 @@ Run the examples:
npm run examples
```
Run the benchmarks in a browser by opening `./performance/index.html` after running:
```
npm run build:performance
```
### Building
```
@@ -57,7 +61,7 @@ To continuously watch and run tests, run the following:
npm run test:watch
```
To perform linting, run the following:
To perform only linting, run the following:
```
npm run lint

View File

@@ -27,7 +27,7 @@ online with [React Native for Web: Playground](http://codepen.io/necolas/pen/PZz
To install in your app:
```
npm install --save react react-native-web
npm install --save react@15.4 react-native-web
```
Read the [Client and Server rendering](docs/guides/rendering.md) guide.
@@ -53,6 +53,7 @@ Exported modules:
* Components
* [`ActivityIndicator`](docs/components/ActivityIndicator.md)
* [`Button`](docs/components/Button.md)
* [`Image`](docs/components/Image.md)
* [`ListView`](docs/components/ListView.md)
* [`ProgressBar`](docs/components/ProgressBar.md)
@@ -69,6 +70,7 @@ Exported modules:
* [`AppRegistry`](docs/apis/AppRegistry.md)
* [`AppState`](docs/apis/AppState.md)
* [`AsyncStorage`](docs/apis/AsyncStorage.md)
* [`Clipboard`](docs/apis/Clipboard.md)
* [`Dimensions`](docs/apis/Dimensions.md)
* [`I18nManager`](docs/apis/I18nManager.md)
* [`NativeMethods`](docs/apis/NativeMethods.md)
@@ -80,6 +82,7 @@ Exported modules:
* [`Vibration`](docs/apis/Vibration.md)
<span id="#why"></span>
## Why?
There are many different teams at Twitter building web applications with React.
@@ -142,6 +145,7 @@ AppRegistry.runApplication('MyApp', { rootTag: document.getElementById('react-ro
* [react-native-web-player](https://github.com/dabbott/react-native-web-player)
* [react-web](https://github.com/taobaofed/react-web)
* [react-native-for-web](https://github.com/KodersLab/react-native-for-web)
* [rhinos-app](https://github.com/rhinos-app/rhinos-app-dev)
## License

View File

@@ -3,8 +3,7 @@
`AppRegistry` is the control point for registering, running, prerendering, and
unmounting all apps. App root components should register themselves with
`AppRegistry.registerComponent`. Apps can be run by invoking
`AppRegistry.runApplication`, and prerendered by invoking
`AppRegistry.prerenderApplication` (see the [client and server rendering
`AppRegistry.runApplication` (see the [client and server rendering
guide](../guides/rendering.md) for more details).
To "stop" an application when a view should be destroyed, call

16
docs/apis/Clipboard.md Normal file
View File

@@ -0,0 +1,16 @@
# Clipboard
Clipboard gives you an interface for setting to the clipboard. (Getting
clipboard content is not supported on web.)
## Methods
static **getString**()
Returns a `Promise` of an empty string.
static **setString**(content: string): boolean
Copies a string to the clipboard. On web, some browsers may not support copying
to the clipboard, therefore, this function returns a boolean to indicate if the
copy was successful.

View File

@@ -15,9 +15,9 @@ Each key of the object passed to `create` must define a style object.
Flattens an array of styles into a single style object.
**render**: function
**renderToString**: function
Returns a React `<style>` element for use in server-side rendering.
Returns a string of the stylesheet for use in server-side rendering.
## Properties

39
docs/components/Button.md Normal file
View File

@@ -0,0 +1,39 @@
# Button
A basic button component. Supports a minimal level of customization. You can
build your own custom button using `TouchableOpacity` or
`TouchableNativeFeedback`.
## Props
**accessibilityLabel**: string
Defines the text available to assistive technologies upon interaction with the
element. (This is implemented using `aria-label`.)
**color**: string
Background color of the button.
**disabled**: bool = false
If true, disable all interactions for this component
**onPress**: function
This function is called on press.
**title**: string
Text to display inside the button.
## Examples
```js
<Button
accessibilityLabel="Learn more about this purple button"
color="#841584"
onPress={onPressLearnMore}
title="Learn More"
/>
```

View File

@@ -75,6 +75,23 @@ Example usage:
<Image resizeMode={Image.resizeMode.contain} />
```
## Methods
static **getSize**(uri: string, success: (width, height) => {}, failure: function)
Retrieve the width and height (in pixels) of an image prior to displaying it.
This method can fail if the image cannot be found, or fails to download.
(In order to retrieve the image dimensions, the image may first need to be
loaded or downloaded, after which it will be cached. This means that in
principle you could use this method to preload images, however it is not
optimized for that purpose, and may in future be implemented in a way that does
not fully load/download the image data.)
static **prefetch**(url: string): Promise
Prefetches a remote image for later use by downloading it.
## Examples
```js

View File

@@ -38,6 +38,18 @@ which this `ScrollView` renders.
Fires at most once per frame during scrolling. The frequency of the events can
be contolled using the `scrollEventThrottle` prop.
Invoked on scroll with the following event:
```js
{
nativeEvent: {
contentOffset: { x, y },
contentSize: { height, width },
layoutMeasurement: { height, width }
}
}
```
**refreshControl**: element
TODO
@@ -51,8 +63,8 @@ When false, the content does not scroll.
**scrollEventThrottle**: number = 0
This controls how often the scroll event will be fired while scrolling (in
events per seconds). A higher number yields better accuracy for code that is
This controls how often the scroll event will be fired while scrolling (as a
time interval in ms). A lower number yields better accuracy for code that is
tracking the scroll position, but can lead to scroll performance problems. The
default value is `0`, which means the scroll event will be sent only once each
time the view is scrolled.
@@ -104,7 +116,7 @@ export default class ScrollViewExample extends Component {
contentContainerStyle={styles.container}
horizontal
onScroll={(e) => this.onScroll(e)}
scrollEventThrottle={60}
scrollEventThrottle={100}
style={styles.root}
/>
)

View File

@@ -99,6 +99,14 @@ from `style`.
+ `alignContent`
+ `alignItems`
+ `alignSelf`
+ `animationDelay`
+ `animationDirection`
+ `animationDuration`
+ `animationFillMode`
+ `animationIterationCount`
+ `animationName`
+ `animationPlayState`
+ `animationTimingFunction`
+ `backfaceVisibility`
+ `backgroundAttachment`
+ `backgroundClip`
@@ -164,10 +172,17 @@ from `style`.
+ `paddingRight`
+ `paddingTop`
+ `paddingVertical`
+ `perspective`
+ `perspectiveOrigin`
+ `position`
+ `right`
+ `top`
+ `transform`
+ `transformOrigin`
+ `transitionDelay`
+ `transitionDuration`
+ `transitionProperty`
+ `transitionTimingFunction`
+ `userSelect`
+ `visibility`
+ `width`

View File

@@ -4,7 +4,7 @@ It is sometimes necessary to make changes directly to a component without using
state/props to trigger a re-render of the entire subtree in the browser, this
is done by directly modifying a DOM node. `setNativeProps` is the React Native
equivalent to setting properties directly on a DOM node. Use direct
manipulation when frequent re-rendering creates a performance bottleneck Direct
manipulation when frequent re-rendering creates a performance bottleneck. Direct
manipulation will not be a tool that you reach for frequently.
## `setNativeProps` and `shouldComponentUpdate`

View File

@@ -27,14 +27,48 @@ the `url-loader` to the webpack config:
module.exports = {
// ...
module: {
loaders: {
test: /\.(gif|jpe?g|png|svg)$/,
loader: 'url-loader',
query: { name: '[name].[hash:16].[ext]' }
}
loaders: [
{
test: /\.(gif|jpe?g|png|svg)$/,
loader: 'url-loader',
query: { name: '[name].[hash:16].[ext]' }
}
]
}
};
```
## Dependencies
Many OSS React Native packages are not compiled to ES5 before being published.
This can result in webpack build errors. To avoid this issue you should
configure webpack (or your bundler of choice) to run
`babel-preset-react-native` over the necessary `node_module`. For example:
```js
// webpack.config.js
module.exports = {
// ...
module: {
loaders: [
{
// transpile to ES5
test: /\.jsx?$/,
include: [
path.resolve(__dirname, 'src'),
path.resolve(__dirname, 'node_modules/react-native-something')
],
loader: 'babel-loader',
query: { cacheDirectory: true }
}
]
}
};
```
Please refer to the webpack documentation for more information.
## Web-specific code
Minor platform differences can use the `Platform` module.

View File

@@ -16,8 +16,9 @@ module.exports = {
}
```
The `react-native-web` package also includes a `core` module that exports only
`ReactNative`, `Image`, `StyleSheet`, `Text`, `TextInput`, and `View`.
The `react-native-web` package also includes a `core` module that exports a
subset of modules: `ReactNative`, `I18nManager`, `Platform`, `StyleSheet`,
`Image`, `Text`, `TextInput`, `Touchable`, and `View`.
```js
// webpack.config.js
@@ -62,16 +63,17 @@ AppRegistry.runApplication('App', {
initialProps: {},
rootTag: document.getElementById('react-app')
})
// prerender the app
const { html, styleElement } = AppRegistry.prerenderApplication('App', { initialProps })
```
Setting `process.env.__REACT_NATIVE_DEBUG_ENABLED__` to `true` will expose some
debugging logs. This can help track down when you're rendering without the
performance benefit of cached styles.
## Server-side rendering
Rendering using the `AppRegistry`:
```
```js
import ReactDOMServer from 'react-dom/server'
import ReactNative, { AppRegistry } from 'react-native'
@@ -84,4 +86,4 @@ AppRegistry.registerComponent('App', () => AppContainer)
// prerender the app
const { element, stylesheet } = AppRegistry.getApplication('App', { initialProps });
const initialHTML = ReactDOMServer.renderToString(element);
```
```

View File

@@ -31,10 +31,9 @@ const styles = StyleSheet.create({
})
```
Using `StyleSheet.create` is optional but provides some key advantages: styles
are immutable in development, certain declarations are automatically converted
to CSS rather than applied as inline styles, and styles are only created once
for the application and not on every render.
Using `StyleSheet.create` is optional but provides the best performance
(`style` is resolved to CSS stylesheets). Avoid creating unregistered style
objects.
The attribute names and values are a subset of CSS. See the `style`
documentation of individual components.
@@ -56,12 +55,6 @@ A common pattern is to conditionally add style based on a condition:
styles.base,
this.state.active && styles.active
]} />
// or
<View style={{
...styles.base,
...(this.state.active && styles.active)
}} />
```
## Composing styles

View File

@@ -1,6 +1,8 @@
const path = require('path')
const webpack = require('webpack')
const DEV = process.env.NODE_ENV !== 'production';
module.exports = {
module: {
loaders: [
@@ -19,13 +21,9 @@ module.exports = {
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.__REACT_NATIVE_DEBUG_ENABLED__': DEV
}),
// 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

@@ -0,0 +1,33 @@
import { Clipboard, Text, TextInput, View } from 'react-native'
import React, { Component } from 'react';
import { action, storiesOf } from '@kadira/storybook';
class ClipboardExample extends Component {
render() {
return (
<View style={{ minWidth: 300 }}>
<Text onPress={this._handleSet}>Copy to clipboard</Text>
<TextInput
multiline={true}
placeholder={'Try pasting here afterwards'}
style={{ borderWidth: 1, height: 200, marginVertical: 20 }}
/>
<Text onPress={this._handleGet}>(Clipboard.getString returns a Promise that always resolves to an empty string on web)</Text>
</View>
)
}
_handleGet() {
Clipboard.getString().then((value) => { console.log(`Clipboard value: ${value}`) });
}
_handleSet() {
const success = Clipboard.setString('This text was copied to the clipboard by React Native');
console.log(`Clipboard.setString success? ${success}`);
}
}
storiesOf('api: Clipboard', module)
.add('setString', () => (
<ClipboardExample />
));

View File

@@ -1,8 +1,8 @@
import { storiesOf } from '@kadira/storybook';
import { I18nManager, StyleSheet, TouchableHighlight, Text, View } from 'react-native'
import React, { Component } from 'react';
import { storiesOf, action } from '@kadira/storybook';
class RTLExample extends Component {
class I18nManagerExample extends Component {
componentWillUnmount() {
I18nManager.setPreferredLanguageRTL(false)
}
@@ -75,5 +75,5 @@ const styles = StyleSheet.create({
storiesOf('api: I18nManager', module)
.add('RTL layout', () => (
<RTLExample />
<I18nManagerExample />
))

View File

@@ -0,0 +1,29 @@
import { Linking, StyleSheet, Text, View } from 'react-native'
import React, { Component } from 'react';
import { storiesOf, action } from '@kadira/storybook';
class LinkingExample extends Component {
render() {
return (
<View>
<Text onPress={() => { Linking.openURL('https://mathiasbynens.github.io/rel-noopener/malicious.html'); }} style={styles.text}>
Linking.openURL (Expect: "The previous tab is safe and intact")
</Text>
<Text accessibilityRole='link' href='https://mathiasbynens.github.io/rel-noopener/malicious.html' style={styles.text} target='_blank'>
target="_blank" (Expect: "The previous tab is safe and intact")
</Text>
</View>
);
}
}
const styles = StyleSheet.create({
text: {
marginVertical: 10
}
});
storiesOf('api: Linking', module)
.add('Safe linking', () => (
<LinkingExample />
));

View File

@@ -50,7 +50,8 @@ const ToggleAnimatingActivityIndicator = React.createClass({
return (
<ActivityIndicator
animating={this.state.animating}
style={[styles.centering, {height: 80}]}
style={styles.centering}
hidesWhenStopped={this.props.hidesWhenStopped}
size="large"
/>
);
@@ -121,7 +122,12 @@ const examples = [
{
title: 'Start/stop',
render() {
return <ToggleAnimatingActivityIndicator />;
return (
<View style={[styles.horizontal, styles.centering]}>
<ToggleAnimatingActivityIndicator />
<ToggleAnimatingActivityIndicator hidesWhenStopped={false} />
</View>
);
}
},
{
@@ -129,7 +135,7 @@ const examples = [
render() {
return (
<View style={[styles.horizontal, styles.centering]}>
<ActivityIndicator size="40" />
<ActivityIndicator size={40} />
<ActivityIndicator
style={{ marginLeft: 20, transform: [ {scale: 1.5} ] }}
size="large"

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { action, storiesOf } from '@kadira/storybook';
import { Button, StyleSheet, View } from 'react-native';
const onButtonPress = action('Button has been pressed!');
const examples = [
{
title: 'Simple Button',
description: 'The title and onPress handler are required. It is ' +
'recommended to set accessibilityLabel to help make your app usable by ' +
'everyone.',
render: function() {
return (
<Button
onPress={onButtonPress}
title="Press Me"
accessibilityLabel="See an informative alert"
/>
);
},
},
{
title: 'Adjusted color',
description: 'Adjusts the color in a way that looks standard on each ' +
'platform. On iOS, the color prop controls the color of the text. On ' +
'Android, the color adjusts the background color of the button.',
render: function() {
return (
<Button
onPress={onButtonPress}
title="Press Purple"
color="#841584"
accessibilityLabel="Learn more about purple"
/>
);
},
},
{
title: 'Fit to text layout',
description: 'This layout strategy lets the title define the width of ' +
'the button',
render: function() {
return (
<View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
<Button
onPress={onButtonPress}
title="This looks great!"
accessibilityLabel="This sounds great!"
/>
<Button
onPress={onButtonPress}
title="Ok!"
color="#841584"
accessibilityLabel="Ok, Great!"
/>
</View>
);
},
},
{
title: 'Disabled Button',
description: 'All interactions for the component are disabled.',
render: function() {
return (
<Button
disabled
onPress={onButtonPress}
title="I Am Disabled"
accessibilityLabel="See an informative alert"
/>
);
},
},
];
examples.forEach((example) => {
storiesOf('component: Button', module)
.add(example.title, () => example.render());
});

View File

@@ -28,10 +28,9 @@ import { ActivityIndicator, Image, Platform, StyleSheet, Text, View } from 'reac
var base64Icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAQAAACSR7JhAAADtUlEQVR4Ac3YA2Bj6QLH0XPT1Fzbtm29tW3btm3bfLZtv7e2ObZnms7d8Uw098tuetPzrxv8wiISrtVudrG2JXQZ4VOv+qUfmqCGGl1mqLhoA52oZlb0mrjsnhKpgeUNEs91Z0pd1kvihA3ULGVHiQO2narKSHKkEMulm9VgUyE60s1aWoMQUbpZOWE+kaqs4eLEjdIlZTcFZB0ndc1+lhB1lZrIuk5P2aib1NBpZaL+JaOGIt0ls47SKzLC7CqrlGF6RZ09HGoNy1lYl2aRSWL5GuzqWU1KafRdoRp0iOQEiDzgZPnG6DbldcomadViflnl/cL93tOoVbsOLVM2jylvdWjXolWX1hmfZbGR/wjypDjFLSZIRov09BgYmtUqPQPlQrPapecLgTIy0jMgPKtTeob2zWtrGH3xvjUkPCtNg/tm1rjwrMa+mdUkPd3hWbH0jArPGiU9ufCsNNWFZ40wpwn+62/66R2RUtoso1OB34tnLOcy7YB1fUdc9e0q3yru8PGM773vXsuZ5YIZX+5xmHwHGVvlrGPN6ZSiP1smOsMMde40wKv2VmwPPVXNut4sVpUreZiLBHi0qln/VQeI/LTMYXpsJtFiclUN+5HVZazim+Ky+7sAvxWnvjXrJFneVtLWLyPJu9K3cXLWeOlbMTlrIelbMDlrLenrjEQOtIF+fuI9xRp9ZBFp6+b6WT8RrxEpdK64BuvHgDk+vUy+b5hYk6zfyfs051gRoNO1usU12WWRWL73/MMEy9pMi9qIrR4ZpV16Rrvduxazmy1FSvuFXRkqTnE7m2kdb5U8xGjLw/spRr1uTov4uOgQE+0N/DvFrG/Jt7i/FzwxbA9kDanhf2w+t4V97G8lrT7wc08aA2QNUkuTfW/KimT01wdlfK4yEw030VfT0RtZbzjeMprNq8m8tnSTASrTLti64oBNdpmMQm0eEwvfPwRbUBywG5TzjPCsdwk3IeAXjQblLCoXnDVeoAz6SfJNk5TTzytCNZk/POtTSV40NwOFWzw86wNJRpubpXsn60NJFlHeqlYRbslqZm2jnEZ3qcSKgm0kTli3zZVS7y/iivZTweYXJ26Y+RTbV1zh3hYkgyFGSTKPfRVbRqWWVReaxYeSLarYv1Qqsmh1s95S7G+eEWK0f3jYKTbV6bOwepjfhtafsvUsqrQvrGC8YhmnO9cSCk3yuY984F1vesdHYhWJ5FvASlacshUsajFt2mUM9pqzvKGcyNJW0arTKN1GGGzQlH0tXwLDgQTurS8eIQAAAABJRU5ErkJggg==';
//var ImageCapInsetsExample = require('./ImageCapInsetsExample');
//const IMAGE_PREFETCH_URL = 'http://facebook.github.io/origami/public/images/blog-hero.jpg?r=1&t=' + Date.now();
//var prefetchTask = Image.prefetch(IMAGE_PREFETCH_URL);
const IMAGE_PREFETCH_URL = 'http://origami.design/public/images/bird-logo.png?r=1&t=' + Date.now();
var prefetchTask = Image.prefetch(IMAGE_PREFETCH_URL);
/*
var NetworkImageCallbackExample = React.createClass({
getInitialState: function() {
return {
@@ -88,7 +87,6 @@ var NetworkImageCallbackExample = React.createClass({
});
}
});
*/
var NetworkImageExample = React.createClass({
getInitialState: function() {
@@ -118,7 +116,6 @@ var NetworkImageExample = React.createClass({
}
});
/*
var ImageSizeExample = React.createClass({
getInitialState: function() {
return {
@@ -133,24 +130,25 @@ var ImageSizeExample = React.createClass({
},
render: function() {
return (
<View style={{flexDirection: 'row'}}>
<Image
style={{
width: 60,
height: 60,
backgroundColor: 'transparent',
marginRight: 10,
}}
source={this.props.source} />
<View>
<Text>
Actual dimensions:{'\n'}
Width: {this.state.width}, Height: {this.state.height}
width: {this.state.width}, height: {this.state.height}
</Text>
<Image
source={this.props.source}
style={{
backgroundColor: '#eee',
height: 227,
marginTop: 10,
width: 323
}}
/>
</View>
);
},
});
*/
/*
var MultipleSourcesExample = React.createClass({
getInitialState: function() {
@@ -239,17 +237,17 @@ const examples = [
);
},
},
/*
{
title: 'Image Loading Events',
render: function() {
return (
<NetworkImageCallbackExample source={{uri: 'http://facebook.github.io/origami/public/images/blog-hero.jpg?r=1&t=' + Date.now()}}
prefetchedSource={{uri: IMAGE_PREFETCH_URL}}/>
<NetworkImageCallbackExample
source={{uri: 'http://origami.design/public/images/bird-logo.png?r=1&t=' + Date.now()}}
prefetchedSource={{uri: IMAGE_PREFETCH_URL}}
/>
);
},
},
*/
{
title: 'Error Handler',
render: function() {
@@ -263,7 +261,7 @@ const examples = [
title: 'Image Download Progress',
render: function() {
return (
<NetworkImageExample source={{uri: 'http://facebook.github.io/origami/public/images/blog-hero.jpg?r=1'}}/>
<NetworkImageExample source={{uri: 'http://origami.design/public/images/bird-logo.png?r=1'}}/>
);
},
platform: 'ios',
@@ -567,14 +565,12 @@ const examples = [
platform: 'ios',
},
*/
/*
{
title: 'Image Size',
render: function() {
return <ImageSizeExample source={fullImage} />;
return <ImageSizeExample source={{ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/d7/Chestnut-mandibled_Toucan.jpg' }} />;
},
},
*/
/*
{
title: 'MultipleSourcesExample',

View File

@@ -1,4 +1,80 @@
import React from 'react';
import { storiesOf, action } from '@kadira/storybook';
import { ListView } from 'react-native'
import { storiesOf } from '@kadira/storybook';
import { ListView, StyleSheet, Text, View } from 'react-native';
const generateData = (length) => Array.from({ length }).map((item, i) => i);
const dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
storiesOf('component: ListView', module)
.add('vertical', () => (
<View style={styles.scrollViewContainer}>
<ListView
contentContainerStyle={styles.scrollViewContentContainerStyle}
dataSource={dataSource.cloneWithRows(generateData(100))}
initialListSize={100}
// eslint-disable-next-line react/jsx-no-bind
onScroll={(e) => { console.log('ScrollView.onScroll', e); } }
// eslint-disable-next-line react/jsx-no-bind
renderRow={(row) => (
<View><Text>{row}</Text></View>
)}
scrollEventThrottle={1000} // 1 event per second
style={styles.scrollViewStyle}
/>
</View>
))
.add('incremental rendering - large pageSize', () => (
<View style={styles.scrollViewContainer}>
<ListView
contentContainerStyle={styles.scrollViewContentContainerStyle}
dataSource={dataSource.cloneWithRows(generateData(5000))}
initialListSize={100}
// eslint-disable-next-line react/jsx-no-bind
onScroll={(e) => { console.log('ScrollView.onScroll', e); } }
pageSize={50}
// eslint-disable-next-line react/jsx-no-bind
renderRow={(row) => (
<View><Text>{row}</Text></View>
)}
scrollEventThrottle={1000} // 1 event per second
style={styles.scrollViewStyle}
/>
</View>
))
.add('incremental rendering - small pageSize', () => (
<View style={styles.scrollViewContainer}>
<ListView
contentContainerStyle={styles.scrollViewContentContainerStyle}
dataSource={dataSource.cloneWithRows(generateData(5000))}
initialListSize={5}
// eslint-disable-next-line react/jsx-no-bind
onScroll={(e) => { console.log('ScrollView.onScroll', e); } }
pageSize={1}
// eslint-disable-next-line react/jsx-no-bind
renderRow={(row) => (
<View><Text>{row}</Text></View>
)}
scrollEventThrottle={1000} // 1 event per second
style={styles.scrollViewStyle}
/>
</View>
));
const styles = StyleSheet.create({
box: {
flexGrow: 1,
justifyContent: 'center',
borderWidth: 1
},
scrollViewContainer: {
height: '200px',
width: 300
},
scrollViewStyle: {
borderWidth: '1px'
},
scrollViewContentContainerStyle: {
backgroundColor: '#eee',
padding: '10px'
}
});

View File

@@ -1,13 +1,15 @@
import React from 'react';
import { storiesOf, action } from '@kadira/storybook';
import { action, storiesOf } from '@kadira/storybook';
import { ScrollView, StyleSheet, Text, TouchableHighlight, View } from 'react-native'
const onScroll = action('ScrollView.onScroll');
storiesOf('component: ScrollView', module)
.add('vertical', () => (
<View style={styles.scrollViewContainer}>
<ScrollView
contentContainerStyle={styles.scrollViewContentContainerStyle}
onScroll={e => { console.log('ScrollView.onScroll', e); } }
onScroll={onScroll}
scrollEventThrottle={1000} // 1 event per second
style={styles.scrollViewStyle}
>
@@ -24,8 +26,8 @@ storiesOf('component: ScrollView', module)
<ScrollView
contentContainerStyle={styles.scrollViewContentContainerStyle}
horizontal
onScroll={e => console.log('ScrollView.onScroll', e)}
scrollEventThrottle={1} // 1 event per second
onScroll={onScroll}
scrollEventThrottle={16} // ~60 events per second
style={styles.scrollViewStyle}
>
{Array.from({ length: 50 }).map((item, i) => (
@@ -48,10 +50,10 @@ const styles = StyleSheet.create({
width: 300
},
scrollViewStyle: {
borderWidth: '1px'
borderWidth: 1
},
scrollViewContentContainerStyle: {
backgroundColor: '#eee',
padding: '10px'
padding: 10
}
})

View File

@@ -59,7 +59,7 @@ class TextEventsExample extends React.Component {
render() {
return (
<View>
<View style={{ alignItems: 'center' }}>
<TextInput
autoCapitalize="none"
placeholder="Enter text to see events"
@@ -83,7 +83,7 @@ class TextEventsExample extends React.Component {
onKeyPress={(event) => {
this.updateText('onKeyPress key: ' + event.nativeEvent.key);
}}
style={styles.default}
style={[ styles.default, { maxWidth: 200 } ]}
/>
<Text style={styles.eventLabel}>
{this.state.curText}{'\n'}

View File

@@ -307,11 +307,11 @@ var TouchableDisabled = React.createClass({
render: function() {
return (
<View>
<TouchableOpacity disabled={true} style={[styles.row, styles.block]}>
<TouchableOpacity disabled={true} style={[styles.row, styles.block]} onPress={action('TouchableOpacity')}>
<Text style={styles.disabledButton}>Disabled TouchableOpacity</Text>
</TouchableOpacity>
<TouchableOpacity disabled={false} style={[styles.row, styles.block]}>
<TouchableOpacity disabled={false} style={[styles.row, styles.block]} onPress={action('TouchableOpacity')}>
<Text style={styles.button}>Enabled TouchableOpacity</Text>
</TouchableOpacity>
@@ -321,7 +321,7 @@ var TouchableDisabled = React.createClass({
animationVelocity={0}
underlayColor="rgb(210, 230, 255)"
style={[styles.row, styles.block]}
onPress={() => console.log('custom THW text - highlight')}>
onPress={action('TouchableHighlight')}>
<Text style={styles.disabledButton}>
Disabled TouchableHighlight
</Text>
@@ -332,7 +332,7 @@ var TouchableDisabled = React.createClass({
animationVelocity={0}
underlayColor="rgb(210, 230, 255)"
style={[styles.row, styles.block]}
onPress={() => console.log('custom THW text - highlight')}>
onPress={action('TouchableHighlight')}>
<Text style={styles.button}>
Enabled TouchableHighlight
</Text>

View File

@@ -31,6 +31,17 @@ var styles = StyleSheet.create({
borderColor: '#000033',
borderWidth: 1,
},
shadowBox: {
width: 100,
height: 100,
borderWidth: 2,
},
shadow: {
shadowOpacity: 0.5,
shadowColor: 'red',
shadowRadius: 3,
shadowOffset: { width: 3, height: 3 },
},
zIndex: {
justifyContent: 'space-around',
width: 100,
@@ -242,6 +253,19 @@ const examples = [
return <ZIndexExample />;
},
},
{
title: 'Basic shadow',
render() {
return <View style={[ styles.shadowBox, styles.shadow ]} />;
}
},
{
title: 'Shaped shadow',
description: 'borderRadius: 50',
render() {
return <View style={[ styles.shadowBox, styles.shadow, {borderRadius: 50} ]} />;
}
}
];
examples.forEach((example) => {

View File

@@ -113,7 +113,7 @@ var Cell = React.createClass({
case 2:
return styles.cellTextO;
default:
return {};
return null;
}
},

View File

@@ -1,43 +1,49 @@
{
"name": "react-native-web",
"version": "0.0.55",
"version": "0.0.68",
"description": "React Native for Web",
"main": "dist/index.js",
"files": [
"dist"
"dist",
"src",
"!**/__tests__"
],
"scripts": {
"build": "del ./dist && mkdir dist && babel src -d dist --ignore **/__tests__",
"build:examples": "build-storybook -o dist-examples -c ./examples/.storybook",
"build:performance": "cd performance && webpack",
"build:umd": "webpack --config webpack.config.js --sort-assets-by --progress",
"deploy:examples": "git checkout gh-pages && rm -rf ./storybook && mv dist-examples storybook && git add -A && git commit -m \"Storybook deploy\" && git push origin gh-pages && git checkout -",
"examples": "start-storybook -p 9001 -c ./examples/.storybook --dont-track",
"lint": "eslint src",
"lint": "eslint performance src",
"prepublish": "npm run build && npm run build:umd",
"test": "jest",
"test:watch": "npm run test -- --watch"
"test": "npm run lint && npm run test:jest",
"test:jest": "jest",
"test:watch": "npm run test:jest -- --watch"
},
"dependencies": {
"animated": "^0.1.3",
"animated": "^0.1.5",
"array-find-index": "^1.0.2",
"babel-runtime": "^6.11.6",
"asap": "^2.0.5",
"babel-runtime": "^6.20.0",
"debounce": "^1.0.0",
"deep-assign": "^2.0.0",
"fbjs": "^0.8.4",
"inline-style-prefixer": "^2.0.1",
"react-dom": "~15.3.2",
"fbjs": "^0.8.8",
"inline-style-prefixer": "^2.0.5",
"normalize-css-color": "^1.0.2",
"react-dom": "~15.4.1",
"react-textarea-autosize": "^4.0.4",
"react-timer-mixin": "^0.13.3"
},
"devDependencies": {
"@kadira/storybook": "^2.5.1",
"babel-cli": "^6.14.0",
"babel-core": "^6.14.0",
"babel-eslint": "^6.1.2",
"babel-loader": "^6.2.5",
"babel-core": "^6.21.0",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.10",
"babel-plugin-transform-react-remove-prop-types": "^0.2.11",
"babel-preset-react-native": "^1.9.0",
"del-cli": "^0.2.0",
"babel-preset-react-native": "^1.9.1",
"del-cli": "^0.2.1",
"enzyme": "^2.4.1",
"eslint": "^3.4.0",
"eslint-plugin-jsx-a11y": "^2.2.0",
@@ -45,16 +51,17 @@
"eslint-plugin-react": "^6.1.2",
"file-loader": "^0.9.0",
"jest": "^16.0.2",
"marky": "^1.1.1",
"node-libs-browser": "^0.5.3",
"react": "~15.3.2",
"react-addons-test-utils": "~15.3.2",
"react-test-renderer": "~15.3.2",
"react": "~15.4.1",
"react-addons-test-utils": "~15.4.1",
"react-test-renderer": "~15.4.1",
"url-loader": "^0.5.7",
"webpack": "^1.13.2",
"webpack-bundle-analyzer": "^1.5.3"
},
"peerDependencies": {
"react": "~15.3.2"
"react": "~15.4.1"
},
"author": "Nicolas Gallagher",
"license": "BSD-3-Clause",

65
performance/benchmark.js Normal file
View File

@@ -0,0 +1,65 @@
import * as marky from 'marky';
const fmt = (time) => `${Math.round(time * 100) / 100}ms`;
const measure = (name, fn) => {
marky.mark(name);
fn();
const performanceMeasure = marky.stop(name);
return performanceMeasure;
};
const benchmark = ({ name, description, setup, teardown, task, runs }) => {
return new Promise((resolve) => {
const performanceMeasures = [];
let i = 0;
setup();
const first = measure('first', task);
teardown();
const done = () => {
const mean = performanceMeasures.reduce((sum, performanceMeasure) => {
return sum + performanceMeasure.duration;
}, 0) / runs;
const firstDuration = fmt(first.duration);
const meanDuration = fmt(mean);
console.log(`First: ${firstDuration}`);
console.log(`Mean: ${meanDuration}`);
console.groupEnd();
resolve(mean);
};
const a = () => {
setup();
window.requestAnimationFrame(b);
};
const b = () => {
performanceMeasures.push(measure('mean', task));
window.requestAnimationFrame(c);
};
const c = () => {
teardown();
window.requestAnimationFrame(d);
};
const d = () => {
i += 1;
if (i < runs) {
window.requestAnimationFrame(a);
} else {
window.requestAnimationFrame(done);
}
};
console.group(`[benchmark] ${name}`);
console.log(description);
window.requestAnimationFrame(a);
});
};
module.exports = benchmark;

View File

@@ -0,0 +1,88 @@
import React, { Component, PropTypes } from 'react';
const createDeepTree = ({ StyleSheet, View }, options = {}) => {
class DeepTree extends Component {
static propTypes = {
breadth: PropTypes.number.isRequired,
depth: PropTypes.number.isRequired,
id: PropTypes.number.isRequired,
wrap: PropTypes.number.isRequired
};
render() {
const { breadth, depth, id, wrap } = this.props;
let result = (
<View
style={[
styles.outer,
depth % 2 === 0 ? styles.even : styles.odd,
styles[`custom${id % 3}`]
]}
>
{depth === 0 && (
<View
style={[
styles.terminal,
styles[`terminal${id % 3}`]
]}
/>
)}
{depth !== 0 && Array.from({ length: breadth }).map((el, i) => (
<DeepTree
breadth={breadth}
depth={depth - 1}
id={i}
key={i}
wrap={wrap}
/>
))}
</View>
);
for (let i = 0; i < wrap; i++) {
result = <View>{result}</View>;
}
return result;
}
}
const stylesObject = {
outer: {
padding: 4
},
odd: {
flexDirection: 'row'
},
even: {
flexDirection: 'column'
},
custom0: {
backgroundColor: '#222'
},
custom1: {
backgroundColor: '#666'
},
custom2: {
backgroundColor: '#999'
},
terminal: {
width: 20,
height: 20
},
terminal0: {
backgroundColor: 'blue'
},
terminal1: {
backgroundColor: 'orange'
},
terminal2: {
backgroundColor: 'red'
}
};
const styles = options.registerStyles ? StyleSheet.create(stylesObject) : stylesObject;
return DeepTree;
};
module.exports = createDeepTree;

View File

@@ -0,0 +1,24 @@
import benchmark from '../../benchmark';
import createDeepTree from './createDeepTree';
import React from 'react';
import ReactDOM from 'react-dom';
import ReactNative from 'react-native';
const deepTreeBenchmark = ({ breadth, depth, registerStyles = true, runs, wrap }, node) => () => {
// React Native for Web implementation of the tree
const DeepTree = createDeepTree(ReactNative, { registerStyles });
const setup = () => {};
const teardown = () => ReactDOM.unmountComponentAtNode(node);
return benchmark({
name: `DeepTree (${registerStyles ? 'registered' : 'unregistered'}) styles)`,
description: `depth=${depth}, breadth=${breadth}, wrap=${wrap}`,
runs,
setup,
teardown,
task: () => ReactDOM.render(<DeepTree breadth={breadth} depth={depth} id={0} wrap={wrap} />, node)
});
};
module.exports = deepTreeBenchmark;

11
performance/index.html Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="root"></div>
<script src="../dist-performance/performance.bundle.js"></script>
</body>
</html>

16
performance/index.js Normal file
View File

@@ -0,0 +1,16 @@
import createDeepTree from './benchmarks/deepTree/createDeepTree';
import deepTree from './benchmarks/deepTree';
import React from 'react';
import ReactDOM from 'react-dom';
import ReactNative from 'react-native';
const node = document.querySelector('.root');
const DeepTree = createDeepTree(ReactNative);
Promise.resolve()
.then(deepTree({ wrap: 4, depth: 3, breadth: 10, runs: 10, registerStyles: false }, node))
.then(deepTree({ wrap: 1, depth: 5, breadth: 3, runs: 20, registerStyles: false }, node))
.then(deepTree({ wrap: 4, depth: 3, breadth: 10, runs: 10 }, node))
.then(deepTree({ wrap: 1, depth: 5, breadth: 3, runs: 20 }, node))
.then(() => ReactDOM.render(<DeepTree breadth={3} depth={5} id={0} wrap={1} />, node));

View File

@@ -0,0 +1,45 @@
const path = require('path');
const webpack = require('webpack');
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
entry: {
performance: './index'
},
output: {
path: path.resolve(__dirname, '../dist-performance'),
filename: 'performance.bundle.js'
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: { cacheDirectory: true }
}
]
},
plugins: [
new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }),
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(),
new webpack.optimize.UglifyJsPlugin({
compress: {
dead_code: true,
screw_ie8: true,
warnings: true
}
})
],
resolve: {
alias: {
'react-native': path.join(__dirname, '../src')
}
}
};

View File

@@ -1,5 +1,6 @@
import Animated from 'animated';
import Image from '../../components/Image';
import ScrollView from '../../components/ScrollView';
import StyleSheet from '../StyleSheet';
import Text from '../../components/Text';
import View from '../../components/View';
@@ -9,6 +10,7 @@ Animated.inject.FlattenStyle(StyleSheet.flatten);
module.exports = {
...Animated,
Image: Animated.createAnimatedComponent(Image),
ScrollView: Animated.createAnimatedComponent(ScrollView),
Text: Animated.createAnimatedComponent(Text),
View: Animated.createAnimatedComponent(View)
};

View File

@@ -0,0 +1,48 @@
exports[`apis/AppRegistry/renderApplication getApplication 1`] = `
"<style id=\"react-native-stylesheet\">
/* React Native StyleSheet*/
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{margin:0}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none}
@keyframes rn-ActivityIndicator-animation{0%{-webkit-transform: rotate(0deg); transform: rotate(0deg);}100%{-webkit-transform: rotate(360deg); transform: rotate(360deg);}}
@keyframes rn-ProgressBar-animation{0%{-webkit-transform: translateX(-100%); transform: translateX(-100%);}100%{-webkit-transform: translateX(400%); transform: translateX(400%);}}
.rn-pointerEvents\\:auto,.rn_pointerEvents\\:box-only,.rn-pointerEvents\\:box-none *{pointer-events:auto}.rn-pointerEvents\\:none,.rn_pointerEvents\\:box-only *,.rn-pointerEvents\\:box-none{pointer-events:none}
.rn-bottom\\:0px{bottom:0px}
.rn-left\\:0px{left:0px}
.rn-position\\:absolute{position:absolute}
.rn-right\\:0px{right:0px}
.rn-top\\:0px{top:0px}
.rn-alignItems\\:stretch{-ms-flex-align:stretch;-webkit-align-items:stretch;-webkit-box-align:stretch;align-items:stretch}
.rn-backgroundColor\\:transparent{background-color:transparent}
.rn-borderTopStyle\\:solid{border-top-style:solid}
.rn-borderRightStyle\\:solid{border-right-style:solid}
.rn-borderBottomStyle\\:solid{border-bottom-style:solid}
.rn-borderLeftStyle\\:solid{border-left-style:solid}
.rn-borderTopWidth\\:0px{border-top-width:0px}
.rn-borderRightWidth\\:0px{border-right-width:0px}
.rn-borderBottomWidth\\:0px{border-bottom-width:0px}
.rn-borderLeftWidth\\:0px{border-left-width:0px}
.rn-boxSizing\\:border-box{-moz-box-sizing:border-box;box-sizing:border-box}
.rn-color\\:inherit{color:inherit}
.rn-display\\:flex{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex}
.rn-flexBasis\\:auto{-ms-preferred-size:auto;-webkit-flex-basis:auto;flex-basis:auto}
.rn-flexDirection\\:column{-ms-flex-direction:column;-webkit-box-direction:normal;-webkit-box-orient:vertical;-webkit-flex-direction:column;flex-direction:column}
.rn-font\\:inherit{font:inherit}
.rn-listStyle\\:none{list-style:none}
.rn-marginTop\\:0px{margin-top:0px}
.rn-marginRight\\:0px{margin-right:0px}
.rn-marginBottom\\:0px{margin-bottom:0px}
.rn-marginLeft\\:0px{margin-left:0px}
.rn-minHeight\\:0px{min-height:0px}
.rn-minWidth\\:0px{min-width:0px}
.rn-paddingTop\\:0px{padding-top:0px}
.rn-paddingRight\\:0px{padding-right:0px}
.rn-paddingBottom\\:0px{padding-bottom:0px}
.rn-paddingLeft\\:0px{padding-left:0px}
.rn-position\\:relative{position:relative}
.rn-textAlign\\:inherit{text-align:inherit}
.rn-textDecoration\\:none{text-decoration:none}
.rn-flexShrink\\:0{-ms-flex-negative:0px;-webkit-flex-shrink:0px;flex-shrink:0}
</style>"
`;

View File

@@ -10,6 +10,6 @@ describe('apis/AppRegistry/renderApplication', () => {
const { element, stylesheet } = getApplication(component, {});
expect(element).toBeTruthy();
expect(stylesheet.type).toEqual('style');
expect(stylesheet).toMatchSnapshot();
});
});

View File

@@ -8,9 +8,10 @@
import { Component } from 'react';
import invariant from 'fbjs/lib/invariant';
import { unmountComponentAtNode } from 'react/lib/ReactMount';
import { unmountComponentAtNode } from 'react-dom/lib/ReactMount';
import renderApplication, { getApplication } from './renderApplication';
const emptyObject = {};
const runnables = {};
type ComponentProvider = () => Component<any, any, any>
@@ -41,8 +42,8 @@ class AppRegistry {
static registerComponent(appKey: string, getComponentFunc: ComponentProvider): string {
runnables[appKey] = {
getApplication: ({ initialProps } = {}) => getApplication(getComponentFunc(), initialProps),
run: ({ initialProps, rootTag }) => renderApplication(getComponentFunc(), initialProps, rootTag)
getApplication: ({ initialProps } = emptyObject) => getApplication(getComponentFunc(), initialProps),
run: ({ initialProps = emptyObject, rootTag }) => renderApplication(getComponentFunc(), initialProps, rootTag)
};
return appKey;
}

View File

@@ -7,7 +7,7 @@
*/
import invariant from 'fbjs/lib/invariant';
import { render } from 'react/lib/ReactMount';
import { render } from 'react-dom/lib/ReactMount';
import ReactNativeApp from './ReactNativeApp';
import StyleSheet from '../../apis/StyleSheet';
import React, { Component } from 'react';
@@ -32,6 +32,6 @@ export function getApplication(RootComponent: Component, initialProps: Object):
rootComponent={RootComponent}
/>
);
const stylesheet = StyleSheet.render();
const stylesheet = StyleSheet.renderToString();
return { element, stylesheet };
}

View File

@@ -0,0 +1,28 @@
/**
* 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.
*
* web stub for BackAndroid.android.js
*
* @providesModule BackAndroid
*/
'use strict';
function emptyFunction() {}
const BackAndroid = {
exitApp: emptyFunction,
addEventListener() {
return {
remove: emptyFunction
};
},
removeEventListener: emptyFunction
};
module.exports = BackAndroid;

View File

@@ -0,0 +1,21 @@
class Clipboard {
static getString() {
return Promise.resolve('');
}
static setString(text) {
let success = false;
const textField = document.createElement('textarea');
textField.innerText = text;
document.body.appendChild(textField);
textField.select();
try {
document.execCommand('copy');
success = true;
} catch (e) {}
document.body.removeChild(textField);
return success;
}
}
module.exports = Clipboard;

36
src/apis/Linking/index.js Normal file
View File

@@ -0,0 +1,36 @@
const Linking = {
addEventListener() {},
removeEventListener() {},
canOpenUrl() { return true; },
getInitialUrl() { return ''; },
openURL(url) {
iframeOpen(url);
}
};
/**
* Tabs opened using JavaScript may redirect the parent tab using
* `window.opener.location`, ignoring cross-origin restrictions and enabling
* phishing attacks.
*
* Safari requires that we open the url by injecting a hidden iframe that calls
* window.open(), then removes the iframe from the DOM.
*
* https://mathiasbynens.github.io/rel-noopener/
*/
const iframeOpen = (url) => {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
const script = iframeDoc.createElement('script');
script.text = `
window.parent = null; window.top = null; window.frameElement = null;
var child = window.open("${url}"); child.opener = null;
`;
iframeDoc.body.appendChild(script);
document.body.removeChild(iframe);
};
module.exports = Linking;

View File

@@ -6,7 +6,7 @@
"use strict";
var TouchHistoryMath = require('react/lib/TouchHistoryMath');
var TouchHistoryMath = require('react-dom/lib/TouchHistoryMath');
var currentCentroidXOfTouchesChangedAfter =
TouchHistoryMath.currentCentroidXOfTouchesChangedAfter;

View File

@@ -9,8 +9,8 @@
import { PropTypes } from 'react'
import ImageStylePropTypes from '../../components/Image/ImageStylePropTypes'
import ReactPropTypeLocations from 'react/lib/ReactPropTypeLocations'
import ReactPropTypesSecret from 'react/lib/ReactPropTypesSecret'
import ReactPropTypeLocations from 'react-dom/lib/ReactPropTypeLocations'
import ReactPropTypesSecret from 'react-dom/lib/ReactPropTypesSecret'
import TextStylePropTypes from '../../components/Text/TextStylePropTypes'
import ViewStylePropTypes from '../../components/View/ViewStylePropTypes'
import warning from 'fbjs/lib/warning'

View File

@@ -0,0 +1,15 @@
exports[`apis/StyleSheet/createReactDOMStyle converts ReactNative style to ReactDOM style 1`] = `
Object {
"borderBottomWidth": "1px",
"borderLeftWidth": "1px",
"borderRightWidth": "1px",
"borderTopWidth": "1px",
"borderWidthLeft": "2px",
"borderWidthRight": "3px",
"boxShadow": "1px 1px 1px 1px #000, 1px 2px 0px rgba(255,0,0,1)",
"display": "flex",
"marginBottom": "0px",
"marginTop": "0px",
"opacity": 0,
}
`;

View File

@@ -0,0 +1,26 @@
exports[`apis/StyleSheet/flattenStyle should merge style objects 1`] = `
Object {
"opacity": 1,
"order": 2,
}
`;
exports[`apis/StyleSheet/flattenStyle should override style properties 1`] = `
Object {
"backgroundColor": "#023c69",
"order": null,
}
`;
exports[`apis/StyleSheet/flattenStyle should overwrite properties with \`undefined\` 1`] = `
Object {
"backgroundColor": undefined,
}
`;
exports[`apis/StyleSheet/flattenStyle should recursively flatten arrays 1`] = `
Object {
"opacity": 1,
"order": 3,
}
`;

View File

@@ -0,0 +1 @@
exports[`apis/StyleSheet/generateCss generates correct css 1`] = `"-webkit-transition-duration:0.1s;border-width-left:2px;border-width-right:3px;box-shadow:1px 1px 1px 1px #000;position:absolute;transition-duration:0.1s"`;

View File

@@ -1,10 +1,14 @@
exports[`apis/StyleSheet resolve 1`] = `
Object {
"className": "test __style_df __style_pebn",
"style": Object {
"display": null,
"opacity": 1,
"pointerEvents": null,
},
}
exports[`apis/StyleSheet renderToString 1`] = `
"<style id=\"react-native-stylesheet\">
.rn-borderTopColor\\:red{border-top-color:red}
.rn-borderRightColor\\:red{border-right-color:red}
.rn-borderBottomColor\\:red{border-bottom-color:red}
.rn-borderLeftColor\\:red{border-left-color:red}
.rn-borderTopWidth\\:0px{border-top-width:0px}
.rn-borderRightWidth\\:0px{border-right-width:0px}
.rn-borderBottomWidth\\:0px{border-bottom-width:0px}
.rn-borderLeftWidth\\:0px{border-left-width:0px}
.rn-left\\:50px{left:50px}
.rn-position\\:absolute{position:absolute}
</style>"
`;

View File

@@ -0,0 +1,179 @@
exports[`apis/StyleSheet/registry resolve with register, resolves to className 1`] = `
Object {
"className": "
rn-borderTopColor:red
rn-borderRightColor:red
rn-borderBottomColor:red
rn-borderLeftColor:red
rn-borderTopWidth:0px
rn-borderRightWidth:0px
rn-borderBottomWidth:0px
rn-borderLeftWidth:0px
rn-left:50px
rn-pointerEvents:box-only
rn-position:absolute
rn-width:100px",
"style": Object {},
}
`;
exports[`apis/StyleSheet/registry resolve with register, resolves to className 2`] = `
Object {
"className": "
rn-borderTopColor:red
rn-borderRightColor:red
rn-borderBottomColor:red
rn-borderLeftColor:red
rn-borderTopWidth:0px
rn-borderRightWidth:0px
rn-borderBottomWidth:0px
rn-borderLeftWidth:0px
rn-left:50px
rn-pointerEvents:box-only
rn-position:absolute
rn-width:200px",
"style": Object {},
}
`;
exports[`apis/StyleSheet/registry resolve with register, resolves to className 3`] = `
Object {
"className": "
rn-borderTopColor:red
rn-borderRightColor:red
rn-borderBottomColor:red
rn-borderLeftColor:red
rn-borderTopWidth:0px
rn-borderRightWidth:0px
rn-borderBottomWidth:0px
rn-borderLeftWidth:0px
rn-left:50px
rn-pointerEvents:box-only
rn-position:absolute
rn-width:100px",
"style": Object {},
}
`;
exports[`apis/StyleSheet/registry resolve with register, resolves to mixed 1`] = `
Object {
"className": "
rn-left:50px
rn-pointerEvents:box-only
rn-position:absolute",
"style": Object {
"borderBottomColor": "red",
"borderBottomWidth": "0px",
"borderLeftColor": "red",
"borderLeftWidth": "0px",
"borderRightColor": "red",
"borderRightWidth": "0px",
"borderTopColor": "red",
"borderTopWidth": "0px",
"width": "100px",
},
}
`;
exports[`apis/StyleSheet/registry resolve with register, resolves to mixed 2`] = `
Object {
"className": "
rn-left:50px
rn-pointerEvents:box-only
rn-position:absolute
rn-width:200px",
"style": Object {
"borderBottomColor": "red",
"borderBottomWidth": "0px",
"borderLeftColor": "red",
"borderLeftWidth": "0px",
"borderRightColor": "red",
"borderRightWidth": "0px",
"borderTopColor": "red",
"borderTopWidth": "0px",
},
}
`;
exports[`apis/StyleSheet/registry resolve with register, resolves to mixed 3`] = `
Object {
"className": "
rn-left:50px
rn-pointerEvents:box-only
rn-position:absolute",
"style": Object {
"borderBottomColor": "red",
"borderBottomWidth": "0px",
"borderLeftColor": "red",
"borderLeftWidth": "0px",
"borderRightColor": "red",
"borderRightWidth": "0px",
"borderTopColor": "red",
"borderTopWidth": "0px",
"width": "100px",
},
}
`;
exports[`apis/StyleSheet/registry resolve without register, resolves to inline styles 1`] = `
Object {
"className": "
",
"style": Object {
"borderBottomColor": "red",
"borderBottomWidth": "0px",
"borderLeftColor": "red",
"borderLeftWidth": "0px",
"borderRightColor": "red",
"borderRightWidth": "0px",
"borderTopColor": "red",
"borderTopWidth": "0px",
"left": "50px",
"pointerEvents": "box-only",
"position": "absolute",
"width": "100px",
},
}
`;
exports[`apis/StyleSheet/registry resolve without register, resolves to inline styles 2`] = `
Object {
"className": "
",
"style": Object {
"borderBottomColor": "red",
"borderBottomWidth": "0px",
"borderLeftColor": "red",
"borderLeftWidth": "0px",
"borderRightColor": "red",
"borderRightWidth": "0px",
"borderTopColor": "red",
"borderTopWidth": "0px",
"left": "50px",
"pointerEvents": "box-only",
"position": "absolute",
"width": "200px",
},
}
`;
exports[`apis/StyleSheet/registry resolve without register, resolves to inline styles 3`] = `
Object {
"className": "
",
"style": Object {
"borderBottomColor": "red",
"borderBottomWidth": "0px",
"borderLeftColor": "red",
"borderLeftWidth": "0px",
"borderRightColor": "red",
"borderRightWidth": "0px",
"borderTopColor": "red",
"borderTopWidth": "0px",
"left": "50px",
"pointerEvents": "box-only",
"position": "absolute",
"width": "100px",
},
}
`;

View File

@@ -0,0 +1,28 @@
/* eslint-env jasmine, jest */
import createReactDOMStyle from '../createReactDOMStyle';
const reactNativeStyle = {
boxShadow: '1px 1px 1px 1px #000',
borderWidthLeft: 2,
borderWidth: 1,
borderWidthRight: 3,
display: 'flex',
marginVertical: 0,
opacity: 0,
shadowColor: 'red',
shadowOffset: { width: 1, height: 2 },
resizeMode: 'contain'
};
describe('apis/StyleSheet/createReactDOMStyle', () => {
test('converts ReactNative style to ReactDOM style', () => {
expect(createReactDOMStyle(reactNativeStyle)).toMatchSnapshot();
});
test('noop on DOM styles', () => {
const firstStyle = createReactDOMStyle(reactNativeStyle);
const secondStyle = createReactDOMStyle(firstStyle);
expect(firstStyle).toEqual(secondStyle);
});
});

View File

@@ -1,12 +0,0 @@
/* eslint-env jasmine, jest */
import createReactStyleObject from '../createReactStyleObject';
describe('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 };
expect(createReactStyleObject(reactNativeStyle)).toEqual(expectedStyle);
});
});

View File

@@ -9,9 +9,9 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
import flattenStyle from '..';
import flattenStyle from '../flattenStyle';
describe('modules/flattenStyle', () => {
describe('apis/StyleSheet/flattenStyle', () => {
test('should merge style objects', () => {
const style = flattenStyle([
{ opacity: 1 },

View File

@@ -0,0 +1,16 @@
/* eslint-env jasmine, jest */
import generateCss from '../generateCss';
describe('apis/StyleSheet/generateCss', () => {
test('generates correct css', () => {
const style = {
boxShadow: '1px 1px 1px 1px #000',
borderWidthLeft: 2,
borderWidthRight: 3,
position: 'absolute',
transitionDuration: '0.1s'
};
expect(generateCss(style)).toMatchSnapshot();
});
});

View File

@@ -1,7 +1,7 @@
/* eslint-env jasmine, jest */
import { getDefaultStyleSheet } from '../css';
import StyleSheet from '..';
import StyleRegistry from '../registry';
const isPlainObject = (x) => {
const toString = Object.prototype.toString;
@@ -16,7 +16,7 @@ const isPlainObject = (x) => {
describe('apis/StyleSheet', () => {
beforeEach(() => {
StyleSheet._reset();
StyleRegistry.reset();
});
test('absoluteFill', () => {
@@ -32,11 +32,6 @@ describe('apis/StyleSheet', () => {
const style = StyleSheet.create({ root: { opacity: 1 } });
expect(Number.isInteger(style.root) === true).toBeTruthy();
});
test('renders a style sheet in the browser', () => {
StyleSheet.create({ root: { color: 'red' } });
expect(document.getElementById('react-native-style__').textContent).toEqual(getDefaultStyleSheet());
});
});
test('flatten', () => {
@@ -47,18 +42,17 @@ describe('apis/StyleSheet', () => {
expect(Number.isInteger(StyleSheet.hairlineWidth) === true).toBeTruthy();
});
test('render', () => {
expect(StyleSheet.render().props.dangerouslySetInnerHTML.__html).toEqual(getDefaultStyleSheet());
});
test('resolve', () => {
expect(StyleSheet.resolve({
className: 'test',
style: {
display: 'flex',
opacity: 1,
pointerEvents: 'box-none'
test('renderToString', () => {
StyleSheet.create({
a: {
borderWidth: 0,
borderColor: 'red'
},
b: {
position: 'absolute',
left: 50
}
})).toMatchSnapshot();
});
expect(StyleSheet.renderToString()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,22 @@
/* eslint-env jasmine, jest */
import injector from '../injector';
describe('apis/StyleSheet/injector', () => {
beforeEach(() => {
document.head.insertAdjacentHTML('afterbegin', `
<style id="react-native-stylesheet">
.rn-alignItems\\:stretch{align-items:stretch;}
.rn-position\\:top{position:top;}
</style>
`);
});
test('hydrates from SSR', () => {
const classList = injector.getClassNames();
expect(classList).toEqual({
'rn-alignItems\\:stretch': true,
'rn-position\\:top': true
});
});
});

View File

@@ -0,0 +1,13 @@
/* eslint-env jasmine, jest */
import prefixInlineStyles from '../prefixInlineStyles';
describe('apis/StyleSheet/prefixInlineStyles', () => {
test('handles array values', () => {
const style = {
display: [ '-webkit-flex', 'flex' ]
};
expect(prefixInlineStyles(style)).toEqual({ display: 'flex' });
});
});

View File

@@ -1,20 +0,0 @@
/* eslint-env jasmine, jest */
import processTextShadow from '../processTextShadow';
describe('apis/StyleSheet/processTextShadow', () => {
test('textShadowOffset', () => {
const style = {
textShadowColor: 'red',
textShadowOffset: { width: 2, height: 2 },
textShadowRadius: 5
};
expect(processTextShadow(style)).toEqual({
textShadow: '2px 2px 5px red',
textShadowColor: null,
textShadowOffset: null,
textShadowRadius: null
});
});
});

View File

@@ -1,28 +0,0 @@
/* eslint-env jasmine, jest */
import processTransform from '../processTransform';
describe('apis/StyleSheet/processTransform', () => {
test('transform', () => {
const style = {
transform: [
{ scaleX: 20 },
{ translateX: 20 },
{ rotate: '20deg' }
]
};
expect(processTransform(style)).toEqual({ transform: 'scaleX(20) translateX(20px) rotate(20deg)' });
});
test('transformMatrix', () => {
const style = {
transformMatrix: [ 1, 2, 3, 4, 5, 6 ]
};
expect(processTransform(style)).toEqual({
transform: 'matrix3d(1,2,3,4,5,6)',
transformMatrix: null
});
});
});

View File

@@ -1,13 +0,0 @@
/* eslint-env jasmine, jest */
import processVendorPrefixes from '../processVendorPrefixes';
describe('apis/StyleSheet/processVendorPrefixes', () => {
test('handles array values', () => {
const style = {
display: [ '-webkit-flex', 'flex' ]
};
expect(processVendorPrefixes(style)).toEqual({ display: 'flex' });
});
});

View File

@@ -0,0 +1,54 @@
/* eslint-env jasmine, jest */
import StyleRegistry from '../registry';
describe('apis/StyleSheet/registry', () => {
beforeEach(() => {
StyleRegistry.reset();
});
test('register', () => {
const style = { opacity: 0 };
const id = StyleRegistry.register(style);
expect(typeof id === 'number').toBe(true);
});
describe('resolve', () => {
const styleA = { borderWidth: 0, borderColor: 'red', width: 100 };
const styleB = { position: 'absolute', left: 50, pointerEvents: 'box-only' };
const styleC = { width: 200 };
const testResolve = (a, b, c) => {
// no common properties, different resolving order, same result
const resolve1 = StyleRegistry.resolve([ a, b ]);
expect(resolve1).toMatchSnapshot();
const resolve2 = StyleRegistry.resolve([ b, a ]);
expect(resolve1).toEqual(resolve2);
// common properties, different resolving order, different result
const resolve3 = StyleRegistry.resolve([ a, b, c ]);
expect(resolve3).toMatchSnapshot();
const resolve4 = StyleRegistry.resolve([ c, a, b ]);
expect(resolve4).toMatchSnapshot();
expect(resolve3).not.toEqual(resolve4);
};
test('with register, resolves to className', () => {
const a = StyleRegistry.register(styleA);
const b = StyleRegistry.register(styleB);
const c = StyleRegistry.register(styleC);
testResolve(a, b, c);
});
test('with register, resolves to mixed', () => {
const a = styleA;
const b = StyleRegistry.register(styleB);
const c = StyleRegistry.register(styleC);
testResolve(a, b, c);
});
test('without register, resolves to inline styles', () => {
testResolve(styleA, styleB, styleC);
});
});
});

View File

@@ -0,0 +1,60 @@
/* eslint-env jasmine, jest */
import resolveBoxShadow from '../resolveBoxShadow';
describe('apis/StyleSheet/resolveBoxShadow', () => {
test('shadowColor only', () => {
const resolvedStyle = {};
const style = { shadowColor: 'red' };
resolveBoxShadow(resolvedStyle, style);
expect(resolvedStyle).toEqual({
boxShadow: '0px 0px 0px rgba(255,0,0,1)'
});
});
test('shadowColor and shadowOpacity only', () => {
const resolvedStyle = {};
const style = { shadowColor: 'red', shadowOpacity: 0.5 };
resolveBoxShadow(resolvedStyle, style);
expect(resolvedStyle).toEqual({
boxShadow: '0px 0px 0px rgba(255,0,0,0.5)'
});
});
test('shadowOffset only', () => {
const resolvedStyle = {};
const style = { shadowOffset: { width: 1, height: 2 } };
resolveBoxShadow(resolvedStyle, style);
expect(resolvedStyle).toEqual({
boxShadow: '1px 2px 0px rgba(0,0,0,0)'
});
});
test('shadowRadius only', () => {
const resolvedStyle = {};
const style = { shadowRadius: 5 };
resolveBoxShadow(resolvedStyle, style);
expect(resolvedStyle).toEqual({
boxShadow: '0px 0px 5px rgba(0,0,0,0)'
});
});
test('shadowOffset, shadowRadius, shadowColor', () => {
const resolvedStyle = {};
const style = {
shadowColor: 'rgba(50,60,70,0.5)',
shadowOffset: { width: 1, height: 2 },
shadowOpacity: 0.5,
shadowRadius: 3
};
resolveBoxShadow(resolvedStyle, style);
expect(resolvedStyle).toEqual({
boxShadow: '1px 2px 3px rgba(50,60,70,0.25)'
});
});
});

View File

@@ -0,0 +1,19 @@
/* eslint-env jasmine, jest */
import resolveTextShadow from '../resolveTextShadow';
describe('apis/StyleSheet/resolveTextShadow', () => {
test('textShadowOffset', () => {
const resolvedStyle = {};
const style = {
textShadowColor: 'red',
textShadowOffset: { width: 1, height: 2 },
textShadowRadius: 5
};
resolveTextShadow(resolvedStyle, style);
expect(resolvedStyle).toEqual({
textShadow: '1px 2px 5px red'
});
});
});

View File

@@ -0,0 +1,32 @@
/* eslint-env jasmine, jest */
import resolveTransform from '../resolveTransform';
describe('apis/StyleSheet/resolveTransform', () => {
test('transform', () => {
const resolvedStyle = {};
const style = {
transform: [
{ perspective: 50 },
{ scaleX: 20 },
{ translateX: 20 },
{ rotate: '20deg' }
]
};
resolveTransform(resolvedStyle, style);
expect(resolvedStyle).toEqual({
transform: 'perspective(50px) scaleX(20) translateX(20px) rotate(20deg)'
});
});
test('transformMatrix', () => {
const resolvedStyle = {};
const style = { transformMatrix: [ 1, 2, 3, 4, 5, 6 ] };
resolveTransform(resolvedStyle, style);
expect(resolvedStyle).toEqual({
transform: 'matrix3d(1,2,3,4,5,6)'
});
});
});

View File

@@ -0,0 +1,6 @@
import expandStyle from './expandStyle';
import i18nStyle from './i18nStyle';
const createReactDOMStyle = (flattenedReactNativeStyle) => expandStyle(i18nStyle(flattenedReactNativeStyle));
module.exports = createReactDOMStyle;

View File

@@ -1,20 +0,0 @@
import expandStyle from './expandStyle';
import flattenStyle from '../../modules/flattenStyle';
import i18nStyle from './i18nStyle';
import processTextShadow from './processTextShadow';
import processTransform from './processTransform';
import processVendorPrefixes from './processVendorPrefixes';
const processors = [
processTextShadow,
processTransform,
processVendorPrefixes
];
const applyProcessors = (style) => processors.reduce((style, processor) => processor(style), style);
const createReactDOMStyleObject = (reactNativeStyle) => applyProcessors(
expandStyle(i18nStyle(flattenStyle(reactNativeStyle)))
);
module.exports = createReactDOMStyleObject;

View File

@@ -1,42 +0,0 @@
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';
/* eslint-disable max-len */
const CSS_RESET =
// reset unwanted styles
'/* 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::-webkit-inner-spin-button,input::-webkit-outer-spin-button,' +
'input::-webkit-search-cancel-button,input::-webkit-search-decoration,' +
'input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none}';
const CSS_HELPERS =
// 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}`;
/* eslint-enable max-len */
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 getDefaultStyleSheet = () => `${CSS_RESET}\n${CSS_HELPERS}`;
export const getStyleAsHelperClassName = (prop, value) => {
return styleAsClassName[prop] && styleAsClassName[prop][value];
};

View File

@@ -10,6 +10,9 @@
*/
import normalizeValue from './normalizeValue';
import resolveBoxShadow from './resolveBoxShadow';
import resolveTextShadow from './resolveTextShadow';
import resolveTransform from './resolveTransform';
const emptyObject = {};
const styleShortFormProperties = {
@@ -28,46 +31,83 @@ const styleShortFormProperties = {
writingDirection: [ 'direction' ]
};
const alphaSort = (arr) => arr.sort((a, b) => {
const alphaSortProps = (propsArray) => propsArray.sort((a, b) => {
if (a < b) { return -1; }
if (a > b) { return 1; }
return 0;
});
const createStyleReducer = (originalStyle) => {
const originalStyleProps = Object.keys(originalStyle);
const expandStyle = (style) => {
if (!style) { return emptyObject; }
const styleProps = Object.keys(style);
const sortedStyleProps = alphaSortProps(styleProps);
let hasResolvedBoxShadow = false;
let hasResolvedTextShadow = false;
return (style, prop) => {
const value = normalizeValue(prop, originalStyle[prop]);
const longFormProperties = styleShortFormProperties[prop];
const reducer = (resolvedStyle, prop) => {
const value = normalizeValue(prop, style[prop]);
if (value == null) { return resolvedStyle; }
// 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;
switch (prop) {
// ignore React Native styles
case 'elevation':
case 'resizeMode': {
break;
}
case 'flex': {
resolvedStyle.flexGrow = value;
resolvedStyle.flexShrink = 1;
resolvedStyle.flexBasis = 'auto';
break;
}
case 'shadowColor':
case 'shadowOffset':
case 'shadowOpacity':
case 'shadowRadius': {
if (!hasResolvedBoxShadow) {
resolveBoxShadow(resolvedStyle, style);
}
});
} else {
style[prop] = value;
hasResolvedBoxShadow = true;
break;
}
case 'textAlignVertical': {
resolvedStyle.verticalAlign = (value === 'center' ? 'middle' : value);
break;
}
case 'textShadowColor':
case 'textShadowOffset':
case 'textShadowRadius': {
if (!hasResolvedTextShadow) {
resolveTextShadow(resolvedStyle, style);
}
hasResolvedTextShadow = true;
break;
}
case 'transform': {
resolveTransform(resolvedStyle, style);
break;
}
default: {
const longFormProperties = styleShortFormProperties[prop];
if (longFormProperties) {
longFormProperties.forEach((longForm, i) => {
// the value of any longform property in the original styles takes
// precedence over the shortform's value
if (styleProps.indexOf(longForm) === -1) {
resolvedStyle[longForm] = value;
}
});
} else {
resolvedStyle[prop] = value;
}
}
}
return style;
};
};
const expandStyle = (style = emptyObject) => {
const sortedStyleProps = alphaSort(Object.keys(style));
const styleReducer = createStyleReducer(style);
return sortedStyleProps.reduce(styleReducer, {});
return resolvedStyle;
};
const resolvedStyle = sortedStyleProps.reduce(reducer, {});
return resolvedStyle;
};
module.exports = expandStyle;

View File

@@ -1,4 +1,3 @@
/* eslint-disable */
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
@@ -10,10 +9,8 @@
* @providesModule flattenStyle
* @flow
*/
'use strict';
var ReactNativePropRegistry = require('../ReactNativePropRegistry');
var invariant = require('fbjs/lib/invariant');
import ReactNativePropRegistry from '../../modules/ReactNativePropRegistry';
import invariant from 'fbjs/lib/invariant';
function getStyle(style) {
if (typeof style === 'number') {
@@ -22,22 +19,26 @@ function getStyle(style) {
return style;
}
function flattenStyle(style: ?StyleObj): ?Object {
function flattenStyle(style) {
if (!style) {
return undefined;
}
invariant(style !== true, 'style may be false but not true');
if (process.env.NODE !== 'production') {
invariant(style !== true, 'style may be false but not true');
}
if (!Array.isArray(style)) {
return getStyle(style);
}
var result = {};
for (var i = 0, styleLength = style.length; i < styleLength; ++i) {
var computedStyle = flattenStyle(style[i]);
const result = {};
for (let i = 0, styleLength = style.length; i < styleLength; ++i) {
const computedStyle = flattenStyle(style[i]);
if (computedStyle) {
for (var key in computedStyle) {
result[key] = computedStyle[key];
for (const key in computedStyle) {
const value = computedStyle[key];
result[key] = value;
}
}
}

View File

@@ -0,0 +1,23 @@
import hyphenate from './hyphenate';
import mapKeyValue from '../../modules/mapKeyValue';
import normalizeValue from './normalizeValue';
import prefixAll from 'inline-style-prefixer/static';
const createDeclarationString = (prop, val) => {
const name = hyphenate(prop);
const value = normalizeValue(prop, val);
if (Array.isArray(val)) {
return val.map((v) => `${name}:${v}`).join(';');
}
return `${name}:${value}`;
};
/**
* Generates valid CSS rule body from a JS object
*
* generateCss({ width: 20, color: 'blue' });
* // => 'color:blue;width:20px'
*/
const generateCss = (style) => mapKeyValue(prefixAll(style), createDeclarationString).sort().join(';');
module.exports = generateCss;

View File

@@ -0,0 +1,3 @@
const RE_1 = /([A-Z])/g;
const RE_2 = /^ms-/;
module.exports = (s) => s.replace(RE_1, '-$1').toLowerCase().replace(RE_2, '-ms-');

View File

@@ -1,6 +1,8 @@
import I18nManager from '../I18nManager';
import multiplyStyleLengthValue from '../../modules/multiplyStyleLengthValue';
const emptyObject = {};
/**
* Map of property names to their BiDi equivalent.
*/
@@ -64,38 +66,40 @@ const swapLtrRtl = (value:String): String => {
return value === 'ltr' ? 'rtl' : value === 'rtl' ? 'ltr' : value;
};
const i18nStyle = (style = {}) => {
const i18nStyle = (style = emptyObject) => {
const newStyle = {};
for (const prop in style) {
if (style.hasOwnProperty(prop)) {
const indexOfNoFlip = prop.indexOf('$noI18n');
if (!Object.prototype.hasOwnProperty.call(style, prop)) {
continue;
}
if (I18nManager.isRTL) {
if (PROPERTIES_TO_SWAP[prop]) {
const newProp = flipProperty(prop);
newStyle[newProp] = style[prop];
} else if (PROPERTIES_SWAP_LEFT_RIGHT[prop]) {
newStyle[prop] = swapLeftRight(style[prop]);
} else if (PROPERTIES_SWAP_LTR_RTL[prop]) {
newStyle[prop] = swapLtrRtl(style[prop]);
} else if (prop === 'textShadowOffset') {
newStyle[prop] = style[prop];
newStyle[prop].width = additiveInverse(style[prop].width);
} else if (prop === 'transform') {
newStyle[prop] = style[prop].map(flipTransform);
} else if (indexOfNoFlip > -1) {
const newProp = prop.substring(0, indexOfNoFlip);
newStyle[newProp] = style[prop];
} else {
newStyle[prop] = style[prop];
}
const indexOfNoFlip = prop.indexOf('$noI18n');
if (I18nManager.isRTL) {
if (PROPERTIES_TO_SWAP[prop]) {
const newProp = flipProperty(prop);
newStyle[newProp] = style[prop];
} else if (PROPERTIES_SWAP_LEFT_RIGHT[prop]) {
newStyle[prop] = swapLeftRight(style[prop]);
} else if (PROPERTIES_SWAP_LTR_RTL[prop]) {
newStyle[prop] = swapLtrRtl(style[prop]);
} else if (prop === 'textShadowOffset') {
newStyle[prop] = style[prop];
newStyle[prop].width = additiveInverse(style[prop].width);
} else if (prop === 'transform') {
newStyle[prop] = style[prop].map(flipTransform);
} else if (indexOfNoFlip > -1) {
const newProp = prop.substring(0, indexOfNoFlip);
newStyle[newProp] = style[prop];
} else {
if (indexOfNoFlip > -1) {
const newProp = prop.substring(0, indexOfNoFlip);
newStyle[newProp] = style[prop];
} else {
newStyle[prop] = style[prop];
}
newStyle[prop] = style[prop];
}
} else {
if (indexOfNoFlip > -1) {
const newProp = prop.substring(0, indexOfNoFlip);
newStyle[newProp] = style[prop];
} else {
newStyle[prop] = style[prop];
}
}
}

View File

@@ -1,89 +1,26 @@
import * as css from './css';
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 flattenStyle from './flattenStyle';
import initialize from './initialize';
import injector from './injector';
import StyleRegistry from './registry';
let styleElement;
let shouldInsertStyleSheet = ExecutionEnvironment.canUseDOM;
const STYLE_SHEET_ID = 'react-native-style__';
initialize();
const absoluteFillObject = { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0 };
const defaultStyleSheet = css.getDefaultStyleSheet();
const insertStyleSheet = () => {
// 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',
`<style id="${STYLE_SHEET_ID}">${defaultStyleSheet}</style>`
);
shouldInsertStyleSheet = false;
}
};
module.exports = {
/**
* For testing
* @private
*/
_reset() {
if (styleElement) {
document.head.removeChild(styleElement);
styleElement = null;
shouldInsertStyleSheet = true;
}
},
absoluteFill: ReactNativePropRegistry.register(absoluteFillObject),
absoluteFill: StyleRegistry.register(absoluteFillObject),
absoluteFillObject,
create(styles) {
if (shouldInsertStyleSheet) {
insertStyleSheet();
}
const result = {};
for (const key in styles) {
Object.keys(styles).forEach((key) => {
if (process.env.NODE_ENV !== 'production') {
require('./StyleSheetValidation').validateStyle(key, styles);
}
result[key] = ReactNativePropRegistry.register(styles[key]);
}
result[key] = StyleRegistry.register(styles[key]);
});
return result;
},
hairlineWidth: 1,
flatten: flattenStyle,
/* @platform web */
render() {
return <style dangerouslySetInnerHTML={{ __html: defaultStyleSheet }} id={STYLE_SHEET_ID} />;
},
/**
* Accepts React props and converts style declarations to classNames when necessary
* @platform web
*/
resolve(props) {
let className = props.className || '';
const style = createReactStyleObject(props.style);
for (const prop in style) {
const value = style[prop];
const replacementClassName = css.getStyleAsHelperClassName(prop, value);
if (replacementClassName) {
className += ` ${replacementClassName}`;
style[prop] = null;
}
}
return { className, style };
}
renderToString: injector.getStyleSheetHtml
};

View File

@@ -0,0 +1,39 @@
import injector from './injector';
import StyleRegistry from './registry';
const initialize = () => {
injector.addRule(
'reset',
'/* React Native StyleSheet*/\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::-webkit-inner-spin-button,input::-webkit-outer-spin-button,' +
'input::-webkit-search-cancel-button,input::-webkit-search-decoration,' +
'input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none}'
);
injector.addRule(
'keyframes',
'@keyframes rn-ActivityIndicator-animation{' +
'0%{-webkit-transform: rotate(0deg); transform: rotate(0deg);}' +
'100%{-webkit-transform: rotate(360deg); transform: rotate(360deg);}' +
'}\n' +
'@keyframes rn-ProgressBar-animation{' +
'0%{-webkit-transform: translateX(-100%); transform: translateX(-100%);}' +
'100%{-webkit-transform: translateX(400%); transform: translateX(400%);}' +
'}'
);
injector.addRule(
'pointer-events',
'.rn-pointerEvents\\:auto,.rn-pointerEvents\\:box-only,.rn-pointerEvents\\:box-none *{pointer-events:auto}' +
'.rn-pointerEvents\\:none,.rn-pointerEvents\\:box-only *,.rn-pointerEvents\\:box-none{pointer-events:none}'
);
const classNames = injector.getClassNames();
StyleRegistry.initialize(classNames);
};
export default initialize;

View File

@@ -0,0 +1,106 @@
/**
* Based on
* https://github.com/lelandrichardson/react-primitives/blob/master/src/StyleSheet/injector.js
*/
import asap from 'asap';
const emptyObject = {};
const hasOwnProperty = Object.prototype.hasOwnProperty;
const CLASSNAME_REXEP = /\.rn-([^{;\s]+)/g;
const STYLE_ELEMENT_ID = 'react-native-stylesheet';
let registry = {};
let isDirty = false;
/**
* Registers a rule and requests an update to the style sheet
*/
const addRule = (key, rule) => {
if (!registry[key]) {
registry[key] = rule;
isDirty = true;
if (global.document) {
asap(frame);
}
}
};
/**
* Returns a string of the registered rules
*/
const getStyleText = () => {
/* eslint prefer-template:0 */
let result = '\n';
for (const key in registry) {
if (hasOwnProperty.call(registry, key)) {
result += registry[key] + '\n';
}
}
return result;
};
/**
* Returns an HTML string for server rendering
*/
const getStyleSheetHtml = () => `<style id="${STYLE_ELEMENT_ID}">${getStyleText()}</style>`;
const reset = () => { registry = {}; };
/**
* Finds or injects the style sheet when in a browser environment
*/
let styleNode = null;
const getStyleNode = () => {
if (global.document) {
if (!styleNode) {
// look for existing style sheet (could also be server-rendered)
styleNode = document.getElementById(STYLE_ELEMENT_ID);
if (!styleNode) {
// if there is no existing stylesheet, inject it style sheet
document.head.insertAdjacentHTML('afterbegin', getStyleSheetHtml());
styleNode = document.getElementById(STYLE_ELEMENT_ID);
}
}
return styleNode;
}
};
/**
* Determines which classes are available in the existing document. Doesn't
* rely on the registry so it can be used to read class names from a SSR style
* sheet.
*/
const getClassNames = () => {
const styleNode = getStyleNode();
if (styleNode) {
const text = styleNode.textContent;
const matches = text.match(CLASSNAME_REXEP);
if (matches) {
return matches.map((name) => name.slice(1)).reduce((classMap, className) => {
classMap[className] = true;
return classMap;
}, {});
}
}
return emptyObject;
};
const frame = () => {
if (!isDirty || !global.document) { return; }
isDirty = false;
const styleNode = getStyleNode();
if (styleNode) {
const css = getStyleText();
styleNode.textContent = css;
}
};
module.exports = {
addRule,
getClassNames,
getStyleSheetHtml,
reset
};

View File

@@ -1,30 +1,45 @@
const unitlessNumbers = {
animationIterationCount: true,
borderImageOutset: true,
borderImageSlice: true,
borderImageWidth: true,
boxFlex: true,
boxFlexGroup: true,
boxOrdinalGroup: true,
columnCount: true,
flex: true,
flexGrow: true,
flexOrder: true,
flexPositive: true,
flexShrink: true,
flexNegative: true,
fontWeight: true,
gridRow: true,
gridColumn: true,
lineClamp: true,
opacity: true,
order: true,
orphans: true,
tabSize: true,
widows: true,
zIndex: true,
zoom: true,
// SVG-related
fillOpacity: true,
floodOpacity: true,
stopOpacity: true,
strokeDasharray: true,
strokeDashoffset: true,
strokeMiterlimit: true,
strokeOpacity: true,
strokeWidth: true,
// transform types
scale: true,
scaleX: true,
scaleY: true,
scaleZ: true
scaleZ: true,
// RN properties
shadowOpacity: true
};
const normalizeValue = (property, value) => {

View File

@@ -1,16 +1,18 @@
import prefixAll from 'inline-style-prefixer/static';
const processVendorPrefixes = (style) => {
const prefixInlineStyles = (style) => {
const 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) {
Object.keys(prefixedStyles).forEach((prop) => {
const value = prefixedStyles[prop];
if (Array.isArray(value)) {
prefixedStyles[prop] = value[value.length - 1];
}
}
});
return prefixedStyles;
};
module.exports = processVendorPrefixes;
module.exports = prefixInlineStyles;

View File

@@ -1,19 +0,0 @@
import normalizeValue from './normalizeValue';
const processTextShadow = (style) => {
if (style && style.textShadowOffset) {
const { height, width } = style.textShadowOffset;
const offsetX = normalizeValue(null, height || 0);
const offsetY = normalizeValue(null, width || 0);
const blurRadius = normalizeValue(null, style.textShadowRadius || 0);
const color = style.textShadowColor || 'currentcolor';
style.textShadow = `${offsetX} ${offsetY} ${blurRadius} ${color}`;
style.textShadowColor = null;
style.textShadowOffset = null;
style.textShadowRadius = null;
}
return style;
};
module.exports = processTextShadow;

View File

@@ -0,0 +1,219 @@
/**
* WARNING: changes to this file in particular can cause significant changes to
* the results of render performance benchmarks.
*/
import createReactDOMStyle from './createReactDOMStyle';
import flattenArray from '../../modules/flattenArray';
import flattenStyle from './flattenStyle';
import generateCss from './generateCss';
import injector from './injector';
import mapKeyValue from '../../modules/mapKeyValue';
import prefixInlineStyles from './prefixInlineStyles';
import ReactNativePropRegistry from '../../modules/ReactNativePropRegistry';
const prefix = 'r';
const SPACE_REGEXP = /\s/g;
const ESCAPE_SELECTOR_CHARS_REGEXP = /[(),":?.%\\$#*]/g;
/**
* Creates an HTML class name for use on elements
*/
const createClassName = (prop, value) => {
const val = `${value}`.replace(SPACE_REGEXP, '-');
return `rn-${prop}:${val}`;
};
/**
* Formatting improves debugging in devtools and snapshot
*/
const mapDeclarationsToClassName = (style, fn) => {
const result = mapKeyValue(style, fn).join('\n').trim();
return `\n${result}`;
};
/**
* Inject a CSS rule for a given declaration and record the availability of the
* resulting class name.
*/
let injectedClassNames = {};
const injectClassNameIfNeeded = (prop, value) => {
const className = createClassName(prop, value);
if (!injectedClassNames[className]) {
// create a valid CSS selector for a given HTML class name
const selector = className.replace(ESCAPE_SELECTOR_CHARS_REGEXP, '\\$&');
const body = generateCss({ [prop]: value });
const css = `.${selector}{${body}}`;
// this adds the rule to the buffer to be injected into the document
injector.addRule(className, css);
injectedClassNames[className] = true;
}
return className;
};
/**
* Converts a React Native style object to HTML class names
*/
let resolvedPropsCache = {};
const registerStyle = (id, flatStyle) => {
const style = createReactDOMStyle(flatStyle);
const className = mapDeclarationsToClassName(style, (prop, value) => {
if (value != null) {
return injectClassNameIfNeeded(prop, value);
}
});
const key = `${prefix}-${id}`;
resolvedPropsCache[key] = { className };
return id;
};
/**
* Resolves a React Native style object to DOM props
*/
const resolveProps = (reactNativeStyle) => {
const flatStyle = flattenStyle(reactNativeStyle);
const domStyle = createReactDOMStyle(flatStyle);
const style = {};
const className = mapDeclarationsToClassName(domStyle, (prop, value) => {
if (value != null) {
const singleClassName = createClassName(prop, value);
if (injectedClassNames[singleClassName]) {
return singleClassName;
} else {
// 4x slower render
style[prop] = value;
}
}
});
const props = {
className,
style: prefixInlineStyles(style)
};
/*
if (process.env.__REACT_NATIVE_DEBUG_ENABLED__) {
console.groupCollapsed('[StyleSheet] resolving uncached styles');
console.log(
'Slow operation. Resolving style objects (uncached result). ' +
'Occurs on first render and when using styles not registered with "StyleSheet.create"'
);
console.log('source => \n', reactNativeStyle);
console.log('flatten => \n', flatStyle);
console.log('resolve => \n', props);
console.groupEnd();
}
*/
return props;
};
/**
* Caching layer over 'resolveProps'
*/
const resolvePropsIfNeeded = (key, style) => {
if (key) {
if (!resolvedPropsCache[key]) {
// slow: convert style object to props and cache
resolvedPropsCache[key] = resolveProps(style);
}
return resolvedPropsCache[key];
}
return resolveProps(style);
};
/**
* Web style registry
*/
const StyleRegistry = {
initialize(classNames) {
injectedClassNames = classNames;
/*
if (process.env.__REACT_NATIVE_DEBUG_ENABLED__) {
if (global.__REACT_NATIVE_DEBUG_ENABLED__styleRegistryTimer) {
clearInterval(global.__REACT_NATIVE_DEBUG_ENABLED__styleRegistryTimer);
}
global.__REACT_NATIVE_DEBUG_ENABLED__styleRegistryTimer = setInterval(() => {
const entryCount = Object.keys(resolvedPropsCache).length;
console.groupCollapsed('[StyleSheet] resolved props cache snapshot:', entryCount, 'entries');
console.log(resolvedPropsCache);
console.groupEnd();
}, 30000);
}
*/
},
reset() {
injectedClassNames = {};
resolvedPropsCache = {};
injector.reset();
},
register(style) {
const id = ReactNativePropRegistry.register(style);
return registerStyle(id, style);
},
resolve(reactNativeStyle) {
if (!reactNativeStyle) {
return undefined;
}
// fast and cachable
if (typeof reactNativeStyle === 'number') {
const key = `${prefix}${reactNativeStyle}`;
return resolvePropsIfNeeded(key, reactNativeStyle);
}
// convert a RN style object to DOM props
if (!Array.isArray(reactNativeStyle)) {
return resolveProps(reactNativeStyle);
}
// flatten the array
// [ 1, [ 2, 3 ], { prop: value }, 4, 5 ] => [ 1, 2, 3, { prop: value }, 4, 5 ];
const flatArray = flattenArray(reactNativeStyle);
let isArrayOfNumbers = true;
for (let i = 0; i < flatArray.length; i++) {
if (typeof flatArray[i] !== 'number') {
isArrayOfNumbers = false;
break;
}
}
// TODO: determine when/if to cache unregistered styles. This produces 2x
// faster benchmark results for unregistered styles. However, the cache
// could be filled with props that are never used again.
//
// let hasValidKey = true;
// let key = flatArray.reduce((keyParts, element) => {
// if (typeof element === 'number') {
// keyParts.push(element);
// } else {
// if (element.transform) {
// hasValidKey = false;
// } else {
// const objectAsKey = Object.keys(element).map((prop) => `${prop}:${element[prop]}`).join(';');
// if (objectAsKey !== '') {
// keyParts.push(objectAsKey);
// }
// }
// }
// return keyParts;
// }, [ prefix ]).join('-');
// if (!hasValidKey) { key = null; }
// cache resolved props when all styles are registered
const key = isArrayOfNumbers ? `${prefix}-${flatArray.join('-')}` : null;
return resolvePropsIfNeeded(key, flatArray);
}
};
module.exports = StyleRegistry;

View File

@@ -0,0 +1,26 @@
import normalizeColor from 'normalize-css-color';
import normalizeValue from './normalizeValue';
const defaultOffset = { height: 0, width: 0 };
const applyOpacity = (color, opacity = 1) => {
const nullableColor = normalizeColor(color);
const colorInt = nullableColor === null ? 0x00000000 : nullableColor;
const { r, g, b, a } = normalizeColor.rgba(colorInt);
const alpha = a.toFixed(2);
return `rgba(${r},${g},${b},${alpha * opacity})`;
};
// TODO: add inset and spread support
const resolveBoxShadow = (resolvedStyle, style) => {
const { height, width } = style.shadowOffset || defaultOffset;
const offsetX = normalizeValue(null, width);
const offsetY = normalizeValue(null, height);
const blurRadius = normalizeValue(null, style.shadowRadius || 0);
const color = applyOpacity(style.shadowColor, style.shadowOpacity);
const boxShadow = `${offsetX} ${offsetY} ${blurRadius} ${color}`;
resolvedStyle.boxShadow = style.boxShadow ? `${style.boxShadow}, ${boxShadow}` : boxShadow;
};
module.exports = resolveBoxShadow;

View File

@@ -0,0 +1,15 @@
import normalizeValue from './normalizeValue';
const defaultOffset = { height: 0, width: 0 };
const resolveTextShadow = (resolvedStyle, style) => {
const { height, width } = style.textShadowOffset || defaultOffset;
const offsetX = normalizeValue(null, width);
const offsetY = normalizeValue(null, height);
const blurRadius = normalizeValue(null, style.textShadowRadius || 0);
const color = style.textShadowColor || 'currentcolor';
resolvedStyle.textShadow = `${offsetX} ${offsetY} ${blurRadius} ${color}`;
};
module.exports = resolveTextShadow;

View File

@@ -1,7 +1,7 @@
import normalizeValue from './normalizeValue';
// { scale: 2 } => 'scale(2)'
// { translateX: 20 } => 'translateX(20px)'
// { translateX: 20 } => 'translateX(20px)'
const mapTransform = (transform) => {
const type = Object.keys(transform)[0];
const value = normalizeValue(type, transform[type]);
@@ -14,16 +14,14 @@ const convertTransformMatrix = (transformMatrix) => {
return `matrix3d(${matrix})`;
};
const processTransform = (style) => {
if (style) {
if (style.transform && Array.isArray(style.transform)) {
style.transform = style.transform.map(mapTransform).join(' ');
} else if (style.transformMatrix) {
style.transform = convertTransformMatrix(style.transformMatrix);
style.transformMatrix = null;
}
const resolveTransform = (resolvedStyle, style) => {
if (Array.isArray(style.transform)) {
const transform = style.transform.map(mapTransform).join(' ');
resolvedStyle.transform = transform;
} else if (style.transformMatrix) {
const transform = convertTransformMatrix(style.transformMatrix);
resolvedStyle.transform = transform;
}
return style;
};
module.exports = processTransform;
module.exports = resolveTransform;

View File

@@ -24,7 +24,7 @@ describe('apis/UIManager', () => {
});
describe('measure', () => {
test('provides correct layout to callback', () => {
test('provides correct layout to callback', (done) => {
const node = createNode({ height: '5000px', left: '100px', position: 'relative', top: '100px', width: '5000px' });
document.body.appendChild(node);
@@ -37,7 +37,14 @@ describe('apis/UIManager', () => {
expect(height).toEqual(5000);
expect(pageX).toEqual(100);
expect(pageY).toEqual(100);
done();
document.body.removeChild(node);
});
});
test('provides correct layout to callback', (done) => {
const node = createNode({ height: '5000px', left: '100px', position: 'relative', top: '100px', width: '5000px' });
document.body.appendChild(node);
// test values account for scroll position
window.scrollTo(200, 200);
@@ -51,14 +58,14 @@ describe('apis/UIManager', () => {
expect(height).toEqual(5000);
expect(pageX).toEqual(-100);
expect(pageY).toEqual(-100);
done();
document.body.removeChild(node);
});
document.body.removeChild(node);
});
});
describe('measureLayout', () => {
test('provides correct layout to onSuccess callback', () => {
test('provides correct layout to onSuccess callback', (done) => {
const node = createNode({ height: '10px', width: '10px' });
const middle = createNode({ padding: '20px' });
const context = createNode({ padding: '20px' });
@@ -78,14 +85,14 @@ describe('apis/UIManager', () => {
expect(y).toEqual(40);
expect(width).toEqual(10);
expect(height).toEqual(10);
done();
document.body.removeChild(context);
});
document.body.removeChild(context);
});
});
describe('measureInWindow', () => {
test('provides correct layout to callback', () => {
test('provides correct layout to callback', (done) => {
const node = createNode({ height: '10px', width: '10px' });
const middle = createNode({ padding: '20px' });
const context = createNode({ padding: '20px' });
@@ -105,9 +112,9 @@ describe('apis/UIManager', () => {
expect(y).toEqual(40);
expect(width).toEqual(10);
expect(height).toEqual(10);
done();
document.body.removeChild(context);
});
document.body.removeChild(context);
});
});
@@ -119,17 +126,16 @@ describe('apis/UIManager', () => {
}
};
test('add new className to existing className', () => {
test('supports className alias for class', () => {
const node = createNode();
node.className = 'existing';
const props = { className: 'extra' };
UIManager.updateView(node, props, componentStub);
expect(node.getAttribute('class')).toEqual('existing extra');
expect(node.getAttribute('class')).toEqual('extra');
});
test('adds correct DOM styles to existing style', () => {
const node = createNode({ color: 'red' });
const props = { style: { marginVertical: 0, opacity: 0 } };
const props = { style: { marginTop: 0, marginBottom: 0, opacity: 0 } };
UIManager.updateView(node, props, componentStub);
expect(node.getAttribute('style')).toEqual('color: red; margin-top: 0px; margin-bottom: 0px; opacity: 0;');
});

View File

@@ -1,13 +1,15 @@
import createReactStyleObject from '../StyleSheet/createReactStyleObject';
import CSSPropertyOperations from 'react/lib/CSSPropertyOperations';
import asap from 'asap';
import CSSPropertyOperations from 'react-dom/lib/CSSPropertyOperations';
const _measureLayout = (node, relativeToNativeNode, callback) => {
const relativeNode = relativeToNativeNode || node.parentNode;
const relativeRect = relativeNode.getBoundingClientRect();
const { height, left, top, width } = node.getBoundingClientRect();
const x = left - relativeRect.left;
const y = top - relativeRect.top;
callback(x, y, width, height, left, top);
asap(() => {
const relativeNode = relativeToNativeNode || node.parentNode;
const relativeRect = relativeNode.getBoundingClientRect();
const { height, left, top, width } = node.getBoundingClientRect();
const x = left - relativeRect.left;
const y = top - relativeRect.top;
callback(x, y, width, height, left, top);
});
};
const UIManager = {
@@ -35,23 +37,19 @@ const UIManager = {
updateView(node, props, component /* only needed to surpress React errors in development */) {
for (const prop in props) {
const value = props[prop];
if (!Object.prototype.hasOwnProperty.call(props, prop)) {
continue;
}
const value = props[prop];
switch (prop) {
case 'style':
// convert styles to DOM-styles
CSSPropertyOperations.setValueForStyles(
node,
createReactStyleObject(value),
component._reactInternalInstance
);
case 'style': {
CSSPropertyOperations.setValueForStyles(node, 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);
node.setAttribute('class', value);
break;
}
case 'text':

View File

@@ -0,0 +1,226 @@
exports[`components/ActivityIndicator default render 1`] = `
<div
aria-valuemax="1"
aria-valuemin="0"
className="
rn-alignItems:center
rn-backgroundColor:transparent
rn-borderTopStyle:solid
rn-borderRightStyle:solid
rn-borderBottomStyle:solid
rn-borderLeftStyle:solid
rn-borderTopWidth:0px
rn-borderRightWidth:0px
rn-borderBottomWidth:0px
rn-borderLeftWidth:0px
rn-boxSizing:border-box
rn-color:inherit
rn-display:flex
rn-flexBasis:auto
rn-flexDirection:column
rn-flexShrink:0
rn-font:inherit
rn-justifyContent:center
rn-listStyle:none
rn-marginTop:0px
rn-marginRight:0px
rn-marginBottom:0px
rn-marginLeft:0px
rn-minHeight:0px
rn-minWidth:0px
rn-paddingTop:0px
rn-paddingRight:0px
rn-paddingBottom:0px
rn-paddingLeft:0px
rn-position:relative
rn-textAlign:inherit
rn-textDecoration:none"
role="progressbar"
style={Object {}}>
<div
className="
rn-alignItems:stretch
rn-animationDuration:0.75s
rn-animationIterationCount:infinite
rn-animationName:rn-ActivityIndicator-animation
rn-animationTimingFunction:linear
rn-backgroundColor:transparent
rn-borderTopStyle:solid
rn-borderRightStyle:solid
rn-borderBottomStyle:solid
rn-borderLeftStyle:solid
rn-borderTopWidth:0px
rn-borderRightWidth:0px
rn-borderBottomWidth:0px
rn-borderLeftWidth:0px
rn-boxSizing:border-box
rn-color:inherit
rn-display:flex
rn-flexBasis:auto
rn-flexDirection:column
rn-flexShrink:0
rn-font:inherit
rn-height:20px
rn-listStyle:none
rn-marginTop:0px
rn-marginRight:0px
rn-marginBottom:0px
rn-marginLeft:0px
rn-minHeight:0px
rn-minWidth:0px
rn-paddingTop:0px
rn-paddingRight:0px
rn-paddingBottom:0px
rn-paddingLeft:0px
rn-position:relative
rn-textAlign:inherit
rn-textDecoration:none
rn-width:20px"
style={Object {}}>
<svg
height="100%"
viewBox="0 0 32 32"
width="100%">
<circle
cx="16"
cy="16"
fill="none"
r="14"
strokeWidth="4"
style={
Object {
"opacity": 0.2,
"stroke": "#1976D2",
}
} />
<circle
cx="16"
cy="16"
fill="none"
r="14"
strokeWidth="4"
style={
Object {
"stroke": "#1976D2",
"strokeDasharray": 80,
"strokeDashoffset": 60,
}
} />
</svg>
</div>
</div>
`;
exports[`components/ActivityIndicator other render 1`] = `
<div
aria-valuemax="1"
aria-valuemin="0"
className="
rn-alignItems:center
rn-backgroundColor:transparent
rn-borderTopStyle:solid
rn-borderRightStyle:solid
rn-borderBottomStyle:solid
rn-borderLeftStyle:solid
rn-borderTopWidth:0px
rn-borderRightWidth:0px
rn-borderBottomWidth:0px
rn-borderLeftWidth:0px
rn-boxSizing:border-box
rn-color:inherit
rn-display:flex
rn-flexBasis:auto
rn-flexDirection:column
rn-flexShrink:0
rn-font:inherit
rn-justifyContent:center
rn-listStyle:none
rn-marginTop:0px
rn-marginRight:0px
rn-marginBottom:0px
rn-marginLeft:0px
rn-minHeight:0px
rn-minWidth:0px
rn-paddingTop:0px
rn-paddingRight:0px
rn-paddingBottom:0px
rn-paddingLeft:0px
rn-position:relative
rn-textAlign:inherit
rn-textDecoration:none"
role="progressbar"
style={Object {}}>
<div
className="
rn-alignItems:stretch
rn-animationDuration:0.75s
rn-animationIterationCount:infinite
rn-animationName:rn-ActivityIndicator-animation
rn-animationPlayState:paused
rn-animationTimingFunction:linear
rn-backgroundColor:transparent
rn-borderTopStyle:solid
rn-borderRightStyle:solid
rn-borderBottomStyle:solid
rn-borderLeftStyle:solid
rn-borderTopWidth:0px
rn-borderRightWidth:0px
rn-borderBottomWidth:0px
rn-borderLeftWidth:0px
rn-boxSizing:border-box
rn-color:inherit
rn-display:flex
rn-flexBasis:auto
rn-flexDirection:column
rn-flexShrink:0
rn-font:inherit
rn-height:36px
rn-listStyle:none
rn-marginTop:0px
rn-marginRight:0px
rn-marginBottom:0px
rn-marginLeft:0px
rn-minHeight:0px
rn-minWidth:0px
rn-paddingTop:0px
rn-paddingRight:0px
rn-paddingBottom:0px
rn-paddingLeft:0px
rn-position:relative
rn-textAlign:inherit
rn-textDecoration:none
rn-width:36px"
style={Object {}}>
<svg
height="100%"
viewBox="0 0 32 32"
width="100%">
<circle
cx="16"
cy="16"
fill="none"
r="14"
strokeWidth="4"
style={
Object {
"opacity": 0.2,
"stroke": "#1976D2",
}
} />
<circle
cx="16"
cy="16"
fill="none"
r="14"
strokeWidth="4"
style={
Object {
"stroke": "#1976D2",
"strokeDasharray": 80,
"strokeDashoffset": 60,
}
} />
</svg>
</div>
</div>
`;

View File

@@ -1,5 +1,17 @@
/* eslint-env jasmine, jest */
import ActivityIndicator from '..';
import React from 'react';
import renderer from 'react-test-renderer';
describe('components/ActivityIndicator', () => {
test.skip('NO TEST COVERAGE', () => {});
test('default render', () => {
const component = renderer.create(<ActivityIndicator />);
expect(component.toJSON()).toMatchSnapshot();
});
test('other render', () => {
const component = renderer.create(<ActivityIndicator animating={false} hidesWhenStopped={false} size='large' />);
expect(component.toJSON()).toMatchSnapshot();
});
});

View File

@@ -1,12 +1,8 @@
import Animated from '../../apis/Animated';
import applyNativeMethods from '../../modules/applyNativeMethods';
import Easing from 'animated/lib/Easing';
import StyleSheet from '../../apis/StyleSheet';
import View from '../View';
import React, { Component, PropTypes } from 'react';
const rotationInterpolation = { inputRange: [ 0, 1 ], outputRange: [ '0deg', '360deg' ] };
class ActivityIndicator extends Component {
static displayName = 'ActivityIndicator';
@@ -25,21 +21,6 @@ class ActivityIndicator extends Component {
size: 'small'
};
constructor(props) {
super(props);
this.state = {
animation: new Animated.Value(0)
};
}
componentDidMount() {
this._manageAnimation();
}
componentDidUpdate() {
this._manageAnimation();
}
render() {
const {
animating,
@@ -50,8 +31,6 @@ class ActivityIndicator extends Component {
...other
} = this.props;
const { animation } = this.state;
const svg = (
<svg height='100%' viewBox='0 0 32 32' width='100%'>
<circle
@@ -88,47 +67,21 @@ class ActivityIndicator extends Component {
style={[
styles.container,
style,
size && { height: size, width: size }
typeof size === 'number' && { height: size, width: size }
]}
>
<Animated.View
<View
children={svg}
style={[
indicatorStyles[size],
hidesWhenStopped && !animating && styles.hidesWhenStopped,
{
transform: [
{ rotate: animation.interpolate(rotationInterpolation) }
]
}
indicatorSizes[size],
styles.animation,
!animating && styles.animationPause,
!animating && hidesWhenStopped && styles.hidesWhenStopped
]}
/>
</View>
);
}
_manageAnimation() {
const { animation } = this.state;
const cycleAnimation = () => {
animation.setValue(0);
Animated.timing(animation, {
duration: 750,
easing: Easing.inOut(Easing.linear),
toValue: 1
}).start((event) => {
if (event.finished) {
cycleAnimation();
}
});
};
if (this.props.animating) {
cycleAnimation();
} else {
animation.stopAnimation();
}
}
}
const styles = StyleSheet.create({
@@ -138,10 +91,19 @@ const styles = StyleSheet.create({
},
hidesWhenStopped: {
visibility: 'hidden'
},
animation: {
animationDuration: '0.75s',
animationName: 'rn-ActivityIndicator-animation',
animationTimingFunction: 'linear',
animationIterationCount: 'infinite'
},
animationPause: {
animationPlayState: 'paused'
}
});
const indicatorStyles = StyleSheet.create({
const indicatorSizes = StyleSheet.create({
small: {
width: 20,
height: 20

View File

@@ -0,0 +1,5 @@
/* eslint-env jasmine, jest */
describe('components/Button', () => {
test.skip('NO TEST COVERAGE', () => {});
});

View File

@@ -0,0 +1,66 @@
import ColorPropType from '../../propTypes/ColorPropType';
import React, { Component, PropTypes } from 'react';
import StyleSheet from '../../apis/StyleSheet';
import TouchableOpacity from '../Touchable/TouchableOpacity';
import Text from '../Text';
class Button extends Component {
static propTypes = {
accessibilityLabel: PropTypes.string,
color: ColorPropType,
disabled: PropTypes.bool,
onPress: PropTypes.func.isRequired,
title: PropTypes.string.isRequired
};
render() {
const {
accessibilityLabel,
color,
disabled,
onPress,
title
} = this.props;
return (
<TouchableOpacity
accessibilityLabel={accessibilityLabel}
accessibilityRole={'button'}
disabled={disabled}
onPress={onPress}
style={[
styles.button,
color && { backgroundColor: color },
disabled && styles.buttonDisabled
]}>
<Text style={[
styles.text,
disabled && styles.textDisabled
]}>
{title}
</Text>
</TouchableOpacity>
);
}
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#2196F3',
borderRadius: 2
},
text: {
textAlign: 'center',
color: '#fff',
padding: 8,
fontWeight: '500'
},
buttonDisabled: {
backgroundColor: '#dfdfdf'
},
textDisabled: {
color: '#a1a1a1'
}
});
module.exports = Button;

File diff suppressed because it is too large Load Diff

View File

@@ -91,4 +91,10 @@ describe('components/Image', () => {
const component = renderer.create(<Image testID='testID' />);
expect(component.toJSON()).toMatchSnapshot();
});
test('passes other props through to underlying View', () => {
const fn = () => {};
const component = renderer.create(<Image onResponderGrant={fn} />);
expect(component.toJSON()).toMatchSnapshot();
});
});

View File

@@ -1,13 +1,16 @@
/* global window */
import applyNativeMethods from '../../modules/applyNativeMethods';
import BaseComponentPropTypes from '../../propTypes/BaseComponentPropTypes';
import ImageResizeMode from './ImageResizeMode';
import ImageLoader from '../../modules/ImageLoader';
import ImageStylePropTypes from './ImageStylePropTypes';
import requestAnimationFrame from 'fbjs/lib/requestAnimationFrame';
import StyleSheet from '../../apis/StyleSheet';
import StyleSheetPropType from '../../propTypes/StyleSheetPropType';
import View from '../View';
import React, { Component, PropTypes } from 'react';
const emptyObject = {};
const STATUS_ERRORED = 'ERRORED';
const STATUS_LOADED = 'LOADED';
const STATUS_LOADING = 'LOADING';
@@ -38,7 +41,7 @@ class Image extends Component {
static displayName = 'Image';
static propTypes = {
...BaseComponentPropTypes,
...View.propTypes,
children: PropTypes.any,
defaultSource: ImageSourcePropType,
onError: PropTypes.func,
@@ -52,14 +55,22 @@ class Image extends Component {
};
static defaultProps = {
style: {}
style: emptyObject
};
static getSize(uri, success, failure) {
ImageLoader.getSize(uri, success, failure);
}
static prefetch(uri) {
return ImageLoader.prefetch(uri);
}
static resizeMode = ImageResizeMode;
constructor(props, context) {
super(props, context);
this.state = { isLoaded: false };
this.state = { shouldDisplaySource: false };
const uri = resolveAssetSource(props.source);
this._imageState = uri ? STATUS_PENDING : STATUS_IDLE;
this._isMounted = false;
@@ -73,7 +84,7 @@ class Image extends Component {
}
componentDidUpdate() {
if (this._imageState === STATUS_PENDING && !this.image) {
if (this._imageState === STATUS_PENDING) {
this._createImageLoader();
}
}
@@ -91,7 +102,7 @@ class Image extends Component {
}
render() {
const { isLoaded } = this.state;
const { shouldDisplaySource } = this.state;
const {
accessibilityLabel,
accessible,
@@ -99,27 +110,36 @@ class Image extends Component {
defaultSource,
onLayout,
source,
testID
testID,
/* eslint-disable */
onError,
onLoad,
onLoadEnd,
onLoadStart,
resizeMode,
/* eslint-enable */
...other
} = this.props;
const displayImage = resolveAssetSource(!isLoaded ? defaultSource : source);
const imageSizeStyle = resolveAssetDimensions(!isLoaded ? defaultSource : source);
const displayImage = resolveAssetSource(shouldDisplaySource ? source : defaultSource);
const imageSizeStyle = resolveAssetDimensions(shouldDisplaySource ? source : defaultSource);
const backgroundImage = displayImage ? `url("${displayImage}")` : null;
const originalStyle = StyleSheet.flatten(this.props.style);
const resizeMode = this.props.resizeMode || originalStyle.resizeMode || ImageResizeMode.cover;
const finalResizeMode = resizeMode || originalStyle.resizeMode || ImageResizeMode.cover;
const style = StyleSheet.flatten([
styles.initial,
imageSizeStyle,
originalStyle,
backgroundImage && { backgroundImage },
resizeModeStyles[resizeMode]
resizeModeStyles[finalResizeMode],
backgroundImage && { backgroundImage }
]);
// View doesn't support 'resizeMode' as a style
delete style.resizeMode;
return (
<View
{...other}
accessibilityLabel={accessibilityLabel}
accessibilityRole='img'
accessible={accessible}
@@ -132,28 +152,21 @@ class Image extends Component {
}
_createImageLoader() {
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 = uri;
const uri = resolveAssetSource(this.props.source);
this._imageRequestId = ImageLoader.load(uri, this._onLoad, this._onError);
this._onLoadStart();
}
_destroyImageLoader() {
if (this.image) {
this.image.onerror = null;
this.image.onload = null;
this.image = null;
if (this._imageRequestId) {
ImageLoader.abort(this._imageRequestId);
this._imageRequestId = null;
}
}
_onError = () => {
const { onError, source } = this.props;
this._destroyImageLoader();
this._onLoadEnd();
this._updateImageState(STATUS_ERRORED);
if (onError) {
onError({
@@ -162,13 +175,13 @@ class Image extends Component {
}
});
}
this._onLoadEnd();
}
_onLoad = (e) => {
const { onLoad } = this.props;
const event = { nativeEvent: e };
this._destroyImageLoader();
this._updateImageState(STATUS_LOADED);
if (onLoad) { onLoad(event); }
this._onLoadEnd();
@@ -187,11 +200,12 @@ class Image extends Component {
_updateImageState(status) {
this._imageState = status;
const isLoaded = this._imageState === STATUS_LOADED;
if (isLoaded !== this.state.isLoaded) {
window.requestAnimationFrame(() => {
const shouldDisplaySource = this._imageState === STATUS_LOADED || this._imageState === STATUS_LOADING;
// only triggers a re-render when the image is loading (to support PJEG), loaded, or failed
if (shouldDisplaySource !== this.state.shouldDisplaySource) {
requestAnimationFrame(() => {
if (this._isMounted) {
this.setState({ isLoaded });
this.setState({ shouldDisplaySource });
}
});
}

View File

@@ -222,6 +222,10 @@ class ListViewDataSource {
return this._cachedRowCount;
}
getRowAndSectionCount(): number {
return (this._cachedRowCount + this.sectionIdentities.length);
}
/**
* Returns if the row is dirtied and needs to be rerendered
*/

View File

@@ -2,18 +2,28 @@ import applyNativeMethods from '../../modules/applyNativeMethods';
import ListViewDataSource from './ListViewDataSource';
import ListViewPropTypes from './ListViewPropTypes';
import ScrollView from '../ScrollView';
import View from '../View';
import StaticRenderer from '../StaticRenderer';
import React, { Component } from 'react';
import isEmpty from 'fbjs/lib/isEmpty';
import requestAnimationFrame from 'fbjs/lib/requestAnimationFrame';
const DEFAULT_PAGE_SIZE = 1;
const DEFAULT_INITIAL_ROWS = 10;
const DEFAULT_SCROLL_RENDER_AHEAD = 1000;
const DEFAULT_END_REACHED_THRESHOLD = 1000;
const DEFAULT_SCROLL_CALLBACK_THROTTLE = 50;
class ListView extends Component {
static propTypes = ListViewPropTypes;
static defaultProps = {
initialListSize: 10,
pageSize: 1,
initialListSize: DEFAULT_INITIAL_ROWS,
pageSize: DEFAULT_PAGE_SIZE,
renderScrollComponent: (props) => <ScrollView {...props} />,
scrollRenderAheadDistance: 1000,
onEndReachedThreshold: 1000,
scrollRenderAheadDistance: DEFAULT_SCROLL_RENDER_AHEAD,
onEndReachedThreshold: DEFAULT_END_REACHED_THRESHOLD,
scrollEventThrottle: DEFAULT_SCROLL_CALLBACK_THROTTLE,
removeClippedSubviews: true,
stickyHeaderIndices: []
};
@@ -26,6 +36,48 @@ class ListView extends Component {
highlightedRow: {}
};
this.onRowHighlighted = (sectionId, rowId) => this._onRowHighlighted(sectionId, rowId);
this.scrollProperties = {};
}
componentWillMount() {
// this data should never trigger a render pass, so don't put in state
this.scrollProperties = {
visibleLength: null,
contentLength: null,
offset: 0
};
this._childFrames = [];
this._visibleRows = {};
this._prevRenderedRowsCount = 0;
this._sentEndForContentLength = null;
}
componentDidMount() {
// do this in animation frame until componentDidMount actually runs after
// the component is laid out
requestAnimationFrame(() => {
this._measureAndUpdateScrollProps();
});
}
componentWillReceiveProps(nextProps: Object) {
if (this.props.dataSource !== nextProps.dataSource || this.props.initialListSize !== nextProps.initialListSize) {
this.setState((state, props) => {
this._prevRenderedRowsCount = 0;
return {
curRenderedRowsCount: Math.min(
Math.max(state.curRenderedRowsCount, props.initialListSize),
props.enableEmptySections ? props.dataSource.getRowAndSectionCount() : props.dataSource.getRowCount()
)
};
}, () => this._renderMoreRowsIfNeeded());
}
}
componentDidUpdate() {
requestAnimationFrame(() => {
this._measureAndUpdateScrollProps();
});
}
getScrollResponder() {
@@ -40,58 +92,313 @@ class ListView extends Component {
return this._scrollViewRef && this._scrollViewRef.setNativeProps(props);
}
_onRowHighlighted(sectionId, rowId) {
_onRowHighlighted = (sectionId, rowId) => {
this.setState({ highlightedRow: { sectionId, rowId } });
}
renderSectionHeaderFn = (data, sectionID) => {
return () => this.props.renderSectionHeader(data, sectionID);
}
renderRowFn = (data, sectionID, rowID) => {
return () => this.props.renderRow(data, sectionID, rowID, this._onRowHighlighted);
}
render() {
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 = <View key={key}>{renderSectionHeader(section, sectionId)}</View>;
children.push(child);
const dataSource = this.props.dataSource;
const allRowIDs = dataSource.rowIdentities;
let rowCount = 0;
const sectionHeaderIndices = [];
const header = this.props.renderHeader && this.props.renderHeader();
const footer = this.props.renderFooter && this.props.renderFooter();
let totalIndex = header ? 1 : 0;
for (let sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) {
const sectionID = dataSource.sectionIdentities[sectionIdx];
const rowIDs = allRowIDs[sectionIdx];
if (rowIDs.length === 0) {
if (this.props.enableEmptySections === undefined) {
const warning = require('fbjs/lib/warning');
warning(false, 'In next release empty section headers will be rendered.' +
' In this release you can use \'enableEmptySections\' flag to render empty section headers.');
continue;
} else {
const invariant = require('fbjs/lib/invariant');
invariant(
this.props.enableEmptySections,
'In next release \'enableEmptySections\' flag will be deprecated,' +
' empty section headers will always be rendered. If empty section headers' +
' are not desirable their indices should be excluded from sectionIDs object.' +
' In this release \'enableEmptySections\' may only have value \'true\'' +
' to allow empty section headers rendering.');
}
}
// 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 = <View key={key}>{renderRow(row, sectionId, rowId, this.onRowHighlighted)}</View>;
children.push(child);
if (this.props.renderSectionHeader) {
const shouldUpdateHeader = rowCount >= this._prevRenderedRowsCount &&
dataSource.sectionHeaderShouldUpdate(sectionIdx);
children.push(
<StaticRenderer
key={`s_${sectionID}`}
render={this.renderSectionHeaderFn(
dataSource.getSectionHeaderData(sectionIdx),
sectionID
)}
shouldUpdate={!!shouldUpdateHeader}
/>
);
sectionHeaderIndices.push(totalIndex++);
}
// render optional separator
if (renderSeparator && ((rowIdx !== rows.length - 1) || (sectionIdx === sections.length - 1))) {
for (let rowIdx = 0; rowIdx < rowIDs.length; rowIdx++) {
const rowID = rowIDs[rowIdx];
const comboID = `${sectionID}_${rowID}`;
const shouldUpdateRow = rowCount >= this._prevRenderedRowsCount &&
dataSource.rowShouldUpdate(sectionIdx, rowIdx);
const row =
<StaticRenderer
key={`r_${comboID}`}
render={this.renderRowFn(
dataSource.getRowData(sectionIdx, rowIdx),
sectionID,
rowID
)}
shouldUpdate={!!shouldUpdateRow}
/>;
children.push(row);
totalIndex++;
if (this.props.renderSeparator &&
(rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.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);
this.state.highlightedRow.sectionID === sectionID && (
this.state.highlightedRow.rowID === rowID ||
this.state.highlightedRow.rowID === rowIDs[rowIdx + 1]
);
const separator = this.props.renderSeparator(
sectionID,
rowID,
adjacentRowHighlighted
);
if (separator) {
children.push(separator);
totalIndex++;
}
}
if (++rowCount === this.state.curRenderedRowsCount) {
break;
}
}
if (rowCount >= this.state.curRenderedRowsCount) {
break;
}
}
return React.cloneElement(this.props.renderScrollComponent(this.props), {
ref: this._setScrollViewRef
const {
renderScrollComponent,
...props
} = this.props;
Object.assign(props, {
onScroll: this._onScroll,
stickyHeaderIndices: this.props.stickyHeaderIndices.concat(sectionHeaderIndices),
// Do not pass these events downstream to ScrollView since they will be
// registered in ListView's own ScrollResponder.Mixin
onKeyboardWillShow: undefined,
onKeyboardWillHide: undefined,
onKeyboardDidShow: undefined,
onKeyboardDidHide: undefined
});
return React.cloneElement(renderScrollComponent(props), {
ref: this._setScrollViewRef,
onContentSizeChange: this._onContentSizeChange,
onLayout: this._onLayout
}, header, children, footer);
}
_measureAndUpdateScrollProps() {
const scrollComponent = this.getScrollResponder();
if (!scrollComponent || !scrollComponent.getInnerViewNode) {
return;
}
this._updateVisibleRows();
}
_onLayout = (event: Object) => {
const { width, height } = event.nativeEvent.layout;
const visibleLength = !this.props.horizontal ? height : width;
if (visibleLength !== this.scrollProperties.visibleLength) {
this.scrollProperties.visibleLength = visibleLength;
this._updateVisibleRows();
this._renderMoreRowsIfNeeded();
}
this.props.onLayout && this.props.onLayout(event);
}
_updateVisibleRows(updatedFrames?: Array<Object>) {
if (!this.props.onChangeVisibleRows) {
return; // No need to compute visible rows if there is no callback
}
if (updatedFrames) {
updatedFrames.forEach((newFrame) => {
this._childFrames[newFrame.index] = Object.assign({}, newFrame);
});
}
const isVertical = !this.props.horizontal;
const dataSource = this.props.dataSource;
const visibleMin = this.scrollProperties.offset;
const visibleMax = visibleMin + this.scrollProperties.visibleLength;
const allRowIDs = dataSource.rowIdentities;
const header = this.props.renderHeader && this.props.renderHeader();
let totalIndex = header ? 1 : 0;
let visibilityChanged = false;
const changedRows = {};
for (let sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) {
const rowIDs = allRowIDs[sectionIdx];
if (rowIDs.length === 0) {
continue;
}
const sectionID = dataSource.sectionIdentities[sectionIdx];
if (this.props.renderSectionHeader) {
totalIndex++;
}
let visibleSection = this._visibleRows[sectionID];
if (!visibleSection) {
visibleSection = {};
}
for (let rowIdx = 0; rowIdx < rowIDs.length; rowIdx++) {
const rowID = rowIDs[rowIdx];
const frame = this._childFrames[totalIndex];
totalIndex++;
if (this.props.renderSeparator &&
(rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1)) {
totalIndex++;
}
if (!frame) {
break;
}
const rowVisible = visibleSection[rowID];
const min = isVertical ? frame.y : frame.x;
const max = min + (isVertical ? frame.height : frame.width);
if ((!min && !max) || (min === max)) {
break;
}
if (min > visibleMax || max < visibleMin) {
if (rowVisible) {
visibilityChanged = true;
delete visibleSection[rowID];
if (!changedRows[sectionID]) {
changedRows[sectionID] = {};
}
changedRows[sectionID][rowID] = false;
}
} else if (!rowVisible) {
visibilityChanged = true;
visibleSection[rowID] = true;
if (!changedRows[sectionID]) {
changedRows[sectionID] = {};
}
changedRows[sectionID][rowID] = true;
}
}
if (!isEmpty(visibleSection)) {
this._visibleRows[sectionID] = visibleSection;
} else if (this._visibleRows[sectionID]) {
delete this._visibleRows[sectionID];
}
}
visibilityChanged && this.props.onChangeVisibleRows(this._visibleRows, changedRows);
}
_onContentSizeChange = (width: number, height: number) => {
const contentLength = !this.props.horizontal ? height : width;
if (contentLength !== this.scrollProperties.contentLength) {
this.scrollProperties.contentLength = contentLength;
this._updateVisibleRows();
this._renderMoreRowsIfNeeded();
}
this.props.onContentSizeChange && this.props.onContentSizeChange(width, height);
}
_getDistanceFromEnd(scrollProperties: Object) {
return scrollProperties.contentLength - scrollProperties.visibleLength - scrollProperties.offset;
}
_maybeCallOnEndReached(event?: Object) {
if (this.props.onEndReached &&
this.scrollProperties.contentLength !== this._sentEndForContentLength &&
this._getDistanceFromEnd(this.scrollProperties) < this.props.onEndReachedThreshold &&
this.state.curRenderedRowsCount === (this.props.enableEmptySections ?
this.props.dataSource.getRowAndSectionCount() : this.props.dataSource.getRowCount())) {
this._sentEndForContentLength = this.scrollProperties.contentLength;
this.props.onEndReached(event);
return true;
}
return false;
}
_renderMoreRowsIfNeeded() {
if (this.scrollProperties.contentLength === null ||
this.scrollProperties.visibleLength === null ||
this.state.curRenderedRowsCount === (this.props.enableEmptySections ?
this.props.dataSource.getRowAndSectionCount() : this.props.dataSource.getRowCount())) {
this._maybeCallOnEndReached();
return;
}
const distanceFromEnd = this._getDistanceFromEnd(this.scrollProperties);
if (distanceFromEnd < this.props.scrollRenderAheadDistance) {
this._pageInNewRows();
}
}
_pageInNewRows() {
this.setState((state, props) => {
const rowsToRender = Math.min(
state.curRenderedRowsCount + props.pageSize,
(props.enableEmptySections ? props.dataSource.getRowAndSectionCount() : props.dataSource.getRowCount())
);
this._prevRenderedRowsCount = state.curRenderedRowsCount;
return {
curRenderedRowsCount: rowsToRender
};
}, () => {
this._measureAndUpdateScrollProps();
this._prevRenderedRowsCount = this.state.curRenderedRowsCount;
});
}
_onScroll = (e: Object) => {
const isVertical = !this.props.horizontal;
this.scrollProperties.visibleLength = e.nativeEvent.layoutMeasurement[
isVertical ? 'height' : 'width'
];
this.scrollProperties.contentLength = e.nativeEvent.contentSize[
isVertical ? 'height' : 'width'
];
this.scrollProperties.offset = e.nativeEvent.contentOffset[
isVertical ? 'y' : 'x'
];
this._updateVisibleRows(e.nativeEvent.updatedChildFrames);
if (!this._maybeCallOnEndReached(e)) {
this._renderMoreRowsIfNeeded();
}
if (this.props.onEndReached &&
this._getDistanceFromEnd(this.scrollProperties) > this.props.onEndReachedThreshold) {
// Scrolled out of the end zone, so it should be able to trigger again.
this._sentEndForContentLength = null;
}
this.props.onScroll && this.props.onScroll(e);
};
_setScrollViewRef = (component) => {
this._scrollViewRef = component;
}

View File

@@ -1,13 +1,9 @@
import Animated from '../../apis/Animated';
import applyNativeMethods from '../../modules/applyNativeMethods';
import ColorPropType from '../../propTypes/ColorPropType';
import StyleSheet from '../../apis/StyleSheet';
import View from '../View';
import React, { Component, PropTypes } from 'react';
const indeterminateWidth = '25%';
const translateInterpolation = { inputRange: [ 0, 1 ], outputRange: [ '-100%', '400%' ] };
class ProgressBar extends Component {
static displayName = 'ProgressBar';
@@ -26,19 +22,12 @@ class ProgressBar extends Component {
trackColor: 'transparent'
};
constructor(props) {
super(props);
this.state = {
animationTranslate: new Animated.Value(0)
};
}
componentDidMount() {
this._manageAnimation();
this._updateProgressWidth();
}
componentDidUpdate() {
this._manageAnimation();
this._updateProgressWidth();
}
render() {
@@ -51,9 +40,7 @@ class ProgressBar extends Component {
...other
} = this.props;
const { animationTranslate } = this.state;
const percentageProgress = indeterminate ? 50 : progress * 100;
const percentageProgress = progress * 100;
return (
<View {...other}
@@ -67,42 +54,29 @@ class ProgressBar extends Component {
{ backgroundColor: trackColor }
]}
>
<Animated.View style={[
styles.progress,
{ backgroundColor: color },
indeterminate ? {
transform: [
{ translateX: animationTranslate.interpolate(translateInterpolation) }
],
width: indeterminateWidth
} : {
width: `${percentageProgress}%`
}
]} />
<View
ref={this._setProgressRef}
style={[
styles.progress,
indeterminate && styles.animation,
{ backgroundColor: color }
]}
/>
</View>
);
}
_manageAnimation() {
const { animationTranslate } = this.state;
_setProgressRef = (component) => {
this._progressRef = component;
}
const cycleAnimation = (animation) => {
animation.setValue(0);
Animated.timing(animation, {
duration: 1000,
toValue: 1
}).start((event) => {
if (event.finished) {
cycleAnimation(animation);
}
});
};
if (this.props.indeterminate) {
cycleAnimation(animationTranslate);
} else {
animationTranslate.stopAnimation();
}
_updateProgressWidth = () => {
const { indeterminate, progress } = this.props;
const percentageProgress = indeterminate ? 50 : progress * 100;
const width = indeterminate ? '25%' : `${percentageProgress}%`;
this._progressRef.setNativeProps({
style: { width }
});
}
}
@@ -114,6 +88,12 @@ const styles = StyleSheet.create({
},
progress: {
height: '100%'
},
animation: {
animationDuration: '1s',
animationName: 'rn-ProgressBar-animation',
animationTimingFunction: 'linear',
animationIterationCount: 'infinite'
}
});

View File

@@ -10,6 +10,35 @@ import debounce from 'debounce';
import View from '../View';
import React, { Component, PropTypes } from 'react';
const normalizeScrollEvent = (e) => ({
nativeEvent: {
contentOffset: {
get x() {
return e.target.scrollLeft;
},
get y() {
return e.target.scrollTop;
}
},
contentSize: {
get height() {
return e.target.scrollHeight;
},
get width() {
return e.target.scrollWidth;
}
},
layoutMeasurement: {
get height() {
return e.target.offsetHeight;
},
get width() {
return e.target.offsetWidth;
}
}
}
});
/**
* Encapsulates the Web-specific scroll throttling and disabling logic
*/
@@ -75,13 +104,13 @@ export default class ScrollViewBase extends Component {
_handleScrollTick(e) {
const { onScroll } = this.props;
this._state.scrollLastTick = Date.now();
if (onScroll) { onScroll(e); }
if (onScroll) { onScroll(normalizeScrollEvent(e)); }
}
_handleScrollEnd(e) {
const { onScroll } = this.props;
this._state.isScrolling = false;
if (onScroll) { onScroll(e); }
if (onScroll) { onScroll(normalizeScrollEvent(e)); }
}
_shouldEmitScrollEvent(lastTick, eventThrottle) {

View File

@@ -17,6 +17,8 @@ import View from '../View';
import ViewStylePropTypes from '../View/ViewStylePropTypes';
import React, { Component, PropTypes } from 'react';
const emptyObject = {};
/* eslint-disable react/prefer-es6-class */
const ScrollView = React.createClass({
propTypes: {
@@ -79,7 +81,7 @@ const ScrollView = React.createClass({
if (typeof y === 'number') {
console.warn('`scrollTo(y, x, animated)` is deprecated. Use `scrollTo({x: 5, y: 5, animated: true})` instead.');
} else {
({ x, y, animated } = y || {});
({ x, y, animated } = y || emptyObject);
}
this.getScrollResponder().scrollResponderScrollTo({ x: x || 0, y: y || 0, animated: animated !== false });
@@ -236,7 +238,6 @@ const styles = StyleSheet.create({
overflowY: 'hidden'
},
contentContainer: {
flexGrow: 1,
transform: [ { translateZ: 0 } ]
},
contentContainerHorizontal: {

View File

@@ -7,6 +7,7 @@ import UIManager from '../../apis/UIManager';
import View from '../View';
import React, { Component, PropTypes } from 'react';
const emptyObject = {};
const thumbDefaultBoxShadow = '0px 1px 3px rgba(0,0,0,0.5)';
const thumbFocusedBoxShadow = `${thumbDefaultBoxShadow}, 0 0 0 10px rgba(0,0,0,0.1)`;
@@ -28,7 +29,7 @@ class Switch extends Component {
activeThumbColor: '#009688',
activeTrackColor: '#A3D3CF',
disabled: false,
style: {},
style: emptyObject,
thumbColor: '#FAFAFA',
trackColor: '#939393',
value: false
@@ -88,7 +89,6 @@ class Switch extends Component {
const thumbStyle = [
styles.thumb,
{
alignSelf: value ? 'flex-end' : 'flex-start',
backgroundColor: thumbCurrentColor,
height: thumbHeight,
width: thumbWidth
@@ -110,7 +110,16 @@ class Switch extends Component {
return (
<View {...other} style={rootStyle}>
<View style={trackStyle} />
<View ref={this._setThumbRef} style={thumbStyle} />
<View
ref={this._setThumbRef}
style={[
thumbStyle,
value && styles.thumbOn,
{
marginLeft: value ? multiplyStyleLengthValue(thumbWidth, -1) : 0
}
]}
/>
{nativeControl}
</View>
);
@@ -151,16 +160,24 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFillObject,
height: '70%',
margin: 'auto',
transition: 'background-color 0.1s',
transitionDuration: '0.1s',
width: '90%'
},
disabledTrack: {
backgroundColor: '#D5D5D5'
},
thumb: {
alignSelf: 'flex-start',
borderRadius: '100%',
boxShadow: thumbDefaultBoxShadow,
transition: 'background-color 0.1s'
left: '0%',
transform: [
{ translateZ: 0 }
],
transitionDuration: '0.1s'
},
thumbOn: {
left: '100%'
},
disabledThumb: {
backgroundColor: '#BDBDBD'

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