From fa5783e3ee1c5e0b73231a2cf0703545cf9b91ac Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Tue, 8 Mar 2016 16:25:29 -0800 Subject: [PATCH] Initial implementation of the Navigator with NavigationExperimental. Summary:This is the initial implementation of the Navigator done with the NavigationExperimental library. There will be following diffs to support more features that are currently available from the Navigator. Reviewed By: ericvicenti Differential Revision: D3016084 fb-gh-sync-id: ed509fc86e9dc67b5334be9e60b582494fd52844 shipit-source-id: ed509fc86e9dc67b5334be9e60b582494fd52844 --- .../NavigationLegacyNavigator.js | 255 +++++++++++++++++- .../NavigationLegacyNavigatorRouteStack.js | 19 +- ...avigationLegacyNavigatorRouteStack-test.js | 7 + 3 files changed, 276 insertions(+), 5 deletions(-) diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigator.js b/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigator.js index a03bd9f4a79..67e8aa01de8 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigator.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigator.js @@ -27,11 +27,258 @@ */ 'use strict'; +const NavigationAnimatedView = require('NavigationAnimatedView'); +const NavigationCard = require('NavigationCard'); +const NavigationContext = require('NavigationContext'); +const NavigationLegacyNavigatorRouteStack = require('NavigationLegacyNavigatorRouteStack'); +const NavigatorBreadcrumbNavigationBar = require('NavigatorBreadcrumbNavigationBar'); +const NavigatorNavigationBar = require('NavigatorNavigationBar'); +const NavigatorSceneConfigs = require('NavigatorSceneConfigs'); +const React = require('React'); +const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); + +const invariant = require('invariant'); +const guid = require('guid'); + +import type { + NavigationSceneRenderer, + NavigationSceneRendererProps, +} from 'NavigationTypeDefinition'; + +type Props = { + configureScene: any, + initialRoute: any, + initialRouteStack: any, +}; + +function getConfigPopDirection(config: any): ?string { + if (config && config.gestures && config.gestures.pop) { + const direction = config.gestures.pop.direction; + return direction ? String(direction) : null; + } + + return null; +} + +const RouteStack = NavigationLegacyNavigatorRouteStack; + /** - * NavigationLegacyNavigator is meant to replace Navigator seemlessly without - * API changes. While the APIs remain compatible with Navigator, it should - * be built with the new Navigation API such as `NavigationAnimatedView`...etc. + * NavigationLegacyNavigator is meant to replace Navigator seemlessly with + * minimum API changes. + * + * While the APIs remain compatible with Navigator, it is built with good + * intention by using the new Navigation API such as + * `NavigationAnimatedView`...etc. */ -const NavigationLegacyNavigator = require('Navigator'); +class NavigationLegacyNavigator extends React.Component { + _renderCard: NavigationSceneRenderer; + _renderHeader: NavigationSceneRenderer; + _renderScene: NavigationSceneRenderer; + + navigationContext: NavigationContext; + + constructor(props: Props, context: any) { + super(props, context); + + this.navigationContext = new NavigationContext(); + + const stack = this._getInitialRouteStack(); + this.state = { + key: guid(), + stack, + }; + } + + jumpTo(route: any): void { + const index = this.state.stack.indexOf(route); + invariant( + index > -1, + 'Cannot jump to route that is not in the route stack' + ); + this._jumpToIndex(index); + } + + jumpForward(): void { + this._jumpToIndex(this.state.stack.index + 1); + } + + jumpBack(): void { + this._jumpToIndex(this.state.stack.index - 1); + } + + push(route: any): void { + this.setState({stack: this.state.stack.push(route)}); + } + + pop(): void { + const {stack} = this.state; + if (stack.size > 1) { + this.setState({stack: stack.pop()}); + } + } + + replaceAtIndex(route: any, index: number): void { + const {stack} = this.state; + + if (index < 0) { + index += stack.size; + } + + if (index >= stack.size) { + // Nothing to replace. + return; + } + + this.setState({stack: stack.replaceAtIndex(index, route)}); + } + + replace(route: any): void { + this.replaceAtIndex(route, this.state.stack.index); + } + + replacePrevious(route: any): void { + this.replaceAtIndex(route, this.state.stack.index - 1); + } + + popToTop(): void { + this.setState({stack: this.state.stack.slice(0, 1)}); + } + + popToRoute(route: any): void { + const {stack} = this.state; + const nextIndex = stack.indexOf(route); + invariant( + nextIndex > -1, + 'Calling popToRoute for a route that doesn\'t exist!' + ); + this.setState({stack: stack.slice(0, nextIndex + 1)}); + } + + replacePreviousAndPop(route: any): void { + const {stack} = this.state; + const nextIndex = stack.index - 1; + if (nextIndex < 0) { + return; + } + this.setState({stack: stack.replaceAtIndex(nextIndex, route).pop()}); + } + + resetTo(route: any): void { + invariant(!!route, 'Must supply route'); + this.setState({stack: this.state.stack.slice(0).replaceAtIndex(0, route)}); + } + + immediatelyResetRouteStack(routes: Array): void { + const index = routes.length - 1; + const stack = new RouteStack(index, routes); + this.setState({ + key: guid(), + stack, + }); + } + + getCurrentRoutes(): Array { + return this.state.stack.toArray(); + } + + _jumpToIndex(index: number): void { + const {stack} = this.state; + if (index < 0 || index >= stack.size) { + return; + } + const nextStack = stack.jumpToIndex(index); + this.setState({stack: nextStack}); + } + + // Lyfe cycle and private methods below. + + shouldComponentUpdate(nextProps: Object, nextState: Object): boolean { + return ReactComponentWithPureRenderMixin.shouldComponentUpdate.call( + this, + nextProps, + nextState + ); + } + + componentWillMount(): void { + this._renderCard = this._renderCard.bind(this); + this._renderHeader = this._renderHeader.bind(this); + this._renderScene = this._renderScene.bind(this); + } + + render(): ReactElement { + return ( + + ); + } + + _getInitialRouteStack(): RouteStack { + const {initialRouteStack, initialRoute} = this.props; + const routes = initialRouteStack || [initialRoute]; + const index = initialRoute ? + routes.indexOf(initialRoute) : + routes.length - 1; + return new RouteStack(index, routes); + } + + _renderHeader(props: NavigationSceneRendererProps): ?ReactElement { + // TODO(hedger): Render the legacy header. + return null; + } + + _renderCard(props: NavigationSceneRendererProps): ReactElement { + let direction = 'horizontal'; + + const {navigationState} = props.scene; + const {configureScene} = this.props; + + if (configureScene) { + const route = RouteStack.getRouteByNavigationState(navigationState); + const config = configureScene(route, this.state.stack.toArray()); + + switch (getConfigPopDirection(config)) { + case 'left-to-right': + direction = 'horizontal'; + break; + + case 'top-to-bottom': + direction = 'vertical'; + break; + + default: + // unsupported config. + if (__DEV__) { + console.warn('unsupported scene configuration'); + } + } + } + + return ( + + ); + } + + _renderScene(props: NavigationSceneRendererProps): ReactElement { + const {navigationState} = props.scene; + const route = RouteStack.getRouteByNavigationState(navigationState); + return this.props.renderScene(route, this); + } +} + +// Legacy static members. +NavigationLegacyNavigator.BreadcrumbNavigationBar = NavigatorBreadcrumbNavigationBar; +NavigationLegacyNavigator.NavigationBar = NavigatorNavigationBar; +NavigationLegacyNavigator.SceneConfigs = NavigatorSceneConfigs; module.exports = NavigationLegacyNavigator; diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigatorRouteStack.js b/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigatorRouteStack.js index f71aa64e9b2..302678f3e0f 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigatorRouteStack.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigatorRouteStack.js @@ -46,6 +46,19 @@ let _nextRouteNodeID = 0; class RouteNode { key: string; route: any; + + /** + * Cast `navigationState` as `RouteNode`. + * Also see `RouteNode#toNavigationState`. + */ + static fromNavigationState(navigationState: NavigationState): RouteNode { + invariant( + navigationState instanceof RouteNode, + 'navigationState should be an instacne of RouteNode' + ); + return navigationState; + } + constructor(route: any) { // Key value gets bigger incrementally. Developer can compare the // keys of two routes then know which route is added to the stack @@ -84,11 +97,15 @@ let _nextRouteStackID = 0; * of the routes. This data structure is implemented as immutable data * and mutation (e.g. push, pop...etc) will yields a new instance. */ -class RouteStack { +class RouteStack { _index: number; _key: string; _routeNodes: Array; + static getRouteByNavigationState(navigationState: NavigationState): any { + return RouteNode.fromNavigationState(navigationState).route; + } + constructor(index: number, routes: Array) { invariant( routes.length > 0, diff --git a/Libraries/CustomComponents/NavigationExperimental/__tests__/NavigationLegacyNavigatorRouteStack-test.js b/Libraries/CustomComponents/NavigationExperimental/__tests__/NavigationLegacyNavigatorRouteStack-test.js index eb84f408cfe..151c33b11a8 100644 --- a/Libraries/CustomComponents/NavigationExperimental/__tests__/NavigationLegacyNavigatorRouteStack-test.js +++ b/Libraries/CustomComponents/NavigationExperimental/__tests__/NavigationLegacyNavigatorRouteStack-test.js @@ -386,4 +386,11 @@ describe('NavigationLegacyNavigatorRouteStack:', () => { ], }); }); + + it('coverts from navigation state', () => { + const stack = new NavigationLegacyNavigatorRouteStack(0, ['a', 'b']); + const state = stack.toNavigationState().children[0]; + const route = NavigationLegacyNavigatorRouteStack.getRouteByNavigationState(state); + expect(route).toBe('a'); + }); });