From 8ab51828ff077ae0ad10c06f62f9f01d58b9bf85 Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Fri, 20 Nov 2015 13:05:34 -0800 Subject: [PATCH] RN: Revamp YellowBox for Warnings Reviewed By: vjeux Differential Revision: D2667624 fb-gh-sync-id: f3c6ed63f3138edd13e7fe283cf877d598018813 --- Examples/UIExplorer/RCTRootViewIOSExample.js | 15 +- Libraries/ReactIOS/WarningBox.js | 393 ------------------ Libraries/ReactIOS/YellowBox.js | 335 +++++++++++++++ .../ReactIOS/renderApplication.android.js | 9 +- Libraries/ReactIOS/renderApplication.ios.js | 13 +- 5 files changed, 361 insertions(+), 404 deletions(-) delete mode 100644 Libraries/ReactIOS/WarningBox.js create mode 100644 Libraries/ReactIOS/YellowBox.js diff --git a/Examples/UIExplorer/RCTRootViewIOSExample.js b/Examples/UIExplorer/RCTRootViewIOSExample.js index 174dcf2bb947c2..69ecab0a7655a1 100644 --- a/Examples/UIExplorer/RCTRootViewIOSExample.js +++ b/Examples/UIExplorer/RCTRootViewIOSExample.js @@ -15,19 +15,20 @@ */ 'use strict'; -var React = require('react-native'); -var { + +const React = require('react-native'); +const { StyleSheet, Text, View, } = React; -var requireNativeComponent = require('requireNativeComponent'); -var UpdatePropertiesExampleView = requireNativeComponent('UpdatePropertiesExampleView'); -var FlexibleSizeExampleView = requireNativeComponent('FlexibleSizeExampleView'); +const requireNativeComponent = require('requireNativeComponent'); class AppPropertiesUpdateExample extends React.Component { render() { + // Do not require this unless we are actually rendering. + const UpdatePropertiesExampleView = requireNativeComponent('UpdatePropertiesExampleView'); return ( @@ -45,6 +46,8 @@ class AppPropertiesUpdateExample extends React.Component { class RootViewSizeFlexibilityExample extends React.Component { render() { + // Do not require this unless we are actually rendering. + const FlexibleSizeExampleView = requireNativeComponent('FlexibleSizeExampleView'); return ( @@ -60,7 +63,7 @@ class RootViewSizeFlexibilityExample extends React.Component { } } -var styles = StyleSheet.create({ +const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#F5FCFF', diff --git a/Libraries/ReactIOS/WarningBox.js b/Libraries/ReactIOS/WarningBox.js deleted file mode 100644 index 09ac954c5502ae..00000000000000 --- a/Libraries/ReactIOS/WarningBox.js +++ /dev/null @@ -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 = []; -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 = ( - - ({warningCounts.get(this.props.warning)}){" "} - - ); - } - return ( - { this.container = container; }} - {...this.panGesture.panHandlers}> - - { this.text = text; }}> - {countText} - {this.props.warning} - - - { this.closeButton = closeButton; }} - style={styles.closeButton}> - { - this.dismissalSpring.setEndValue(1); - }}> - - - - - ); - } -}); - -var WarningBoxOpened = React.createClass({ - render: function() { - var countText; - if (warningCounts.get(this.props.warning) > 1) { - countText = ( - - ({warningCounts.get(this.props.warning)}){" "} - - ); - } - return ( - - - {countText} - {this.props.warning} - - - - - Dismiss - - - - - Ignore - - - - - ); - }, -}); - -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 ; - } - if (this.state.openWarning) { - return ( - { - 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( - { - this.setState({ openWarning: warning }); - }} - onDismissed={this._onDismiss.bind(this, warning)} - warning={warning} - /> - ); - }); - return ( - - {warningRows} - - ); - }, -}); - -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; diff --git a/Libraries/ReactIOS/YellowBox.js b/Libraries/ReactIOS/YellowBox.js new file mode 100644 index 00000000000000..c439b4e20cba9b --- /dev/null +++ b/Libraries/ReactIOS/YellowBox.js @@ -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 ): + * + * 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 ? + {'(' + count + ') '} : + null; + + return ( + + + + {countText} + {warning} + + + + ); +}; + +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 ( + + + + {countSentence} + + + {warning} + + + + + Dismiss Warning + + + + + + ); +}; + +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 ? + this.setState({inspecting: null})} + onDismiss={() => this.dismissWarning(inspecting)} + /> : + null; + + const rows = []; + this.state.warningMap.forEach((count, warning) => { + if (!isWarningIgnored(warning)) { + rows.push( + 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 ( + + + {rows} + + {inspector} + + ); + } +} + +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; diff --git a/Libraries/ReactIOS/renderApplication.android.js b/Libraries/ReactIOS/renderApplication.android.js index d299c2eb384225..05b87178b6f545 100644 --- a/Libraries/ReactIOS/renderApplication.android.js +++ b/Libraries/ReactIOS/renderApplication.android.js @@ -8,6 +8,7 @@ * * @providesModule renderApplication */ + 'use strict'; var Inspector = require('Inspector'); @@ -20,6 +21,8 @@ var View = require('View'); 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'); @@ -89,10 +92,14 @@ var AppContainer = React.createClass({ ; - + let yellowBox = null; + if (__DEV__) { + yellowBox = ; + } return this.state.enabled ? {appView} + {yellowBox} {this.renderInspector()} : appView; diff --git a/Libraries/ReactIOS/renderApplication.ios.js b/Libraries/ReactIOS/renderApplication.ios.js index 01eb1e5d53f976..0b0f306d6be33a 100644 --- a/Libraries/ReactIOS/renderApplication.ios.js +++ b/Libraries/ReactIOS/renderApplication.ios.js @@ -8,6 +8,7 @@ * * @providesModule renderApplication */ + 'use strict'; var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); @@ -19,7 +20,7 @@ var View = require('View'); var invariant = require('invariant'); var Inspector = __DEV__ ? require('Inspector') : null; -var WarningBox = __DEV__ ? require('WarningBox') : null; +var YellowBox = __DEV__ ? require('YellowBox') : null; var AppContainer = React.createClass({ mixins: [Subscribable.Mixin], @@ -47,14 +48,16 @@ var AppContainer = React.createClass({ }, render: function() { - var shouldRenderWarningBox = __DEV__ && console.yellowBoxEnabled; - var warningBox = shouldRenderWarningBox ? : null; + let yellowBox = null; + if (__DEV__) { + yellowBox = ; + } return ( {this.props.children} - {warningBox} + {yellowBox} {this.state.inspector} ); @@ -70,6 +73,7 @@ function renderApplication( rootTag, 'Expect to have a valid rootTag, instead got ', rootTag ); + /* eslint-disable jsx-no-undef-with-namespace */ React.render( ( , rootTag ); + /* eslint-enable jsx-no-undef-with-namespace */ } var styles = StyleSheet.create({