Permalink
Browse files

[ReactNative][RFC] Yellow Box for warnings

  • Loading branch information...
ericvicenti committed Apr 27, 2015
1 parent bae4e44 commit 3f723f451d2eccd73d1306813c9d40d4b9e683a6
Showing with 419 additions and 3 deletions.
  1. +398 −0 Libraries/ReactIOS/WarningBox.js
  2. +21 −3 Libraries/ReactIOS/renderApplication.ios.js
@@ -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;
Oops, something went wrong.

0 comments on commit 3f723f4

Please sign in to comment.