Compare commits

...

40 Commits

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

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

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

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

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

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

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

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

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

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

Fix #149
2016-07-05 13:49:37 -07:00
Nicolas Gallagher
03d413bca4 0.0.28 2016-07-05 11:33:29 -07:00
IjzerenHein
aef5efbad3 [add] basic ListView component
Close #87
2016-07-05 11:33:02 -07:00
Nicolas Gallagher
8fb8645723 Use 'enzyme' for 'View' tests 2016-07-05 11:33:02 -07:00
Cesar Andreu
d69406b4b1 Add an API to wrap and initialize animated (#159) 2016-07-03 11:00:50 -07:00
Nicolas Gallagher
2c2a96a183 update rendering docs 2016-06-29 17:42:06 -07:00
Nicolas Gallagher
b4a3053b5b fix README install command 2016-06-29 17:00:50 -07:00
87 changed files with 1492 additions and 1229 deletions

View File

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

View File

@@ -30,7 +30,7 @@ styles defined in JavaScript into "Atomic CSS".
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.
@@ -102,7 +102,6 @@ Exported modules:
* [`ActivityIndicator`](docs/components/ActivityIndicator.md)
* [`Image`](docs/components/Image.md)
* [`ListView`](docs/components/ListView.md)
* [`Portal`](docs/components/Portal.md)
* [`ScrollView`](docs/components/ScrollView.md)
* [`Text`](docs/components/Text.md)
* [`TextInput`](docs/components/TextInput.md)

View File

@@ -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.
**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
**absoluteFill**: number
A very common pattern is to create overlays with position absolute and zero positioning,
so `absoluteFill` can be used for convenience and to reduce duplication of these repeated
styles.
```js
<View style={StyleSheet.absoluteFill} />
```
**absoluteFillObject**: object
Sometimes you may want `absoluteFill` but with a couple tweaks - `absoluteFillObject` can be
used to create a customized entry in a `StyleSheet`, e.g.:
```js
const styles = StyleSheet.create({
wrapper: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'transparent',
top: 10
}
})
```
**hairlineWidth**: number
## Example
```js
<View style={styles.container}>
<Text
children={'Title text'}
style={[
styles.title,
this.props.isActive && styles.activeTitle
]}
/>
</View>
const styles = StyleSheet.create({
container: {
borderRadius: 4,
@@ -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>
```

View File

@@ -31,7 +31,8 @@ Invoked on load error with `{nativeEvent: {error}}`.
**onLayout**: function
TODO
Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width,
height } } }`, where `x` and `y` are the offsets from the parent node.
**onLoad**: function
@@ -57,7 +58,7 @@ could be an http address or a base64 encoded image.
**style**: style
+ ...[View#style](View.md)
+ ...[View#style](./View.md)
+ `resizeMode`
**testID**: string

View File

@@ -4,6 +4,8 @@ TODO
## Props
[...ScrollView props](./ScrollView.md)
**children**: any
Content to display over the image.

View File

@@ -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>
)
}
}
```

View File

@@ -29,8 +29,6 @@ Determines whether the keyboard gets dismissed in response to a scroll drag.
**onContentSizeChange**: function
TODO
Called when scrollable content view of the `ScrollView` changes. It's
implemented using the `onLayout` handler attached to the content container
which this `ScrollView` renders.

View File

@@ -45,6 +45,11 @@ Child content.
Truncates the text with an ellipsis after this many lines. Currently only supports `1`.
**onLayout**: function
Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width,
height } } }`, where `x` and `y` are the offsets from the parent node.
**onPress**: function
This function is called on press.

View File

@@ -14,16 +14,11 @@ Unsupported React Native props:
`enablesReturnKeyAutomatically` (ios),
`returnKeyType` (ios),
`selectionState` (ios),
`textAlign` (android),
`textAlignVertical` (android),
`underlineColorAndroid` (android)
## Props
(web) **accessibilityLabel**: string
Defines the text label available to assistive technologies upon interaction
with the element. (This is implemented using `aria-label`.)
[...View props](./View.md)
(web) **autoComplete**: bool = false
@@ -92,10 +87,6 @@ as an argument to the callback handler.
Callback that is called when the text input is focused.
**onLayout**: function
TODO
(web) **onSelectionChange**: function
Callback that is called when the text input's selection changes. The following
@@ -132,7 +123,7 @@ If `true`, all text will automatically be selected on focus.
**style**: style
+ ...[Text#style](Text.md)
+ ...[Text#style](./Text.md)
+ `outline`
**testID**: string

View File

@@ -9,6 +9,8 @@ several child components, wrap them in a View.
## Props
[...View props](./View.md)
**accessibilityLabel**: string
Overrides the text that's read by the screen reader when the user interacts
@@ -22,6 +24,8 @@ Allows assistive technologies to present and support interaction with the view
When `false`, the view is hidden from screenreaders.
**children**: View
**delayLongPress**: number
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
Invoked on mount and layout changes with.
`{nativeEvent: {layout: {x, y, width, height}}}`
Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width,
height } } }`, where `x` and `y` are the offsets from the parent node.
**onLongPress**: function

View File

@@ -48,7 +48,8 @@ implemented using `aria-hidden`.)
**onLayout**: function
(TODO)
Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width,
height } } }`, where `x` and `y` are the offsets from the parent node.
**onMoveShouldSetResponder**: function

View File

@@ -21,80 +21,38 @@ module.exports = {
Rendering without using the `AppRegistry`:
```js
import React from 'react'
import ReactNative from 'react-native'
// component that renders the app
const AppHeaderContainer = (props) => { /* ... */ }
// DOM render
ReactNative.render(<div />, document.getElementById('react-app'))
ReactNative.render(<AppHeaderContainer />, document.getElementById('react-app-header'))
// Server render
ReactNative.renderToString(<div />)
ReactNative.renderToStaticMarkup(<div />)
ReactNative.renderToString(<AppHeaderContainer />)
ReactNative.renderToStaticMarkup(<AppHeaderContainer />)
```
Rendering using the `AppRegistry`:
```js
// App.js
import React from 'react'
import ReactNative, { AppRegistry } from 'react-native'
// component that renders the app
const AppContainer = (props) => { /* ... */ }
export default AppContainer
```
```js
// client.js
// register the app
AppRegistry.registerComponent('App', () => AppContainer)
import App from './App'
import { AppRegistry } from 'react-native'
// registers the app
AppRegistry.registerComponent('App', () => App)
// mounts and runs the app within the `rootTag` DOM node
// DOM render
AppRegistry.runApplication('App', {
initialProps: {},
rootTag: document.getElementById('react-app')
})
```
React Native for Web extends `AppRegistry` to provide support for server-side
rendering.
```js
// AppShell.js
import React from 'react'
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} />)
// prerender the app
const { html, styleElement } = AppRegistry.prerenderApplication('App', { initialProps })
```

View File

@@ -33,6 +33,7 @@ export default class App extends React.Component {
<Heading size='large'>Image</Heading>
<Image
onLayout={(e) => { console.log(e.nativeEvent.layout) }}
accessibilityLabel='accessible image'
children={<Text>Inner content</Text>}
defaultSource={{
@@ -57,6 +58,7 @@ export default class App extends React.Component {
<Heading size='large'>Text</Heading>
<Text
onPress={(e) => { console.log('Text.onPress', e) }}
onLayout={(e) => { console.log(e.nativeEvent.layout) }}
testID={'Example.text'}
>
PRESS ME.

View File

@@ -20,24 +20,24 @@ export default class GridView extends Component {
render() {
const { alley, children, gutter, style, ...other } = this.props
const rootStyle = {
...style,
...styles.root
}
const rootStyle = [
style,
styles.root
]
const contentContainerStyle = {
...styles.contentContainer,
marginHorizontal: `calc(-0.5 * ${alley})`,
paddingHorizontal: `${gutter}`
}
const contentContainerStyle = [
styles.contentContainer,
{ marginHorizontal: `calc(-0.5 * ${alley})` },
{ paddingHorizontal: `${gutter}` }
]
const newChildren = React.Children.map(children, (child) => {
return child && React.cloneElement(child, {
style: {
...child.props.style,
...styles.column,
marginHorizontal: `calc(0.5 * ${alley})`
}
style: [
child.props.style,
styles.column,
{ marginHorizontal: `calc(0.5 * ${alley})` }
]
})
})

View File

@@ -5,7 +5,7 @@ const Heading = ({ children, size = 'normal' }) => (
<Text
accessibilityRole='heading'
children={children}
style={{ ...styles.root, ...sizeStyles[size] }}
style={[ styles.root, sizeStyles[size] ]}
/>
)

View File

@@ -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 Game2048 from './2048/Game2048'
import TicTacToeApp from './TicTacToe/TicTacToe'
const rootTag = document.getElementById('react-root')
AppRegistry.registerComponent('App', () => App)
AppRegistry.runApplication('App', {
rootTag: document.getElementById('react-root')
})
AppRegistry.runApplication('App', { rootTag })
// ReactNative.render(<App />, rootTag)

View File

@@ -1,13 +1,13 @@
{
"name": "react-native-web",
"version": "0.0.26",
"version": "0.0.35",
"description": "React Native for Web",
"main": "dist/index.js",
"files": [
"dist"
],
"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",
"examples": "webpack-dev-server --config examples/webpack.config.js --inline --hot --colors --quiet",
"lint": "eslint src",
@@ -19,7 +19,7 @@
"animated": "^0.1.3",
"babel-runtime": "^6.9.2",
"fbjs": "^0.8.1",
"inline-style-prefix-all": "^2.0.2",
"inline-style-prefixer": "^2.0.0",
"lodash": "^4.13.1",
"react-dom": "^15.1.0",
"react-textarea-autosize": "^4.0.2",
@@ -27,13 +27,10 @@
},
"devDependencies": {
"babel-cli": "^6.10.1",
"babel-core": "^6.9.1",
"babel-eslint": "^6.0.4",
"babel-core": "^6.10.4",
"babel-eslint": "^6.1.0",
"babel-loader": "^6.2.4",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-1": "^6.5.0",
"babel-preset-react-native": "^1.9.0",
"del-cli": "^0.2.0",
"enzyme": "^2.3.0",
"eslint": "^2.12.0",
@@ -46,14 +43,14 @@
"karma-browserstack-launcher": "^1.0.1",
"karma-chrome-launcher": "^1.0.1",
"karma-firefox-launcher": "^1.0.0",
"karma-mocha": "^1.0.1",
"karma-mocha": "^1.1.1",
"karma-mocha-reporter": "^2.0.4",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^1.7.0",
"mocha": "^2.5.3",
"node-libs-browser": "^0.5.3",
"react": "^15.1.0",
"react-addons-test-utils": "^15.1.0",
"react": "^15.2.0",
"react-addons-test-utils": "^15.2.0",
"webpack": "^1.13.1",
"webpack-dev-server": "^1.14.1"
},

View 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)
}

View File

@@ -1,6 +1,4 @@
import Portal from '../../components/Portal'
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import StyleSheet from '../StyleSheet'
import View from '../../components/View'
@@ -16,25 +14,15 @@ class ReactNativeApp extends Component {
return (
<View style={styles.appContainer}>
<RootComponent {...initialProps} ref={this._createRootRef} rootTag={rootTag} />
<Portal onModalVisibilityChanged={this._handleModalVisibilityChange} />
<RootComponent {...initialProps} rootTag={rootTag} />
</View>
)
}
_createRootRef = (component) => {
this._root = component
}
_handleModalVisibilityChange = (modalVisible) => {
ReactDOM.findDOMNode(this._root).setAttribute('aria-hidden', `${modalVisible}`)
}
}
const styles = StyleSheet.create({
/**
* Ensure that the application covers the whole screen. This prevents the
* Portal content from being clipped.
* Ensure that the application covers the whole screen.
*/
appContainer: {
position: 'absolute',

View File

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

View File

@@ -13,17 +13,9 @@ import ReactDOMServer from 'react-dom/server'
import ReactNativeApp from './ReactNativeApp'
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) {
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 = (
<ReactNativeApp
initialProps={initialProps}
@@ -42,7 +34,6 @@ export function prerenderApplication(RootComponent: Component, initialProps: Obj
/>
)
const html = ReactDOMServer.renderToString(component)
const style = renderStyleSheetToString()
const styleElement = styleAsElement(style)
return { html, style, styleElement }
const styleElement = StyleSheet.render()
return { html, styleElement }
}

View File

@@ -6,7 +6,7 @@
"use strict";
import normalizeNativeEvent from './normalizeNativeEvent';
import normalizeNativeEvent from '../../modules/normalizeNativeEvent';
var TouchHistoryMath = require('./TouchHistoryMath');
var currentCentroidXOfTouchesChangedAfter =

View File

@@ -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

View File

@@ -10,7 +10,7 @@ import { PropTypes } from 'react'
import ImageStylePropTypes from '../../components/Image/ImageStylePropTypes'
import TextStylePropTypes from '../../components/Text/TextStylePropTypes'
import ViewStylePropTypes from '../../components/View/ViewStylePropTypes'
import invariant from 'fbjs/lib/invariant'
import warning from 'fbjs/lib/warning'
class StyleSheetValidation {
static validateStyleProp(prop, style, caller) {
@@ -19,10 +19,11 @@ class StyleSheetValidation {
const message1 = `"${prop}" is not a valid style property.`
const message2 = '\nValid style props: ' + JSON.stringify(Object.keys(allStylePropTypes).sort(), null, ' ')
styleError(message1, style, caller, message2)
}
const error = allStylePropTypes[prop](style, prop, caller, 'prop')
if (error) {
styleError(error.message, style, caller)
} else {
const error = allStylePropTypes[prop](style, prop, caller, 'prop')
if (error) {
styleError(error.message, style, caller)
}
}
}
}
@@ -43,7 +44,7 @@ class StyleSheetValidation {
}
const styleError = (message1, style, caller, message2) => {
invariant(
warning(
false,
message1 + '\n' + (caller || '<<unknown>>') + ': ' +
JSON.stringify(style, null, ' ') + (message2 || '')

View File

@@ -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

View File

@@ -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)
})
})

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
/* eslint-env mocha */
import assert from 'assert'
import hyphenate from '../hyphenate'
suite('apis/StyleSheet/hyphenate', () => {
test('style property', () => {
assert.equal(hyphenate('alignItems'), 'align-items')
assert.equal(hyphenate('color'), 'color')
})
test('vendor prefixed style property', () => {
assert.equal(hyphenate('MozTransition'), '-moz-transition')
assert.equal(hyphenate('msTransition'), '-ms-transition')
assert.equal(hyphenate('WebkitTransition'), '-webkit-transition')
})
})

View File

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

View File

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

View File

@@ -8,13 +8,14 @@ suite('apis/StyleSheet/processTransform', () => {
const style = {
transform: [
{ scaleX: 20 },
{ translateX: 20 },
{ rotate: '20deg' }
]
}
assert.deepEqual(
processTransform(style),
{ transform: 'scaleX(20) rotate(20deg)' }
{ transform: 'scaleX(20) translateX(20px) rotate(20deg)' }
)
})

View File

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

View File

@@ -1,6 +1,18 @@
/**
* The browser implements the CSS cascade, where the order of properties is a
* factor in determining which styles to paint. React Native is different in
* giving precedence to the more specific styles. For example, the value of
* `paddingTop` takes precedence over that of `padding`.
*
* This module creates mutally exclusive style declarations by expanding all of
* React Native's supported shortform properties (e.g. `padding`) to their
* longfrom equivalents.
*/
import normalizeValue from './normalizeValue'
const styleShortHands = {
const emptyObject = {}
const styleShortFormProperties = {
borderColor: [ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor' ],
borderRadius: [ 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius' ],
borderStyle: [ 'borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle' ],
@@ -16,50 +28,46 @@ const styleShortHands = {
writingDirection: [ 'direction' ]
}
/**
* Alpha-sort properties, apart from shorthands they must appear before the
* longhand properties that they expand into. This lets more specific styles
* override less specific styles, whatever the order in which they were
* originally declared.
*/
const sortProps = (propsArray) => propsArray.sort((a, b) => {
const expandedA = styleShortHands[a]
const expandedB = styleShortHands[b]
if (expandedA && expandedA.indexOf(b) > -1) {
return -1
} else if (expandedB && expandedB.indexOf(a) > -1) {
return 1
}
return a < b ? -1 : a > b ? 1 : 0
const alphaSort = (arr) => arr.sort((a, b) => {
if (a < b) { return -1 }
if (a > b) { return 1 }
return 0
})
/**
* Expand the shorthand properties to isolate every declaration from the others.
*/
const expandStyle = (style) => {
const propsArray = Object.keys(style)
const sortedProps = sortProps(propsArray)
const createStyleReducer = (originalStyle) => {
const originalStyleProps = Object.keys(originalStyle)
return sortedProps.reduce((resolvedStyle, key) => {
const expandedProps = styleShortHands[key]
const value = normalizeValue(key, style[key])
return (style, prop) => {
const value = normalizeValue(prop, originalStyle[prop])
const longFormProperties = styleShortFormProperties[prop]
// React Native treats `flex:1` like `flex:1 1 auto`
if (key === 'flex') {
resolvedStyle.flexGrow = value
resolvedStyle.flexShrink = 1
resolvedStyle.flexBasis = 'auto'
} else if (key === 'textAlignVertical') {
resolvedStyle.verticalAlign = (value === 'center' ? 'middle' : value)
} else if (expandedProps) {
expandedProps.forEach((prop, i) => {
resolvedStyle[expandedProps[i]] = value
if (prop === 'flex') {
style.flexGrow = value
style.flexShrink = 1
style.flexBasis = 'auto'
// React Native accepts 'center' as a value
} else if (prop === 'textAlignVertical') {
style.verticalAlign = (value === 'center' ? 'middle' : value)
} else if (longFormProperties) {
longFormProperties.forEach((longForm, i) => {
// the value of any longform property in the original styles takes
// precedence over the shortform's value
if (originalStyleProps.indexOf(longForm) === -1) {
style[longForm] = value
}
})
} else {
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

View File

@@ -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
}

View File

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

View File

@@ -1,75 +1,76 @@
import { resetCSS, predefinedCSS } from './predefs'
import flattenStyle from './flattenStyle'
import StyleSheetRegistry from './StyleSheetRegistry'
import createReactStyleObject from './createReactStyleObject'
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
import flattenStyle from '../../modules/flattenStyle'
import React from 'react'
import ReactNativePropRegistry from '../../modules/ReactNativePropRegistry'
import StyleSheetValidation from './StyleSheetValidation'
import { defaultStyles, mapStyleToClassName } from './predefs'
const ELEMENT_ID = 'react-stylesheet'
let isRendered = false
let lastStyleSheet = ''
let styleElement
const STYLE_SHEET_ID = '__react-native-style'
/**
* Initialize the store with pointer-event styles mapping to our custom pointer
* event classes
*/
/**
* Destroy existing styles
*/
const _destroy = () => {
isRendered = false
StyleSheetRegistry._reset()
const _injectStyleSheet = () => {
// check if the server rendered the style sheet
styleElement = document.getElementById(STYLE_SHEET_ID)
// if not, inject the style sheet
if (!styleElement) { document.head.insertAdjacentHTML('afterbegin', renderToString()) }
isRendered = true
}
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 => {
for (const key in styles) {
StyleSheetValidation.validateStyle(key, styles)
StyleSheetRegistry.registerStyle(styles[key])
if (!isRendered && ExecutionEnvironment.canUseDOM) {
_injectStyleSheet()
}
// update the style sheet in place
if (isRendered) {
const stylesheet = document.getElementById(ELEMENT_ID)
if (stylesheet) {
const newStyleSheet = renderToString()
if (lastStyleSheet !== newStyleSheet) {
stylesheet.textContent = newStyleSheet
lastStyleSheet = newStyleSheet
}
} else if (process.env.NODE_ENV !== 'production') {
console.error(`ReactNative: cannot find "${ELEMENT_ID}" element`)
const result = {}
for (let key in styles) {
StyleSheetValidation.validateStyle(key, styles)
result[key] = ReactNativePropRegistry.register(styles[key])
}
return result
}
const render = () => <style dangerouslySetInnerHTML={{ __html: defaultStyles }} id={STYLE_SHEET_ID} />
const renderToString = () => `<style id="${STYLE_SHEET_ID}">${defaultStyles}</style>`
/**
* Accepts React props and converts style declarations to classNames when necessary
*/
const resolve = (props) => {
let className = props.className || ''
let style = createReactStyleObject(props.style)
for (const prop in style) {
const value = style[prop]
const replacementClassName = mapStyleToClassName(prop, value)
if (replacementClassName) {
className += ` ${replacementClassName}`
// delete style[prop]
}
}
return styles
}
/**
* 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
}
return { className, style }
}
module.exports = {
_destroy,
_reset,
absoluteFill,
absoluteFillObject,
create,
elementId: ELEMENT_ID,
hairlineWidth: 1,
flatten: flattenStyle,
renderToString,
/* @platform web */
render,
/* @platform web */
resolve
}

View File

@@ -19,7 +19,12 @@ const unitlessNumbers = {
fillOpacity: true,
strokeDashoffset: true,
strokeOpacity: true,
strokeWidth: true
strokeWidth: true,
// transform types
scale: true,
scaleX: true,
scaleY: true,
scaleZ: true
}
const normalizeValue = (property, value) => {

View File

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

View File

@@ -1,7 +1,11 @@
import normalizeValue from './normalizeValue'
// { scale: 2 } => 'scale(2)'
// { translateX: 20 } => 'translateX(20px)'
const mapTransform = (transform) => {
var key = Object.keys(transform)[0]
return `${key}(${transform[key]})`
const type = Object.keys(transform)[0]
const value = normalizeValue(type, transform[type])
return `${type}(${value})`
}
// [1,2,3,4,5,6] => 'matrix3d(1,2,3,4,5,6)'

View File

@@ -109,11 +109,11 @@ suite('apis/UIManager', () => {
assert.equal(node.getAttribute('class'), 'existing extra')
})
test('adds new style to existing style', () => {
test('adds correct DOM styles to existing style', () => {
const node = createNode({ color: 'red' })
const props = { style: { opacity: 0 } }
const props = { style: { marginVertical: 0, opacity: 0 } }
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', () => {

View File

@@ -1,6 +1,5 @@
import createReactStyleObject from '../StyleSheet/createReactStyleObject'
import CSSPropertyOperations from 'react/lib/CSSPropertyOperations'
import flattenStyle from '../StyleSheet/flattenStyle'
import processTransform from '../StyleSheet/processTransform'
const _measureLayout = (node, relativeToNativeNode, callback) => {
const relativeNode = relativeToNativeNode || node.parentNode
@@ -34,9 +33,8 @@ const UIManager = {
_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) {
let nativeProp
const value = props[prop]
switch (prop) {
@@ -44,17 +42,18 @@ const UIManager = {
// convert styles to DOM-styles
CSSPropertyOperations.setValueForStyles(
node,
processTransform(flattenStyle(value)),
createReactStyleObject(value),
component._reactInternalInstance
)
break
case 'class':
case 'className':
nativeProp = 'class'
case 'className': {
const nativeProp = 'class'
// prevent class names managed by React Native from being replaced
const className = node.getAttribute(nativeProp) + ' ' + value
node.setAttribute(nativeProp, className)
break
}
case 'text':
case 'value':
// native platforms use `text` prop to replace text input value

View File

@@ -1,4 +1,4 @@
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
import applyNativeMethods from '../../modules/applyNativeMethods'
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import StyleSheet from '../../apis/StyleSheet'
@@ -19,7 +19,6 @@ const keyframeEffects = [
{ transform: 'scale(0.95)', opacity: 0.5 }
]
@NativeMethodsDecorator
class ActivityIndicator extends Component {
static propTypes = {
animating: PropTypes.bool,
@@ -87,6 +86,8 @@ class ActivityIndicator extends Component {
}
}
applyNativeMethods(ActivityIndicator)
const styles = StyleSheet.create({
container: {
alignItems: 'center',

View File

@@ -1,12 +1,14 @@
import { PropTypes } from 'react'
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
import LayoutPropTypes from '../../apis/StyleSheet/LayoutPropTypes'
import TransformPropTypes from '../../apis/StyleSheet/TransformPropTypes'
import BorderPropTypes from '../../propTypes/BorderPropTypes'
import ColorPropType from '../../propTypes/ColorPropType'
import LayoutPropTypes from '../../propTypes/LayoutPropTypes'
import TransformPropTypes from '../../propTypes/TransformPropTypes'
import ImageResizeMode from './ImageResizeMode'
const hiddenOrVisible = PropTypes.oneOf([ 'hidden', 'visible' ])
module.exports = {
...BorderPropTypes,
...LayoutPropTypes,
...TransformPropTypes,
backfaceVisibility: hiddenOrVisible,

View File

@@ -1,12 +1,12 @@
/* global window */
import createNativeComponent from '../../modules/createNativeComponent'
import applyNativeMethods from '../../modules/applyNativeMethods'
import createReactDOMComponent from '../../modules/createReactDOMComponent'
import ImageResizeMode from './ImageResizeMode'
import ImageStylePropTypes from './ImageStylePropTypes'
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
import resolveAssetSource from './resolveAssetSource'
import React, { Component, PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
import View from '../View'
const STATUS_ERRORED = 'ERRORED'
@@ -22,21 +22,23 @@ const ImageSourcePropType = PropTypes.oneOfType([
PropTypes.string
])
@NativeMethodsDecorator
class Image extends Component {
static displayName = 'Image'
static propTypes = {
accessibilityLabel: createNativeComponent.propTypes.accessibilityLabel,
accessible: createNativeComponent.propTypes.accessible,
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
accessible: createReactDOMComponent.propTypes.accessible,
children: PropTypes.any,
defaultSource: ImageSourcePropType,
onError: PropTypes.func,
onLayout: PropTypes.func,
onLoad: PropTypes.func,
onLoadEnd: PropTypes.func,
onLoadStart: PropTypes.func,
resizeMode: PropTypes.oneOf(['contain', 'cover', 'none', 'stretch']),
source: ImageSourcePropType,
style: StyleSheetPropType(ImageStylePropTypes),
testID: createNativeComponent.propTypes.testID
testID: createReactDOMComponent.propTypes.testID
};
static defaultProps = {
@@ -83,6 +85,7 @@ class Image extends Component {
accessible,
children,
defaultSource,
onLayout,
source,
testID
} = this.props
@@ -108,6 +111,7 @@ class Image extends Component {
accessibilityLabel={accessibilityLabel}
accessibilityRole='img'
accessible={accessible}
onLayout={onLayout}
ref='root'
style={[
styles.initial,
@@ -117,7 +121,7 @@ class Image extends Component {
]}
testID={testID}
>
{createNativeComponent({ component: 'img', src: displayImage, style: styles.img })}
{createReactDOMComponent({ component: 'img', src: displayImage, style: styles.img })}
{children ? (
<View children={children} pointerEvents='box-none' style={styles.children} />
) : null}
@@ -176,6 +180,8 @@ class Image extends Component {
}
}
applyNativeMethods(Image)
const styles = StyleSheet.create({
initial: {
alignSelf: 'flex-start',

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -16,7 +16,11 @@ import View from '../View'
export default class ScrollViewBase extends Component {
static propTypes = {
...View.propTypes,
onMomentumScrollBegin: PropTypes.func,
onMomentumScrollEnd: PropTypes.func,
onScroll: PropTypes.func,
onScrollBeginDrag: PropTypes.func,
onScrollEndDrag: PropTypes.func,
onTouchMove: PropTypes.func,
onWheel: PropTypes.func,
scrollEnabled: PropTypes.bool,
@@ -30,12 +34,10 @@ export default class ScrollViewBase extends Component {
constructor(props) {
super(props)
this._debouncedOnScrollEnd = debounce(this._handleScrollEnd, 100)
this._handlePreventableScrollEvent = this._handlePreventableScrollEvent.bind(this)
this._handleScroll = this._handleScroll.bind(this)
this._state = { isScrolling: false }
}
_handlePreventableScrollEvent(handler) {
_handlePreventableScrollEvent = (handler) => {
return (e) => {
if (!this.props.scrollEnabled) {
e.preventDefault()
@@ -45,7 +47,7 @@ export default class ScrollViewBase extends Component {
}
}
_handleScroll(e) {
_handleScroll = (e) => {
const { scrollEventThrottle } = this.props
// A scroll happened, so the scroll bumps the debounce.
this._debouncedOnScrollEnd(e)
@@ -83,9 +85,14 @@ export default class ScrollViewBase extends Component {
}
render() {
const {
onMomentumScrollBegin, onMomentumScrollEnd, onScrollBeginDrag, onScrollEndDrag, scrollEnabled, scrollEventThrottle, // eslint-disable-line
...other
} = this.props
return (
<View
{...this.props}
{...other}
onScroll={this._handleScroll}
onTouchMove={this._handlePreventableScrollEvent(this.props.onTouchMove)}
onWheel={this._handlePreventableScrollEvent(this.props.onWheel)}

View File

@@ -13,7 +13,7 @@ import ReactDOM from 'react-dom'
import ScrollResponder from '../../modules/ScrollResponder'
import ScrollViewBase from './ScrollViewBase'
import StyleSheet from '../../apis/StyleSheet'
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
import View from '../View'
import ViewStylePropTypes from '../View/ViewStylePropTypes'
@@ -121,16 +121,15 @@ const ScrollView = React.createClass({
},
render() {
const scrollViewStyle = [
styles.base,
this.props.horizontal && styles.baseHorizontal
]
const contentContainerStyle = [
styles.contentContainer,
this.props.horizontal && styles.contentContainerHorizontal,
this.props.contentContainerStyle
]
const {
contentContainerStyle,
horizontal,
keyboardDismissMode, // eslint-disable-line
onContentSizeChange,
onScroll, // eslint-disable-line
refreshControl,
...other
} = this.props
if (process.env.NODE_ENV !== 'production' && this.props.style) {
const style = StyleSheet.flatten(this.props.style)
@@ -143,7 +142,7 @@ const ScrollView = React.createClass({
}
let contentSizeChangeProps = {}
if (this.props.onContentSizeChange) {
if (onContentSizeChange) {
contentSizeChangeProps = {
onLayout: this._handleContentOnLayout
}
@@ -155,13 +154,21 @@ const ScrollView = React.createClass({
children={this.props.children}
collapsable={false}
ref={INNERVIEW}
style={contentContainerStyle}
style={[
styles.contentContainer,
horizontal && styles.contentContainerHorizontal,
contentContainerStyle
]}
/>
)
const props = {
...this.props,
style: [scrollViewStyle, this.props.style],
...other,
style: [
styles.base,
horizontal && styles.baseHorizontal,
this.props.style
],
onTouchStart: this.scrollResponderHandleTouchStart,
onTouchMove: this.scrollResponderHandleTouchMove,
onTouchEnd: this.scrollResponderHandleTouchEnd,
@@ -187,7 +194,6 @@ const ScrollView = React.createClass({
'ScrollViewClass must not be undefined'
)
var refreshControl = this.props.refreshControl
if (refreshControl) {
return React.cloneElement(
refreshControl,

View File

@@ -1,5 +1,5 @@
import { PropTypes } from 'react'
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
import ColorPropType from '../../propTypes/ColorPropType'
import ViewStylePropTypes from '../View/ViewStylePropTypes'
const { number, oneOf, oneOfType, string } = PropTypes

View File

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

View File

@@ -1,42 +1,40 @@
import createNativeComponent from '../../modules/createNativeComponent'
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
import applyLayout from '../../modules/applyLayout'
import applyNativeMethods from '../../modules/applyNativeMethods'
import createReactDOMComponent from '../../modules/createReactDOMComponent'
import { Component, PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
import TextStylePropTypes from './TextStylePropTypes'
@NativeMethodsDecorator
class Text extends Component {
static displayName = 'Text'
static propTypes = {
accessibilityLabel: createNativeComponent.propTypes.accessibilityLabel,
accessibilityRole: createNativeComponent.propTypes.accessibilityRole,
accessible: createNativeComponent.propTypes.accessible,
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
accessibilityRole: createReactDOMComponent.propTypes.accessibilityRole,
accessible: createReactDOMComponent.propTypes.accessible,
children: PropTypes.any,
numberOfLines: PropTypes.number,
onLayout: PropTypes.func,
onPress: PropTypes.func,
style: StyleSheetPropType(TextStylePropTypes),
testID: createNativeComponent.propTypes.testID
testID: createReactDOMComponent.propTypes.testID
};
static defaultProps = {
accessible: true
};
_onPress = (e) => {
if (this.props.onPress) this.props.onPress(e)
}
render() {
const {
numberOfLines,
/* eslint-disable no-unused-vars */
onPress,
/* eslint-enable no-unused-vars */
onLayout, // eslint-disable-line
onPress, // eslint-disable-line
style,
...other
} = this.props
return createNativeComponent({
return createReactDOMComponent({
...other,
component: 'span',
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({
initial: {
color: 'inherit',

View File

@@ -1,88 +1,96 @@
/* eslint-env mocha */
import * as utils from '../../../modules/specHelpers'
import assert from 'assert'
import React from 'react'
import ReactTestUtils from 'react-addons-test-utils'
import StyleSheet from '../../../apis/StyleSheet'
import TextareaAutosize from 'react-textarea-autosize'
import TextInput from '..'
import { mount, shallow } from 'enzyme'
import TextInput from '../'
const placeholderText = 'placeholderText'
const findNativeInput = (wrapper) => wrapper.find('input')
const findNativeTextarea = (wrapper) => wrapper.find(TextareaAutosize)
const findPlaceholder = (wrapper) => wrapper.find({ children: placeholderText })
const findInput = (dom) => dom.querySelector('input, textarea')
const findShallowInput = (vdom) => vdom.props.children.props.children[0]
const findShallowPlaceholder = (vdom) => vdom.props.children.props.children[1]
const testIfDocumentIsFocused = (message, fn) => {
if (document.hasFocus && document.hasFocus()) {
test(message, fn)
} else {
test.skip(`${message} document is not focused`)
}
}
suite('components/TextInput', () => {
test('prop "autoComplete"', () => {
// off
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('autocomplete'), undefined)
let input = findNativeInput(shallow(<TextInput />))
assert.equal(input.prop('autoComplete'), undefined)
// on
input = findInput(utils.renderToDOM(<TextInput autoComplete />))
assert.equal(input.getAttribute('autocomplete'), 'on')
input = findNativeInput(shallow(<TextInput autoComplete />))
assert.equal(input.prop('autoComplete'), 'on')
})
test.skip('prop "autoFocus"', () => {
test('prop "autoFocus"', () => {
// false
let input = findInput(utils.renderToDOM(<TextInput />))
assert.deepEqual(document.activeElement, document.body)
let input = findNativeInput(mount(<TextInput />))
assert.equal(input.prop('autoFocus'), undefined)
// true
input = findInput(utils.renderToDOM(<TextInput autoFocus />))
assert.deepEqual(document.activeElement, input)
input = findNativeInput(mount(<TextInput autoFocus />))
assert.equal(input.prop('autoFocus'), true)
})
utils.testIfDocumentFocused('prop "clearTextOnFocus"', () => {
testIfDocumentIsFocused('prop "clearTextOnFocus"', () => {
const defaultValue = 'defaultValue'
// false
let input = findInput(utils.renderAndInject(<TextInput defaultValue={defaultValue} />))
input.focus()
assert.equal(input.value, defaultValue)
let input = findNativeInput(mount(<TextInput defaultValue={defaultValue} />))
input.simulate('focus')
assert.equal(input.node.value, defaultValue)
// true
input = findInput(utils.renderAndInject(<TextInput clearTextOnFocus defaultValue={defaultValue} />))
input.focus()
assert.equal(input.value, '')
input = findNativeInput(mount(<TextInput clearTextOnFocus defaultValue={defaultValue} />))
input.simulate('focus')
assert.equal(input.node.value, '')
})
test('prop "defaultValue"', () => {
const defaultValue = 'defaultValue'
const input = findShallowInput(utils.shallowRender(<TextInput defaultValue={defaultValue} />))
assert.equal(input.props.defaultValue, defaultValue)
const input = findNativeInput(shallow(<TextInput defaultValue={defaultValue} />))
assert.equal(input.prop('defaultValue'), defaultValue)
})
test('prop "editable"', () => {
// true
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('readonly'), undefined)
let input = findNativeInput(shallow(<TextInput />))
assert.equal(input.prop('readOnly'), false)
// false
input = findInput(utils.renderToDOM(<TextInput editable={false} />))
assert.equal(input.getAttribute('readonly'), '')
input = findNativeInput(shallow(<TextInput editable={false} />))
assert.equal(input.prop('readOnly'), true)
})
test('prop "keyboardType"', () => {
// default
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('type'), undefined)
input = findInput(utils.renderToDOM(<TextInput keyboardType='default' />))
assert.equal(input.getAttribute('type'), undefined)
let input = findNativeInput(shallow(<TextInput />))
assert.equal(input.prop('type'), undefined)
input = findNativeInput(shallow(<TextInput keyboardType='default' />))
assert.equal(input.prop('type'), undefined)
// email-address
input = findInput(utils.renderToDOM(<TextInput keyboardType='email-address' />))
assert.equal(input.getAttribute('type'), 'email')
input = findNativeInput(shallow(<TextInput keyboardType='email-address' />))
assert.equal(input.prop('type'), 'email')
// numeric
input = findInput(utils.renderToDOM(<TextInput keyboardType='numeric' />))
assert.equal(input.getAttribute('type'), 'number')
input = findNativeInput(shallow(<TextInput keyboardType='numeric' />))
assert.equal(input.prop('type'), 'number')
// phone-pad
input = findInput(utils.renderToDOM(<TextInput keyboardType='phone-pad' />))
assert.equal(input.getAttribute('type'), 'tel')
input = findNativeInput(shallow(<TextInput keyboardType='phone-pad' />))
assert.equal(input.prop('type'), 'tel')
// url
input = findInput(utils.renderToDOM(<TextInput keyboardType='url' />))
assert.equal(input.getAttribute('type'), 'url')
input = findNativeInput(shallow(<TextInput keyboardType='url' />))
assert.equal(input.prop('type'), 'url')
})
test('prop "maxLength"', () => {
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.getAttribute('maxlength'), undefined)
input = findInput(utils.renderToDOM(<TextInput maxLength={10} />))
assert.equal(input.getAttribute('maxlength'), '10')
let input = findNativeInput(shallow(<TextInput />))
assert.equal(input.prop('maxLength'), undefined)
input = findNativeInput(shallow(<TextInput maxLength={10} />))
assert.equal(input.prop('maxLength'), '10')
})
test('prop "maxNumberOfLines"', () => {
@@ -92,45 +100,45 @@ suite('components/TextInput', () => {
return str
}
const result = utils.shallowRender(
const input = findNativeTextarea(shallow(
<TextInput
maxNumberOfLines={3}
multiline
value={generateValue()}
/>
)
assert.equal(findShallowInput(result).props.maxRows, 3)
))
assert.equal(input.prop('maxRows'), 3)
})
test('prop "multiline"', () => {
// false
let input = findInput(utils.renderToDOM(<TextInput />))
assert.equal(input.tagName, 'INPUT')
let input = findNativeInput(shallow(<TextInput />))
assert.equal(input.length, 1)
// true
input = findInput(utils.renderToDOM(<TextInput multiline />))
assert.equal(input.tagName, 'TEXTAREA')
input = findNativeTextarea(shallow(<TextInput multiline />))
assert.equal(input.length, 1)
})
test('prop "numberOfLines"', () => {
// missing multiline
let input = findInput(utils.renderToDOM(<TextInput numberOfLines={2} />))
assert.equal(input.tagName, 'INPUT')
let input = findNativeInput(shallow(<TextInput numberOfLines={2} />))
assert.equal(input.length, 1)
// with multiline
input = findInput(utils.renderAndInject(<TextInput multiline numberOfLines={2} />))
assert.equal(input.tagName, 'TEXTAREA')
input = findNativeTextarea(shallow(<TextInput multiline numberOfLines={2} />))
assert.equal(input.length, 1)
const result = utils.shallowRender(
input = findNativeTextarea(shallow(
<TextInput
multiline
numberOfLines={3}
/>
)
assert.equal(findShallowInput(result).props.minRows, 3)
))
assert.equal(input.prop('minRows'), 3)
})
test('prop "onBlur"', (done) => {
const input = findInput(utils.renderToDOM(<TextInput onBlur={onBlur} />))
ReactTestUtils.Simulate.blur(input)
const input = findNativeInput(mount(<TextInput onBlur={onBlur} />))
input.simulate('blur')
function onBlur(e) {
assert.ok(e)
done()
@@ -138,8 +146,8 @@ suite('components/TextInput', () => {
})
test('prop "onChange"', (done) => {
const input = findInput(utils.renderToDOM(<TextInput onChange={onChange} />))
ReactTestUtils.Simulate.change(input)
const input = findNativeInput(mount(<TextInput onChange={onChange} />))
input.simulate('change')
function onChange(e) {
assert.ok(e)
done()
@@ -148,8 +156,8 @@ suite('components/TextInput', () => {
test('prop "onChangeText"', (done) => {
const newText = 'newText'
const input = findInput(utils.renderToDOM(<TextInput onChangeText={onChangeText} />))
ReactTestUtils.Simulate.change(input, { target: { value: newText } })
const input = findNativeInput(mount(<TextInput onChangeText={onChangeText} />))
input.simulate('change', { target: { value: newText } })
function onChangeText(text) {
assert.equal(text, newText)
done()
@@ -157,8 +165,8 @@ suite('components/TextInput', () => {
})
test('prop "onFocus"', (done) => {
const input = findInput(utils.renderToDOM(<TextInput onFocus={onFocus} />))
ReactTestUtils.Simulate.focus(input)
const input = findNativeInput(mount(<TextInput onFocus={onFocus} />))
input.simulate('focus')
function onFocus(e) {
assert.ok(e)
done()
@@ -168,8 +176,8 @@ suite('components/TextInput', () => {
test('prop "onLayout"')
test('prop "onSelectionChange"', (done) => {
const input = findInput(utils.renderAndInject(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />))
ReactTestUtils.Simulate.select(input, { target: { selectionStart: 0, selectionEnd: 3 } })
const input = findNativeInput(mount(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />))
input.simulate('select', { target: { selectionStart: 0, selectionEnd: 3 } })
function onSelectionChange(e) {
assert.equal(e.selectionEnd, 3)
assert.equal(e.selectionStart, 0)
@@ -178,46 +186,46 @@ suite('components/TextInput', () => {
})
test('prop "placeholder"', () => {
const placeholder = 'placeholder'
const result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} />))
assert.equal(result.props.children.props.children, placeholder)
let textInput = shallow(<TextInput />)
assert.equal(findPlaceholder(textInput).length, 0)
textInput = shallow(<TextInput placeholder={placeholderText} />)
assert.equal(findPlaceholder(textInput).length, 1)
})
test('prop "placeholderTextColor"', () => {
const placeholder = 'placeholder'
let placeholderElement = findPlaceholder(shallow(<TextInput placeholder={placeholderText} />))
assert.equal(StyleSheet.flatten(placeholderElement.prop('style')).color, 'darkgray')
let result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} />))
assert.equal(StyleSheet.flatten(result.props.children.props.style).color, 'darkgray')
result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} placeholderTextColor='red' />))
assert.equal(StyleSheet.flatten(result.props.children.props.style).color, 'red')
placeholderElement = findPlaceholder(shallow(<TextInput placeholder={placeholderText} placeholderTextColor='red' />))
assert.equal(StyleSheet.flatten(placeholderElement.prop('style')).color, 'red')
})
test('prop "secureTextEntry"', () => {
let input = findInput(utils.renderToDOM(<TextInput secureTextEntry />))
assert.equal(input.getAttribute('type'), 'password')
let input = findNativeInput(shallow(<TextInput secureTextEntry />))
assert.equal(input.prop('type'), 'password')
// ignored for multiline
input = findInput(utils.renderToDOM(<TextInput multiline secureTextEntry />))
assert.equal(input.getAttribute('type'), undefined)
input = findNativeTextarea(shallow(<TextInput multiline secureTextEntry />))
assert.equal(input.prop('type'), undefined)
})
utils.testIfDocumentFocused('prop "selectTextOnFocus"', () => {
testIfDocumentIsFocused('prop "selectTextOnFocus"', () => {
const text = 'Text'
// false
let input = findInput(utils.renderAndInject(<TextInput defaultValue={text} />))
input.focus()
assert.equal(input.selectionEnd, 0)
assert.equal(input.selectionStart, 0)
let input = findNativeInput(mount(<TextInput defaultValue={text} />))
input.node.focus()
assert.equal(input.node.selectionEnd, 4)
assert.equal(input.node.selectionStart, 4)
// true
input = findInput(utils.renderAndInject(<TextInput defaultValue={text} selectTextOnFocus />))
input.focus()
assert.equal(input.selectionEnd, 4)
assert.equal(input.selectionStart, 0)
// input = findNativeInput(mount(<TextInput defaultValue={text} selectTextOnFocus />))
// input.node.focus()
// assert.equal(input.node.selectionEnd, 4)
// assert.equal(input.node.selectionStart, 0)
})
test('prop "value"', () => {
const value = 'value'
const input = findShallowInput(utils.shallowRender(<TextInput value={value} />))
assert.equal(input.props.value, value)
const input = findNativeInput(shallow(<TextInput value={value} />))
assert.equal(input.prop('value'), value)
})
})

View File

@@ -1,5 +1,5 @@
import createNativeComponent from '../../modules/createNativeComponent'
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
import applyNativeMethods from '../../modules/applyNativeMethods'
import createReactDOMComponent from '../../modules/createReactDOMComponent'
import omit from 'lodash/omit'
import pick from 'lodash/pick'
import React, { Component, PropTypes } from 'react'
@@ -8,12 +8,12 @@ import StyleSheet from '../../apis/StyleSheet'
import Text from '../Text'
import TextareaAutosize from 'react-textarea-autosize'
import TextInputState from './TextInputState'
import UIManager from '../../apis/UIManager'
import View from '../View'
import ViewStylePropTypes from '../View/ViewStylePropTypes'
const viewStyleProps = Object.keys(ViewStylePropTypes)
@NativeMethodsDecorator
class TextInput extends Component {
static propTypes = {
...View.propTypes,
@@ -68,14 +68,12 @@ class TextInput extends Component {
}
setNativeProps(props) {
this.refs.input.setNativeProps(props)
UIManager.updateView(this.refs.input, props, this)
}
render() {
const {
/* eslint-disable react/prop-types */
accessibilityLabel,
/* eslint-enable react/prop-types */
accessibilityLabel, // eslint-disable-line
autoComplete,
autoFocus,
defaultValue,
@@ -85,6 +83,7 @@ class TextInput extends Component {
maxNumberOfLines,
multiline,
numberOfLines,
onLayout,
onSelectionChange,
placeholder,
placeholderTextColor,
@@ -135,7 +134,7 @@ class TextInput extends Component {
onFocus: this._handleFocus,
onSelect: onSelectionChange && this._handleSelectionChange,
readOnly: !editable,
style: { ...styles.input, ...textStyles, outline: style.outline },
style: [ styles.input, textStyles, { outline: style.outline } ],
value
}
@@ -171,11 +170,12 @@ class TextInput extends Component {
<View
accessibilityLabel={accessibilityLabel}
onClick={this._handleClick}
onLayout={onLayout}
style={[ styles.initial, rootStyles ]}
testID={testID}
>
<View style={styles.wrapper}>
{createNativeComponent({ ...props, ref: 'input' })}
{createReactDOMComponent({ ...props, ref: 'input' })}
{optionalPlaceholder}
</View>
</View>
@@ -232,13 +232,15 @@ class TextInput extends Component {
}
}
applyNativeMethods(TextInput)
const styles = StyleSheet.create({
initial: {
borderColor: 'black',
borderWidth: 1
},
wrapper: {
flexGrow: 1
flex: 1
},
input: {
appearance: 'none',
@@ -246,8 +248,9 @@ const styles = StyleSheet.create({
borderWidth: 0,
boxSizing: 'border-box',
color: 'inherit',
flexGrow: 1,
flex: 1,
font: 'inherit',
minHeight: '100%', // center small inputs (fix #139)
padding: 0,
zIndex: 1
},

View File

@@ -15,7 +15,7 @@
/* @edit start */
const BoundingDimensions = require('./BoundingDimensions');
const keyMirror = require('fbjs/lib/keyMirror');
const normalizeColor = require('../../apis/StyleSheet/normalizeColor');
const normalizeColor = require('../../modules/normalizeColor');
const Position = require('./Position');
const React = require('react');
const TouchEventUtils = require('fbjs/lib/TouchEventUtils');
@@ -735,7 +735,7 @@ var Touchable = {
if (!Touchable.TOUCH_TARGET_DEBUG) {
return null;
}
if (!__DEV__) {
if (process.env.NODE_ENV === 'production') {
throw Error('Touchable.TOUCH_TARGET_DEBUG should not be enabled in prod!');
}
const debugHitSlopStyle = {};

View File

@@ -12,8 +12,8 @@
*/
'use strict';
var Animated = require('animated');
var EdgeInsetsPropType = require('../../apis/StyleSheet/EdgeInsetsPropType');
var Animated = require('../../apis/Animated');
var EdgeInsetsPropType = require('../../propTypes/EdgeInsetsPropType');
var NativeMethodsMixin = require('../../modules/NativeMethodsMixin');
var React = require('react');
var StyleSheet = require('../../apis/StyleSheet');

View File

@@ -14,7 +14,7 @@
// 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 React = require('react');
var StyleSheet = require('../../apis/StyleSheet');
@@ -106,11 +106,10 @@ var TouchableHighlight = React.createClass({
backgroundColor: underlayColor,
}
},
underlayProps: {
style: {
backgroundColor: style && style.backgroundColor || null
}
}
underlayStyle: [
INACTIVE_UNDERLAY_PROPS.style,
props.style,
]
};
},
@@ -206,7 +205,10 @@ var TouchableHighlight = React.createClass({
this._hideTimeout = null;
if (this._hasPressHandler() && this.refs[UNDERLAY_REF]) {
this.refs[CHILD_REF].setNativeProps(INACTIVE_CHILD_PROPS);
this.refs[UNDERLAY_REF].setNativeProps(this.state.underlayProps);
this.refs[UNDERLAY_REF].setNativeProps({
...INACTIVE_UNDERLAY_PROPS,
style: this.state.underlayStyle,
});
this.props.onHideUnderlay && this.props.onHideUnderlay();
}
},
@@ -245,7 +247,7 @@ var TouchableHighlight = React.createClass({
onResponderRelease={this.touchableHandleResponderRelease}
onResponderTerminate={this.touchableHandleResponderTerminate}
ref={UNDERLAY_REF}
style={[styles.root, this.props.style]}
style={this.state.underlayStyle}
tabIndex='0'
testID={this.props.testID}>
{React.cloneElement(
@@ -264,6 +266,9 @@ var UNDERLAY_REF = keyOf({underlayRef: null});
var INACTIVE_CHILD_PROPS = {
style: StyleSheet.create({x: {opacity: 1.0}}).x,
};
var INACTIVE_UNDERLAY_PROPS = {
style: StyleSheet.create({x: {backgroundColor: 'transparent'}}).x,
};
var styles = StyleSheet.create({
root: {

View File

@@ -14,7 +14,7 @@
// 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 React = require('react');
var StyleSheet = require('../../apis/StyleSheet');
@@ -23,7 +23,7 @@ var Touchable = require('./Touchable');
var TouchableWithoutFeedback = require('./TouchableWithoutFeedback');
var ensurePositiveDelayProps = require('./ensurePositiveDelayProps');
var flattenStyle = require('../../apis/StyleSheet/flattenStyle');
var flattenStyle = StyleSheet.flatten
type Event = Object;
@@ -166,7 +166,7 @@ var TouchableOpacity = React.createClass({
render: function() {
return (
<Animated.View
accessible={true}
accessible={this.props.accessible !== false}
accessibilityLabel={this.props.accessibilityLabel}
accessibilityRole={this.props.accessibilityRole || 'button'}
style={[styles.root, this.props.style, {opacity: this.state.anim}]}

View File

@@ -12,12 +12,13 @@
*/
'use strict';
var EdgeInsetsPropType = require('../../apis/StyleSheet/EdgeInsetsPropType');
var EdgeInsetsPropType = require('../../propTypes/EdgeInsetsPropType');
var React = require('react');
var TimerMixin = require('react-timer-mixin');
var Touchable = require('./Touchable');
var View = require('../View');
var ensurePositiveDelayProps = require('./ensurePositiveDelayProps');
var warning = require('fbjs/lib/warning');
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.
*/
var TouchableWithoutFeedback = React.createClass({
const TouchableWithoutFeedback = React.createClass({
mixins: [TimerMixin, Touchable.Mixin],
propTypes: {
accessible: React.PropTypes.bool,
accessible: View.propTypes.accessible,
accessibilityLabel: View.propTypes.accessibilityLabel,
accessibilityRole: View.propTypes.accessibilityRole,
/**
@@ -143,9 +144,25 @@ var TouchableWithoutFeedback = React.createClass({
return this.props.delayPressOut || 0;
},
render: function(): ReactElement {
render: function(): ReactElement<any> {
// 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,
accessibilityLabel: this.props.accessibilityLabel,
accessibilityRole: this.props.accessibilityRole,
@@ -158,6 +175,8 @@ var TouchableWithoutFeedback = React.createClass({
onResponderMove: this.touchableHandleResponderMove,
onResponderRelease: this.touchableHandleResponderRelease,
onResponderTerminate: this.touchableHandleResponderTerminate,
style,
children,
tabIndex: '0'
});
}

View File

@@ -1,8 +1,8 @@
import { PropTypes } from 'react'
import BorderPropTypes from '../../apis/StyleSheet/BorderPropTypes'
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
import LayoutPropTypes from '../../apis/StyleSheet/LayoutPropTypes'
import TransformPropTypes from '../../apis/StyleSheet/TransformPropTypes'
import BorderPropTypes from '../../propTypes/BorderPropTypes'
import ColorPropType from '../../propTypes/ColorPropType'
import LayoutPropTypes from '../../propTypes/LayoutPropTypes'
import TransformPropTypes from '../../propTypes/TransformPropTypes'
const { number, oneOf, string } = PropTypes
const autoOrHiddenOrVisible = oneOf([ 'auto', 'hidden', 'visible' ])

View File

@@ -1,34 +1,43 @@
/* eslint-env mocha */
import * as utils from '../../../modules/specHelpers'
import assert from 'assert'
import includes from 'lodash/includes'
import React from 'react'
import View from '../'
import { mount, shallow } from 'enzyme'
suite('components/View', () => {
test('prop "children"', () => {
const children = 'children'
const result = utils.shallowRender(<View>{children}</View>)
assert.equal(result.props.children, children)
const view = shallow(<View>{children}</View>)
assert.equal(view.prop('children'), children)
})
test('prop "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"', () => {
const result = utils.shallowRender(<View pointerEvents='box-only' />)
assert.equal(result.props.className, '__style_pebo')
const view = shallow(<View pointerEvents='box-only' />)
assert.ok(includes(view.prop('className'), '__style_pebo') === true)
})
test('prop "style"', () => {
const noFlex = utils.shallowRender(<View />)
assert.equal(noFlex.props.style.flexShrink, 0)
const view = shallow(<View />)
assert.equal(view.prop('style').flexShrink, 0)
const flex = utils.shallowRender(<View style={{ flex: 1 }} />)
assert.equal(flex.props.style.flexShrink, 1)
const flexView = shallow(<View style={{ flex: 1 }} />)
assert.equal(flexView.prop('style').flexShrink, 1)
const flexShrink = utils.shallowRender(<View style={{ flexShrink: 1 }} />)
assert.equal(flexShrink.props.style.flexShrink, 1)
const flexShrinkView = shallow(<View style={{ flexShrink: 1 }} />)
assert.equal(flexShrinkView.prop('style').flexShrink, 1)
const flexAndShrink = utils.shallowRender(<View style={{ flex: 1, flexShrink: 2 }} />)
assert.equal(flexAndShrink.props.style.flexShrink, 2)
const flexAndShrinkView = shallow(<View style={{ flex: 1, flexShrink: 2 }} />)
assert.equal(flexAndShrinkView.prop('style').flexShrink, 2)
})
})

View File

@@ -1,21 +1,27 @@
import createNativeComponent from '../../modules/createNativeComponent'
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
import normalizeNativeEvent from '../../apis/PanResponder/normalizeNativeEvent'
import applyLayout from '../../modules/applyLayout'
import applyNativeMethods from '../../modules/applyNativeMethods'
import createReactDOMComponent from '../../modules/createReactDOMComponent'
import EdgeInsetsPropType from '../../propTypes/EdgeInsetsPropType'
import normalizeNativeEvent from '../../modules/normalizeNativeEvent'
import { Component, PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
import ViewStylePropTypes from './ViewStylePropTypes'
@NativeMethodsDecorator
class View extends Component {
static displayName = 'View'
static propTypes = {
accessibilityLabel: createNativeComponent.propTypes.accessibilityLabel,
accessibilityLiveRegion: createNativeComponent.propTypes.accessibilityLiveRegion,
accessibilityRole: createNativeComponent.propTypes.accessibilityRole,
accessible: createNativeComponent.propTypes.accessible,
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
accessibilityLiveRegion: createReactDOMComponent.propTypes.accessibilityLiveRegion,
accessibilityRole: createReactDOMComponent.propTypes.accessibilityRole,
accessible: createReactDOMComponent.propTypes.accessible,
children: PropTypes.any,
collapsable: PropTypes.bool,
hitSlop: EdgeInsetsPropType,
onClick: PropTypes.func,
onClickCapture: PropTypes.func,
onLayout: PropTypes.func,
onMoveShouldSetResponder: PropTypes.func,
onMoveShouldSetResponderCapture: PropTypes.func,
onResponderGrant: PropTypes.func,
@@ -36,7 +42,7 @@ class View extends Component {
onTouchStartCapture: PropTypes.func,
pointerEvents: PropTypes.oneOf(['auto', 'box-none', 'box-only', 'none']),
style: StyleSheetPropType(ViewStylePropTypes),
testID: createNativeComponent.propTypes.testID
testID: createReactDOMComponent.propTypes.testID
};
static defaultProps = {
@@ -51,6 +57,9 @@ class View extends Component {
render() {
const {
collapsable, // eslint-disable-line
hitSlop, // eslint-disable-line
onLayout, // eslint-disable-line
pointerEvents,
style,
...other
@@ -58,6 +67,8 @@ class View extends Component {
const flattenedStyle = StyleSheet.flatten(style)
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 = {
...other,
@@ -74,13 +85,12 @@ class View extends Component {
style: [
styles.initial,
style,
// 'View' needs to use 'flexShrink' in its reset when there is no 'flex' style provided
(flattenedStyle.flex == null && flattenedStyle.flexShrink == null) && styles.flexReset,
needsFlexReset && styles.flexReset,
pointerEventsStyle
]
}
return createNativeComponent(props)
return createReactDOMComponent(props)
}
/**
@@ -98,6 +108,8 @@ class View extends Component {
}
}
applyLayout(applyNativeMethods(View))
const styles = StyleSheet.create({
// https://github.com/facebook/css-layout#default-values
initial: {

View File

@@ -1,11 +1,11 @@
import './apis/PanResponder/injectResponderEventPlugin'
import './modules/injectResponderEventPlugin'
import findNodeHandle from './modules/findNodeHandle'
import ReactDOM from 'react-dom'
import ReactDOMServer from 'react-dom/server'
// apis
import Animated from 'animated'
import Animated from './apis/Animated'
import AppRegistry from './apis/AppRegistry'
import AppState from './apis/AppState'
import AsyncStorage from './apis/AsyncStorage'
@@ -23,7 +23,6 @@ import UIManager from './apis/UIManager'
import ActivityIndicator from './components/ActivityIndicator'
import Image from './components/Image'
import ListView from './components/ListView'
import Portal from './components/Portal'
import ScrollView from './components/ScrollView'
import Text from './components/Text'
import TextInput from './components/TextInput'
@@ -39,11 +38,9 @@ import NativeModules from './modules/NativeModules'
// propTypes
import ColorPropType from './apis/StyleSheet/ColorPropType'
import EdgeInsetsPropType from './apis/StyleSheet/EdgeInsetsPropType'
import PointPropType from './apis/StyleSheet/PointPropType'
Animated.inject.FlattenStyle(StyleSheet.flatten)
import ColorPropType from './propTypes/ColorPropType'
import EdgeInsetsPropType from './propTypes/EdgeInsetsPropType'
import PointPropType from './propTypes/PointPropType'
const ReactNative = {
// top-level API
@@ -55,12 +52,7 @@ const ReactNative = {
renderToString: ReactDOMServer.renderToString,
// apis
Animated: {
...Animated,
Image: Animated.createAnimatedComponent(Image),
Text: Animated.createAnimatedComponent(Text),
View: Animated.createAnimatedComponent(View)
},
Animated,
AppRegistry,
AppState,
AsyncStorage,
@@ -78,7 +70,6 @@ const ReactNative = {
ActivityIndicator,
Image,
ListView,
Portal,
ScrollView,
Text,
TextInput,

View File

@@ -113,11 +113,11 @@ const NativeMethodsMixin = {
* In the future, we should cleanup callbacks by cancelling them instead of
* using this.
*/
const mountSafeCallback = (context: Component, callback: ?Function) => () => {
if (!callback || (context.isMounted && !context.isMounted())) {
return
const mountSafeCallback = (context: Component, callback: ?Function) => (...args) => {
if (!callback) {
return undefined
}
return callback.apply(context, arguments)
return callback.apply(context, args)
}
module.exports = NativeMethodsMixin

View 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;

View 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

View File

@@ -7,7 +7,7 @@
import NativeMethodsMixin from '../NativeMethodsMixin'
const NativeMethodsDecorator = (Component) => {
const applyNativeMethods = (Component) => {
Object.keys(NativeMethodsMixin).forEach((method) => {
if (!Component.prototype[method]) {
Component.prototype[method] = NativeMethodsMixin[method]
@@ -16,4 +16,4 @@ const NativeMethodsDecorator = (Component) => {
return Component
}
module.exports = NativeMethodsDecorator
module.exports = applyNativeMethods

View File

@@ -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)
})
})

View 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)
})
})

View File

@@ -17,7 +17,7 @@ const roleComponents = {
region: 'section'
}
const createNativeComponent = ({
const createReactDOMComponent = ({
accessibilityLabel,
accessibilityLiveRegion,
accessibilityRole,
@@ -27,7 +27,8 @@ const createNativeComponent = ({
type,
...other
}) => {
const Component = accessibilityRole && roleComponents[accessibilityRole] ? roleComponents[accessibilityRole] : component
const role = accessibilityRole
const Component = role && roleComponents[role] ? roleComponents[role] : component
return (
<Component
@@ -37,13 +38,13 @@ const createNativeComponent = ({
aria-label={accessibilityLabel}
aria-live={accessibilityLiveRegion}
data-testid={testID}
role={accessibilityRole}
type={accessibilityRole === 'button' ? 'button' : type}
role={role}
type={role === 'button' ? 'button' : type}
/>
)
}
createNativeComponent.propTypes = {
createReactDOMComponent.propTypes = {
accessibilityLabel: PropTypes.string,
accessibilityLiveRegion: PropTypes.oneOf([ 'assertive', 'off', 'polite' ]),
accessibilityRole: PropTypes.string,
@@ -54,4 +55,4 @@ createNativeComponent.propTypes = {
type: PropTypes.string
}
module.exports = createNativeComponent
module.exports = createReactDOMComponent

View File

@@ -10,9 +10,9 @@
*/
import assert from 'assert'
import flattenStyle from '../flattenStyle'
import flattenStyle from '..'
suite('apis/StyleSheet/flattenStyle', () => {
suite('modules/flattenStyle', () => {
test('should merge style objects', () => {
const style1 = {opacity: 1}
const style2 = {order: 2}

View 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;

View File

@@ -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`)
}
}

View File

@@ -1,5 +1,5 @@
import { PropTypes } from 'react'
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
import ColorPropType from './ColorPropType'
const numberOrString = PropTypes.oneOfType([ PropTypes.number, PropTypes.string ])
const BorderStylePropType = PropTypes.oneOf([ 'solid', 'dotted', 'dashed' ])

View File

@@ -13,7 +13,7 @@
import { PropTypes } from 'react'
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 color = props[propName];

View File

@@ -6,7 +6,7 @@
*/
import createStrictShapeTypeChecker from './createStrictShapeTypeChecker'
import flattenStyle from './flattenStyle'
import flattenStyle from '../modules/flattenStyle'
module.exports = function StyleSheetPropType(shape) {
const shapePropType = createStrictShapeTypeChecker(shape)

View 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