diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index a15e38bc1..0ad3c4269 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -27,11 +27,14 @@ 'use strict'; var AnimationsDebugModule = require('NativeModules').AnimationsDebugModule; +var BackAndroid = require('BackAndroid'); var Dimensions = require('Dimensions'); var InteractionMixin = require('InteractionMixin'); -var NavigatorSceneConfigs = require('NavigatorSceneConfigs'); -var NavigatorNavigationBar = require('NavigatorNavigationBar'); var NavigatorBreadcrumbNavigationBar = require('NavigatorBreadcrumbNavigationBar'); +var NavigatorInterceptor = require('NavigatorInterceptor'); +var NavigatorNavigationBar = require('NavigatorNavigationBar'); +var NavigatorSceneConfigs = require('NavigatorSceneConfigs'); +var NavigatorStaticContextContainer = require('NavigatorStaticContextContainer'); var PanResponder = require('PanResponder'); var React = require('React'); var StaticContainer = require('StaticContainer.react'); @@ -40,6 +43,7 @@ var Subscribable = require('Subscribable'); var TimerMixin = require('react-timer-mixin'); var View = require('View'); +var getNavigatorContext = require('getNavigatorContext'); var clamp = require('clamp'); var invariant = require('invariant'); var keyMirror = require('keyMirror'); @@ -182,12 +186,11 @@ var Navigator = React.createClass({ /** * Required function which renders the scene for a given route. Will be - * invoked with the route, the navigator object, and a ref handler that - * will allow a ref to your scene to be provided by props.onItemRef + * invoked with the route and the navigator object * * ``` - * (route, navigator, onRef) => - * + * (route, navigator) => + * * ``` */ renderScene: PropTypes.func.isRequired, @@ -241,10 +244,16 @@ var Navigator = React.createClass({ sceneStyle: View.propTypes.style, }, + contextTypes: { + navigator: PropTypes.object, + }, + statics: { BreadcrumbNavigationBar: NavigatorBreadcrumbNavigationBar, NavigationBar: NavigatorNavigationBar, SceneConfigs: NavigatorSceneConfigs, + Interceptor: NavigatorInterceptor, + getContext: getNavigatorContext, }, mixins: [TimerMixin, InteractionMixin, Subscribable.Mixin], @@ -293,7 +302,20 @@ var Navigator = React.createClass({ }, componentWillMount: function() { - this.navigatorActions = { + this.parentNavigator = getNavigatorContext(this) || this.props.navigator; + this.navigatorContext = { + setHandlerForRoute: this.setHandlerForRoute, + request: this.request, + + parentNavigator: this.parentNavigator, + getCurrentRoutes: this.getCurrentRoutes, + // We want to bubble focused routes to the top navigation stack. If we + // are a child navigator, this allows us to call props.navigator.on*Focus + // of the topmost Navigator + onWillFocus: this.props.onWillFocus, + onDidFocus: this.props.onDidFocus, + + // Legacy, imperitive nav actions. Use request when possible. jumpBack: this.jumpBack, jumpForward: this.jumpForward, jumpTo: this.jumpTo, @@ -307,14 +329,8 @@ var Navigator = React.createClass({ resetTo: this.resetTo, popToRoute: this.popToRoute, popToTop: this.popToTop, - parentNavigator: this.props.navigator, - getCurrentRoutes: this.getCurrentRoutes, - // We want to bubble focused routes to the top navigation stack. If we - // are a child navigator, this allows us to call props.navigator.on*Focus - // of the topmost Navigator - onWillFocus: this.props.onWillFocus, - onDidFocus: this.props.onDidFocus, }; + this._handlers = {}; this.panGesture = PanResponder.create({ onStartShouldSetPanResponderCapture: this._handleStartShouldSetPanResponderCapture, @@ -330,6 +346,46 @@ var Navigator = React.createClass({ this._emitWillFocus(this.state.presentedIndex); }, + request: function(action, arg1, arg2) { + if (this.parentNavigator) { + return this.parentNavigator.request.apply(null, arguments); + } + return this._handleRequest.apply(null, arguments); + }, + + _handleRequest: function(action, arg1, arg2) { + var childHandler = this._handlers[this.state.presentedIndex]; + if (childHandler && childHandler(action, arg1, arg2)) { + return true; + } + switch (action) { + case 'pop': + return this._handlePop(); + case 'push': + return this._handlePush(arg1); + default: + invariant(false, 'Unsupported request type ' + action); + return false; + } + }, + + _handlePop: function() { + if (this.state.presentedIndex === 0) { + return false; + } + this.pop(); + return true; + }, + + _handlePush: function(route) { + this.push(route); + return true; + }, + + setHandlerForRoute: function(route, handler) { + this._handlers[this.state.routeStack.indexOf(route)] = handler; + }, + _configureSpring: function(animationConfig) { var config = this.spring.getSpringConfig(); config.friction = animationConfig.springFriction; @@ -345,6 +401,28 @@ var Navigator = React.createClass({ this.spring.addListener(this); this.onSpringUpdate(); this._emitDidFocus(this.state.presentedIndex); + if (this.parentNavigator) { + this.parentNavigator.setHandler(this._handleRequest); + } else { + // There is no navigator in our props or context, so this is the + // top-level navigator. We will handle back button presses here + BackAndroid.addEventListener('hardwareBackPress', this._handleBackPress); + } + }, + + componentWillUnmount: function() { + if (this.parentNavigator) { + this.parentNavigator.setHandler(null); + } else { + BackAndroid.removeEventListener('hardwareBackPress', this._handleBackPress); + } + }, + + _handleBackPress: function() { + var didPop = this.request('pop'); + if (!didPop) { + BackAndroid.exitApp(); + } }, /** @@ -791,8 +869,9 @@ var Navigator = React.createClass({ }, pop: function() { - if (this.props.navigator && this.state.routeStack.length === 1) { - return this.props.navigator.pop(); + // TODO (t6707686): remove this parentNavigator call after transitioning call sites to `.request('pop')` + if (this.parentNavigator && this.state.routeStack.length === 1) { + return this.parentNavigator.pop(); } this.popN(1); }, @@ -889,7 +968,7 @@ var Navigator = React.createClass({ return this.state.routeStack; }, - _onItemRef: function(itemId, ref) { + _handleItemRef: function(itemId, ref) { this._itemRefs[itemId] = ref; var itemIndex = this.state.idStack.indexOf(itemId); if (itemIndex === -1) { @@ -922,23 +1001,33 @@ var Navigator = React.createClass({ this.state.updatingRangeLength !== 0 && i >= this.state.updatingRangeStart && i <= this.state.updatingRangeStart + this.state.updatingRangeLength; + var sceneNavigatorContext = { + ...this.navigatorContext, + route, + setHandler: (handler) => { + this.navigatorContext.setHandlerForRoute(route, handler); + }, + }; var child = this.props.renderScene( route, - this.navigatorActions, - this._onItemRef.bind(null, this.state.idStack[i]) + sceneNavigatorContext ); - var initialSceneStyle = i === this.state.presentedIndex ? styles.presentNavItem : styles.futureNavItem; return ( - + - {child} + {React.cloneElement(child, { + ref: this._handleItemRef.bind(null, this.state.idStack[i]), + })} - + ); }, @@ -967,7 +1056,7 @@ var Navigator = React.createClass({ } return React.cloneElement(this.props.navigationBar, { ref: (navBar) => { this._navBar = navBar; }, - navigator: this.navigatorActions, + navigator: this.navigatorContext, navState: this.state, }); }, diff --git a/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js b/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js new file mode 100644 index 000000000..dcc5d43ef --- /dev/null +++ b/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @providesModule NavigatorInterceptor + */ +'use strict'; + +var React = require('React'); + +var getNavigatorContext = require('getNavigatorContext'); + +var NavigatorInterceptor = React.createClass({ + + contextTypes: { + navigator: React.PropTypes.object, + }, + + componentWillMount: function() { + this.navigator = getNavigatorContext(this); + }, + + componentDidMount: function() { + this.navigator.setHandler(this._navigatorHandleRequest); + }, + + childContextTypes: { + navigator: React.PropTypes.object, + }, + + getChildContext: function() { + return { + navigator: { + ...this.navigator, + setHandler: (handler) => { + this._childNavigationHandler = handler; + }, + } + }; + }, + + componentWillUnmount: function() { + this.navigator.setHandler(null); + }, + + _navigatorHandleRequest: function(action, arg1, arg2) { + if (this._interceptorHandle(action, arg1, arg2)) { + return true; + } + if (this._childNavigationHandler && this._childNavigationHandler(action, arg1, arg2)) { + return true; + } + }, + + _interceptorHandle: function(action, arg1, arg2) { + if (this.props.onRequest && this.props.onRequest(action, arg1, arg2)) { + return true; + } + switch (action) { + case 'pop': + return this.props.onPopRequest && this.props.onPopRequest(action, arg1, arg2); + case 'push': + return this.props.onPushRequest && this.props.onPushRequest(action, arg1, arg2); + default: + return false; + } + }, + + render: function() { + return this.props.children; + }, + +}); + +module.exports = NavigatorInterceptor; diff --git a/Libraries/CustomComponents/Navigator/NavigatorStaticContextContainer.js b/Libraries/CustomComponents/Navigator/NavigatorStaticContextContainer.js new file mode 100644 index 000000000..ded8048e4 --- /dev/null +++ b/Libraries/CustomComponents/Navigator/NavigatorStaticContextContainer.js @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @providesModule NavigatorStaticContextContainer + */ +'use strict'; + +var React = require('React'); +var StaticContainer = require('StaticContainer.react'); + +var PropTypes = React.PropTypes; + +var NavigatorStaticContextContainer = React.createClass({ + + childContextTypes: { + navigator: PropTypes.object, + }, + + getChildContext: function() { + return { + navigator: this.props.navigatorContext, + }; + }, + + render: function() { + return ( + + ); + }, +}); + +module.exports = NavigatorStaticContextContainer; diff --git a/Libraries/CustomComponents/Navigator/getNavigatorContext.js b/Libraries/CustomComponents/Navigator/getNavigatorContext.js new file mode 100644 index 000000000..0c169fe0f --- /dev/null +++ b/Libraries/CustomComponents/Navigator/getNavigatorContext.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @providesModule getNavigatorContext + */ +'use strict'; + + +var ReactInstanceMap = require('ReactInstanceMap'); + +function getNavigatorContext(el) { + // TODO (t6707746): replace with `el.context.navigator` when parent context is supported + return ReactInstanceMap.get(el)._context.navigator; +} + +module.exports = getNavigatorContext;