mirror of
https://github.com/zhigang1992/react-native-web.git
synced 2026-03-30 23:23:35 +08:00
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3564bbf840 | ||
|
|
297b2e5afb | ||
|
|
215697234e | ||
|
|
9efa7e94bd | ||
|
|
c44da41497 | ||
|
|
331c92fb3a | ||
|
|
26758e905c | ||
|
|
a15b15c55d | ||
|
|
f0202dbe61 | ||
|
|
d4d67dafc0 | ||
|
|
579bdeb8a5 | ||
|
|
7132a18440 | ||
|
|
18881b1edb | ||
|
|
4d1e7d8c0b | ||
|
|
a7158aeb6f | ||
|
|
03d413bca4 | ||
|
|
aef5efbad3 | ||
|
|
8fb8645723 | ||
|
|
d69406b4b1 | ||
|
|
2c2a96a183 | ||
|
|
b4a3053b5b | ||
|
|
24836afd6a | ||
|
|
c46f242f6b | ||
|
|
1940868065 | ||
|
|
65a9317756 | ||
|
|
3da05c48b0 | ||
|
|
f33312a4dd | ||
|
|
4516c72296 | ||
|
|
7f94c4bf06 | ||
|
|
37781171aa | ||
|
|
22f45e350b | ||
|
|
af40f98f23 | ||
|
|
eca2f69593 | ||
|
|
d03d89ac71 | ||
|
|
393a6ef835 | ||
|
|
36e89d5275 | ||
|
|
d53d1e6e56 | ||
|
|
2cb68a45be | ||
|
|
b56b8e494a | ||
|
|
60ad0e9ec5 | ||
|
|
f2ea7c089c | ||
|
|
a3b59ed2b4 | ||
|
|
a378d3cce2 | ||
|
|
462f9793ea | ||
|
|
ae38bb538c | ||
|
|
93d1488cc7 | ||
|
|
a16e542bd8 | ||
|
|
62cd335788 | ||
|
|
288e14cd70 | ||
|
|
71cfd23624 | ||
|
|
77b8e4a1fc | ||
|
|
9543a79c3f | ||
|
|
e3eea6e132 | ||
|
|
4d3418a968 | ||
|
|
ea9bc734f1 | ||
|
|
e03af435ac | ||
|
|
97c0a31ce6 | ||
|
|
25d11ded46 | ||
|
|
6a73d77030 | ||
|
|
0b63ba4e89 | ||
|
|
51109d0768 | ||
|
|
ac04ecd69e | ||
|
|
1a670ba6a7 | ||
|
|
7a16d5711c | ||
|
|
9dde70fff5 | ||
|
|
203980ab66 | ||
|
|
924dc36d4a | ||
|
|
9b2421cdfa |
7
.babelrc
7
.babelrc
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015",
|
||||
"stage-1",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-decorators-legacy"
|
||||
"react-native"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "5"
|
||||
- "4"
|
||||
- "6"
|
||||
before_script:
|
||||
- export DISPLAY=:99.0
|
||||
- sh -e /etc/init.d/xvfb start
|
||||
|
||||
11
README.md
11
README.md
@@ -20,7 +20,7 @@ vendor prefixes), or require build tool configuration. This project allows
|
||||
components built upon React Native to be run on the Web, and it manages all
|
||||
component styling out-of-the-box.
|
||||
|
||||
For example, the [`View`](docs/apis/View.md) component makes it easy to build
|
||||
For example, the [`View`](docs/components/View.md) component makes it easy to build
|
||||
cross-browser layouts with flexbox, such as stacked and nested boxes with
|
||||
margin and padding. And the [`StyleSheet`](docs/guides/style.md) API converts
|
||||
styles defined in JavaScript into "Atomic CSS".
|
||||
@@ -30,11 +30,14 @@ styles defined in JavaScript into "Atomic CSS".
|
||||
To install in your app:
|
||||
|
||||
```
|
||||
npm install --save react@0.14 react-dom@0.14 react-native-web
|
||||
npm install --save react react-native-web
|
||||
```
|
||||
|
||||
Read the [Client and Server rendering](docs/guides/rendering.md) guide.
|
||||
|
||||
You can also bootstrap a standard React Native project structure for web by
|
||||
using [react-native-web-starter](https://github.com/grabcode/react-native-web-starter).
|
||||
|
||||
## Examples
|
||||
|
||||
Demos:
|
||||
@@ -46,7 +49,8 @@ Demos:
|
||||
Sample:
|
||||
|
||||
```js
|
||||
import React, { AppRegistry, Image, StyleSheet, Text, View } from 'react-native'
|
||||
import React from 'react'
|
||||
import { AppRegistry, Image, StyleSheet, Text, View } from 'react-native'
|
||||
|
||||
// Components
|
||||
const Card = ({ children }) => <View style={styles.card}>{children}</View>
|
||||
@@ -98,7 +102,6 @@ Exported modules:
|
||||
* [`ActivityIndicator`](docs/components/ActivityIndicator.md)
|
||||
* [`Image`](docs/components/Image.md)
|
||||
* [`ListView`](docs/components/ListView.md)
|
||||
* [`Portal`](docs/components/Portal.md)
|
||||
* [`ScrollView`](docs/components/ScrollView.md)
|
||||
* [`Text`](docs/components/Text.md)
|
||||
* [`TextInput`](docs/components/TextInput.md)
|
||||
|
||||
@@ -16,8 +16,11 @@ into `runApplication`. These should always be used as a pair.
|
||||
(web) static **prerenderApplication**(appKey:string, appParameters: object)
|
||||
|
||||
Renders the given application to an HTML string. Use this for server-side
|
||||
rendering. Return object is of type `{ html: string; style: string; }`, where
|
||||
`html` the prerendered HTML, and `style` is the prerendered style sheet.
|
||||
rendering. Return object is of type `{ html: string; style: string;
|
||||
styleElement: ReactComponent }`. `html` is the prerendered HTML, `style` is the
|
||||
prerendered style sheet, and `styleElement` is a React Component. It's
|
||||
recommended that you use `styleElement` to render the style sheet in an app
|
||||
shell.
|
||||
|
||||
static **registerConfig**(config: Array<AppConfig>)
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ class Example extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { currentAppState: AppState.currentState }
|
||||
this._handleAppStateChange = this._handleAppStateChange.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -48,7 +47,7 @@ class Example extends React.Component {
|
||||
AppState.removeEventListener('change', this._handleAppStateChange);
|
||||
}
|
||||
|
||||
_handleAppStateChange(currentAppState) {
|
||||
_handleAppStateChange = (currentAppState) => {
|
||||
this.setState({ currentAppState });
|
||||
}
|
||||
|
||||
|
||||
@@ -10,19 +10,36 @@ specific.
|
||||
|
||||
`Platform.OS` will be `web` when running in a Web browser.
|
||||
|
||||
**userAgent**: string
|
||||
|
||||
On Web, the `Platform` module can be also be used to detect the browser
|
||||
`userAgent`.
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
height: (Platform.OS === 'web') ? 200 : 100,
|
||||
});
|
||||
|
||||
if (Platform.userAgent.includes('Android')) {
|
||||
console.log('Running on Android!');
|
||||
}
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
**select**: any
|
||||
|
||||
`Platform.select` takes an object containing `Platform.OS` as keys and returns
|
||||
the value for the platform you are currently running on.
|
||||
|
||||
```js
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
const containerStyles = {
|
||||
flex: 1,
|
||||
...Platform.select({
|
||||
android: {
|
||||
backgroundColor: 'blue'
|
||||
},
|
||||
ios: {
|
||||
backgroundColor: 'red'
|
||||
},
|
||||
web: {
|
||||
backgroundColor: 'green'
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
@@ -11,12 +11,18 @@ outside of the render loop and are applied as inline styles. Read more about to
|
||||
|
||||
Each key of the object passed to `create` must define a style object.
|
||||
|
||||
**flatten**: function
|
||||
|
||||
Flattens an array of styles into a single style object.
|
||||
|
||||
**renderToString**: function
|
||||
|
||||
Returns a string of CSS used to style the application.
|
||||
|
||||
## Properties
|
||||
|
||||
**hairlineWidth**: number
|
||||
|
||||
**flatten**: function
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
|
||||
@@ -23,7 +23,8 @@ Size of the indicator. Small has a height of `20`, large has a height of `36`.
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { ActivityIndicator, Component, StyleSheet, View } from 'react-native'
|
||||
import React, { Component } from 'react'
|
||||
import { ActivityIndicator, StyleSheet, View } from 'react-native'
|
||||
|
||||
class ToggleAnimatingActivityIndicator extends Component {
|
||||
constructor(props) {
|
||||
|
||||
@@ -78,7 +78,8 @@ Example usage:
|
||||
|
||||
```js
|
||||
import placeholderAvatar from './placeholderAvatar.png'
|
||||
import React, { Component, Image, PropTypes, StyleSheet } from 'react-native'
|
||||
import React, { Component } from 'react'
|
||||
import { Image, PropTypes, StyleSheet } from 'react-native'
|
||||
|
||||
export default class ImageExample extends Component {
|
||||
constructor(props, context) {
|
||||
|
||||
@@ -15,7 +15,8 @@ Content to display over the image.
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { Component, ListView, PropTypes } from 'react-native'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { ListView } from 'react-native'
|
||||
|
||||
export default class ListViewExample extends Component {
|
||||
static propTypes = {}
|
||||
|
||||
@@ -33,7 +33,8 @@ React component to render. This same tag can later be used in `closeModal`.
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { Portal, Text, Touchable } from 'react-native'
|
||||
import React, { Component } from 'react'
|
||||
import { Portal, Text, Touchable } from 'react-native'
|
||||
|
||||
export default class PortalExample extends Component {
|
||||
componentWillMount() {
|
||||
|
||||
@@ -83,7 +83,8 @@ Scrolls to a given `x`, `y` offset (animation is not currently supported).
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { Component, ScrollView, StyleSheet } from 'react-native'
|
||||
import React, { Component } from 'react'
|
||||
import { ScrollView, StyleSheet } from 'react-native'
|
||||
import Item from './Item'
|
||||
|
||||
export default class ScrollViewExample extends Component {
|
||||
|
||||
@@ -60,7 +60,8 @@ This function is called on press.
|
||||
+ `letterSpacing`
|
||||
+ `lineHeight`
|
||||
+ `textAlign`
|
||||
+ `textDecoration`
|
||||
+ `textAlignVertical`
|
||||
+ `textDecorationLine`
|
||||
+ `textShadow`
|
||||
+ `textTransform`
|
||||
+ `whiteSpace`
|
||||
@@ -74,7 +75,8 @@ Used to locate this view in end-to-end tests.
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { Component, PropTypes, StyleSheet, Text } from 'react-native'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { StyleSheet, Text } from 'react-native'
|
||||
|
||||
export default class PrettyText extends Component {
|
||||
static propTypes = {
|
||||
|
||||
@@ -164,7 +164,8 @@ Focus the underlying DOM input.
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { Component, StyleSheet, TextInput } from 'react-native'
|
||||
import React, { Component } from 'react'
|
||||
import { StyleSheet, TextInput } from 'react-native'
|
||||
|
||||
export default class TextInputExample extends Component {
|
||||
constructor(props, context) {
|
||||
|
||||
@@ -184,7 +184,8 @@ Used to locate this view in end-to-end tests.
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { Component, PropTypes, StyleSheet, View } from 'react-native'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
|
||||
export default class ViewExample extends Component {
|
||||
render() {
|
||||
|
||||
@@ -40,11 +40,7 @@ module.exports = {
|
||||
Minor platform differences can use the `Platform` module.
|
||||
|
||||
```js
|
||||
import { AppRegistry, Platform, StyleSheet } from 'react-native'
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
height: (Platform.OS === 'web') ? 200 : 100
|
||||
})
|
||||
import { AppRegistry, Platform } from 'react-native'
|
||||
|
||||
AppRegistry.registerComponent('MyApp', () => MyApp)
|
||||
|
||||
|
||||
@@ -21,77 +21,38 @@ module.exports = {
|
||||
Rendering without using the `AppRegistry`:
|
||||
|
||||
```js
|
||||
import React from 'react-native'
|
||||
import React from 'react'
|
||||
import ReactNative from 'react-native'
|
||||
|
||||
// component that renders the app
|
||||
const AppHeaderContainer = (props) => { /* ... */ }
|
||||
|
||||
// DOM render
|
||||
React.render(<div />, document.getElementById('react-app'))
|
||||
ReactNative.render(<AppHeaderContainer />, document.getElementById('react-app-header'))
|
||||
|
||||
// Server render
|
||||
React.renderToString(<div />)
|
||||
React.renderToStaticMarkup(<div />)
|
||||
ReactNative.renderToString(<AppHeaderContainer />)
|
||||
ReactNative.renderToStaticMarkup(<AppHeaderContainer />)
|
||||
```
|
||||
|
||||
Rendering using the `AppRegistry`:
|
||||
|
||||
```
|
||||
// App.js
|
||||
|
||||
import React, { AppRegistry } from 'react-native'
|
||||
```js
|
||||
import React from 'react'
|
||||
import ReactNative, { AppRegistry } from 'react-native'
|
||||
|
||||
// component that renders the app
|
||||
const AppContainer = (props) => { /* ... */ }
|
||||
export default AppContainer
|
||||
```
|
||||
|
||||
```js
|
||||
// client.js
|
||||
|
||||
import React, { AppRegistry } from 'react-native'
|
||||
import App from './App'
|
||||
|
||||
// registers the app
|
||||
AppRegistry.registerComponent('App', () => App)
|
||||
|
||||
// mounts and runs the app within the `rootTag` DOM node
|
||||
AppRegistry.runApplication('App', { initialProps, rootTag: document.getElementById('react-app') })
|
||||
```
|
||||
|
||||
React Native for Web extends `AppRegistry` to provide support for server-side
|
||||
rendering.
|
||||
|
||||
```js
|
||||
// AppShell.js
|
||||
|
||||
import React from 'react-native'
|
||||
|
||||
const AppShell = (html, style) => (
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta content="initial-scale=1,width=device-width" name="viewport" />
|
||||
{style}
|
||||
</head>
|
||||
<body>
|
||||
<div id="react-app" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
export default AppShell
|
||||
```
|
||||
|
||||
```js
|
||||
// server.js
|
||||
|
||||
import React, { AppRegistry } from 'react-native'
|
||||
import App from './App'
|
||||
import AppShell from './AppShell'
|
||||
|
||||
// registers the app
|
||||
AppRegistry.registerComponent('App', () => App)
|
||||
|
||||
// prerenders the app
|
||||
const { html, style } = AppRegistry.prerenderApplication('App', { initialProps })
|
||||
|
||||
// renders the full-page markup
|
||||
const renderedApplicationHTML = React.renderToString(<AppShell html={html} style={style} />)
|
||||
|
||||
// register the app
|
||||
AppRegistry.registerComponent('App', () => AppContainer)
|
||||
|
||||
// DOM render
|
||||
AppRegistry.runApplication('App', {
|
||||
initialProps: {},
|
||||
rootTag: document.getElementById('react-app')
|
||||
})
|
||||
|
||||
// prerender the app
|
||||
const { html, style, styleElement } = AppRegistry.prerenderApplication('App', { initialProps })
|
||||
```
|
||||
|
||||
@@ -174,16 +174,16 @@ const styles = StyleSheet.create({
|
||||
CSS output:
|
||||
|
||||
```css
|
||||
._s1 { color: gray; }
|
||||
._s2 { font-size: 2rem; }
|
||||
._s3 { font-size: 1.25rem; }
|
||||
.__style1 { color: gray; }
|
||||
.__style2 { font-size: 2rem; }
|
||||
.__style3 { font-size: 1.25rem; }
|
||||
```
|
||||
|
||||
Rendered HTML:
|
||||
|
||||
```html
|
||||
<span className="_s1 _s2">Heading</span>
|
||||
<span className="_s1 _s3">Text</span>
|
||||
<span className="__style1 __style2">Heading</span>
|
||||
<span className="__style1 __style3">Text</span>
|
||||
```
|
||||
|
||||
### Reset
|
||||
@@ -200,6 +200,7 @@ html {
|
||||
font-family: sans-serif;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color:rgba(0,0,0,0)
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -214,12 +215,6 @@ input::-moz-focus-inner {
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
li {
|
||||
list-style:none
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var React = require('react');
|
||||
var ReactNative = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
AppRegistry,
|
||||
@@ -24,7 +25,7 @@ var {
|
||||
Text,
|
||||
TouchableBounce,
|
||||
View,
|
||||
} = React;
|
||||
} = ReactNative;
|
||||
|
||||
var GameBoard = require('./GameBoard');
|
||||
|
||||
|
||||
@@ -16,14 +16,15 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var React = require('react');
|
||||
var ReactNative = require('react-native');
|
||||
var {
|
||||
AppRegistry,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View,
|
||||
} = React;
|
||||
} = ReactNative;
|
||||
|
||||
class Board {
|
||||
grid: Array<Array<number>>;
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
import GridView from './GridView'
|
||||
import Heading from './Heading'
|
||||
import MediaQueryWidget from './MediaQueryWidget'
|
||||
import React, { Image, StyleSheet, ScrollView, Text, TextInput, TouchableHighlight, View } from 'react-native'
|
||||
import React from 'react'
|
||||
import { Image, StyleSheet, ScrollView, Text, TextInput, TouchableHighlight, View } from 'react-native'
|
||||
|
||||
export default class App extends React.Component {
|
||||
static propTypes = {
|
||||
mediaQuery: React.PropTypes.object,
|
||||
style: View.propTypes.style
|
||||
}
|
||||
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
scrollEnabled: true
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mediaQuery } = this.props
|
||||
const finalRootStyles = [
|
||||
rootStyles.common,
|
||||
mediaQuery.small.matches && rootStyles.mqSmall,
|
||||
mediaQuery.large.matches && rootStyles.mqLarge
|
||||
rootStyles.common
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -35,8 +31,6 @@ export default class App extends React.Component {
|
||||
scroll views – from which more complex components and apps can be
|
||||
constructed.</Text>
|
||||
|
||||
<MediaQueryWidget mediaQuery={mediaQuery} />
|
||||
|
||||
<Heading size='large'>Image</Heading>
|
||||
<Image
|
||||
accessibilityLabel='accessible image'
|
||||
@@ -96,7 +90,10 @@ export default class App extends React.Component {
|
||||
/>
|
||||
<TextInput secureTextEntry />
|
||||
<TextInput defaultValue='read only' editable={false} />
|
||||
<TextInput keyboardType='email-address' placeholder='you@domain.com' placeholderTextColor='red' />
|
||||
<TextInput
|
||||
style={{ flex:1, height: 60, padding: 20, fontSize: 20, textAlign: 'center' }}
|
||||
keyboardType='email-address' placeholder='you@domain.com' placeholderTextColor='red'
|
||||
/>
|
||||
<TextInput keyboardType='numeric' />
|
||||
<TextInput keyboardType='phone-pad' />
|
||||
<TextInput defaultValue='https://delete-me' keyboardType='url' placeholder='https://www.some-website.com' selectTextOnFocus />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { Component, PropTypes, StyleSheet, View } from 'react-native'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
|
||||
export default class GridView extends Component {
|
||||
static propTypes = {
|
||||
@@ -12,8 +13,8 @@ export default class GridView extends Component {
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
alley: '0',
|
||||
gutter: '0'
|
||||
alley: '0px',
|
||||
gutter: '0px'
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { StyleSheet, Text } from 'react-native'
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text } from 'react-native'
|
||||
|
||||
const Heading = ({ children, size = 'normal' }) => (
|
||||
<Text
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import React, { StyleSheet, Text, View } from 'react-native'
|
||||
|
||||
const MediaQueryWidget = ({ mediaQuery = {} }) => {
|
||||
const active = Object.keys(mediaQuery).reduce((active, alias) => {
|
||||
if (mediaQuery[alias].matches) {
|
||||
active = {
|
||||
alias,
|
||||
mql: mediaQuery[alias]
|
||||
}
|
||||
}
|
||||
return active
|
||||
}, {})
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<Text style={styles.heading}>Active Media Query</Text>
|
||||
<Text style={styles.text}>{`"${active.alias}"`} {active.mql && active.mql.media}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
marginVertical: 10,
|
||||
padding: 10
|
||||
},
|
||||
heading: {
|
||||
fontWeight: 'bold',
|
||||
padding: 5,
|
||||
textAlign: 'center'
|
||||
},
|
||||
text: {
|
||||
textAlign: 'center'
|
||||
}
|
||||
})
|
||||
|
||||
export default MediaQueryWidget
|
||||
@@ -1,7 +1,10 @@
|
||||
import React, { AppRegistry } from 'react-native'
|
||||
import { AppRegistry } from 'react-native'
|
||||
import App from './components/App'
|
||||
import Game2048 from './2048/Game2048'
|
||||
import TicTacToeApp from './TicTacToe/TicTacToe'
|
||||
|
||||
AppRegistry.runApplication('Game2048', {
|
||||
AppRegistry.registerComponent('App', () => App)
|
||||
|
||||
AppRegistry.runApplication('App', {
|
||||
rootTag: document.getElementById('react-root')
|
||||
})
|
||||
|
||||
@@ -28,6 +28,11 @@ module.exports = {
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
|
||||
}),
|
||||
new webpack.optimize.DedupePlugin(),
|
||||
// https://github.com/animatedjs/animated/issues/40
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/es6-set/,
|
||||
path.join(__dirname, '../src/modules/polyfills/Set.js')
|
||||
),
|
||||
new webpack.optimize.OccurenceOrderPlugin()
|
||||
],
|
||||
resolve: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var webpack = require('webpack')
|
||||
const webpack = require('webpack')
|
||||
|
||||
var testEntry = 'tests.webpack.js'
|
||||
const testEntry = 'tests.webpack.js'
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
@@ -19,17 +19,24 @@ module.exports = function (config) {
|
||||
'karma-chrome-launcher',
|
||||
'karma-firefox-launcher',
|
||||
'karma-mocha',
|
||||
'karma-mocha-reporter',
|
||||
'karma-sourcemap-loader',
|
||||
'karma-spec-reporter',
|
||||
'karma-webpack'
|
||||
],
|
||||
preprocessors: {
|
||||
[testEntry]: [ 'webpack', 'sourcemap' ]
|
||||
},
|
||||
reporters: process.env.TRAVIS ? [ 'dots' ] : [ 'spec' ],
|
||||
reporters: process.env.TRAVIS ? [ 'dots' ] : [ 'mocha' ],
|
||||
singleRun: true,
|
||||
webpack: {
|
||||
devtool: 'inline-source-map',
|
||||
// required by 'enzyme'
|
||||
externals: {
|
||||
'cheerio': 'window',
|
||||
'react/addons': true,
|
||||
'react/lib/ExecutionEnvironment': true,
|
||||
'react/lib/ReactContext': true
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
|
||||
71
package.json
71
package.json
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "react-native-web",
|
||||
"version": "0.0.19",
|
||||
"version": "0.0.32",
|
||||
"description": "React Native for Web",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rm -rf ./dist && mkdir dist && babel src -d dist --ignore **/__tests__,src/modules/specHelpers",
|
||||
"build": "del ./dist && mkdir dist && babel src -d dist --ignore **/__tests__",
|
||||
"build:umd": "webpack --config webpack.config.js --sort-assets-by --progress",
|
||||
"examples": "webpack-dev-server --config examples/webpack.config.js --inline --hot --colors --quiet",
|
||||
"lint": "eslint src",
|
||||
@@ -16,47 +16,46 @@
|
||||
"test:watch": "npm run test -- --no-single-run"
|
||||
},
|
||||
"dependencies": {
|
||||
"fbjs": "^0.7.2",
|
||||
"inline-style-prefix-all": "^1.0.3",
|
||||
"lodash.debounce": "^4.0.3",
|
||||
"react-textarea-autosize": "^3.1.0",
|
||||
"animated": "^0.1.3",
|
||||
"babel-runtime": "^6.9.2",
|
||||
"fbjs": "^0.8.1",
|
||||
"inline-style-prefix-all": "^2.0.2",
|
||||
"lodash": "^4.13.1",
|
||||
"react-dom": "^15.1.0",
|
||||
"react-textarea-autosize": "^4.0.2",
|
||||
"react-timer-mixin": "^0.13.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.3.17",
|
||||
"babel-core": "^6.3.13",
|
||||
"babel-eslint": "^4.1.6",
|
||||
"babel-loader": "^6.2.0",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-preset-es2015": "^6.3.13",
|
||||
"babel-preset-react": "^6.3.13",
|
||||
"babel-preset-stage-1": "^6.3.13",
|
||||
"babel-runtime": "^6.3.19",
|
||||
"eslint": "^1.10.3",
|
||||
"eslint-config-standard": "^4.4.0",
|
||||
"eslint-config-standard-react": "^1.2.1",
|
||||
"eslint-plugin-react": "^3.13.1",
|
||||
"eslint-plugin-standard": "^1.3.1",
|
||||
"karma": "^0.13.16",
|
||||
"karma-browserstack-launcher": "^0.1.8",
|
||||
"karma-chrome-launcher": "^0.2.2",
|
||||
"karma-firefox-launcher": "^0.1.7",
|
||||
"karma-mocha": "^0.2.1",
|
||||
"karma-sourcemap-loader": "^0.3.6",
|
||||
"karma-spec-reporter": "0.0.23",
|
||||
"babel-cli": "^6.10.1",
|
||||
"babel-core": "^6.10.4",
|
||||
"babel-eslint": "^6.1.0",
|
||||
"babel-loader": "^6.2.4",
|
||||
"babel-preset-react-native": "^1.9.0",
|
||||
"del-cli": "^0.2.0",
|
||||
"enzyme": "^2.3.0",
|
||||
"eslint": "^2.12.0",
|
||||
"eslint-config-standard": "^5.3.1",
|
||||
"eslint-config-standard-react": "^2.4.0",
|
||||
"eslint-plugin-promise": "^1.3.2",
|
||||
"eslint-plugin-react": "^5.1.1",
|
||||
"eslint-plugin-standard": "^1.3.2",
|
||||
"karma": "^0.13.22",
|
||||
"karma-browserstack-launcher": "^1.0.1",
|
||||
"karma-chrome-launcher": "^1.0.1",
|
||||
"karma-firefox-launcher": "^1.0.0",
|
||||
"karma-mocha": "^1.1.1",
|
||||
"karma-mocha-reporter": "^2.0.4",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-webpack": "^1.7.0",
|
||||
"mocha": "^2.3.4",
|
||||
"mocha": "^2.5.3",
|
||||
"node-libs-browser": "^0.5.3",
|
||||
"react": "^0.14.3",
|
||||
"react-addons-test-utils": "^0.14.3",
|
||||
"react-dom": "^0.14.3",
|
||||
"react-media-queries": "^2.0.1",
|
||||
"webpack": "^1.12.9",
|
||||
"webpack-dev-server": "^1.14.0"
|
||||
"react": "^15.1.0",
|
||||
"react-addons-test-utils": "^15.1.0",
|
||||
"webpack": "^1.13.1",
|
||||
"webpack-dev-server": "^1.14.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^0.14.3",
|
||||
"react-dom": "^0.14.3"
|
||||
"react": "^15.1.0"
|
||||
},
|
||||
"author": "Nicolas Gallagher",
|
||||
"license": "BSD-3-Clause",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import React from '..'
|
||||
|
||||
suite('ReactNativeWeb', () => {
|
||||
suite('exports', () => {
|
||||
test('React', () => {
|
||||
assert.ok(React)
|
||||
})
|
||||
|
||||
test('ReactDOM methods', () => {
|
||||
assert.ok(React.findDOMNode)
|
||||
assert.ok(React.render)
|
||||
assert.ok(React.unmountComponentAtNode)
|
||||
})
|
||||
|
||||
test('ReactDOM/server methods', () => {
|
||||
assert.ok(React.renderToString)
|
||||
assert.ok(React.renderToStaticMarkup)
|
||||
})
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,288 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule Interpolation
|
||||
* @flow
|
||||
*/
|
||||
/* eslint no-bitwise: 0 */
|
||||
'use strict';
|
||||
|
||||
/* @edit start */
|
||||
var normalizeColor = require('../StyleSheet/normalizeColor');
|
||||
var invariant = require('fbjs/lib/invariant');
|
||||
/* @edit end */
|
||||
|
||||
type ExtrapolateType = 'extend' | 'identity' | 'clamp';
|
||||
|
||||
export type InterpolationConfigType = {
|
||||
inputRange: Array<number>;
|
||||
outputRange: (Array<number> | Array<string>);
|
||||
easing?: ((input: number) => number);
|
||||
extrapolate?: ExtrapolateType;
|
||||
extrapolateLeft?: ExtrapolateType;
|
||||
extrapolateRight?: ExtrapolateType;
|
||||
};
|
||||
|
||||
var linear = (t) => t;
|
||||
|
||||
/**
|
||||
* Very handy helper to map input ranges to output ranges with an easing
|
||||
* function and custom behavior outside of the ranges.
|
||||
*/
|
||||
class Interpolation {
|
||||
static create(config: InterpolationConfigType): (input: number) => number | string {
|
||||
|
||||
if (config.outputRange && typeof config.outputRange[0] === 'string') {
|
||||
return createInterpolationFromStringOutputRange(config);
|
||||
}
|
||||
|
||||
var outputRange: Array<number> = (config.outputRange: any);
|
||||
checkInfiniteRange('outputRange', outputRange);
|
||||
|
||||
var inputRange = config.inputRange;
|
||||
checkInfiniteRange('inputRange', inputRange);
|
||||
checkValidInputRange(inputRange);
|
||||
|
||||
invariant(
|
||||
inputRange.length === outputRange.length,
|
||||
'inputRange (' + inputRange.length + ') and outputRange (' +
|
||||
outputRange.length + ') must have the same length'
|
||||
);
|
||||
|
||||
var easing = config.easing || linear;
|
||||
|
||||
var extrapolateLeft: ExtrapolateType = 'extend';
|
||||
if (config.extrapolateLeft !== undefined) {
|
||||
extrapolateLeft = config.extrapolateLeft;
|
||||
} else if (config.extrapolate !== undefined) {
|
||||
extrapolateLeft = config.extrapolate;
|
||||
}
|
||||
|
||||
var extrapolateRight: ExtrapolateType = 'extend';
|
||||
if (config.extrapolateRight !== undefined) {
|
||||
extrapolateRight = config.extrapolateRight;
|
||||
} else if (config.extrapolate !== undefined) {
|
||||
extrapolateRight = config.extrapolate;
|
||||
}
|
||||
|
||||
return (input) => {
|
||||
invariant(
|
||||
typeof input === 'number',
|
||||
'Cannot interpolation an input which is not a number'
|
||||
);
|
||||
|
||||
var range = findRange(input, inputRange);
|
||||
return interpolate(
|
||||
input,
|
||||
inputRange[range],
|
||||
inputRange[range + 1],
|
||||
outputRange[range],
|
||||
outputRange[range + 1],
|
||||
easing,
|
||||
extrapolateLeft,
|
||||
extrapolateRight,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function interpolate(
|
||||
input: number,
|
||||
inputMin: number,
|
||||
inputMax: number,
|
||||
outputMin: number,
|
||||
outputMax: number,
|
||||
easing: ((input: number) => number),
|
||||
extrapolateLeft: ExtrapolateType,
|
||||
extrapolateRight: ExtrapolateType,
|
||||
) {
|
||||
var result = input;
|
||||
|
||||
// Extrapolate
|
||||
if (result < inputMin) {
|
||||
if (extrapolateLeft === 'identity') {
|
||||
return result;
|
||||
} else if (extrapolateLeft === 'clamp') {
|
||||
result = inputMin;
|
||||
} else if (extrapolateLeft === 'extend') {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (result > inputMax) {
|
||||
if (extrapolateRight === 'identity') {
|
||||
return result;
|
||||
} else if (extrapolateRight === 'clamp') {
|
||||
result = inputMax;
|
||||
} else if (extrapolateRight === 'extend') {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (outputMin === outputMax) {
|
||||
return outputMin;
|
||||
}
|
||||
|
||||
if (inputMin === inputMax) {
|
||||
if (input <= inputMin) {
|
||||
return outputMin;
|
||||
}
|
||||
return outputMax;
|
||||
}
|
||||
|
||||
// Input Range
|
||||
if (inputMin === -Infinity) {
|
||||
result = -result;
|
||||
} else if (inputMax === Infinity) {
|
||||
result = result - inputMin;
|
||||
} else {
|
||||
result = (result - inputMin) / (inputMax - inputMin);
|
||||
}
|
||||
|
||||
// Easing
|
||||
result = easing(result);
|
||||
|
||||
// Output Range
|
||||
if (outputMin === -Infinity) {
|
||||
result = -result;
|
||||
} else if (outputMax === Infinity) {
|
||||
result = result + outputMin;
|
||||
} else {
|
||||
result = result * (outputMax - outputMin) + outputMin;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function colorToRgba(input: string): string {
|
||||
var int32Color = normalizeColor(input);
|
||||
if (int32Color === null) {
|
||||
return input;
|
||||
}
|
||||
|
||||
int32Color = int32Color || 0; // $FlowIssue
|
||||
|
||||
var r = (int32Color & 0xff000000) >>> 24;
|
||||
var g = (int32Color & 0x00ff0000) >>> 16;
|
||||
var b = (int32Color & 0x0000ff00) >>> 8;
|
||||
var a = (int32Color & 0x000000ff) / 255;
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
|
||||
var stringShapeRegex = /[0-9\.-]+/g;
|
||||
|
||||
/**
|
||||
* Supports string shapes by extracting numbers so new values can be computed,
|
||||
* and recombines those values into new strings of the same shape. Supports
|
||||
* things like:
|
||||
*
|
||||
* rgba(123, 42, 99, 0.36) // colors
|
||||
* -45deg // values with units
|
||||
*/
|
||||
function createInterpolationFromStringOutputRange(
|
||||
config: InterpolationConfigType,
|
||||
): (input: number) => string {
|
||||
var outputRange: Array<string> = (config.outputRange: any);
|
||||
invariant(outputRange.length >= 2, 'Bad output range');
|
||||
outputRange = outputRange.map(colorToRgba);
|
||||
checkPattern(outputRange);
|
||||
|
||||
// ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
|
||||
// ->
|
||||
// [
|
||||
// [0, 50],
|
||||
// [100, 150],
|
||||
// [200, 250],
|
||||
// [0, 0.5],
|
||||
// ]
|
||||
/* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to
|
||||
* guard against this possibility.
|
||||
*/
|
||||
var outputRanges = outputRange[0].match(stringShapeRegex).map(() => []);
|
||||
outputRange.forEach(value => {
|
||||
/* $FlowFixMe(>=0.18.0): `value.match()` can return `null`. Need to guard
|
||||
* against this possibility.
|
||||
*/
|
||||
value.match(stringShapeRegex).forEach((number, i) => {
|
||||
outputRanges[i].push(+number);
|
||||
});
|
||||
});
|
||||
|
||||
/* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to
|
||||
* guard against this possibility.
|
||||
*/
|
||||
var interpolations = outputRange[0].match(stringShapeRegex).map((value, i) => {
|
||||
return Interpolation.create({
|
||||
...config,
|
||||
outputRange: outputRanges[i],
|
||||
});
|
||||
});
|
||||
|
||||
return (input) => {
|
||||
var i = 0;
|
||||
// 'rgba(0, 100, 200, 0)'
|
||||
// ->
|
||||
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
|
||||
return outputRange[0].replace(stringShapeRegex, () => {
|
||||
return String(interpolations[i++](input));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function checkPattern(arr: Array<string>) {
|
||||
var pattern = arr[0].replace(stringShapeRegex, '');
|
||||
for (var i = 1; i < arr.length; ++i) {
|
||||
invariant(
|
||||
pattern === arr[i].replace(stringShapeRegex, ''),
|
||||
'invalid pattern ' + arr[0] + ' and ' + arr[i],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function findRange(input: number, inputRange: Array<number>) {
|
||||
for (var i = 1; i < inputRange.length - 1; ++i) {
|
||||
if (inputRange[i] >= input) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return i - 1;
|
||||
}
|
||||
|
||||
function checkValidInputRange(arr: Array<number>) {
|
||||
invariant(arr.length >= 2, 'inputRange must have at least 2 elements');
|
||||
for (var i = 1; i < arr.length; ++i) {
|
||||
invariant(
|
||||
arr[i] >= arr[i - 1],
|
||||
/* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
|
||||
* one or both of the operands may be something that doesn't cleanly
|
||||
* convert to a string, like undefined, null, and object, etc. If you really
|
||||
* mean this implicit string conversion, you can do something like
|
||||
* String(myThing)
|
||||
*/
|
||||
'inputRange must be monotonically increasing ' + arr
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function checkInfiniteRange(name: string, arr: Array<number>) {
|
||||
invariant(arr.length >= 2, name + ' must have at least 2 elements');
|
||||
invariant(
|
||||
arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity,
|
||||
/* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
|
||||
* one or both of the operands may be something that doesn't cleanly convert
|
||||
* to a string, like undefined, null, and object, etc. If you really mean
|
||||
* this implicit string conversion, you can do something like
|
||||
* String(myThing)
|
||||
*/
|
||||
name + 'cannot be ]-infinity;+infinity[ ' + arr
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = Interpolation;
|
||||
@@ -1,103 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule SpringConfig
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
type SpringConfigType = {
|
||||
tension: number,
|
||||
friction: number,
|
||||
};
|
||||
|
||||
function tensionFromOrigamiValue(oValue) {
|
||||
return (oValue - 30) * 3.62 + 194;
|
||||
}
|
||||
|
||||
function frictionFromOrigamiValue(oValue) {
|
||||
return (oValue - 8) * 3 + 25;
|
||||
}
|
||||
|
||||
function fromOrigamiTensionAndFriction(
|
||||
tension: number,
|
||||
friction: number,
|
||||
): SpringConfigType {
|
||||
return {
|
||||
tension: tensionFromOrigamiValue(tension),
|
||||
friction: frictionFromOrigamiValue(friction)
|
||||
};
|
||||
}
|
||||
|
||||
function fromBouncinessAndSpeed(
|
||||
bounciness: number,
|
||||
speed: number,
|
||||
): SpringConfigType {
|
||||
function normalize(value, startValue, endValue) {
|
||||
return (value - startValue) / (endValue - startValue);
|
||||
}
|
||||
|
||||
function projectNormal(n, start, end) {
|
||||
return start + (n * (end - start));
|
||||
}
|
||||
|
||||
function linearInterpolation(t, start, end) {
|
||||
return t * end + (1 - t) * start;
|
||||
}
|
||||
|
||||
function quadraticOutInterpolation(t, start, end) {
|
||||
return linearInterpolation(2 * t - t * t, start, end);
|
||||
}
|
||||
|
||||
function b3Friction1(x) {
|
||||
return (0.0007 * Math.pow(x, 3)) -
|
||||
(0.031 * Math.pow(x, 2)) + 0.64 * x + 1.28;
|
||||
}
|
||||
|
||||
function b3Friction2(x) {
|
||||
return (0.000044 * Math.pow(x, 3)) -
|
||||
(0.006 * Math.pow(x, 2)) + 0.36 * x + 2;
|
||||
}
|
||||
|
||||
function b3Friction3(x) {
|
||||
return (0.00000045 * Math.pow(x, 3)) -
|
||||
(0.000332 * Math.pow(x, 2)) + 0.1078 * x + 5.84;
|
||||
}
|
||||
|
||||
function b3Nobounce(tension) {
|
||||
if (tension <= 18) {
|
||||
return b3Friction1(tension);
|
||||
} else if (tension > 18 && tension <= 44) {
|
||||
return b3Friction2(tension);
|
||||
} else {
|
||||
return b3Friction3(tension);
|
||||
}
|
||||
}
|
||||
|
||||
var b = normalize(bounciness / 1.7, 0, 20);
|
||||
b = projectNormal(b, 0, 0.8);
|
||||
var s = normalize(speed / 1.7, 0, 20);
|
||||
var bouncyTension = projectNormal(s, 0.5, 200);
|
||||
var bouncyFriction = quadraticOutInterpolation(
|
||||
b,
|
||||
b3Nobounce(bouncyTension),
|
||||
0.01
|
||||
);
|
||||
|
||||
return {
|
||||
tension: tensionFromOrigamiValue(bouncyTension),
|
||||
friction: frictionFromOrigamiValue(bouncyFriction)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fromOrigamiTensionAndFriction,
|
||||
fromBouncinessAndSpeed,
|
||||
};
|
||||
@@ -1,19 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) 2016-present, Nicolas Gallagher.
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import AnimatedImplementation from './AnimatedImplementation'
|
||||
import Animated from 'animated'
|
||||
import StyleSheet from '../StyleSheet'
|
||||
import Image from '../../components/Image'
|
||||
import Text from '../../components/Text'
|
||||
import View from '../../components/View'
|
||||
|
||||
Animated.inject.FlattenStyle(StyleSheet.flatten)
|
||||
|
||||
module.exports = {
|
||||
...AnimatedImplementation,
|
||||
View: AnimatedImplementation.createAnimatedComponent(View),
|
||||
Text: AnimatedImplementation.createAnimatedComponent(Text),
|
||||
Image: AnimatedImplementation.createAnimatedComponent(Image)
|
||||
...Animated,
|
||||
Image: Animated.createAnimatedComponent(Image),
|
||||
Text: Animated.createAnimatedComponent(Text),
|
||||
View: Animated.createAnimatedComponent(View)
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
function SetPolyfill() {
|
||||
this._cache = []
|
||||
}
|
||||
|
||||
SetPolyfill.prototype.add = function (e) {
|
||||
if (this._cache.indexOf(e) === -1) {
|
||||
this._cache.push(e)
|
||||
}
|
||||
}
|
||||
|
||||
SetPolyfill.prototype.forEach = function (cb) {
|
||||
this._cache.forEach(cb)
|
||||
}
|
||||
|
||||
export default SetPolyfill
|
||||
@@ -1,6 +1,4 @@
|
||||
import Portal from '../../components/Portal'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import StyleSheet from '../StyleSheet'
|
||||
import View from '../../components/View'
|
||||
|
||||
@@ -11,22 +9,12 @@ class ReactNativeApp extends Component {
|
||||
rootTag: PropTypes.any
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this._handleModalVisibilityChange = this._handleModalVisibilityChange.bind(this)
|
||||
}
|
||||
|
||||
_handleModalVisibilityChange(modalVisible) {
|
||||
ReactDOM.findDOMNode(this._root).setAttribute('aria-hidden', `${modalVisible}`)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { initialProps, rootComponent: RootComponent, rootTag } = this.props
|
||||
|
||||
return (
|
||||
<View style={styles.appContainer}>
|
||||
<RootComponent {...initialProps} ref={(c) => { this._root = c }} rootTag={rootTag} />
|
||||
<Portal onModalVisibilityChanged={this._handleModalVisibilityChange} />
|
||||
<RootComponent {...initialProps} rootTag={rootTag} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -34,8 +22,7 @@ class ReactNativeApp extends Component {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
/**
|
||||
* Ensure that the application covers the whole screen. This prevents the
|
||||
* Portal content from being clipped.
|
||||
* Ensure that the application covers the whole screen.
|
||||
*/
|
||||
appContainer: {
|
||||
position: 'absolute',
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
import { elementId } from '../../StyleSheet'
|
||||
import { prerenderApplication } from '../renderApplication'
|
||||
|
||||
const component = () => <div />
|
||||
|
||||
suite('apis/AppRegistry/renderApplication', () => {
|
||||
test('prerenderApplication', () => {
|
||||
const { html, style, styleElement } = prerenderApplication(component, {})
|
||||
|
||||
assert.ok(html.indexOf('<div ') > -1)
|
||||
assert.ok(typeof style === 'string')
|
||||
assert.equal(styleElement.type, 'style')
|
||||
assert.equal(styleElement.props.id, elementId)
|
||||
assert.equal(styleElement.props.dangerouslySetInnerHTML.__html, style)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,7 +33,7 @@ class AppRegistry {
|
||||
invariant(
|
||||
runnables[appKey] && runnables[appKey].prerender,
|
||||
`Application ${appKey} has not been registered. ` +
|
||||
`This is either due to an import error during initialization or failure to call AppRegistry.registerComponent.`
|
||||
'This is either due to an import error during initialization or failure to call AppRegistry.registerComponent.'
|
||||
)
|
||||
|
||||
return runnables[appKey].prerender(appParameters)
|
||||
@@ -78,7 +78,7 @@ class AppRegistry {
|
||||
invariant(
|
||||
runnables[appKey] && runnables[appKey].run,
|
||||
`Application "${appKey}" has not been registered. ` +
|
||||
`This is either due to an import error during initialization or failure to call AppRegistry.registerComponent.`
|
||||
'This is either due to an import error during initialization or failure to call AppRegistry.registerComponent.'
|
||||
)
|
||||
|
||||
runnables[appKey].run(appParameters)
|
||||
|
||||
@@ -13,14 +13,16 @@ import ReactDOMServer from 'react-dom/server'
|
||||
import ReactNativeApp from './ReactNativeApp'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
|
||||
const renderStyleSheetToString = () => `<style id="${StyleSheet.elementId}">${StyleSheet._renderToString()}</style>`
|
||||
const renderStyleSheetToString = () => StyleSheet.renderToString()
|
||||
const styleAsElement = (style) => <style dangerouslySetInnerHTML={{ __html: style }} id={StyleSheet.elementId} />
|
||||
const styleAsTagString = (style) => `<style id="${StyleSheet.elementId}">${style}</style>`
|
||||
|
||||
export default function renderApplication(RootComponent: Component, initialProps: Object, rootTag: any) {
|
||||
invariant(rootTag, 'Expect to have a valid rootTag, instead got ', rootTag)
|
||||
|
||||
// insert style sheet if needed
|
||||
const styleElement = document.getElementById(StyleSheet.elementId)
|
||||
if (!styleElement) { rootTag.insertAdjacentHTML('beforebegin', renderStyleSheetToString()) }
|
||||
if (!styleElement) { rootTag.insertAdjacentHTML('beforebegin', styleAsTagString(renderStyleSheetToString())) }
|
||||
|
||||
const component = (
|
||||
<ReactNativeApp
|
||||
@@ -41,5 +43,6 @@ export function prerenderApplication(RootComponent: Component, initialProps: Obj
|
||||
)
|
||||
const html = ReactDOMServer.renderToString(component)
|
||||
const style = renderStyleSheetToString()
|
||||
return { html, style }
|
||||
const styleElement = styleAsElement(style)
|
||||
return { html, style, styleElement }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import AppState from '..'
|
||||
import assert from 'assert'
|
||||
|
||||
suite('apis/AppState', () => {
|
||||
test.skip('NO TEST COVERAGE', () => {})
|
||||
const handler = () => {}
|
||||
|
||||
teardown(() => {
|
||||
try { AppState.removeEventListener('change', handler) } catch (e) {}
|
||||
})
|
||||
|
||||
suite('addEventListener', () => {
|
||||
test('throws if the provided "eventType" is not supported', () => {
|
||||
assert.throws(() => AppState.addEventListener('foo', handler))
|
||||
assert.doesNotThrow(() => AppState.addEventListener('change', handler))
|
||||
})
|
||||
})
|
||||
|
||||
suite('removeEventListener', () => {
|
||||
test('throws if the handler is not registered', () => {
|
||||
assert.throws(() => AppState.removeEventListener('change', handler))
|
||||
})
|
||||
|
||||
test('throws if the provided "eventType" is not supported', () => {
|
||||
AppState.addEventListener('change', handler)
|
||||
assert.throws(() => AppState.removeEventListener('foo', handler))
|
||||
assert.doesNotThrow(() => AppState.removeEventListener('change', handler))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,30 +1,53 @@
|
||||
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
|
||||
import findIndex from 'lodash/findIndex'
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
|
||||
const listeners = {}
|
||||
const eventTypes = [ 'change' ]
|
||||
const EVENT_TYPES = [ 'change' ]
|
||||
const VISIBILITY_CHANGE_EVENT = 'visibilitychange'
|
||||
|
||||
const AppStates = {
|
||||
BACKGROUND: 'background',
|
||||
ACTIVE: 'active'
|
||||
}
|
||||
|
||||
const listeners = []
|
||||
|
||||
class AppState {
|
||||
static isSupported = ExecutionEnvironment.canUseDOM && document.visibilityState
|
||||
|
||||
static get currentState() {
|
||||
if (!AppState.isSupported) {
|
||||
return AppState.ACTIVE
|
||||
}
|
||||
|
||||
switch (document.visibilityState) {
|
||||
case 'hidden':
|
||||
case 'prerender':
|
||||
case 'unloaded':
|
||||
return 'background'
|
||||
return AppStates.BACKGROUND
|
||||
default:
|
||||
return 'active'
|
||||
return AppStates.ACTIVE
|
||||
}
|
||||
}
|
||||
|
||||
static addEventListener(type: string, handler: Function) {
|
||||
listeners[handler] = () => handler(AppState.currentState)
|
||||
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
|
||||
document.addEventListener('visibilitychange', listeners[handler], false)
|
||||
if (AppState.isSupported) {
|
||||
invariant(EVENT_TYPES.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
|
||||
const callback = () => handler(AppState.currentState)
|
||||
listeners.push([ handler, callback ])
|
||||
document.addEventListener(VISIBILITY_CHANGE_EVENT, callback, false)
|
||||
}
|
||||
}
|
||||
|
||||
static removeEventListener(type: string, handler: Function) {
|
||||
invariant(eventTypes.indexOf(type) !== -1, 'Trying to remove listener for unknown event: "%s"', type)
|
||||
document.removeEventListener('visibilitychange', listeners[handler], false)
|
||||
delete listeners[handler]
|
||||
if (AppState.isSupported) {
|
||||
invariant(EVENT_TYPES.indexOf(type) !== -1, 'Trying to remove listener for unknown event: "%s"', type)
|
||||
const listenerIndex = findIndex(listeners, (pair) => pair[0] === handler)
|
||||
invariant(listenerIndex !== -1, 'Trying to remove AppState listener for unregistered handler')
|
||||
const callback = listeners[listenerIndex][1]
|
||||
document.removeEventListener(VISIBILITY_CHANGE_EVENT, callback, false)
|
||||
listeners.splice(listenerIndex, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,28 +6,38 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import debounce from 'lodash/debounce'
|
||||
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
|
||||
const dimensions = {
|
||||
screen: {
|
||||
fontScale: 1,
|
||||
get height() { return window.screen.height },
|
||||
scale: window.devicePixelRatio || 1,
|
||||
get width() { return window.screen.width }
|
||||
},
|
||||
window: {
|
||||
fontScale: 1,
|
||||
get height() { return window.innerHeight },
|
||||
scale: window.devicePixelRatio || 1,
|
||||
get width() { return window.innerWidth }
|
||||
}
|
||||
}
|
||||
const win = ExecutionEnvironment.canUseDOM ? window : { screen: {} }
|
||||
|
||||
const dimensions = {}
|
||||
|
||||
class Dimensions {
|
||||
static get(dimension: string): Object {
|
||||
invariant(dimensions[dimension], 'No dimension set for key ' + dimension)
|
||||
return dimensions[dimension]
|
||||
}
|
||||
|
||||
static set(): void {
|
||||
dimensions.window = {
|
||||
fontScale: 1,
|
||||
height: win.innerHeight,
|
||||
scale: win.devicePixelRatio || 1,
|
||||
width: win.innerWidth
|
||||
}
|
||||
|
||||
dimensions.screen = {
|
||||
fontScale: 1,
|
||||
height: win.screen.height,
|
||||
scale: win.devicePixelRatio || 1,
|
||||
width: win.screen.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dimensions.set()
|
||||
ExecutionEnvironment.canUseDOM && window.addEventListener('resize', debounce(Dimensions.set, 50))
|
||||
|
||||
module.exports = Dimensions
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* https://github.com/arian/cubic-bezier
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2013 Arian Stolwijk
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @providesModule bezier
|
||||
* @nolint
|
||||
*/
|
||||
|
||||
module.exports = function(x1, y1, x2, y2, epsilon){
|
||||
|
||||
var curveX = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * v * v * t * x1 + 3 * v * t * t * x2 + t * t * t;
|
||||
};
|
||||
|
||||
var curveY = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * v * v * t * y1 + 3 * v * t * t * y2 + t * t * t;
|
||||
};
|
||||
|
||||
var derivativeCurveX = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * (2 * (t - 1) * t + v * v) * x1 + 3 * (-t * t * t + 2 * v * t) * x2;
|
||||
};
|
||||
|
||||
return function(t){
|
||||
|
||||
var x = t, t0, t1, t2, x2, d2, i;
|
||||
|
||||
// First try a few iterations of Newton's method -- normally very fast.
|
||||
for (t2 = x, i = 0; i < 8; i++){
|
||||
x2 = curveX(t2) - x;
|
||||
if (Math.abs(x2) < epsilon) { return curveY(t2); }
|
||||
d2 = derivativeCurveX(t2);
|
||||
if (Math.abs(d2) < 1e-6) { break; }
|
||||
t2 = t2 - x2 / d2;
|
||||
}
|
||||
|
||||
t0 = 0;
|
||||
t1 = 1;
|
||||
t2 = x;
|
||||
|
||||
if (t2 < t0) { return curveY(t0); }
|
||||
if (t2 > t1) { return curveY(t1); }
|
||||
|
||||
// Fallback to the bisection method for reliability.
|
||||
while (t0 < t1){
|
||||
x2 = curveX(t2);
|
||||
if (Math.abs(x2 - x) < epsilon) { return curveY(t2); }
|
||||
if (x > x2) { t0 = t2; }
|
||||
else { t1 = t2; }
|
||||
t2 = (t1 - t0) * 0.5 + t0;
|
||||
}
|
||||
|
||||
// Failure
|
||||
return curveY(t2);
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
@@ -1,155 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule Easing
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var _bezier = require('./bezier');
|
||||
|
||||
/**
|
||||
* This class implements common easing functions. The math is pretty obscure,
|
||||
* but this cool website has nice visual illustrations of what they represent:
|
||||
* http://xaedes.de/dev/transitions/
|
||||
*/
|
||||
class Easing {
|
||||
static step0(n) {
|
||||
return n > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
static step1(n) {
|
||||
return n >= 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
static linear(t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
static ease(t: number): number {
|
||||
return ease(t);
|
||||
}
|
||||
|
||||
static quad(t) {
|
||||
return t * t;
|
||||
}
|
||||
|
||||
static cubic(t) {
|
||||
return t * t * t;
|
||||
}
|
||||
|
||||
static poly(n) {
|
||||
return (t) => Math.pow(t, n);
|
||||
}
|
||||
|
||||
static sin(t) {
|
||||
return 1 - Math.cos(t * Math.PI / 2);
|
||||
}
|
||||
|
||||
static circle(t) {
|
||||
return 1 - Math.sqrt(1 - t * t);
|
||||
}
|
||||
|
||||
static exp(t) {
|
||||
return Math.pow(2, 10 * (t - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple elastic interaction, similar to a spring. Default bounciness
|
||||
* is 1, which overshoots a little bit once. 0 bounciness doesn't overshoot
|
||||
* at all, and bounciness of N > 1 will overshoot about N times.
|
||||
*
|
||||
* Wolfram Plots:
|
||||
*
|
||||
* http://tiny.cc/elastic_b_1 (default bounciness = 1)
|
||||
* http://tiny.cc/elastic_b_3 (bounciness = 3)
|
||||
*/
|
||||
static elastic(bounciness: number = 1): (t: number) => number {
|
||||
var p = bounciness * Math.PI;
|
||||
return (t) => 1 - Math.pow(Math.cos(t * Math.PI / 2), 3) * Math.cos(t * p);
|
||||
}
|
||||
|
||||
static back(s: number): (t: number) => number {
|
||||
if (s === undefined) {
|
||||
s = 1.70158;
|
||||
}
|
||||
return (t) => t * t * ((s + 1) * t - s);
|
||||
}
|
||||
|
||||
static bounce(t: number): number {
|
||||
if (t < 1 / 2.75) {
|
||||
return 7.5625 * t * t;
|
||||
}
|
||||
|
||||
if (t < 2 / 2.75) {
|
||||
t -= 1.5 / 2.75;
|
||||
return 7.5625 * t * t + 0.75;
|
||||
}
|
||||
|
||||
if (t < 2.5 / 2.75) {
|
||||
t -= 2.25 / 2.75;
|
||||
return 7.5625 * t * t + 0.9375;
|
||||
}
|
||||
|
||||
t -= 2.625 / 2.75;
|
||||
return 7.5625 * t * t + 0.984375;
|
||||
}
|
||||
|
||||
static bezier(
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
epsilon?: ?number,
|
||||
): (t: number) => number {
|
||||
if (epsilon === undefined) {
|
||||
// epsilon determines the precision of the solved values
|
||||
// a good approximation is:
|
||||
var duration = 500; // duration of animation in milliseconds.
|
||||
epsilon = (1000 / 60 / duration) / 4;
|
||||
}
|
||||
|
||||
return _bezier(x1, y1, x2, y2, epsilon);
|
||||
}
|
||||
|
||||
static in(
|
||||
easing: (t: number) => number,
|
||||
): (t: number) => number {
|
||||
return easing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an easing function backwards.
|
||||
*/
|
||||
static out(
|
||||
easing: (t: number) => number,
|
||||
): (t: number) => number {
|
||||
return (t) => 1 - easing(1 - t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes any easing function symmetrical.
|
||||
*/
|
||||
static inOut(
|
||||
easing: (t: number) => number,
|
||||
): (t: number) => number {
|
||||
return (t) => {
|
||||
if (t < 0.5) {
|
||||
return easing(t * 2) / 2;
|
||||
}
|
||||
return 1 - easing((1 - t) * 2) / 2;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var ease = Easing.bezier(0.42, 0, 1, 1);
|
||||
|
||||
|
||||
|
||||
module.exports = Easing;
|
||||
@@ -6,9 +6,15 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
|
||||
const connection = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection
|
||||
const connection = ExecutionEnvironment.canUseDOM && (
|
||||
window.navigator.connection ||
|
||||
window.navigator.mozConnection ||
|
||||
window.navigator.webkitConnection
|
||||
)
|
||||
|
||||
const eventTypes = [ 'change' ]
|
||||
|
||||
/**
|
||||
@@ -50,8 +56,8 @@ const NetInfo = {
|
||||
isConnected: {
|
||||
addEventListener(type: string, handler: Function): { remove: () => void } {
|
||||
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
|
||||
window.addEventListener('online', handler.bind(true), false)
|
||||
window.addEventListener('offline', handler.bind(false), false)
|
||||
window.addEventListener('online', handler.bind(null, true), false)
|
||||
window.addEventListener('offline', handler.bind(null, false), false)
|
||||
|
||||
return {
|
||||
remove: () => NetInfo.isConnected.removeEventListener(type, handler)
|
||||
@@ -60,8 +66,8 @@ const NetInfo = {
|
||||
|
||||
removeEventListener(type: string, handler: Function): void {
|
||||
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
|
||||
window.removeEventListener('online', handler.bind(true), false)
|
||||
window.removeEventListener('offline', handler.bind(false), false)
|
||||
window.removeEventListener('online', handler.bind(null, true), false)
|
||||
window.removeEventListener('offline', handler.bind(null, false), false)
|
||||
},
|
||||
|
||||
fetch(): Promise {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import EventConstants from 'react/lib/EventConstants'
|
||||
import EventPluginRegistry from 'react/lib/EventPluginRegistry'
|
||||
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
|
||||
import ResponderEventPlugin from 'react/lib/ResponderEventPlugin'
|
||||
import ResponderTouchHistoryStore from 'react/lib/ResponderTouchHistoryStore'
|
||||
import normalizeNativeEvent from './normalizeNativeEvent'
|
||||
@@ -18,7 +19,10 @@ const {
|
||||
topTouchStart
|
||||
} = EventConstants.topLevelTypes
|
||||
|
||||
const supportsTouch = ('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch
|
||||
const supportsTouch = ExecutionEnvironment.canUseDOM && (
|
||||
'ontouchstart' in window ||
|
||||
window.DocumentTouch && document instanceof window.DocumentTouch
|
||||
)
|
||||
|
||||
const endDependencies = supportsTouch ? [ topTouchCancel, topTouchEnd ] : [ topMouseUp ]
|
||||
const moveDependencies = supportsTouch ? [ topTouchMove ] : [ topMouseMove ]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'
|
||||
|
||||
const Platform = {
|
||||
OS: 'web',
|
||||
userAgent: canUseDOM ? window.navigator.userAgent : ''
|
||||
select: (obj: Object) => obj.web
|
||||
}
|
||||
|
||||
module.exports = Platform
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import prefixAll from 'inline-style-prefix-all'
|
||||
import hyphenate from './hyphenate'
|
||||
|
||||
class Store {
|
||||
constructor(
|
||||
initialState:Object = {},
|
||||
options:Object = { obfuscateClassNames: false }
|
||||
) {
|
||||
this._counter = 0
|
||||
this._classNames = { ...initialState.classNames }
|
||||
this._declarations = { ...initialState.declarations }
|
||||
this._options = options
|
||||
}
|
||||
|
||||
get(property, value) {
|
||||
const key = this._getDeclarationKey(property, value)
|
||||
return this._classNames[key]
|
||||
}
|
||||
|
||||
set(property, value) {
|
||||
if (value != null) {
|
||||
const values = this._getPropertyValues(property) || []
|
||||
if (values.indexOf(value) === -1) {
|
||||
values.push(value)
|
||||
this._setClassName(property, value)
|
||||
this._setPropertyValues(property, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
const obfuscate = this._options.obfuscateClassNames
|
||||
|
||||
// sort the properties to ensure shorthands are first in the cascade
|
||||
const properties = Object.keys(this._declarations).sort()
|
||||
|
||||
// transform the class name to a valid CSS selector
|
||||
const getCssSelector = (property, value) => {
|
||||
let className = this.get(property, value)
|
||||
if (!obfuscate && className) {
|
||||
className = className.replace(/[(),":?.%\\$#]/g, '\\$&')
|
||||
}
|
||||
return className
|
||||
}
|
||||
|
||||
// transform the declarations into CSS rules with vendor-prefixes
|
||||
const buildCSSRules = (property, values) => {
|
||||
return values.reduce((cssRules, value) => {
|
||||
const declarations = prefixAll({ [property]: value })
|
||||
const cssDeclarations = Object.keys(declarations).reduce((str, prop) => {
|
||||
const value = declarations[prop]
|
||||
str += `${hyphenate(prop)}:${value};`
|
||||
return str
|
||||
}, '')
|
||||
const selector = getCssSelector(property, value)
|
||||
|
||||
cssRules += `\n.${selector}{${cssDeclarations}}`
|
||||
|
||||
return cssRules
|
||||
}, '')
|
||||
}
|
||||
|
||||
const css = properties.reduce((css, property) => {
|
||||
const values = this._declarations[property]
|
||||
css += buildCSSRules(property, values)
|
||||
return css
|
||||
}, '')
|
||||
|
||||
return (`/* ${this._counter} unique declarations */${css}`)
|
||||
}
|
||||
|
||||
_getDeclarationKey(property, value) {
|
||||
return `${property}:${value}`
|
||||
}
|
||||
|
||||
_getPropertyValues(property) {
|
||||
return this._declarations[property]
|
||||
}
|
||||
|
||||
_setPropertyValues(property, values) {
|
||||
this._declarations[property] = values.map(value => value)
|
||||
}
|
||||
|
||||
_setClassName(property, value) {
|
||||
const key = this._getDeclarationKey(property, value)
|
||||
const exists = !!this._classNames[key]
|
||||
if (!exists) {
|
||||
this._counter += 1
|
||||
if (this._options.obfuscateClassNames) {
|
||||
this._classNames[key] = `_s_${this._counter}`
|
||||
} else {
|
||||
const val = `${value}`.replace(/\s/g, '-')
|
||||
this._classNames[key] = `${property}:${val}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Store
|
||||
@@ -7,42 +7,112 @@
|
||||
*/
|
||||
|
||||
import prefixAll from 'inline-style-prefix-all'
|
||||
import hyphenate from './hyphenate'
|
||||
import expandStyle from './expandStyle'
|
||||
import flattenStyle from './flattenStyle'
|
||||
import processTransform from './processTransform'
|
||||
import { predefinedClassNames } from './predefs'
|
||||
|
||||
let stylesCache = {}
|
||||
let uniqueID = 0
|
||||
|
||||
const getCacheKey = (prop, value) => `${prop}:${value}`
|
||||
|
||||
const normalizeStyle = (style) => {
|
||||
return processTransform(expandStyle(flattenStyle(style)))
|
||||
}
|
||||
|
||||
const createCssDeclarations = (style) => {
|
||||
return Object.keys(style).map((prop) => {
|
||||
const property = hyphenate(prop)
|
||||
const value = style[prop]
|
||||
if (Array.isArray(value)) {
|
||||
return value.reduce((acc, curr) => {
|
||||
acc += `${property}:${curr};`
|
||||
return acc
|
||||
}, '')
|
||||
} else {
|
||||
return `${property}:${value};`
|
||||
}
|
||||
}).sort().join('')
|
||||
}
|
||||
|
||||
class StyleSheetRegistry {
|
||||
static registerStyle(style: Object, store): number {
|
||||
/* for testing */
|
||||
static _reset() {
|
||||
stylesCache = {}
|
||||
uniqueID = 0
|
||||
}
|
||||
|
||||
static renderToString() {
|
||||
let str = `/* ${uniqueID} unique declarations */`
|
||||
|
||||
return Object.keys(stylesCache).reduce((str, key) => {
|
||||
const id = stylesCache[key].id
|
||||
const style = stylesCache[key].style
|
||||
const declarations = createCssDeclarations(style)
|
||||
const rule = `\n.${id}{${declarations}}`
|
||||
str += rule
|
||||
return str
|
||||
}, str)
|
||||
}
|
||||
|
||||
static registerStyle(style: Object): number {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Object.freeze(style)
|
||||
}
|
||||
|
||||
const normalizedStyle = processTransform(flattenStyle(style))
|
||||
const normalizedStyle = normalizeStyle(style)
|
||||
|
||||
Object.keys(normalizedStyle).forEach((prop) => {
|
||||
// add each declaration to the store
|
||||
store.set(prop, normalizedStyle[prop])
|
||||
const value = normalizedStyle[prop]
|
||||
const cacheKey = getCacheKey(prop, value)
|
||||
const exists = stylesCache[cacheKey] && stylesCache[cacheKey].id
|
||||
if (!exists) {
|
||||
const id = ++uniqueID
|
||||
// add new declaration to the store
|
||||
stylesCache[cacheKey] = {
|
||||
id: `__style${id}`,
|
||||
style: prefixAll({ [prop]: value })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return style
|
||||
}
|
||||
|
||||
static getStyleAsNativeProps(style, store) {
|
||||
let _className
|
||||
let _style = {}
|
||||
static getStyleAsNativeProps(styleSheetObject, canUseCSS = false) {
|
||||
const classList = []
|
||||
const normalizedStyle = processTransform(flattenStyle(style))
|
||||
const normalizedStyle = normalizeStyle(styleSheetObject)
|
||||
let style = {}
|
||||
|
||||
for (const prop in normalizedStyle) {
|
||||
let styleClass = store.get(prop, normalizedStyle[prop])
|
||||
const value = normalizedStyle[prop]
|
||||
const cacheKey = getCacheKey(prop, value)
|
||||
let selector = stylesCache[cacheKey] && stylesCache[cacheKey].id || predefinedClassNames[cacheKey]
|
||||
|
||||
if (styleClass) {
|
||||
classList.push(styleClass)
|
||||
if (selector && canUseCSS) {
|
||||
classList.push(selector)
|
||||
} else {
|
||||
_style[prop] = normalizedStyle[prop]
|
||||
style[prop] = normalizedStyle[prop]
|
||||
}
|
||||
}
|
||||
|
||||
_className = classList.join(' ')
|
||||
_style = prefixAll(_style)
|
||||
/**
|
||||
* React 15 removed undocumented support for fallback values in
|
||||
* inline-styles. For now, pick the last value and regress browser support
|
||||
* for CSS features like flexbox.
|
||||
*/
|
||||
const finalStyle = Object.keys(prefixAll(style)).reduce((acc, prop) => {
|
||||
const value = style[prop]
|
||||
acc[prop] = Array.isArray(value) ? value[value.length - 1] : value
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return { className: _className, style: _style }
|
||||
return {
|
||||
className: classList.join(' '),
|
||||
style: finalStyle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,8 +63,7 @@ StyleSheetValidation.addValidStylePropTypes({
|
||||
direction: PropTypes.string, /* @private */
|
||||
float: PropTypes.oneOf([ 'left', 'none', 'right' ]),
|
||||
font: PropTypes.string, /* @private */
|
||||
listStyle: PropTypes.string,
|
||||
verticalAlign: PropTypes.string
|
||||
listStyle: PropTypes.string
|
||||
})
|
||||
|
||||
module.exports = StyleSheetValidation
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import Store from '../Store'
|
||||
|
||||
suite('apis/StyleSheet/Store', () => {
|
||||
suite('the constructor', () => {
|
||||
test('initialState', () => {
|
||||
const initialState = { classNames: { 'textAlign:center': '__classname__' } }
|
||||
const store = new Store(initialState)
|
||||
assert.deepEqual(store._classNames['textAlign:center'], '__classname__')
|
||||
})
|
||||
})
|
||||
|
||||
suite('#get', () => {
|
||||
test('returns a declaration-specific className', () => {
|
||||
const initialState = {
|
||||
classNames: {
|
||||
'textAlign:center': '__expected__',
|
||||
'textAlign:left': '__error__'
|
||||
}
|
||||
}
|
||||
const store = new Store(initialState)
|
||||
assert.deepEqual(store.get('textAlign', 'center'), '__expected__')
|
||||
})
|
||||
})
|
||||
|
||||
suite('#set', () => {
|
||||
test('stores declarations', () => {
|
||||
const store = new Store()
|
||||
store.set('textAlign', 'center')
|
||||
store.set('marginTop', 0)
|
||||
store.set('marginTop', 1)
|
||||
store.set('marginTop', 2)
|
||||
assert.deepEqual(store._declarations, {
|
||||
textAlign: [ 'center' ],
|
||||
marginTop: [ 0, 1, 2 ]
|
||||
})
|
||||
})
|
||||
|
||||
test('human-readable classNames', () => {
|
||||
const store = new Store()
|
||||
store.set('textAlign', 'center')
|
||||
store.set('marginTop', 0)
|
||||
store.set('marginTop', 1)
|
||||
store.set('marginTop', 2)
|
||||
assert.deepEqual(store._classNames, {
|
||||
'textAlign:center': 'textAlign:center',
|
||||
'marginTop:0': 'marginTop:0',
|
||||
'marginTop:1': 'marginTop:1',
|
||||
'marginTop:2': 'marginTop:2'
|
||||
})
|
||||
})
|
||||
|
||||
test('obfuscated classNames', () => {
|
||||
const store = new Store({}, { obfuscateClassNames: true })
|
||||
store.set('textAlign', 'center')
|
||||
store.set('marginTop', 0)
|
||||
store.set('marginTop', 1)
|
||||
store.set('marginTop', 2)
|
||||
assert.deepEqual(store._classNames, {
|
||||
'textAlign:center': '_s_1',
|
||||
'marginTop:0': '_s_2',
|
||||
'marginTop:1': '_s_3',
|
||||
'marginTop:2': '_s_4'
|
||||
})
|
||||
})
|
||||
|
||||
test('replaces space characters', () => {
|
||||
const store = new Store()
|
||||
|
||||
store.set('backgroundPosition', 'top left')
|
||||
assert.equal(store.get('backgroundPosition', 'top left'), 'backgroundPosition\:top-left')
|
||||
})
|
||||
})
|
||||
|
||||
suite('#toString', () => {
|
||||
test('human-readable style sheet', () => {
|
||||
const store = new Store()
|
||||
store.set('textAlign', 'center')
|
||||
store.set('backgroundColor', 'rgba(0,0,0,0)')
|
||||
store.set('color', '#fff')
|
||||
store.set('fontFamily', '"Helvetica Neue", Arial, sans-serif')
|
||||
store.set('marginBottom', '0px')
|
||||
store.set('width', '100%')
|
||||
|
||||
const expected = '/* 6 unique declarations */\n' +
|
||||
'.backgroundColor\\:rgba\\(0\\,0\\,0\\,0\\){background-color:rgba(0,0,0,0);}\n' +
|
||||
'.color\\:\\#fff{color:#fff;}\n' +
|
||||
'.fontFamily\\:\\"Helvetica-Neue\\"\\,-Arial\\,-sans-serif{font-family:"Helvetica Neue", Arial, sans-serif;}\n' +
|
||||
'.marginBottom\\:0px{margin-bottom:0px;}\n' +
|
||||
'.textAlign\\:center{text-align:center;}\n' +
|
||||
'.width\\:100\\%{width:100%;}'
|
||||
|
||||
assert.equal(store.toString(), expected)
|
||||
})
|
||||
|
||||
test('obfuscated style sheet', () => {
|
||||
const store = new Store({}, { obfuscateClassNames: true })
|
||||
store.set('textAlign', 'center')
|
||||
store.set('marginBottom', '0px')
|
||||
store.set('margin', '1px')
|
||||
store.set('margin', '2px')
|
||||
store.set('margin', '3px')
|
||||
|
||||
const expected = '/* 5 unique declarations */\n' +
|
||||
'._s_3{margin:1px;}\n' +
|
||||
'._s_4{margin:2px;}\n' +
|
||||
'._s_5{margin:3px;}\n' +
|
||||
'._s_2{margin-bottom:0px;}\n' +
|
||||
'._s_1{text-align:center;}'
|
||||
|
||||
assert.equal(store.toString(), expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
55
src/apis/StyleSheet/__tests__/StyleSheetRegistry-test.js
Normal file
55
src/apis/StyleSheet/__tests__/StyleSheetRegistry-test.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
import assert from 'assert'
|
||||
import StyleSheetRegistry from '../StyleSheetRegistry'
|
||||
|
||||
suite('apis/StyleSheet/StyleSheetRegistry', () => {
|
||||
setup(() => {
|
||||
StyleSheetRegistry._reset()
|
||||
})
|
||||
|
||||
test('static renderToString', () => {
|
||||
const style1 = { alignItems: 'center', opacity: 1 }
|
||||
const style2 = { alignItems: 'center', opacity: 1 }
|
||||
StyleSheetRegistry.registerStyle(style1)
|
||||
StyleSheetRegistry.registerStyle(style2)
|
||||
|
||||
const actual = StyleSheetRegistry.renderToString()
|
||||
const expected = `/* 2 unique declarations */
|
||||
.__style1{-ms-flex-align:center;-webkit-align-items:center;-webkit-box-align:center;align-items:center;}
|
||||
.__style2{opacity:1;}`
|
||||
|
||||
assert.equal(actual, expected)
|
||||
})
|
||||
|
||||
test('static getStyleAsNativeProps', () => {
|
||||
const style = { borderColorTop: 'white', opacity: 1 }
|
||||
const style1 = { opacity: 1 }
|
||||
StyleSheetRegistry.registerStyle(style1)
|
||||
|
||||
// canUseCSS = false
|
||||
const actual1 = StyleSheetRegistry.getStyleAsNativeProps(style)
|
||||
const expected1 = {
|
||||
className: '',
|
||||
style: { borderColorTop: 'white', opacity: 1 }
|
||||
}
|
||||
assert.deepEqual(actual1, expected1)
|
||||
|
||||
// canUseCSS = true
|
||||
const actual2 = StyleSheetRegistry.getStyleAsNativeProps(style, true)
|
||||
const expected2 = {
|
||||
className: '__style1',
|
||||
style: { borderColorTop: 'white' }
|
||||
}
|
||||
assert.deepEqual(actual2, expected2)
|
||||
})
|
||||
})
|
||||
@@ -4,20 +4,29 @@ import assert from 'assert'
|
||||
import expandStyle from '../expandStyle'
|
||||
|
||||
suite('apis/StyleSheet/expandStyle', () => {
|
||||
test('style resolution', () => {
|
||||
test('shortform -> longform', () => {
|
||||
const initial = {
|
||||
borderTopWidth: 1,
|
||||
borderWidth: 2,
|
||||
borderStyle: 'solid',
|
||||
boxSizing: 'border-box',
|
||||
borderBottomColor: 'white',
|
||||
borderBottomWidth: 1,
|
||||
borderWidth: 0,
|
||||
marginTop: 50,
|
||||
marginVertical: 25,
|
||||
margin: 10
|
||||
}
|
||||
|
||||
const expected = {
|
||||
borderTopWidth: '1px',
|
||||
borderLeftWidth: '2px',
|
||||
borderRightWidth: '2px',
|
||||
borderBottomWidth: '2px',
|
||||
borderBottomStyle: 'solid',
|
||||
borderLeftStyle: 'solid',
|
||||
borderRightStyle: 'solid',
|
||||
boxSizing: 'border-box',
|
||||
borderBottomColor: 'white',
|
||||
borderTopStyle: 'solid',
|
||||
borderTopWidth: '0px',
|
||||
borderLeftWidth: '0px',
|
||||
borderRightWidth: '0px',
|
||||
borderBottomWidth: '1px',
|
||||
marginTop: '50px',
|
||||
marginBottom: '25px',
|
||||
marginLeft: '10px',
|
||||
@@ -27,6 +36,18 @@ suite('apis/StyleSheet/expandStyle', () => {
|
||||
assert.deepEqual(expandStyle(initial), expected)
|
||||
})
|
||||
|
||||
test('textAlignVertical', () => {
|
||||
const initial = {
|
||||
textAlignVertical: 'center'
|
||||
}
|
||||
|
||||
const expected = {
|
||||
verticalAlign: 'middle'
|
||||
}
|
||||
|
||||
assert.deepEqual(expandStyle(initial), expected)
|
||||
})
|
||||
|
||||
test('flex', () => {
|
||||
const value = 10
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { resetCSS, predefinedCSS } from '../predefs'
|
||||
import assert from 'assert'
|
||||
import StyleSheet from '..'
|
||||
|
||||
const styles = { root: { borderWidth: 1 } }
|
||||
const styles = { root: { opacity: 1 } }
|
||||
|
||||
suite('apis/StyleSheet', () => {
|
||||
setup(() => {
|
||||
@@ -12,55 +12,50 @@ suite('apis/StyleSheet', () => {
|
||||
})
|
||||
|
||||
suite('create', () => {
|
||||
const div = document.createElement('div')
|
||||
|
||||
setup(() => {
|
||||
document.body.appendChild(div)
|
||||
StyleSheet.create(styles)
|
||||
div.innerHTML = `<style id='${StyleSheet.elementId}'>${StyleSheet._renderToString()}</style>`
|
||||
})
|
||||
|
||||
teardown(() => {
|
||||
document.body.removeChild(div)
|
||||
})
|
||||
|
||||
test('returns styles object', () => {
|
||||
assert.equal(StyleSheet.create(styles), styles)
|
||||
})
|
||||
|
||||
test('updates already-rendered style sheet', () => {
|
||||
StyleSheet.create({ root: { color: 'red' } })
|
||||
// setup
|
||||
const div = document.createElement('div')
|
||||
document.body.appendChild(div)
|
||||
StyleSheet.create(styles)
|
||||
div.innerHTML = `<style id='${StyleSheet.elementId}'>${StyleSheet.renderToString()}</style>`
|
||||
|
||||
// test
|
||||
StyleSheet.create({ root: { color: 'red' } })
|
||||
assert.equal(
|
||||
document.getElementById(StyleSheet.elementId).textContent,
|
||||
`${resetCSS}\n${predefinedCSS}\n` +
|
||||
`/* 5 unique declarations */\n` +
|
||||
`.borderBottomWidth\\:1px{border-bottom-width:1px;}\n` +
|
||||
`.borderLeftWidth\\:1px{border-left-width:1px;}\n` +
|
||||
`.borderRightWidth\\:1px{border-right-width:1px;}\n` +
|
||||
`.borderTopWidth\\:1px{border-top-width:1px;}\n` +
|
||||
`.color\\:red{color:red;}`
|
||||
`/* 2 unique declarations */\n` +
|
||||
`.__style1{opacity:1;}\n` +
|
||||
'.__style2{color:red;}'
|
||||
)
|
||||
|
||||
// teardown
|
||||
document.body.removeChild(div)
|
||||
})
|
||||
})
|
||||
|
||||
test('resolve', () => {
|
||||
const props = { style: styles.root }
|
||||
const expected = { className: 'borderTopWidth:1px borderRightWidth:1px borderBottomWidth:1px borderLeftWidth:1px', style: {} }
|
||||
test('renderToString', () => {
|
||||
StyleSheet.create(styles)
|
||||
assert.deepEqual(StyleSheet.resolve(props), expected)
|
||||
|
||||
assert.equal(
|
||||
StyleSheet.renderToString(),
|
||||
`${resetCSS}\n${predefinedCSS}\n` +
|
||||
`/* 1 unique declarations */\n` +
|
||||
'.__style1{opacity:1;}'
|
||||
)
|
||||
})
|
||||
|
||||
test('_renderToString', () => {
|
||||
StyleSheet.create(styles)
|
||||
assert.equal(
|
||||
StyleSheet._renderToString(),
|
||||
`${resetCSS}\n${predefinedCSS}\n` +
|
||||
`/* 4 unique declarations */\n` +
|
||||
`.borderBottomWidth\\:1px{border-bottom-width:1px;}\n` +
|
||||
`.borderLeftWidth\\:1px{border-left-width:1px;}\n` +
|
||||
`.borderRightWidth\\:1px{border-right-width:1px;}\n` +
|
||||
`.borderTopWidth\\:1px{border-top-width:1px;}`
|
||||
test('resolve', () => {
|
||||
assert.deepEqual(
|
||||
StyleSheet.resolve({ className: 'test', style: styles.root }),
|
||||
{
|
||||
className: 'test',
|
||||
style: { opacity: 1 }
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
32
src/apis/StyleSheet/__tests__/processTransform-test.js
Normal file
32
src/apis/StyleSheet/__tests__/processTransform-test.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import processTransform from '../processTransform'
|
||||
|
||||
suite('apis/StyleSheet/processTransform', () => {
|
||||
test('transform', () => {
|
||||
const style = {
|
||||
transform: [
|
||||
{ scaleX: 20 },
|
||||
{ translateX: 20 },
|
||||
{ rotate: '20deg' }
|
||||
]
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
processTransform(style),
|
||||
{ transform: 'scaleX(20) translateX(20px) rotate(20deg)' }
|
||||
)
|
||||
})
|
||||
|
||||
test('transformMatrix', () => {
|
||||
const style = {
|
||||
transformMatrix: [ 1, 2, 3, 4, 5, 6 ]
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
processTransform(style),
|
||||
{ transform: 'matrix3d(1,2,3,4,5,6)' }
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,17 @@
|
||||
/**
|
||||
* The browser implements the CSS cascade, where the order of properties is a
|
||||
* factor in determining which styles to paint. React Native is different in
|
||||
* giving precedence to the more specific styles. For example, the value of
|
||||
* `paddingTop` takes precedence over that of `padding`.
|
||||
*
|
||||
* This module creates mutally exclusive style declarations by expanding all of
|
||||
* React Native's supported shortform properties (e.g. `padding`) to their
|
||||
* longfrom equivalents.
|
||||
*/
|
||||
|
||||
import normalizeValue from './normalizeValue'
|
||||
|
||||
const styleShortHands = {
|
||||
const styleShortFormProperties = {
|
||||
borderColor: [ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor' ],
|
||||
borderRadius: [ 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius' ],
|
||||
borderStyle: [ 'borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle' ],
|
||||
@@ -8,52 +19,54 @@ const styleShortHands = {
|
||||
margin: [ 'marginTop', 'marginRight', 'marginBottom', 'marginLeft' ],
|
||||
marginHorizontal: [ 'marginRight', 'marginLeft' ],
|
||||
marginVertical: [ 'marginTop', 'marginBottom' ],
|
||||
overflow: [ 'overflowX', 'overflowY' ],
|
||||
padding: [ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft' ],
|
||||
paddingHorizontal: [ 'paddingRight', 'paddingLeft' ],
|
||||
paddingVertical: [ 'paddingTop', 'paddingBottom' ],
|
||||
textDecorationLine: [ 'textDecoration' ],
|
||||
writingDirection: [ 'direction' ]
|
||||
}
|
||||
|
||||
/**
|
||||
* Alpha-sort properties, apart from shorthands which appear before the
|
||||
* properties they expand into. This ensures that more specific styles override
|
||||
* the shorthands, whatever the order in which they were originally declared.
|
||||
*/
|
||||
const sortProps = (propsArray) => propsArray.sort((a, b) => {
|
||||
const expandedA = styleShortHands[a]
|
||||
const expandedB = styleShortHands[b]
|
||||
if (expandedA && expandedA.indexOf(b) > -1) {
|
||||
return -1
|
||||
} else if (expandedB && expandedB.indexOf(a) > -1) {
|
||||
return 1
|
||||
}
|
||||
return a < b ? -1 : a > b ? 1 : 0
|
||||
const alphaSort = (arr) => arr.sort((a, b) => {
|
||||
if (a < b) { return -1 }
|
||||
if (a > b) { return 1 }
|
||||
return 0
|
||||
})
|
||||
|
||||
/**
|
||||
* Expand the shorthand properties to isolate every declaration from the others.
|
||||
*/
|
||||
const expandStyle = (style) => {
|
||||
const propsArray = Object.keys(style)
|
||||
const sortedProps = sortProps(propsArray)
|
||||
const createStyleReducer = (originalStyle) => {
|
||||
const originalStyleProps = Object.keys(originalStyle)
|
||||
|
||||
return sortedProps.reduce((resolvedStyle, key) => {
|
||||
const expandedProps = styleShortHands[key]
|
||||
const value = normalizeValue(key, style[key])
|
||||
return (style, prop) => {
|
||||
const value = normalizeValue(prop, originalStyle[prop])
|
||||
const longFormProperties = styleShortFormProperties[prop]
|
||||
|
||||
if (expandedProps) {
|
||||
expandedProps.forEach((prop, i) => {
|
||||
resolvedStyle[expandedProps[i]] = value
|
||||
// React Native treats `flex:1` like `flex:1 1 auto`
|
||||
if (prop === 'flex') {
|
||||
style.flexGrow = value
|
||||
if (style.flexShrink == null) { style.flexShrink = 1 }
|
||||
if (style.flexBasis == null) { style.flexBasis = 'auto' }
|
||||
// React Native accepts 'center' as a value
|
||||
} else if (prop === 'textAlignVertical') {
|
||||
style.verticalAlign = (value === 'center' ? 'middle' : value)
|
||||
} else if (longFormProperties) {
|
||||
longFormProperties.forEach((longForm, i) => {
|
||||
// the value of any longform property in the original styles takes
|
||||
// precedence over the shortform's value
|
||||
if (originalStyleProps.indexOf(longForm) === -1) {
|
||||
style[longForm] = value
|
||||
}
|
||||
})
|
||||
} else if (key === 'flex') {
|
||||
resolvedStyle.flexGrow = value
|
||||
resolvedStyle.flexShrink = 1
|
||||
resolvedStyle.flexBasis = 'auto'
|
||||
} else {
|
||||
resolvedStyle[key] = value
|
||||
style[prop] = value
|
||||
}
|
||||
return resolvedStyle
|
||||
}, {})
|
||||
return style
|
||||
}
|
||||
}
|
||||
|
||||
const expandStyle = (style) => {
|
||||
const sortedStyleProps = alphaSort(Object.keys(style))
|
||||
const styleReducer = createStyleReducer(style)
|
||||
return sortedStyleProps.reduce(styleReducer, {})
|
||||
}
|
||||
|
||||
module.exports = expandStyle
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* @flow
|
||||
*/
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
import expandStyle from './expandStyle'
|
||||
|
||||
module.exports = function flattenStyle(style): ?Object {
|
||||
if (!style) {
|
||||
@@ -16,9 +15,7 @@ module.exports = function flattenStyle(style): ?Object {
|
||||
invariant(style !== true, 'style may be false but not true')
|
||||
|
||||
if (!Array.isArray(style)) {
|
||||
// we must expand styles during the flattening because expanded styles
|
||||
// override shorthands
|
||||
return expandStyle(style)
|
||||
return style
|
||||
}
|
||||
|
||||
const result = {}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { resetCSS, predefinedCSS, predefinedClassNames } from './predefs'
|
||||
import { resetCSS, predefinedCSS } from './predefs'
|
||||
import flattenStyle from './flattenStyle'
|
||||
import Store from './Store'
|
||||
import StyleSheetRegistry from './StyleSheetRegistry'
|
||||
import StyleSheetValidation from './StyleSheetValidation'
|
||||
|
||||
@@ -12,65 +11,65 @@ let lastStyleSheet = ''
|
||||
* Initialize the store with pointer-event styles mapping to our custom pointer
|
||||
* event classes
|
||||
*/
|
||||
const initialState = { classNames: predefinedClassNames }
|
||||
const options = { obfuscateClassNames: !(process.env.NODE_ENV !== 'production') }
|
||||
const createStore = () => new Store(initialState, options)
|
||||
let store = createStore()
|
||||
|
||||
/**
|
||||
* Destroy existing styles
|
||||
*/
|
||||
const _destroy = () => {
|
||||
store = createStore()
|
||||
isRendered = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the styles as a CSS style sheet
|
||||
*/
|
||||
const _renderToString = () => {
|
||||
const css = store.toString()
|
||||
isRendered = true
|
||||
return `${resetCSS}\n${predefinedCSS}\n${css}`
|
||||
StyleSheetRegistry._reset()
|
||||
}
|
||||
|
||||
const create = (styles: Object): Object => {
|
||||
for (const key in styles) {
|
||||
StyleSheetValidation.validateStyle(key, styles)
|
||||
StyleSheetRegistry.registerStyle(styles[key], store)
|
||||
StyleSheetRegistry.registerStyle(styles[key])
|
||||
}
|
||||
|
||||
// update the style sheet in place
|
||||
if (isRendered) {
|
||||
const stylesheet = document.getElementById(ELEMENT_ID)
|
||||
if (stylesheet) {
|
||||
const newStyleSheet = _renderToString()
|
||||
const newStyleSheet = renderToString()
|
||||
if (lastStyleSheet !== newStyleSheet) {
|
||||
stylesheet.textContent = newStyleSheet
|
||||
lastStyleSheet = newStyleSheet
|
||||
}
|
||||
} else if (process.env.NODE_ENV !== 'production') {
|
||||
console.error('ReactNative: cannot find "react-stylesheet" element')
|
||||
console.error(`ReactNative: cannot find "${ELEMENT_ID}" element`)
|
||||
}
|
||||
}
|
||||
|
||||
return styles
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the styles as a CSS style sheet
|
||||
*/
|
||||
const renderToString = () => {
|
||||
const css = StyleSheetRegistry.renderToString()
|
||||
isRendered = true
|
||||
return `${resetCSS}\n${predefinedCSS}\n${css}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts React props and converts inline styles to single purpose classes
|
||||
* where possible.
|
||||
*/
|
||||
const resolve = ({ style = {} }) => {
|
||||
return StyleSheetRegistry.getStyleAsNativeProps(style, store)
|
||||
const resolve = ({ className, style = {} }) => {
|
||||
const props = StyleSheetRegistry.getStyleAsNativeProps(style, isRendered)
|
||||
return {
|
||||
...props,
|
||||
className: className ? `${props.className} ${className}`.trim() : props.className
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
_destroy,
|
||||
_renderToString,
|
||||
create,
|
||||
elementId: ELEMENT_ID,
|
||||
hairlineWidth: 1,
|
||||
flatten: flattenStyle,
|
||||
renderToString,
|
||||
resolve
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ const unitlessNumbers = {
|
||||
flexNegative: true,
|
||||
fontWeight: true,
|
||||
lineClamp: true,
|
||||
lineHeight: true,
|
||||
opacity: true,
|
||||
order: true,
|
||||
orphans: true,
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
* Reset unwanted styles beyond the control of React inline styles
|
||||
*/
|
||||
export const resetCSS =
|
||||
`/* React Native Web */
|
||||
`/* React Native for Web */
|
||||
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[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {-webkit-appearance:none}`
|
||||
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {display:none}`
|
||||
|
||||
/**
|
||||
* Custom pointer event styles
|
||||
*/
|
||||
export const predefinedCSS =
|
||||
`/* pointer-events */
|
||||
._s_pe-a, ._s_pe-bo, ._s_pe-bn * {pointer-events:auto}
|
||||
._s_pe-n, ._s_pe-bo *, ._s_pe-bn {pointer-events:none}`
|
||||
.__style_pea, .__style_pebo, .__style_pebn * {pointer-events:auto}
|
||||
.__style_pen, .__style_pebo *, .__style_pebn {pointer-events:none}`
|
||||
|
||||
export const predefinedClassNames = {
|
||||
'pointerEvents:auto': '_s_pe-a',
|
||||
'pointerEvents:box-none': '_s_pe-bn',
|
||||
'pointerEvents:box-only': '_s_pe-bo',
|
||||
'pointerEvents:none': '_s_pe-n'
|
||||
'pointerEvents:auto': '__style_pea',
|
||||
'pointerEvents:box-none': '__style_pebn',
|
||||
'pointerEvents:box-only': '__style_pebo',
|
||||
'pointerEvents:none': '__style_pen'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
const translateProperties = {
|
||||
translateX: true,
|
||||
translateY: true,
|
||||
translateZ: true
|
||||
}
|
||||
|
||||
const processTransformValue = (key, value) => {
|
||||
if (translateProperties[key] && typeof value === 'number') {
|
||||
value += 'px';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// { scale: 2 } => 'scale(2)'
|
||||
// { translateX: 20 } => 'translateX(20px)'
|
||||
const mapTransform = (transform) => {
|
||||
var key = Object.keys(transform)[0]
|
||||
return `${key}(${transform[key]})`
|
||||
const type = Object.keys(transform)[0]
|
||||
const value = processTransformValue(type, transform[type])
|
||||
return `${type}(${value})`
|
||||
}
|
||||
|
||||
// [1,2,3,4,5,6] => 'matrix3d(1,2,3,4,5,6)'
|
||||
@@ -15,7 +30,8 @@ const processTransform = (style) => {
|
||||
if (style.transform) {
|
||||
style.transform = style.transform.map(mapTransform).join(' ')
|
||||
} else if (style.transformMatrix) {
|
||||
style.transformMatrix = convertTransformMatrix(style.transformMatrix)
|
||||
style.transform = convertTransformMatrix(style.transformMatrix)
|
||||
delete style.transformMatrix
|
||||
}
|
||||
}
|
||||
return style
|
||||
|
||||
@@ -94,18 +94,25 @@ suite('apis/UIManager', () => {
|
||||
})
|
||||
|
||||
suite('updateView', () => {
|
||||
const componentStub = {
|
||||
_reactInternalInstance: {
|
||||
_currentElement: { _owner: {} },
|
||||
_debugID: 1
|
||||
}
|
||||
}
|
||||
|
||||
test('add new className to existing className', () => {
|
||||
const node = createNode()
|
||||
node.className = 'existing'
|
||||
const props = { className: 'extra' }
|
||||
UIManager.updateView(node, props)
|
||||
UIManager.updateView(node, props, componentStub)
|
||||
assert.equal(node.getAttribute('class'), 'existing extra')
|
||||
})
|
||||
|
||||
test('adds new style to existing style', () => {
|
||||
const node = createNode({ color: 'red' })
|
||||
const props = { style: { opacity: 0 } }
|
||||
UIManager.updateView(node, props)
|
||||
UIManager.updateView(node, props, componentStub)
|
||||
assert.equal(node.getAttribute('style'), 'color: red; opacity: 0;')
|
||||
})
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const UIManager = {
|
||||
_measureLayout(node, relativeTo, onSuccess)
|
||||
},
|
||||
|
||||
updateView(node, props) {
|
||||
updateView(node, props, component /* only needed to surpress React errors in __DEV__ */) {
|
||||
for (const prop in props) {
|
||||
let nativeProp
|
||||
const value = props[prop]
|
||||
@@ -42,7 +42,11 @@ const UIManager = {
|
||||
switch (prop) {
|
||||
case 'style':
|
||||
// convert styles to DOM-styles
|
||||
CSSPropertyOperations.setValueForStyles(node, processTransform(flattenStyle(value)))
|
||||
CSSPropertyOperations.setValueForStyles(
|
||||
node,
|
||||
processTransform(flattenStyle(value)),
|
||||
component._reactInternalInstance
|
||||
)
|
||||
break
|
||||
case 'class':
|
||||
case 'className':
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import applyNativeMethods from '../../modules/applyNativeMethods'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
@@ -19,7 +19,6 @@ const keyframeEffects = [
|
||||
{ transform: 'scale(0.95)', opacity: 0.5 }
|
||||
]
|
||||
|
||||
@NativeMethodsDecorator
|
||||
class ActivityIndicator extends Component {
|
||||
static propTypes = {
|
||||
animating: PropTypes.bool,
|
||||
@@ -61,7 +60,7 @@ class ActivityIndicator extends Component {
|
||||
return (
|
||||
<View {...other} style={[ styles.container, style ]}>
|
||||
<View
|
||||
ref={(c) => { this._indicatorRef = c }}
|
||||
ref={this._createIndicatorRef}
|
||||
style={[
|
||||
indicatorStyles[size],
|
||||
hidesWhenStopped && !animating && styles.hidesWhenStopped,
|
||||
@@ -72,6 +71,10 @@ class ActivityIndicator extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
_createIndicatorRef = (component) => {
|
||||
this._indicatorRef = component
|
||||
}
|
||||
|
||||
_manageAnimation() {
|
||||
if (this._player) {
|
||||
if (this.props.animating) {
|
||||
@@ -83,6 +86,8 @@ class ActivityIndicator extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
applyNativeMethods(ActivityIndicator)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
|
||||
import CoreComponent from '../'
|
||||
|
||||
suite('components/CoreComponent', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const dom = utils.renderToDOM(<CoreComponent accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(dom.getAttribute('aria-label'), accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessibilityLiveRegion"', () => {
|
||||
const accessibilityLiveRegion = 'polite'
|
||||
const dom = utils.renderToDOM(<CoreComponent accessibilityLiveRegion={accessibilityLiveRegion} />)
|
||||
assert.equal(dom.getAttribute('aria-live'), accessibilityLiveRegion)
|
||||
})
|
||||
|
||||
test('prop "accessibilityRole"', () => {
|
||||
const accessibilityRole = 'banner'
|
||||
let dom = utils.renderToDOM(<CoreComponent accessibilityRole={accessibilityRole} />)
|
||||
assert.equal(dom.getAttribute('role'), accessibilityRole)
|
||||
assert.equal((dom.tagName).toLowerCase(), 'header')
|
||||
|
||||
const button = 'button'
|
||||
dom = utils.renderToDOM(<CoreComponent accessibilityRole={button} />)
|
||||
assert.equal(dom.getAttribute('type'), button)
|
||||
assert.equal((dom.tagName).toLowerCase(), button)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
// accessible (implicit)
|
||||
let dom = utils.renderToDOM(<CoreComponent />)
|
||||
assert.equal(dom.getAttribute('aria-hidden'), null)
|
||||
// accessible (explicit)
|
||||
dom = utils.renderToDOM(<CoreComponent accessible />)
|
||||
assert.equal(dom.getAttribute('aria-hidden'), null)
|
||||
// not accessible
|
||||
dom = utils.renderToDOM(<CoreComponent accessible={false} />)
|
||||
assert.equal(dom.getAttribute('aria-hidden'), 'true')
|
||||
})
|
||||
|
||||
test('prop "component"', () => {
|
||||
const component = 'main'
|
||||
const dom = utils.renderToDOM(<CoreComponent component={component} />)
|
||||
const tagName = (dom.tagName).toLowerCase()
|
||||
assert.equal(tagName, component)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
// no testID
|
||||
let dom = utils.renderToDOM(<CoreComponent />)
|
||||
assert.equal(dom.getAttribute('data-testid'), null)
|
||||
// with testID
|
||||
const testID = 'Example.testID'
|
||||
dom = utils.renderToDOM(<CoreComponent testID={testID} />)
|
||||
assert.equal(dom.getAttribute('data-testid'), testID)
|
||||
})
|
||||
})
|
||||
@@ -1,68 +0,0 @@
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
|
||||
const roleComponents = {
|
||||
article: 'article',
|
||||
banner: 'header',
|
||||
button: 'button',
|
||||
complementary: 'aside',
|
||||
contentinfo: 'footer',
|
||||
form: 'form',
|
||||
heading: 'h1',
|
||||
link: 'a',
|
||||
list: 'ul',
|
||||
listitem: 'li',
|
||||
main: 'main',
|
||||
navigation: 'nav',
|
||||
region: 'section'
|
||||
}
|
||||
|
||||
@NativeMethodsDecorator
|
||||
class CoreComponent extends Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: PropTypes.string,
|
||||
accessibilityLiveRegion: PropTypes.oneOf([ 'assertive', 'off', 'polite' ]),
|
||||
accessibilityRole: PropTypes.string,
|
||||
accessible: PropTypes.bool,
|
||||
component: PropTypes.oneOfType([ PropTypes.func, PropTypes.string ]),
|
||||
style: PropTypes.oneOfType([ PropTypes.array, PropTypes.object ]),
|
||||
testID: PropTypes.string,
|
||||
type: PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
accessible: true,
|
||||
component: 'div'
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
accessibilityLabel,
|
||||
accessibilityLiveRegion,
|
||||
accessibilityRole,
|
||||
accessible,
|
||||
component,
|
||||
testID,
|
||||
type,
|
||||
...other
|
||||
} = this.props
|
||||
|
||||
const Component = roleComponents[accessibilityRole] || component
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...other}
|
||||
{...StyleSheet.resolve(other)}
|
||||
aria-hidden={accessible ? null : true}
|
||||
aria-label={accessibilityLabel}
|
||||
aria-live={accessibilityLiveRegion}
|
||||
data-testid={testID}
|
||||
role={accessibilityRole}
|
||||
type={accessibilityRole === 'button' ? 'button' : type}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CoreComponent
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PropTypes } from 'react'
|
||||
import BorderPropTypes from '../../apis/StyleSheet/BorderPropTypes'
|
||||
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
|
||||
import LayoutPropTypes from '../../apis/StyleSheet/LayoutPropTypes'
|
||||
import TransformPropTypes from '../../apis/StyleSheet/TransformPropTypes'
|
||||
@@ -7,6 +8,7 @@ import ImageResizeMode from './ImageResizeMode'
|
||||
const hiddenOrVisible = PropTypes.oneOf([ 'hidden', 'visible' ])
|
||||
|
||||
module.exports = {
|
||||
...BorderPropTypes,
|
||||
...LayoutPropTypes,
|
||||
...TransformPropTypes,
|
||||
backfaceVisibility: hiddenOrVisible,
|
||||
|
||||
@@ -1,55 +1,56 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import { mount, shallow } from 'enzyme'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
import flattenStyle from '../../../apis/StyleSheet/flattenStyle'
|
||||
import StyleSheet from '../../../apis/StyleSheet'
|
||||
|
||||
import Image from '../'
|
||||
|
||||
const getStyleBackgroundSize = (element) => flattenStyle(element.props.style).backgroundSize
|
||||
|
||||
suite('components/Image', () => {
|
||||
test('default accessibility', () => {
|
||||
const dom = utils.renderToDOM(<Image />)
|
||||
assert.equal(dom.getAttribute('role'), 'img')
|
||||
test('sets correct accessibility role"', () => {
|
||||
const image = shallow(<Image />)
|
||||
assert.equal(image.prop('accessibilityRole'), 'img')
|
||||
})
|
||||
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = utils.shallowRender(<Image accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
|
||||
const image = shallow(<Image accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(image.prop('accessibilityLabel'), accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
const accessible = false
|
||||
const result = utils.shallowRender(<Image accessible={accessible} />)
|
||||
assert.equal(result.props.accessible, accessible)
|
||||
const image = shallow(<Image accessible={accessible} />)
|
||||
assert.equal(image.prop('accessible'), accessible)
|
||||
})
|
||||
|
||||
test('prop "children"')
|
||||
|
||||
test('prop "defaultSource"', () => {
|
||||
const defaultSource = { uri: 'https://google.com/favicon.ico' }
|
||||
const dom = utils.renderToDOM(<Image defaultSource={defaultSource} />)
|
||||
const backgroundImage = dom.style.backgroundImage
|
||||
assert(backgroundImage.indexOf(defaultSource.uri) > -1)
|
||||
test('prop "children"', () => {
|
||||
const children = <div className='unique' />
|
||||
const wrapper = shallow(<Image>{children}</Image>)
|
||||
assert.equal(wrapper.contains(children), true)
|
||||
})
|
||||
|
||||
test('prop "defaultSource" with string value"', () => {
|
||||
// emulate require-ed asset
|
||||
const defaultSource = 'https://google.com/favicon.ico'
|
||||
const dom = utils.renderToDOM(<Image defaultSource={defaultSource} />)
|
||||
const backgroundImage = dom.style.backgroundImage
|
||||
assert(backgroundImage.indexOf(defaultSource) > -1)
|
||||
suite('prop "defaultSource"', () => {
|
||||
test('sets background image when value is an object', () => {
|
||||
const defaultSource = { uri: 'https://google.com/favicon.ico' }
|
||||
const image = shallow(<Image defaultSource={defaultSource} />)
|
||||
const backgroundImage = StyleSheet.flatten(image.prop('style')).backgroundImage
|
||||
assert(backgroundImage.indexOf(defaultSource.uri) > -1)
|
||||
})
|
||||
|
||||
test('sets background image when value is a string', () => {
|
||||
// emulate require-ed asset
|
||||
const defaultSource = 'https://google.com/favicon.ico'
|
||||
const image = shallow(<Image defaultSource={defaultSource} />)
|
||||
const backgroundImage = StyleSheet.flatten(image.prop('style')).backgroundImage
|
||||
assert(backgroundImage.indexOf(defaultSource) > -1)
|
||||
})
|
||||
})
|
||||
|
||||
test('prop "onError"', function (done) {
|
||||
this.timeout(5000)
|
||||
utils.render(<Image
|
||||
onError={onError}
|
||||
source={{ uri: 'https://google.com/favicon.icox' }}
|
||||
/>)
|
||||
mount(<Image onError={onError} source={{ uri: 'https://google.com/favicon.icox' }} />)
|
||||
function onError(e) {
|
||||
assert.equal(e.nativeEvent.type, 'error')
|
||||
done()
|
||||
@@ -58,82 +59,104 @@ suite('components/Image', () => {
|
||||
|
||||
test('prop "onLoad"', function (done) {
|
||||
this.timeout(5000)
|
||||
utils.render(<Image onLoad={onLoad} source={{ uri: 'https://google.com/favicon.ico' }} />)
|
||||
const image = mount(<Image onLoad={onLoad} source={{ uri: 'https://google.com/favicon.ico' }} />)
|
||||
function onLoad(e) {
|
||||
assert.equal(e.nativeEvent.type, 'load')
|
||||
const backgroundImage = StyleSheet.flatten(image.ref('root').prop('style')).backgroundImage
|
||||
assert.notDeepEqual(backgroundImage, undefined)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "onLoadEnd"')
|
||||
test('prop "onLoadEnd"', function (done) {
|
||||
this.timeout(5000)
|
||||
const image = mount(<Image onLoadEnd={onLoadEnd} source={{ uri: 'https://google.com/favicon.ico' }} />)
|
||||
function onLoadEnd() {
|
||||
assert.ok(true)
|
||||
const backgroundImage = StyleSheet.flatten(image.ref('root').prop('style')).backgroundImage
|
||||
assert.notDeepEqual(backgroundImage, undefined)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "onLoadStart"')
|
||||
test('prop "onLoadStart"', function (done) {
|
||||
this.timeout(5000)
|
||||
mount(<Image onLoadStart={onLoadStart} source={{ uri: 'https://google.com/favicon.ico' }} />)
|
||||
function onLoadStart() {
|
||||
assert.ok(true)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
suite('prop "resizeMode"', () => {
|
||||
const getBackgroundSize = (image) => StyleSheet.flatten(image.prop('style')).backgroundSize
|
||||
|
||||
test('value "contain"', () => {
|
||||
const result = utils.shallowRender(<Image resizeMode={Image.resizeMode.contain} />)
|
||||
assert.equal(getStyleBackgroundSize(result), 'contain')
|
||||
const image = shallow(<Image resizeMode={Image.resizeMode.contain} />)
|
||||
assert.equal(getBackgroundSize(image), 'contain')
|
||||
})
|
||||
|
||||
test('value "cover"', () => {
|
||||
const result = utils.shallowRender(<Image resizeMode={Image.resizeMode.cover} />)
|
||||
assert.equal(getStyleBackgroundSize(result), 'cover')
|
||||
const image = shallow(<Image resizeMode={Image.resizeMode.cover} />)
|
||||
assert.equal(getBackgroundSize(image), 'cover')
|
||||
})
|
||||
|
||||
test('value "none"', () => {
|
||||
const result = utils.shallowRender(<Image resizeMode={Image.resizeMode.none} />)
|
||||
assert.equal(getStyleBackgroundSize(result), 'auto')
|
||||
const image = shallow(<Image resizeMode={Image.resizeMode.none} />)
|
||||
assert.equal(getBackgroundSize(image), 'auto')
|
||||
})
|
||||
|
||||
test('value "stretch"', () => {
|
||||
const result = utils.shallowRender(<Image resizeMode={Image.resizeMode.stretch} />)
|
||||
assert.equal(getStyleBackgroundSize(result), '100% 100%')
|
||||
const image = shallow(<Image resizeMode={Image.resizeMode.stretch} />)
|
||||
assert.equal(getBackgroundSize(image), '100% 100%')
|
||||
})
|
||||
|
||||
test('no value', () => {
|
||||
const result = utils.shallowRender(<Image />)
|
||||
assert.equal(getStyleBackgroundSize(result), 'cover')
|
||||
const image = shallow(<Image />)
|
||||
assert.equal(getBackgroundSize(image), 'cover')
|
||||
})
|
||||
})
|
||||
|
||||
test('prop "source"', function (done) {
|
||||
suite('prop "source"', function () {
|
||||
this.timeout(5000)
|
||||
const source = { uri: 'https://google.com/favicon.ico' }
|
||||
utils.render(<Image onLoad={onLoad} source={source} />)
|
||||
function onLoad(e) {
|
||||
const src = e.nativeEvent.target.src
|
||||
assert.equal(src, source.uri)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "source" with string value', function (done) {
|
||||
this.timeout(5000)
|
||||
// emulate require-ed asset
|
||||
const source = 'https://google.com/favicon.ico'
|
||||
utils.render(<Image onLoad={onLoad} source={source} />)
|
||||
function onLoad(e) {
|
||||
const src = e.nativeEvent.target.src
|
||||
assert.equal(src, source)
|
||||
done()
|
||||
}
|
||||
test('sets background image when value is an object', (done) => {
|
||||
const source = { uri: 'https://google.com/favicon.ico' }
|
||||
mount(<Image onLoad={onLoad} source={source} />)
|
||||
function onLoad(e) {
|
||||
const src = e.nativeEvent.target.src
|
||||
assert.equal(src, source.uri)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('sets background image when value is a string', (done) => {
|
||||
// emulate require-ed asset
|
||||
const source = 'https://google.com/favicon.ico'
|
||||
mount(<Image onLoad={onLoad} source={source} />)
|
||||
function onLoad(e) {
|
||||
const src = e.nativeEvent.target.src
|
||||
assert.equal(src, source)
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
suite('prop "style"', () => {
|
||||
test('converts "resizeMode" property', () => {
|
||||
const result = utils.shallowRender(<Image style={{ resizeMode: Image.resizeMode.contain }} />)
|
||||
assert.equal(getStyleBackgroundSize(result), 'contain')
|
||||
const image = shallow(<Image style={{ resizeMode: Image.resizeMode.contain }} />)
|
||||
assert.equal(StyleSheet.flatten(image.prop('style')).backgroundSize, 'contain')
|
||||
})
|
||||
|
||||
test('removes "resizeMode" property', () => {
|
||||
const result = utils.shallowRender(<Image style={{ resizeMode: Image.resizeMode.contain }} />)
|
||||
assert.equal(flattenStyle(result.props.style).resizeMode, undefined)
|
||||
const image = shallow(<Image style={{ resizeMode: Image.resizeMode.contain }} />)
|
||||
assert.equal(StyleSheet.flatten(image.prop('style')).resizeMode, undefined)
|
||||
})
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
const testID = 'testID'
|
||||
const result = utils.shallowRender(<Image testID={testID} />)
|
||||
assert.equal(result.props.testID, testID)
|
||||
const image = shallow(<Image testID={testID} />)
|
||||
assert.equal(image.prop('testID'), testID)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* global window */
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import resolveAssetSource from './resolveAssetSource'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import applyNativeMethods from '../../modules/applyNativeMethods'
|
||||
import createNativeComponent from '../../modules/createNativeComponent'
|
||||
import ImageResizeMode from './ImageResizeMode'
|
||||
import ImageStylePropTypes from './ImageStylePropTypes'
|
||||
import resolveAssetSource from './resolveAssetSource'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
|
||||
@@ -22,11 +22,10 @@ const ImageSourcePropType = PropTypes.oneOfType([
|
||||
PropTypes.string
|
||||
])
|
||||
|
||||
@NativeMethodsDecorator
|
||||
class Image extends Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
accessible: CoreComponent.propTypes.accessible,
|
||||
accessibilityLabel: createNativeComponent.propTypes.accessibilityLabel,
|
||||
accessible: createNativeComponent.propTypes.accessible,
|
||||
children: PropTypes.any,
|
||||
defaultSource: ImageSourcePropType,
|
||||
onError: PropTypes.func,
|
||||
@@ -36,7 +35,7 @@ class Image extends Component {
|
||||
resizeMode: PropTypes.oneOf(['contain', 'cover', 'none', 'stretch']),
|
||||
source: ImageSourcePropType,
|
||||
style: StyleSheetPropType(ImageStylePropTypes),
|
||||
testID: CoreComponent.propTypes.testID
|
||||
testID: createNativeComponent.propTypes.testID
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -49,61 +48,7 @@ class Image extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
const uri = resolveAssetSource(props.source)
|
||||
// state
|
||||
this.state = { status: uri ? STATUS_PENDING : STATUS_IDLE }
|
||||
// autobinding
|
||||
this._onError = this._onError.bind(this)
|
||||
this._onLoad = this._onLoad.bind(this)
|
||||
}
|
||||
|
||||
_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
|
||||
this._onLoadStart()
|
||||
}
|
||||
|
||||
_destroyImageLoader() {
|
||||
if (this.image) {
|
||||
this.image.onerror = null
|
||||
this.image.onload = null
|
||||
this.image = null
|
||||
}
|
||||
}
|
||||
|
||||
_onError(e) {
|
||||
const { onError } = this.props
|
||||
const event = { nativeEvent: e }
|
||||
|
||||
this._destroyImageLoader()
|
||||
this.setState({ status: STATUS_ERRORED })
|
||||
this._onLoadEnd()
|
||||
if (onError) onError(event)
|
||||
}
|
||||
|
||||
_onLoad(e) {
|
||||
const { onLoad } = this.props
|
||||
const event = { nativeEvent: e }
|
||||
|
||||
this._destroyImageLoader()
|
||||
this.setState({ status: STATUS_LOADED })
|
||||
if (onLoad) onLoad(event)
|
||||
this._onLoadEnd()
|
||||
}
|
||||
|
||||
_onLoadEnd() {
|
||||
const { onLoadEnd } = this.props
|
||||
if (onLoadEnd) onLoadEnd()
|
||||
}
|
||||
|
||||
_onLoadStart() {
|
||||
const { onLoadStart } = this.props
|
||||
this.setState({ status: STATUS_LOADING })
|
||||
if (onLoadStart) onLoadStart()
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -162,6 +107,7 @@ class Image extends Component {
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityRole='img'
|
||||
accessible={accessible}
|
||||
ref='root'
|
||||
style={[
|
||||
styles.initial,
|
||||
style,
|
||||
@@ -170,15 +116,67 @@ class Image extends Component {
|
||||
]}
|
||||
testID={testID}
|
||||
>
|
||||
<img src={displayImage} style={styles.img} />
|
||||
{createNativeComponent({ component: 'img', src: displayImage, style: styles.img })}
|
||||
{children ? (
|
||||
<View children={children} pointerEvents='box-none' style={styles.children} />
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
_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
|
||||
this._onLoadStart()
|
||||
}
|
||||
|
||||
_destroyImageLoader() {
|
||||
if (this.image) {
|
||||
this.image.onerror = null
|
||||
this.image.onload = null
|
||||
this.image = null
|
||||
}
|
||||
}
|
||||
|
||||
_onError = (e) => {
|
||||
const { onError } = this.props
|
||||
const event = { nativeEvent: e }
|
||||
|
||||
this._destroyImageLoader()
|
||||
this.setState({ status: STATUS_ERRORED })
|
||||
this._onLoadEnd()
|
||||
if (onError) onError(event)
|
||||
}
|
||||
|
||||
_onLoad = (e) => {
|
||||
const { onLoad } = this.props
|
||||
const event = { nativeEvent: e }
|
||||
|
||||
this._destroyImageLoader()
|
||||
this.setState({ status: STATUS_LOADED })
|
||||
if (onLoad) onLoad(event)
|
||||
this._onLoadEnd()
|
||||
}
|
||||
|
||||
_onLoadEnd() {
|
||||
const { onLoadEnd } = this.props
|
||||
if (onLoadEnd) onLoadEnd()
|
||||
}
|
||||
|
||||
_onLoadStart() {
|
||||
const { onLoadStart } = this.props
|
||||
this.setState({ status: STATUS_LOADING })
|
||||
if (onLoadStart) onLoadStart()
|
||||
}
|
||||
}
|
||||
|
||||
applyNativeMethods(Image)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
alignSelf: 'flex-start',
|
||||
|
||||
408
src/components/ListView/ListViewDataSource.js
Normal file
408
src/components/ListView/ListViewDataSource.js
Normal file
@@ -0,0 +1,408 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Copyright (c) 2015, Facebook, Inc. All rights reserved.
|
||||
*
|
||||
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
|
||||
* all intellectual property and other proprietary rights, in and to the React
|
||||
* Native CustomComponents software (the "Software"). Subject to your
|
||||
* compliance with these terms, you are hereby granted a non-exclusive,
|
||||
* worldwide, royalty-free copyright license to (1) use and copy the Software;
|
||||
* and (2) reproduce and distribute the Software as part of your own software
|
||||
* ("Your Software"). Facebook reserves all rights not expressly granted to
|
||||
* you in this license agreement.
|
||||
*
|
||||
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
|
||||
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
|
||||
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @providesModule ListViewDataSource
|
||||
* @typechecks
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var invariant = require('fbjs/lib/invariant');
|
||||
var isEmpty = require('fbjs/lib/isEmpty');
|
||||
var warning = require('fbjs/lib/warning');
|
||||
|
||||
function defaultGetRowData(
|
||||
dataBlob: any,
|
||||
sectionID: number | string,
|
||||
rowID: number | string
|
||||
): any {
|
||||
return dataBlob[sectionID][rowID];
|
||||
}
|
||||
|
||||
function defaultGetSectionHeaderData(
|
||||
dataBlob: any,
|
||||
sectionID: number | string
|
||||
): any {
|
||||
return dataBlob[sectionID];
|
||||
}
|
||||
|
||||
type differType = (data1: any, data2: any) => bool;
|
||||
|
||||
type ParamType = {
|
||||
rowHasChanged: differType;
|
||||
getRowData: ?typeof defaultGetRowData;
|
||||
sectionHeaderHasChanged: ?differType;
|
||||
getSectionHeaderData: ?typeof defaultGetSectionHeaderData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides efficient data processing and access to the
|
||||
* `ListView` component. A `ListViewDataSource` is created with functions for
|
||||
* extracting data from the input blob, and comparing elements (with default
|
||||
* implementations for convenience). The input blob can be as simple as an
|
||||
* array of strings, or an object with rows nested inside section objects.
|
||||
*
|
||||
* To update the data in the datasource, use `cloneWithRows` (or
|
||||
* `cloneWithRowsAndSections` if you care about sections). The data in the
|
||||
* data source is immutable, so you can't modify it directly. The clone methods
|
||||
* suck in the new data and compute a diff for each row so ListView knows
|
||||
* whether to re-render it or not.
|
||||
*
|
||||
* In this example, a component receives data in chunks, handled by
|
||||
* `_onDataArrived`, which concats the new data onto the old data and updates the
|
||||
* data source. We use `concat` to create a new array - mutating `this._data`,
|
||||
* e.g. with `this._data.push(newRowData)`, would be an error. `_rowHasChanged`
|
||||
* understands the shape of the row data and knows how to efficiently compare
|
||||
* it.
|
||||
*
|
||||
* ```
|
||||
* getInitialState: function() {
|
||||
* var ds = new ListViewDataSource({rowHasChanged: this._rowHasChanged});
|
||||
* return {ds};
|
||||
* },
|
||||
* _onDataArrived(newData) {
|
||||
* this._data = this._data.concat(newData);
|
||||
* this.setState({
|
||||
* ds: this.state.ds.cloneWithRows(this._data)
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
class ListViewDataSource {
|
||||
|
||||
/**
|
||||
* You can provide custom extraction and `hasChanged` functions for section
|
||||
* headers and rows. If absent, data will be extracted with the
|
||||
* `defaultGetRowData` and `defaultGetSectionHeaderData` functions.
|
||||
*
|
||||
* The default extractor expects data of one of the following forms:
|
||||
*
|
||||
* { sectionID_1: { rowID_1: <rowData1>, ... }, ... }
|
||||
*
|
||||
* or
|
||||
*
|
||||
* { sectionID_1: [ <rowData1>, <rowData2>, ... ], ... }
|
||||
*
|
||||
* or
|
||||
*
|
||||
* [ [ <rowData1>, <rowData2>, ... ], ... ]
|
||||
*
|
||||
* The constructor takes in a params argument that can contain any of the
|
||||
* following:
|
||||
*
|
||||
* - getRowData(dataBlob, sectionID, rowID);
|
||||
* - getSectionHeaderData(dataBlob, sectionID);
|
||||
* - rowHasChanged(prevRowData, nextRowData);
|
||||
* - sectionHeaderHasChanged(prevSectionData, nextSectionData);
|
||||
*/
|
||||
constructor(params: ParamType) {
|
||||
invariant(
|
||||
params && typeof params.rowHasChanged === 'function',
|
||||
'Must provide a rowHasChanged function.'
|
||||
);
|
||||
this._rowHasChanged = params.rowHasChanged;
|
||||
this._getRowData = params.getRowData || defaultGetRowData;
|
||||
this._sectionHeaderHasChanged = params.sectionHeaderHasChanged;
|
||||
this._getSectionHeaderData =
|
||||
params.getSectionHeaderData || defaultGetSectionHeaderData;
|
||||
|
||||
this._dataBlob = null;
|
||||
this._dirtyRows = [];
|
||||
this._dirtySections = [];
|
||||
this._cachedRowCount = 0;
|
||||
|
||||
// These two private variables are accessed by outsiders because ListView
|
||||
// uses them to iterate over the data in this class.
|
||||
this.rowIdentities = [];
|
||||
this.sectionIdentities = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones this `ListViewDataSource` with the specified `dataBlob` and
|
||||
* `rowIdentities`. The `dataBlob` is just an arbitrary blob of data. At
|
||||
* construction an extractor to get the interesting information was defined
|
||||
* (or the default was used).
|
||||
*
|
||||
* The `rowIdentities` is is a 2D array of identifiers for rows.
|
||||
* ie. [['a1', 'a2'], ['b1', 'b2', 'b3'], ...]. If not provided, it's
|
||||
* assumed that the keys of the section data are the row identities.
|
||||
*
|
||||
* Note: This function does NOT clone the data in this data source. It simply
|
||||
* passes the functions defined at construction to a new data source with
|
||||
* the data specified. If you wish to maintain the existing data you must
|
||||
* handle merging of old and new data separately and then pass that into
|
||||
* this function as the `dataBlob`.
|
||||
*/
|
||||
cloneWithRows(
|
||||
dataBlob: Array<any> | {[key: string]: any},
|
||||
rowIdentities: ?Array<string>
|
||||
): ListViewDataSource {
|
||||
var rowIds = rowIdentities ? [rowIdentities] : null;
|
||||
if (!this._sectionHeaderHasChanged) {
|
||||
this._sectionHeaderHasChanged = () => false;
|
||||
}
|
||||
return this.cloneWithRowsAndSections({s1: dataBlob}, ['s1'], rowIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* This performs the same function as the `cloneWithRows` function but here
|
||||
* you also specify what your `sectionIdentities` are. If you don't care
|
||||
* about sections you should safely be able to use `cloneWithRows`.
|
||||
*
|
||||
* `sectionIdentities` is an array of identifiers for sections.
|
||||
* ie. ['s1', 's2', ...]. If not provided, it's assumed that the
|
||||
* keys of dataBlob are the section identities.
|
||||
*
|
||||
* Note: this returns a new object!
|
||||
*/
|
||||
cloneWithRowsAndSections(
|
||||
dataBlob: any,
|
||||
sectionIdentities: ?Array<string>,
|
||||
rowIdentities: ?Array<Array<string>>
|
||||
): ListViewDataSource {
|
||||
invariant(
|
||||
typeof this._sectionHeaderHasChanged === 'function',
|
||||
'Must provide a sectionHeaderHasChanged function with section data.'
|
||||
);
|
||||
var newSource = new ListViewDataSource({
|
||||
getRowData: this._getRowData,
|
||||
getSectionHeaderData: this._getSectionHeaderData,
|
||||
rowHasChanged: this._rowHasChanged,
|
||||
sectionHeaderHasChanged: this._sectionHeaderHasChanged,
|
||||
});
|
||||
newSource._dataBlob = dataBlob;
|
||||
if (sectionIdentities) {
|
||||
newSource.sectionIdentities = sectionIdentities;
|
||||
} else {
|
||||
newSource.sectionIdentities = Object.keys(dataBlob);
|
||||
}
|
||||
if (rowIdentities) {
|
||||
newSource.rowIdentities = rowIdentities;
|
||||
} else {
|
||||
newSource.rowIdentities = [];
|
||||
newSource.sectionIdentities.forEach((sectionID) => {
|
||||
newSource.rowIdentities.push(Object.keys(dataBlob[sectionID]));
|
||||
});
|
||||
}
|
||||
newSource._cachedRowCount = countRows(newSource.rowIdentities);
|
||||
|
||||
newSource._calculateDirtyArrays(
|
||||
this._dataBlob,
|
||||
this.sectionIdentities,
|
||||
this.rowIdentities
|
||||
);
|
||||
|
||||
return newSource;
|
||||
}
|
||||
|
||||
getRowCount(): number {
|
||||
return this._cachedRowCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the row is dirtied and needs to be rerendered
|
||||
*/
|
||||
rowShouldUpdate(sectionIndex: number, rowIndex: number): bool {
|
||||
var needsUpdate = this._dirtyRows[sectionIndex][rowIndex];
|
||||
warning(needsUpdate !== undefined,
|
||||
'missing dirtyBit for section, row: ' + sectionIndex + ', ' + rowIndex);
|
||||
return needsUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data required to render the row.
|
||||
*/
|
||||
getRowData(sectionIndex: number, rowIndex: number): any {
|
||||
var sectionID = this.sectionIdentities[sectionIndex];
|
||||
var rowID = this.rowIdentities[sectionIndex][rowIndex];
|
||||
warning(
|
||||
sectionID !== undefined && rowID !== undefined,
|
||||
'rendering invalid section, row: ' + sectionIndex + ', ' + rowIndex
|
||||
);
|
||||
return this._getRowData(this._dataBlob, sectionID, rowID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the rowID at index provided if the dataSource arrays were flattened,
|
||||
* or null of out of range indexes.
|
||||
*/
|
||||
getRowIDForFlatIndex(index: number): ?string {
|
||||
var accessIndex = index;
|
||||
for (var ii = 0; ii < this.sectionIdentities.length; ii++) {
|
||||
if (accessIndex >= this.rowIdentities[ii].length) {
|
||||
accessIndex -= this.rowIdentities[ii].length;
|
||||
} else {
|
||||
return this.rowIdentities[ii][accessIndex];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sectionID at index provided if the dataSource arrays were flattened,
|
||||
* or null for out of range indexes.
|
||||
*/
|
||||
getSectionIDForFlatIndex(index: number): ?string {
|
||||
var accessIndex = index;
|
||||
for (var ii = 0; ii < this.sectionIdentities.length; ii++) {
|
||||
if (accessIndex >= this.rowIdentities[ii].length) {
|
||||
accessIndex -= this.rowIdentities[ii].length;
|
||||
} else {
|
||||
return this.sectionIdentities[ii];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing the number of rows in each section
|
||||
*/
|
||||
getSectionLengths(): Array<number> {
|
||||
var results = [];
|
||||
for (var ii = 0; ii < this.sectionIdentities.length; ii++) {
|
||||
results.push(this.rowIdentities[ii].length);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the section header is dirtied and needs to be rerendered
|
||||
*/
|
||||
sectionHeaderShouldUpdate(sectionIndex: number): bool {
|
||||
var needsUpdate = this._dirtySections[sectionIndex];
|
||||
warning(needsUpdate !== undefined,
|
||||
'missing dirtyBit for section: ' + sectionIndex);
|
||||
return needsUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data required to render the section header
|
||||
*/
|
||||
getSectionHeaderData(sectionIndex: number): any {
|
||||
if (!this._getSectionHeaderData) {
|
||||
return null;
|
||||
}
|
||||
var sectionID = this.sectionIdentities[sectionIndex];
|
||||
warning(sectionID !== undefined,
|
||||
'renderSection called on invalid section: ' + sectionIndex);
|
||||
return this._getSectionHeaderData(this._dataBlob, sectionID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private members and methods.
|
||||
*/
|
||||
|
||||
_getRowData: typeof defaultGetRowData;
|
||||
_getSectionHeaderData: typeof defaultGetSectionHeaderData;
|
||||
_rowHasChanged: differType;
|
||||
_sectionHeaderHasChanged: ?differType;
|
||||
|
||||
_dataBlob: any;
|
||||
_dirtyRows: Array<Array<bool>>;
|
||||
_dirtySections: Array<bool>;
|
||||
_cachedRowCount: number;
|
||||
|
||||
// These two 'protected' variables are accessed by ListView to iterate over
|
||||
// the data in this class.
|
||||
rowIdentities: Array<Array<string>>;
|
||||
sectionIdentities: Array<string>;
|
||||
|
||||
_calculateDirtyArrays(
|
||||
prevDataBlob: any,
|
||||
prevSectionIDs: Array<string>,
|
||||
prevRowIDs: Array<Array<string>>
|
||||
): void {
|
||||
// construct a hashmap of the existing (old) id arrays
|
||||
var prevSectionsHash = keyedDictionaryFromArray(prevSectionIDs);
|
||||
var prevRowsHash = {};
|
||||
for (var ii = 0; ii < prevRowIDs.length; ii++) {
|
||||
var sectionID = prevSectionIDs[ii];
|
||||
warning(
|
||||
!prevRowsHash[sectionID],
|
||||
'SectionID appears more than once: ' + sectionID
|
||||
);
|
||||
prevRowsHash[sectionID] = keyedDictionaryFromArray(prevRowIDs[ii]);
|
||||
}
|
||||
|
||||
// compare the 2 identity array and get the dirtied rows
|
||||
this._dirtySections = [];
|
||||
this._dirtyRows = [];
|
||||
|
||||
var dirty;
|
||||
for (var sIndex = 0; sIndex < this.sectionIdentities.length; sIndex++) {
|
||||
var sectionID = this.sectionIdentities[sIndex];
|
||||
// dirty if the sectionHeader is new or _sectionHasChanged is true
|
||||
dirty = !prevSectionsHash[sectionID];
|
||||
var sectionHeaderHasChanged = this._sectionHeaderHasChanged;
|
||||
if (!dirty && sectionHeaderHasChanged) {
|
||||
dirty = sectionHeaderHasChanged(
|
||||
this._getSectionHeaderData(prevDataBlob, sectionID),
|
||||
this._getSectionHeaderData(this._dataBlob, sectionID)
|
||||
);
|
||||
}
|
||||
this._dirtySections.push(!!dirty);
|
||||
|
||||
this._dirtyRows[sIndex] = [];
|
||||
for (var rIndex = 0; rIndex < this.rowIdentities[sIndex].length; rIndex++) {
|
||||
var rowID = this.rowIdentities[sIndex][rIndex];
|
||||
// dirty if the section is new, row is new or _rowHasChanged is true
|
||||
dirty =
|
||||
!prevSectionsHash[sectionID] ||
|
||||
!prevRowsHash[sectionID][rowID] ||
|
||||
this._rowHasChanged(
|
||||
this._getRowData(prevDataBlob, sectionID, rowID),
|
||||
this._getRowData(this._dataBlob, sectionID, rowID)
|
||||
);
|
||||
this._dirtyRows[sIndex].push(!!dirty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function countRows(allRowIDs) {
|
||||
var totalRows = 0;
|
||||
for (var sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) {
|
||||
var rowIDs = allRowIDs[sectionIdx];
|
||||
totalRows += rowIDs.length;
|
||||
}
|
||||
return totalRows;
|
||||
}
|
||||
|
||||
function keyedDictionaryFromArray(arr) {
|
||||
if (isEmpty(arr)) {
|
||||
return {};
|
||||
}
|
||||
var result = {};
|
||||
for (var ii = 0; ii < arr.length; ii++) {
|
||||
var key = arr[ii];
|
||||
warning(!result[key], 'Value appears more than once in array: ' + key);
|
||||
result[key] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
module.exports = ListViewDataSource;
|
||||
22
src/components/ListView/ListViewPropTypes.js
Normal file
22
src/components/ListView/ListViewPropTypes.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { PropTypes } from 'react'
|
||||
import ScrollView from '../ScrollView'
|
||||
import ListViewDataSource from './ListViewDataSource'
|
||||
|
||||
export default {
|
||||
...ScrollView.propTypes,
|
||||
dataSource: PropTypes.instanceOf(ListViewDataSource).isRequired,
|
||||
renderSeparator: PropTypes.func,
|
||||
renderRow: PropTypes.func.isRequired,
|
||||
initialListSize: PropTypes.number,
|
||||
onEndReached: PropTypes.func,
|
||||
onEndReachedThreshold: PropTypes.number,
|
||||
pageSize: PropTypes.number,
|
||||
renderFooter: PropTypes.func,
|
||||
renderHeader: PropTypes.func,
|
||||
renderSectionHeader: PropTypes.func,
|
||||
renderScrollComponent: PropTypes.func.isRequired,
|
||||
scrollRenderAheadDistance: PropTypes.number,
|
||||
onChangeVisibleRows: PropTypes.func,
|
||||
removeClippedSubviews: PropTypes.bool,
|
||||
stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number)
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
suite('components/ListView', () => {
|
||||
test('NO TEST COVERAGE')
|
||||
})
|
||||
|
||||
@@ -1,23 +1,104 @@
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import applyNativeMethods from '../../modules/applyNativeMethods'
|
||||
import React, { Component } from 'react'
|
||||
import ScrollView from '../ScrollView'
|
||||
import ListViewDataSource from './ListViewDataSource'
|
||||
import ListViewPropTypes from './ListViewPropTypes'
|
||||
|
||||
const SCROLLVIEW_REF = 'listviewscroll'
|
||||
|
||||
@NativeMethodsDecorator
|
||||
class ListView extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
style: ScrollView.propTypes.style
|
||||
};
|
||||
static propTypes = ListViewPropTypes;
|
||||
|
||||
static defaultProps = {
|
||||
style: {}
|
||||
initialListSize: 10,
|
||||
pageSize: 1,
|
||||
renderScrollComponent: (props) => <ScrollView {...props} />,
|
||||
scrollRenderAheadDistance: 1000,
|
||||
onEndReachedThreshold: 1000,
|
||||
stickyHeaderIndices: []
|
||||
};
|
||||
|
||||
static DataSource = ListViewDataSource;
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
curRenderedRowsCount: this.props.initialListSize,
|
||||
highlightedRow: {}
|
||||
}
|
||||
this.onRowHighlighted = (sectionId, rowId) => this._onRowHighlighted(sectionId, rowId)
|
||||
}
|
||||
|
||||
getScrollResponder() {
|
||||
return this.refs[SCROLLVIEW_REF] && this.refs[SCROLLVIEW_REF].getScrollResponder()
|
||||
}
|
||||
|
||||
scrollTo(...args) {
|
||||
return this.refs[SCROLLVIEW_REF] && this.refs[SCROLLVIEW_REF].scrollTo(...args)
|
||||
}
|
||||
|
||||
setNativeProps(props) {
|
||||
return this.refs[SCROLLVIEW_REF] && this.refs[SCROLLVIEW_REF].setNativeProps(props)
|
||||
}
|
||||
|
||||
_onRowHighlighted(sectionId, rowId) {
|
||||
this.setState({highlightedRow: {sectionId, rowId}})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ScrollView {...this.props} />
|
||||
)
|
||||
const dataSource = this.props.dataSource
|
||||
const header = this.props.renderHeader ? this.props.renderHeader() : undefined
|
||||
const footer = this.props.renderFooter ? this.props.renderFooter() : undefined
|
||||
|
||||
// render sections and rows
|
||||
const children = []
|
||||
const sections = dataSource.rowIdentities
|
||||
const renderRow = this.props.renderRow
|
||||
const renderSectionHeader = this.props.renderSectionHeader
|
||||
const renderSeparator = this.props.renderSeparator
|
||||
for (let sectionIdx = 0, sectionCnt = sections.length; sectionIdx < sectionCnt; sectionIdx++) {
|
||||
const rows = sections[sectionIdx]
|
||||
const sectionId = dataSource.sectionIdentities[sectionIdx]
|
||||
|
||||
// render optional section header
|
||||
if (renderSectionHeader) {
|
||||
const section = dataSource.getSectionHeaderData(sectionIdx)
|
||||
const key = 's_' + sectionId
|
||||
const child = <div key={key}>{renderSectionHeader(section, sectionId)}</div>
|
||||
children.push(child)
|
||||
}
|
||||
|
||||
// render rows
|
||||
for (let rowIdx = 0, rowCnt = rows.length; rowIdx < rowCnt; rowIdx++) {
|
||||
const rowId = rows[rowIdx]
|
||||
const row = dataSource.getRowData(sectionIdx, rowIdx)
|
||||
const key = 'r_' + sectionId + '_' + rowId
|
||||
const child = <div key={key}>{renderRow(row, sectionId, rowId, this.onRowHighlighted)}</div>
|
||||
children.push(child)
|
||||
|
||||
// render optional separator
|
||||
if (renderSeparator && ((rowIdx !== rows.length - 1) || (sectionIdx === sections.length - 1))) {
|
||||
const adjacentRowHighlighted =
|
||||
this.state.highlightedRow.sectionID === sectionId && (
|
||||
this.state.highlightedRow.rowID === rowId ||
|
||||
this.state.highlightedRow.rowID === rows[rowIdx + 1])
|
||||
const separator = renderSeparator(sectionId, rowId, adjacentRowHighlighted)
|
||||
children.push(separator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
renderScrollComponent,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
return React.cloneElement(renderScrollComponent(props), {
|
||||
ref: SCROLLVIEW_REF
|
||||
}, header, children, footer)
|
||||
}
|
||||
}
|
||||
|
||||
applyNativeMethods(ListView)
|
||||
|
||||
module.exports = ListView
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
/**
|
||||
* Copyright 2015-present, Nicolas Gallagher
|
||||
* Copyright 2004-present, Facebook Inc.
|
||||
* All Rights Reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import Platform from '../../apis/Platform'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import View from '../View'
|
||||
|
||||
let _portalRef: any
|
||||
// unique identifiers for modals
|
||||
let lastUsedTag = 0
|
||||
|
||||
/**
|
||||
* A container that renders all the modals on top of everything else in the application.
|
||||
*/
|
||||
class Portal extends Component {
|
||||
static propTypes = {
|
||||
onModalVisibilityChanged: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new unique tag.
|
||||
*/
|
||||
static allocateTag(): string {
|
||||
return `__modal_${++lastUsedTag}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a new modal.
|
||||
*/
|
||||
static showModal(tag: string, component: any) {
|
||||
if (!_portalRef) {
|
||||
console.error('Calling showModal but no "Portal" has been rendered.')
|
||||
return
|
||||
}
|
||||
_portalRef._showModal(tag, component)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a modal from the collection of modals to be rendered.
|
||||
*/
|
||||
static closeModal(tag: string) {
|
||||
if (!_portalRef) {
|
||||
console.error('Calling closeModal but no "Portal" has been rendered.')
|
||||
return
|
||||
}
|
||||
_portalRef._closeModal(tag)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of all the open modals, as identified by their tag string.
|
||||
*/
|
||||
static getOpenModals(): Array<string> {
|
||||
if (!_portalRef) {
|
||||
console.error('Calling getOpenModals but no "Portal" has been rendered.')
|
||||
return []
|
||||
}
|
||||
return _portalRef._getOpenModals()
|
||||
}
|
||||
|
||||
static notifyAccessibilityService() {
|
||||
if (!_portalRef) {
|
||||
console.error('Calling closeModal but no "Portal" has been rendered.')
|
||||
return
|
||||
}
|
||||
_portalRef._notifyAccessibilityService()
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { modals: {} }
|
||||
this._closeModal = this._closeModal.bind(this)
|
||||
this._getOpenModals = this._getOpenModals.bind(this)
|
||||
this._showModal = this._showModal.bind(this)
|
||||
}
|
||||
|
||||
render() {
|
||||
_portalRef = this
|
||||
if (!this.state.modals) { return null }
|
||||
const modals = []
|
||||
for (const tag in this.state.modals) {
|
||||
modals.push(this.state.modals[tag])
|
||||
}
|
||||
if (modals.length === 0) { return null }
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
{modals}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
_closeModal(tag: string) {
|
||||
if (!this.state.modals.hasOwnProperty(tag)) {
|
||||
return
|
||||
}
|
||||
// We are about to close last modal, so Portal will disappear.
|
||||
// Let's enable accessibility for application view.
|
||||
if (this._getOpenModals().length === 1) {
|
||||
this.props.onModalVisibilityChanged(false)
|
||||
}
|
||||
// This way state is chained through multiple calls to
|
||||
// _showModal, _closeModal correctly.
|
||||
this.setState((state) => {
|
||||
const modals = state.modals
|
||||
delete modals[tag]
|
||||
return { modals }
|
||||
})
|
||||
}
|
||||
|
||||
_getOpenModals(): Array<string> {
|
||||
return Object.keys(this.state.modals)
|
||||
}
|
||||
|
||||
_notifyAccessibilityService() {
|
||||
if (Platform.OS === 'web') {
|
||||
// We need to send accessibility event in a new batch, as otherwise
|
||||
// TextViews have no text set at the moment of populating event.
|
||||
}
|
||||
}
|
||||
|
||||
_showModal(tag: string, component: any) {
|
||||
// We are about to open first modal, so Portal will appear.
|
||||
// Let's disable accessibility for background view on Android.
|
||||
if (this._getOpenModals().length === 0) {
|
||||
this.props.onModalVisibilityChanged(true)
|
||||
}
|
||||
// This way state is chained through multiple calls to
|
||||
// _showModal, _closeModal correctly.
|
||||
this.setState((state) => {
|
||||
const modals = state.modals
|
||||
modals[tag] = component
|
||||
return { modals }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = Portal
|
||||
@@ -6,7 +6,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import debounce from 'lodash.debounce'
|
||||
import debounce from 'lodash/debounce'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import View from '../View'
|
||||
|
||||
|
||||
@@ -15,14 +15,17 @@ module.exports = {
|
||||
letterSpacing: numberOrString,
|
||||
lineHeight: numberOrString,
|
||||
textAlign: oneOf([ 'center', 'inherit', 'justify', 'justify-all', 'left', 'right' ]),
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
textDecoration: string,
|
||||
textAlignVertical: oneOf([ 'auto', 'bottom', 'center', 'top' ]),
|
||||
textDecorationLine: string,
|
||||
/* @platform web */
|
||||
textOverflow: string,
|
||||
/* @platform web */
|
||||
textShadow: string,
|
||||
/* @platform web */
|
||||
textTransform: oneOf([ 'capitalize', 'lowercase', 'none', 'uppercase' ]),
|
||||
/* @platform web */
|
||||
whiteSpace: string,
|
||||
/* @platform web */
|
||||
wordWrap: string,
|
||||
writingDirection: string
|
||||
writingDirection: oneOf([ 'auto', 'ltr', 'rtl' ])
|
||||
}
|
||||
|
||||
@@ -1,51 +1,25 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
import ReactTestUtils from 'react-addons-test-utils'
|
||||
|
||||
import Text from '../'
|
||||
import { mount, shallow } from 'enzyme'
|
||||
|
||||
suite('components/Text', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = utils.shallowRender(<Text accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessibilityRole"', () => {
|
||||
const accessibilityRole = 'accessibilityRole'
|
||||
const result = utils.shallowRender(<Text accessibilityRole={accessibilityRole} />)
|
||||
assert.equal(result.props.accessibilityRole, accessibilityRole)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
const accessible = false
|
||||
const result = utils.shallowRender(<Text accessible={accessible} />)
|
||||
assert.equal(result.props.accessible, accessible)
|
||||
})
|
||||
|
||||
test('prop "children"', () => {
|
||||
const children = 'children'
|
||||
const result = utils.shallowRender(<Text>{children}</Text>)
|
||||
assert.equal(result.props.children, children)
|
||||
const text = shallow(<Text>{children}</Text>)
|
||||
assert.equal(text.prop('children'), children)
|
||||
})
|
||||
|
||||
test('prop "numberOfLines"')
|
||||
|
||||
test('prop "onPress"', (done) => {
|
||||
const dom = utils.renderToDOM(<Text onPress={onPress} />)
|
||||
ReactTestUtils.Simulate.click(dom)
|
||||
const text = mount(<Text onPress={onPress} />)
|
||||
text.simulate('click')
|
||||
function onPress(e) {
|
||||
assert.ok(e.nativeEvent)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
const testID = 'testID'
|
||||
const result = utils.shallowRender(<Text testID={testID} />)
|
||||
assert.equal(result.props.testID, testID)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,54 +1,55 @@
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import applyNativeMethods from '../../modules/applyNativeMethods'
|
||||
import createNativeComponent from '../../modules/createNativeComponent'
|
||||
import { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
|
||||
import TextStylePropTypes from './TextStylePropTypes'
|
||||
|
||||
@NativeMethodsDecorator
|
||||
class Text extends Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
accessibilityRole: CoreComponent.propTypes.accessibilityRole,
|
||||
accessible: CoreComponent.propTypes.accessible,
|
||||
accessibilityLabel: createNativeComponent.propTypes.accessibilityLabel,
|
||||
accessibilityRole: createNativeComponent.propTypes.accessibilityRole,
|
||||
accessible: createNativeComponent.propTypes.accessible,
|
||||
children: PropTypes.any,
|
||||
numberOfLines: PropTypes.number,
|
||||
onPress: PropTypes.func,
|
||||
style: StyleSheetPropType(TextStylePropTypes),
|
||||
testID: CoreComponent.propTypes.testID
|
||||
testID: createNativeComponent.propTypes.testID
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
accessible: true
|
||||
};
|
||||
|
||||
_onPress(e) {
|
||||
_onPress = (e) => {
|
||||
if (this.props.onPress) this.props.onPress(e)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
numberOfLines,
|
||||
/* eslint-disable no-unused-vars */
|
||||
onPress,
|
||||
/* eslint-enable no-unused-vars */
|
||||
style,
|
||||
...other
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<CoreComponent
|
||||
{...other}
|
||||
component='span'
|
||||
onClick={this._onPress.bind(this)}
|
||||
style={[
|
||||
styles.initial,
|
||||
style,
|
||||
numberOfLines === 1 && styles.singleLineStyle
|
||||
]}
|
||||
/>
|
||||
)
|
||||
return createNativeComponent({
|
||||
...other,
|
||||
component: 'span',
|
||||
onClick: this._onPress,
|
||||
style: [
|
||||
styles.initial,
|
||||
style,
|
||||
numberOfLines === 1 && styles.singleLineStyle
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
applyNativeMethods(Text)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
color: 'inherit',
|
||||
@@ -56,7 +57,7 @@ const styles = StyleSheet.create({
|
||||
font: 'inherit',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
textDecoration: 'none',
|
||||
textDecorationLine: 'none',
|
||||
wordWrap: 'break-word'
|
||||
},
|
||||
singleLineStyle: {
|
||||
|
||||
@@ -1,94 +1,96 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
import ReactTestUtils from 'react-addons-test-utils'
|
||||
import StyleSheet from '../../../apis/StyleSheet'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import TextInput from '..'
|
||||
import { mount, shallow } from 'enzyme'
|
||||
|
||||
import TextInput from '../'
|
||||
const placeholderText = 'placeholderText'
|
||||
const findNativeInput = (wrapper) => wrapper.find('input')
|
||||
const findNativeTextarea = (wrapper) => wrapper.find(TextareaAutosize)
|
||||
const findPlaceholder = (wrapper) => wrapper.find({ children: placeholderText })
|
||||
|
||||
const findInput = (dom) => dom.querySelector('input, textarea')
|
||||
const findShallowInput = (vdom) => vdom.props.children.props.children[0]
|
||||
const findShallowPlaceholder = (vdom) => vdom.props.children.props.children[1]
|
||||
const testIfDocumentIsFocused = (message, fn) => {
|
||||
if (document.hasFocus && document.hasFocus()) {
|
||||
test(message, fn)
|
||||
} else {
|
||||
test.skip(`${message} – document is not focused`)
|
||||
}
|
||||
}
|
||||
|
||||
suite('components/TextInput', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = utils.shallowRender(<TextInput accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "autoComplete"', () => {
|
||||
// off
|
||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
||||
assert.equal(input.getAttribute('autocomplete'), undefined)
|
||||
let input = findNativeInput(shallow(<TextInput />))
|
||||
assert.equal(input.prop('autoComplete'), undefined)
|
||||
// on
|
||||
input = findInput(utils.renderToDOM(<TextInput autoComplete />))
|
||||
assert.equal(input.getAttribute('autocomplete'), 'on')
|
||||
input = findNativeInput(shallow(<TextInput autoComplete />))
|
||||
assert.equal(input.prop('autoComplete'), 'on')
|
||||
})
|
||||
|
||||
test('prop "autoFocus"', () => {
|
||||
// false
|
||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
||||
assert.deepEqual(document.activeElement, document.body)
|
||||
let input = findNativeInput(mount(<TextInput />))
|
||||
assert.equal(input.prop('autoFocus'), undefined)
|
||||
// true
|
||||
input = findInput(utils.renderToDOM(<TextInput autoFocus />))
|
||||
assert.deepEqual(document.activeElement, input)
|
||||
input = findNativeInput(mount(<TextInput autoFocus />))
|
||||
assert.equal(input.prop('autoFocus'), true)
|
||||
})
|
||||
|
||||
utils.testIfDocumentFocused('prop "clearTextOnFocus"', () => {
|
||||
testIfDocumentIsFocused('prop "clearTextOnFocus"', () => {
|
||||
const defaultValue = 'defaultValue'
|
||||
// false
|
||||
let input = findInput(utils.renderAndInject(<TextInput defaultValue={defaultValue} />))
|
||||
input.focus()
|
||||
assert.equal(input.value, defaultValue)
|
||||
let input = findNativeInput(mount(<TextInput defaultValue={defaultValue} />))
|
||||
input.simulate('focus')
|
||||
assert.equal(input.node.value, defaultValue)
|
||||
// true
|
||||
input = findInput(utils.renderAndInject(<TextInput clearTextOnFocus defaultValue={defaultValue} />))
|
||||
input.focus()
|
||||
assert.equal(input.value, '')
|
||||
input = findNativeInput(mount(<TextInput clearTextOnFocus defaultValue={defaultValue} />))
|
||||
input.simulate('focus')
|
||||
assert.equal(input.node.value, '')
|
||||
})
|
||||
|
||||
test('prop "defaultValue"', () => {
|
||||
const defaultValue = 'defaultValue'
|
||||
const input = findShallowInput(utils.shallowRender(<TextInput defaultValue={defaultValue} />))
|
||||
assert.equal(input.props.defaultValue, defaultValue)
|
||||
const input = findNativeInput(shallow(<TextInput defaultValue={defaultValue} />))
|
||||
assert.equal(input.prop('defaultValue'), defaultValue)
|
||||
})
|
||||
|
||||
test('prop "editable"', () => {
|
||||
// true
|
||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
||||
assert.equal(input.getAttribute('readonly'), undefined)
|
||||
let input = findNativeInput(shallow(<TextInput />))
|
||||
assert.equal(input.prop('readOnly'), false)
|
||||
// false
|
||||
input = findInput(utils.renderToDOM(<TextInput editable={false} />))
|
||||
assert.equal(input.getAttribute('readonly'), '')
|
||||
input = findNativeInput(shallow(<TextInput editable={false} />))
|
||||
assert.equal(input.prop('readOnly'), true)
|
||||
})
|
||||
|
||||
test('prop "keyboardType"', () => {
|
||||
// default
|
||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
||||
assert.equal(input.getAttribute('type'), undefined)
|
||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='default' />))
|
||||
assert.equal(input.getAttribute('type'), undefined)
|
||||
let input = findNativeInput(shallow(<TextInput />))
|
||||
assert.equal(input.prop('type'), undefined)
|
||||
input = findNativeInput(shallow(<TextInput keyboardType='default' />))
|
||||
assert.equal(input.prop('type'), undefined)
|
||||
// email-address
|
||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='email-address' />))
|
||||
assert.equal(input.getAttribute('type'), 'email')
|
||||
input = findNativeInput(shallow(<TextInput keyboardType='email-address' />))
|
||||
assert.equal(input.prop('type'), 'email')
|
||||
// numeric
|
||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='numeric' />))
|
||||
assert.equal(input.getAttribute('type'), 'number')
|
||||
input = findNativeInput(shallow(<TextInput keyboardType='numeric' />))
|
||||
assert.equal(input.prop('type'), 'number')
|
||||
// phone-pad
|
||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='phone-pad' />))
|
||||
assert.equal(input.getAttribute('type'), 'tel')
|
||||
input = findNativeInput(shallow(<TextInput keyboardType='phone-pad' />))
|
||||
assert.equal(input.prop('type'), 'tel')
|
||||
// url
|
||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='url' />))
|
||||
assert.equal(input.getAttribute('type'), 'url')
|
||||
input = findNativeInput(shallow(<TextInput keyboardType='url' />))
|
||||
assert.equal(input.prop('type'), 'url')
|
||||
})
|
||||
|
||||
test('prop "maxLength"', () => {
|
||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
||||
assert.equal(input.getAttribute('maxlength'), undefined)
|
||||
input = findInput(utils.renderToDOM(<TextInput maxLength={10} />))
|
||||
assert.equal(input.getAttribute('maxlength'), '10')
|
||||
let input = findNativeInput(shallow(<TextInput />))
|
||||
assert.equal(input.prop('maxLength'), undefined)
|
||||
input = findNativeInput(shallow(<TextInput maxLength={10} />))
|
||||
assert.equal(input.prop('maxLength'), '10')
|
||||
})
|
||||
|
||||
test('prop "maxNumberOfLines"', () => {
|
||||
@@ -98,45 +100,45 @@ suite('components/TextInput', () => {
|
||||
return str
|
||||
}
|
||||
|
||||
const result = utils.shallowRender(
|
||||
const input = findNativeTextarea(shallow(
|
||||
<TextInput
|
||||
maxNumberOfLines={3}
|
||||
multiline
|
||||
value={generateValue()}
|
||||
/>
|
||||
)
|
||||
assert.equal(findShallowInput(result).props.maxRows, 3)
|
||||
))
|
||||
assert.equal(input.prop('maxRows'), 3)
|
||||
})
|
||||
|
||||
test('prop "multiline"', () => {
|
||||
// false
|
||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
||||
assert.equal(input.tagName, 'INPUT')
|
||||
let input = findNativeInput(shallow(<TextInput />))
|
||||
assert.equal(input.length, 1)
|
||||
// true
|
||||
input = findInput(utils.renderToDOM(<TextInput multiline />))
|
||||
assert.equal(input.tagName, 'TEXTAREA')
|
||||
input = findNativeTextarea(shallow(<TextInput multiline />))
|
||||
assert.equal(input.length, 1)
|
||||
})
|
||||
|
||||
test('prop "numberOfLines"', () => {
|
||||
// missing multiline
|
||||
let input = findInput(utils.renderToDOM(<TextInput numberOfLines={2} />))
|
||||
assert.equal(input.tagName, 'INPUT')
|
||||
let input = findNativeInput(shallow(<TextInput numberOfLines={2} />))
|
||||
assert.equal(input.length, 1)
|
||||
// with multiline
|
||||
input = findInput(utils.renderAndInject(<TextInput multiline numberOfLines={2} />))
|
||||
assert.equal(input.tagName, 'TEXTAREA')
|
||||
input = findNativeTextarea(shallow(<TextInput multiline numberOfLines={2} />))
|
||||
assert.equal(input.length, 1)
|
||||
|
||||
const result = utils.shallowRender(
|
||||
input = findNativeTextarea(shallow(
|
||||
<TextInput
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
/>
|
||||
)
|
||||
assert.equal(findShallowInput(result).props.minRows, 3)
|
||||
))
|
||||
assert.equal(input.prop('minRows'), 3)
|
||||
})
|
||||
|
||||
test('prop "onBlur"', (done) => {
|
||||
const input = findInput(utils.renderToDOM(<TextInput onBlur={onBlur} />))
|
||||
ReactTestUtils.Simulate.blur(input)
|
||||
const input = findNativeInput(mount(<TextInput onBlur={onBlur} />))
|
||||
input.simulate('blur')
|
||||
function onBlur(e) {
|
||||
assert.ok(e)
|
||||
done()
|
||||
@@ -144,8 +146,8 @@ suite('components/TextInput', () => {
|
||||
})
|
||||
|
||||
test('prop "onChange"', (done) => {
|
||||
const input = findInput(utils.renderToDOM(<TextInput onChange={onChange} />))
|
||||
ReactTestUtils.Simulate.change(input)
|
||||
const input = findNativeInput(mount(<TextInput onChange={onChange} />))
|
||||
input.simulate('change')
|
||||
function onChange(e) {
|
||||
assert.ok(e)
|
||||
done()
|
||||
@@ -154,8 +156,8 @@ suite('components/TextInput', () => {
|
||||
|
||||
test('prop "onChangeText"', (done) => {
|
||||
const newText = 'newText'
|
||||
const input = findInput(utils.renderToDOM(<TextInput onChangeText={onChangeText} />))
|
||||
ReactTestUtils.Simulate.change(input, { target: { value: newText } })
|
||||
const input = findNativeInput(mount(<TextInput onChangeText={onChangeText} />))
|
||||
input.simulate('change', { target: { value: newText } })
|
||||
function onChangeText(text) {
|
||||
assert.equal(text, newText)
|
||||
done()
|
||||
@@ -163,8 +165,8 @@ suite('components/TextInput', () => {
|
||||
})
|
||||
|
||||
test('prop "onFocus"', (done) => {
|
||||
const input = findInput(utils.renderToDOM(<TextInput onFocus={onFocus} />))
|
||||
ReactTestUtils.Simulate.focus(input)
|
||||
const input = findNativeInput(mount(<TextInput onFocus={onFocus} />))
|
||||
input.simulate('focus')
|
||||
function onFocus(e) {
|
||||
assert.ok(e)
|
||||
done()
|
||||
@@ -174,8 +176,8 @@ suite('components/TextInput', () => {
|
||||
test('prop "onLayout"')
|
||||
|
||||
test('prop "onSelectionChange"', (done) => {
|
||||
const input = findInput(utils.renderAndInject(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />))
|
||||
ReactTestUtils.Simulate.select(input, { target: { selectionStart: 0, selectionEnd: 3 } })
|
||||
const input = findNativeInput(mount(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />))
|
||||
input.simulate('select', { target: { selectionStart: 0, selectionEnd: 3 } })
|
||||
function onSelectionChange(e) {
|
||||
assert.equal(e.selectionEnd, 3)
|
||||
assert.equal(e.selectionStart, 0)
|
||||
@@ -184,52 +186,46 @@ suite('components/TextInput', () => {
|
||||
})
|
||||
|
||||
test('prop "placeholder"', () => {
|
||||
const placeholder = 'placeholder'
|
||||
const result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} />))
|
||||
assert.equal(result.props.children, placeholder)
|
||||
let textInput = shallow(<TextInput />)
|
||||
assert.equal(findPlaceholder(textInput).length, 0)
|
||||
|
||||
textInput = shallow(<TextInput placeholder={placeholderText} />)
|
||||
assert.equal(findPlaceholder(textInput).length, 1)
|
||||
})
|
||||
|
||||
test('prop "placeholderTextColor"', () => {
|
||||
const placeholder = 'placeholder'
|
||||
let placeholderElement = findPlaceholder(shallow(<TextInput placeholder={placeholderText} />))
|
||||
assert.equal(StyleSheet.flatten(placeholderElement.prop('style')).color, 'darkgray')
|
||||
|
||||
let result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} />))
|
||||
assert.equal(StyleSheet.flatten(result.props.style).color, 'darkgray')
|
||||
|
||||
result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} placeholderTextColor='red' />))
|
||||
assert.equal(StyleSheet.flatten(result.props.style).color, 'red')
|
||||
placeholderElement = findPlaceholder(shallow(<TextInput placeholder={placeholderText} placeholderTextColor='red' />))
|
||||
assert.equal(StyleSheet.flatten(placeholderElement.prop('style')).color, 'red')
|
||||
})
|
||||
|
||||
test('prop "secureTextEntry"', () => {
|
||||
let input = findInput(utils.renderToDOM(<TextInput secureTextEntry />))
|
||||
assert.equal(input.getAttribute('type'), 'password')
|
||||
let input = findNativeInput(shallow(<TextInput secureTextEntry />))
|
||||
assert.equal(input.prop('type'), 'password')
|
||||
// ignored for multiline
|
||||
input = findInput(utils.renderToDOM(<TextInput multiline secureTextEntry />))
|
||||
assert.equal(input.getAttribute('type'), undefined)
|
||||
input = findNativeTextarea(shallow(<TextInput multiline secureTextEntry />))
|
||||
assert.equal(input.prop('type'), undefined)
|
||||
})
|
||||
|
||||
utils.testIfDocumentFocused('prop "selectTextOnFocus"', () => {
|
||||
testIfDocumentIsFocused('prop "selectTextOnFocus"', () => {
|
||||
const text = 'Text'
|
||||
// false
|
||||
let input = findInput(utils.renderAndInject(<TextInput defaultValue={text} />))
|
||||
input.focus()
|
||||
assert.equal(input.selectionEnd, 0)
|
||||
assert.equal(input.selectionStart, 0)
|
||||
let input = findNativeInput(mount(<TextInput defaultValue={text} />))
|
||||
input.node.focus()
|
||||
assert.equal(input.node.selectionEnd, 4)
|
||||
assert.equal(input.node.selectionStart, 4)
|
||||
// true
|
||||
input = findInput(utils.renderAndInject(<TextInput defaultValue={text} selectTextOnFocus />))
|
||||
input.focus()
|
||||
assert.equal(input.selectionEnd, 4)
|
||||
assert.equal(input.selectionStart, 0)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
const testID = 'testID'
|
||||
const result = utils.shallowRender(<TextInput testID={testID} />)
|
||||
assert.equal(result.props.testID, testID)
|
||||
// input = findNativeInput(mount(<TextInput defaultValue={text} selectTextOnFocus />))
|
||||
// input.node.focus()
|
||||
// assert.equal(input.node.selectionEnd, 4)
|
||||
// assert.equal(input.node.selectionStart, 0)
|
||||
})
|
||||
|
||||
test('prop "value"', () => {
|
||||
const value = 'value'
|
||||
const input = findShallowInput(utils.shallowRender(<TextInput value={value} />))
|
||||
assert.equal(input.props.value, value)
|
||||
const input = findNativeInput(shallow(<TextInput value={value} />))
|
||||
assert.equal(input.prop('value'), value)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import applyNativeMethods from '../../modules/applyNativeMethods'
|
||||
import createNativeComponent from '../../modules/createNativeComponent'
|
||||
import omit from 'lodash/omit'
|
||||
import pick from 'lodash/pick'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import Text from '../Text'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import TextInputState from './TextInputState'
|
||||
import UIManager from '../../apis/UIManager'
|
||||
import View from '../View'
|
||||
import ViewStylePropTypes from '../View/ViewStylePropTypes'
|
||||
|
||||
const viewStyleProps = Object.keys(ViewStylePropTypes)
|
||||
|
||||
@NativeMethodsDecorator
|
||||
class TextInput extends Component {
|
||||
static propTypes = {
|
||||
...View.propTypes,
|
||||
@@ -32,7 +37,7 @@ class TextInput extends Component {
|
||||
secureTextEntry: PropTypes.bool,
|
||||
selectTextOnFocus: PropTypes.bool,
|
||||
style: Text.propTypes.style,
|
||||
testID: CoreComponent.propTypes.testID,
|
||||
testID: Text.propTypes.testID,
|
||||
value: PropTypes.string
|
||||
};
|
||||
|
||||
@@ -63,51 +68,7 @@ class TextInput extends Component {
|
||||
}
|
||||
|
||||
setNativeProps(props) {
|
||||
this.refs.input.setNativeProps(props)
|
||||
}
|
||||
|
||||
_onBlur(e) {
|
||||
const { onBlur } = this.props
|
||||
const text = e.target.value
|
||||
this.setState({ showPlaceholder: text === '' })
|
||||
this.blur()
|
||||
if (onBlur) onBlur(e)
|
||||
}
|
||||
|
||||
_onChange(e) {
|
||||
const { onChange, onChangeText } = this.props
|
||||
const text = e.target.value
|
||||
this.setState({ showPlaceholder: text === '' })
|
||||
if (onChange) onChange(e)
|
||||
if (onChangeText) onChangeText(text)
|
||||
if (!this.refs.input) {
|
||||
// calling `this.props.onChange` or `this.props.onChangeText`
|
||||
// may clean up the input itself. Exits here.
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_onFocus(e) {
|
||||
const { clearTextOnFocus, onFocus, selectTextOnFocus } = this.props
|
||||
const node = ReactDOM.findDOMNode(this.refs.input)
|
||||
const text = e.target.value
|
||||
this.focus()
|
||||
if (onFocus) onFocus(e)
|
||||
if (clearTextOnFocus) this.clear()
|
||||
if (selectTextOnFocus) node.select()
|
||||
this.setState({ showPlaceholder: text === '' })
|
||||
}
|
||||
|
||||
_onSelectionChange(e) {
|
||||
const { onSelectionChange } = this.props
|
||||
const { selectionDirection, selectionEnd, selectionStart } = e.target
|
||||
const event = {
|
||||
selectionDirection,
|
||||
selectionEnd,
|
||||
selectionStart,
|
||||
nativeEvent: e.nativeEvent
|
||||
}
|
||||
if (onSelectionChange) onSelectionChange(event)
|
||||
UIManager.updateView(this.refs.input, props, this)
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -158,17 +119,23 @@ class TextInput extends Component {
|
||||
type = 'password'
|
||||
}
|
||||
|
||||
// In order to support 'Text' styles on 'TextInput', we split the 'Text'
|
||||
// and 'View' styles and apply them to the 'Text' and 'View' components
|
||||
// used in the implementation
|
||||
const rootStyles = pick(style, viewStyleProps)
|
||||
const textStyles = omit(style, viewStyleProps)
|
||||
|
||||
const propsCommon = {
|
||||
autoComplete: autoComplete && 'on',
|
||||
autoFocus,
|
||||
defaultValue,
|
||||
maxLength,
|
||||
onBlur: this._onBlur.bind(this),
|
||||
onChange: this._onChange.bind(this),
|
||||
onFocus: this._onFocus.bind(this),
|
||||
onSelect: onSelectionChange && this._onSelectionChange.bind(this),
|
||||
onBlur: this._handleBlur,
|
||||
onChange: this._handleChange,
|
||||
onFocus: this._handleFocus,
|
||||
onSelect: onSelectionChange && this._handleSelectionChange,
|
||||
readOnly: !editable,
|
||||
style: { ...styles.input, outline: style.outline },
|
||||
style: { ...styles.input, ...textStyles, outline: style.outline },
|
||||
value
|
||||
}
|
||||
|
||||
@@ -187,37 +154,93 @@ class TextInput extends Component {
|
||||
|
||||
const props = multiline ? propsMultiline : propsSingleline
|
||||
|
||||
const optionalPlaceholder = placeholder && this.state.showPlaceholder && (
|
||||
<View pointerEvents='none' style={styles.placeholder}>
|
||||
<Text
|
||||
children={placeholder}
|
||||
style={[
|
||||
styles.placeholderText,
|
||||
textStyles,
|
||||
placeholderTextColor && { color: placeholderTextColor }
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
||||
return (
|
||||
<View
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
style={[
|
||||
styles.initial,
|
||||
style
|
||||
]}
|
||||
onClick={this._handleClick}
|
||||
style={[ styles.initial, rootStyles ]}
|
||||
testID={testID}
|
||||
>
|
||||
<View style={styles.wrapper}>
|
||||
<CoreComponent {...props} ref='input' />
|
||||
{placeholder && this.state.showPlaceholder && <Text
|
||||
pointerEvents='none'
|
||||
style={[
|
||||
styles.placeholder,
|
||||
placeholderTextColor && { color: placeholderTextColor }
|
||||
]}
|
||||
>{placeholder}</Text>}
|
||||
{createNativeComponent({ ...props, ref: 'input' })}
|
||||
{optionalPlaceholder}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
_handleBlur = (e) => {
|
||||
const { onBlur } = this.props
|
||||
const text = e.target.value
|
||||
this.setState({ showPlaceholder: text === '' })
|
||||
this.blur()
|
||||
if (onBlur) onBlur(e)
|
||||
}
|
||||
|
||||
_handleChange = (e) => {
|
||||
const { onChange, onChangeText } = this.props
|
||||
const text = e.target.value
|
||||
this.setState({ showPlaceholder: text === '' })
|
||||
if (onChange) onChange(e)
|
||||
if (onChangeText) onChangeText(text)
|
||||
if (!this.refs.input) {
|
||||
// calling `this.props.onChange` or `this.props.onChangeText`
|
||||
// may clean up the input itself. Exits here.
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_handleClick = (e) => {
|
||||
this.focus()
|
||||
}
|
||||
|
||||
_handleFocus = (e) => {
|
||||
const { clearTextOnFocus, onFocus, selectTextOnFocus } = this.props
|
||||
const node = ReactDOM.findDOMNode(this.refs.input)
|
||||
const text = e.target.value
|
||||
if (onFocus) onFocus(e)
|
||||
if (clearTextOnFocus) this.clear()
|
||||
if (selectTextOnFocus) node.select()
|
||||
this.setState({ showPlaceholder: text === '' })
|
||||
}
|
||||
|
||||
_handleSelectionChange = (e) => {
|
||||
const { onSelectionChange } = this.props
|
||||
try {
|
||||
const { selectionDirection, selectionEnd, selectionStart } = e.target
|
||||
const event = {
|
||||
selectionDirection,
|
||||
selectionEnd,
|
||||
selectionStart,
|
||||
nativeEvent: e.nativeEvent
|
||||
}
|
||||
if (onSelectionChange) onSelectionChange(event)
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
applyNativeMethods(TextInput)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
borderColor: 'black',
|
||||
borderWidth: 1
|
||||
},
|
||||
wrapper: {
|
||||
flexGrow: 1
|
||||
flex: 1
|
||||
},
|
||||
input: {
|
||||
appearance: 'none',
|
||||
@@ -225,19 +248,23 @@ const styles = StyleSheet.create({
|
||||
borderWidth: 0,
|
||||
boxSizing: 'border-box',
|
||||
color: 'inherit',
|
||||
flexGrow: 1,
|
||||
flex: 1,
|
||||
font: 'inherit',
|
||||
minHeight: '100%', // center small inputs (fix #139)
|
||||
padding: 0,
|
||||
zIndex: 1
|
||||
},
|
||||
placeholder: {
|
||||
bottom: 0,
|
||||
color: 'darkgray',
|
||||
justifyContent: 'center',
|
||||
left: 0,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
top: 0
|
||||
},
|
||||
placeholderText: {
|
||||
color: 'darkgray',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'pre'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule Touchable
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/* @edit start */
|
||||
var BoundingDimensions = require('./BoundingDimensions');
|
||||
var Position = require('./Position');
|
||||
var TouchEventUtils = require('fbjs/lib/TouchEventUtils');
|
||||
var keyMirror = require('fbjs/lib/keyMirror');
|
||||
var UIManager = require('../../apis/UIManager');
|
||||
const BoundingDimensions = require('./BoundingDimensions');
|
||||
const keyMirror = require('fbjs/lib/keyMirror');
|
||||
const normalizeColor = require('../../apis/StyleSheet/normalizeColor');
|
||||
const Position = require('./Position');
|
||||
const React = require('react');
|
||||
const TouchEventUtils = require('fbjs/lib/TouchEventUtils');
|
||||
const UIManager = require('../../apis/UIManager');
|
||||
const View = require('../../components/View');
|
||||
/* @edit end */
|
||||
|
||||
/**
|
||||
@@ -353,10 +362,10 @@ var TouchableMixin = {
|
||||
/**
|
||||
* Place as callback for a DOM element's `onResponderGrant` event.
|
||||
* @param {SyntheticEvent} e Synthetic event from event system.
|
||||
* @param {string} dispatchID ID of node that e was dispatched to.
|
||||
*
|
||||
*/
|
||||
touchableHandleResponderGrant: function(e, dispatchID) {
|
||||
touchableHandleResponderGrant: function(e) {
|
||||
var dispatchID = e.currentTarget;
|
||||
// Since e is used in a callback invoked on another event loop
|
||||
// (as in setTimeout etc), we need to call e.persist() on the
|
||||
// event to make sure it doesn't get reused in the event object pool.
|
||||
@@ -717,7 +726,38 @@ var TouchableMixin = {
|
||||
};
|
||||
|
||||
var Touchable = {
|
||||
Mixin: TouchableMixin
|
||||
Mixin: TouchableMixin,
|
||||
TOUCH_TARGET_DEBUG: false, // Highlights all touchable targets. Toggle with Inspector.
|
||||
/**
|
||||
* Renders a debugging overlay to visualize touch target with hitSlop (might not work on Android).
|
||||
*/
|
||||
renderDebugView: ({color, hitSlop}) => {
|
||||
if (!Touchable.TOUCH_TARGET_DEBUG) {
|
||||
return null;
|
||||
}
|
||||
if (!__DEV__) {
|
||||
throw Error('Touchable.TOUCH_TARGET_DEBUG should not be enabled in prod!');
|
||||
}
|
||||
const debugHitSlopStyle = {};
|
||||
hitSlop = hitSlop || {top: 0, bottom: 0, left: 0, right: 0};
|
||||
for (const key in hitSlop) {
|
||||
debugHitSlopStyle[key] = -hitSlop[key];
|
||||
}
|
||||
const hexColor = '#' + ('00000000' + normalizeColor(color).toString(16)).substr(-8);
|
||||
return (
|
||||
<View
|
||||
pointerEvents="none"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
borderColor: hexColor.slice(0, -2) + '55', // More opaque
|
||||
borderWidth: 1,
|
||||
borderStyle: 'dashed',
|
||||
backgroundColor: hexColor.slice(0, -2) + '0F', // Less opaque
|
||||
...debugHitSlopStyle
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Touchable;
|
||||
|
||||
@@ -93,21 +93,24 @@ var TouchableHighlight = React.createClass({
|
||||
getDefaultProps: () => DEFAULT_PROPS,
|
||||
|
||||
// Performance optimization to avoid constantly re-generating these objects.
|
||||
computeSyntheticState: function(props) {
|
||||
computeSyntheticState: (props) => {
|
||||
const { activeOpacity, style, underlayColor } = props;
|
||||
return {
|
||||
activeProps: {
|
||||
style: {
|
||||
opacity: props.activeOpacity,
|
||||
opacity: activeOpacity,
|
||||
}
|
||||
},
|
||||
activeUnderlayProps: {
|
||||
style: {
|
||||
backgroundColor: props.underlayColor,
|
||||
backgroundColor: underlayColor,
|
||||
}
|
||||
},
|
||||
underlayStyle: [
|
||||
INACTIVE_UNDERLAY_PROPS.style
|
||||
]
|
||||
underlayProps: {
|
||||
style: {
|
||||
backgroundColor: style && style.backgroundColor || null
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -203,10 +206,7 @@ var TouchableHighlight = React.createClass({
|
||||
this._hideTimeout = null;
|
||||
if (this._hasPressHandler() && this.refs[UNDERLAY_REF]) {
|
||||
this.refs[CHILD_REF].setNativeProps(INACTIVE_CHILD_PROPS);
|
||||
this.refs[UNDERLAY_REF].setNativeProps({
|
||||
...INACTIVE_UNDERLAY_PROPS,
|
||||
style: this.state.underlayStyle,
|
||||
});
|
||||
this.refs[UNDERLAY_REF].setNativeProps(this.state.underlayProps);
|
||||
this.props.onHideUnderlay && this.props.onHideUnderlay();
|
||||
}
|
||||
},
|
||||
@@ -233,19 +233,19 @@ var TouchableHighlight = React.createClass({
|
||||
accessible={true}
|
||||
accessibilityLabel={this.props.accessibilityLabel}
|
||||
accessibilityRole={this.props.accessibilityRole || this.props.accessibilityTraits || 'button'}
|
||||
ref={UNDERLAY_REF}
|
||||
style={[styles.root, this.props.style]}
|
||||
onLayout={this.props.onLayout}
|
||||
hitSlop={this.props.hitSlop}
|
||||
onKeyDown={(e) => { this._onKeyEnter(e, this.touchableHandleActivePressIn) }}
|
||||
onKeyPress={(e) => { this._onKeyEnter(e, this.touchableHandlePress) }}
|
||||
onKeyUp={(e) => { this._onKeyEnter(e, this.touchableHandleActivePressOut) }}
|
||||
onLayout={this.props.onLayout}
|
||||
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
|
||||
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
|
||||
onResponderGrant={this.touchableHandleResponderGrant}
|
||||
onResponderMove={this.touchableHandleResponderMove}
|
||||
onResponderRelease={this.touchableHandleResponderRelease}
|
||||
onResponderTerminate={this.touchableHandleResponderTerminate}
|
||||
ref={UNDERLAY_REF}
|
||||
style={[styles.root, this.props.style]}
|
||||
tabIndex='0'
|
||||
testID={this.props.testID}>
|
||||
{React.cloneElement(
|
||||
@@ -264,9 +264,6 @@ var UNDERLAY_REF = keyOf({underlayRef: null});
|
||||
var INACTIVE_CHILD_PROPS = {
|
||||
style: StyleSheet.create({x: {opacity: 1.0}}).x,
|
||||
};
|
||||
var INACTIVE_UNDERLAY_PROPS = {
|
||||
style: {backgroundColor: null}
|
||||
};
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
root: {
|
||||
|
||||
@@ -145,7 +145,7 @@ var TouchableWithoutFeedback = React.createClass({
|
||||
|
||||
render: function(): ReactElement {
|
||||
// Note(avik): remove dynamic typecast once Flow has been upgraded
|
||||
return (React: any).cloneElement(React.children.only(this.props.children), {
|
||||
return (React: any).cloneElement(React.Children.only(this.props.children), {
|
||||
accessible: this.props.accessible !== false,
|
||||
accessibilityLabel: this.props.accessibilityLabel,
|
||||
accessibilityRole: this.props.accessibilityRole,
|
||||
|
||||
@@ -1,51 +1,33 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
import StyleSheet from '../../../apis/StyleSheet'
|
||||
|
||||
import { shallow } from 'enzyme'
|
||||
import View from '../'
|
||||
|
||||
suite('components/View', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = utils.shallowRender(<View accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessibilityLiveRegion"', () => {
|
||||
const accessibilityLiveRegion = 'polite'
|
||||
const result = utils.shallowRender(<View accessibilityLiveRegion={accessibilityLiveRegion} />)
|
||||
assert.equal(result.props.accessibilityLiveRegion, accessibilityLiveRegion)
|
||||
})
|
||||
|
||||
test('prop "accessibilityRole"', () => {
|
||||
const accessibilityRole = 'accessibilityRole'
|
||||
const result = utils.shallowRender(<View accessibilityRole={accessibilityRole} />)
|
||||
assert.equal(result.props.accessibilityRole, accessibilityRole)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
const accessible = false
|
||||
const result = utils.shallowRender(<View accessible={accessible} />)
|
||||
assert.equal(result.props.accessible, accessible)
|
||||
})
|
||||
|
||||
test('prop "children"', () => {
|
||||
const children = 'children'
|
||||
const result = utils.shallowRender(<View>{children}</View>)
|
||||
assert.equal(result.props.children, children)
|
||||
const view = shallow(<View>{children}</View>)
|
||||
assert.equal(view.prop('children'), children)
|
||||
})
|
||||
|
||||
test('prop "pointerEvents"', () => {
|
||||
const result = utils.shallowRender(<View pointerEvents='box-only' />)
|
||||
assert.equal(StyleSheet.flatten(result.props.style).pointerEvents, 'box-only')
|
||||
const view = shallow(<View pointerEvents='box-only' />)
|
||||
assert.equal(view.prop('className'), '__style_pebo')
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
const testID = 'testID'
|
||||
const result = utils.shallowRender(<View testID={testID} />)
|
||||
assert.equal(result.props.testID, testID)
|
||||
test('prop "style"', () => {
|
||||
const view = shallow(<View />)
|
||||
assert.equal(view.prop('style').flexShrink, 0)
|
||||
|
||||
const flexView = shallow(<View style={{ flex: 1 }} />)
|
||||
assert.equal(flexView.prop('style').flexShrink, 1)
|
||||
|
||||
const flexShrinkView = shallow(<View style={{ flexShrink: 1 }} />)
|
||||
assert.equal(flexShrinkView.prop('style').flexShrink, 1)
|
||||
|
||||
const flexAndShrinkView = shallow(<View style={{ flex: 1, flexShrink: 2 }} />)
|
||||
assert.equal(flexAndShrinkView.prop('style').flexShrink, 2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import applyNativeMethods from '../../modules/applyNativeMethods'
|
||||
import createNativeComponent from '../../modules/createNativeComponent'
|
||||
import normalizeNativeEvent from '../../apis/PanResponder/normalizeNativeEvent'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
|
||||
import ViewStylePropTypes from './ViewStylePropTypes'
|
||||
|
||||
@NativeMethodsDecorator
|
||||
class View extends Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
accessibilityLiveRegion: CoreComponent.propTypes.accessibilityLiveRegion,
|
||||
accessibilityRole: CoreComponent.propTypes.accessibilityRole,
|
||||
accessible: CoreComponent.propTypes.accessible,
|
||||
accessibilityLabel: createNativeComponent.propTypes.accessibilityLabel,
|
||||
accessibilityLiveRegion: createNativeComponent.propTypes.accessibilityLiveRegion,
|
||||
accessibilityRole: createNativeComponent.propTypes.accessibilityRole,
|
||||
accessible: createNativeComponent.propTypes.accessible,
|
||||
children: PropTypes.any,
|
||||
onClick: PropTypes.func,
|
||||
onClickCapture: PropTypes.func,
|
||||
@@ -36,11 +35,12 @@ class View extends Component {
|
||||
onTouchStartCapture: PropTypes.func,
|
||||
pointerEvents: PropTypes.oneOf(['auto', 'box-none', 'box-only', 'none']),
|
||||
style: StyleSheetPropType(ViewStylePropTypes),
|
||||
testID: CoreComponent.propTypes.testID
|
||||
testID: createNativeComponent.propTypes.testID
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
accessible: true
|
||||
accessible: true,
|
||||
style: {}
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
@@ -55,28 +55,31 @@ class View extends Component {
|
||||
...other
|
||||
} = this.props
|
||||
|
||||
const flattenedStyle = StyleSheet.flatten(style)
|
||||
const pointerEventsStyle = pointerEvents && { pointerEvents }
|
||||
|
||||
return (
|
||||
<CoreComponent
|
||||
{...other}
|
||||
onClick={this._handleClick}
|
||||
onClickCapture={this._normalizeEventForHandler(this.props.onClickCapture)}
|
||||
onTouchCancel={this._normalizeEventForHandler(this.props.onTouchCancel)}
|
||||
onTouchCancelCapture={this._normalizeEventForHandler(this.props.onTouchCancelCapture)}
|
||||
onTouchEnd={this._normalizeEventForHandler(this.props.onTouchEnd)}
|
||||
onTouchEndCapture={this._normalizeEventForHandler(this.props.onTouchEndCapture)}
|
||||
onTouchMove={this._normalizeEventForHandler(this.props.onTouchMove)}
|
||||
onTouchMoveCapture={this._normalizeEventForHandler(this.props.onTouchMoveCapture)}
|
||||
onTouchStart={this._normalizeEventForHandler(this.props.onTouchStart)}
|
||||
onTouchStartCapture={this._normalizeEventForHandler(this.props.onTouchStartCapture)}
|
||||
style={[
|
||||
styles.initial,
|
||||
style,
|
||||
pointerEventsStyle
|
||||
]}
|
||||
/>
|
||||
)
|
||||
const props = {
|
||||
...other,
|
||||
onClick: this._normalizeEventForHandler(this.props.onClick),
|
||||
onClickCapture: this._normalizeEventForHandler(this.props.onClickCapture),
|
||||
onTouchCancel: this._normalizeEventForHandler(this.props.onTouchCancel),
|
||||
onTouchCancelCapture: this._normalizeEventForHandler(this.props.onTouchCancelCapture),
|
||||
onTouchEnd: this._normalizeEventForHandler(this.props.onTouchEnd),
|
||||
onTouchEndCapture: this._normalizeEventForHandler(this.props.onTouchEndCapture),
|
||||
onTouchMove: this._normalizeEventForHandler(this.props.onTouchMove),
|
||||
onTouchMoveCapture: this._normalizeEventForHandler(this.props.onTouchMoveCapture),
|
||||
onTouchStart: this._normalizeEventForHandler(this.props.onTouchStart),
|
||||
onTouchStartCapture: this._normalizeEventForHandler(this.props.onTouchStartCapture),
|
||||
style: [
|
||||
styles.initial,
|
||||
style,
|
||||
// 'View' needs to use 'flexShrink' in its reset when there is no 'flex' style provided
|
||||
(flattenedStyle.flex == null && flattenedStyle.flexShrink == null) && styles.flexReset,
|
||||
pointerEventsStyle
|
||||
]
|
||||
}
|
||||
|
||||
return createNativeComponent(props)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,6 +97,8 @@ class View extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
applyNativeMethods(View)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
// https://github.com/facebook/css-layout#default-values
|
||||
initial: {
|
||||
@@ -104,7 +109,6 @@ const styles = StyleSheet.create({
|
||||
display: 'flex',
|
||||
flexBasis: 'auto',
|
||||
flexDirection: 'column',
|
||||
flexShrink: 0,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
@@ -113,13 +117,16 @@ const styles = StyleSheet.create({
|
||||
color: 'inherit',
|
||||
font: 'inherit',
|
||||
textAlign: 'inherit',
|
||||
textDecoration: 'none',
|
||||
textDecorationLine: 'none',
|
||||
// list reset
|
||||
listStyle: 'none',
|
||||
// fix flexbox bugs
|
||||
maxWidth: '100%',
|
||||
minHeight: 0,
|
||||
minWidth: 0
|
||||
},
|
||||
flexReset: {
|
||||
flexShrink: 0
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
25
src/index.js
25
src/index.js
@@ -1,16 +1,16 @@
|
||||
import React from 'react'
|
||||
import './apis/PanResponder/injectResponderEventPlugin'
|
||||
|
||||
import findNodeHandle from './modules/findNodeHandle'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ReactDOMServer from 'react-dom/server'
|
||||
|
||||
import './apis/PanResponder/injectResponderEventPlugin'
|
||||
|
||||
// apis
|
||||
import Animated from './apis/Animated'
|
||||
import AppRegistry from './apis/AppRegistry'
|
||||
import AppState from './apis/AppState'
|
||||
import AsyncStorage from './apis/AsyncStorage'
|
||||
import Dimensions from './apis/Dimensions'
|
||||
import Easing from './apis/Easing'
|
||||
import Easing from 'animated/lib/Easing'
|
||||
import InteractionManager from './apis/InteractionManager'
|
||||
import NetInfo from './apis/NetInfo'
|
||||
import PanResponder from './apis/PanResponder'
|
||||
@@ -23,7 +23,6 @@ import UIManager from './apis/UIManager'
|
||||
import ActivityIndicator from './components/ActivityIndicator'
|
||||
import Image from './components/Image'
|
||||
import ListView from './components/ListView'
|
||||
import Portal from './components/Portal'
|
||||
import ScrollView from './components/ScrollView'
|
||||
import Text from './components/Text'
|
||||
import TextInput from './components/TextInput'
|
||||
@@ -44,6 +43,14 @@ import EdgeInsetsPropType from './apis/StyleSheet/EdgeInsetsPropType'
|
||||
import PointPropType from './apis/StyleSheet/PointPropType'
|
||||
|
||||
const ReactNative = {
|
||||
// top-level API
|
||||
findNodeHandle,
|
||||
render: ReactDOM.render,
|
||||
unmountComponentAtNode: ReactDOM.unmountComponentAtNode,
|
||||
// web-only
|
||||
renderToStaticMarkup: ReactDOMServer.renderToStaticMarkup,
|
||||
renderToString: ReactDOMServer.renderToString,
|
||||
|
||||
// apis
|
||||
Animated,
|
||||
AppRegistry,
|
||||
@@ -63,7 +70,6 @@ const ReactNative = {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
ListView,
|
||||
Portal,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
@@ -80,12 +86,7 @@ const ReactNative = {
|
||||
// propTypes
|
||||
ColorPropType,
|
||||
EdgeInsetsPropType,
|
||||
PointPropType,
|
||||
|
||||
// React
|
||||
...React,
|
||||
...ReactDOM,
|
||||
...ReactDOMServer
|
||||
PointPropType
|
||||
}
|
||||
|
||||
module.exports = ReactNative
|
||||
|
||||
@@ -103,7 +103,8 @@ const NativeMethodsMixin = {
|
||||
setNativeProps(nativeProps: Object) {
|
||||
UIManager.updateView(
|
||||
ReactDOM.findDOMNode(this),
|
||||
nativeProps
|
||||
nativeProps,
|
||||
this
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import NativeMethodsMixin from '../NativeMethodsMixin'
|
||||
|
||||
const NativeMethodsDecorator = (Component) => {
|
||||
const applyNativeMethods = (Component) => {
|
||||
Object.keys(NativeMethodsMixin).forEach((method) => {
|
||||
if (!Component.prototype[method]) {
|
||||
Component.prototype[method] = NativeMethodsMixin[method]
|
||||
@@ -16,4 +16,4 @@ const NativeMethodsDecorator = (Component) => {
|
||||
return Component
|
||||
}
|
||||
|
||||
module.exports = NativeMethodsDecorator
|
||||
module.exports = applyNativeMethods
|
||||
59
src/modules/createNativeComponent/__tests__/index-test.js
Normal file
59
src/modules/createNativeComponent/__tests__/index-test.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import createNativeComponent from '..'
|
||||
import { shallow } from 'enzyme'
|
||||
|
||||
suite('modules/createNativeComponent', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const element = shallow(createNativeComponent({ accessibilityLabel }))
|
||||
assert.equal(element.prop('aria-label'), accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessibilityLiveRegion"', () => {
|
||||
const accessibilityLiveRegion = 'polite'
|
||||
const element = shallow(createNativeComponent({ accessibilityLiveRegion }))
|
||||
assert.equal(element.prop('aria-live'), accessibilityLiveRegion)
|
||||
})
|
||||
|
||||
test('prop "accessibilityRole"', () => {
|
||||
const accessibilityRole = 'banner'
|
||||
let element = shallow(createNativeComponent({ accessibilityRole }))
|
||||
assert.equal(element.prop('role'), accessibilityRole)
|
||||
assert.equal(element.is('header'), true)
|
||||
|
||||
const button = 'button'
|
||||
element = shallow(createNativeComponent({ accessibilityRole: 'button' }))
|
||||
assert.equal(element.prop('type'), button)
|
||||
assert.equal(element.is('button'), true)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
// accessible (implicit)
|
||||
let element = shallow(createNativeComponent({}))
|
||||
assert.equal(element.prop('aria-hidden'), null)
|
||||
// accessible (explicit)
|
||||
element = shallow(createNativeComponent({ accessible: true }))
|
||||
assert.equal(element.prop('aria-hidden'), null)
|
||||
// not accessible
|
||||
element = shallow(createNativeComponent({ accessible: false }))
|
||||
assert.equal(element.prop('aria-hidden'), true)
|
||||
})
|
||||
|
||||
test('prop "component"', () => {
|
||||
const component = 'main'
|
||||
const element = shallow(createNativeComponent({ component }))
|
||||
assert.equal(element.is('main'), true)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
// no testID
|
||||
let element = shallow(createNativeComponent({}))
|
||||
assert.equal(element.prop('data-testid'), null)
|
||||
// with testID
|
||||
const testID = 'Example.testID'
|
||||
element = shallow(createNativeComponent({ testID }))
|
||||
assert.equal(element.prop('data-testid'), testID)
|
||||
})
|
||||
})
|
||||
57
src/modules/createNativeComponent/index.js
Normal file
57
src/modules/createNativeComponent/index.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
|
||||
const roleComponents = {
|
||||
article: 'article',
|
||||
banner: 'header',
|
||||
button: 'button',
|
||||
complementary: 'aside',
|
||||
contentinfo: 'footer',
|
||||
form: 'form',
|
||||
heading: 'h1',
|
||||
link: 'a',
|
||||
list: 'ul',
|
||||
listitem: 'li',
|
||||
main: 'main',
|
||||
navigation: 'nav',
|
||||
region: 'section'
|
||||
}
|
||||
|
||||
const createNativeComponent = ({
|
||||
accessibilityLabel,
|
||||
accessibilityLiveRegion,
|
||||
accessibilityRole,
|
||||
accessible = true,
|
||||
component = 'div',
|
||||
testID,
|
||||
type,
|
||||
...other
|
||||
}) => {
|
||||
const Component = accessibilityRole && roleComponents[accessibilityRole] ? roleComponents[accessibilityRole] : component
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...other}
|
||||
{...StyleSheet.resolve(other)}
|
||||
aria-hidden={accessible ? null : true}
|
||||
aria-label={accessibilityLabel}
|
||||
aria-live={accessibilityLiveRegion}
|
||||
data-testid={testID}
|
||||
role={accessibilityRole}
|
||||
type={accessibilityRole === 'button' ? 'button' : type}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
createNativeComponent.propTypes = {
|
||||
accessibilityLabel: PropTypes.string,
|
||||
accessibilityLiveRegion: PropTypes.oneOf([ 'assertive', 'off', 'polite' ]),
|
||||
accessibilityRole: PropTypes.string,
|
||||
accessible: PropTypes.bool,
|
||||
component: PropTypes.oneOfType([ PropTypes.func, PropTypes.string ]),
|
||||
style: PropTypes.oneOfType([ PropTypes.array, PropTypes.object ]),
|
||||
testID: PropTypes.string,
|
||||
type: PropTypes.string
|
||||
}
|
||||
|
||||
module.exports = createNativeComponent
|
||||
3
src/modules/findNodeHandle/index.js
Normal file
3
src/modules/findNodeHandle/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
const findNodeHandle = ReactDOM.findDOMNode
|
||||
export default findNodeHandle
|
||||
8
src/modules/polyfills/Set.js
Normal file
8
src/modules/polyfills/Set.js
Normal file
@@ -0,0 +1,8 @@
|
||||
function SetPolyfill() { this._cache = [] }
|
||||
SetPolyfill.prototype.add = function (e) {
|
||||
if (this._cache.indexOf(e) === -1) { this._cache.push(e) }
|
||||
}
|
||||
SetPolyfill.prototype.forEach = function (cb) {
|
||||
this._cache.forEach(cb)
|
||||
}
|
||||
module.exports = SetPolyfill
|
||||
@@ -1,74 +0,0 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ReactTestUtils from 'react-addons-test-utils'
|
||||
|
||||
export const assertProps = {
|
||||
style: function (Component, props) {
|
||||
let shallow
|
||||
// default styles
|
||||
shallow = shallowRender(<Component {...props} />)
|
||||
assert.deepEqual(
|
||||
shallow.props.style,
|
||||
Component.defaultProps.style
|
||||
)
|
||||
// filtering of unsupported styles
|
||||
const styleToFilter = { unsupported: 'unsupported' }
|
||||
shallow = shallowRender(<Component {...props} style={styleToFilter} />)
|
||||
assert.deepEqual(
|
||||
shallow.props.style.unsupported,
|
||||
undefined,
|
||||
'unsupported "style" properties must not be transferred'
|
||||
)
|
||||
// merging of custom styles
|
||||
const styleToMerge = { margin: '10' }
|
||||
shallow = shallowRender(<Component {...props} style={styleToMerge} />)
|
||||
assert.deepEqual(
|
||||
shallow.props.style,
|
||||
{ ...Component.defaultProps.style, ...styleToMerge }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function render(element, container) {
|
||||
return container
|
||||
? ReactDOM.render(element, container)
|
||||
: ReactTestUtils.renderIntoDocument(element)
|
||||
}
|
||||
|
||||
export function renderAndInject(element) {
|
||||
const id = '_renderAndInject'
|
||||
let div = document.getElementById(id)
|
||||
|
||||
if (!div) {
|
||||
div = document.createElement('div')
|
||||
div.id = id
|
||||
document.body.appendChild(div)
|
||||
} else {
|
||||
div.innerHTML = ''
|
||||
}
|
||||
|
||||
const result = render(element, div)
|
||||
return ReactDOM.findDOMNode(result)
|
||||
}
|
||||
|
||||
export function renderToDOM(element, container) {
|
||||
const result = render(element, container)
|
||||
return ReactDOM.findDOMNode(result)
|
||||
}
|
||||
|
||||
export function shallowRender(component, context = {}) {
|
||||
const shallowRenderer = ReactTestUtils.createRenderer()
|
||||
shallowRenderer.render(component, context)
|
||||
return shallowRenderer.getRenderOutput()
|
||||
}
|
||||
|
||||
export function testIfDocumentFocused(message, fn) {
|
||||
if (document.hasFocus && document.hasFocus()) {
|
||||
test(message, fn)
|
||||
} else {
|
||||
test.skip(`${message} – WARNING: document is not focused`)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
var webpack = require('webpack')
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
|
||||
const DIST_DIRECTORY = './dist'
|
||||
|
||||
@@ -6,20 +7,20 @@ module.exports = {
|
||||
entry: {
|
||||
main: DIST_DIRECTORY
|
||||
},
|
||||
externals: [{
|
||||
'react': true,
|
||||
'react-dom': true,
|
||||
'react-dom/server': true
|
||||
}],
|
||||
output: {
|
||||
filename: 'react-native-web.js',
|
||||
library: 'ReactNativeWeb',
|
||||
filename: 'ReactNative.js',
|
||||
library: 'ReactNative',
|
||||
libraryTarget: 'umd',
|
||||
path: DIST_DIRECTORY
|
||||
},
|
||||
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: {
|
||||
|
||||
Reference in New Issue
Block a user