mirror of
https://github.com/status-im/react-native.git
synced 2025-01-14 19:44:13 +00:00
[ReactNative][RFC] Yellow Box for warnings
This commit is contained in:
parent
bae4e44c60
commit
3f723f451d
398
Libraries/ReactIOS/WarningBox.js
Normal file
398
Libraries/ReactIOS/WarningBox.js
Normal file
@ -0,0 +1,398 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule WarningBox
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var AsyncStorage = require('AsyncStorage');
|
||||
var EventEmitter = require('EventEmitter');
|
||||
var Map = require('Map');
|
||||
var PanResponder = require('PanResponder');
|
||||
var React = require('React');
|
||||
var StyleSheet = require('StyleSheet');
|
||||
var Text = require('Text');
|
||||
var TouchableOpacity = require('TouchableOpacity');
|
||||
var View = require('View');
|
||||
|
||||
var invariant = require('invariant');
|
||||
var rebound = require('rebound');
|
||||
var stringifySafe = require('stringifySafe');
|
||||
|
||||
var SCREEN_WIDTH = require('Dimensions').get('window').width;
|
||||
var IGNORED_WARNINGS_KEY = '__DEV_WARNINGS_IGNORED';
|
||||
|
||||
var consoleWarn = console.warn.bind(console);
|
||||
|
||||
var warningCounts = new Map();
|
||||
var ignoredWarnings: Array<string> = [];
|
||||
var totalWarningCount = 0;
|
||||
var warningCountEvents = new EventEmitter();
|
||||
|
||||
/**
|
||||
* WarningBox renders warnings on top of the app being developed. Warnings help
|
||||
* guard against subtle yet significant issues that can impact the quality of
|
||||
* your application, such as accessibility and memory leaks. This "in your
|
||||
* face" style of warning allows developers to notice and correct these issues
|
||||
* as quickly as possible.
|
||||
*
|
||||
* The warning box is currently opt-in. Set the following flag to enable it:
|
||||
*
|
||||
* `console.yellowBoxEnabled = true;`
|
||||
*
|
||||
* If "ignore" is tapped on a warning, the WarningBox will record that warning
|
||||
* and will not display it again. This is useful for hiding errors that already
|
||||
* exist or have been introduced elsewhere. To re-enable all of the errors, set
|
||||
* the following:
|
||||
*
|
||||
* `console.yellowBoxResetIgnored = true;`
|
||||
*
|
||||
* This can also be set permanently, and ignore will only silence the warnings
|
||||
* until the next refresh.
|
||||
*/
|
||||
|
||||
if (__DEV__) {
|
||||
console.warn = function() {
|
||||
consoleWarn.apply(null, arguments);
|
||||
if (!console.yellowBoxEnabled) {
|
||||
return;
|
||||
}
|
||||
var warning = Array.prototype.map.call(arguments, stringifySafe).join(' ');
|
||||
if (!console.yellowBoxResetIgnored &&
|
||||
ignoredWarnings.indexOf(warning) !== -1) {
|
||||
return;
|
||||
}
|
||||
var count = warningCounts.has(warning) ? warningCounts.get(warning) + 1 : 1;
|
||||
warningCounts.set(warning, count);
|
||||
totalWarningCount += 1;
|
||||
warningCountEvents.emit('count', totalWarningCount);
|
||||
};
|
||||
}
|
||||
|
||||
function saveIgnoredWarnings() {
|
||||
AsyncStorage.setItem(
|
||||
IGNORED_WARNINGS_KEY,
|
||||
JSON.stringify(ignoredWarnings),
|
||||
function(err) {
|
||||
if (err) {
|
||||
console.warn('Could not save ignored warnings.', err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
AsyncStorage.getItem(IGNORED_WARNINGS_KEY, function(err, data) {
|
||||
if (!err && data && !console.yellowBoxResetIgnored) {
|
||||
ignoredWarnings = JSON.parse(data);
|
||||
}
|
||||
});
|
||||
|
||||
var WarningRow = React.createClass({
|
||||
componentWillMount: function() {
|
||||
this.springSystem = new rebound.SpringSystem();
|
||||
this.dismissalSpring = this.springSystem.createSpring();
|
||||
this.dismissalSpring.setRestSpeedThreshold(0.05);
|
||||
this.dismissalSpring.setCurrentValue(0);
|
||||
this.dismissalSpring.addListener({
|
||||
onSpringUpdate: () => {
|
||||
var val = this.dismissalSpring.getCurrentValue();
|
||||
this.text && this.text.setNativeProps({
|
||||
left: SCREEN_WIDTH * val,
|
||||
});
|
||||
this.container && this.container.setNativeProps({
|
||||
opacity: 1 - val,
|
||||
});
|
||||
this.closeButton && this.closeButton.setNativeProps({
|
||||
opacity: 1 - (val * 5),
|
||||
});
|
||||
},
|
||||
onSpringAtRest: () => {
|
||||
if (this.dismissalSpring.getCurrentValue()) {
|
||||
this.collapseSpring.setEndValue(1);
|
||||
}
|
||||
},
|
||||
});
|
||||
this.collapseSpring = this.springSystem.createSpring();
|
||||
this.collapseSpring.setRestSpeedThreshold(0.05);
|
||||
this.collapseSpring.setCurrentValue(0);
|
||||
this.collapseSpring.getSpringConfig().friction = 20;
|
||||
this.collapseSpring.getSpringConfig().tension = 200;
|
||||
this.collapseSpring.addListener({
|
||||
onSpringUpdate: () => {
|
||||
var val = this.collapseSpring.getCurrentValue();
|
||||
this.container && this.container.setNativeProps({
|
||||
height: Math.abs(46 - (val * 46)),
|
||||
});
|
||||
},
|
||||
onSpringAtRest: () => {
|
||||
this.props.onDismissed();
|
||||
},
|
||||
});
|
||||
this.panGesture = PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => {
|
||||
return !! this.dismissalSpring.getCurrentValue();
|
||||
},
|
||||
onMoveShouldSetPanResponder: () => true,
|
||||
onPanResponderGrant: () => {
|
||||
this.isResponderOnlyToBlockTouches =
|
||||
!! this.dismissalSpring.getCurrentValue();
|
||||
},
|
||||
onPanResponderMove: (e, gestureState) => {
|
||||
if (this.isResponderOnlyToBlockTouches) {
|
||||
return;
|
||||
}
|
||||
this.dismissalSpring.setCurrentValue(gestureState.dx / SCREEN_WIDTH);
|
||||
},
|
||||
onPanResponderRelease: (e, gestureState) => {
|
||||
if (this.isResponderOnlyToBlockTouches) {
|
||||
return;
|
||||
}
|
||||
var gestureCompletion = gestureState.dx / SCREEN_WIDTH;
|
||||
var doesGestureRelease = (gestureState.vx + gestureCompletion) > 0.5;
|
||||
this.dismissalSpring.setEndValue(doesGestureRelease ? 1 : 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
var countText;
|
||||
if (warningCounts.get(this.props.warning) > 1) {
|
||||
countText = (
|
||||
<Text style={styles.bold}>
|
||||
({warningCounts.get(this.props.warning)}){" "}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View
|
||||
style={styles.warningBox}
|
||||
ref={container => { this.container = container; }}
|
||||
{...this.panGesture.panHandlers}>
|
||||
<TouchableOpacity
|
||||
onPress={this.props.onOpened}>
|
||||
<View>
|
||||
<Text
|
||||
style={styles.warningText}
|
||||
numberOfLines={2}
|
||||
ref={text => { this.text = text; }}>
|
||||
{countText}
|
||||
{this.props.warning}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View
|
||||
ref={closeButton => { this.closeButton = closeButton; }}
|
||||
style={styles.closeButton}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
this.dismissalSpring.setEndValue(1);
|
||||
}}>
|
||||
<Text style={styles.closeButtonText}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var WarningBoxOpened = React.createClass({
|
||||
render: function() {
|
||||
var countText;
|
||||
if (warningCounts.get(this.props.warning) > 1) {
|
||||
countText = (
|
||||
<Text style={styles.bold}>
|
||||
({warningCounts.get(this.props.warning)}){" "}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={this.props.onClose}>
|
||||
<View style={styles.yellowBox}>
|
||||
<Text style={styles.yellowBoxText}>
|
||||
{countText}
|
||||
{this.props.warning}
|
||||
</Text>
|
||||
<View style={styles.yellowBoxButtons}>
|
||||
<View style={styles.yellowBoxButton}>
|
||||
<TouchableOpacity
|
||||
onPress={this.props.onDismissed}>
|
||||
<Text style={styles.yellowBoxButtonText}>
|
||||
Dismiss
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.yellowBoxButton}>
|
||||
<TouchableOpacity
|
||||
onPress={this.props.onIgnored}>
|
||||
<Text style={styles.yellowBoxButtonText}>
|
||||
Ignore
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
var canMountWarningBox = true;
|
||||
|
||||
var WarningBox = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
totalWarningCount,
|
||||
openWarning: null,
|
||||
};
|
||||
},
|
||||
componentWillMount: function() {
|
||||
if (console.yellowBoxResetIgnored) {
|
||||
AsyncStorage.setItem(IGNORED_WARNINGS_KEY, '[]', function(err) {
|
||||
if (err) {
|
||||
console.warn('Could not reset ignored warnings.', err);
|
||||
}
|
||||
});
|
||||
ignoredWarnings = [];
|
||||
}
|
||||
},
|
||||
componentDidMount: function() {
|
||||
invariant(
|
||||
canMountWarningBox,
|
||||
'There can only be one WarningBox'
|
||||
);
|
||||
canMountWarningBox = false;
|
||||
warningCountEvents.addListener(
|
||||
'count',
|
||||
this._onWarningCount
|
||||
);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
warningCountEvents.removeAllListeners();
|
||||
canMountWarningBox = true;
|
||||
},
|
||||
_onWarningCount: function(totalWarningCount) {
|
||||
// Must use setImmediate because warnings often happen during render and
|
||||
// state cannot be set while rendering
|
||||
setImmediate(() => {
|
||||
this.setState({ totalWarningCount, });
|
||||
});
|
||||
},
|
||||
_onDismiss: function(warning) {
|
||||
warningCounts.delete(warning);
|
||||
this.setState({
|
||||
openWarning: null,
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
if (warningCounts.size === 0) {
|
||||
return <View />;
|
||||
}
|
||||
if (this.state.openWarning) {
|
||||
return (
|
||||
<WarningBoxOpened
|
||||
warning={this.state.openWarning}
|
||||
onClose={() => {
|
||||
this.setState({ openWarning: null });
|
||||
}}
|
||||
onDismissed={this._onDismiss.bind(this, this.state.openWarning)}
|
||||
onIgnored={() => {
|
||||
ignoredWarnings.push(this.state.openWarning);
|
||||
saveIgnoredWarnings();
|
||||
this._onDismiss(this.state.openWarning);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
var warningRows = [];
|
||||
warningCounts.forEach((count, warning) => {
|
||||
warningRows.push(
|
||||
<WarningRow
|
||||
key={warning}
|
||||
onOpened={() => {
|
||||
this.setState({ openWarning: warning });
|
||||
}}
|
||||
onDismissed={this._onDismiss.bind(this, warning)}
|
||||
warning={warning}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<View style={styles.warningContainer}>
|
||||
{warningRows}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
bold: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
height: 46,
|
||||
width: 46,
|
||||
},
|
||||
closeButtonText: {
|
||||
color: 'white',
|
||||
fontSize: 32,
|
||||
position: 'relative',
|
||||
left: 8,
|
||||
},
|
||||
warningContainer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0
|
||||
},
|
||||
warningBox: {
|
||||
position: 'relative',
|
||||
backgroundColor: 'rgba(171, 124, 36, 0.9)',
|
||||
flex: 1,
|
||||
height: 46,
|
||||
},
|
||||
warningText: {
|
||||
color: 'white',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
marginLeft: 15,
|
||||
marginRight: 46,
|
||||
top: 7,
|
||||
},
|
||||
yellowBox: {
|
||||
backgroundColor: 'rgba(171, 124, 36, 0.9)',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
padding: 15,
|
||||
paddingTop: 35,
|
||||
},
|
||||
yellowBoxText: {
|
||||
color: 'white',
|
||||
fontSize: 20,
|
||||
},
|
||||
yellowBoxButtons: {
|
||||
flexDirection: 'row',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
},
|
||||
yellowBoxButton: {
|
||||
flex: 1,
|
||||
padding: 25,
|
||||
},
|
||||
yellowBoxButtonText: {
|
||||
color: 'white',
|
||||
fontSize: 16,
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = WarningBox;
|
@ -12,6 +12,9 @@
|
||||
'use strict';
|
||||
|
||||
var React = require('React');
|
||||
var StyleSheet = require('StyleSheet');
|
||||
var View = require('View');
|
||||
var WarningBox = require('WarningBox');
|
||||
|
||||
var invariant = require('invariant');
|
||||
|
||||
@ -24,12 +27,27 @@ function renderApplication<D, P, S>(
|
||||
rootTag,
|
||||
'Expect to have a valid rootTag, instead got ', rootTag
|
||||
);
|
||||
var shouldRenderWarningBox = __DEV__ && console.yellowBoxEnabled;
|
||||
var warningBox = shouldRenderWarningBox ? <WarningBox /> : null;
|
||||
React.render(
|
||||
<RootComponent
|
||||
{...initialProps}
|
||||
/>,
|
||||
<View style={styles.appContainer}>
|
||||
<RootComponent
|
||||
{...initialProps}
|
||||
/>
|
||||
{warningBox}
|
||||
</View>,
|
||||
rootTag
|
||||
);
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
appContainer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = renderApplication;
|
||||
|
Loading…
x
Reference in New Issue
Block a user