RN: Revamp YellowBox for Warnings

Reviewed By: vjeux

Differential Revision: D2667624

fb-gh-sync-id: f3c6ed63f3138edd13e7fe283cf877d598018813
This commit is contained in:
Tim Yung 2015-11-20 13:05:34 -08:00 committed by facebook-github-bot-7
parent b641d3de37
commit 8ab51828ff
5 changed files with 361 additions and 404 deletions

View File

@ -15,19 +15,20 @@
*/ */
'use strict'; 'use strict';
var React = require('react-native');
var { const React = require('react-native');
const {
StyleSheet, StyleSheet,
Text, Text,
View, View,
} = React; } = React;
var requireNativeComponent = require('requireNativeComponent'); const requireNativeComponent = require('requireNativeComponent');
var UpdatePropertiesExampleView = requireNativeComponent('UpdatePropertiesExampleView');
var FlexibleSizeExampleView = requireNativeComponent('FlexibleSizeExampleView');
class AppPropertiesUpdateExample extends React.Component { class AppPropertiesUpdateExample extends React.Component {
render() { render() {
// Do not require this unless we are actually rendering.
const UpdatePropertiesExampleView = requireNativeComponent('UpdatePropertiesExampleView');
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.text}> <Text style={styles.text}>
@ -45,6 +46,8 @@ class AppPropertiesUpdateExample extends React.Component {
class RootViewSizeFlexibilityExample extends React.Component { class RootViewSizeFlexibilityExample extends React.Component {
render() { render() {
// Do not require this unless we are actually rendering.
const FlexibleSizeExampleView = requireNativeComponent('FlexibleSizeExampleView');
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.text}> <Text style={styles.text}>
@ -60,7 +63,7 @@ class RootViewSizeFlexibilityExample extends React.Component {
} }
} }
var styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#F5FCFF', backgroundColor: '#F5FCFF',

View File

@ -1,393 +0,0 @@
/**
* 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}>
<Text
style={styles.warningText}
numberOfLines={2}
ref={text => { this.text = text; }}>
{countText}
{this.props.warning}
</Text>
</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}
style={styles.yellowBox}>
<Text style={styles.yellowBoxText}>
{countText}
{this.props.warning}
</Text>
<View style={styles.yellowBoxButtons}>
<TouchableOpacity
onPress={this.props.onDismissed}
style={styles.yellowBoxButton}>
<Text style={styles.yellowBoxButtonText}>
Dismiss
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={this.props.onIgnored}
style={styles.yellowBoxButton}>
<Text style={styles.yellowBoxButtonText}>
Ignore
</Text>
</TouchableOpacity>
</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;

View File

@ -0,0 +1,335 @@
/**
* 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 YellowBox
* @flow
*/
'use strict';
const EventEmitter = require('EventEmitter');
import type EmitterSubscription from 'EmitterSubscription';
const Map = require('Map');
const Platform = require('Platform');
const React = require('React');
const StyleSheet = require('StyleSheet');
const _warningEmitter = new EventEmitter();
const _warningMap = new Map();
/**
* YellowBox renders warnings at the bottom of the app being developed.
*
* Warnings help guard against subtle yet significant issues that can impact the
* quality of the app. This "in your face" style of warning allows developers to
* notice and correct these issues as quickly as possible.
*
* By default, the warning box is enabled in `__DEV__`. Set the following flag
* to disable it (and call `console.warn` to update any rendered <YellowBox>):
*
* console.disableYellowBox = true;
* console.warn('YellowBox is disabled.');
*
* Warnings can be ignored programmatically by setting the array:
*
* console.ignoredYellowBox = ['Warning: ...'];
*
* Strings in `console.ignoredYellowBox` can be a prefix of the warning that
* should be ignored.
*/
if (__DEV__) {
const {error, warn} = console;
console.error = function() {
error.apply(console, arguments);
// Show yellow box for the `warning` module.
if (typeof arguments[0] === 'string' &&
arguments[0].startsWith('Warning: ')) {
updateWarningMap.apply(null, arguments);
}
};
console.warn = function() {
warn.apply(console, arguments);
updateWarningMap.apply(null, arguments);
};
}
function updateWarningMap(format, ...args): void {
const sprintf = require('sprintf');
const stringifySafe = require('stringifySafe');
format = String(format);
const argCount = (format.match(/%s/g) || []).length;
const warning = [
sprintf(format, ...args.slice(0, argCount)),
...args.slice(argCount).map(stringifySafe),
].join(' ');
const count = _warningMap.has(warning) ? _warningMap.get(warning) : 0;
_warningMap.set(warning, count + 2);
_warningEmitter.emit('warning', _warningMap);
}
function isWarningIgnored(warning: string): boolean {
return (
Array.isArray(console.ignoredYellowBox) &&
console.ignoredYellowBox.some(
ignorePrefix => warning.startsWith(ignorePrefix)
)
);
}
const WarningRow = ({count, warning, onPress}) => {
const Text = require('Text');
const TouchableHighlight = require('TouchableHighlight');
const View = require('View');
const countText = count > 1 ?
<Text style={styles.listRowCount}>{'(' + count + ') '}</Text> :
null;
return (
<View style={styles.listRow}>
<TouchableHighlight
activeOpacity={0.5}
onPress={onPress}
style={styles.listRowContent}
underlayColor="transparent">
<Text style={styles.listRowText} numberOfLines={2}>
{countText}
{warning}
</Text>
</TouchableHighlight>
</View>
);
};
const WarningInspector = ({count, warning, onClose, onDismiss}) => {
const ScrollView = require('ScrollView');
const Text = require('Text');
const TouchableHighlight = require('TouchableHighlight');
const View = require('View');
const countSentence =
'Warning encountered ' + count + ' time' + (count - 1 ? 's' : '') + '.';
return (
<TouchableHighlight
activeOpacity={0.95}
underlayColor={backgroundColor(0.8)}
onPress={onClose}
style={styles.inspector}>
<View style={styles.inspectorContent}>
<View style={styles.inspectorCount}>
<Text style={styles.inspectorCountText}>{countSentence}</Text>
</View>
<ScrollView style={styles.inspectorWarning}>
<Text style={styles.inspectorWarningText}>{warning}</Text>
</ScrollView>
<View style={styles.inspectorButtons}>
<TouchableHighlight
activeOpacity={0.5}
onPress={onDismiss}
style={styles.inspectorButton}
underlayColor="transparent">
<Text style={styles.inspectorButtonText}>
Dismiss Warning
</Text>
</TouchableHighlight>
</View>
</View>
</TouchableHighlight>
);
};
class YellowBox extends React.Component {
state: {
inspecting: ?string;
warningMap: Map;
};
_listener: ?EmitterSubscription;
constructor(props: mixed, context: mixed) {
super(props, context);
this.state = {
inspecting: null,
warningMap: _warningMap,
};
this.dismissWarning = warning => {
const {inspecting, warningMap} = this.state;
warningMap.delete(warning);
this.setState({
inspecting: inspecting === warning ? null : inspecting,
warningMap,
});
};
}
componentDidMount() {
let scheduled = null;
this._listener = _warningEmitter.addListener('warning', warningMap => {
// Use `setImmediate` because warnings often happen during render, but
// state cannot be set while rendering.
scheduled = scheduled || setImmediate(() => {
scheduled = null;
this.setState({
warningMap,
});
});
});
}
componentWillUnmount() {
if (this._listener) {
this._listener.remove();
}
}
render() {
if (console.disableYellowBox || this.state.warningMap.size === 0) {
return null;
}
const ScrollView = require('ScrollView');
const View = require('View');
const inspecting = this.state.inspecting;
const inspector = inspecting !== null ?
<WarningInspector
count={this.state.warningMap.get(inspecting)}
warning={inspecting}
onClose={() => this.setState({inspecting: null})}
onDismiss={() => this.dismissWarning(inspecting)}
/> :
null;
const rows = [];
this.state.warningMap.forEach((count, warning) => {
if (!isWarningIgnored(warning)) {
rows.push(
<WarningRow
key={warning}
count={count}
warning={warning}
onPress={() => this.setState({inspecting: warning})}
onDismiss={() => this.dismissWarning(warning)}
/>
);
}
});
const listStyle = [
styles.list,
// Additional `0.4` so the 5th row can peek into view.
{height: Math.min(rows.length, 4.4) * (rowGutter + rowHeight)},
];
return (
<View style={inspector ? styles.fullScreen : listStyle}>
<ScrollView style={listStyle}>
{rows}
</ScrollView>
{inspector}
</View>
);
}
}
const backgroundColor = opacity => 'rgba(250, 186, 48, ' + opacity + ')';
const textColor = 'white';
const rowGutter = 1;
const rowHeight = 46;
var styles = StyleSheet.create({
fullScreen: {
backgroundColor: 'transparent',
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
inspector: {
backgroundColor: backgroundColor(0.95),
flex: 1,
},
inspectorContainer: {
flex: 1,
},
inspectorButtons: {
flexDirection: 'row',
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
},
inspectorButton: {
padding: 22,
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
},
inspectorButtonText: {
color: textColor,
fontSize: 14,
opacity: 0.8,
textAlign: 'center',
},
inspectorContent: {
flex: 1,
paddingTop: 5,
},
inspectorCount: {
padding: 15,
paddingBottom: 0,
},
inspectorCountText: {
color: textColor,
fontSize: 14,
},
inspectorWarning: {
padding: 15,
position: 'absolute',
top: 39,
bottom: 60,
},
inspectorWarningText: {
color: textColor,
fontSize: 16,
fontWeight: '600',
},
list: {
backgroundColor: 'transparent',
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
},
listRow: {
position: 'relative',
backgroundColor: backgroundColor(0.95),
flex: 1,
height: rowHeight,
marginTop: rowGutter,
},
listRowContent: {
flex: 1,
},
listRowCount: {
color: 'rgba(255, 255, 255, 0.5)',
},
listRowText: {
color: textColor,
position: 'absolute',
left: 0,
top: Platform.OS === 'android' ? 5 : 7,
marginLeft: 15,
marginRight: 15,
},
});
module.exports = YellowBox;

View File

@ -8,6 +8,7 @@
* *
* @providesModule renderApplication * @providesModule renderApplication
*/ */
'use strict'; 'use strict';
var Inspector = require('Inspector'); var Inspector = require('Inspector');
@ -20,6 +21,8 @@ var View = require('View');
var invariant = require('invariant'); var invariant = require('invariant');
var YellowBox = __DEV__ ? require('YellowBox') : null;
// require BackAndroid so it sets the default handler that exits the app if no listeners respond // require BackAndroid so it sets the default handler that exits the app if no listeners respond
require('BackAndroid'); require('BackAndroid');
@ -89,10 +92,14 @@ var AppContainer = React.createClass({
<Portal <Portal
onModalVisibilityChanged={this.setRootAccessibility}/> onModalVisibilityChanged={this.setRootAccessibility}/>
</View>; </View>;
let yellowBox = null;
if (__DEV__) {
yellowBox = <YellowBox />;
}
return this.state.enabled ? return this.state.enabled ?
<View style={styles.appContainer}> <View style={styles.appContainer}>
{appView} {appView}
{yellowBox}
{this.renderInspector()} {this.renderInspector()}
</View> : </View> :
appView; appView;

View File

@ -8,6 +8,7 @@
* *
* @providesModule renderApplication * @providesModule renderApplication
*/ */
'use strict'; 'use strict';
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
@ -19,7 +20,7 @@ var View = require('View');
var invariant = require('invariant'); var invariant = require('invariant');
var Inspector = __DEV__ ? require('Inspector') : null; var Inspector = __DEV__ ? require('Inspector') : null;
var WarningBox = __DEV__ ? require('WarningBox') : null; var YellowBox = __DEV__ ? require('YellowBox') : null;
var AppContainer = React.createClass({ var AppContainer = React.createClass({
mixins: [Subscribable.Mixin], mixins: [Subscribable.Mixin],
@ -47,14 +48,16 @@ var AppContainer = React.createClass({
}, },
render: function() { render: function() {
var shouldRenderWarningBox = __DEV__ && console.yellowBoxEnabled; let yellowBox = null;
var warningBox = shouldRenderWarningBox ? <WarningBox /> : null; if (__DEV__) {
yellowBox = <YellowBox />;
}
return ( return (
<View style={styles.appContainer}> <View style={styles.appContainer}>
<View collapsible={false} style={styles.appContainer} ref="main"> <View collapsible={false} style={styles.appContainer} ref="main">
{this.props.children} {this.props.children}
</View> </View>
{warningBox} {yellowBox}
{this.state.inspector} {this.state.inspector}
</View> </View>
); );
@ -70,6 +73,7 @@ function renderApplication<D, P, S>(
rootTag, rootTag,
'Expect to have a valid rootTag, instead got ', rootTag 'Expect to have a valid rootTag, instead got ', rootTag
); );
/* eslint-disable jsx-no-undef-with-namespace */
React.render( React.render(
<AppContainer rootTag={rootTag}> <AppContainer rootTag={rootTag}>
<RootComponent <RootComponent
@ -79,6 +83,7 @@ function renderApplication<D, P, S>(
</AppContainer>, </AppContainer>,
rootTag rootTag
); );
/* eslint-enable jsx-no-undef-with-namespace */
} }
var styles = StyleSheet.create({ var styles = StyleSheet.create({