Compare commits

...

10 Commits

Author SHA1 Message Date
Nicolas Gallagher
3e3cfc5325 0.1.16 2017-12-02 14:48:35 -08:00
Nicolas Gallagher
da86ea98fc [fix] NetInfo event listeners and types
* Fix 'addEventListener' handler registration.
* Fix event object provided to handlers.
* Fix event object type - always include 'type' and 'effectiveType'.
* Fix unit test semantics.
* Fix documented NetInfo types.

Close #724
2017-12-02 12:47:12 -08:00
Nicolas Gallagher
5f3e422b5c 0.1.15 2017-12-01 17:55:17 -08:00
Nicolas Gallagher
1f1f89b062 [fix] Image 'onLoad' callback on update
'onLoad' should not be called when a component updates, if the 'uri' is
unchanged.

Fixes a regression introduced by
92952ee746
2017-12-01 17:52:47 -08:00
Nicolas Gallagher
0f79960b85 0.1.14 2017-11-15 15:21:45 -08:00
Nicolas Gallagher
117ce59f27 [fix] TextInput focus/blur management
1. Focusing/blurring a TextInput should update TextInputState.
2. Using the focus/blur instance methods should trigger related events.

Close #715
2017-11-15 15:20:21 -08:00
Louis Lagrange
214121480e [fix] stub for Picker.Item
Add stub function for API compatibility.

Close #690
2017-11-15 14:45:33 -08:00
Li Jie
6261536f57 Fix benchmarks documentation
Close #706
2017-11-15 14:43:13 -08:00
Nicolas Gallagher
a748b7e606 [fix] ScrollView 'setNativeProps'
Fix #709
Close #710
2017-11-15 14:39:58 -08:00
Zero Cho
92952ee746 [fix] call Image 'onLoad' when image is loaded from cache
Fix #452
Close #712
2017-11-15 13:33:13 -08:00
13 changed files with 201 additions and 102 deletions

View File

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

View File

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

View File

@@ -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": [

View File

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

View File

@@ -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());
});
},

View File

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

View File

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

View File

@@ -1,2 +1,4 @@
import UnimplementedView from '../UnimplementedView';
export default UnimplementedView;
const Picker = UnimplementedView;
Picker.Item = UnimplementedView;
export default Picker;

View File

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

View File

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

View File

@@ -49,7 +49,9 @@ const ScrollView = createReactClass({
},
setNativeProps(props: Object) {
this._scrollViewRef.setNativeProps(props);
if (this._scrollViewRef) {
this._scrollViewRef.setNativeProps(props);
}
},
/**

View File

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

View File

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