diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js
new file mode 100644
index 00000000000000..2139397908a517
--- /dev/null
+++ b/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js
@@ -0,0 +1,105 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+'use strict';
+
+var React = require('react-native');
+var {
+ NavigationExperimental,
+ StyleSheet,
+ ScrollView,
+} = React;
+var NavigationExampleRow = require('./NavigationExampleRow');
+var {
+ AnimatedView: NavigationAnimatedView,
+ Card: NavigationCard,
+ RootContainer: NavigationRootContainer,
+ Reducer: NavigationReducer,
+ Header: NavigationHeader,
+} = NavigationExperimental;
+
+const NavigationBasicReducer = NavigationReducer.StackReducer({
+ initialStates: [
+ {key: 'First Route'}
+ ],
+ matchAction: action => true,
+ actionStateMap: actionString => ({key: actionString}),
+});
+
+class NavigationAnimatedExample extends React.Component {
+ componentWillMount() {
+ this._renderNavigated = this._renderNavigated.bind(this);
+ }
+ render() {
+ return (
+
+ );
+ }
+ _renderNavigated(navigationState, onNavigate) {
+ if (!navigationState) {
+ return null;
+ }
+ return (
+ (
+ state.key}
+ />
+ )}
+ renderScene={(state, index, position, layout) => (
+
+
+
+ {
+ onNavigate('Route #' + navigationState.children.length);
+ }}
+ />
+
+
+
+ )}
+ />
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ animatedView: {
+ flex: 1,
+ },
+ scrollView: {
+ marginTop: 64
+ },
+});
+
+module.exports = NavigationAnimatedExample;
diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js
new file mode 100644
index 00000000000000..21f03eb63889b1
--- /dev/null
+++ b/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js
@@ -0,0 +1,82 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+'use strict';
+
+const React = require('react-native');
+const {
+ NavigationExperimental,
+ ScrollView,
+ StyleSheet,
+} = React;
+const NavigationExampleRow = require('./NavigationExampleRow');
+const {
+ RootContainer: NavigationRootContainer,
+ Reducer: NavigationReducer,
+} = NavigationExperimental;
+const StackReducer = NavigationReducer.StackReducer;
+
+const NavigationBasicReducer = StackReducer({
+ initialStates: [
+ {key: 'first_page'}
+ ],
+ matchAction: action => true,
+ actionStateMap: action => ({key: action}),
+});
+
+const NavigationBasicExample = React.createClass({
+ render: function() {
+ return (
+ {
+ if (!navState) { return null; }
+ return (
+
+
+ {
+ onNavigate('page #' + navState.children.length);
+ }}
+ />
+ {
+ onNavigate(StackReducer.PopAction());
+ }}
+ />
+
+
+ );
+ }}
+ />
+ );
+ },
+});
+
+const styles = StyleSheet.create({
+ topView: {
+ backgroundColor: '#E9E9EF',
+ flex: 1,
+ paddingTop: 30,
+ },
+});
+
+module.exports = NavigationBasicExample;
diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js
new file mode 100644
index 00000000000000..1af4c63bbeb4cb
--- /dev/null
+++ b/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js
@@ -0,0 +1,264 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+'use strict';
+
+const React = require('react-native');
+const {
+ NavigationExperimental,
+ ScrollView,
+ StyleSheet,
+ View,
+} = React;
+const {
+ AnimatedView: NavigationAnimatedView,
+ Card: NavigationCard,
+ Container: NavigationContainer,
+ RootContainer: NavigationRootContainer,
+ Header: NavigationHeader,
+ Reducer: NavigationReducer,
+ View: NavigationView,
+} = NavigationExperimental;
+const NavigationExampleRow = require('./NavigationExampleRow');
+const NavigationExampleTabBar = require('./NavigationExampleTabBar');
+
+const ExampleExitAction = () => ({
+ isExitAction: true,
+});
+ExampleExitAction.match = (action) => (
+ action && action.isExitAction === true
+);
+
+const ExamplePageAction = (type) => ({
+ type,
+ isPageAction: true,
+});
+ExamplePageAction.match = (action) => (
+ action && action.isPageAction === true
+);
+
+const ExampleSettingsPageAction = (type) => ({
+ ...ExamplePageAction(type),
+ isSettingsPageAction: true,
+});
+ExampleSettingsPageAction.match = (action) => (
+ action && action.isSettingsPageAction === true
+);
+
+const ExampleInfoAction = () => ExamplePageAction('InfoPage');
+
+const ExampleNotifSettingsAction = () => ExampleSettingsPageAction('NotifSettingsPage');
+
+const _jsInstanceUniqueId = '' + Date.now();
+let _uniqueIdCount = 0;
+function pageStateActionMap(action) {
+ return {
+ key: 'page-' + _jsInstanceUniqueId + '-' + (_uniqueIdCount++),
+ type: action.type,
+ };
+}
+
+function getTabActionMatcher(key) {
+ return function (action) {
+ if (!ExamplePageAction.match(action)) {
+ return false;
+ }
+ if (ExampleSettingsPageAction.match(action)) {
+ return key === 'settings';
+ }
+ return true;
+ };
+}
+
+var ExampleTabs = [
+ {
+ label: 'Account',
+ reducer: NavigationReducer.StackReducer({
+ initialStates: [
+ {type: 'AccountPage', key: 'base'}
+ ],
+ key: 'account',
+ matchAction: getTabActionMatcher('account'),
+ actionStateMap: pageStateActionMap,
+ }),
+ },
+ {
+ label: 'Notifications',
+ reducer: NavigationReducer.StackReducer({
+ initialStates: [
+ {type: 'NotifsPage', key: 'base'}
+ ],
+ key: 'notifs',
+ matchAction: getTabActionMatcher('notifs'),
+ actionStateMap: pageStateActionMap,
+ }),
+ },
+ {
+ label: 'Settings',
+ reducer: NavigationReducer.StackReducer({
+ initialStates: [
+ {type: 'SettingsPage', key: 'base'}
+ ],
+ key: 'settings',
+ matchAction: getTabActionMatcher('settings'),
+ actionStateMap: pageStateActionMap,
+ }),
+ },
+];
+
+const ExampleAppReducer = NavigationReducer.TabsReducer({
+ tabReducers: ExampleTabs.map(tab => tab.reducer),
+});
+
+function stateTypeTitleMap(pageState) {
+ switch (pageState.type) {
+ case 'AccountPage':
+ return 'Account Page';
+ case 'NotifsPage':
+ return 'Notifications';
+ case 'SettingsPage':
+ return 'Settings';
+ case 'InfoPage':
+ return 'Info Page';
+ case 'NotifSettingsPage':
+ return 'Notification Settings';
+ }
+}
+
+let ExampleTabScreen = React.createClass({
+ render: function() {
+ return (
+
+ );
+ },
+ _renderHeader: function(position, layout) {
+ return (
+ stateTypeTitleMap(state)}
+ />
+ );
+ },
+ _renderScene: function(child, index, position, layout) {
+ return (
+
+
+ {
+ this.props.onNavigate(ExampleInfoAction());
+ }}
+ />
+ {
+ this.props.onNavigate(ExampleNotifSettingsAction());
+ }}
+ />
+ {
+ this.props.onNavigate(ExampleExitAction());
+ }}
+ />
+
+
+ );
+ },
+});
+ExampleTabScreen = NavigationContainer.create(ExampleTabScreen);
+
+const NavigationCompositionExample = React.createClass({
+ render: function() {
+ return (
+
+ );
+ },
+ renderApp: function(navigationState, onNavigate) {
+ if (!navigationState) {
+ return null;
+ }
+ return (
+
+
+
+
+ );
+ },
+});
+
+let ExampleMainView = React.createClass({
+ render: function() {
+ return (
+ (
+
+ )}
+ />
+ );
+ },
+ _handleNavigation: function(tabKey, action) {
+ if (ExampleExitAction.match(action)) {
+ this.props.onExampleExit();
+ return;
+ }
+ this.props.onNavigate(action);
+ },
+});
+ExampleMainView = NavigationContainer.create(ExampleMainView);
+
+const styles = StyleSheet.create({
+ topView: {
+ flex: 1,
+ },
+ tabsContent: {
+ flex: 1,
+ },
+ scrollView: {
+ marginTop: 64
+ },
+ tabContent: {
+ flex: 1,
+ },
+});
+
+module.exports = NavigationCompositionExample;
diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationExampleRow.js b/Examples/UIExplorer/NavigationExperimental/NavigationExampleRow.js
new file mode 100644
index 00000000000000..4e431e30a722b2
--- /dev/null
+++ b/Examples/UIExplorer/NavigationExperimental/NavigationExampleRow.js
@@ -0,0 +1,65 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+'use strict';
+
+var React = require('react-native');
+var {
+ Text,
+ PixelRatio,
+ StyleSheet,
+ View,
+ TouchableHighlight,
+} = React;
+
+var NavigationExampleRow = React.createClass({
+ render: function() {
+ if (this.props.onPress) {
+ return (
+
+
+ {this.props.text}
+
+
+ );
+ }
+ return (
+
+
+ {this.props.text}
+
+
+ );
+ },
+});
+
+const styles = StyleSheet.create({
+ row: {
+ padding: 15,
+ backgroundColor: 'white',
+ borderBottomWidth: 1 / PixelRatio.get(),
+ borderBottomColor: '#CDCDCD',
+ },
+ rowText: {
+ fontSize: 17,
+ },
+ buttonText: {
+ fontSize: 17,
+ fontWeight: '500',
+ },
+});
+
+module.exports = NavigationExampleRow;
diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationExampleTabBar.js b/Examples/UIExplorer/NavigationExperimental/NavigationExampleTabBar.js
new file mode 100644
index 00000000000000..6dbaab095b9bd6
--- /dev/null
+++ b/Examples/UIExplorer/NavigationExperimental/NavigationExampleTabBar.js
@@ -0,0 +1,81 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+'use strict';
+
+var React = require('react-native');
+var {
+ NavigationExperimental,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} = React;
+const {
+ Container: NavigationContainer,
+ Reducer: NavigationReducer,
+} = NavigationExperimental;
+const {
+ JumpToAction,
+} = NavigationReducer.TabsReducer;
+
+var NavigationExampleTabBar = React.createClass({
+ render: function() {
+ return (
+
+ {this.props.tabs.map(this._renderTab)}
+
+ );
+ },
+ _renderTab: function(tab, index) {
+ var textStyle = [styles.tabButtonText];
+ if (this.props.index === index) {
+ textStyle.push(styles.selectedTabButtonText);
+ }
+ return (
+ {
+ this.props.onNavigate(JumpToAction(index));
+ }}>
+
+ {tab.key}
+
+
+ );
+ },
+});
+
+NavigationExampleTabBar = NavigationContainer.create(NavigationExampleTabBar);
+
+const styles = StyleSheet.create({
+ tabBar: {
+ height: 50,
+ flexDirection: 'row',
+ },
+ tabButton: {
+ flex: 1,
+ },
+ tabButtonText: {
+ paddingTop: 20,
+ textAlign: 'center',
+ fontSize: 17,
+ fontWeight: '500',
+ },
+ selectedTabButtonText: {
+ color: 'blue',
+ },
+});
+
+module.exports = NavigationExampleTabBar;
diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js
new file mode 100644
index 00000000000000..0d11a8e649fcee
--- /dev/null
+++ b/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js
@@ -0,0 +1,129 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+'use strict';
+
+var React = require('react-native');
+var {
+ AsyncStorage,
+ ScrollView,
+ StyleSheet,
+ View,
+} = React;
+var NavigationExampleRow = require('./NavigationExampleRow');
+
+/*
+ * Heads up! This file is not the real navigation example- only a utility to switch between them.
+ *
+ * To learn how to use the Navigation API, take a look at the following exmample files:
+ */
+var EXAMPLES = {
+ 'Tabs': require('./NavigationTabsExample'),
+ 'Basic': require('./NavigationBasicExample'),
+ 'Animated Card Stack': require('./NavigationAnimatedExample'),
+ 'Composition': require('./NavigationCompositionExample'),
+};
+
+var EXAMPLE_STORAGE_KEY = 'NavigationExampleExample';
+
+var NavigationExperimentalExample = React.createClass({
+ statics: {
+ title: 'Navigation (Experimental)',
+ description: 'Upcoming navigation APIs and animated navigation views',
+ external: true,
+ },
+
+ getInitialState: function() {
+ return {
+ exampe: null,
+ };
+ },
+
+ componentDidMount() {
+ AsyncStorage.getItem(EXAMPLE_STORAGE_KEY, (err, example) => {
+ if (err || !example) {
+ this.setState({
+ example: 'menu',
+ });
+ return;
+ }
+ this.setState({
+ example,
+ });
+ });
+ },
+
+ setExample: function(example) {
+ this.setState({
+ example,
+ });
+ AsyncStorage.setItem(EXAMPLE_STORAGE_KEY, example);
+ },
+
+ _renderMenu: function() {
+ var exitRow = null;
+ if (this.props.onExampleExit) {
+ exitRow = (
+
+ );
+ }
+ return (
+
+
+ {this._renderExampleList()}
+ {exitRow}
+
+
+ );
+ },
+
+ _renderExampleList: function() {
+ return Object.keys(EXAMPLES).map(exampleName => (
+ {
+ this.setExample(exampleName);
+ }}
+ />
+ ));
+ },
+
+ _exitInnerExample: function() {
+ this.setExample('menu');
+ },
+
+ render: function() {
+ if (this.state.example === 'menu') {
+ return this._renderMenu();
+ }
+ if (EXAMPLES[this.state.example]) {
+ var Component = EXAMPLES[this.state.example];
+ return ;
+ }
+ return null;
+ },
+});
+
+const styles = StyleSheet.create({
+ menu: {
+ backgroundColor: '#E9E9EF',
+ flex: 1,
+ marginTop: 20,
+ },
+});
+
+module.exports = NavigationExperimentalExample;
diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationTabsExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationTabsExample.js
new file mode 100644
index 00000000000000..beb3a6e35af396
--- /dev/null
+++ b/Examples/UIExplorer/NavigationExperimental/NavigationTabsExample.js
@@ -0,0 +1,104 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+'use strict';
+
+const React = require('react-native');
+const {
+ NavigationExperimental,
+ ScrollView,
+ StyleSheet,
+ View,
+} = React;
+const {
+ Container: NavigationContainer,
+ RootContainer: NavigationRootContainer,
+ Reducer: NavigationReducer,
+} = NavigationExperimental;
+
+const NavigationExampleRow = require('./NavigationExampleRow');
+const NavigationExampleTabBar = require('./NavigationExampleTabBar');
+
+class ExmpleTabPage extends React.Component {
+ render() {
+ const currentTabRoute = this.props.tabs[this.props.index];
+ return (
+
+
+ {this.props.tabs.map((tab, index) => (
+ {
+ this.props.onNavigate(NavigationReducer.TabsReducer.JumpToAction(index));
+ }}
+ />
+ ))}
+
+
+ );
+ }
+}
+ExmpleTabPage = NavigationContainer.create(ExmpleTabPage);
+
+const ExampleTabsReducer = NavigationReducer.TabsReducer({
+ tabReducers: [
+ (lastRoute) => lastRoute || {key: 'one'},
+ (lastRoute) => lastRoute || {key: 'two'},
+ (lastRoute) => lastRoute || {key: 'three'},
+ ],
+});
+
+const NavigationTabsExample = React.createClass({
+ render: function() {
+ return (
+ {
+ if (!navigationState) { return null; }
+ return (
+
+
+
+
+ );
+ }}
+ />
+ );
+ },
+});
+
+const styles = StyleSheet.create({
+ topView: {
+ flex: 1,
+ paddingTop: 30,
+ },
+ tabPage: {
+ backgroundColor: '#E9E9EF',
+ },
+});
+
+module.exports = NavigationTabsExample;
diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js
index 1cd0eaaaf0b53f..5c33d7f645e539 100644
--- a/Examples/UIExplorer/UIExplorerList.ios.js
+++ b/Examples/UIExplorer/UIExplorerList.ios.js
@@ -74,6 +74,7 @@ var APIS = [
require('./GeolocationExample'),
require('./ImageEditingExample'),
require('./LayoutExample'),
+ require('./NavigationExperimental/NavigationExperimentalExample'),
require('./NetInfoExample'),
require('./PanResponderExample'),
require('./PointerEventsExample'),
diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js
new file mode 100644
index 00000000000000..d25c45aefdec56
--- /dev/null
+++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js
@@ -0,0 +1,160 @@
+/**
+ * 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 NavigationCard
+ * @flow
+ */
+'use strict';
+
+const Animated = require('Animated');
+const NavigationReducer = require('NavigationReducer');
+const NavigationContainer = require('NavigationContainer');
+const PanResponder = require('PanResponder');
+const React = require('React');
+const StyleSheet = require('StyleSheet');
+const View = require('View');
+
+import type {
+ NavigationParentState
+} from 'NavigationState';
+
+type Layout = {
+ initWidth: number,
+ initHeight: number,
+ width: Animated.Value;
+ height: Animated.Value;
+};
+
+type Props = {
+ navigationState: NavigationParentState;
+ index: number;
+ position: Animated.Value;
+ layout: Layout;
+ onNavigate: Function;
+ children: Object;
+};
+
+class NavigationCard extends React.Component {
+ _responder: ?Object;
+ _lastHeight: number;
+ _lastWidth: number;
+ _widthListener: string;
+ _heightListener: string;
+ props: Props;
+ componentWillMount(props) {
+ this._responder = PanResponder.create({
+ onMoveShouldSetPanResponder: (e, {dx, dy, moveX, moveY, x0, y0}) => {
+ if (this.props.navigationState.index === 0) {
+ return false;
+ }
+ if (moveX > 30) {
+ return false;
+ }
+ if (dx > 5 && Math.abs(dy) < 4) {
+ return true;
+ }
+ return false;
+ },
+ onPanResponderGrant: (e, {dx, dy, moveX, moveY, x0, y0}) => {
+ },
+ onPanResponderMove: (e, {dx}) => {
+ const a = (-dx / this._lastWidth) + this.props.navigationState.index;
+ this.props.position.setValue(a);
+ },
+ onPanResponderRelease: (e, {vx, dx}) => {
+ const xRatio = dx / this._lastWidth;
+ const doesPop = (xRatio + vx) > 0.45;
+ if (doesPop) {
+ // todo: add an action which accepts velocity of the pop action/gesture, which is caught and used by NavigationAnimatedView
+ this.props.onNavigate(NavigationReducer.StackReducer.PopAction());
+ return;
+ }
+ Animated.spring(this.props.position, {
+ toValue: this.props.navigationState.index,
+ }).start();
+ },
+ onPanResponderTerminate: (e, {vx, dx}) => {
+ Animated.spring(this.props.position, {
+ toValue: this.props.navigationState.index,
+ }).start();
+ },
+ });
+ }
+ componentDidMount() {
+ this._lastHeight = this.props.layout.initHeight;
+ this._lastWidth = this.props.layout.initWidth;
+ this._widthListener = this.props.layout.width.addListener(({value}) => {
+ this._lastWidth = value;
+ });
+ this._heightListener = this.props.layout.height.addListener(({value}) => {
+ this._lastHeight = value;
+ });
+ // todo: fix listener and last layout dimentsions when props change. potential bugs here
+ }
+ componentWillUnmount() {
+ this.props.layout.width.removeListener(this._widthListener);
+ this.props.layout.height.removeListener(this._heightListener);
+ }
+ render() {
+ const cardPosition = Animated.add(this.props.position, new Animated.Value(-this.props.index));
+ const gestureValue = Animated.multiply(cardPosition, this.props.layout.width);
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+NavigationCard = NavigationContainer.create(NavigationCard);
+
+const styles = StyleSheet.create({
+ card: {
+ backgroundColor: '#E9E9EF',
+ shadowColor: 'black',
+ shadowOpacity: 0.4,
+ shadowOffset: {width: 0, height: 0},
+ shadowRadius: 10,
+ top: 0,
+ bottom: 0,
+ position: 'absolute',
+ },
+});
+
+module.exports = NavigationCard;
diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js b/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js
new file mode 100644
index 00000000000000..98a4c5e9140caa
--- /dev/null
+++ b/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js
@@ -0,0 +1,149 @@
+/**
+ * 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 NavigationHeader
+ * @flow
+ */
+'use strict';
+
+const Animated = require('Animated');
+const Image = require('Image');
+const NavigationContainer = require('NavigationContainer');
+const NavigationReducer = require('NavigationReducer');
+const React = require('react-native');
+const StyleSheet = require('StyleSheet');
+const Text = require('Text');
+const TouchableOpacity = require('TouchableOpacity');
+const View = require('View');
+
+import type {
+ NavigationState,
+ NavigationParentState
+} from 'NavigationState';
+
+type Props = {
+ navigationState: NavigationParentState,
+ onNavigate: Function,
+ position: Animated.Value,
+ getTitle: (navState: NavigationState) => string,
+};
+
+class NavigationHeader extends React.Component {
+ _handleBackPress: Function;
+ props: Props;
+ componentWillMount() {
+ this._handleBackPress = this._handleBackPress.bind(this);
+ }
+ render() {
+ var state = this.props.navigationState;
+ return (
+
+ {state.children.map(this._renderTitle, this)}
+ {this._renderBackButton()}
+
+ );
+ }
+ _renderBackButton() {
+ if (this.props.navigationState.index === 0) {
+ return null;
+ }
+ return (
+
+
+
+ );
+ }
+ _renderTitle(childState, index) {
+ return (
+
+ {this.props.getTitle(childState)}
+
+ );
+ }
+ _handleBackPress() {
+ this.props.onNavigate(NavigationReducer.StackReducer.PopAction());
+ }
+}
+
+NavigationHeader = NavigationContainer.create(NavigationHeader);
+
+const styles = StyleSheet.create({
+ title: {
+ textAlign: 'center',
+ marginTop: 10,
+ fontSize: 18,
+ fontWeight: '500',
+ color: '#0A0A0A',
+ position: 'absolute',
+ top: 20,
+ left: 0,
+ right: 0,
+ },
+ header: {
+ backgroundColor: '#EFEFF2',
+ paddingTop: 20,
+ top: 0,
+ height: 64,
+ right: 0,
+ left: 0,
+ borderBottomWidth: 0.5,
+ borderBottomColor: '#828287',
+ position: 'absolute',
+ },
+ backButton: {
+ width: 29,
+ height: 37,
+ position: 'absolute',
+ bottom: 4,
+ left: 2,
+ padding: 8,
+ },
+ backButtonImage: {
+ width: 13,
+ height: 21,
+ },
+});
+
+module.exports = NavigationHeader;
diff --git a/Libraries/CustomComponents/NavigationExperimental/back_chevron.png b/Libraries/CustomComponents/NavigationExperimental/back_chevron.png
new file mode 100644
index 00000000000000..af21cf9d5718b7
Binary files /dev/null and b/Libraries/CustomComponents/NavigationExperimental/back_chevron.png differ
diff --git a/Libraries/NavigationExperimental/NavigationAnimatedView.js b/Libraries/NavigationExperimental/NavigationAnimatedView.js
new file mode 100644
index 00000000000000..8e6dce68045287
--- /dev/null
+++ b/Libraries/NavigationExperimental/NavigationAnimatedView.js
@@ -0,0 +1,225 @@
+/**
+ * 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 NavigationAnimatedView
+ * @flow
+ */
+'use strict';
+
+var Animated = require('Animated');
+var Map = require('Map');
+var NavigationStateUtils = require('NavigationState');
+var NavigationContainer = require('NavigationContainer');
+var React = require('react-native');
+var View = require('View');
+
+import type {
+ NavigationState,
+ NavigationParentState,
+} from 'NavigationState';
+
+type NavigationScene = {
+ index: number,
+ state: NavigationState,
+ isStale: boolean,
+};
+
+/**
+ * Helper function to compare route keys (e.g. "9", "11").
+ */
+function compareKey(one: string, two: string): number {
+ var delta = one.length - two.length;
+ if (delta > 0) {
+ return 1;
+ }
+ if (delta < 0) {
+ return -1;
+ }
+ return one > two ? 1 : -1;
+}
+
+/**
+ * Helper function to sort scenes based on their index and view key.
+ */
+function compareScenes(
+ one: NavigationScene,
+ two: NavigationScene
+): number {
+ if (one.index > two.index) {
+ return 1;
+ }
+ if (one.index < two.index) {
+ return -1;
+ }
+
+ return compareKey(
+ one.state.key,
+ two.state.key
+ );
+}
+
+type Layout = {
+ initWidth: number,
+ initHeight: number,
+ width: Animated.Value;
+ height: Animated.Value;
+};
+
+type OverlayRenderer = (
+ position: Animated.Value,
+ layout: Layout
+) => ReactElement;
+
+type SceneRenderer = (
+ state: NavigationState,
+ index: number,
+ position: Animated.Value,
+ layout: Layout
+) => ReactElement
+
+type Props = {
+ navigationState: NavigationParentState;
+ renderScene: SceneRenderer;
+ renderOverlay: ?OverlayRenderer;
+ style: any;
+};
+
+class NavigationAnimatedView extends React.Component {
+ _animatedHeight: Animated.Value;
+ _animatedWidth: Animated.Value;
+ _lastHeight: number;
+ _lastWidth: number;
+ props: Props;
+ constructor(props) {
+ super(props);
+ this._animatedHeight = new Animated.Value(0);
+ this._animatedWidth = new Animated.Value(0);
+ this.state = {
+ position: new Animated.Value(this.props.navigationState.index),
+ scenes: new Map(),
+ };
+ }
+ componentWillMount() {
+ this.setState({
+ scenes: this._reduceScenes(this.state.scenes, this.props.navigationState),
+ });
+ }
+ componentDidMount() {
+ this.postionListener = this.state.position.addListener(this._onProgressChange.bind(this));
+ }
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.navigationState !== this.props.navigationState) {
+ this.setState({
+ scenes: this._reduceScenes(this.state.scenes, nextProps.navigationState, this.props.navigationState),
+ });
+ }
+ }
+ componentDidUpdate(lastProps) {
+ if (lastProps.navigationState.index !== this.props.navigationState.index) {
+ Animated.spring(
+ this.state.position,
+ {toValue: this.props.navigationState.index}
+ ).start();
+ }
+ }
+ componentWillUnmount() {
+ if (this.postionListener) {
+ this.state.position.removeListener(this.postionListener);
+ this.postionListener = null;
+ }
+ }
+ _onProgressChange(data: Object): void {
+ if (Math.abs(data.value - this.props.navigationState.index) > Number.EPSILON) {
+ return;
+ }
+ this.state.scenes.forEach((scene, index) => {
+ if (scene.isStale) {
+ const scenes = this.state.scenes.slice();
+ scenes.splice(index, 1);
+ this.setState({ scenes, });
+ }
+ });
+ }
+ _reduceScenes(
+ scenes: Array,
+ nextState: NavigationParentState,
+ lastState: ?NavigationParentState
+ ): Array {
+ let nextScenes = nextState.children.map((child, index) => {
+ return {
+ index,
+ state: child,
+ isStale: false,
+ };
+ });
+
+ if (lastState) {
+ lastState.children.forEach((child, index) => {
+ if (!NavigationStateUtils.get(nextState, child.key)) {
+ nextScenes.push({
+ index,
+ state: child,
+ isStale: true,
+ });
+ }
+ });
+ }
+
+ nextScenes = nextScenes.sort(compareScenes);
+
+ return nextScenes;
+ }
+ render() {
+ return (
+ {
+ const {height, width} = e.nativeEvent.layout;
+ this._animatedHeight &&
+ this._animatedHeight.setValue(height);
+ this._animatedWidth &&
+ this._animatedWidth.setValue(width);
+ this._lastHeight = height;
+ this._lastWidth = width;
+ }}
+ style={this.props.style}>
+ {this.state.scenes.map(this._renderScene, this)}
+ {this._renderOverlay(this._renderOverlay, this)}
+
+ );
+ }
+ _getLayout() {
+ return {
+ height: this._animatedHeight,
+ width: this._animatedWidth,
+ initWidth: this._lastWidth,
+ initHeight: this._lastHeight,
+ };
+ }
+ _renderScene(scene: NavigationScene) {
+ return this.props.renderScene(
+ scene.state,
+ scene.index,
+ this.state.position,
+ this._getLayout()
+ );
+ }
+ _renderOverlay() {
+ const {renderOverlay} = this.props;
+ if (renderOverlay) {
+ return renderOverlay(
+ this.state.position,
+ this._getLayout()
+ );
+ }
+ return null;
+ }
+}
+
+NavigationAnimatedView = NavigationContainer.create(NavigationAnimatedView);
+
+module.exports = NavigationAnimatedView;
diff --git a/Libraries/NavigationExperimental/NavigationContainer.js b/Libraries/NavigationExperimental/NavigationContainer.js
new file mode 100644
index 00000000000000..8e178f1c6faa56
--- /dev/null
+++ b/Libraries/NavigationExperimental/NavigationContainer.js
@@ -0,0 +1,51 @@
+/**
+ * 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 NavigationContainer
+ * @flow
+ */
+'use strict';
+
+var React = require('react-native');
+var NavigationRootContainer = require('NavigationRootContainer');
+
+function createNavigationContainer(Component: React.Component): React.Component {
+ class NavigationComponent extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+ getNavigationHandler() {
+ return this.props.onNavigate || this.context.onNavigate;
+ }
+ getChildContext() {
+ return {
+ onNavigate: this.getNavigationHandler(),
+ };
+ }
+ }
+ NavigationComponent.contextTypes = {
+ onNavigate: React.PropTypes.func,
+ };
+ NavigationComponent.childContextTypes = {
+ onNavigate: React.PropTypes.func,
+ };
+ return NavigationComponent;
+}
+
+var NavigationContainer = {
+ create: createNavigationContainer,
+ RootContainer: NavigationRootContainer,
+};
+
+
+module.exports = NavigationContainer;
diff --git a/Libraries/NavigationExperimental/NavigationExperimental.js b/Libraries/NavigationExperimental/NavigationExperimental.js
new file mode 100644
index 00000000000000..9d37015f6a17b2
--- /dev/null
+++ b/Libraries/NavigationExperimental/NavigationExperimental.js
@@ -0,0 +1,41 @@
+/**
+ * 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 NavigationExperimental
+ * @flow
+ */
+'use strict';
+
+const NavigationAnimatedView = require('NavigationAnimatedView');
+const NavigationCard = require('NavigationCard');
+const NavigationContainer = require('NavigationContainer');
+const NavigationHeader = require('NavigationHeader');
+const NavigationRootContainer = require('NavigationRootContainer');
+const NavigationReducer = require('NavigationReducer');
+const NavigationState = require('NavigationState');
+const NavigationView = require('NavigationView');
+
+const NavigationExperimental = {
+ // Core
+ State: NavigationState,
+ Reducer: NavigationReducer,
+
+ // Containers
+ Container: NavigationContainer,
+ RootContainer: NavigationRootContainer,
+
+ // Views
+ View: NavigationView,
+ AnimatedView: NavigationAnimatedView,
+
+ // CustomComponents:
+ Header: NavigationHeader,
+ Card: NavigationCard,
+};
+
+module.exports = NavigationExperimental;
diff --git a/Libraries/NavigationExperimental/NavigationRootContainer.js b/Libraries/NavigationExperimental/NavigationRootContainer.js
new file mode 100644
index 00000000000000..a9beeba62741b8
--- /dev/null
+++ b/Libraries/NavigationExperimental/NavigationRootContainer.js
@@ -0,0 +1,86 @@
+/**
+ * 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 NavigationRootContainer
+ * @flow
+ */
+'use strict';
+
+var AsyncStorage = require('AsyncStorage');
+var React = require('React');
+
+import type {
+ NavigationState,
+ NavigationReducer
+} from 'NavigationState';
+
+type NavigationRenderer = (
+ navigationState: NavigationState,
+ onNavigate: Function
+) => ReactElement;
+
+type Props = {
+ renderNavigation: NavigationRenderer;
+ reducer: NavigationReducer;
+ persistenceKey: ?string;
+};
+
+class NavigationRootContainer extends React.Component {
+ props: Props;
+ constructor(props: Props) {
+ super(props);
+ this.handleNavigation = this.handleNavigation.bind(this);
+ let navState = null;
+ if (!this.props.persistenceKey) {
+ navState = this.props.reducer(null, null);
+ }
+ this.state = { navState };
+ }
+ componentDidMount() {
+ if (this.props.persistenceKey) {
+ AsyncStorage.getItem(this.props.persistenceKey, (err, storedString) => {
+ if (err || !storedString) {
+ this.setState({
+ navState: this.props.reducer(null, null),
+ });
+ return;
+ }
+ this.setState({
+ navState: JSON.parse(storedString),
+ });
+ });
+ }
+ }
+ getChildContext(): Object {
+ return {
+ onNavigate: this.handleNavigation,
+ };
+ }
+ handleNavigation(action: Object) {
+ const navState = this.props.reducer(this.state.navState, action);
+ this.setState({
+ navState,
+ });
+ if (this.props.persistenceKey) {
+ AsyncStorage.setItem(this.props.persistenceKey, JSON.stringify(navState));
+ }
+ }
+ render(): ReactElement {
+ var navigation = this.props.renderNavigation(
+ this.state.navState,
+ this.handleNavigation
+ );
+ return navigation;
+ }
+}
+
+NavigationRootContainer.childContextTypes = {
+ onNavigate: React.PropTypes.func,
+};
+
+module.exports = NavigationRootContainer;
diff --git a/Libraries/NavigationExperimental/NavigationState.js b/Libraries/NavigationExperimental/NavigationState.js
new file mode 100644
index 00000000000000..8ef085fd9b2ad7
--- /dev/null
+++ b/Libraries/NavigationExperimental/NavigationState.js
@@ -0,0 +1,196 @@
+/**
+ * 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 NavigationState
+ * @flow
+ */
+'use strict';
+
+const invariant = require('invariant');
+
+export type NavigationState = {
+ key: string;
+};
+
+export type NavigationParentState = {
+ key: string;
+ index: number;
+ children: Array;
+};
+
+export type NavigationReducer = (
+ state: ?NavigationState,
+ action: ?any
+) => ?NavigationState;
+
+export type NavigationReducerWithDefault = (
+ state: ?NavigationState,
+ action: ?any
+) => NavigationState;
+
+export function getParent(state: NavigationState): ?NavigationParentState {
+ if (
+ (state instanceof Object) &&
+ (state.children instanceof Array) &&
+ (state.children[0] !== undefined) &&
+ (typeof state.index === 'number') &&
+ (state.children[state.index] !== undefined)
+ ) {
+ return state;
+ }
+ return null;
+}
+
+export function get(state: NavigationState, key: string): ?NavigationState {
+ const parentState = getParent(state);
+ if (!parentState) {
+ return null;
+ }
+ const childState = parentState.children.find(child => child.key === key);
+ return childState || null;
+}
+
+export function indexOf(state: NavigationState, key: string): ?number {
+ const parentState = getParent(state);
+ if (!parentState) {
+ return null;
+ }
+ const index = parentState.children.map(child => child.key).indexOf(key);
+ if (index === -1) {
+ return null;
+ }
+ return index;
+}
+
+export function push(state: NavigationState, newChildState: NavigationState): NavigationState {
+ const parentState = getParent(state);
+ if (!parentState) {
+ return state;
+ }
+ var lastChildren: Array = parentState.children;
+ return {
+ ...parentState,
+ children: [
+ ...lastChildren,
+ newChildState,
+ ],
+ index: lastChildren.length,
+ };
+}
+
+export function pop(state: NavigationState): NavigationState {
+ const parentState = getParent(state);
+ if (!parentState) {
+ return state;
+ }
+ const lastChildren = parentState.children;
+ return {
+ ...parentState,
+ children: lastChildren.slice(0, lastChildren.length - 1),
+ index: lastChildren.length - 2,
+ };
+}
+
+export function reset(state: NavigationState, nextChildren: ?Array, nextIndex: ?number): NavigationState {
+ const parentState = getParent(state);
+ if (!parentState) {
+ return state;
+ }
+ const children = nextChildren || parentState.children;
+ const index = nextIndex == null ? parentState.index : nextIndex;
+ if (children === parentState.children && index === parentState.index) {
+ return state;
+ }
+ return {
+ ...parentState,
+ children,
+ index,
+ };
+}
+
+export function set(state: ?NavigationState, key: string, nextChildren: Array, nextIndex: number): NavigationState {
+ if (!state) {
+ return {
+ children: nextChildren,
+ index: nextIndex,
+ key,
+ };
+ }
+ const parentState = getParent(state);
+ if (!parentState) {
+ return {
+ children: nextChildren,
+ index: nextIndex,
+ key,
+ };
+ }
+ if (nextChildren === parentState.children && nextIndex === parentState.index && key === parentState.key) {
+ return parentState;
+ }
+ return {
+ ...parentState,
+ children: nextChildren,
+ index: nextIndex,
+ key,
+ };
+}
+
+export function jumpToIndex(state: NavigationState, index: number): NavigationState {
+ const parentState = getParent(state);
+ return {
+ ...parentState,
+ index,
+ };
+}
+
+export function jumpTo(state: NavigationState, key: string): NavigationState {
+ const parentState = getParent(state);
+ if (!parentState) {
+ return state;
+ }
+ const index = parentState.children.indexOf(parentState.children.find(child => child.key === key));
+ invariant(
+ index !== -1,
+ 'Cannot find child with matching key in this NavigationState'
+ );
+ return {
+ ...parentState,
+ index,
+ };
+}
+
+export function replaceAt(state: NavigationState, key: string, newState: NavigationState): NavigationState {
+ const parentState = getParent(state);
+ if (!parentState) {
+ return state;
+ }
+ const children = [...parentState.children];
+ const index = parentState.children.indexOf(parentState.children.find(child => child.key === key));
+ invariant(
+ index !== -1,
+ 'Cannot find child with matching key in this NavigationState'
+ );
+ children[index] = newState;
+ return {
+ ...parentState,
+ children,
+ };
+}
+
+export function replaceAtIndex(state: NavigationState, index: number, newState: NavigationState): NavigationState {
+ const parentState = getParent(state);
+ if (!parentState) {
+ return state;
+ }
+ const children = [...parentState.children];
+ children[index] = newState;
+ return {
+ ...parentState,
+ children,
+ };
+}
diff --git a/Libraries/NavigationExperimental/NavigationView.js b/Libraries/NavigationExperimental/NavigationView.js
new file mode 100644
index 00000000000000..5e0fc8b28f71e2
--- /dev/null
+++ b/Libraries/NavigationExperimental/NavigationView.js
@@ -0,0 +1,63 @@
+/**
+ * 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 NavigationView
+ * @flow
+ */
+'use strict';
+
+var React = require('react-native');
+var NavigationContainer = require('NavigationContainer');
+var {
+ StyleSheet,
+ View,
+} = React;
+
+var NavigationView = React.createClass({
+ propTypes: {
+ // todo, figure out a propType for getK
+ navigationState: React.PropTypes.object.isRequired,
+ renderScene: React.PropTypes.func.isRequired,
+ },
+ render: function() {
+ return (
+
+ {this.props.navigationState.children.map(this._renderScene)}
+
+ );
+ },
+ _renderScene: function(route, index) {
+ var isSelected = index === this.props.navigationState.index;
+ return (
+
+ {this.props.renderScene(route, index)}
+
+ );
+ },
+});
+
+NavigationView = NavigationContainer.create(NavigationView);
+
+var styles = StyleSheet.create({
+ navView: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ },
+});
+
+module.exports = NavigationView;
diff --git a/Libraries/NavigationExperimental/Reducer/NavigationFindReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationFindReducer.js
new file mode 100644
index 00000000000000..db8bb8f400715e
--- /dev/null
+++ b/Libraries/NavigationExperimental/Reducer/NavigationFindReducer.js
@@ -0,0 +1,38 @@
+/**
+ * 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 NavigationFindReducer
+ * @flow
+ */
+'use strict';
+
+/*
+ * NavigationFindReducer takes an array of reducers, and returns a reducer that
+ * iterates through all of the reducers and the result of the first reducer
+ * that modifies the input
+ */
+
+import type {
+ NavigationState,
+ NavigationReducer
+} from 'NavigationState';
+
+function NavigationFindReducer(reducers: Array): ?NavigationReducer {
+ return function(lastState: ?NavigationState, action: ?any): ?NavigationState {
+ for (let i = 0; i < reducers.length; i++) {
+ let reducer = reducers[i];
+ let newState = reducer(lastState, action);
+ if (newState !== lastState) {
+ return newState;
+ }
+ }
+ return lastState;
+ };
+}
+
+module.exports = NavigationFindReducer;
diff --git a/Libraries/NavigationExperimental/Reducer/NavigationReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationReducer.js
new file mode 100644
index 00000000000000..4fb52297daaa7e
--- /dev/null
+++ b/Libraries/NavigationExperimental/Reducer/NavigationReducer.js
@@ -0,0 +1,24 @@
+/**
+ * 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 NavigationReducer
+ * @flow
+ */
+'use strict';
+
+var NavigationFindReducer = require('NavigationFindReducer');
+var NavigationStackReducer = require('NavigationStackReducer');
+var NavigationTabsReducer = require('NavigationTabsReducer');
+
+const NavigationReducer = {
+ FindReducer: NavigationFindReducer,
+ StackReducer: NavigationStackReducer,
+ TabsReducer: NavigationTabsReducer,
+};
+
+module.exports = NavigationReducer;
diff --git a/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js
new file mode 100644
index 00000000000000..7aec5f4ee4cd08
--- /dev/null
+++ b/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js
@@ -0,0 +1,141 @@
+/**
+ * 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 NavigationStackReducer
+ * @flow
+ */
+'use strict';
+
+var NavigationStateUtils = require('NavigationState');
+
+import type {
+ NavigationState,
+ NavigationReducer,
+} from 'NavigationState';
+
+export type NavigationStackReducerAction = {
+ type: string,
+};
+
+const ActionTypes = {
+ PUSH: 'react-native/NavigationExperimental/stack-push',
+ POP: 'react-native/NavigationExperimental/stack-pop',
+ JUMP_TO: 'react-native/NavigationExperimental/stack-jumpTo',
+ JUMP_TO_INDEX: 'react-native/NavigationExperimental/stack-jumpToIndex',
+ RESET: 'react-native/NavigationExperimental/stack-reset',
+};
+
+const DEFAULT_KEY = 'NAV_STACK_DEFAULT_KEY';
+
+function NavigationStackPushAction(state: NavigationState): NavigationStackReducerAction {
+ return {
+ type: ActionTypes.PUSH,
+ state,
+ };
+}
+
+function NavigationStackPopAction(): NavigationStackReducerAction {
+ return {
+ type: ActionTypes.POP,
+ };
+}
+
+function NavigationStackJumpToAction(key: string): NavigationStackReducerAction {
+ return {
+ type: ActionTypes.JUMP_TO,
+ key,
+ };
+}
+
+function NavigationStackJumpToIndexAction(index: number): NavigationStackReducerAction {
+ return {
+ type: ActionTypes.JUMP_TO_INDEX,
+ index,
+ };
+}
+
+function NavigationStackResetAction(children: Array, index: number): NavigationStackReducerAction {
+ return {
+ type: ActionTypes.RESET,
+ index,
+ children,
+ };
+}
+
+type StackReducerConfig = {
+ initialStates: Array;
+ initialIndex: ?number;
+ key: ?string;
+ matchAction: (action: any) => boolean;
+ actionStateMap: (action: any) => NavigationState;
+};
+
+function NavigationStackReducer({initialStates, initialIndex, key, matchAction, actionStateMap}: StackReducerConfig): NavigationReducer {
+ return function (lastState: ?NavigationState, action: any): NavigationState {
+ if (key == null) {
+ key = DEFAULT_KEY;
+ }
+ if (initialIndex == null) {
+ initialIndex = initialStates.length - 1;
+ }
+ if (!lastState) {
+ lastState = {
+ index: initialIndex,
+ children: initialStates,
+ key,
+ };
+ }
+ const lastParentState = NavigationStateUtils.getParent(lastState);
+ if (!action || !lastParentState) {
+ return lastState;
+ }
+ switch (action.type) {
+ case ActionTypes.PUSH:
+ return NavigationStateUtils.push(
+ lastParentState,
+ action.state
+ );
+ case ActionTypes.POP:
+ if (lastParentState.index === 0 || lastParentState.children.length === 1) {
+ return lastParentState;
+ }
+ return NavigationStateUtils.pop(lastParentState);
+ case ActionTypes.JUMP_TO:
+ return NavigationStateUtils.jumpTo(
+ lastParentState,
+ action.key
+ );
+ case ActionTypes.JUMP_TO_INDEX:
+ return NavigationStateUtils.jumpToIndex(
+ lastParentState,
+ action.index
+ );
+ case ActionTypes.RESET:
+ return {
+ ...lastParentState,
+ index: action.index,
+ children: action.children,
+ };
+ }
+ if (matchAction(action)) {
+ return NavigationStateUtils.push(
+ lastParentState,
+ actionStateMap(action)
+ );
+ }
+ return lastParentState;
+ };
+}
+
+NavigationStackReducer.PushAction = NavigationStackPushAction;
+NavigationStackReducer.PopAction = NavigationStackPopAction;
+NavigationStackReducer.JumpToAction = NavigationStackJumpToAction;
+NavigationStackReducer.JumpToIndexAction = NavigationStackJumpToIndexAction;
+NavigationStackReducer.ResetAction = NavigationStackResetAction;
+
+module.exports = NavigationStackReducer;
diff --git a/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js
new file mode 100644
index 00000000000000..6d31164f0cac33
--- /dev/null
+++ b/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js
@@ -0,0 +1,143 @@
+/**
+ * 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 NavigationTabsReducer
+ * @flow
+ */
+'use strict';
+
+const NavigationFindReducer = require('NavigationFindReducer');
+const NavigationStateUtils = require('NavigationState');
+
+import type {
+ NavigationReducer,
+ NavigationReducerWithDefault,
+ NavigationState
+} from 'NavigationState';
+
+const ActionTypes = {
+ JUMP_TO: 'react-native/NavigationExperimental/tabs-jumpTo',
+ ON_TAB_ACTION: 'react-native/NavigationExperimental/tabs-onTabAction',
+};
+
+const DEFAULT_KEY = 'TABS_STATE_DEFAULT_KEY';
+
+export type JumpToAction = {
+ type: typeof ActionTypes.JUMP_TO,
+ index: number,
+};
+function NavigationTabsJumpToAction(index: number): JumpToAction {
+ return {
+ type: ActionTypes.JUMP_TO,
+ index,
+ };
+}
+
+export type OnTabAction = {
+ type: string,
+ index: number,
+ action: any,
+};
+function NavigationTabsOnTabAction(index: number, action: any): OnTabAction {
+ return {
+ type: ActionTypes.ON_TAB_ACTION,
+ index,
+ action,
+ };
+}
+
+type TabsReducerConfig = {
+ key: string;
+ initialIndex: ?number;
+ tabReducers: Array;
+};
+
+function NavigationTabsReducer({key, initialIndex, tabReducers}: TabsReducerConfig): NavigationReducer {
+ if (initialIndex == null) {
+ initialIndex = 0;
+ }
+ if (key == null) {
+ key = DEFAULT_KEY;
+ }
+ return function(lastNavState: ?NavigationState, action: ?any): ?NavigationState {
+ if (!lastNavState) {
+ lastNavState = {
+ children: tabReducers.map(reducer => reducer(null, null)),
+ index: initialIndex,
+ key,
+ };
+ }
+ const lastParentNavState = NavigationStateUtils.getParent(lastNavState);
+ if (!action || !lastParentNavState) {
+ return lastNavState;
+ }
+ if (
+ action.type === ActionTypes.JUMP_TO &&
+ action.index !== lastParentNavState.index
+ ) {
+ return NavigationStateUtils.jumpToIndex(
+ lastParentNavState,
+ action.index,
+ );
+ }
+ if (action.type === ActionTypes.ON_TAB_ACTION) {
+ const onTabAction: OnTabAction = action;
+ const lastTabRoute = lastParentNavState.children[onTabAction.index];
+ const tabReducer = tabReducers[onTabAction.index];
+ if (tabReducer) {
+ const newTabRoute = tabReducer(lastTabRoute, action.action);
+ if (newTabRoute && newTabRoute !== lastTabRoute) {
+ let navState = NavigationStateUtils.replaceAtIndex(
+ lastParentNavState,
+ onTabAction.index,
+ newTabRoute
+ );
+ navState = NavigationStateUtils.jumpToIndex(
+ navState,
+ onTabAction.index
+ );
+ return navState;
+ }
+ }
+ }
+ const subReducers = tabReducers.map((tabReducer, tabIndex) => {
+ return function reduceTab(lastTabState: ?NavigationState, tabAction: ?any): ?NavigationState {
+ if (!lastTabState) {
+ return tabReducer(lastTabState, tabAction);
+ }
+ if (!lastParentNavState) {
+ return lastTabState;
+ }
+ const lastSubTabState = lastParentNavState.children[tabIndex];
+ const nextSubTabState = tabReducer(lastSubTabState, tabAction);
+ if (nextSubTabState && lastSubTabState !== nextSubTabState) {
+ const tabs = lastParentNavState.children;
+ tabs[tabIndex] = nextSubTabState;
+ return {
+ ...lastParentNavState,
+ tabs,
+ index: tabIndex,
+ };
+ }
+ return lastParentNavState;
+ };
+ });
+ let selectedTabReducer = subReducers.splice(lastParentNavState.index, 1)[0];
+ subReducers.unshift(selectedTabReducer);
+ const findReducer = NavigationFindReducer(subReducers);
+ if (findReducer) {
+ return findReducer(lastParentNavState, action);
+ }
+ return lastParentNavState;
+ };
+}
+
+NavigationTabsReducer.JumpToAction = NavigationTabsJumpToAction;
+NavigationTabsReducer.OnTabAction = NavigationTabsOnTabAction;
+
+module.exports = NavigationTabsReducer;
diff --git a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationFindReducer-test.js b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationFindReducer-test.js
new file mode 100644
index 00000000000000..e67e64f2e481b7
--- /dev/null
+++ b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationFindReducer-test.js
@@ -0,0 +1,46 @@
+/**
+ * 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.
+ *
+ * @flow-broken
+ */
+'use strict';
+
+jest
+ .autoMockOff()
+ .mock('ErrorUtils');
+
+const NavigationFindReducer = require('NavigationFindReducer');
+
+describe('NavigationFindReducer', () => {
+
+ it('handles basic find reducing with strings', () => {
+ let reducer = NavigationFindReducer([
+ s => s,
+ s => s + '_yes',
+ s => 'nope',
+ ]);
+ let route = reducer('input');
+ expect(route).toBe('input_yes');
+
+ reducer = NavigationFindReducer([
+ (s, action) => s,
+ (s, action) => 'origRoute',
+ (s, action) => 'firstChangedState',
+ ]);
+ route = reducer('origRoute', 'action1');
+ expect(route).toBe('firstChangedState');
+
+ reducer = NavigationFindReducer([
+ (s, action) => s,
+ (s, action) => action,
+ ]);
+ route = reducer('inputState', 'action2');
+ expect(route).toBe('action2');
+ });
+
+});
diff --git a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationStackReducer-test.js b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationStackReducer-test.js
new file mode 100644
index 00000000000000..4d497346cfeb9d
--- /dev/null
+++ b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationStackReducer-test.js
@@ -0,0 +1,197 @@
+/**
+ * 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.
+ *
+ * @flow-broken
+ */
+'use strict';
+
+jest
+ .autoMockOff()
+ .mock('ErrorUtils');
+
+const NavigationStackReducer = require('NavigationStackReducer');
+
+const {
+ JumpToAction,
+ JumpToIndexAction,
+ PopAction,
+ PushAction,
+ ResetAction,
+} = NavigationStackReducer;
+
+describe('NavigationStackReducer', () => {
+
+ it('handles PushAction', () => {
+ const initialStates = [
+ {key: 'route0'},
+ {key: 'route1'},
+ ];
+ let reducer = NavigationStackReducer({
+ initialStates,
+ matchAction: () => true,
+ actionStateMap: (action) => action,
+ });
+
+ let state = reducer();
+ expect(state.children).toBe(initialStates);
+ expect(state.index).toBe(1);
+ expect(state.key).toBe('NAV_STACK_DEFAULT_KEY');
+
+ state = reducer(state, PushAction({key: 'route2'}));
+ expect(state.children[0].key).toBe('route0');
+ expect(state.children[1].key).toBe('route1');
+ expect(state.children[2].key).toBe('route2');
+ expect(state.index).toBe(2);
+ });
+
+ it('handles PopAction', () => {
+ let reducer = NavigationStackReducer({
+ initialStates: [
+ {key: 'a'},
+ {key: 'b'},
+ ],
+ initialIndex: 1,
+ key: 'myStack',
+ matchAction: () => true,
+ actionStateMap: (action) => action,
+ });
+
+ let state = reducer();
+ expect(state.children[0].key).toBe('a');
+ expect(state.children[1].key).toBe('b');
+ expect(state.children.length).toBe(2);
+ expect(state.index).toBe(1);
+ expect(state.key).toBe('myStack');
+
+ state = reducer(state, PopAction());
+ expect(state.children[0].key).toBe('a');
+ expect(state.children.length).toBe(1);
+ expect(state.index).toBe(0);
+ expect(state.key).toBe('myStack');
+
+ // make sure Pop on an single-route state is a no-op
+ state = reducer(state, PopAction());
+ expect(state.children[0].key).toBe('a');
+ expect(state.children.length).toBe(1);
+ expect(state.index).toBe(0);
+ expect(state.key).toBe('myStack');
+ });
+
+ it('handles JumpToAction', () => {
+ let reducer = NavigationStackReducer({
+ initialStates: [
+ {key: 'a'},
+ {key: 'b'},
+ {key: 'c'},
+ ],
+ initialIndex: 0,
+ key: 'myStack',
+ matchAction: () => true,
+ actionStateMap: (action) => action,
+ });
+
+ let state = reducer();
+ expect(state.children[0].key).toBe('a');
+ expect(state.children[1].key).toBe('b');
+ expect(state.children[2].key).toBe('c');
+ expect(state.children.length).toBe(3);
+ expect(state.index).toBe(0);
+
+ state = reducer(state, JumpToAction('b'));
+ expect(state.children[0].key).toBe('a');
+ expect(state.children[1].key).toBe('b');
+ expect(state.children[2].key).toBe('c');
+ expect(state.children.length).toBe(3);
+ expect(state.index).toBe(1);
+
+ state = reducer(state, JumpToAction('c'));
+ expect(state.children[0].key).toBe('a');
+ expect(state.children[1].key).toBe('b');
+ expect(state.children[2].key).toBe('c');
+ expect(state.children.length).toBe(3);
+ expect(state.index).toBe(2);
+
+ state = reducer(state, JumpToAction('c'));
+ expect(state.children[0].key).toBe('a');
+ expect(state.children[1].key).toBe('b');
+ expect(state.children[2].key).toBe('c');
+ expect(state.children.length).toBe(3);
+ expect(state.index).toBe(2);
+ expect(state.key).toBe('myStack');
+ });
+
+ it('handles JumpToIndexAction', () => {
+ let reducer = NavigationStackReducer({
+ initialStates: [
+ {key: 'a'},
+ {key: 'b'},
+ {key: 'c'},
+ ],
+ initialIndex: 2,
+ key: 'myStack',
+ matchAction: () => true,
+ actionStateMap: (action) => action,
+ });
+
+ let state = reducer();
+ expect(state.children.length).toBe(3);
+ expect(state.index).toBe(2);
+
+ state = reducer(state, JumpToIndexAction(0));
+ expect(state.children.length).toBe(3);
+ expect(state.index).toBe(0);
+
+ state = reducer(state, JumpToIndexAction(1));
+ expect(state.children[0].key).toBe('a');
+ expect(state.children[1].key).toBe('b');
+ expect(state.children[2].key).toBe('c');
+ expect(state.children.length).toBe(3);
+ expect(state.index).toBe(1);
+ expect(state.key).toBe('myStack');
+ });
+
+ it('handles ResetAction', () => {
+ let reducer = NavigationStackReducer({
+ initialStates: [
+ {key: 'a'},
+ {key: 'b'},
+ ],
+ initialIndex: 1,
+ key: 'myStack',
+ matchAction: () => true,
+ actionStateMap: (action) => action,
+ });
+
+ let state = reducer();
+ expect(state.children[0].key).toBe('a');
+ expect(state.children[1].key).toBe('b');
+ expect(state.children.length).toBe(2);
+ expect(state.index).toBe(1);
+
+ state = reducer(state, ResetAction([{key: 'c'}, {key: 'd'}], 0));
+ expect(state.children[0].key).toBe('c');
+ expect(state.children[1].key).toBe('d');
+ expect(state.children.length).toBe(2);
+ expect(state.index).toBe(0);
+
+ const newStates = [
+ {key: 'e'},
+ {key: 'f'},
+ {key: 'g'},
+ ];
+
+ state = reducer(state, ResetAction(newStates, 1));
+ expect(state.children[0].key).toBe('e');
+ expect(state.children[1].key).toBe('f');
+ expect(state.children[2].key).toBe('g');
+ expect(state.children.length).toBe(3);
+ expect(state.index).toBe(1);
+ expect(state.key).toBe('myStack');
+ });
+
+});
diff --git a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationTabsReducer-test.js b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationTabsReducer-test.js
new file mode 100644
index 00000000000000..a39aa76bfd2311
--- /dev/null
+++ b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationTabsReducer-test.js
@@ -0,0 +1,55 @@
+/**
+ * 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.
+ *
+ * @flow-broken
+ */
+'use strict';
+
+jest
+ .autoMockOff()
+ .mock('ErrorUtils');
+
+const NavigationTabsReducer = require('NavigationTabsReducer');
+
+const {
+ JumpToAction,
+} = NavigationTabsReducer;
+
+describe('NavigationTabsReducer', () => {
+
+ it('handles JumpTo with index', () => {
+ let reducer = NavigationTabsReducer({
+ tabReducers: [
+ (tabState, action) => tabState || 'a',
+ (tabState, action) => tabState || 'b',
+ (tabState, action) => tabState || 'c',
+ ],
+ initialIndex: 1,
+ });
+
+ let navState = reducer();
+
+ expect(navState.children[0]).toBe('a');
+ expect(navState.children[1]).toBe('b');
+ expect(navState.children[2]).toBe('c');
+ expect(navState.children.length).toBe(3);
+ expect(navState.index).toBe(1);
+
+ navState = reducer(
+ navState,
+ JumpToAction(2)
+ );
+
+ expect(navState.children[0]).toEqual('a');
+ expect(navState.children[1]).toEqual('b');
+ expect(navState.children[2]).toEqual('c');
+ expect(navState.children.length).toBe(3);
+ expect(navState.index).toBe(2);
+ });
+
+});
diff --git a/Libraries/NavigationExperimental/__tests__/NavigationState-test.js b/Libraries/NavigationExperimental/__tests__/NavigationState-test.js
new file mode 100644
index 00000000000000..b28aaaca20dc78
--- /dev/null
+++ b/Libraries/NavigationExperimental/__tests__/NavigationState-test.js
@@ -0,0 +1,57 @@
+/**
+ * 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.
+ *
+ * @flow-broken
+ */
+'use strict';
+
+jest
+ .autoMockOff()
+ .mock('ErrorUtils');
+
+var NavigationState = require('NavigationState');
+
+var VALID_PARENT_STATES = [
+ {children: ['a','b'], index: 0},
+ {children: [{key: 'a'},{key: 'b', foo: 123}], index: 1},
+ {children: [{key: 'a'},{key: 'b'}], index: 0},
+ {children: [{key: 'a'},{key: 'b'}], index: 2},
+];
+var INVALID_PARENT_STATES = [
+ 'foo',
+ {},
+ {children: [{key: 'a'}], index: 4},
+ {children: [{key: 'a'}], index: -1},
+ {children: [{key: 'a'}]},
+ {children: {key: 'foo'}},
+ 12,
+ null,
+ undefined,
+ [],
+];
+
+describe('NavigationState', () => {
+
+ it('identifies parents correctly with getParent', () => {
+ for (var i = 0; i <= VALID_PARENT_STATES.length; i++) {
+ var navState = VALID_PARENT_STATES[0];
+ expect(NavigationState.getParent(navState)).toBe(navState);
+ }
+ for (var i = 0; i <= INVALID_PARENT_STATES.length; i++) {
+ var navState = INVALID_PARENT_STATES[0];
+ expect(NavigationState.getParent(navState)).toBe(null);
+ }
+ });
+
+ it('can get children', () => {
+ var fooState = {key: 'foo'};
+ var navState = {children: [{key: 'foobar'}, fooState], index: 0};
+ expect(NavigationState.get(navState, 'foo')).toBe(fooState);
+ expect(NavigationState.get(navState, 'missing')).toBe(null);
+ });
+});
diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js
index 79a4ec0d5a8a59..e8eca3c72a5ad9 100644
--- a/Libraries/react-native/react-native.js
+++ b/Libraries/react-native/react-native.js
@@ -76,6 +76,7 @@ var ReactNative = {
get LayoutAnimation() { return require('LayoutAnimation'); },
get Linking() { return require('Linking'); },
get LinkingIOS() { return require('LinkingIOS'); },
+ get NavigationExperimental() { return require('NavigationExperimental'); },
get NetInfo() { return require('NetInfo'); },
get PanResponder() { return require('PanResponder'); },
get PixelRatio() { return require('PixelRatio'); },