mirror of
https://github.com/zhigang1992/react-native-web.git
synced 2026-04-01 17:29:34 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e3cfc5325 | ||
|
|
da86ea98fc | ||
|
|
5f3e422b5c | ||
|
|
1f1f89b062 | ||
|
|
0f79960b85 | ||
|
|
117ce59f27 | ||
|
|
214121480e | ||
|
|
6261536f57 | ||
|
|
a748b7e606 | ||
|
|
92952ee746 |
@@ -1,10 +1,10 @@
|
||||
# Performance
|
||||
|
||||
To run these benchmarks:
|
||||
To run these benchmarks from the root of the project:
|
||||
|
||||
```
|
||||
npm run build:performance
|
||||
open ./performance/index.html
|
||||
yarn benchmark
|
||||
open ./benchmarks/index.html
|
||||
```
|
||||
|
||||
Append `?fastest` to the URL to include the fastest "other libraries", and
|
||||
|
||||
@@ -32,8 +32,9 @@ const NetInfoScreen = () => (
|
||||
<DocItem
|
||||
description={
|
||||
<AppText>
|
||||
One of <Code>slow-2g</Code>, <Code>2g</Code>, <Code>3g</Code>, <Code>4g</Code>,{' '}
|
||||
<Code>unknown</Code>.
|
||||
One of <Code>bluebooth</Code>, <Code>cellular</Code>, <Code>ethernet</Code>,{' '}
|
||||
<Code>mixed</Code>, <Code>mixed</Code>, <Code>none</Code>, <Code>other</Code>,{' '}
|
||||
<Code>unknown</Code>, <Code>wifi</Code>, <Code>wimax</Code>
|
||||
</AppText>
|
||||
}
|
||||
name="ConnectionType"
|
||||
@@ -41,9 +42,8 @@ const NetInfoScreen = () => (
|
||||
<DocItem
|
||||
description={
|
||||
<AppText>
|
||||
One of <Code>bluebooth</Code>, <Code>cellular</Code>, <Code>ethernet</Code>,{' '}
|
||||
<Code>mixed</Code>, <Code>mixed</Code>, <Code>none</Code>, <Code>other</Code>,{' '}
|
||||
<Code>unknown</Code>, <Code>wifi</Code>, <Code>wimax</Code>
|
||||
One of <Code>slow-2g</Code>, <Code>2g</Code>, <Code>3g</Code>, <Code>4g</Code>,{' '}
|
||||
<Code>unknown</Code>.
|
||||
</AppText>
|
||||
}
|
||||
name="EffectiveConnectionType"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-native-web",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.16",
|
||||
"description": "React Native for Web",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import NetInfo from '..';
|
||||
|
||||
const handler = () => {};
|
||||
|
||||
describe('apis/NetInfo', () => {
|
||||
describe('getConnectionInfo', () => {
|
||||
test('fills out basic fields', done => {
|
||||
@@ -13,9 +15,22 @@ describe('apis/NetInfo', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConnected', () => {
|
||||
const handler = () => {};
|
||||
describe('addEventListener', () => {
|
||||
test('throws if the provided "eventType" is not supported', () => {
|
||||
expect(() => NetInfo.addEventListener('foo', handler)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeEventListener', () => {
|
||||
test('throws if the provided "eventType" is not supported', () => {
|
||||
expect(() => NetInfo.removeEventListener('foo', handler)).toThrow();
|
||||
});
|
||||
test('throws if the handler is not registered', () => {
|
||||
expect(() => NetInfo.removeEventListener('connectionChange', handler)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConnected', () => {
|
||||
afterEach(() => {
|
||||
try {
|
||||
NetInfo.isConnected.removeEventListener('connectionChange', handler);
|
||||
@@ -25,22 +40,18 @@ describe('apis/NetInfo', () => {
|
||||
describe('addEventListener', () => {
|
||||
test('throws if the provided "eventType" is not supported', () => {
|
||||
expect(() => NetInfo.isConnected.addEventListener('foo', handler)).toThrow();
|
||||
expect(() =>
|
||||
NetInfo.isConnected.addEventListener('connectionChange', handler)
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeEventListener', () => {
|
||||
test('throws if the handler is not registered', () => {
|
||||
expect(() => NetInfo.isConnected.removeEventListener('connectionChange', handler)).toThrow;
|
||||
});
|
||||
|
||||
test('throws if the provided "eventType" is not supported', () => {
|
||||
NetInfo.isConnected.addEventListener('connectionChange', handler);
|
||||
expect(() => NetInfo.isConnected.removeEventListener('foo', handler)).toThrow;
|
||||
expect(() => NetInfo.isConnected.removeEventListener('connectionChange', handler)).not
|
||||
.toThrow;
|
||||
expect(() => NetInfo.isConnected.removeEventListener('foo', handler)).toThrow();
|
||||
});
|
||||
test('throws if the handler is not registered', () => {
|
||||
expect(() =>
|
||||
NetInfo.isConnected.removeEventListener('connectionChange', handler)
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,13 +23,17 @@ const connection =
|
||||
// Prevent the underlying event handlers from leaking and include additional
|
||||
// properties available in browsers
|
||||
const getConnectionInfoObject = () => {
|
||||
const result = {};
|
||||
const result = {
|
||||
effectiveType: 'unknown',
|
||||
type: 'unknown'
|
||||
};
|
||||
if (!connection) {
|
||||
return result;
|
||||
}
|
||||
for (const prop in connection) {
|
||||
if (typeof connection[prop] !== 'function') {
|
||||
result[prop] = connection[prop];
|
||||
const value = connection[prop];
|
||||
if (typeof value !== 'function' && value != null) {
|
||||
result[prop] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -43,6 +47,7 @@ const eventTypesMap = {
|
||||
const eventTypes = Object.keys(eventTypesMap);
|
||||
|
||||
const connectionListeners = [];
|
||||
const netInfoListeners = [];
|
||||
|
||||
/**
|
||||
* Navigator online: https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine
|
||||
@@ -63,21 +68,29 @@ const NetInfo = {
|
||||
};
|
||||
}
|
||||
|
||||
connection.addEventListener(eventTypesMap[type], handler);
|
||||
const wrappedHandler = () => handler(getConnectionInfoObject());
|
||||
netInfoListeners.push([handler, wrappedHandler]);
|
||||
connection.addEventListener(eventTypesMap[type], wrappedHandler);
|
||||
return {
|
||||
remove: () => NetInfo.removeEventListener(eventTypesMap[type], handler)
|
||||
};
|
||||
},
|
||||
|
||||
removeEventListener(type: string, handler: Function): void {
|
||||
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type);
|
||||
invariant(
|
||||
eventTypes.indexOf(type) !== -1,
|
||||
'Trying to unsubscribe from unknown event: "%s"',
|
||||
type
|
||||
);
|
||||
if (type === 'change') {
|
||||
console.warn('Listening to event `change` is deprecated. Use `connectionChange` instead.');
|
||||
}
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
connection.removeEventListener(eventTypesMap[type], handler);
|
||||
|
||||
const listenerIndex = findIndex(netInfoListeners, pair => pair[0] === handler);
|
||||
invariant(listenerIndex !== -1, 'Trying to remove NetInfo listener for unregistered handler');
|
||||
const [, wrappedHandler] = netInfoListeners[listenerIndex];
|
||||
connection.removeEventListener(eventTypesMap[type], wrappedHandler);
|
||||
netInfoListeners.splice(listenerIndex, 1);
|
||||
},
|
||||
|
||||
fetch(): Promise<any> {
|
||||
@@ -93,11 +106,7 @@ const NetInfo = {
|
||||
|
||||
getConnectionInfo(): Promise<Object> {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
effectiveType: 'unknown',
|
||||
type: 'unknown',
|
||||
...getConnectionInfoObject()
|
||||
});
|
||||
resolve(getConnectionInfoObject());
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-env jasmine, jest */
|
||||
|
||||
import Image from '../';
|
||||
import ImageLoader from '../../../modules/ImageLoader';
|
||||
import ImageUriCache from '../ImageUriCache';
|
||||
import React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
@@ -86,6 +87,55 @@ describe('components/Image', () => {
|
||||
expect(component.find('img').prop('draggable')).toBe(false);
|
||||
});
|
||||
|
||||
describe('prop "onLoad"', () => {
|
||||
test('is called after image is loaded from network', () => {
|
||||
jest.useFakeTimers();
|
||||
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
|
||||
onLoad();
|
||||
});
|
||||
const onLoadStub = jest.fn();
|
||||
shallow(<Image onLoad={onLoadStub} source="https://test.com/img.jpg" />);
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(ImageLoader.load).toBeCalled();
|
||||
expect(onLoadStub).toBeCalled();
|
||||
});
|
||||
|
||||
test('is called after image is loaded from cache', () => {
|
||||
jest.useFakeTimers();
|
||||
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
|
||||
onLoad();
|
||||
});
|
||||
const onLoadStub = jest.fn();
|
||||
const uri = 'https://test.com/img.jpg';
|
||||
shallow(<Image onLoad={onLoadStub} source={uri} />);
|
||||
ImageUriCache.add(uri);
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(ImageLoader.load).not.toBeCalled();
|
||||
expect(onLoadStub).toBeCalled();
|
||||
ImageUriCache.remove(uri);
|
||||
});
|
||||
|
||||
test('is called on update if "uri" is different', () => {
|
||||
jest.useFakeTimers();
|
||||
const onLoadStub = jest.fn();
|
||||
const uri = 'https://test.com/img.jpg';
|
||||
const component = mount(<Image onLoad={onLoadStub} source={uri} />);
|
||||
component.setProps({ source: `https://blah.com/img.png` });
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(onLoadStub.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
test('is not called on update if "uri" is the same', () => {
|
||||
jest.useFakeTimers();
|
||||
const onLoadStub = jest.fn();
|
||||
const uri = 'https://test.com/img.jpg';
|
||||
const component = mount(<Image onLoad={onLoadStub} source={uri} />);
|
||||
component.setProps({ resizeMode: 'stretch' });
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(onLoadStub.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prop "resizeMode"', () => {
|
||||
[
|
||||
Image.resizeMode.contain,
|
||||
|
||||
@@ -140,6 +140,9 @@ class Image extends Component {
|
||||
this._isMounted = true;
|
||||
if (this._imageState === STATUS_PENDING) {
|
||||
this._createImageLoader();
|
||||
} else if (this._imageState === STATUS_LOADED) {
|
||||
const { onLoad } = this.props;
|
||||
onLoad && onLoad();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
import UnimplementedView from '../UnimplementedView';
|
||||
export default UnimplementedView;
|
||||
const Picker = UnimplementedView;
|
||||
Picker.Item = UnimplementedView;
|
||||
export default Picker;
|
||||
|
||||
@@ -49,6 +49,8 @@ const normalizeScrollEvent = e => ({
|
||||
* Encapsulates the Web-specific scroll throttling and disabling logic
|
||||
*/
|
||||
export default class ScrollViewBase extends Component {
|
||||
_viewRef: View;
|
||||
|
||||
static propTypes = {
|
||||
...ViewPropTypes,
|
||||
onMomentumScrollBegin: func,
|
||||
@@ -73,6 +75,66 @@ export default class ScrollViewBase extends Component {
|
||||
_debouncedOnScrollEnd = debounce(this._handleScrollEnd, 100);
|
||||
_state = { isScrolling: false, scrollLastTick: 0 };
|
||||
|
||||
setNativeProps(props: Object) {
|
||||
if (this._viewRef) {
|
||||
this._viewRef.setNativeProps(props);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
scrollEnabled,
|
||||
style,
|
||||
/* eslint-disable */
|
||||
alwaysBounceHorizontal,
|
||||
alwaysBounceVertical,
|
||||
automaticallyAdjustContentInsets,
|
||||
bounces,
|
||||
bouncesZoom,
|
||||
canCancelContentTouches,
|
||||
centerContent,
|
||||
contentInset,
|
||||
contentInsetAdjustmentBehavior,
|
||||
contentOffset,
|
||||
decelerationRate,
|
||||
directionalLockEnabled,
|
||||
endFillColor,
|
||||
indicatorStyle,
|
||||
keyboardShouldPersistTaps,
|
||||
maximumZoomScale,
|
||||
minimumZoomScale,
|
||||
onMomentumScrollBegin,
|
||||
onMomentumScrollEnd,
|
||||
onScrollBeginDrag,
|
||||
onScrollEndDrag,
|
||||
overScrollMode,
|
||||
pinchGestureEnabled,
|
||||
removeClippedSubviews,
|
||||
scrollEventThrottle,
|
||||
scrollIndicatorInsets,
|
||||
scrollPerfTag,
|
||||
scrollsToTop,
|
||||
showsHorizontalScrollIndicator,
|
||||
showsVerticalScrollIndicator,
|
||||
snapToInterval,
|
||||
snapToAlignment,
|
||||
zoomScale,
|
||||
/* eslint-enable */
|
||||
...other
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
{...other}
|
||||
onScroll={this._handleScroll}
|
||||
onTouchMove={this._createPreventableScrollHandler(this.props.onTouchMove)}
|
||||
onWheel={this._createPreventableScrollHandler(this.props.onWheel)}
|
||||
ref={this._setViewRef}
|
||||
style={[style, !scrollEnabled && styles.scrollDisabled]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_createPreventableScrollHandler = (handler: Function) => {
|
||||
return (e: Object) => {
|
||||
if (this.props.scrollEnabled) {
|
||||
@@ -124,63 +186,14 @@ export default class ScrollViewBase extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_setViewRef = (element: View) => {
|
||||
this._viewRef = element;
|
||||
};
|
||||
|
||||
_shouldEmitScrollEvent(lastTick: number, eventThrottle: number) {
|
||||
const timeSinceLastTick = Date.now() - lastTick;
|
||||
return eventThrottle > 0 && timeSinceLastTick >= eventThrottle;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
scrollEnabled,
|
||||
style,
|
||||
/* eslint-disable */
|
||||
alwaysBounceHorizontal,
|
||||
alwaysBounceVertical,
|
||||
automaticallyAdjustContentInsets,
|
||||
bounces,
|
||||
bouncesZoom,
|
||||
canCancelContentTouches,
|
||||
centerContent,
|
||||
contentInset,
|
||||
contentInsetAdjustmentBehavior,
|
||||
contentOffset,
|
||||
decelerationRate,
|
||||
directionalLockEnabled,
|
||||
endFillColor,
|
||||
indicatorStyle,
|
||||
keyboardShouldPersistTaps,
|
||||
maximumZoomScale,
|
||||
minimumZoomScale,
|
||||
onMomentumScrollBegin,
|
||||
onMomentumScrollEnd,
|
||||
onScrollBeginDrag,
|
||||
onScrollEndDrag,
|
||||
overScrollMode,
|
||||
pinchGestureEnabled,
|
||||
removeClippedSubviews,
|
||||
scrollEventThrottle,
|
||||
scrollIndicatorInsets,
|
||||
scrollPerfTag,
|
||||
scrollsToTop,
|
||||
showsHorizontalScrollIndicator,
|
||||
showsVerticalScrollIndicator,
|
||||
snapToInterval,
|
||||
snapToAlignment,
|
||||
zoomScale,
|
||||
/* eslint-enable */
|
||||
...other
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
{...other}
|
||||
onScroll={this._handleScroll}
|
||||
onTouchMove={this._createPreventableScrollHandler(this.props.onTouchMove)}
|
||||
onWheel={this._createPreventableScrollHandler(this.props.onWheel)}
|
||||
style={[style, !scrollEnabled && styles.scrollDisabled]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome doesn't support e.preventDefault in this case; touch-action must be
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
/* eslint-env jasmine, jest */
|
||||
|
||||
import React from 'react';
|
||||
import ScrollView from '..';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
describe('components/ScrollView', () => {
|
||||
test('NO TEST COVERAGE');
|
||||
test('instance method setNativeProps', () => {
|
||||
const instance = mount(<ScrollView />).instance();
|
||||
expect(() => {
|
||||
instance.setNativeProps();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,7 +49,9 @@ const ScrollView = createReactClass({
|
||||
},
|
||||
|
||||
setNativeProps(props: Object) {
|
||||
this._scrollViewRef.setNativeProps(props);
|
||||
if (this._scrollViewRef) {
|
||||
this._scrollViewRef.setNativeProps(props);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,9 +40,11 @@ const TextInputState = {
|
||||
* noop if the text field was already focused
|
||||
*/
|
||||
focusTextInput(textFieldNode: ?Object) {
|
||||
if (document.activeElement !== textFieldNode && textFieldNode !== null) {
|
||||
if (textFieldNode !== null) {
|
||||
this._currentlyFocusedNode = textFieldNode;
|
||||
UIManager.focus(textFieldNode);
|
||||
if (document.activeElement !== textFieldNode) {
|
||||
UIManager.focus(textFieldNode);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -52,9 +54,11 @@ const TextInputState = {
|
||||
* noop if it wasn't focused
|
||||
*/
|
||||
blurTextInput(textFieldNode: ?Object) {
|
||||
if (document.activeElement === textFieldNode && textFieldNode !== null) {
|
||||
if (textFieldNode !== null) {
|
||||
this._currentlyFocusedNode = null;
|
||||
UIManager.blur(textFieldNode);
|
||||
if (document.activeElement === textFieldNode) {
|
||||
UIManager.blur(textFieldNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -150,18 +150,12 @@ class TextInput extends Component {
|
||||
|
||||
static State = TextInputState;
|
||||
|
||||
blur() {
|
||||
TextInputState.blurTextInput(this._node);
|
||||
}
|
||||
blur: Function;
|
||||
|
||||
clear() {
|
||||
this._node.value = '';
|
||||
}
|
||||
|
||||
focus() {
|
||||
TextInputState.focusTextInput(this._node);
|
||||
}
|
||||
|
||||
isFocused() {
|
||||
return TextInputState.currentlyFocusedField() === this._node;
|
||||
}
|
||||
@@ -270,6 +264,7 @@ class TextInput extends Component {
|
||||
|
||||
_handleBlur = e => {
|
||||
const { onBlur } = this.props;
|
||||
TextInputState.blurTextInput(this._node);
|
||||
if (onBlur) {
|
||||
onBlur(e);
|
||||
}
|
||||
@@ -289,6 +284,7 @@ class TextInput extends Component {
|
||||
_handleFocus = e => {
|
||||
const { clearTextOnFocus, onFocus, selectTextOnFocus } = this.props;
|
||||
const node = this._node;
|
||||
TextInputState.focusTextInput(this._node);
|
||||
if (onFocus) {
|
||||
onFocus(e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user