mirror of
https://github.com/zhigang1992/react-native-web.git
synced 2026-03-27 22:55:56 +08:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
640e41dc34 | ||
|
|
c609a6ff2b | ||
|
|
294d94d869 | ||
|
|
179d624917 | ||
|
|
61860b6d49 | ||
|
|
597fcc65e8 | ||
|
|
5e1e0ec8e5 | ||
|
|
0ac243038f | ||
|
|
c9d68fe93e | ||
|
|
77f72aa129 | ||
|
|
216885406f | ||
|
|
f15bf2664a | ||
|
|
79998e0acc | ||
|
|
44fc48f7a0 | ||
|
|
37f2d78f34 | ||
|
|
1dc769bfb1 | ||
|
|
4b3cb41107 | ||
|
|
ed2cbfd5d3 | ||
|
|
8c4b5b68c3 | ||
|
|
3564bbf840 | ||
|
|
297b2e5afb | ||
|
|
215697234e | ||
|
|
9efa7e94bd | ||
|
|
c44da41497 | ||
|
|
331c92fb3a | ||
|
|
26758e905c | ||
|
|
a15b15c55d | ||
|
|
f0202dbe61 | ||
|
|
d4d67dafc0 | ||
|
|
579bdeb8a5 | ||
|
|
7132a18440 | ||
|
|
18881b1edb | ||
|
|
4d1e7d8c0b | ||
|
|
a7158aeb6f | ||
|
|
03d413bca4 | ||
|
|
aef5efbad3 | ||
|
|
8fb8645723 | ||
|
|
d69406b4b1 | ||
|
|
2c2a96a183 | ||
|
|
b4a3053b5b |
7
.babelrc
7
.babelrc
@@ -1,10 +1,5 @@
|
|||||||
{
|
{
|
||||||
"presets": [
|
"presets": [
|
||||||
"es2015",
|
"react-native"
|
||||||
"stage-1",
|
|
||||||
"react"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"transform-decorators-legacy"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ styles defined in JavaScript into "Atomic CSS".
|
|||||||
To install in your app:
|
To install in your app:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm install --save react@0.15 react-native-web
|
npm install --save react react-native-web
|
||||||
```
|
```
|
||||||
|
|
||||||
Read the [Client and Server rendering](docs/guides/rendering.md) guide.
|
Read the [Client and Server rendering](docs/guides/rendering.md) guide.
|
||||||
@@ -102,7 +102,6 @@ Exported modules:
|
|||||||
* [`ActivityIndicator`](docs/components/ActivityIndicator.md)
|
* [`ActivityIndicator`](docs/components/ActivityIndicator.md)
|
||||||
* [`Image`](docs/components/Image.md)
|
* [`Image`](docs/components/Image.md)
|
||||||
* [`ListView`](docs/components/ListView.md)
|
* [`ListView`](docs/components/ListView.md)
|
||||||
* [`Portal`](docs/components/Portal.md)
|
|
||||||
* [`ScrollView`](docs/components/ScrollView.md)
|
* [`ScrollView`](docs/components/ScrollView.md)
|
||||||
* [`Text`](docs/components/Text.md)
|
* [`Text`](docs/components/Text.md)
|
||||||
* [`TextInput`](docs/components/TextInput.md)
|
* [`TextInput`](docs/components/TextInput.md)
|
||||||
|
|||||||
@@ -15,17 +15,52 @@ Each key of the object passed to `create` must define a style object.
|
|||||||
|
|
||||||
Flattens an array of styles into a single style object.
|
Flattens an array of styles into a single style object.
|
||||||
|
|
||||||
**renderToString**: function
|
**render**: function
|
||||||
|
|
||||||
Returns a string of CSS used to style the application.
|
Returns a React `<style>` element for use in server-side rendering.
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
|
**absoluteFill**: number
|
||||||
|
|
||||||
|
A very common pattern is to create overlays with position absolute and zero positioning,
|
||||||
|
so `absoluteFill` can be used for convenience and to reduce duplication of these repeated
|
||||||
|
styles.
|
||||||
|
|
||||||
|
```js
|
||||||
|
<View style={StyleSheet.absoluteFill} />
|
||||||
|
```
|
||||||
|
|
||||||
|
**absoluteFillObject**: object
|
||||||
|
|
||||||
|
Sometimes you may want `absoluteFill` but with a couple tweaks - `absoluteFillObject` can be
|
||||||
|
used to create a customized entry in a `StyleSheet`, e.g.:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
wrapper: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
top: 10
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
**hairlineWidth**: number
|
**hairlineWidth**: number
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text
|
||||||
|
children={'Title text'}
|
||||||
|
style={[
|
||||||
|
styles.title,
|
||||||
|
this.props.isActive && styles.activeTitle
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
@@ -41,29 +76,3 @@ const styles = StyleSheet.create({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
Use styles:
|
|
||||||
|
|
||||||
```js
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.title,
|
|
||||||
this.props.isActive && styles.activeTitle
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
```
|
|
||||||
|
|
||||||
Or:
|
|
||||||
|
|
||||||
```js
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
...styles.title,
|
|
||||||
...(this.props.isActive && styles.activeTitle)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ Invoked on load error with `{nativeEvent: {error}}`.
|
|||||||
|
|
||||||
**onLayout**: function
|
**onLayout**: function
|
||||||
|
|
||||||
TODO
|
Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width,
|
||||||
|
height } } }`, where `x` and `y` are the offsets from the parent node.
|
||||||
|
|
||||||
**onLoad**: function
|
**onLoad**: function
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ could be an http address or a base64 encoded image.
|
|||||||
|
|
||||||
**style**: style
|
**style**: style
|
||||||
|
|
||||||
+ ...[View#style](View.md)
|
+ ...[View#style](./View.md)
|
||||||
+ `resizeMode`
|
+ `resizeMode`
|
||||||
|
|
||||||
**testID**: string
|
**testID**: string
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ TODO
|
|||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
|
[...ScrollView props](./ScrollView.md)
|
||||||
|
|
||||||
**children**: any
|
**children**: any
|
||||||
|
|
||||||
Content to display over the image.
|
Content to display over the image.
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
# Portal
|
|
||||||
|
|
||||||
`Portal` is used to render modal content on top of everything else in the
|
|
||||||
application. It passes modal views all the way up to the root element created
|
|
||||||
by `AppRegistry.runApplication`.
|
|
||||||
|
|
||||||
There can only be one `Portal` instance rendered in an application, and this
|
|
||||||
instance is controlled by React Native for Web.
|
|
||||||
|
|
||||||
## Methods
|
|
||||||
|
|
||||||
static **allocateTag**()
|
|
||||||
|
|
||||||
Creates a new unique tag for the modal that your component is rendering. A
|
|
||||||
good place to allocate a tag is in `componentWillMount`. Returns a string. See
|
|
||||||
`showModal` and `closeModal`.
|
|
||||||
|
|
||||||
static **closeModal**(tag: string)
|
|
||||||
|
|
||||||
Remove a modal from the collection of modals to be rendered. The `tag` must
|
|
||||||
exactly match the tag previous passed to `showModal` to identify the React
|
|
||||||
component.
|
|
||||||
|
|
||||||
static **getOpenModals**()
|
|
||||||
|
|
||||||
Get an array of all the open modals, as identified by their tag string.
|
|
||||||
|
|
||||||
static **showModal**(tag: string, component: any)
|
|
||||||
|
|
||||||
Render a new modal. The `tag` must be unique as it is used to identify the
|
|
||||||
React component to render. This same tag can later be used in `closeModal`.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
```js
|
|
||||||
import React, { Component } from 'react'
|
|
||||||
import { Portal, Text, Touchable } from 'react-native'
|
|
||||||
|
|
||||||
export default class PortalExample extends Component {
|
|
||||||
componentWillMount() {
|
|
||||||
this._portalTag = Portal.allocateTag()
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Touchable onPress={this._handlePortalOpen.bind(this)}>
|
|
||||||
<Text>Open portal</Text>
|
|
||||||
</Touchable>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_handlePortalClose(e) {
|
|
||||||
Portal.closeModal(this._portalTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
_handlePortalOpen(e) {
|
|
||||||
Portal.showModal(this._portalTag, this._renderPortalContent())
|
|
||||||
}
|
|
||||||
|
|
||||||
_renderPortalContent() {
|
|
||||||
return (
|
|
||||||
<Touchable onPress={this._handlePortalClose.bind(this)}>
|
|
||||||
<Text>Close portal</Text>
|
|
||||||
</Touchable>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -29,8 +29,6 @@ Determines whether the keyboard gets dismissed in response to a scroll drag.
|
|||||||
|
|
||||||
**onContentSizeChange**: function
|
**onContentSizeChange**: function
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
Called when scrollable content view of the `ScrollView` changes. It's
|
Called when scrollable content view of the `ScrollView` changes. It's
|
||||||
implemented using the `onLayout` handler attached to the content container
|
implemented using the `onLayout` handler attached to the content container
|
||||||
which this `ScrollView` renders.
|
which this `ScrollView` renders.
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ Child content.
|
|||||||
|
|
||||||
Truncates the text with an ellipsis after this many lines. Currently only supports `1`.
|
Truncates the text with an ellipsis after this many lines. Currently only supports `1`.
|
||||||
|
|
||||||
|
**onLayout**: function
|
||||||
|
|
||||||
|
Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width,
|
||||||
|
height } } }`, where `x` and `y` are the offsets from the parent node.
|
||||||
|
|
||||||
**onPress**: function
|
**onPress**: function
|
||||||
|
|
||||||
This function is called on press.
|
This function is called on press.
|
||||||
|
|||||||
@@ -14,16 +14,11 @@ Unsupported React Native props:
|
|||||||
`enablesReturnKeyAutomatically` (ios),
|
`enablesReturnKeyAutomatically` (ios),
|
||||||
`returnKeyType` (ios),
|
`returnKeyType` (ios),
|
||||||
`selectionState` (ios),
|
`selectionState` (ios),
|
||||||
`textAlign` (android),
|
|
||||||
`textAlignVertical` (android),
|
|
||||||
`underlineColorAndroid` (android)
|
`underlineColorAndroid` (android)
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
(web) **accessibilityLabel**: string
|
[...View props](./View.md)
|
||||||
|
|
||||||
Defines the text label available to assistive technologies upon interaction
|
|
||||||
with the element. (This is implemented using `aria-label`.)
|
|
||||||
|
|
||||||
(web) **autoComplete**: bool = false
|
(web) **autoComplete**: bool = false
|
||||||
|
|
||||||
@@ -92,10 +87,6 @@ as an argument to the callback handler.
|
|||||||
|
|
||||||
Callback that is called when the text input is focused.
|
Callback that is called when the text input is focused.
|
||||||
|
|
||||||
**onLayout**: function
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
(web) **onSelectionChange**: function
|
(web) **onSelectionChange**: function
|
||||||
|
|
||||||
Callback that is called when the text input's selection changes. The following
|
Callback that is called when the text input's selection changes. The following
|
||||||
@@ -132,7 +123,7 @@ If `true`, all text will automatically be selected on focus.
|
|||||||
|
|
||||||
**style**: style
|
**style**: style
|
||||||
|
|
||||||
+ ...[Text#style](Text.md)
|
+ ...[Text#style](./Text.md)
|
||||||
+ `outline`
|
+ `outline`
|
||||||
|
|
||||||
**testID**: string
|
**testID**: string
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ several child components, wrap them in a View.
|
|||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
|
[...View props](./View.md)
|
||||||
|
|
||||||
**accessibilityLabel**: string
|
**accessibilityLabel**: string
|
||||||
|
|
||||||
Overrides the text that's read by the screen reader when the user interacts
|
Overrides the text that's read by the screen reader when the user interacts
|
||||||
@@ -22,6 +24,8 @@ Allows assistive technologies to present and support interaction with the view
|
|||||||
|
|
||||||
When `false`, the view is hidden from screenreaders.
|
When `false`, the view is hidden from screenreaders.
|
||||||
|
|
||||||
|
**children**: View
|
||||||
|
|
||||||
**delayLongPress**: number
|
**delayLongPress**: number
|
||||||
|
|
||||||
Delay in ms, from `onPressIn`, before `onLongPress` is called.
|
Delay in ms, from `onPressIn`, before `onLongPress` is called.
|
||||||
@@ -47,9 +51,8 @@ always takes precedence if a touch hits two overlapping views.
|
|||||||
|
|
||||||
**onLayout**: function
|
**onLayout**: function
|
||||||
|
|
||||||
Invoked on mount and layout changes with.
|
Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width,
|
||||||
|
height } } }`, where `x` and `y` are the offsets from the parent node.
|
||||||
`{nativeEvent: {layout: {x, y, width, height}}}`
|
|
||||||
|
|
||||||
**onLongPress**: function
|
**onLongPress**: function
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ implemented using `aria-hidden`.)
|
|||||||
|
|
||||||
**onLayout**: function
|
**onLayout**: function
|
||||||
|
|
||||||
(TODO)
|
Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width,
|
||||||
|
height } } }`, where `x` and `y` are the offsets from the parent node.
|
||||||
|
|
||||||
**onMoveShouldSetResponder**: function
|
**onMoveShouldSetResponder**: function
|
||||||
|
|
||||||
|
|||||||
@@ -21,80 +21,38 @@ module.exports = {
|
|||||||
Rendering without using the `AppRegistry`:
|
Rendering without using the `AppRegistry`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
import React from 'react'
|
||||||
import ReactNative from 'react-native'
|
import ReactNative from 'react-native'
|
||||||
|
|
||||||
|
// component that renders the app
|
||||||
|
const AppHeaderContainer = (props) => { /* ... */ }
|
||||||
|
|
||||||
// DOM render
|
// DOM render
|
||||||
ReactNative.render(<div />, document.getElementById('react-app'))
|
ReactNative.render(<AppHeaderContainer />, document.getElementById('react-app-header'))
|
||||||
|
|
||||||
// Server render
|
// Server render
|
||||||
ReactNative.renderToString(<div />)
|
ReactNative.renderToString(<AppHeaderContainer />)
|
||||||
ReactNative.renderToStaticMarkup(<div />)
|
ReactNative.renderToStaticMarkup(<AppHeaderContainer />)
|
||||||
```
|
```
|
||||||
|
|
||||||
Rendering using the `AppRegistry`:
|
Rendering using the `AppRegistry`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// App.js
|
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import ReactNative, { AppRegistry } from 'react-native'
|
||||||
|
|
||||||
// component that renders the app
|
// component that renders the app
|
||||||
const AppContainer = (props) => { /* ... */ }
|
const AppContainer = (props) => { /* ... */ }
|
||||||
export default AppContainer
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
// register the app
|
||||||
// client.js
|
AppRegistry.registerComponent('App', () => AppContainer)
|
||||||
|
|
||||||
import App from './App'
|
// DOM render
|
||||||
import { AppRegistry } from 'react-native'
|
|
||||||
|
|
||||||
// registers the app
|
|
||||||
AppRegistry.registerComponent('App', () => App)
|
|
||||||
|
|
||||||
// mounts and runs the app within the `rootTag` DOM node
|
|
||||||
AppRegistry.runApplication('App', {
|
AppRegistry.runApplication('App', {
|
||||||
initialProps: {},
|
initialProps: {},
|
||||||
rootTag: document.getElementById('react-app')
|
rootTag: document.getElementById('react-app')
|
||||||
})
|
})
|
||||||
```
|
|
||||||
|
// prerender the app
|
||||||
React Native for Web extends `AppRegistry` to provide support for server-side
|
const { html, styleElement } = AppRegistry.prerenderApplication('App', { initialProps })
|
||||||
rendering.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// AppShell.js
|
|
||||||
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
const AppShell = (html, styleElement) => (
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charSet="utf-8" />
|
|
||||||
<meta content="initial-scale=1,width=device-width" name="viewport" />
|
|
||||||
{styleElement}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="react-app" dangerouslySetInnerHTML={{ __html: html }} />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
export default AppShell
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
// server.js
|
|
||||||
|
|
||||||
import App from './App'
|
|
||||||
import AppShell from './AppShell'
|
|
||||||
import ReactNative, { AppRegistry } from 'react-native'
|
|
||||||
|
|
||||||
// registers the app
|
|
||||||
AppRegistry.registerComponent('App', () => App)
|
|
||||||
|
|
||||||
// prerenders the app
|
|
||||||
const { html, style, styleElement } = AppRegistry.prerenderApplication('App', { initialProps })
|
|
||||||
|
|
||||||
// renders the full-page markup
|
|
||||||
const renderedApplicationHTML = ReactNative.renderToStaticMarkup(<AppShell html={html} styleElement={styleElement} />)
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export default class App extends React.Component {
|
|||||||
|
|
||||||
<Heading size='large'>Image</Heading>
|
<Heading size='large'>Image</Heading>
|
||||||
<Image
|
<Image
|
||||||
|
onLayout={(e) => { console.log(e.nativeEvent.layout) }}
|
||||||
accessibilityLabel='accessible image'
|
accessibilityLabel='accessible image'
|
||||||
children={<Text>Inner content</Text>}
|
children={<Text>Inner content</Text>}
|
||||||
defaultSource={{
|
defaultSource={{
|
||||||
@@ -57,6 +58,7 @@ export default class App extends React.Component {
|
|||||||
<Heading size='large'>Text</Heading>
|
<Heading size='large'>Text</Heading>
|
||||||
<Text
|
<Text
|
||||||
onPress={(e) => { console.log('Text.onPress', e) }}
|
onPress={(e) => { console.log('Text.onPress', e) }}
|
||||||
|
onLayout={(e) => { console.log(e.nativeEvent.layout) }}
|
||||||
testID={'Example.text'}
|
testID={'Example.text'}
|
||||||
>
|
>
|
||||||
PRESS ME.
|
PRESS ME.
|
||||||
|
|||||||
@@ -20,24 +20,24 @@ export default class GridView extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const { alley, children, gutter, style, ...other } = this.props
|
const { alley, children, gutter, style, ...other } = this.props
|
||||||
|
|
||||||
const rootStyle = {
|
const rootStyle = [
|
||||||
...style,
|
style,
|
||||||
...styles.root
|
styles.root
|
||||||
}
|
]
|
||||||
|
|
||||||
const contentContainerStyle = {
|
const contentContainerStyle = [
|
||||||
...styles.contentContainer,
|
styles.contentContainer,
|
||||||
marginHorizontal: `calc(-0.5 * ${alley})`,
|
{ marginHorizontal: `calc(-0.5 * ${alley})` },
|
||||||
paddingHorizontal: `${gutter}`
|
{ paddingHorizontal: `${gutter}` }
|
||||||
}
|
]
|
||||||
|
|
||||||
const newChildren = React.Children.map(children, (child) => {
|
const newChildren = React.Children.map(children, (child) => {
|
||||||
return child && React.cloneElement(child, {
|
return child && React.cloneElement(child, {
|
||||||
style: {
|
style: [
|
||||||
...child.props.style,
|
child.props.style,
|
||||||
...styles.column,
|
styles.column,
|
||||||
marginHorizontal: `calc(0.5 * ${alley})`
|
{ marginHorizontal: `calc(0.5 * ${alley})` }
|
||||||
}
|
]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const Heading = ({ children, size = 'normal' }) => (
|
|||||||
<Text
|
<Text
|
||||||
accessibilityRole='heading'
|
accessibilityRole='heading'
|
||||||
children={children}
|
children={children}
|
||||||
style={{ ...styles.root, ...sizeStyles[size] }}
|
style={[ styles.root, sizeStyles[size] ]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { AppRegistry } from 'react-native'
|
import React from 'react'
|
||||||
|
import ReactNative, { AppRegistry } from 'react-native'
|
||||||
import App from './components/App'
|
import App from './components/App'
|
||||||
import Game2048 from './2048/Game2048'
|
import Game2048 from './2048/Game2048'
|
||||||
import TicTacToeApp from './TicTacToe/TicTacToe'
|
import TicTacToeApp from './TicTacToe/TicTacToe'
|
||||||
|
|
||||||
|
const rootTag = document.getElementById('react-root')
|
||||||
AppRegistry.registerComponent('App', () => App)
|
AppRegistry.registerComponent('App', () => App)
|
||||||
|
AppRegistry.runApplication('App', { rootTag })
|
||||||
AppRegistry.runApplication('App', {
|
// ReactNative.render(<App />, rootTag)
|
||||||
rootTag: document.getElementById('react-root')
|
|
||||||
})
|
|
||||||
|
|||||||
21
package.json
21
package.json
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "react-native-web",
|
"name": "react-native-web",
|
||||||
"version": "0.0.26",
|
"version": "0.0.35",
|
||||||
"description": "React Native for Web",
|
"description": "React Native for Web",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "del ./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",
|
"build:umd": "webpack --config webpack.config.js --sort-assets-by --progress",
|
||||||
"examples": "webpack-dev-server --config examples/webpack.config.js --inline --hot --colors --quiet",
|
"examples": "webpack-dev-server --config examples/webpack.config.js --inline --hot --colors --quiet",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"animated": "^0.1.3",
|
"animated": "^0.1.3",
|
||||||
"babel-runtime": "^6.9.2",
|
"babel-runtime": "^6.9.2",
|
||||||
"fbjs": "^0.8.1",
|
"fbjs": "^0.8.1",
|
||||||
"inline-style-prefix-all": "^2.0.2",
|
"inline-style-prefixer": "^2.0.0",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"react-dom": "^15.1.0",
|
"react-dom": "^15.1.0",
|
||||||
"react-textarea-autosize": "^4.0.2",
|
"react-textarea-autosize": "^4.0.2",
|
||||||
@@ -27,13 +27,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-cli": "^6.10.1",
|
"babel-cli": "^6.10.1",
|
||||||
"babel-core": "^6.9.1",
|
"babel-core": "^6.10.4",
|
||||||
"babel-eslint": "^6.0.4",
|
"babel-eslint": "^6.1.0",
|
||||||
"babel-loader": "^6.2.4",
|
"babel-loader": "^6.2.4",
|
||||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
"babel-preset-react-native": "^1.9.0",
|
||||||
"babel-preset-es2015": "^6.9.0",
|
|
||||||
"babel-preset-react": "^6.5.0",
|
|
||||||
"babel-preset-stage-1": "^6.5.0",
|
|
||||||
"del-cli": "^0.2.0",
|
"del-cli": "^0.2.0",
|
||||||
"enzyme": "^2.3.0",
|
"enzyme": "^2.3.0",
|
||||||
"eslint": "^2.12.0",
|
"eslint": "^2.12.0",
|
||||||
@@ -46,14 +43,14 @@
|
|||||||
"karma-browserstack-launcher": "^1.0.1",
|
"karma-browserstack-launcher": "^1.0.1",
|
||||||
"karma-chrome-launcher": "^1.0.1",
|
"karma-chrome-launcher": "^1.0.1",
|
||||||
"karma-firefox-launcher": "^1.0.0",
|
"karma-firefox-launcher": "^1.0.0",
|
||||||
"karma-mocha": "^1.0.1",
|
"karma-mocha": "^1.1.1",
|
||||||
"karma-mocha-reporter": "^2.0.4",
|
"karma-mocha-reporter": "^2.0.4",
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"karma-sourcemap-loader": "^0.3.7",
|
||||||
"karma-webpack": "^1.7.0",
|
"karma-webpack": "^1.7.0",
|
||||||
"mocha": "^2.5.3",
|
"mocha": "^2.5.3",
|
||||||
"node-libs-browser": "^0.5.3",
|
"node-libs-browser": "^0.5.3",
|
||||||
"react": "^15.1.0",
|
"react": "^15.2.0",
|
||||||
"react-addons-test-utils": "^15.1.0",
|
"react-addons-test-utils": "^15.2.0",
|
||||||
"webpack": "^1.13.1",
|
"webpack": "^1.13.1",
|
||||||
"webpack-dev-server": "^1.14.1"
|
"webpack-dev-server": "^1.14.1"
|
||||||
},
|
},
|
||||||
|
|||||||
14
src/apis/Animated/index.js
Normal file
14
src/apis/Animated/index.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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 = {
|
||||||
|
...Animated,
|
||||||
|
Image: Animated.createAnimatedComponent(Image),
|
||||||
|
Text: Animated.createAnimatedComponent(Text),
|
||||||
|
View: Animated.createAnimatedComponent(View)
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import Portal from '../../components/Portal'
|
|
||||||
import React, { Component, PropTypes } from 'react'
|
import React, { Component, PropTypes } from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import StyleSheet from '../StyleSheet'
|
import StyleSheet from '../StyleSheet'
|
||||||
import View from '../../components/View'
|
import View from '../../components/View'
|
||||||
|
|
||||||
@@ -16,25 +14,15 @@ class ReactNativeApp extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.appContainer}>
|
<View style={styles.appContainer}>
|
||||||
<RootComponent {...initialProps} ref={this._createRootRef} rootTag={rootTag} />
|
<RootComponent {...initialProps} rootTag={rootTag} />
|
||||||
<Portal onModalVisibilityChanged={this._handleModalVisibilityChange} />
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
_createRootRef = (component) => {
|
|
||||||
this._root = component
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleModalVisibilityChange = (modalVisible) => {
|
|
||||||
ReactDOM.findDOMNode(this._root).setAttribute('aria-hidden', `${modalVisible}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
/**
|
/**
|
||||||
* Ensure that the application covers the whole screen. This prevents the
|
* Ensure that the application covers the whole screen.
|
||||||
* Portal content from being clipped.
|
|
||||||
*/
|
*/
|
||||||
appContainer: {
|
appContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
/* eslint-env mocha */
|
/* eslint-env mocha */
|
||||||
|
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import React from 'react'
|
|
||||||
import { elementId } from '../../StyleSheet'
|
|
||||||
import { prerenderApplication } from '../renderApplication'
|
import { prerenderApplication } from '../renderApplication'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
const component = () => <div />
|
const component = () => <div />
|
||||||
|
|
||||||
suite('apis/AppRegistry/renderApplication', () => {
|
suite('apis/AppRegistry/renderApplication', () => {
|
||||||
test('prerenderApplication', () => {
|
test('prerenderApplication', () => {
|
||||||
const { html, style, styleElement } = prerenderApplication(component, {})
|
const { html, styleElement } = prerenderApplication(component, {})
|
||||||
|
|
||||||
assert.ok(html.indexOf('<div ') > -1)
|
assert.ok(html.indexOf('<div ') > -1)
|
||||||
assert.ok(typeof style === 'string')
|
|
||||||
assert.equal(styleElement.type, 'style')
|
assert.equal(styleElement.type, 'style')
|
||||||
assert.equal(styleElement.props.id, elementId)
|
|
||||||
assert.equal(styleElement.props.dangerouslySetInnerHTML.__html, style)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,17 +13,9 @@ import ReactDOMServer from 'react-dom/server'
|
|||||||
import ReactNativeApp from './ReactNativeApp'
|
import ReactNativeApp from './ReactNativeApp'
|
||||||
import StyleSheet from '../../apis/StyleSheet'
|
import StyleSheet from '../../apis/StyleSheet'
|
||||||
|
|
||||||
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) {
|
export default function renderApplication(RootComponent: Component, initialProps: Object, rootTag: any) {
|
||||||
invariant(rootTag, 'Expect to have a valid rootTag, instead got ', rootTag)
|
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', styleAsTagString(renderStyleSheetToString())) }
|
|
||||||
|
|
||||||
const component = (
|
const component = (
|
||||||
<ReactNativeApp
|
<ReactNativeApp
|
||||||
initialProps={initialProps}
|
initialProps={initialProps}
|
||||||
@@ -42,7 +34,6 @@ export function prerenderApplication(RootComponent: Component, initialProps: Obj
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
const html = ReactDOMServer.renderToString(component)
|
const html = ReactDOMServer.renderToString(component)
|
||||||
const style = renderStyleSheetToString()
|
const styleElement = StyleSheet.render()
|
||||||
const styleElement = styleAsElement(style)
|
return { html, styleElement }
|
||||||
return { html, style, styleElement }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import normalizeNativeEvent from './normalizeNativeEvent';
|
import normalizeNativeEvent from '../../modules/normalizeNativeEvent';
|
||||||
var TouchHistoryMath = require('./TouchHistoryMath');
|
var TouchHistoryMath = require('./TouchHistoryMath');
|
||||||
|
|
||||||
var currentCentroidXOfTouchesChangedAfter =
|
var currentCentroidXOfTouchesChangedAfter =
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2016-present, Nicolas Gallagher.
|
|
||||||
* Copyright (c) 2015-present, Facebook, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 {
|
|
||||||
/* 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 = normalizeStyle(style)
|
|
||||||
|
|
||||||
Object.keys(normalizedStyle).forEach((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(styleSheetObject, canUseCSS = false) {
|
|
||||||
const classList = []
|
|
||||||
const normalizedStyle = normalizeStyle(styleSheetObject)
|
|
||||||
let style = {}
|
|
||||||
|
|
||||||
for (const prop in normalizedStyle) {
|
|
||||||
const value = normalizedStyle[prop]
|
|
||||||
const cacheKey = getCacheKey(prop, value)
|
|
||||||
let selector = stylesCache[cacheKey] && stylesCache[cacheKey].id || predefinedClassNames[cacheKey]
|
|
||||||
|
|
||||||
if (selector && canUseCSS) {
|
|
||||||
classList.push(selector)
|
|
||||||
} else {
|
|
||||||
style[prop] = normalizedStyle[prop]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: classList.join(' '),
|
|
||||||
style: finalStyle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = StyleSheetRegistry
|
|
||||||
@@ -10,7 +10,7 @@ import { PropTypes } from 'react'
|
|||||||
import ImageStylePropTypes from '../../components/Image/ImageStylePropTypes'
|
import ImageStylePropTypes from '../../components/Image/ImageStylePropTypes'
|
||||||
import TextStylePropTypes from '../../components/Text/TextStylePropTypes'
|
import TextStylePropTypes from '../../components/Text/TextStylePropTypes'
|
||||||
import ViewStylePropTypes from '../../components/View/ViewStylePropTypes'
|
import ViewStylePropTypes from '../../components/View/ViewStylePropTypes'
|
||||||
import invariant from 'fbjs/lib/invariant'
|
import warning from 'fbjs/lib/warning'
|
||||||
|
|
||||||
class StyleSheetValidation {
|
class StyleSheetValidation {
|
||||||
static validateStyleProp(prop, style, caller) {
|
static validateStyleProp(prop, style, caller) {
|
||||||
@@ -19,10 +19,11 @@ class StyleSheetValidation {
|
|||||||
const message1 = `"${prop}" is not a valid style property.`
|
const message1 = `"${prop}" is not a valid style property.`
|
||||||
const message2 = '\nValid style props: ' + JSON.stringify(Object.keys(allStylePropTypes).sort(), null, ' ')
|
const message2 = '\nValid style props: ' + JSON.stringify(Object.keys(allStylePropTypes).sort(), null, ' ')
|
||||||
styleError(message1, style, caller, message2)
|
styleError(message1, style, caller, message2)
|
||||||
}
|
} else {
|
||||||
const error = allStylePropTypes[prop](style, prop, caller, 'prop')
|
const error = allStylePropTypes[prop](style, prop, caller, 'prop')
|
||||||
if (error) {
|
if (error) {
|
||||||
styleError(error.message, style, caller)
|
styleError(error.message, style, caller)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,7 @@ class StyleSheetValidation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styleError = (message1, style, caller, message2) => {
|
const styleError = (message1, style, caller, message2) => {
|
||||||
invariant(
|
warning(
|
||||||
false,
|
false,
|
||||||
message1 + '\n' + (caller || '<<unknown>>') + ': ' +
|
message1 + '\n' + (caller || '<<unknown>>') + ': ' +
|
||||||
JSON.stringify(style, null, ' ') + (message2 || '')
|
JSON.stringify(style, null, ' ') + (message2 || '')
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2015-present, Facebook, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { PropTypes } from 'react'
|
|
||||||
|
|
||||||
const ArrayOfNumberPropType = PropTypes.arrayOf(PropTypes.number)
|
|
||||||
const numberOrString = PropTypes.oneOfType([ PropTypes.number, PropTypes.string ])
|
|
||||||
|
|
||||||
const TransformMatrixPropType = function (
|
|
||||||
props : Object,
|
|
||||||
propName : string,
|
|
||||||
componentName : string
|
|
||||||
) : ?Error {
|
|
||||||
if (props.transform && props.transformMatrix) {
|
|
||||||
return new Error(
|
|
||||||
'transformMatrix and transform styles cannot be used on the same ' +
|
|
||||||
'component'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return ArrayOfNumberPropType(props, propName, componentName)
|
|
||||||
}
|
|
||||||
|
|
||||||
const TransformPropTypes = {
|
|
||||||
transform: PropTypes.arrayOf(
|
|
||||||
PropTypes.oneOfType([
|
|
||||||
PropTypes.shape({ perspective: numberOrString }),
|
|
||||||
PropTypes.shape({ rotate: numberOrString }),
|
|
||||||
PropTypes.shape({ rotateX: numberOrString }),
|
|
||||||
PropTypes.shape({ rotateY: numberOrString }),
|
|
||||||
PropTypes.shape({ rotateZ: numberOrString }),
|
|
||||||
PropTypes.shape({ scale: numberOrString }),
|
|
||||||
PropTypes.shape({ scaleX: numberOrString }),
|
|
||||||
PropTypes.shape({ scaleY: numberOrString }),
|
|
||||||
PropTypes.shape({ skewX: numberOrString }),
|
|
||||||
PropTypes.shape({ skewY: numberOrString }),
|
|
||||||
PropTypes.shape({ translateX: numberOrString }),
|
|
||||||
PropTypes.shape({ translateY: numberOrString }),
|
|
||||||
PropTypes.shape({ translateZ: numberOrString }),
|
|
||||||
PropTypes.shape({ translate3d: PropTypes.string })
|
|
||||||
])
|
|
||||||
),
|
|
||||||
transformMatrix: TransformMatrixPropType
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = TransformPropTypes
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/* 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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
13
src/apis/StyleSheet/__tests__/createReactStyleObject-test.js
Normal file
13
src/apis/StyleSheet/__tests__/createReactStyleObject-test.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/* eslint-env mocha */
|
||||||
|
|
||||||
|
import assert from 'assert'
|
||||||
|
import createReactStyleObject from '../createReactStyleObject'
|
||||||
|
|
||||||
|
suite('apis/StyleSheet/createReactStyleObject', () => {
|
||||||
|
test('converts ReactNative style to ReactDOM style', () => {
|
||||||
|
const reactNativeStyle = { display: 'flex', marginVertical: 0, opacity: 0 }
|
||||||
|
const expectedStyle = { display: 'flex', marginTop: '0px', marginBottom: '0px', opacity: 0 }
|
||||||
|
|
||||||
|
assert.deepEqual(createReactStyleObject(reactNativeStyle), expectedStyle)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -4,20 +4,29 @@ import assert from 'assert'
|
|||||||
import expandStyle from '../expandStyle'
|
import expandStyle from '../expandStyle'
|
||||||
|
|
||||||
suite('apis/StyleSheet/expandStyle', () => {
|
suite('apis/StyleSheet/expandStyle', () => {
|
||||||
test('style resolution', () => {
|
test('shortform -> longform', () => {
|
||||||
const initial = {
|
const initial = {
|
||||||
borderTopWidth: 1,
|
borderStyle: 'solid',
|
||||||
borderWidth: 2,
|
boxSizing: 'border-box',
|
||||||
|
borderBottomColor: 'white',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderWidth: 0,
|
||||||
marginTop: 50,
|
marginTop: 50,
|
||||||
marginVertical: 25,
|
marginVertical: 25,
|
||||||
margin: 10
|
margin: 10
|
||||||
}
|
}
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
borderTopWidth: '1px',
|
borderBottomStyle: 'solid',
|
||||||
borderLeftWidth: '2px',
|
borderLeftStyle: 'solid',
|
||||||
borderRightWidth: '2px',
|
borderRightStyle: 'solid',
|
||||||
borderBottomWidth: '2px',
|
boxSizing: 'border-box',
|
||||||
|
borderBottomColor: 'white',
|
||||||
|
borderTopStyle: 'solid',
|
||||||
|
borderTopWidth: '0px',
|
||||||
|
borderLeftWidth: '0px',
|
||||||
|
borderRightWidth: '0px',
|
||||||
|
borderBottomWidth: '1px',
|
||||||
marginTop: '50px',
|
marginTop: '50px',
|
||||||
marginBottom: '25px',
|
marginBottom: '25px',
|
||||||
marginLeft: '10px',
|
marginLeft: '10px',
|
||||||
@@ -27,6 +36,18 @@ suite('apis/StyleSheet/expandStyle', () => {
|
|||||||
assert.deepEqual(expandStyle(initial), expected)
|
assert.deepEqual(expandStyle(initial), expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('textAlignVertical', () => {
|
||||||
|
const initial = {
|
||||||
|
textAlignVertical: 'center'
|
||||||
|
}
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.deepEqual(expandStyle(initial), expected)
|
||||||
|
})
|
||||||
|
|
||||||
test('flex', () => {
|
test('flex', () => {
|
||||||
const value = 10
|
const value = 10
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
/* eslint-env mocha */
|
|
||||||
|
|
||||||
import assert from 'assert'
|
|
||||||
import hyphenate from '../hyphenate'
|
|
||||||
|
|
||||||
suite('apis/StyleSheet/hyphenate', () => {
|
|
||||||
test('style property', () => {
|
|
||||||
assert.equal(hyphenate('alignItems'), 'align-items')
|
|
||||||
assert.equal(hyphenate('color'), 'color')
|
|
||||||
})
|
|
||||||
test('vendor prefixed style property', () => {
|
|
||||||
assert.equal(hyphenate('MozTransition'), '-moz-transition')
|
|
||||||
assert.equal(hyphenate('msTransition'), '-ms-transition')
|
|
||||||
assert.equal(hyphenate('WebkitTransition'), '-webkit-transition')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,60 +1,70 @@
|
|||||||
/* eslint-env mocha */
|
/* eslint-env mocha */
|
||||||
|
|
||||||
import { resetCSS, predefinedCSS } from '../predefs'
|
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
|
import { defaultStyles } from '../predefs'
|
||||||
|
import isPlainObject from 'lodash/isPlainObject'
|
||||||
import StyleSheet from '..'
|
import StyleSheet from '..'
|
||||||
|
|
||||||
const styles = { root: { opacity: 1 } }
|
|
||||||
|
|
||||||
suite('apis/StyleSheet', () => {
|
suite('apis/StyleSheet', () => {
|
||||||
setup(() => {
|
setup(() => {
|
||||||
StyleSheet._destroy()
|
StyleSheet._reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('absoluteFill', () => {
|
||||||
|
assert(Number.isInteger(StyleSheet.absoluteFill) === true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('absoluteFillObject', () => {
|
||||||
|
assert.ok(isPlainObject(StyleSheet.absoluteFillObject) === true)
|
||||||
})
|
})
|
||||||
|
|
||||||
suite('create', () => {
|
suite('create', () => {
|
||||||
test('returns styles object', () => {
|
test('replaces styles with numbers', () => {
|
||||||
assert.equal(StyleSheet.create(styles), styles)
|
const style = StyleSheet.create({ root: { opacity: 1 } })
|
||||||
|
assert(Number.isInteger(style.root) === true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('updates already-rendered style sheet', () => {
|
test('renders a style sheet in the browser', () => {
|
||||||
// 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' } })
|
StyleSheet.create({ root: { color: 'red' } })
|
||||||
assert.equal(
|
assert.equal(
|
||||||
document.getElementById(StyleSheet.elementId).textContent,
|
document.getElementById('__react-native-style').textContent,
|
||||||
`${resetCSS}\n${predefinedCSS}\n` +
|
defaultStyles
|
||||||
`/* 2 unique declarations */\n` +
|
|
||||||
`.__style1{opacity:1;}\n` +
|
|
||||||
'.__style2{color:red;}'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// teardown
|
|
||||||
document.body.removeChild(div)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('renderToString', () => {
|
test('flatten', () => {
|
||||||
StyleSheet.create(styles)
|
assert(typeof StyleSheet.flatten === 'function')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hairlineWidth', () => {
|
||||||
|
assert(Number.isInteger(StyleSheet.hairlineWidth) === true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('render', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
StyleSheet.renderToString(),
|
StyleSheet.render().props.dangerouslySetInnerHTML.__html,
|
||||||
`${resetCSS}\n${predefinedCSS}\n` +
|
defaultStyles
|
||||||
`/* 1 unique declarations */\n` +
|
|
||||||
'.__style1{opacity:1;}'
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('resolve', () => {
|
test('resolve', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
StyleSheet.resolve({ className: 'test', style: styles.root }),
|
StyleSheet.resolve({
|
||||||
{
|
|
||||||
className: 'test',
|
className: 'test',
|
||||||
style: { opacity: 1 }
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
opacity: 1,
|
||||||
|
pointerEvents: 'box-none'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
className: 'test __style_df __style_pebn',
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
opacity: 1,
|
||||||
|
pointerEvents: 'box-none'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ suite('apis/StyleSheet/normalizeValue', () => {
|
|||||||
})
|
})
|
||||||
test('ignores unitless property values', () => {
|
test('ignores unitless property values', () => {
|
||||||
assert.deepEqual(normalizeValue('flexGrow', 1), 1)
|
assert.deepEqual(normalizeValue('flexGrow', 1), 1)
|
||||||
|
assert.deepEqual(normalizeValue('scale', 2), 2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ suite('apis/StyleSheet/processTransform', () => {
|
|||||||
const style = {
|
const style = {
|
||||||
transform: [
|
transform: [
|
||||||
{ scaleX: 20 },
|
{ scaleX: 20 },
|
||||||
|
{ translateX: 20 },
|
||||||
{ rotate: '20deg' }
|
{ rotate: '20deg' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
processTransform(style),
|
processTransform(style),
|
||||||
{ transform: 'scaleX(20) rotate(20deg)' }
|
{ transform: 'scaleX(20) translateX(20px) rotate(20deg)' }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
22
src/apis/StyleSheet/createReactStyleObject.js
Normal file
22
src/apis/StyleSheet/createReactStyleObject.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import expandStyle from './expandStyle'
|
||||||
|
import flattenStyle from '../../modules/flattenStyle'
|
||||||
|
import prefixAll from 'inline-style-prefixer/static'
|
||||||
|
import processTransform from './processTransform'
|
||||||
|
|
||||||
|
const addVendorPrefixes = (style) => {
|
||||||
|
let prefixedStyles = prefixAll(style)
|
||||||
|
// React@15 removed undocumented support for fallback values in
|
||||||
|
// inline-styles. Revert array values to the standard CSS value
|
||||||
|
for (const prop in prefixedStyles) {
|
||||||
|
const value = prefixedStyles[prop]
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
prefixedStyles[prop] = value[value.length - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prefixedStyles
|
||||||
|
}
|
||||||
|
|
||||||
|
const _createReactDOMStyleObject = (reactNativeStyle) => processTransform(expandStyle(flattenStyle(reactNativeStyle)))
|
||||||
|
const createReactDOMStyleObject = (reactNativeStyle) => addVendorPrefixes(_createReactDOMStyleObject(reactNativeStyle))
|
||||||
|
|
||||||
|
module.exports = createReactDOMStyleObject
|
||||||
@@ -1,6 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* The browser implements the CSS cascade, where the order of properties is a
|
||||||
|
* factor in determining which styles to paint. React Native is different in
|
||||||
|
* giving precedence to the more specific styles. For example, the value of
|
||||||
|
* `paddingTop` takes precedence over that of `padding`.
|
||||||
|
*
|
||||||
|
* This module creates mutally exclusive style declarations by expanding all of
|
||||||
|
* React Native's supported shortform properties (e.g. `padding`) to their
|
||||||
|
* longfrom equivalents.
|
||||||
|
*/
|
||||||
|
|
||||||
import normalizeValue from './normalizeValue'
|
import normalizeValue from './normalizeValue'
|
||||||
|
|
||||||
const styleShortHands = {
|
const emptyObject = {}
|
||||||
|
const styleShortFormProperties = {
|
||||||
borderColor: [ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor' ],
|
borderColor: [ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor' ],
|
||||||
borderRadius: [ 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius' ],
|
borderRadius: [ 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius' ],
|
||||||
borderStyle: [ 'borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle' ],
|
borderStyle: [ 'borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle' ],
|
||||||
@@ -16,50 +28,46 @@ const styleShortHands = {
|
|||||||
writingDirection: [ 'direction' ]
|
writingDirection: [ 'direction' ]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const alphaSort = (arr) => arr.sort((a, b) => {
|
||||||
* Alpha-sort properties, apart from shorthands – they must appear before the
|
if (a < b) { return -1 }
|
||||||
* longhand properties that they expand into. This lets more specific styles
|
if (a > b) { return 1 }
|
||||||
* override less specific styles, whatever the order in which they were
|
return 0
|
||||||
* 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 createStyleReducer = (originalStyle) => {
|
||||||
* Expand the shorthand properties to isolate every declaration from the others.
|
const originalStyleProps = Object.keys(originalStyle)
|
||||||
*/
|
|
||||||
const expandStyle = (style) => {
|
|
||||||
const propsArray = Object.keys(style)
|
|
||||||
const sortedProps = sortProps(propsArray)
|
|
||||||
|
|
||||||
return sortedProps.reduce((resolvedStyle, key) => {
|
return (style, prop) => {
|
||||||
const expandedProps = styleShortHands[key]
|
const value = normalizeValue(prop, originalStyle[prop])
|
||||||
const value = normalizeValue(key, style[key])
|
const longFormProperties = styleShortFormProperties[prop]
|
||||||
|
|
||||||
// React Native treats `flex:1` like `flex:1 1 auto`
|
// React Native treats `flex:1` like `flex:1 1 auto`
|
||||||
if (key === 'flex') {
|
if (prop === 'flex') {
|
||||||
resolvedStyle.flexGrow = value
|
style.flexGrow = value
|
||||||
resolvedStyle.flexShrink = 1
|
style.flexShrink = 1
|
||||||
resolvedStyle.flexBasis = 'auto'
|
style.flexBasis = 'auto'
|
||||||
} else if (key === 'textAlignVertical') {
|
// React Native accepts 'center' as a value
|
||||||
resolvedStyle.verticalAlign = (value === 'center' ? 'middle' : value)
|
} else if (prop === 'textAlignVertical') {
|
||||||
} else if (expandedProps) {
|
style.verticalAlign = (value === 'center' ? 'middle' : value)
|
||||||
expandedProps.forEach((prop, i) => {
|
} else if (longFormProperties) {
|
||||||
resolvedStyle[expandedProps[i]] = value
|
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 {
|
} else {
|
||||||
resolvedStyle[key] = value
|
style[prop] = value
|
||||||
}
|
}
|
||||||
return resolvedStyle
|
return style
|
||||||
}, {})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandStyle = (style = emptyObject) => {
|
||||||
|
const sortedStyleProps = alphaSort(Object.keys(style))
|
||||||
|
const styleReducer = createStyleReducer(style)
|
||||||
|
return sortedStyleProps.reduce(styleReducer, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = expandStyle
|
module.exports = expandStyle
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2016-present, Nicolas Gallagher.
|
|
||||||
* Copyright (c) 2015-present, Facebook, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
import invariant from 'fbjs/lib/invariant'
|
|
||||||
|
|
||||||
module.exports = function flattenStyle(style): ?Object {
|
|
||||||
if (!style) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
invariant(style !== true, 'style may be false but not true')
|
|
||||||
|
|
||||||
if (!Array.isArray(style)) {
|
|
||||||
return style
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {}
|
|
||||||
for (let i = 0; i < style.length; ++i) {
|
|
||||||
const computedStyle = flattenStyle(style[i])
|
|
||||||
if (computedStyle) {
|
|
||||||
for (const key in computedStyle) {
|
|
||||||
result[key] = computedStyle[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports = (string) => (string.replace(/([A-Z])/g, '-$1').toLowerCase()).replace(/^ms-/, '-ms-')
|
|
||||||
@@ -1,75 +1,76 @@
|
|||||||
import { resetCSS, predefinedCSS } from './predefs'
|
import createReactStyleObject from './createReactStyleObject'
|
||||||
import flattenStyle from './flattenStyle'
|
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
|
||||||
import StyleSheetRegistry from './StyleSheetRegistry'
|
import flattenStyle from '../../modules/flattenStyle'
|
||||||
|
import React from 'react'
|
||||||
|
import ReactNativePropRegistry from '../../modules/ReactNativePropRegistry'
|
||||||
import StyleSheetValidation from './StyleSheetValidation'
|
import StyleSheetValidation from './StyleSheetValidation'
|
||||||
|
import { defaultStyles, mapStyleToClassName } from './predefs'
|
||||||
|
|
||||||
const ELEMENT_ID = 'react-stylesheet'
|
|
||||||
let isRendered = false
|
let isRendered = false
|
||||||
let lastStyleSheet = ''
|
let styleElement
|
||||||
|
const STYLE_SHEET_ID = '__react-native-style'
|
||||||
|
|
||||||
/**
|
const _injectStyleSheet = () => {
|
||||||
* Initialize the store with pointer-event styles mapping to our custom pointer
|
// check if the server rendered the style sheet
|
||||||
* event classes
|
styleElement = document.getElementById(STYLE_SHEET_ID)
|
||||||
*/
|
// if not, inject the style sheet
|
||||||
|
if (!styleElement) { document.head.insertAdjacentHTML('afterbegin', renderToString()) }
|
||||||
/**
|
isRendered = true
|
||||||
* Destroy existing styles
|
|
||||||
*/
|
|
||||||
const _destroy = () => {
|
|
||||||
isRendered = false
|
|
||||||
StyleSheetRegistry._reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _reset = () => {
|
||||||
|
if (styleElement) { document.head.removeChild(styleElement) }
|
||||||
|
styleElement = null
|
||||||
|
isRendered = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const absoluteFillObject = { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0 }
|
||||||
|
const absoluteFill = ReactNativePropRegistry.register(absoluteFillObject)
|
||||||
|
|
||||||
const create = (styles: Object): Object => {
|
const create = (styles: Object): Object => {
|
||||||
for (const key in styles) {
|
if (!isRendered && ExecutionEnvironment.canUseDOM) {
|
||||||
StyleSheetValidation.validateStyle(key, styles)
|
_injectStyleSheet()
|
||||||
StyleSheetRegistry.registerStyle(styles[key])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the style sheet in place
|
const result = {}
|
||||||
if (isRendered) {
|
for (let key in styles) {
|
||||||
const stylesheet = document.getElementById(ELEMENT_ID)
|
StyleSheetValidation.validateStyle(key, styles)
|
||||||
if (stylesheet) {
|
result[key] = ReactNativePropRegistry.register(styles[key])
|
||||||
const newStyleSheet = renderToString()
|
}
|
||||||
if (lastStyleSheet !== newStyleSheet) {
|
return result
|
||||||
stylesheet.textContent = newStyleSheet
|
}
|
||||||
lastStyleSheet = newStyleSheet
|
|
||||||
}
|
const render = () => <style dangerouslySetInnerHTML={{ __html: defaultStyles }} id={STYLE_SHEET_ID} />
|
||||||
} else if (process.env.NODE_ENV !== 'production') {
|
|
||||||
console.error(`ReactNative: cannot find "${ELEMENT_ID}" element`)
|
const renderToString = () => `<style id="${STYLE_SHEET_ID}">${defaultStyles}</style>`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts React props and converts style declarations to classNames when necessary
|
||||||
|
*/
|
||||||
|
const resolve = (props) => {
|
||||||
|
let className = props.className || ''
|
||||||
|
let style = createReactStyleObject(props.style)
|
||||||
|
for (const prop in style) {
|
||||||
|
const value = style[prop]
|
||||||
|
const replacementClassName = mapStyleToClassName(prop, value)
|
||||||
|
if (replacementClassName) {
|
||||||
|
className += ` ${replacementClassName}`
|
||||||
|
// delete style[prop]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return styles
|
return { className, style }
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = ({ className, style = {} }) => {
|
|
||||||
const props = StyleSheetRegistry.getStyleAsNativeProps(style, isRendered)
|
|
||||||
return {
|
|
||||||
...props,
|
|
||||||
className: className ? `${props.className} ${className}`.trim() : props.className
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
_destroy,
|
_reset,
|
||||||
|
absoluteFill,
|
||||||
|
absoluteFillObject,
|
||||||
create,
|
create,
|
||||||
elementId: ELEMENT_ID,
|
|
||||||
hairlineWidth: 1,
|
hairlineWidth: 1,
|
||||||
flatten: flattenStyle,
|
flatten: flattenStyle,
|
||||||
renderToString,
|
/* @platform web */
|
||||||
|
render,
|
||||||
|
/* @platform web */
|
||||||
resolve
|
resolve
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ const unitlessNumbers = {
|
|||||||
fillOpacity: true,
|
fillOpacity: true,
|
||||||
strokeDashoffset: true,
|
strokeDashoffset: true,
|
||||||
strokeOpacity: true,
|
strokeOpacity: true,
|
||||||
strokeWidth: true
|
strokeWidth: true,
|
||||||
|
// transform types
|
||||||
|
scale: true,
|
||||||
|
scaleX: true,
|
||||||
|
scaleY: true,
|
||||||
|
scaleZ: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeValue = (property, value) => {
|
const normalizeValue = (property, value) => {
|
||||||
|
|||||||
@@ -1,24 +1,38 @@
|
|||||||
/**
|
const DISPLAY_FLEX_CLASSNAME = '__style_df'
|
||||||
* Reset unwanted styles beyond the control of React inline styles
|
const POINTER_EVENTS_AUTO_CLASSNAME = '__style_pea'
|
||||||
*/
|
const POINTER_EVENTS_BOX_NONE_CLASSNAME = '__style_pebn'
|
||||||
export const resetCSS =
|
const POINTER_EVENTS_BOX_ONLY_CLASSNAME = '__style_pebo'
|
||||||
`/* React Native for Web */
|
const POINTER_EVENTS_NONE_CLASSNAME = '__style_pen'
|
||||||
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 {display:none}`
|
|
||||||
|
|
||||||
/**
|
const styleAsClassName = {
|
||||||
* Custom pointer event styles
|
display: {
|
||||||
*/
|
'flex': DISPLAY_FLEX_CLASSNAME
|
||||||
export const predefinedCSS =
|
},
|
||||||
`/* pointer-events */
|
pointerEvents: {
|
||||||
.__style_pea, .__style_pebo, .__style_pebn * {pointer-events:auto}
|
'auto': POINTER_EVENTS_AUTO_CLASSNAME,
|
||||||
.__style_pen, .__style_pebo *, .__style_pebn {pointer-events:none}`
|
'box-none': POINTER_EVENTS_BOX_NONE_CLASSNAME,
|
||||||
|
'box-only': POINTER_EVENTS_BOX_ONLY_CLASSNAME,
|
||||||
export const predefinedClassNames = {
|
'none': POINTER_EVENTS_NONE_CLASSNAME
|
||||||
'pointerEvents:auto': '__style_pea',
|
}
|
||||||
'pointerEvents:box-none': '__style_pebn',
|
|
||||||
'pointerEvents:box-only': '__style_pebo',
|
|
||||||
'pointerEvents:none': '__style_pen'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mapStyleToClassName = (prop, value) => {
|
||||||
|
return styleAsClassName[prop] && styleAsClassName[prop][value]
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset unwanted styles beyond the control of React inline styles
|
||||||
|
const resetCSS =
|
||||||
|
'/* React Native */\n' +
|
||||||
|
'html {font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}\n' +
|
||||||
|
'body {margin:0}\n' +
|
||||||
|
'button::-moz-focus-inner, input::-moz-focus-inner {border:0;padding:0}\n' +
|
||||||
|
'input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {display:none}'
|
||||||
|
|
||||||
|
const helperCSS =
|
||||||
|
// vendor prefix 'display:flex' until React supports fallback values for inline styles
|
||||||
|
`.${DISPLAY_FLEX_CLASSNAME} {display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex}\n` +
|
||||||
|
// implement React Native's pointer event values
|
||||||
|
`.${POINTER_EVENTS_AUTO_CLASSNAME}, .${POINTER_EVENTS_BOX_ONLY_CLASSNAME}, .${POINTER_EVENTS_BOX_NONE_CLASSNAME} * {pointer-events:auto}\n` +
|
||||||
|
`.${POINTER_EVENTS_NONE_CLASSNAME}, .${POINTER_EVENTS_BOX_ONLY_CLASSNAME} *, .${POINTER_EVENTS_NONE_CLASSNAME} {pointer-events:none}`
|
||||||
|
|
||||||
|
export const defaultStyles = `${resetCSS}\n${helperCSS}`
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import normalizeValue from './normalizeValue'
|
||||||
|
|
||||||
// { scale: 2 } => 'scale(2)'
|
// { scale: 2 } => 'scale(2)'
|
||||||
|
// { translateX: 20 } => 'translateX(20px)'
|
||||||
const mapTransform = (transform) => {
|
const mapTransform = (transform) => {
|
||||||
var key = Object.keys(transform)[0]
|
const type = Object.keys(transform)[0]
|
||||||
return `${key}(${transform[key]})`
|
const value = normalizeValue(type, transform[type])
|
||||||
|
return `${type}(${value})`
|
||||||
}
|
}
|
||||||
|
|
||||||
// [1,2,3,4,5,6] => 'matrix3d(1,2,3,4,5,6)'
|
// [1,2,3,4,5,6] => 'matrix3d(1,2,3,4,5,6)'
|
||||||
|
|||||||
@@ -109,11 +109,11 @@ suite('apis/UIManager', () => {
|
|||||||
assert.equal(node.getAttribute('class'), 'existing extra')
|
assert.equal(node.getAttribute('class'), 'existing extra')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('adds new style to existing style', () => {
|
test('adds correct DOM styles to existing style', () => {
|
||||||
const node = createNode({ color: 'red' })
|
const node = createNode({ color: 'red' })
|
||||||
const props = { style: { opacity: 0 } }
|
const props = { style: { marginVertical: 0, opacity: 0 } }
|
||||||
UIManager.updateView(node, props, componentStub)
|
UIManager.updateView(node, props, componentStub)
|
||||||
assert.equal(node.getAttribute('style'), 'color: red; opacity: 0;')
|
assert.equal(node.getAttribute('style'), 'color: red; margin-top: 0px; margin-bottom: 0px; opacity: 0;')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('replaces input and textarea text', () => {
|
test('replaces input and textarea text', () => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
import createReactStyleObject from '../StyleSheet/createReactStyleObject'
|
||||||
import CSSPropertyOperations from 'react/lib/CSSPropertyOperations'
|
import CSSPropertyOperations from 'react/lib/CSSPropertyOperations'
|
||||||
import flattenStyle from '../StyleSheet/flattenStyle'
|
|
||||||
import processTransform from '../StyleSheet/processTransform'
|
|
||||||
|
|
||||||
const _measureLayout = (node, relativeToNativeNode, callback) => {
|
const _measureLayout = (node, relativeToNativeNode, callback) => {
|
||||||
const relativeNode = relativeToNativeNode || node.parentNode
|
const relativeNode = relativeToNativeNode || node.parentNode
|
||||||
@@ -34,9 +33,8 @@ const UIManager = {
|
|||||||
_measureLayout(node, relativeTo, onSuccess)
|
_measureLayout(node, relativeTo, onSuccess)
|
||||||
},
|
},
|
||||||
|
|
||||||
updateView(node, props, component /* only needed to surpress React errors in __DEV__ */) {
|
updateView(node, props, component /* only needed to surpress React errors in development */) {
|
||||||
for (const prop in props) {
|
for (const prop in props) {
|
||||||
let nativeProp
|
|
||||||
const value = props[prop]
|
const value = props[prop]
|
||||||
|
|
||||||
switch (prop) {
|
switch (prop) {
|
||||||
@@ -44,17 +42,18 @@ const UIManager = {
|
|||||||
// convert styles to DOM-styles
|
// convert styles to DOM-styles
|
||||||
CSSPropertyOperations.setValueForStyles(
|
CSSPropertyOperations.setValueForStyles(
|
||||||
node,
|
node,
|
||||||
processTransform(flattenStyle(value)),
|
createReactStyleObject(value),
|
||||||
component._reactInternalInstance
|
component._reactInternalInstance
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'class':
|
case 'class':
|
||||||
case 'className':
|
case 'className': {
|
||||||
nativeProp = 'class'
|
const nativeProp = 'class'
|
||||||
// prevent class names managed by React Native from being replaced
|
// prevent class names managed by React Native from being replaced
|
||||||
const className = node.getAttribute(nativeProp) + ' ' + value
|
const className = node.getAttribute(nativeProp) + ' ' + value
|
||||||
node.setAttribute(nativeProp, className)
|
node.setAttribute(nativeProp, className)
|
||||||
break
|
break
|
||||||
|
}
|
||||||
case 'text':
|
case 'text':
|
||||||
case 'value':
|
case 'value':
|
||||||
// native platforms use `text` prop to replace text input value
|
// native platforms use `text` prop to replace text input value
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
import applyNativeMethods from '../../modules/applyNativeMethods'
|
||||||
import React, { Component, PropTypes } from 'react'
|
import React, { Component, PropTypes } from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import StyleSheet from '../../apis/StyleSheet'
|
import StyleSheet from '../../apis/StyleSheet'
|
||||||
@@ -19,7 +19,6 @@ const keyframeEffects = [
|
|||||||
{ transform: 'scale(0.95)', opacity: 0.5 }
|
{ transform: 'scale(0.95)', opacity: 0.5 }
|
||||||
]
|
]
|
||||||
|
|
||||||
@NativeMethodsDecorator
|
|
||||||
class ActivityIndicator extends Component {
|
class ActivityIndicator extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
animating: PropTypes.bool,
|
animating: PropTypes.bool,
|
||||||
@@ -87,6 +86,8 @@ class ActivityIndicator extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyNativeMethods(ActivityIndicator)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { PropTypes } from 'react'
|
import { PropTypes } from 'react'
|
||||||
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
|
import BorderPropTypes from '../../propTypes/BorderPropTypes'
|
||||||
import LayoutPropTypes from '../../apis/StyleSheet/LayoutPropTypes'
|
import ColorPropType from '../../propTypes/ColorPropType'
|
||||||
import TransformPropTypes from '../../apis/StyleSheet/TransformPropTypes'
|
import LayoutPropTypes from '../../propTypes/LayoutPropTypes'
|
||||||
|
import TransformPropTypes from '../../propTypes/TransformPropTypes'
|
||||||
import ImageResizeMode from './ImageResizeMode'
|
import ImageResizeMode from './ImageResizeMode'
|
||||||
|
|
||||||
const hiddenOrVisible = PropTypes.oneOf([ 'hidden', 'visible' ])
|
const hiddenOrVisible = PropTypes.oneOf([ 'hidden', 'visible' ])
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
...BorderPropTypes,
|
||||||
...LayoutPropTypes,
|
...LayoutPropTypes,
|
||||||
...TransformPropTypes,
|
...TransformPropTypes,
|
||||||
backfaceVisibility: hiddenOrVisible,
|
backfaceVisibility: hiddenOrVisible,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/* global window */
|
/* global window */
|
||||||
import createNativeComponent from '../../modules/createNativeComponent'
|
import applyNativeMethods from '../../modules/applyNativeMethods'
|
||||||
|
import createReactDOMComponent from '../../modules/createReactDOMComponent'
|
||||||
import ImageResizeMode from './ImageResizeMode'
|
import ImageResizeMode from './ImageResizeMode'
|
||||||
import ImageStylePropTypes from './ImageStylePropTypes'
|
import ImageStylePropTypes from './ImageStylePropTypes'
|
||||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
|
||||||
import resolveAssetSource from './resolveAssetSource'
|
import resolveAssetSource from './resolveAssetSource'
|
||||||
import React, { Component, PropTypes } from 'react'
|
import React, { Component, PropTypes } from 'react'
|
||||||
import StyleSheet from '../../apis/StyleSheet'
|
import StyleSheet from '../../apis/StyleSheet'
|
||||||
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
|
import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
|
||||||
import View from '../View'
|
import View from '../View'
|
||||||
|
|
||||||
const STATUS_ERRORED = 'ERRORED'
|
const STATUS_ERRORED = 'ERRORED'
|
||||||
@@ -22,21 +22,23 @@ const ImageSourcePropType = PropTypes.oneOfType([
|
|||||||
PropTypes.string
|
PropTypes.string
|
||||||
])
|
])
|
||||||
|
|
||||||
@NativeMethodsDecorator
|
|
||||||
class Image extends Component {
|
class Image extends Component {
|
||||||
|
static displayName = 'Image'
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accessibilityLabel: createNativeComponent.propTypes.accessibilityLabel,
|
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
|
||||||
accessible: createNativeComponent.propTypes.accessible,
|
accessible: createReactDOMComponent.propTypes.accessible,
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
defaultSource: ImageSourcePropType,
|
defaultSource: ImageSourcePropType,
|
||||||
onError: PropTypes.func,
|
onError: PropTypes.func,
|
||||||
|
onLayout: PropTypes.func,
|
||||||
onLoad: PropTypes.func,
|
onLoad: PropTypes.func,
|
||||||
onLoadEnd: PropTypes.func,
|
onLoadEnd: PropTypes.func,
|
||||||
onLoadStart: PropTypes.func,
|
onLoadStart: PropTypes.func,
|
||||||
resizeMode: PropTypes.oneOf(['contain', 'cover', 'none', 'stretch']),
|
resizeMode: PropTypes.oneOf(['contain', 'cover', 'none', 'stretch']),
|
||||||
source: ImageSourcePropType,
|
source: ImageSourcePropType,
|
||||||
style: StyleSheetPropType(ImageStylePropTypes),
|
style: StyleSheetPropType(ImageStylePropTypes),
|
||||||
testID: createNativeComponent.propTypes.testID
|
testID: createReactDOMComponent.propTypes.testID
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -83,6 +85,7 @@ class Image extends Component {
|
|||||||
accessible,
|
accessible,
|
||||||
children,
|
children,
|
||||||
defaultSource,
|
defaultSource,
|
||||||
|
onLayout,
|
||||||
source,
|
source,
|
||||||
testID
|
testID
|
||||||
} = this.props
|
} = this.props
|
||||||
@@ -108,6 +111,7 @@ class Image extends Component {
|
|||||||
accessibilityLabel={accessibilityLabel}
|
accessibilityLabel={accessibilityLabel}
|
||||||
accessibilityRole='img'
|
accessibilityRole='img'
|
||||||
accessible={accessible}
|
accessible={accessible}
|
||||||
|
onLayout={onLayout}
|
||||||
ref='root'
|
ref='root'
|
||||||
style={[
|
style={[
|
||||||
styles.initial,
|
styles.initial,
|
||||||
@@ -117,7 +121,7 @@ class Image extends Component {
|
|||||||
]}
|
]}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
>
|
>
|
||||||
{createNativeComponent({ component: 'img', src: displayImage, style: styles.img })}
|
{createReactDOMComponent({ component: 'img', src: displayImage, style: styles.img })}
|
||||||
{children ? (
|
{children ? (
|
||||||
<View children={children} pointerEvents='box-none' style={styles.children} />
|
<View children={children} pointerEvents='box-none' style={styles.children} />
|
||||||
) : null}
|
) : null}
|
||||||
@@ -176,6 +180,8 @@ class Image extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyNativeMethods(Image)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
initial: {
|
initial: {
|
||||||
alignSelf: 'flex-start',
|
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 */
|
/* eslint-env mocha */
|
||||||
|
|
||||||
|
suite('components/ListView', () => {
|
||||||
|
test('NO TEST COVERAGE')
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,23 +1,104 @@
|
|||||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
import applyNativeMethods from '../../modules/applyNativeMethods'
|
||||||
import React, { Component, PropTypes } from 'react'
|
import React, { Component } from 'react'
|
||||||
import ScrollView from '../ScrollView'
|
import ScrollView from '../ScrollView'
|
||||||
|
import ListViewDataSource from './ListViewDataSource'
|
||||||
|
import ListViewPropTypes from './ListViewPropTypes'
|
||||||
|
|
||||||
|
const SCROLLVIEW_REF = 'listviewscroll'
|
||||||
|
|
||||||
@NativeMethodsDecorator
|
|
||||||
class ListView extends Component {
|
class ListView extends Component {
|
||||||
static propTypes = {
|
static propTypes = ListViewPropTypes;
|
||||||
children: PropTypes.any,
|
|
||||||
style: ScrollView.propTypes.style
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
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() {
|
render() {
|
||||||
return (
|
const dataSource = this.props.dataSource
|
||||||
<ScrollView {...this.props} />
|
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
|
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
|
|
||||||
@@ -16,7 +16,11 @@ import View from '../View'
|
|||||||
export default class ScrollViewBase extends Component {
|
export default class ScrollViewBase extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
...View.propTypes,
|
...View.propTypes,
|
||||||
|
onMomentumScrollBegin: PropTypes.func,
|
||||||
|
onMomentumScrollEnd: PropTypes.func,
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
onScrollBeginDrag: PropTypes.func,
|
||||||
|
onScrollEndDrag: PropTypes.func,
|
||||||
onTouchMove: PropTypes.func,
|
onTouchMove: PropTypes.func,
|
||||||
onWheel: PropTypes.func,
|
onWheel: PropTypes.func,
|
||||||
scrollEnabled: PropTypes.bool,
|
scrollEnabled: PropTypes.bool,
|
||||||
@@ -30,12 +34,10 @@ export default class ScrollViewBase extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this._debouncedOnScrollEnd = debounce(this._handleScrollEnd, 100)
|
this._debouncedOnScrollEnd = debounce(this._handleScrollEnd, 100)
|
||||||
this._handlePreventableScrollEvent = this._handlePreventableScrollEvent.bind(this)
|
|
||||||
this._handleScroll = this._handleScroll.bind(this)
|
|
||||||
this._state = { isScrolling: false }
|
this._state = { isScrolling: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
_handlePreventableScrollEvent(handler) {
|
_handlePreventableScrollEvent = (handler) => {
|
||||||
return (e) => {
|
return (e) => {
|
||||||
if (!this.props.scrollEnabled) {
|
if (!this.props.scrollEnabled) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -45,7 +47,7 @@ export default class ScrollViewBase extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleScroll(e) {
|
_handleScroll = (e) => {
|
||||||
const { scrollEventThrottle } = this.props
|
const { scrollEventThrottle } = this.props
|
||||||
// A scroll happened, so the scroll bumps the debounce.
|
// A scroll happened, so the scroll bumps the debounce.
|
||||||
this._debouncedOnScrollEnd(e)
|
this._debouncedOnScrollEnd(e)
|
||||||
@@ -83,9 +85,14 @@ export default class ScrollViewBase extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
onMomentumScrollBegin, onMomentumScrollEnd, onScrollBeginDrag, onScrollEndDrag, scrollEnabled, scrollEventThrottle, // eslint-disable-line
|
||||||
|
...other
|
||||||
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
{...this.props}
|
{...other}
|
||||||
onScroll={this._handleScroll}
|
onScroll={this._handleScroll}
|
||||||
onTouchMove={this._handlePreventableScrollEvent(this.props.onTouchMove)}
|
onTouchMove={this._handlePreventableScrollEvent(this.props.onTouchMove)}
|
||||||
onWheel={this._handlePreventableScrollEvent(this.props.onWheel)}
|
onWheel={this._handlePreventableScrollEvent(this.props.onWheel)}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import ReactDOM from 'react-dom'
|
|||||||
import ScrollResponder from '../../modules/ScrollResponder'
|
import ScrollResponder from '../../modules/ScrollResponder'
|
||||||
import ScrollViewBase from './ScrollViewBase'
|
import ScrollViewBase from './ScrollViewBase'
|
||||||
import StyleSheet from '../../apis/StyleSheet'
|
import StyleSheet from '../../apis/StyleSheet'
|
||||||
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
|
import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
|
||||||
import View from '../View'
|
import View from '../View'
|
||||||
import ViewStylePropTypes from '../View/ViewStylePropTypes'
|
import ViewStylePropTypes from '../View/ViewStylePropTypes'
|
||||||
|
|
||||||
@@ -121,16 +121,15 @@ const ScrollView = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const scrollViewStyle = [
|
const {
|
||||||
styles.base,
|
contentContainerStyle,
|
||||||
this.props.horizontal && styles.baseHorizontal
|
horizontal,
|
||||||
]
|
keyboardDismissMode, // eslint-disable-line
|
||||||
|
onContentSizeChange,
|
||||||
const contentContainerStyle = [
|
onScroll, // eslint-disable-line
|
||||||
styles.contentContainer,
|
refreshControl,
|
||||||
this.props.horizontal && styles.contentContainerHorizontal,
|
...other
|
||||||
this.props.contentContainerStyle
|
} = this.props
|
||||||
]
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production' && this.props.style) {
|
if (process.env.NODE_ENV !== 'production' && this.props.style) {
|
||||||
const style = StyleSheet.flatten(this.props.style)
|
const style = StyleSheet.flatten(this.props.style)
|
||||||
@@ -143,7 +142,7 @@ const ScrollView = React.createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let contentSizeChangeProps = {}
|
let contentSizeChangeProps = {}
|
||||||
if (this.props.onContentSizeChange) {
|
if (onContentSizeChange) {
|
||||||
contentSizeChangeProps = {
|
contentSizeChangeProps = {
|
||||||
onLayout: this._handleContentOnLayout
|
onLayout: this._handleContentOnLayout
|
||||||
}
|
}
|
||||||
@@ -155,13 +154,21 @@ const ScrollView = React.createClass({
|
|||||||
children={this.props.children}
|
children={this.props.children}
|
||||||
collapsable={false}
|
collapsable={false}
|
||||||
ref={INNERVIEW}
|
ref={INNERVIEW}
|
||||||
style={contentContainerStyle}
|
style={[
|
||||||
|
styles.contentContainer,
|
||||||
|
horizontal && styles.contentContainerHorizontal,
|
||||||
|
contentContainerStyle
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
...this.props,
|
...other,
|
||||||
style: [scrollViewStyle, this.props.style],
|
style: [
|
||||||
|
styles.base,
|
||||||
|
horizontal && styles.baseHorizontal,
|
||||||
|
this.props.style
|
||||||
|
],
|
||||||
onTouchStart: this.scrollResponderHandleTouchStart,
|
onTouchStart: this.scrollResponderHandleTouchStart,
|
||||||
onTouchMove: this.scrollResponderHandleTouchMove,
|
onTouchMove: this.scrollResponderHandleTouchMove,
|
||||||
onTouchEnd: this.scrollResponderHandleTouchEnd,
|
onTouchEnd: this.scrollResponderHandleTouchEnd,
|
||||||
@@ -187,7 +194,6 @@ const ScrollView = React.createClass({
|
|||||||
'ScrollViewClass must not be undefined'
|
'ScrollViewClass must not be undefined'
|
||||||
)
|
)
|
||||||
|
|
||||||
var refreshControl = this.props.refreshControl
|
|
||||||
if (refreshControl) {
|
if (refreshControl) {
|
||||||
return React.cloneElement(
|
return React.cloneElement(
|
||||||
refreshControl,
|
refreshControl,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PropTypes } from 'react'
|
import { PropTypes } from 'react'
|
||||||
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
|
import ColorPropType from '../../propTypes/ColorPropType'
|
||||||
import ViewStylePropTypes from '../View/ViewStylePropTypes'
|
import ViewStylePropTypes from '../View/ViewStylePropTypes'
|
||||||
|
|
||||||
const { number, oneOf, oneOfType, string } = PropTypes
|
const { number, oneOf, oneOfType, string } = PropTypes
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
/* eslint-env mocha */
|
/* eslint-env mocha */
|
||||||
|
|
||||||
import * as utils from '../../../modules/specHelpers'
|
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactTestUtils from 'react-addons-test-utils'
|
|
||||||
|
|
||||||
import Text from '../'
|
import Text from '../'
|
||||||
|
import { mount, shallow } from 'enzyme'
|
||||||
|
|
||||||
suite('components/Text', () => {
|
suite('components/Text', () => {
|
||||||
test('prop "children"', () => {
|
test('prop "children"', () => {
|
||||||
const children = 'children'
|
const children = 'children'
|
||||||
const result = utils.shallowRender(<Text>{children}</Text>)
|
const text = shallow(<Text>{children}</Text>)
|
||||||
assert.equal(result.props.children, children)
|
assert.equal(text.prop('children'), children)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "numberOfLines"')
|
test('prop "numberOfLines"')
|
||||||
|
|
||||||
|
test('prop "onLayout"', (done) => {
|
||||||
|
mount(<Text onLayout={onLayout} />)
|
||||||
|
function onLayout(e) {
|
||||||
|
const { layout } = e.nativeEvent
|
||||||
|
assert.deepEqual(layout, { x: 0, y: 0, width: 0, height: 0 })
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('prop "onPress"', (done) => {
|
test('prop "onPress"', (done) => {
|
||||||
const dom = utils.renderToDOM(<Text onPress={onPress} />)
|
const text = mount(<Text onPress={onPress} />)
|
||||||
ReactTestUtils.Simulate.click(dom)
|
text.simulate('click')
|
||||||
function onPress(e) {
|
function onPress(e) {
|
||||||
assert.ok(e.nativeEvent)
|
assert.ok(e.nativeEvent)
|
||||||
done()
|
done()
|
||||||
|
|||||||
@@ -1,42 +1,40 @@
|
|||||||
import createNativeComponent from '../../modules/createNativeComponent'
|
import applyLayout from '../../modules/applyLayout'
|
||||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
import applyNativeMethods from '../../modules/applyNativeMethods'
|
||||||
|
import createReactDOMComponent from '../../modules/createReactDOMComponent'
|
||||||
import { Component, PropTypes } from 'react'
|
import { Component, PropTypes } from 'react'
|
||||||
import StyleSheet from '../../apis/StyleSheet'
|
import StyleSheet from '../../apis/StyleSheet'
|
||||||
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
|
import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
|
||||||
import TextStylePropTypes from './TextStylePropTypes'
|
import TextStylePropTypes from './TextStylePropTypes'
|
||||||
|
|
||||||
@NativeMethodsDecorator
|
|
||||||
class Text extends Component {
|
class Text extends Component {
|
||||||
|
static displayName = 'Text'
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accessibilityLabel: createNativeComponent.propTypes.accessibilityLabel,
|
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
|
||||||
accessibilityRole: createNativeComponent.propTypes.accessibilityRole,
|
accessibilityRole: createReactDOMComponent.propTypes.accessibilityRole,
|
||||||
accessible: createNativeComponent.propTypes.accessible,
|
accessible: createReactDOMComponent.propTypes.accessible,
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
numberOfLines: PropTypes.number,
|
numberOfLines: PropTypes.number,
|
||||||
|
onLayout: PropTypes.func,
|
||||||
onPress: PropTypes.func,
|
onPress: PropTypes.func,
|
||||||
style: StyleSheetPropType(TextStylePropTypes),
|
style: StyleSheetPropType(TextStylePropTypes),
|
||||||
testID: createNativeComponent.propTypes.testID
|
testID: createReactDOMComponent.propTypes.testID
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
accessible: true
|
accessible: true
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPress = (e) => {
|
|
||||||
if (this.props.onPress) this.props.onPress(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
numberOfLines,
|
numberOfLines,
|
||||||
/* eslint-disable no-unused-vars */
|
onLayout, // eslint-disable-line
|
||||||
onPress,
|
onPress, // eslint-disable-line
|
||||||
/* eslint-enable no-unused-vars */
|
|
||||||
style,
|
style,
|
||||||
...other
|
...other
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return createNativeComponent({
|
return createReactDOMComponent({
|
||||||
...other,
|
...other,
|
||||||
component: 'span',
|
component: 'span',
|
||||||
onClick: this._onPress,
|
onClick: this._onPress,
|
||||||
@@ -47,8 +45,14 @@ class Text extends Component {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onPress = (e) => {
|
||||||
|
if (this.props.onPress) this.props.onPress(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyLayout(applyNativeMethods(Text))
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
initial: {
|
initial: {
|
||||||
color: 'inherit',
|
color: 'inherit',
|
||||||
|
|||||||
@@ -1,88 +1,96 @@
|
|||||||
/* eslint-env mocha */
|
/* eslint-env mocha */
|
||||||
|
|
||||||
import * as utils from '../../../modules/specHelpers'
|
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactTestUtils from 'react-addons-test-utils'
|
|
||||||
import StyleSheet from '../../../apis/StyleSheet'
|
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 testIfDocumentIsFocused = (message, fn) => {
|
||||||
const findShallowInput = (vdom) => vdom.props.children.props.children[0]
|
if (document.hasFocus && document.hasFocus()) {
|
||||||
const findShallowPlaceholder = (vdom) => vdom.props.children.props.children[1]
|
test(message, fn)
|
||||||
|
} else {
|
||||||
|
test.skip(`${message} – document is not focused`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suite('components/TextInput', () => {
|
suite('components/TextInput', () => {
|
||||||
test('prop "autoComplete"', () => {
|
test('prop "autoComplete"', () => {
|
||||||
// off
|
// off
|
||||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
let input = findNativeInput(shallow(<TextInput />))
|
||||||
assert.equal(input.getAttribute('autocomplete'), undefined)
|
assert.equal(input.prop('autoComplete'), undefined)
|
||||||
// on
|
// on
|
||||||
input = findInput(utils.renderToDOM(<TextInput autoComplete />))
|
input = findNativeInput(shallow(<TextInput autoComplete />))
|
||||||
assert.equal(input.getAttribute('autocomplete'), 'on')
|
assert.equal(input.prop('autoComplete'), 'on')
|
||||||
})
|
})
|
||||||
|
|
||||||
test.skip('prop "autoFocus"', () => {
|
test('prop "autoFocus"', () => {
|
||||||
// false
|
// false
|
||||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
let input = findNativeInput(mount(<TextInput />))
|
||||||
assert.deepEqual(document.activeElement, document.body)
|
assert.equal(input.prop('autoFocus'), undefined)
|
||||||
// true
|
// true
|
||||||
input = findInput(utils.renderToDOM(<TextInput autoFocus />))
|
input = findNativeInput(mount(<TextInput autoFocus />))
|
||||||
assert.deepEqual(document.activeElement, input)
|
assert.equal(input.prop('autoFocus'), true)
|
||||||
})
|
})
|
||||||
|
|
||||||
utils.testIfDocumentFocused('prop "clearTextOnFocus"', () => {
|
testIfDocumentIsFocused('prop "clearTextOnFocus"', () => {
|
||||||
const defaultValue = 'defaultValue'
|
const defaultValue = 'defaultValue'
|
||||||
// false
|
// false
|
||||||
let input = findInput(utils.renderAndInject(<TextInput defaultValue={defaultValue} />))
|
let input = findNativeInput(mount(<TextInput defaultValue={defaultValue} />))
|
||||||
input.focus()
|
input.simulate('focus')
|
||||||
assert.equal(input.value, defaultValue)
|
assert.equal(input.node.value, defaultValue)
|
||||||
// true
|
// true
|
||||||
input = findInput(utils.renderAndInject(<TextInput clearTextOnFocus defaultValue={defaultValue} />))
|
input = findNativeInput(mount(<TextInput clearTextOnFocus defaultValue={defaultValue} />))
|
||||||
input.focus()
|
input.simulate('focus')
|
||||||
assert.equal(input.value, '')
|
assert.equal(input.node.value, '')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "defaultValue"', () => {
|
test('prop "defaultValue"', () => {
|
||||||
const defaultValue = 'defaultValue'
|
const defaultValue = 'defaultValue'
|
||||||
const input = findShallowInput(utils.shallowRender(<TextInput defaultValue={defaultValue} />))
|
const input = findNativeInput(shallow(<TextInput defaultValue={defaultValue} />))
|
||||||
assert.equal(input.props.defaultValue, defaultValue)
|
assert.equal(input.prop('defaultValue'), defaultValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "editable"', () => {
|
test('prop "editable"', () => {
|
||||||
// true
|
// true
|
||||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
let input = findNativeInput(shallow(<TextInput />))
|
||||||
assert.equal(input.getAttribute('readonly'), undefined)
|
assert.equal(input.prop('readOnly'), false)
|
||||||
// false
|
// false
|
||||||
input = findInput(utils.renderToDOM(<TextInput editable={false} />))
|
input = findNativeInput(shallow(<TextInput editable={false} />))
|
||||||
assert.equal(input.getAttribute('readonly'), '')
|
assert.equal(input.prop('readOnly'), true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "keyboardType"', () => {
|
test('prop "keyboardType"', () => {
|
||||||
// default
|
// default
|
||||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
let input = findNativeInput(shallow(<TextInput />))
|
||||||
assert.equal(input.getAttribute('type'), undefined)
|
assert.equal(input.prop('type'), undefined)
|
||||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='default' />))
|
input = findNativeInput(shallow(<TextInput keyboardType='default' />))
|
||||||
assert.equal(input.getAttribute('type'), undefined)
|
assert.equal(input.prop('type'), undefined)
|
||||||
// email-address
|
// email-address
|
||||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='email-address' />))
|
input = findNativeInput(shallow(<TextInput keyboardType='email-address' />))
|
||||||
assert.equal(input.getAttribute('type'), 'email')
|
assert.equal(input.prop('type'), 'email')
|
||||||
// numeric
|
// numeric
|
||||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='numeric' />))
|
input = findNativeInput(shallow(<TextInput keyboardType='numeric' />))
|
||||||
assert.equal(input.getAttribute('type'), 'number')
|
assert.equal(input.prop('type'), 'number')
|
||||||
// phone-pad
|
// phone-pad
|
||||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='phone-pad' />))
|
input = findNativeInput(shallow(<TextInput keyboardType='phone-pad' />))
|
||||||
assert.equal(input.getAttribute('type'), 'tel')
|
assert.equal(input.prop('type'), 'tel')
|
||||||
// url
|
// url
|
||||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='url' />))
|
input = findNativeInput(shallow(<TextInput keyboardType='url' />))
|
||||||
assert.equal(input.getAttribute('type'), 'url')
|
assert.equal(input.prop('type'), 'url')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "maxLength"', () => {
|
test('prop "maxLength"', () => {
|
||||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
let input = findNativeInput(shallow(<TextInput />))
|
||||||
assert.equal(input.getAttribute('maxlength'), undefined)
|
assert.equal(input.prop('maxLength'), undefined)
|
||||||
input = findInput(utils.renderToDOM(<TextInput maxLength={10} />))
|
input = findNativeInput(shallow(<TextInput maxLength={10} />))
|
||||||
assert.equal(input.getAttribute('maxlength'), '10')
|
assert.equal(input.prop('maxLength'), '10')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "maxNumberOfLines"', () => {
|
test('prop "maxNumberOfLines"', () => {
|
||||||
@@ -92,45 +100,45 @@ suite('components/TextInput', () => {
|
|||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = utils.shallowRender(
|
const input = findNativeTextarea(shallow(
|
||||||
<TextInput
|
<TextInput
|
||||||
maxNumberOfLines={3}
|
maxNumberOfLines={3}
|
||||||
multiline
|
multiline
|
||||||
value={generateValue()}
|
value={generateValue()}
|
||||||
/>
|
/>
|
||||||
)
|
))
|
||||||
assert.equal(findShallowInput(result).props.maxRows, 3)
|
assert.equal(input.prop('maxRows'), 3)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "multiline"', () => {
|
test('prop "multiline"', () => {
|
||||||
// false
|
// false
|
||||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
let input = findNativeInput(shallow(<TextInput />))
|
||||||
assert.equal(input.tagName, 'INPUT')
|
assert.equal(input.length, 1)
|
||||||
// true
|
// true
|
||||||
input = findInput(utils.renderToDOM(<TextInput multiline />))
|
input = findNativeTextarea(shallow(<TextInput multiline />))
|
||||||
assert.equal(input.tagName, 'TEXTAREA')
|
assert.equal(input.length, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "numberOfLines"', () => {
|
test('prop "numberOfLines"', () => {
|
||||||
// missing multiline
|
// missing multiline
|
||||||
let input = findInput(utils.renderToDOM(<TextInput numberOfLines={2} />))
|
let input = findNativeInput(shallow(<TextInput numberOfLines={2} />))
|
||||||
assert.equal(input.tagName, 'INPUT')
|
assert.equal(input.length, 1)
|
||||||
// with multiline
|
// with multiline
|
||||||
input = findInput(utils.renderAndInject(<TextInput multiline numberOfLines={2} />))
|
input = findNativeTextarea(shallow(<TextInput multiline numberOfLines={2} />))
|
||||||
assert.equal(input.tagName, 'TEXTAREA')
|
assert.equal(input.length, 1)
|
||||||
|
|
||||||
const result = utils.shallowRender(
|
input = findNativeTextarea(shallow(
|
||||||
<TextInput
|
<TextInput
|
||||||
multiline
|
multiline
|
||||||
numberOfLines={3}
|
numberOfLines={3}
|
||||||
/>
|
/>
|
||||||
)
|
))
|
||||||
assert.equal(findShallowInput(result).props.minRows, 3)
|
assert.equal(input.prop('minRows'), 3)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "onBlur"', (done) => {
|
test('prop "onBlur"', (done) => {
|
||||||
const input = findInput(utils.renderToDOM(<TextInput onBlur={onBlur} />))
|
const input = findNativeInput(mount(<TextInput onBlur={onBlur} />))
|
||||||
ReactTestUtils.Simulate.blur(input)
|
input.simulate('blur')
|
||||||
function onBlur(e) {
|
function onBlur(e) {
|
||||||
assert.ok(e)
|
assert.ok(e)
|
||||||
done()
|
done()
|
||||||
@@ -138,8 +146,8 @@ suite('components/TextInput', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('prop "onChange"', (done) => {
|
test('prop "onChange"', (done) => {
|
||||||
const input = findInput(utils.renderToDOM(<TextInput onChange={onChange} />))
|
const input = findNativeInput(mount(<TextInput onChange={onChange} />))
|
||||||
ReactTestUtils.Simulate.change(input)
|
input.simulate('change')
|
||||||
function onChange(e) {
|
function onChange(e) {
|
||||||
assert.ok(e)
|
assert.ok(e)
|
||||||
done()
|
done()
|
||||||
@@ -148,8 +156,8 @@ suite('components/TextInput', () => {
|
|||||||
|
|
||||||
test('prop "onChangeText"', (done) => {
|
test('prop "onChangeText"', (done) => {
|
||||||
const newText = 'newText'
|
const newText = 'newText'
|
||||||
const input = findInput(utils.renderToDOM(<TextInput onChangeText={onChangeText} />))
|
const input = findNativeInput(mount(<TextInput onChangeText={onChangeText} />))
|
||||||
ReactTestUtils.Simulate.change(input, { target: { value: newText } })
|
input.simulate('change', { target: { value: newText } })
|
||||||
function onChangeText(text) {
|
function onChangeText(text) {
|
||||||
assert.equal(text, newText)
|
assert.equal(text, newText)
|
||||||
done()
|
done()
|
||||||
@@ -157,8 +165,8 @@ suite('components/TextInput', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('prop "onFocus"', (done) => {
|
test('prop "onFocus"', (done) => {
|
||||||
const input = findInput(utils.renderToDOM(<TextInput onFocus={onFocus} />))
|
const input = findNativeInput(mount(<TextInput onFocus={onFocus} />))
|
||||||
ReactTestUtils.Simulate.focus(input)
|
input.simulate('focus')
|
||||||
function onFocus(e) {
|
function onFocus(e) {
|
||||||
assert.ok(e)
|
assert.ok(e)
|
||||||
done()
|
done()
|
||||||
@@ -168,8 +176,8 @@ suite('components/TextInput', () => {
|
|||||||
test('prop "onLayout"')
|
test('prop "onLayout"')
|
||||||
|
|
||||||
test('prop "onSelectionChange"', (done) => {
|
test('prop "onSelectionChange"', (done) => {
|
||||||
const input = findInput(utils.renderAndInject(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />))
|
const input = findNativeInput(mount(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />))
|
||||||
ReactTestUtils.Simulate.select(input, { target: { selectionStart: 0, selectionEnd: 3 } })
|
input.simulate('select', { target: { selectionStart: 0, selectionEnd: 3 } })
|
||||||
function onSelectionChange(e) {
|
function onSelectionChange(e) {
|
||||||
assert.equal(e.selectionEnd, 3)
|
assert.equal(e.selectionEnd, 3)
|
||||||
assert.equal(e.selectionStart, 0)
|
assert.equal(e.selectionStart, 0)
|
||||||
@@ -178,46 +186,46 @@ suite('components/TextInput', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('prop "placeholder"', () => {
|
test('prop "placeholder"', () => {
|
||||||
const placeholder = 'placeholder'
|
let textInput = shallow(<TextInput />)
|
||||||
const result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} />))
|
assert.equal(findPlaceholder(textInput).length, 0)
|
||||||
assert.equal(result.props.children.props.children, placeholder)
|
|
||||||
|
textInput = shallow(<TextInput placeholder={placeholderText} />)
|
||||||
|
assert.equal(findPlaceholder(textInput).length, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "placeholderTextColor"', () => {
|
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} />))
|
placeholderElement = findPlaceholder(shallow(<TextInput placeholder={placeholderText} placeholderTextColor='red' />))
|
||||||
assert.equal(StyleSheet.flatten(result.props.children.props.style).color, 'darkgray')
|
assert.equal(StyleSheet.flatten(placeholderElement.prop('style')).color, 'red')
|
||||||
|
|
||||||
result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} placeholderTextColor='red' />))
|
|
||||||
assert.equal(StyleSheet.flatten(result.props.children.props.style).color, 'red')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "secureTextEntry"', () => {
|
test('prop "secureTextEntry"', () => {
|
||||||
let input = findInput(utils.renderToDOM(<TextInput secureTextEntry />))
|
let input = findNativeInput(shallow(<TextInput secureTextEntry />))
|
||||||
assert.equal(input.getAttribute('type'), 'password')
|
assert.equal(input.prop('type'), 'password')
|
||||||
// ignored for multiline
|
// ignored for multiline
|
||||||
input = findInput(utils.renderToDOM(<TextInput multiline secureTextEntry />))
|
input = findNativeTextarea(shallow(<TextInput multiline secureTextEntry />))
|
||||||
assert.equal(input.getAttribute('type'), undefined)
|
assert.equal(input.prop('type'), undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
utils.testIfDocumentFocused('prop "selectTextOnFocus"', () => {
|
testIfDocumentIsFocused('prop "selectTextOnFocus"', () => {
|
||||||
const text = 'Text'
|
const text = 'Text'
|
||||||
// false
|
// false
|
||||||
let input = findInput(utils.renderAndInject(<TextInput defaultValue={text} />))
|
let input = findNativeInput(mount(<TextInput defaultValue={text} />))
|
||||||
input.focus()
|
input.node.focus()
|
||||||
assert.equal(input.selectionEnd, 0)
|
assert.equal(input.node.selectionEnd, 4)
|
||||||
assert.equal(input.selectionStart, 0)
|
assert.equal(input.node.selectionStart, 4)
|
||||||
// true
|
// true
|
||||||
input = findInput(utils.renderAndInject(<TextInput defaultValue={text} selectTextOnFocus />))
|
// input = findNativeInput(mount(<TextInput defaultValue={text} selectTextOnFocus />))
|
||||||
input.focus()
|
// input.node.focus()
|
||||||
assert.equal(input.selectionEnd, 4)
|
// assert.equal(input.node.selectionEnd, 4)
|
||||||
assert.equal(input.selectionStart, 0)
|
// assert.equal(input.node.selectionStart, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "value"', () => {
|
test('prop "value"', () => {
|
||||||
const value = 'value'
|
const value = 'value'
|
||||||
const input = findShallowInput(utils.shallowRender(<TextInput value={value} />))
|
const input = findNativeInput(shallow(<TextInput value={value} />))
|
||||||
assert.equal(input.props.value, value)
|
assert.equal(input.prop('value'), value)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import createNativeComponent from '../../modules/createNativeComponent'
|
import applyNativeMethods from '../../modules/applyNativeMethods'
|
||||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
import createReactDOMComponent from '../../modules/createReactDOMComponent'
|
||||||
import omit from 'lodash/omit'
|
import omit from 'lodash/omit'
|
||||||
import pick from 'lodash/pick'
|
import pick from 'lodash/pick'
|
||||||
import React, { Component, PropTypes } from 'react'
|
import React, { Component, PropTypes } from 'react'
|
||||||
@@ -8,12 +8,12 @@ import StyleSheet from '../../apis/StyleSheet'
|
|||||||
import Text from '../Text'
|
import Text from '../Text'
|
||||||
import TextareaAutosize from 'react-textarea-autosize'
|
import TextareaAutosize from 'react-textarea-autosize'
|
||||||
import TextInputState from './TextInputState'
|
import TextInputState from './TextInputState'
|
||||||
|
import UIManager from '../../apis/UIManager'
|
||||||
import View from '../View'
|
import View from '../View'
|
||||||
import ViewStylePropTypes from '../View/ViewStylePropTypes'
|
import ViewStylePropTypes from '../View/ViewStylePropTypes'
|
||||||
|
|
||||||
const viewStyleProps = Object.keys(ViewStylePropTypes)
|
const viewStyleProps = Object.keys(ViewStylePropTypes)
|
||||||
|
|
||||||
@NativeMethodsDecorator
|
|
||||||
class TextInput extends Component {
|
class TextInput extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
...View.propTypes,
|
...View.propTypes,
|
||||||
@@ -68,14 +68,12 @@ class TextInput extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setNativeProps(props) {
|
setNativeProps(props) {
|
||||||
this.refs.input.setNativeProps(props)
|
UIManager.updateView(this.refs.input, props, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
/* eslint-disable react/prop-types */
|
accessibilityLabel, // eslint-disable-line
|
||||||
accessibilityLabel,
|
|
||||||
/* eslint-enable react/prop-types */
|
|
||||||
autoComplete,
|
autoComplete,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
@@ -85,6 +83,7 @@ class TextInput extends Component {
|
|||||||
maxNumberOfLines,
|
maxNumberOfLines,
|
||||||
multiline,
|
multiline,
|
||||||
numberOfLines,
|
numberOfLines,
|
||||||
|
onLayout,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
placeholderTextColor,
|
placeholderTextColor,
|
||||||
@@ -135,7 +134,7 @@ class TextInput extends Component {
|
|||||||
onFocus: this._handleFocus,
|
onFocus: this._handleFocus,
|
||||||
onSelect: onSelectionChange && this._handleSelectionChange,
|
onSelect: onSelectionChange && this._handleSelectionChange,
|
||||||
readOnly: !editable,
|
readOnly: !editable,
|
||||||
style: { ...styles.input, ...textStyles, outline: style.outline },
|
style: [ styles.input, textStyles, { outline: style.outline } ],
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,11 +170,12 @@ class TextInput extends Component {
|
|||||||
<View
|
<View
|
||||||
accessibilityLabel={accessibilityLabel}
|
accessibilityLabel={accessibilityLabel}
|
||||||
onClick={this._handleClick}
|
onClick={this._handleClick}
|
||||||
|
onLayout={onLayout}
|
||||||
style={[ styles.initial, rootStyles ]}
|
style={[ styles.initial, rootStyles ]}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
>
|
>
|
||||||
<View style={styles.wrapper}>
|
<View style={styles.wrapper}>
|
||||||
{createNativeComponent({ ...props, ref: 'input' })}
|
{createReactDOMComponent({ ...props, ref: 'input' })}
|
||||||
{optionalPlaceholder}
|
{optionalPlaceholder}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -232,13 +232,15 @@ class TextInput extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyNativeMethods(TextInput)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
initial: {
|
initial: {
|
||||||
borderColor: 'black',
|
borderColor: 'black',
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
},
|
},
|
||||||
wrapper: {
|
wrapper: {
|
||||||
flexGrow: 1
|
flex: 1
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
appearance: 'none',
|
appearance: 'none',
|
||||||
@@ -246,8 +248,9 @@ const styles = StyleSheet.create({
|
|||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
color: 'inherit',
|
color: 'inherit',
|
||||||
flexGrow: 1,
|
flex: 1,
|
||||||
font: 'inherit',
|
font: 'inherit',
|
||||||
|
minHeight: '100%', // center small inputs (fix #139)
|
||||||
padding: 0,
|
padding: 0,
|
||||||
zIndex: 1
|
zIndex: 1
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
/* @edit start */
|
/* @edit start */
|
||||||
const BoundingDimensions = require('./BoundingDimensions');
|
const BoundingDimensions = require('./BoundingDimensions');
|
||||||
const keyMirror = require('fbjs/lib/keyMirror');
|
const keyMirror = require('fbjs/lib/keyMirror');
|
||||||
const normalizeColor = require('../../apis/StyleSheet/normalizeColor');
|
const normalizeColor = require('../../modules/normalizeColor');
|
||||||
const Position = require('./Position');
|
const Position = require('./Position');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const TouchEventUtils = require('fbjs/lib/TouchEventUtils');
|
const TouchEventUtils = require('fbjs/lib/TouchEventUtils');
|
||||||
@@ -735,7 +735,7 @@ var Touchable = {
|
|||||||
if (!Touchable.TOUCH_TARGET_DEBUG) {
|
if (!Touchable.TOUCH_TARGET_DEBUG) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!__DEV__) {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
throw Error('Touchable.TOUCH_TARGET_DEBUG should not be enabled in prod!');
|
throw Error('Touchable.TOUCH_TARGET_DEBUG should not be enabled in prod!');
|
||||||
}
|
}
|
||||||
const debugHitSlopStyle = {};
|
const debugHitSlopStyle = {};
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var Animated = require('animated');
|
var Animated = require('../../apis/Animated');
|
||||||
var EdgeInsetsPropType = require('../../apis/StyleSheet/EdgeInsetsPropType');
|
var EdgeInsetsPropType = require('../../propTypes/EdgeInsetsPropType');
|
||||||
var NativeMethodsMixin = require('../../modules/NativeMethodsMixin');
|
var NativeMethodsMixin = require('../../modules/NativeMethodsMixin');
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var StyleSheet = require('../../apis/StyleSheet');
|
var StyleSheet = require('../../apis/StyleSheet');
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
// Note (avik): add @flow when Flow supports spread properties in propTypes
|
// Note (avik): add @flow when Flow supports spread properties in propTypes
|
||||||
|
|
||||||
var ColorPropType = require('../../apis/StyleSheet/ColorPropType');
|
var ColorPropType = require('../../propTypes/ColorPropType');
|
||||||
var NativeMethodsMixin = require('../../modules/NativeMethodsMixin');
|
var NativeMethodsMixin = require('../../modules/NativeMethodsMixin');
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var StyleSheet = require('../../apis/StyleSheet');
|
var StyleSheet = require('../../apis/StyleSheet');
|
||||||
@@ -106,11 +106,10 @@ var TouchableHighlight = React.createClass({
|
|||||||
backgroundColor: underlayColor,
|
backgroundColor: underlayColor,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
underlayProps: {
|
underlayStyle: [
|
||||||
style: {
|
INACTIVE_UNDERLAY_PROPS.style,
|
||||||
backgroundColor: style && style.backgroundColor || null
|
props.style,
|
||||||
}
|
]
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -206,7 +205,10 @@ var TouchableHighlight = React.createClass({
|
|||||||
this._hideTimeout = null;
|
this._hideTimeout = null;
|
||||||
if (this._hasPressHandler() && this.refs[UNDERLAY_REF]) {
|
if (this._hasPressHandler() && this.refs[UNDERLAY_REF]) {
|
||||||
this.refs[CHILD_REF].setNativeProps(INACTIVE_CHILD_PROPS);
|
this.refs[CHILD_REF].setNativeProps(INACTIVE_CHILD_PROPS);
|
||||||
this.refs[UNDERLAY_REF].setNativeProps(this.state.underlayProps);
|
this.refs[UNDERLAY_REF].setNativeProps({
|
||||||
|
...INACTIVE_UNDERLAY_PROPS,
|
||||||
|
style: this.state.underlayStyle,
|
||||||
|
});
|
||||||
this.props.onHideUnderlay && this.props.onHideUnderlay();
|
this.props.onHideUnderlay && this.props.onHideUnderlay();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -245,7 +247,7 @@ var TouchableHighlight = React.createClass({
|
|||||||
onResponderRelease={this.touchableHandleResponderRelease}
|
onResponderRelease={this.touchableHandleResponderRelease}
|
||||||
onResponderTerminate={this.touchableHandleResponderTerminate}
|
onResponderTerminate={this.touchableHandleResponderTerminate}
|
||||||
ref={UNDERLAY_REF}
|
ref={UNDERLAY_REF}
|
||||||
style={[styles.root, this.props.style]}
|
style={this.state.underlayStyle}
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
testID={this.props.testID}>
|
testID={this.props.testID}>
|
||||||
{React.cloneElement(
|
{React.cloneElement(
|
||||||
@@ -264,6 +266,9 @@ var UNDERLAY_REF = keyOf({underlayRef: null});
|
|||||||
var INACTIVE_CHILD_PROPS = {
|
var INACTIVE_CHILD_PROPS = {
|
||||||
style: StyleSheet.create({x: {opacity: 1.0}}).x,
|
style: StyleSheet.create({x: {opacity: 1.0}}).x,
|
||||||
};
|
};
|
||||||
|
var INACTIVE_UNDERLAY_PROPS = {
|
||||||
|
style: StyleSheet.create({x: {backgroundColor: 'transparent'}}).x,
|
||||||
|
};
|
||||||
|
|
||||||
var styles = StyleSheet.create({
|
var styles = StyleSheet.create({
|
||||||
root: {
|
root: {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
// Note (avik): add @flow when Flow supports spread properties in propTypes
|
// Note (avik): add @flow when Flow supports spread properties in propTypes
|
||||||
|
|
||||||
var Animated = require('animated');
|
var Animated = require('../../apis/Animated');
|
||||||
var NativeMethodsMixin = require('../../modules/NativeMethodsMixin');
|
var NativeMethodsMixin = require('../../modules/NativeMethodsMixin');
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var StyleSheet = require('../../apis/StyleSheet');
|
var StyleSheet = require('../../apis/StyleSheet');
|
||||||
@@ -23,7 +23,7 @@ var Touchable = require('./Touchable');
|
|||||||
var TouchableWithoutFeedback = require('./TouchableWithoutFeedback');
|
var TouchableWithoutFeedback = require('./TouchableWithoutFeedback');
|
||||||
|
|
||||||
var ensurePositiveDelayProps = require('./ensurePositiveDelayProps');
|
var ensurePositiveDelayProps = require('./ensurePositiveDelayProps');
|
||||||
var flattenStyle = require('../../apis/StyleSheet/flattenStyle');
|
var flattenStyle = StyleSheet.flatten
|
||||||
|
|
||||||
type Event = Object;
|
type Event = Object;
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ var TouchableOpacity = React.createClass({
|
|||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
accessible={true}
|
accessible={this.props.accessible !== false}
|
||||||
accessibilityLabel={this.props.accessibilityLabel}
|
accessibilityLabel={this.props.accessibilityLabel}
|
||||||
accessibilityRole={this.props.accessibilityRole || 'button'}
|
accessibilityRole={this.props.accessibilityRole || 'button'}
|
||||||
style={[styles.root, this.props.style, {opacity: this.state.anim}]}
|
style={[styles.root, this.props.style, {opacity: this.state.anim}]}
|
||||||
|
|||||||
@@ -12,12 +12,13 @@
|
|||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var EdgeInsetsPropType = require('../../apis/StyleSheet/EdgeInsetsPropType');
|
var EdgeInsetsPropType = require('../../propTypes/EdgeInsetsPropType');
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var TimerMixin = require('react-timer-mixin');
|
var TimerMixin = require('react-timer-mixin');
|
||||||
var Touchable = require('./Touchable');
|
var Touchable = require('./Touchable');
|
||||||
var View = require('../View');
|
var View = require('../View');
|
||||||
var ensurePositiveDelayProps = require('./ensurePositiveDelayProps');
|
var ensurePositiveDelayProps = require('./ensurePositiveDelayProps');
|
||||||
|
var warning = require('fbjs/lib/warning');
|
||||||
|
|
||||||
type Event = Object;
|
type Event = Object;
|
||||||
|
|
||||||
@@ -32,11 +33,11 @@ var PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
|
|||||||
* >
|
* >
|
||||||
* > If you wish to have several child components, wrap them in a View.
|
* > If you wish to have several child components, wrap them in a View.
|
||||||
*/
|
*/
|
||||||
var TouchableWithoutFeedback = React.createClass({
|
const TouchableWithoutFeedback = React.createClass({
|
||||||
mixins: [TimerMixin, Touchable.Mixin],
|
mixins: [TimerMixin, Touchable.Mixin],
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
accessible: React.PropTypes.bool,
|
accessible: View.propTypes.accessible,
|
||||||
accessibilityLabel: View.propTypes.accessibilityLabel,
|
accessibilityLabel: View.propTypes.accessibilityLabel,
|
||||||
accessibilityRole: View.propTypes.accessibilityRole,
|
accessibilityRole: View.propTypes.accessibilityRole,
|
||||||
/**
|
/**
|
||||||
@@ -143,9 +144,25 @@ var TouchableWithoutFeedback = React.createClass({
|
|||||||
return this.props.delayPressOut || 0;
|
return this.props.delayPressOut || 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(): ReactElement {
|
render: function(): ReactElement<any> {
|
||||||
// Note(avik): remove dynamic typecast once Flow has been upgraded
|
// Note(avik): remove dynamic typecast once Flow has been upgraded
|
||||||
return (React: any).cloneElement(React.Children.only(this.props.children), {
|
const child = React.Children.only(this.props.children);
|
||||||
|
let children = child.props.children;
|
||||||
|
warning(
|
||||||
|
!child.type || child.type.displayName !== 'Text',
|
||||||
|
'TouchableWithoutFeedback does not work well with Text children. Wrap children in a View instead. See ' +
|
||||||
|
((child._owner && child._owner.getName && child._owner.getName()) || '<unknown>')
|
||||||
|
);
|
||||||
|
if (Touchable.TOUCH_TARGET_DEBUG && child.type && child.type.displayName === 'View') {
|
||||||
|
if (!Array.isArray(children)) {
|
||||||
|
children = [children];
|
||||||
|
}
|
||||||
|
children.push(Touchable.renderDebugView({color: 'red', hitSlop: this.props.hitSlop}));
|
||||||
|
}
|
||||||
|
const style = (Touchable.TOUCH_TARGET_DEBUG && child.type && child.type.displayName === 'Text') ?
|
||||||
|
[child.props.style, {color: 'red'}] :
|
||||||
|
child.props.style;
|
||||||
|
return (React: any).cloneElement(child, {
|
||||||
accessible: this.props.accessible !== false,
|
accessible: this.props.accessible !== false,
|
||||||
accessibilityLabel: this.props.accessibilityLabel,
|
accessibilityLabel: this.props.accessibilityLabel,
|
||||||
accessibilityRole: this.props.accessibilityRole,
|
accessibilityRole: this.props.accessibilityRole,
|
||||||
@@ -158,6 +175,8 @@ var TouchableWithoutFeedback = React.createClass({
|
|||||||
onResponderMove: this.touchableHandleResponderMove,
|
onResponderMove: this.touchableHandleResponderMove,
|
||||||
onResponderRelease: this.touchableHandleResponderRelease,
|
onResponderRelease: this.touchableHandleResponderRelease,
|
||||||
onResponderTerminate: this.touchableHandleResponderTerminate,
|
onResponderTerminate: this.touchableHandleResponderTerminate,
|
||||||
|
style,
|
||||||
|
children,
|
||||||
tabIndex: '0'
|
tabIndex: '0'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { PropTypes } from 'react'
|
import { PropTypes } from 'react'
|
||||||
import BorderPropTypes from '../../apis/StyleSheet/BorderPropTypes'
|
import BorderPropTypes from '../../propTypes/BorderPropTypes'
|
||||||
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
|
import ColorPropType from '../../propTypes/ColorPropType'
|
||||||
import LayoutPropTypes from '../../apis/StyleSheet/LayoutPropTypes'
|
import LayoutPropTypes from '../../propTypes/LayoutPropTypes'
|
||||||
import TransformPropTypes from '../../apis/StyleSheet/TransformPropTypes'
|
import TransformPropTypes from '../../propTypes/TransformPropTypes'
|
||||||
|
|
||||||
const { number, oneOf, string } = PropTypes
|
const { number, oneOf, string } = PropTypes
|
||||||
const autoOrHiddenOrVisible = oneOf([ 'auto', 'hidden', 'visible' ])
|
const autoOrHiddenOrVisible = oneOf([ 'auto', 'hidden', 'visible' ])
|
||||||
|
|||||||
@@ -1,34 +1,43 @@
|
|||||||
/* eslint-env mocha */
|
/* eslint-env mocha */
|
||||||
|
|
||||||
import * as utils from '../../../modules/specHelpers'
|
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
|
import includes from 'lodash/includes'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import View from '../'
|
import View from '../'
|
||||||
|
import { mount, shallow } from 'enzyme'
|
||||||
|
|
||||||
suite('components/View', () => {
|
suite('components/View', () => {
|
||||||
test('prop "children"', () => {
|
test('prop "children"', () => {
|
||||||
const children = 'children'
|
const children = 'children'
|
||||||
const result = utils.shallowRender(<View>{children}</View>)
|
const view = shallow(<View>{children}</View>)
|
||||||
assert.equal(result.props.children, children)
|
assert.equal(view.prop('children'), children)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('prop "onLayout"', (done) => {
|
||||||
|
mount(<View onLayout={onLayout} />)
|
||||||
|
function onLayout(e) {
|
||||||
|
const { layout } = e.nativeEvent
|
||||||
|
assert.deepEqual(layout, { x: 0, y: 0, width: 0, height: 0 })
|
||||||
|
done()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "pointerEvents"', () => {
|
test('prop "pointerEvents"', () => {
|
||||||
const result = utils.shallowRender(<View pointerEvents='box-only' />)
|
const view = shallow(<View pointerEvents='box-only' />)
|
||||||
assert.equal(result.props.className, '__style_pebo')
|
assert.ok(includes(view.prop('className'), '__style_pebo') === true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prop "style"', () => {
|
test('prop "style"', () => {
|
||||||
const noFlex = utils.shallowRender(<View />)
|
const view = shallow(<View />)
|
||||||
assert.equal(noFlex.props.style.flexShrink, 0)
|
assert.equal(view.prop('style').flexShrink, 0)
|
||||||
|
|
||||||
const flex = utils.shallowRender(<View style={{ flex: 1 }} />)
|
const flexView = shallow(<View style={{ flex: 1 }} />)
|
||||||
assert.equal(flex.props.style.flexShrink, 1)
|
assert.equal(flexView.prop('style').flexShrink, 1)
|
||||||
|
|
||||||
const flexShrink = utils.shallowRender(<View style={{ flexShrink: 1 }} />)
|
const flexShrinkView = shallow(<View style={{ flexShrink: 1 }} />)
|
||||||
assert.equal(flexShrink.props.style.flexShrink, 1)
|
assert.equal(flexShrinkView.prop('style').flexShrink, 1)
|
||||||
|
|
||||||
const flexAndShrink = utils.shallowRender(<View style={{ flex: 1, flexShrink: 2 }} />)
|
const flexAndShrinkView = shallow(<View style={{ flex: 1, flexShrink: 2 }} />)
|
||||||
assert.equal(flexAndShrink.props.style.flexShrink, 2)
|
assert.equal(flexAndShrinkView.prop('style').flexShrink, 2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
import createNativeComponent from '../../modules/createNativeComponent'
|
import applyLayout from '../../modules/applyLayout'
|
||||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
import applyNativeMethods from '../../modules/applyNativeMethods'
|
||||||
import normalizeNativeEvent from '../../apis/PanResponder/normalizeNativeEvent'
|
import createReactDOMComponent from '../../modules/createReactDOMComponent'
|
||||||
|
import EdgeInsetsPropType from '../../propTypes/EdgeInsetsPropType'
|
||||||
|
import normalizeNativeEvent from '../../modules/normalizeNativeEvent'
|
||||||
import { Component, PropTypes } from 'react'
|
import { Component, PropTypes } from 'react'
|
||||||
import StyleSheet from '../../apis/StyleSheet'
|
import StyleSheet from '../../apis/StyleSheet'
|
||||||
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
|
import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
|
||||||
import ViewStylePropTypes from './ViewStylePropTypes'
|
import ViewStylePropTypes from './ViewStylePropTypes'
|
||||||
|
|
||||||
@NativeMethodsDecorator
|
|
||||||
class View extends Component {
|
class View extends Component {
|
||||||
|
static displayName = 'View'
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accessibilityLabel: createNativeComponent.propTypes.accessibilityLabel,
|
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
|
||||||
accessibilityLiveRegion: createNativeComponent.propTypes.accessibilityLiveRegion,
|
accessibilityLiveRegion: createReactDOMComponent.propTypes.accessibilityLiveRegion,
|
||||||
accessibilityRole: createNativeComponent.propTypes.accessibilityRole,
|
accessibilityRole: createReactDOMComponent.propTypes.accessibilityRole,
|
||||||
accessible: createNativeComponent.propTypes.accessible,
|
accessible: createReactDOMComponent.propTypes.accessible,
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
|
collapsable: PropTypes.bool,
|
||||||
|
hitSlop: EdgeInsetsPropType,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
onClickCapture: PropTypes.func,
|
onClickCapture: PropTypes.func,
|
||||||
|
onLayout: PropTypes.func,
|
||||||
onMoveShouldSetResponder: PropTypes.func,
|
onMoveShouldSetResponder: PropTypes.func,
|
||||||
onMoveShouldSetResponderCapture: PropTypes.func,
|
onMoveShouldSetResponderCapture: PropTypes.func,
|
||||||
onResponderGrant: PropTypes.func,
|
onResponderGrant: PropTypes.func,
|
||||||
@@ -36,7 +42,7 @@ class View extends Component {
|
|||||||
onTouchStartCapture: PropTypes.func,
|
onTouchStartCapture: PropTypes.func,
|
||||||
pointerEvents: PropTypes.oneOf(['auto', 'box-none', 'box-only', 'none']),
|
pointerEvents: PropTypes.oneOf(['auto', 'box-none', 'box-only', 'none']),
|
||||||
style: StyleSheetPropType(ViewStylePropTypes),
|
style: StyleSheetPropType(ViewStylePropTypes),
|
||||||
testID: createNativeComponent.propTypes.testID
|
testID: createReactDOMComponent.propTypes.testID
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -51,6 +57,9 @@ class View extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
collapsable, // eslint-disable-line
|
||||||
|
hitSlop, // eslint-disable-line
|
||||||
|
onLayout, // eslint-disable-line
|
||||||
pointerEvents,
|
pointerEvents,
|
||||||
style,
|
style,
|
||||||
...other
|
...other
|
||||||
@@ -58,6 +67,8 @@ class View extends Component {
|
|||||||
|
|
||||||
const flattenedStyle = StyleSheet.flatten(style)
|
const flattenedStyle = StyleSheet.flatten(style)
|
||||||
const pointerEventsStyle = pointerEvents && { pointerEvents }
|
const pointerEventsStyle = pointerEvents && { pointerEvents }
|
||||||
|
// 'View' needs to set 'flexShrink:0' only when there is no 'flex' or 'flexShrink' style provided
|
||||||
|
const needsFlexReset = flattenedStyle.flex == null && flattenedStyle.flexShrink == null
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
...other,
|
...other,
|
||||||
@@ -74,13 +85,12 @@ class View extends Component {
|
|||||||
style: [
|
style: [
|
||||||
styles.initial,
|
styles.initial,
|
||||||
style,
|
style,
|
||||||
// 'View' needs to use 'flexShrink' in its reset when there is no 'flex' style provided
|
needsFlexReset && styles.flexReset,
|
||||||
(flattenedStyle.flex == null && flattenedStyle.flexShrink == null) && styles.flexReset,
|
|
||||||
pointerEventsStyle
|
pointerEventsStyle
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return createNativeComponent(props)
|
return createReactDOMComponent(props)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,6 +108,8 @@ class View extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyLayout(applyNativeMethods(View))
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
// https://github.com/facebook/css-layout#default-values
|
// https://github.com/facebook/css-layout#default-values
|
||||||
initial: {
|
initial: {
|
||||||
|
|||||||
21
src/index.js
21
src/index.js
@@ -1,11 +1,11 @@
|
|||||||
import './apis/PanResponder/injectResponderEventPlugin'
|
import './modules/injectResponderEventPlugin'
|
||||||
|
|
||||||
import findNodeHandle from './modules/findNodeHandle'
|
import findNodeHandle from './modules/findNodeHandle'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import ReactDOMServer from 'react-dom/server'
|
import ReactDOMServer from 'react-dom/server'
|
||||||
|
|
||||||
// apis
|
// apis
|
||||||
import Animated from 'animated'
|
import Animated from './apis/Animated'
|
||||||
import AppRegistry from './apis/AppRegistry'
|
import AppRegistry from './apis/AppRegistry'
|
||||||
import AppState from './apis/AppState'
|
import AppState from './apis/AppState'
|
||||||
import AsyncStorage from './apis/AsyncStorage'
|
import AsyncStorage from './apis/AsyncStorage'
|
||||||
@@ -23,7 +23,6 @@ import UIManager from './apis/UIManager'
|
|||||||
import ActivityIndicator from './components/ActivityIndicator'
|
import ActivityIndicator from './components/ActivityIndicator'
|
||||||
import Image from './components/Image'
|
import Image from './components/Image'
|
||||||
import ListView from './components/ListView'
|
import ListView from './components/ListView'
|
||||||
import Portal from './components/Portal'
|
|
||||||
import ScrollView from './components/ScrollView'
|
import ScrollView from './components/ScrollView'
|
||||||
import Text from './components/Text'
|
import Text from './components/Text'
|
||||||
import TextInput from './components/TextInput'
|
import TextInput from './components/TextInput'
|
||||||
@@ -39,11 +38,9 @@ import NativeModules from './modules/NativeModules'
|
|||||||
|
|
||||||
// propTypes
|
// propTypes
|
||||||
|
|
||||||
import ColorPropType from './apis/StyleSheet/ColorPropType'
|
import ColorPropType from './propTypes/ColorPropType'
|
||||||
import EdgeInsetsPropType from './apis/StyleSheet/EdgeInsetsPropType'
|
import EdgeInsetsPropType from './propTypes/EdgeInsetsPropType'
|
||||||
import PointPropType from './apis/StyleSheet/PointPropType'
|
import PointPropType from './propTypes/PointPropType'
|
||||||
|
|
||||||
Animated.inject.FlattenStyle(StyleSheet.flatten)
|
|
||||||
|
|
||||||
const ReactNative = {
|
const ReactNative = {
|
||||||
// top-level API
|
// top-level API
|
||||||
@@ -55,12 +52,7 @@ const ReactNative = {
|
|||||||
renderToString: ReactDOMServer.renderToString,
|
renderToString: ReactDOMServer.renderToString,
|
||||||
|
|
||||||
// apis
|
// apis
|
||||||
Animated: {
|
Animated,
|
||||||
...Animated,
|
|
||||||
Image: Animated.createAnimatedComponent(Image),
|
|
||||||
Text: Animated.createAnimatedComponent(Text),
|
|
||||||
View: Animated.createAnimatedComponent(View)
|
|
||||||
},
|
|
||||||
AppRegistry,
|
AppRegistry,
|
||||||
AppState,
|
AppState,
|
||||||
AsyncStorage,
|
AsyncStorage,
|
||||||
@@ -78,7 +70,6 @@ const ReactNative = {
|
|||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Image,
|
Image,
|
||||||
ListView,
|
ListView,
|
||||||
Portal,
|
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
|||||||
@@ -113,11 +113,11 @@ const NativeMethodsMixin = {
|
|||||||
* In the future, we should cleanup callbacks by cancelling them instead of
|
* In the future, we should cleanup callbacks by cancelling them instead of
|
||||||
* using this.
|
* using this.
|
||||||
*/
|
*/
|
||||||
const mountSafeCallback = (context: Component, callback: ?Function) => () => {
|
const mountSafeCallback = (context: Component, callback: ?Function) => (...args) => {
|
||||||
if (!callback || (context.isMounted && !context.isMounted())) {
|
if (!callback) {
|
||||||
return
|
return undefined
|
||||||
}
|
}
|
||||||
return callback.apply(context, arguments)
|
return callback.apply(context, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = NativeMethodsMixin
|
module.exports = NativeMethodsMixin
|
||||||
|
|||||||
45
src/modules/ReactNativePropRegistry/index.js
Normal file
45
src/modules/ReactNativePropRegistry/index.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/* 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 ReactNativePropRegistry
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const emptyObject = {};
|
||||||
|
const objects = {};
|
||||||
|
let uniqueID = 1;
|
||||||
|
|
||||||
|
class ReactNativePropRegistry {
|
||||||
|
static register(object: Object): number {
|
||||||
|
let id = ++uniqueID;
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
Object.freeze(object);
|
||||||
|
}
|
||||||
|
objects[id] = object;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getByID(id: number): Object {
|
||||||
|
if (!id) {
|
||||||
|
// Used in the style={[condition && id]} pattern,
|
||||||
|
// we want it to be a no-op when the value is false or null
|
||||||
|
return emptyObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
const object = objects[id];
|
||||||
|
if (!object) {
|
||||||
|
console.warn('Invalid style with id `' + id + '`. Skipping ...');
|
||||||
|
return emptyObject;
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ReactNativePropRegistry;
|
||||||
43
src/modules/applyLayout/index.js
Normal file
43
src/modules/applyLayout/index.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2016-present, Nicolas Gallagher.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
import emptyFunction from 'fbjs/lib/emptyFunction'
|
||||||
|
|
||||||
|
const applyLayout = (Component) => {
|
||||||
|
const componentDidMount = Component.prototype.componentDidMount || emptyFunction
|
||||||
|
const componentDidUpdate = Component.prototype.componentDidUpdate || emptyFunction
|
||||||
|
|
||||||
|
Component.prototype.componentDidMount = function () {
|
||||||
|
componentDidMount()
|
||||||
|
this._layoutState = {}
|
||||||
|
this._handleLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.prototype.componentDidUpdate = function () {
|
||||||
|
componentDidUpdate()
|
||||||
|
this._handleLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.prototype._handleLayout = function () {
|
||||||
|
const layout = this._layoutState
|
||||||
|
const { onLayout } = this.props
|
||||||
|
|
||||||
|
if (onLayout) {
|
||||||
|
this.measure((x, y, width, height) => {
|
||||||
|
if (layout.x !== x || layout.y !== y || layout.width !== width || layout.height !== height) {
|
||||||
|
const nextLayout = { x, y, width, height }
|
||||||
|
const nativeEvent = { layout: nextLayout }
|
||||||
|
onLayout({ nativeEvent })
|
||||||
|
this._layoutState = nextLayout
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Component
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = applyLayout
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import NativeMethodsMixin from '../NativeMethodsMixin'
|
import NativeMethodsMixin from '../NativeMethodsMixin'
|
||||||
|
|
||||||
const NativeMethodsDecorator = (Component) => {
|
const applyNativeMethods = (Component) => {
|
||||||
Object.keys(NativeMethodsMixin).forEach((method) => {
|
Object.keys(NativeMethodsMixin).forEach((method) => {
|
||||||
if (!Component.prototype[method]) {
|
if (!Component.prototype[method]) {
|
||||||
Component.prototype[method] = NativeMethodsMixin[method]
|
Component.prototype[method] = NativeMethodsMixin[method]
|
||||||
@@ -16,4 +16,4 @@ const NativeMethodsDecorator = (Component) => {
|
|||||||
return Component
|
return Component
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = NativeMethodsDecorator
|
module.exports = applyNativeMethods
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/* eslint-env mocha */
|
|
||||||
|
|
||||||
import * as utils from '../../specHelpers'
|
|
||||||
import assert from 'assert'
|
|
||||||
|
|
||||||
import createNativeComponent from '../'
|
|
||||||
|
|
||||||
suite('modules/createNativeComponent', () => {
|
|
||||||
test('prop "accessibilityLabel"', () => {
|
|
||||||
const accessibilityLabel = 'accessibilityLabel'
|
|
||||||
const dom = utils.renderToDOM(createNativeComponent({ accessibilityLabel }))
|
|
||||||
assert.equal(dom.getAttribute('aria-label'), accessibilityLabel)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('prop "accessibilityLiveRegion"', () => {
|
|
||||||
const accessibilityLiveRegion = 'polite'
|
|
||||||
const dom = utils.renderToDOM(createNativeComponent({ accessibilityLiveRegion }))
|
|
||||||
assert.equal(dom.getAttribute('aria-live'), accessibilityLiveRegion)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('prop "accessibilityRole"', () => {
|
|
||||||
const accessibilityRole = 'banner'
|
|
||||||
let dom = utils.renderToDOM(createNativeComponent({ accessibilityRole }))
|
|
||||||
assert.equal(dom.getAttribute('role'), accessibilityRole)
|
|
||||||
assert.equal((dom.tagName).toLowerCase(), 'header')
|
|
||||||
|
|
||||||
const button = 'button'
|
|
||||||
dom = utils.renderToDOM(createNativeComponent({ accessibilityRole: 'button' }))
|
|
||||||
assert.equal(dom.getAttribute('type'), button)
|
|
||||||
assert.equal((dom.tagName).toLowerCase(), button)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('prop "accessible"', () => {
|
|
||||||
// accessible (implicit)
|
|
||||||
let dom = utils.renderToDOM(createNativeComponent({}))
|
|
||||||
assert.equal(dom.getAttribute('aria-hidden'), null)
|
|
||||||
// accessible (explicit)
|
|
||||||
dom = utils.renderToDOM(createNativeComponent({ accessible: true }))
|
|
||||||
assert.equal(dom.getAttribute('aria-hidden'), null)
|
|
||||||
// not accessible
|
|
||||||
dom = utils.renderToDOM(createNativeComponent({ accessible: false }))
|
|
||||||
assert.equal(dom.getAttribute('aria-hidden'), 'true')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('prop "component"', () => {
|
|
||||||
const component = 'main'
|
|
||||||
const dom = utils.renderToDOM(createNativeComponent({ component }))
|
|
||||||
const tagName = (dom.tagName).toLowerCase()
|
|
||||||
assert.equal(tagName, component)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('prop "testID"', () => {
|
|
||||||
// no testID
|
|
||||||
let dom = utils.renderToDOM(createNativeComponent({}))
|
|
||||||
assert.equal(dom.getAttribute('data-testid'), null)
|
|
||||||
// with testID
|
|
||||||
const testID = 'Example.testID'
|
|
||||||
dom = utils.renderToDOM(createNativeComponent({ testID }))
|
|
||||||
assert.equal(dom.getAttribute('data-testid'), testID)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
59
src/modules/createReactDOMComponent/__tests__/index-test.js
Normal file
59
src/modules/createReactDOMComponent/__tests__/index-test.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/* eslint-env mocha */
|
||||||
|
|
||||||
|
import assert from 'assert'
|
||||||
|
import createReactDOMComponent from '..'
|
||||||
|
import { shallow } from 'enzyme'
|
||||||
|
|
||||||
|
suite('modules/createReactDOMComponent', () => {
|
||||||
|
test('prop "accessibilityLabel"', () => {
|
||||||
|
const accessibilityLabel = 'accessibilityLabel'
|
||||||
|
const element = shallow(createReactDOMComponent({ accessibilityLabel }))
|
||||||
|
assert.equal(element.prop('aria-label'), accessibilityLabel)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('prop "accessibilityLiveRegion"', () => {
|
||||||
|
const accessibilityLiveRegion = 'polite'
|
||||||
|
const element = shallow(createReactDOMComponent({ accessibilityLiveRegion }))
|
||||||
|
assert.equal(element.prop('aria-live'), accessibilityLiveRegion)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('prop "accessibilityRole"', () => {
|
||||||
|
const accessibilityRole = 'banner'
|
||||||
|
let element = shallow(createReactDOMComponent({ accessibilityRole }))
|
||||||
|
assert.equal(element.prop('role'), accessibilityRole)
|
||||||
|
assert.equal(element.is('header'), true)
|
||||||
|
|
||||||
|
const button = 'button'
|
||||||
|
element = shallow(createReactDOMComponent({ accessibilityRole: 'button' }))
|
||||||
|
assert.equal(element.prop('type'), button)
|
||||||
|
assert.equal(element.is('button'), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('prop "accessible"', () => {
|
||||||
|
// accessible (implicit)
|
||||||
|
let element = shallow(createReactDOMComponent({}))
|
||||||
|
assert.equal(element.prop('aria-hidden'), null)
|
||||||
|
// accessible (explicit)
|
||||||
|
element = shallow(createReactDOMComponent({ accessible: true }))
|
||||||
|
assert.equal(element.prop('aria-hidden'), null)
|
||||||
|
// not accessible
|
||||||
|
element = shallow(createReactDOMComponent({ accessible: false }))
|
||||||
|
assert.equal(element.prop('aria-hidden'), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('prop "component"', () => {
|
||||||
|
const component = 'main'
|
||||||
|
const element = shallow(createReactDOMComponent({ component }))
|
||||||
|
assert.equal(element.is('main'), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('prop "testID"', () => {
|
||||||
|
// no testID
|
||||||
|
let element = shallow(createReactDOMComponent({}))
|
||||||
|
assert.equal(element.prop('data-testid'), null)
|
||||||
|
// with testID
|
||||||
|
const testID = 'Example.testID'
|
||||||
|
element = shallow(createReactDOMComponent({ testID }))
|
||||||
|
assert.equal(element.prop('data-testid'), testID)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -17,7 +17,7 @@ const roleComponents = {
|
|||||||
region: 'section'
|
region: 'section'
|
||||||
}
|
}
|
||||||
|
|
||||||
const createNativeComponent = ({
|
const createReactDOMComponent = ({
|
||||||
accessibilityLabel,
|
accessibilityLabel,
|
||||||
accessibilityLiveRegion,
|
accessibilityLiveRegion,
|
||||||
accessibilityRole,
|
accessibilityRole,
|
||||||
@@ -27,7 +27,8 @@ const createNativeComponent = ({
|
|||||||
type,
|
type,
|
||||||
...other
|
...other
|
||||||
}) => {
|
}) => {
|
||||||
const Component = accessibilityRole && roleComponents[accessibilityRole] ? roleComponents[accessibilityRole] : component
|
const role = accessibilityRole
|
||||||
|
const Component = role && roleComponents[role] ? roleComponents[role] : component
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
@@ -37,13 +38,13 @@ const createNativeComponent = ({
|
|||||||
aria-label={accessibilityLabel}
|
aria-label={accessibilityLabel}
|
||||||
aria-live={accessibilityLiveRegion}
|
aria-live={accessibilityLiveRegion}
|
||||||
data-testid={testID}
|
data-testid={testID}
|
||||||
role={accessibilityRole}
|
role={role}
|
||||||
type={accessibilityRole === 'button' ? 'button' : type}
|
type={role === 'button' ? 'button' : type}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
createNativeComponent.propTypes = {
|
createReactDOMComponent.propTypes = {
|
||||||
accessibilityLabel: PropTypes.string,
|
accessibilityLabel: PropTypes.string,
|
||||||
accessibilityLiveRegion: PropTypes.oneOf([ 'assertive', 'off', 'polite' ]),
|
accessibilityLiveRegion: PropTypes.oneOf([ 'assertive', 'off', 'polite' ]),
|
||||||
accessibilityRole: PropTypes.string,
|
accessibilityRole: PropTypes.string,
|
||||||
@@ -54,4 +55,4 @@ createNativeComponent.propTypes = {
|
|||||||
type: PropTypes.string
|
type: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = createNativeComponent
|
module.exports = createReactDOMComponent
|
||||||
@@ -10,9 +10,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import flattenStyle from '../flattenStyle'
|
import flattenStyle from '..'
|
||||||
|
|
||||||
suite('apis/StyleSheet/flattenStyle', () => {
|
suite('modules/flattenStyle', () => {
|
||||||
test('should merge style objects', () => {
|
test('should merge style objects', () => {
|
||||||
const style1 = {opacity: 1}
|
const style1 = {opacity: 1}
|
||||||
const style2 = {order: 2}
|
const style2 = {order: 2}
|
||||||
47
src/modules/flattenStyle/index.js
Normal file
47
src/modules/flattenStyle/index.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/* 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 flattenStyle
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var ReactNativePropRegistry = require('../ReactNativePropRegistry');
|
||||||
|
var invariant = require('fbjs/lib/invariant');
|
||||||
|
|
||||||
|
function getStyle(style) {
|
||||||
|
if (typeof style === 'number') {
|
||||||
|
return ReactNativePropRegistry.getByID(style);
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenStyle(style: ?StyleObj): ?Object {
|
||||||
|
if (!style) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
invariant(style !== true, 'style may be false but not true');
|
||||||
|
|
||||||
|
if (!Array.isArray(style)) {
|
||||||
|
return getStyle(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = {};
|
||||||
|
for (var i = 0, styleLength = style.length; i < styleLength; ++i) {
|
||||||
|
var computedStyle = flattenStyle(style[i]);
|
||||||
|
if (computedStyle) {
|
||||||
|
for (var key in computedStyle) {
|
||||||
|
result[key] = computedStyle[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = flattenStyle;
|
||||||
@@ -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,5 +1,5 @@
|
|||||||
import { PropTypes } from 'react'
|
import { PropTypes } from 'react'
|
||||||
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
|
import ColorPropType from './ColorPropType'
|
||||||
|
|
||||||
const numberOrString = PropTypes.oneOfType([ PropTypes.number, PropTypes.string ])
|
const numberOrString = PropTypes.oneOfType([ PropTypes.number, PropTypes.string ])
|
||||||
const BorderStylePropType = PropTypes.oneOf([ 'solid', 'dotted', 'dashed' ])
|
const BorderStylePropType = PropTypes.oneOf([ 'solid', 'dotted', 'dashed' ])
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
import { PropTypes } from 'react'
|
import { PropTypes } from 'react'
|
||||||
import ReactPropTypeLocationNames from 'react/lib/ReactPropTypeLocationNames'
|
import ReactPropTypeLocationNames from 'react/lib/ReactPropTypeLocationNames'
|
||||||
|
|
||||||
var normalizeColor = require('./normalizeColor');
|
var normalizeColor = require('../modules/normalizeColor');
|
||||||
|
|
||||||
var colorPropType = function(isRequired, props, propName, componentName, location, propFullName) {
|
var colorPropType = function(isRequired, props, propName, componentName, location, propFullName) {
|
||||||
var color = props[propName];
|
var color = props[propName];
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import createStrictShapeTypeChecker from './createStrictShapeTypeChecker'
|
import createStrictShapeTypeChecker from './createStrictShapeTypeChecker'
|
||||||
import flattenStyle from './flattenStyle'
|
import flattenStyle from '../modules/flattenStyle'
|
||||||
|
|
||||||
module.exports = function StyleSheetPropType(shape) {
|
module.exports = function StyleSheetPropType(shape) {
|
||||||
const shapePropType = createStrictShapeTypeChecker(shape)
|
const shapePropType = createStrictShapeTypeChecker(shape)
|
||||||
50
src/propTypes/TransformPropTypes.js
Normal file
50
src/propTypes/TransformPropTypes.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PropTypes } from 'react'
|
||||||
|
|
||||||
|
const { arrayOf, number, oneOfType, shape, string } = PropTypes
|
||||||
|
const ArrayOfNumberPropType = arrayOf(number)
|
||||||
|
const numberOrString = oneOfType([ number, string ])
|
||||||
|
|
||||||
|
const TransformMatrixPropType = function (
|
||||||
|
props : Object,
|
||||||
|
propName : string,
|
||||||
|
componentName : string
|
||||||
|
) : ?Error {
|
||||||
|
if (props.transform && props.transformMatrix) {
|
||||||
|
return new Error(
|
||||||
|
'transformMatrix and transform styles cannot be used on the same ' +
|
||||||
|
'component'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ArrayOfNumberPropType(props, propName, componentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransformPropTypes = {
|
||||||
|
transform: arrayOf(
|
||||||
|
oneOfType([
|
||||||
|
shape({ perspective: numberOrString }),
|
||||||
|
shape({ rotate: string }),
|
||||||
|
shape({ rotateX: string }),
|
||||||
|
shape({ rotateY: string }),
|
||||||
|
shape({ rotateZ: string }),
|
||||||
|
shape({ scale: number }),
|
||||||
|
shape({ scaleX: number }),
|
||||||
|
shape({ scaleY: number }),
|
||||||
|
shape({ skewX: string }),
|
||||||
|
shape({ skewY: string }),
|
||||||
|
shape({ translateX: numberOrString }),
|
||||||
|
shape({ translateY: numberOrString }),
|
||||||
|
shape({ translateZ: numberOrString }),
|
||||||
|
shape({ translate3d: string })
|
||||||
|
])
|
||||||
|
),
|
||||||
|
transformMatrix: TransformMatrixPropType
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TransformPropTypes
|
||||||
Reference in New Issue
Block a user