Compare commits

..

13 Commits

Author SHA1 Message Date
Nicolas Gallagher
7cda89c5ce 0.0.60 2016-12-16 12:15:23 +00:00
Nicolas Gallagher
695eba45af [add] Clipboard API
Close #125
Fix #122
2016-12-16 11:59:22 +00:00
Nicolas Gallagher
92a2cb274a [fix] remove TextInput default flex value 2016-12-16 11:39:12 +00:00
Nicolas Gallagher
b1ca04d11e Rename I18nManager example 2016-12-16 11:35:11 +00:00
Nicolas Gallagher
22ab70ea6f 0.0.59 2016-12-14 17:42:15 +00:00
Gethin Webster
49f36d8eb1 Update to ListView functionality
Re-build ListView from the core react-native component, to get better
feature parity

Ensure lists with small initialListSize render correctly

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

Fix #198
2016-12-12 11:45:30 +00:00
Nicolas Gallagher
4d5819ae28 [fix] RTL translateX; Switch transition 2016-12-08 19:40:34 -08:00
31 changed files with 931 additions and 109 deletions

View File

@@ -27,7 +27,7 @@ online with [React Native for Web: Playground](http://codepen.io/necolas/pen/PZz
To install in your app: To install in your app:
``` ```
npm install --save react react-native-web npm install --save react@15.3 react-native-web
``` ```
Read the [Client and Server rendering](docs/guides/rendering.md) guide. Read the [Client and Server rendering](docs/guides/rendering.md) guide.
@@ -53,6 +53,7 @@ Exported modules:
* Components * Components
* [`ActivityIndicator`](docs/components/ActivityIndicator.md) * [`ActivityIndicator`](docs/components/ActivityIndicator.md)
* [`Button`](docs/components/Button.md)
* [`Image`](docs/components/Image.md) * [`Image`](docs/components/Image.md)
* [`ListView`](docs/components/ListView.md) * [`ListView`](docs/components/ListView.md)
* [`ProgressBar`](docs/components/ProgressBar.md) * [`ProgressBar`](docs/components/ProgressBar.md)
@@ -69,6 +70,7 @@ Exported modules:
* [`AppRegistry`](docs/apis/AppRegistry.md) * [`AppRegistry`](docs/apis/AppRegistry.md)
* [`AppState`](docs/apis/AppState.md) * [`AppState`](docs/apis/AppState.md)
* [`AsyncStorage`](docs/apis/AsyncStorage.md) * [`AsyncStorage`](docs/apis/AsyncStorage.md)
* [`Clipboard`](docs/apis/Clipboard.md)
* [`Dimensions`](docs/apis/Dimensions.md) * [`Dimensions`](docs/apis/Dimensions.md)
* [`I18nManager`](docs/apis/I18nManager.md) * [`I18nManager`](docs/apis/I18nManager.md)
* [`NativeMethods`](docs/apis/NativeMethods.md) * [`NativeMethods`](docs/apis/NativeMethods.md)

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "react-native-web", "name": "react-native-web",
"version": "0.0.56", "version": "0.0.60",
"description": "React Native for Web", "description": "React Native for Web",
"main": "dist/index.js", "main": "dist/index.js",
"files": [ "files": [

View File

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

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

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
import applyNativeMethods from '../../modules/applyNativeMethods'; import applyNativeMethods from '../../modules/applyNativeMethods';
import ImageResizeMode from './ImageResizeMode'; import ImageResizeMode from './ImageResizeMode';
import ImageStylePropTypes from './ImageStylePropTypes'; import ImageStylePropTypes from './ImageStylePropTypes';
import requestAnimationFrame from 'fbjs/lib/requestAnimationFrame';
import StyleSheet from '../../apis/StyleSheet'; import StyleSheet from '../../apis/StyleSheet';
import StyleSheetPropType from '../../propTypes/StyleSheetPropType'; import StyleSheetPropType from '../../propTypes/StyleSheetPropType';
import View from '../View'; import View from '../View';
@@ -193,7 +194,7 @@ class Image extends Component {
this._imageState = status; this._imageState = status;
const isLoaded = this._imageState === STATUS_LOADED; const isLoaded = this._imageState === STATUS_LOADED;
if (isLoaded !== this.state.isLoaded) { if (isLoaded !== this.state.isLoaded) {
window.requestAnimationFrame(() => { requestAnimationFrame(() => {
if (this._isMounted) { if (this._isMounted) {
this.setState({ isLoaded }); this.setState({ isLoaded });
} }

View File

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

View File

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

View File

@@ -236,7 +236,6 @@ const styles = StyleSheet.create({
overflowY: 'hidden' overflowY: 'hidden'
}, },
contentContainer: { contentContainer: {
flexGrow: 1,
transform: [ { translateZ: 0 } ] transform: [ { translateZ: 0 } ]
}, },
contentContainerHorizontal: { contentContainerHorizontal: {

View File

@@ -67,7 +67,7 @@ exports[`components/Switch disabled when "false" a default checkbox is rendered
"WebkitFlexBasis": "auto", "WebkitFlexBasis": "auto",
"WebkitFlexDirection": "column", "WebkitFlexDirection": "column",
"WebkitFlexShrink": 0, "WebkitFlexShrink": 0,
"WebkitTransition": "background-color 0.1s", "WebkitTransition": "0.1s",
"alignItems": "stretch", "alignItems": "stretch",
"backgroundColor": "#939393", "backgroundColor": "#939393",
"borderBottomLeftRadius": "10px", "borderBottomLeftRadius": "10px",
@@ -112,7 +112,7 @@ exports[`components/Switch disabled when "false" a default checkbox is rendered
"textAlign": "inherit", "textAlign": "inherit",
"textDecoration": "none", "textDecoration": "none",
"top": "0px", "top": "0px",
"transition": "background-color 0.1s", "transition": "0.1s",
"width": "90%", "width": "90%",
} }
} /> } />
@@ -129,7 +129,8 @@ exports[`components/Switch disabled when "false" a default checkbox is rendered
"WebkitFlexBasis": "auto", "WebkitFlexBasis": "auto",
"WebkitFlexDirection": "column", "WebkitFlexDirection": "column",
"WebkitFlexShrink": 0, "WebkitFlexShrink": 0,
"WebkitTransition": "background-color 0.1s", "WebkitTransform": "translateX(0%)",
"WebkitTransition": "0.1s",
"alignItems": "stretch", "alignItems": "stretch",
"alignSelf": "flex-start", "alignSelf": "flex-start",
"backgroundColor": "#FAFAFA", "backgroundColor": "#FAFAFA",
@@ -166,6 +167,7 @@ exports[`components/Switch disabled when "false" a default checkbox is rendered
"msFlexItemAlign": "start", "msFlexItemAlign": "start",
"msFlexNegative": 0, "msFlexNegative": 0,
"msPreferredSize": "auto", "msPreferredSize": "auto",
"msTransform": "translateX(0%)",
"paddingBottom": "0px", "paddingBottom": "0px",
"paddingLeft": "0px", "paddingLeft": "0px",
"paddingRight": "0px", "paddingRight": "0px",
@@ -173,7 +175,8 @@ exports[`components/Switch disabled when "false" a default checkbox is rendered
"position": "relative", "position": "relative",
"textAlign": "inherit", "textAlign": "inherit",
"textDecoration": "none", "textDecoration": "none",
"transition": "background-color 0.1s", "transform": "translateX(0%)",
"transition": "0.1s",
"width": "20px", "width": "20px",
} }
} /> } />
@@ -278,7 +281,7 @@ exports[`components/Switch disabled when "true" a disabled checkbox is rendered
"WebkitFlexBasis": "auto", "WebkitFlexBasis": "auto",
"WebkitFlexDirection": "column", "WebkitFlexDirection": "column",
"WebkitFlexShrink": 0, "WebkitFlexShrink": 0,
"WebkitTransition": "background-color 0.1s", "WebkitTransition": "0.1s",
"alignItems": "stretch", "alignItems": "stretch",
"backgroundColor": "#D5D5D5", "backgroundColor": "#D5D5D5",
"borderBottomLeftRadius": "10px", "borderBottomLeftRadius": "10px",
@@ -323,7 +326,7 @@ exports[`components/Switch disabled when "true" a disabled checkbox is rendered
"textAlign": "inherit", "textAlign": "inherit",
"textDecoration": "none", "textDecoration": "none",
"top": "0px", "top": "0px",
"transition": "background-color 0.1s", "transition": "0.1s",
"width": "90%", "width": "90%",
} }
} /> } />
@@ -340,7 +343,8 @@ exports[`components/Switch disabled when "true" a disabled checkbox is rendered
"WebkitFlexBasis": "auto", "WebkitFlexBasis": "auto",
"WebkitFlexDirection": "column", "WebkitFlexDirection": "column",
"WebkitFlexShrink": 0, "WebkitFlexShrink": 0,
"WebkitTransition": "background-color 0.1s", "WebkitTransform": "translateX(0%)",
"WebkitTransition": "0.1s",
"alignItems": "stretch", "alignItems": "stretch",
"alignSelf": "flex-start", "alignSelf": "flex-start",
"backgroundColor": "#BDBDBD", "backgroundColor": "#BDBDBD",
@@ -377,6 +381,7 @@ exports[`components/Switch disabled when "true" a disabled checkbox is rendered
"msFlexItemAlign": "start", "msFlexItemAlign": "start",
"msFlexNegative": 0, "msFlexNegative": 0,
"msPreferredSize": "auto", "msPreferredSize": "auto",
"msTransform": "translateX(0%)",
"paddingBottom": "0px", "paddingBottom": "0px",
"paddingLeft": "0px", "paddingLeft": "0px",
"paddingRight": "0px", "paddingRight": "0px",
@@ -384,7 +389,8 @@ exports[`components/Switch disabled when "true" a disabled checkbox is rendered
"position": "relative", "position": "relative",
"textAlign": "inherit", "textAlign": "inherit",
"textDecoration": "none", "textDecoration": "none",
"transition": "background-color 0.1s", "transform": "translateX(0%)",
"transition": "0.1s",
"width": "20px", "width": "20px",
} }
} /> } />
@@ -489,7 +495,7 @@ exports[`components/Switch value when "false" an unchecked checkbox is rendered
"WebkitFlexBasis": "auto", "WebkitFlexBasis": "auto",
"WebkitFlexDirection": "column", "WebkitFlexDirection": "column",
"WebkitFlexShrink": 0, "WebkitFlexShrink": 0,
"WebkitTransition": "background-color 0.1s", "WebkitTransition": "0.1s",
"alignItems": "stretch", "alignItems": "stretch",
"backgroundColor": "#939393", "backgroundColor": "#939393",
"borderBottomLeftRadius": "10px", "borderBottomLeftRadius": "10px",
@@ -534,7 +540,7 @@ exports[`components/Switch value when "false" an unchecked checkbox is rendered
"textAlign": "inherit", "textAlign": "inherit",
"textDecoration": "none", "textDecoration": "none",
"top": "0px", "top": "0px",
"transition": "background-color 0.1s", "transition": "0.1s",
"width": "90%", "width": "90%",
} }
} /> } />
@@ -551,7 +557,8 @@ exports[`components/Switch value when "false" an unchecked checkbox is rendered
"WebkitFlexBasis": "auto", "WebkitFlexBasis": "auto",
"WebkitFlexDirection": "column", "WebkitFlexDirection": "column",
"WebkitFlexShrink": 0, "WebkitFlexShrink": 0,
"WebkitTransition": "background-color 0.1s", "WebkitTransform": "translateX(0%)",
"WebkitTransition": "0.1s",
"alignItems": "stretch", "alignItems": "stretch",
"alignSelf": "flex-start", "alignSelf": "flex-start",
"backgroundColor": "#FAFAFA", "backgroundColor": "#FAFAFA",
@@ -588,6 +595,7 @@ exports[`components/Switch value when "false" an unchecked checkbox is rendered
"msFlexItemAlign": "start", "msFlexItemAlign": "start",
"msFlexNegative": 0, "msFlexNegative": 0,
"msPreferredSize": "auto", "msPreferredSize": "auto",
"msTransform": "translateX(0%)",
"paddingBottom": "0px", "paddingBottom": "0px",
"paddingLeft": "0px", "paddingLeft": "0px",
"paddingRight": "0px", "paddingRight": "0px",
@@ -595,7 +603,8 @@ exports[`components/Switch value when "false" an unchecked checkbox is rendered
"position": "relative", "position": "relative",
"textAlign": "inherit", "textAlign": "inherit",
"textDecoration": "none", "textDecoration": "none",
"transition": "background-color 0.1s", "transform": "translateX(0%)",
"transition": "0.1s",
"width": "20px", "width": "20px",
} }
} /> } />
@@ -700,7 +709,7 @@ exports[`components/Switch value when "true" a checked checkbox is rendered 1`]
"WebkitFlexBasis": "auto", "WebkitFlexBasis": "auto",
"WebkitFlexDirection": "column", "WebkitFlexDirection": "column",
"WebkitFlexShrink": 0, "WebkitFlexShrink": 0,
"WebkitTransition": "background-color 0.1s", "WebkitTransition": "0.1s",
"alignItems": "stretch", "alignItems": "stretch",
"backgroundColor": "#A3D3CF", "backgroundColor": "#A3D3CF",
"borderBottomLeftRadius": "10px", "borderBottomLeftRadius": "10px",
@@ -745,7 +754,7 @@ exports[`components/Switch value when "true" a checked checkbox is rendered 1`]
"textAlign": "inherit", "textAlign": "inherit",
"textDecoration": "none", "textDecoration": "none",
"top": "0px", "top": "0px",
"transition": "background-color 0.1s", "transition": "0.1s",
"width": "90%", "width": "90%",
} }
} /> } />
@@ -755,16 +764,17 @@ exports[`components/Switch value when "true" a checked checkbox is rendered 1`]
Object { Object {
"MozBoxSizing": "border-box", "MozBoxSizing": "border-box",
"WebkitAlignItems": "stretch", "WebkitAlignItems": "stretch",
"WebkitAlignSelf": "flex-end", "WebkitAlignSelf": "flex-start",
"WebkitBoxAlign": "stretch", "WebkitBoxAlign": "stretch",
"WebkitBoxDirection": "normal", "WebkitBoxDirection": "normal",
"WebkitBoxOrient": "vertical", "WebkitBoxOrient": "vertical",
"WebkitFlexBasis": "auto", "WebkitFlexBasis": "auto",
"WebkitFlexDirection": "column", "WebkitFlexDirection": "column",
"WebkitFlexShrink": 0, "WebkitFlexShrink": 0,
"WebkitTransition": "background-color 0.1s", "WebkitTransform": "translateX(100%)",
"WebkitTransition": "0.1s",
"alignItems": "stretch", "alignItems": "stretch",
"alignSelf": "flex-end", "alignSelf": "flex-start",
"backgroundColor": "#009688", "backgroundColor": "#009688",
"borderBottomLeftRadius": "100%", "borderBottomLeftRadius": "100%",
"borderBottomRightRadius": "100%", "borderBottomRightRadius": "100%",
@@ -796,9 +806,10 @@ exports[`components/Switch value when "true" a checked checkbox is rendered 1`]
"minWidth": "0px", "minWidth": "0px",
"msFlexAlign": "stretch", "msFlexAlign": "stretch",
"msFlexDirection": "column", "msFlexDirection": "column",
"msFlexItemAlign": "end", "msFlexItemAlign": "start",
"msFlexNegative": 0, "msFlexNegative": 0,
"msPreferredSize": "auto", "msPreferredSize": "auto",
"msTransform": "translateX(100%)",
"paddingBottom": "0px", "paddingBottom": "0px",
"paddingLeft": "0px", "paddingLeft": "0px",
"paddingRight": "0px", "paddingRight": "0px",
@@ -806,7 +817,8 @@ exports[`components/Switch value when "true" a checked checkbox is rendered 1`]
"position": "relative", "position": "relative",
"textAlign": "inherit", "textAlign": "inherit",
"textDecoration": "none", "textDecoration": "none",
"transition": "background-color 0.1s", "transform": "translateX(100%)",
"transition": "0.1s",
"width": "20px", "width": "20px",
} }
} /> } />

View File

@@ -88,9 +88,9 @@ class Switch extends Component {
const thumbStyle = [ const thumbStyle = [
styles.thumb, styles.thumb,
{ {
alignSelf: value ? 'flex-end' : 'flex-start',
backgroundColor: thumbCurrentColor, backgroundColor: thumbCurrentColor,
height: thumbHeight, height: thumbHeight,
transform: [ { translateX: value ? '100%' : '0%' } ],
width: thumbWidth width: thumbWidth
}, },
disabled && styles.disabledThumb disabled && styles.disabledThumb
@@ -151,16 +151,17 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
height: '70%', height: '70%',
margin: 'auto', margin: 'auto',
transition: 'background-color 0.1s', transition: '0.1s',
width: '90%' width: '90%'
}, },
disabledTrack: { disabledTrack: {
backgroundColor: '#D5D5D5' backgroundColor: '#D5D5D5'
}, },
thumb: { thumb: {
alignSelf: 'flex-start',
borderRadius: '100%', borderRadius: '100%',
boxShadow: thumbDefaultBoxShadow, boxShadow: thumbDefaultBoxShadow,
transition: 'background-color 0.1s' transition: '0.1s'
}, },
disabledThumb: { disabledThumb: {
backgroundColor: '#BDBDBD' backgroundColor: '#BDBDBD'

View File

@@ -1,7 +1,6 @@
exports[`components/Text prop "children" 1`] = ` exports[`components/Text prop "children" 1`] = `
<span <span
className="" className=""
onClick={[Function]}
style={ style={
Object { Object {
"borderBottomWidth": "0px", "borderBottomWidth": "0px",
@@ -27,10 +26,39 @@ exports[`components/Text prop "children" 1`] = `
</span> </span>
`; `;
exports[`components/Text prop "selectable" 1`] = ` exports[`components/Text prop "onPress" 1`] = `
<span <span
className="" className=""
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"borderBottomWidth": "0px",
"borderLeftWidth": "0px",
"borderRightWidth": "0px",
"borderTopWidth": "0px",
"color": "inherit",
"cursor": "pointer",
"display": "inline",
"font": "inherit",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"marginTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"paddingTop": "0px",
"textDecoration": "none",
"wordWrap": "break-word",
}
}
tabIndex={0} />
`;
exports[`components/Text prop "selectable" 1`] = `
<span
className=""
style={ style={
Object { Object {
"borderBottomWidth": "0px", "borderBottomWidth": "0px",
@@ -57,7 +85,6 @@ exports[`components/Text prop "selectable" 1`] = `
exports[`components/Text prop "selectable" 2`] = ` exports[`components/Text prop "selectable" 2`] = `
<span <span
className="" className=""
onClick={[Function]}
style={ style={
Object { Object {
"MozUserSelect": "none", "MozUserSelect": "none",

View File

@@ -14,6 +14,12 @@ describe('components/Text', () => {
test('prop "numberOfLines"'); test('prop "numberOfLines"');
test('prop "onPress"', () => {
const onPress = (e) => {};
const component = renderer.create(<Text onPress={onPress} />);
expect(component.toJSON()).toMatchSnapshot();
});
test('prop "selectable"', () => { test('prop "selectable"', () => {
let component = renderer.create(<Text />); let component = renderer.create(<Text />);
expect(component.toJSON()).toMatchSnapshot(); expect(component.toJSON()).toMatchSnapshot();

View File

@@ -29,6 +29,7 @@ class Text extends Component {
render() { render() {
const { const {
numberOfLines, numberOfLines,
onPress,
selectable, selectable,
style, style,
/* eslint-disable */ /* eslint-disable */
@@ -37,26 +38,35 @@ class Text extends Component {
ellipsizeMode, ellipsizeMode,
minimumFontScale, minimumFontScale,
onLayout, onLayout,
onPress,
suppressHighlighting, suppressHighlighting,
/* eslint-enable */ /* eslint-enable */
...other ...other
} = this.props; } = this.props;
if (onPress) {
other.onClick = onPress;
other.onKeyDown = this._createEnterHandler(onPress);
other.tabIndex = 0;
}
return createDOMElement('span', { return createDOMElement('span', {
...other, ...other,
onClick: this._onPress,
style: [ style: [
styles.initial, styles.initial,
style, style,
!selectable && styles.notSelectable, !selectable && styles.notSelectable,
numberOfLines === 1 && styles.singleLineStyle numberOfLines === 1 && styles.singleLineStyle,
onPress && styles.pressable
] ]
}); });
} }
_onPress = (e) => { _createEnterHandler(fn) {
if (this.props.onPress) { this.props.onPress(e); } return (e) => {
if (e.keyCode === 13) {
fn && fn(e);
}
};
} }
} }
@@ -74,6 +84,9 @@ const styles = StyleSheet.create({
notSelectable: { notSelectable: {
userSelect: 'none' userSelect: 'none'
}, },
pressable: {
cursor: 'pointer'
},
singleLineStyle: { singleLineStyle: {
maxWidth: '100%', maxWidth: '100%',
overflow: 'hidden', overflow: 'hidden',

View File

@@ -270,7 +270,6 @@ const styles = StyleSheet.create({
borderWidth: 0, borderWidth: 0,
boxSizing: 'border-box', boxSizing: 'border-box',
color: 'inherit', color: 'inherit',
flex: 1,
font: 'inherit', font: 'inherit',
padding: 0 padding: 0
} }

View File

@@ -9,10 +9,12 @@ import Animated from './apis/Animated';
import AppRegistry from './apis/AppRegistry'; import AppRegistry from './apis/AppRegistry';
import AppState from './apis/AppState'; import AppState from './apis/AppState';
import AsyncStorage from './apis/AsyncStorage'; import AsyncStorage from './apis/AsyncStorage';
import Clipboard from './apis/Clipboard';
import Dimensions from './apis/Dimensions'; import Dimensions from './apis/Dimensions';
import Easing from 'animated/lib/Easing'; import Easing from 'animated/lib/Easing';
import I18nManager from './apis/I18nManager'; import I18nManager from './apis/I18nManager';
import InteractionManager from './apis/InteractionManager'; import InteractionManager from './apis/InteractionManager';
import Linking from './apis/Linking';
import NetInfo from './apis/NetInfo'; import NetInfo from './apis/NetInfo';
import PanResponder from './apis/PanResponder'; import PanResponder from './apis/PanResponder';
import PixelRatio from './apis/PixelRatio'; import PixelRatio from './apis/PixelRatio';
@@ -23,6 +25,7 @@ import Vibration from './apis/Vibration';
// components // components
import ActivityIndicator from './components/ActivityIndicator'; import ActivityIndicator from './components/ActivityIndicator';
import Button from './components/Button';
import Image from './components/Image'; import Image from './components/Image';
import ListView from './components/ListView'; import ListView from './components/ListView';
import ProgressBar from './components/ProgressBar'; import ProgressBar from './components/ProgressBar';
@@ -56,10 +59,12 @@ const ReactNative = {
AppRegistry, AppRegistry,
AppState, AppState,
AsyncStorage, AsyncStorage,
Clipboard,
Dimensions, Dimensions,
Easing, Easing,
I18nManager, I18nManager,
InteractionManager, InteractionManager,
Linking,
NetInfo, NetInfo,
PanResponder, PanResponder,
PixelRatio, PixelRatio,
@@ -70,6 +75,7 @@ const ReactNative = {
// components // components
ActivityIndicator, ActivityIndicator,
Button,
Image, Image,
ListView, ListView,
ProgressBar, ProgressBar,

View File

@@ -12,14 +12,7 @@ exports[`modules/createDOMElement prop "accessibilityLiveRegion" 1`] = `
style={Object {}} /> style={Object {}} />
`; `;
exports[`modules/createDOMElement prop "accessibilityRole" 1`] = ` exports[`modules/createDOMElement prop "accessibilityRole" button 1`] = `
<header
className=""
role="banner"
style={Object {}} />
`;
exports[`modules/createDOMElement prop "accessibilityRole" 2`] = `
<button <button
className="" className=""
role="button" role="button"
@@ -27,6 +20,22 @@ exports[`modules/createDOMElement prop "accessibilityRole" 2`] = `
type="button" /> type="button" />
`; `;
exports[`modules/createDOMElement prop "accessibilityRole" link and target="_blank" 1`] = `
<a
className=""
rel=" noopener noreferrer"
role="link"
style={Object {}}
target="_blank" />
`;
exports[`modules/createDOMElement prop "accessibilityRole" roles 1`] = `
<header
className=""
role="banner"
style={Object {}} />
`;
exports[`modules/createDOMElement prop "accessible" 1`] = ` exports[`modules/createDOMElement prop "accessible" 1`] = `
<span <span
className="" className=""

View File

@@ -23,12 +23,21 @@ describe('modules/createDOMElement', () => {
expect(component.toJSON()).toMatchSnapshot(); expect(component.toJSON()).toMatchSnapshot();
}); });
test('prop "accessibilityRole"', () => { describe('prop "accessibilityRole"', () => {
const accessibilityRole = 'banner'; test('roles', () => {
let component = renderer.create(createDOMElement('span', { accessibilityRole })); const component = renderer.create(createDOMElement('span', { accessibilityRole: 'banner' }));
expect(component.toJSON()).toMatchSnapshot(); expect(component.toJSON()).toMatchSnapshot();
component = renderer.create(createDOMElement('span', { accessibilityRole: 'button' })); });
expect(component.toJSON()).toMatchSnapshot();
test('button', () => {
const component = renderer.create(createDOMElement('span', { accessibilityRole: 'button' }));
expect(component.toJSON()).toMatchSnapshot();
});
test('link and target="_blank"', () => {
const component = renderer.create(createDOMElement('span', { accessibilityRole: 'link', target: '_blank' }));
expect(component.toJSON()).toMatchSnapshot();
});
}); });
test('prop "accessible"', () => { test('prop "accessible"', () => {

View File

@@ -42,6 +42,8 @@ const createDOMElement = (component, rnProps = {}) => {
domProps.role = accessibilityRole; domProps.role = accessibilityRole;
if (accessibilityRole === 'button') { if (accessibilityRole === 'button') {
domProps.type = 'button'; domProps.type = 'button';
} else if (accessibilityRole === 'link' && domProps.target === '_blank') {
domProps.rel = `${domProps.rel || ''} noopener noreferrer`;
} }
} }
if (type) { domProps.type = type; } if (type) { domProps.type = type; }

View File

@@ -1,4 +1,4 @@
const CSS_UNIT_RE = /^[+-]?\d*(?:\.\d+)?(?:[Ee][+-]?\d+)?(\w*)/; const CSS_UNIT_RE = /^[+-]?\d*(?:\.\d+)?(?:[Ee][+-]?\d+)?(%|\w*)/;
const getUnit = (str) => str.match(CSS_UNIT_RE)[1]; const getUnit = (str) => str.match(CSS_UNIT_RE)[1];