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'); },