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:
```
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.
@@ -53,6 +53,7 @@ Exported modules:
* Components
* [`ActivityIndicator`](docs/components/ActivityIndicator.md)
* [`Button`](docs/components/Button.md)
* [`Image`](docs/components/Image.md)
* [`ListView`](docs/components/ListView.md)
* [`ProgressBar`](docs/components/ProgressBar.md)
@@ -69,6 +70,7 @@ Exported modules:
* [`AppRegistry`](docs/apis/AppRegistry.md)
* [`AppState`](docs/apis/AppState.md)
* [`AsyncStorage`](docs/apis/AsyncStorage.md)
* [`Clipboard`](docs/apis/Clipboard.md)
* [`Dimensions`](docs/apis/Dimensions.md)
* [`I18nManager`](docs/apis/I18nManager.md)
* [`NativeMethods`](docs/apis/NativeMethods.md)

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "react-native-web",
"version": "0.0.56",
"version": "0.0.60",
"description": "React Native for Web",
"main": "dist/index.js",
"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 ImageResizeMode from './ImageResizeMode';
import ImageStylePropTypes from './ImageStylePropTypes';
import requestAnimationFrame from 'fbjs/lib/requestAnimationFrame';
import StyleSheet from '../../apis/StyleSheet';
import StyleSheetPropType from '../../propTypes/StyleSheetPropType';
import View from '../View';
@@ -193,7 +194,7 @@ class Image extends Component {
this._imageState = status;
const isLoaded = this._imageState === STATUS_LOADED;
if (isLoaded !== this.state.isLoaded) {
window.requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (this._isMounted) {
this.setState({ isLoaded });
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
exports[`components/Text prop "children" 1`] = `
<span
className=""
onClick={[Function]}
style={
Object {
"borderBottomWidth": "0px",
@@ -27,10 +26,39 @@ exports[`components/Text prop "children" 1`] = `
</span>
`;
exports[`components/Text prop "selectable" 1`] = `
exports[`components/Text prop "onPress" 1`] = `
<span
className=""
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={
Object {
"borderBottomWidth": "0px",
@@ -57,7 +85,6 @@ exports[`components/Text prop "selectable" 1`] = `
exports[`components/Text prop "selectable" 2`] = `
<span
className=""
onClick={[Function]}
style={
Object {
"MozUserSelect": "none",

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,14 +12,7 @@ exports[`modules/createDOMElement prop "accessibilityLiveRegion" 1`] = `
style={Object {}} />
`;
exports[`modules/createDOMElement prop "accessibilityRole" 1`] = `
<header
className=""
role="banner"
style={Object {}} />
`;
exports[`modules/createDOMElement prop "accessibilityRole" 2`] = `
exports[`modules/createDOMElement prop "accessibilityRole" button 1`] = `
<button
className=""
role="button"
@@ -27,6 +20,22 @@ exports[`modules/createDOMElement prop "accessibilityRole" 2`] = `
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`] = `
<span
className=""

View File

@@ -23,12 +23,21 @@ describe('modules/createDOMElement', () => {
expect(component.toJSON()).toMatchSnapshot();
});
test('prop "accessibilityRole"', () => {
const accessibilityRole = 'banner';
let component = renderer.create(createDOMElement('span', { accessibilityRole }));
expect(component.toJSON()).toMatchSnapshot();
component = renderer.create(createDOMElement('span', { accessibilityRole: 'button' }));
expect(component.toJSON()).toMatchSnapshot();
describe('prop "accessibilityRole"', () => {
test('roles', () => {
const component = renderer.create(createDOMElement('span', { accessibilityRole: 'banner' }));
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"', () => {

View File

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