diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 0000000..c693a48 --- /dev/null +++ b/.flowconfig @@ -0,0 +1,46 @@ +[ignore] +; We fork some components by platform +.*/*[.]android.js + +; Ignore "BUCK" generated dirs +/\.buckd/ + +; Ignore unexpected extra "@providesModule" +.*/node_modules/.*/node_modules/fbjs/.* + +; Ignore duplicate module providers +; For RN Apps installed via npm, "Libraries" folder is inside +; "node_modules/react-native" but in the source repo it is in the root +.*/Libraries/react-native/React.js +.*/Libraries/react-native/ReactNative.js + +[include] + +[libs] +node_modules/react-native/Libraries/react-native/react-native-interface.js +node_modules/react-native/flow +flow/ + +[options] +emoji=true + +module.system=haste + +experimental.strict_type_args=true + +munge_underscores=true + +module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' + +suppress_type=$FlowIssue +suppress_type=$FlowFixMe +suppress_type=$FixMe + +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-8]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-8]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy + +unsafe.enable_getters_and_setters=true + +[version] +^0.38.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md index 6730681..d017fab 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,24 @@ Latest documentation is available here: http://facebook.github.io/react-native/r - Navigator.props.sceneStyle must be a plain object, not a stylesheet! -(this breaking change is needed to avoid calling React Native's private APIs) \ No newline at end of file +(this breaking change is needed to avoid calling React Native's private APIs) + +### Usage + +Simply replace Navigator imports with the one from this package. + +```js +import { Navigator } from 'react-native-deprecated-custom-components'; +``` + +## NavigationExperimental + +The NavigationExperimental module will behave identically as the one in React Native 0.43. + +### Usage + +Simply replace NavigationExperimental imports with the one from this package. + +```js +import { NavigationExperimental } from 'react-native-deprecated-custom-components'; +``` diff --git a/package.json b/package.json index 0e78c9e..74e5095 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,24 @@ "type": "git", "url": "git@github.com:facebookarchive/react-native-custom-components.git" }, + "scripts": { + "flow": "flow" + }, "main": "src/CustomComponents.js", "dependencies": { + "clamp": "^1.0.1", "fbjs": "~0.8.9", "immutable": "~3.7.6", "react-timer-mixin": "^0.13.2", "rebound": "^0.0.13" }, "peerDependencies": { + "react": "*", "react-native": "*" + }, + "devDependencies": { + "flow-bin": "^0.38.0", + "react": "~15.4.1", + "react-native": "^0.42.3" } -} \ No newline at end of file +} diff --git a/src/CustomComponents.js b/src/CustomComponents.js index b8ef6b1..85495bb 100644 --- a/src/CustomComponents.js +++ b/src/CustomComponents.js @@ -22,11 +22,14 @@ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * + * @flow */ +const NavigationExperimental = require('./NavigationExperimental'); const Navigator = require('./Navigator'); module.exports = { + NavigationExperimental, Navigator, }; diff --git a/src/NavigationExperimental/NavigationAbstractPanResponder.js b/src/NavigationExperimental/NavigationAbstractPanResponder.js new file mode 100644 index 0000000..bb78a1d --- /dev/null +++ b/src/NavigationExperimental/NavigationAbstractPanResponder.js @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2013-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 + */ +'use strict'; + +const {PanResponder} = require('react-native'); + +const invariant = require('fbjs/lib/invariant'); + +import type { + NavigationPanPanHandlers, +} from './NavigationTypeDefinition'; + +const EmptyPanHandlers = { + onMoveShouldSetPanResponder: null, + onPanResponderGrant: null, + onPanResponderMove: null, + onPanResponderRelease: null, + onPanResponderTerminate: null, +}; + +/** + * Abstract class that defines the common interface of PanResponder that handles + * the gesture actions. + */ +class NavigationAbstractPanResponder { + + panHandlers: NavigationPanPanHandlers; + + constructor() { + const config = {}; + Object.keys(EmptyPanHandlers).forEach(name => { + const fn: any = (this: any)[name]; + + invariant( + typeof fn === 'function', + 'subclass of `NavigationAbstractPanResponder` must implement method %s', + name + ); + + config[name] = fn.bind(this); + }, this); + + this.panHandlers = PanResponder.create(config).panHandlers; + } +} + +module.exports = NavigationAbstractPanResponder; diff --git a/src/clamp.js b/src/NavigationExperimental/NavigationAnimatedValueSubscription.js similarity index 76% rename from src/clamp.js rename to src/NavigationExperimental/NavigationAnimatedValueSubscription.js index cce0a31..4d176e0 100644 --- a/src/clamp.js +++ b/src/NavigationExperimental/NavigationAnimatedValueSubscription.js @@ -22,24 +22,26 @@ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * - * @typechecks + * @flow */ 'use strict'; -/** - * @param {number} value - * @param {number} min - * @param {number} max - * @return {number} - */ -function clamp(min, value, max) { - if (value < min) { - return min; +import type { + NavigationAnimatedValue +} from './NavigationTypeDefinition'; + +class NavigationAnimatedValueSubscription { + _value: NavigationAnimatedValue; + _token: string; + + constructor(value: NavigationAnimatedValue, callback: Function) { + this._value = value; + this._token = value.addListener(callback); } - if (value > max) { - return max; + + remove(): void { + this._value.removeListener(this._token); } - return value; } -module.exports = clamp; +module.exports = NavigationAnimatedValueSubscription; diff --git a/src/NavigationExperimental/NavigationCard.js b/src/NavigationExperimental/NavigationCard.js new file mode 100644 index 0000000..fccdff6 --- /dev/null +++ b/src/NavigationExperimental/NavigationCard.js @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2013-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. + * + * 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. + * + * @flow + */ +'use strict'; + +const {Animated, StyleSheet} = require('react-native'); +const NavigationCardStackPanResponder = require('./NavigationCardStackPanResponder'); +const NavigationCardStackStyleInterpolator = require('./NavigationCardStackStyleInterpolator'); +const NavigationPagerPanResponder = require('./NavigationPagerPanResponder'); +const NavigationPagerStyleInterpolator = require('./NavigationPagerStyleInterpolator'); +const NavigationPointerEventsContainer = require('./NavigationPointerEventsContainer'); +const NavigationPropTypes = require('./NavigationPropTypes'); +const React = require('react'); + +import type { + NavigationPanPanHandlers, + NavigationSceneRenderer, + NavigationSceneRendererProps, +} from './NavigationTypeDefinition'; + +type Props = NavigationSceneRendererProps & { + onComponentRef: (ref: any) => void, + onNavigateBack: ?Function, + panHandlers: ?NavigationPanPanHandlers, + pointerEvents: string, + renderScene: NavigationSceneRenderer, + style: any, +}; + +const {PropTypes} = React; + +/** + * Component that renders the scene as card for the . + */ +class NavigationCard extends React.Component { + props: Props; + + static propTypes = { + ...NavigationPropTypes.SceneRendererProps, + onComponentRef: PropTypes.func.isRequired, + onNavigateBack: PropTypes.func, + panHandlers: NavigationPropTypes.panHandlers, + pointerEvents: PropTypes.string.isRequired, + renderScene: PropTypes.func.isRequired, + style: PropTypes.any, + }; + + render(): React.Element { + const { + panHandlers, + pointerEvents, + renderScene, + style, + ...props /* NavigationSceneRendererProps */ + } = this.props; + + const viewStyle = style === undefined ? + NavigationCardStackStyleInterpolator.forHorizontal(props) : + style; + + const viewPanHandlers = panHandlers === undefined ? + NavigationCardStackPanResponder.forHorizontal({ + ...props, + onNavigateBack: this.props.onNavigateBack, + }) : + panHandlers; + + return ( + + {renderScene(props)} + + ); + } +} + +const styles = StyleSheet.create({ + main: { + backgroundColor: '#E9E9EF', + bottom: 0, + left: 0, + position: 'absolute', + right: 0, + shadowColor: 'black', + shadowOffset: {width: 0, height: 0}, + shadowOpacity: 0.4, + shadowRadius: 10, + top: 0, + }, +}); + +NavigationCard = NavigationPointerEventsContainer.create(NavigationCard); + +NavigationCard.CardStackPanResponder = NavigationCardStackPanResponder; +NavigationCard.CardStackStyleInterpolator = NavigationCardStackStyleInterpolator; +NavigationCard.PagerPanResponder = NavigationPagerPanResponder; +NavigationCard.PagerStyleInterpolator = NavigationPagerStyleInterpolator; + +module.exports = NavigationCard; diff --git a/src/NavigationExperimental/NavigationCardStack.js b/src/NavigationExperimental/NavigationCardStack.js new file mode 100644 index 0000000..4a05fe3 --- /dev/null +++ b/src/NavigationExperimental/NavigationCardStack.js @@ -0,0 +1,327 @@ +/** + * Copyright (c) 2013-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. + * + * 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. + * + * @flow + */ +'use strict'; + +const NavigationCard = require('./NavigationCard'); +const NavigationCardStackPanResponder = require('./NavigationCardStackPanResponder'); +const NavigationCardStackStyleInterpolator = require('./NavigationCardStackStyleInterpolator'); +const NavigationPropTypes = require('./NavigationPropTypes'); +const NavigationTransitioner = require('./NavigationTransitioner'); +const React = require('react'); +const {NativeModules, StyleSheet, View} = require('react-native'); + +const {NativeAnimatedModule} = NativeModules; +const {PropTypes} = React; +const {Directions} = NavigationCardStackPanResponder; + +import type { + NavigationState, + NavigationSceneRenderer, + NavigationSceneRendererProps, + NavigationTransitionProps, + NavigationStyleInterpolator, +} from './NavigationTypeDefinition'; + +import type { + NavigationGestureDirection, +} from 'NavigationCardStackPanResponder'; + +type Props = { + direction: NavigationGestureDirection, + navigationState: NavigationState, + onNavigateBack?: Function, + renderHeader: ?NavigationSceneRenderer, + renderScene: NavigationSceneRenderer, + cardStyle?: any, + style: any, + gestureResponseDistance?: ?number, + enableGestures: ?boolean, + cardStyleInterpolator?: ?NavigationStyleInterpolator, + scenesStyle?: any, +}; + +type DefaultProps = { + direction: NavigationGestureDirection, + enableGestures: boolean, +}; + +/** + * A controlled navigation view that renders a stack of cards. + * + * ```html + * +------------+ + * +-| Header | + * +-+ |------------| + * | | | | + * | | | Focused | + * | | | Card | + * | | | | + * +-+ | | + * +-+ | + * +------------+ + * ``` + * + * ## Example + * + * ```js + * + * class App extends React.Component { + * constructor(props, context) { + * this.state = { + * navigation: { + * index: 0, + * routes: [ + * {key: 'page 1'}, + * }, + * }, + * }; + * } + * + * render() { + * return ( + * + * ); + * } + * + * _renderScene: (props) => { + * return ( + * + * {props.scene.route.key} + * + * ); + * }; + * ``` + */ +class NavigationCardStack extends React.Component { + _render : NavigationSceneRenderer; + _renderScene : NavigationSceneRenderer; + + static propTypes = { + /** + * Custom style applied to the card. + */ + cardStyle: PropTypes.any, + + /** + * Direction of the cards movement. Value could be `horizontal` or + * `vertical`. Default value is `horizontal`. + */ + direction: PropTypes.oneOf([Directions.HORIZONTAL, Directions.VERTICAL]), + + /** + * The distance from the edge of the card which gesture response can start + * for. Default value is `30`. + */ + gestureResponseDistance: PropTypes.number, + + /** + * An interpolator function that is passed an object parameter of type + * NavigationSceneRendererProps and should return a style object to apply to + * the transitioning navigation card. + * + * Default interpolator transitions translateX, scale, and opacity. + */ + cardStyleInterpolator: PropTypes.func, + + /** + * Enable gestures. Default value is true. + * + * When disabled, transition animations will be handled natively, which + * improves performance of the animation. In future iterations, gestures + * will also work with native-driven animation. + */ + enableGestures: PropTypes.bool, + + /** + * The controlled navigation state. Typically, the navigation state + * look like this: + * + * ```js + * const navigationState = { + * index: 0, // the index of the selected route. + * routes: [ // A list of routes. + * {key: 'page 1'}, // The 1st route. + * {key: 'page 2'}, // The second route. + * ], + * }; + * ``` + */ + navigationState: NavigationPropTypes.navigationState.isRequired, + + /** + * Callback that is called when the "back" action is performed. + * This happens when the back button is pressed or the back gesture is + * performed. + */ + onNavigateBack: PropTypes.func, + + /** + * Function that renders the header. + */ + renderHeader: PropTypes.func, + + /** + * Function that renders the a scene for a route. + */ + renderScene: PropTypes.func.isRequired, + + /** + * Custom style applied to the cards stack. + */ + style: View.propTypes.style, + + /** + * Custom style applied to the scenes stack. + */ + scenesStyle: View.propTypes.style, + }; + + static defaultProps: DefaultProps = { + direction: Directions.HORIZONTAL, + enableGestures: true, + }; + + constructor(props: Props, context: any) { + super(props, context); + } + + componentWillMount(): void { + this._render = this._render.bind(this); + this._renderScene = this._renderScene.bind(this); + } + + render(): React.Element { + return ( + + ); + } + + _configureTransition = () => { + const isVertical = this.props.direction === 'vertical'; + const animationConfig = {}; + if ( + !!NativeAnimatedModule + + // Gestures do not work with the current iteration of native animation + // driving. When gestures are disabled, we can drive natively. + && !this.props.enableGestures + + // Native animation support also depends on the transforms used: + && NavigationCardStackStyleInterpolator.canUseNativeDriver(isVertical) + ) { + animationConfig.useNativeDriver = true; + } + return animationConfig; + } + + _render(props: NavigationTransitionProps): React.Element { + const { + renderHeader, + } = this.props; + + const header = renderHeader ? {renderHeader(props)} : null; + + const scenes = props.scenes.map( + scene => this._renderScene({ + ...props, + scene, + }) + ); + + return ( + + + {scenes} + + {header} + + ); + } + + _renderScene(props: NavigationSceneRendererProps): React.Element { + const isVertical = this.props.direction === 'vertical'; + + const interpolator = this.props.cardStyleInterpolator || (isVertical ? + NavigationCardStackStyleInterpolator.forVertical : + NavigationCardStackStyleInterpolator.forHorizontal); + + const style = interpolator(props); + + let panHandlers = null; + + if (this.props.enableGestures) { + const panHandlersProps = { + ...props, + onNavigateBack: this.props.onNavigateBack, + gestureResponseDistance: this.props.gestureResponseDistance, + }; + panHandlers = isVertical ? + NavigationCardStackPanResponder.forVertical(panHandlersProps) : + NavigationCardStackPanResponder.forHorizontal(panHandlersProps); + } + + return ( + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + // Header is physically rendered after scenes so that Header won't be + // covered by the shadows of the scenes. + // That said, we'd have use `flexDirection: 'column-reverse'` to move + // Header above the scenes. + flexDirection: 'column-reverse', + }, + scenes: { + flex: 1, + }, +}); + +module.exports = NavigationCardStack; diff --git a/src/NavigationExperimental/NavigationCardStackPanResponder.js b/src/NavigationExperimental/NavigationCardStackPanResponder.js new file mode 100644 index 0000000..8a559d0 --- /dev/null +++ b/src/NavigationExperimental/NavigationCardStackPanResponder.js @@ -0,0 +1,269 @@ +/** + * Copyright (c) 2013-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 + */ +'use strict'; + +const {Animated, I18nManager} = require('react-native'); +const NavigationAbstractPanResponder = require('./NavigationAbstractPanResponder'); + +const clamp = require('clamp'); + +import type { + NavigationPanPanHandlers, + NavigationSceneRendererProps, +} from './NavigationTypeDefinition'; + +const emptyFunction = () => {}; + +/** + * The duration of the card animation in milliseconds. + */ +const ANIMATION_DURATION = 250; + +/** + * The threshold to invoke the `onNavigateBack` action. + * For instance, `1 / 3` means that moving greater than 1 / 3 of the width of + * the view will navigate. + */ +const POSITION_THRESHOLD = 1 / 3; + +/** + * The threshold (in pixels) to start the gesture action. + */ +const RESPOND_THRESHOLD = 15; + +/** + * The threshold (in pixels) to finish the gesture action. + */ +const DISTANCE_THRESHOLD = 100; + +/** + * Primitive gesture directions. + */ +const Directions = { + 'HORIZONTAL': 'horizontal', + 'VERTICAL': 'vertical', +}; + +export type NavigationGestureDirection = 'horizontal' | 'vertical'; + +type Props = NavigationSceneRendererProps & { + onNavigateBack: ?Function, + /** + * The distance from the edge of the navigator which gesture response can start for. + **/ + gestureResponseDistance: ?number, +}; + +/** + * Pan responder that handles gesture for a card in the cards stack. + * + * +------------+ + * +-+ | + * +-+ | | + * | | | | + * | | | Focused | + * | | | Card | + * | | | | + * +-+ | | + * +-+ | + * +------------+ + */ +class NavigationCardStackPanResponder extends NavigationAbstractPanResponder { + + _isResponding: boolean; + _isVertical: boolean; + _props: Props; + _startValue: number; + + constructor( + direction: NavigationGestureDirection, + props: Props, + ) { + super(); + this._isResponding = false; + this._isVertical = direction === Directions.VERTICAL; + this._props = props; + this._startValue = 0; + + // Hack to make this work with native driven animations. We add a single listener + // so the JS value of the following animated values gets updated. We rely on + // some Animated private APIs and not doing so would require using a bunch of + // value listeners but we'd have to remove them to not leak and I'm not sure + // when we'd do that with the current structure we have. `stopAnimation` callback + // is also broken with native animated values that have no listeners so if we + // want to remove this we have to fix this too. + this._addNativeListener(this._props.layout.width); + this._addNativeListener(this._props.layout.height); + this._addNativeListener(this._props.position); + } + + onMoveShouldSetPanResponder(event: any, gesture: any): boolean { + const props = this._props; + + if (props.navigationState.index !== props.scene.index) { + return false; + } + + const layout = props.layout; + const isVertical = this._isVertical; + const index = props.navigationState.index; + const currentDragDistance = gesture[isVertical ? 'dy' : 'dx']; + const currentDragPosition = gesture[isVertical ? 'moveY' : 'moveX']; + const maxDragDistance = isVertical ? + layout.height.__getValue() : + layout.width.__getValue(); + + const positionMax = isVertical ? + props.gestureResponseDistance : + /** + * For horizontal scroll views, a distance of 30 from the left of the screen is the + * standard maximum position to start touch responsiveness. + */ + props.gestureResponseDistance || 30; + + if (positionMax != null && currentDragPosition > positionMax) { + return false; + } + + return ( + Math.abs(currentDragDistance) > RESPOND_THRESHOLD && + maxDragDistance > 0 && + index > 0 + ); + } + + onPanResponderGrant(): void { + this._isResponding = false; + this._props.position.stopAnimation((value: number) => { + this._isResponding = true; + this._startValue = value; + }); + } + + onPanResponderMove(event: any, gesture: any): void { + if (!this._isResponding) { + return; + } + + const props = this._props; + const layout = props.layout; + const isVertical = this._isVertical; + const axis = isVertical ? 'dy' : 'dx'; + const index = props.navigationState.index; + const distance = isVertical ? + layout.height.__getValue() : + layout.width.__getValue(); + const currentValue = I18nManager.isRTL && axis === 'dx' ? + this._startValue + (gesture[axis] / distance) : + this._startValue - (gesture[axis] / distance); + + const value = clamp( + index - 1, + currentValue, + index + ); + + props.position.setValue(value); + } + + onPanResponderRelease(event: any, gesture: any): void { + if (!this._isResponding) { + return; + } + + this._isResponding = false; + + const props = this._props; + const isVertical = this._isVertical; + const axis = isVertical ? 'dy' : 'dx'; + const index = props.navigationState.index; + const distance = I18nManager.isRTL && axis === 'dx' ? + -gesture[axis] : + gesture[axis]; + + props.position.stopAnimation((value: number) => { + this._reset(); + + if (!props.onNavigateBack) { + return; + } + + if ( + distance > DISTANCE_THRESHOLD || + value <= index - POSITION_THRESHOLD + ) { + props.onNavigateBack(); + } + }); + } + + onPanResponderTerminate(): void { + this._isResponding = false; + this._reset(); + } + + _reset(): void { + const props = this._props; + Animated.timing( + props.position, + { + toValue: props.navigationState.index, + duration: ANIMATION_DURATION, + useNativeDriver: props.position.__isNative, + } + ).start(); + } + + _addNativeListener(animatedValue) { + if (!animatedValue.__isNative) { + return; + } + + if (Object.keys(animatedValue._listeners).length === 0) { + animatedValue.addListener(emptyFunction); + } + } +} + +function createPanHandlers( + direction: NavigationGestureDirection, + props: Props, +): NavigationPanPanHandlers { + const responder = new NavigationCardStackPanResponder(direction, props); + return responder.panHandlers; +} + +function forHorizontal( + props: Props, +): NavigationPanPanHandlers { + return createPanHandlers(Directions.HORIZONTAL, props); +} + +function forVertical( + props: Props, +): NavigationPanPanHandlers { + return createPanHandlers(Directions.VERTICAL, props); +} + +module.exports = { + // constants + ANIMATION_DURATION, + DISTANCE_THRESHOLD, + POSITION_THRESHOLD, + RESPOND_THRESHOLD, + + // enums + Directions, + + // methods. + forHorizontal, + forVertical, +}; diff --git a/src/NavigationExperimental/NavigationCardStackStyleInterpolator.js b/src/NavigationExperimental/NavigationCardStackStyleInterpolator.js new file mode 100644 index 0000000..3710b01 --- /dev/null +++ b/src/NavigationExperimental/NavigationCardStackStyleInterpolator.js @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2013-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. + * + * 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. + * + * @flow + */ +'use strict'; + +const {I18nManager} = require('react-native'); + +import type { + NavigationSceneRendererProps, +} from './NavigationTypeDefinition'; + +/** + * Utility that builds the style for the card in the cards stack. + * + * +------------+ + * +-+ | + * +-+ | | + * | | | | + * | | | Focused | + * | | | Card | + * | | | | + * +-+ | | + * +-+ | + * +------------+ + */ + +/** + * Render the initial style when the initial layout isn't measured yet. + */ +function forInitial(props: NavigationSceneRendererProps): Object { + const { + navigationState, + scene, + } = props; + + const focused = navigationState.index === scene.index; + const opacity = focused ? 1 : 0; + // If not focused, move the scene to the far away. + const translate = focused ? 0 : 1000000; + return { + opacity, + transform: [ + { translateX: translate }, + { translateY: translate }, + ], + }; +} + +function forHorizontal(props: NavigationSceneRendererProps): Object { + const { + layout, + position, + scene, + } = props; + + if (!layout.isMeasured) { + return forInitial(props); + } + + const index = scene.index; + const inputRange = [index - 1, index, index + 0.99, index + 1]; + const width = layout.initWidth; + const outputRange = I18nManager.isRTL ? + ([-width, 0, 10, 10]: Array) : + ([width, 0, -10, -10]: Array); + + + const opacity = position.interpolate({ + inputRange, + outputRange: ([1, 1, 0.3, 0]: Array), + }); + + const scale = position.interpolate({ + inputRange, + outputRange: ([1, 1, 0.95, 0.95]: Array), + }); + + const translateY = 0; + const translateX = position.interpolate({ + inputRange, + outputRange, + }); + + return { + opacity, + transform: [ + { scale }, + { translateX }, + { translateY }, + ], + }; +} + +function forVertical(props: NavigationSceneRendererProps): Object { + const { + layout, + position, + scene, + } = props; + + if (!layout.isMeasured) { + return forInitial(props); + } + + const index = scene.index; + const inputRange = [index - 1, index, index + 0.99, index + 1]; + const height = layout.initHeight; + + const opacity = position.interpolate({ + inputRange, + outputRange: ([1, 1, 0.3, 0]: Array), + }); + + const scale = position.interpolate({ + inputRange, + outputRange: ([1, 1, 0.95, 0.95]: Array), + }); + + const translateX = 0; + const translateY = position.interpolate({ + inputRange, + outputRange: ([height, 0, -10, -10]: Array), + }); + + return { + opacity, + transform: [ + { scale }, + { translateX }, + { translateY }, + ], + }; +} + +function canUseNativeDriver(isVertical: boolean): boolean { + // The native driver can be enabled for this interpolator because the scale, + // translateX, and translateY transforms are supported with the native + // animation driver. + + return true; +} + +module.exports = { + forHorizontal, + forVertical, + canUseNativeDriver, +}; diff --git a/src/NavigationExperimental/NavigationHeader.js b/src/NavigationExperimental/NavigationHeader.js new file mode 100644 index 0000000..d692a46 --- /dev/null +++ b/src/NavigationExperimental/NavigationHeader.js @@ -0,0 +1,281 @@ +/** + * Copyright (c) 2013-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. + * + * 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. + * + * @flow + */ +'use strict'; + +const NavigationHeaderBackButton = require('./NavigationHeaderBackButton'); +const NavigationHeaderStyleInterpolator = require('./NavigationHeaderStyleInterpolator'); +const NavigationHeaderTitle = require('./NavigationHeaderTitle'); +const NavigationPropTypes = require('./NavigationPropTypes'); +const React = require('react'); +const ReactNative = require('react-native'); + +const { + Animated, + Platform, + StyleSheet, + TVEventHandler, + View, +} = ReactNative; + +import type { + NavigationSceneRendererProps, + NavigationStyleInterpolator, +} from './NavigationTypeDefinition'; + +type SubViewProps = NavigationSceneRendererProps & { + onNavigateBack: ?Function, +}; + +type SubViewRenderer = (subViewProps: SubViewProps) => ?React.Element; + +type DefaultProps = { + renderLeftComponent: SubViewRenderer, + renderRightComponent: SubViewRenderer, + renderTitleComponent: SubViewRenderer, + statusBarHeight: number | Animated.Value, +}; + +type Props = NavigationSceneRendererProps & { + onNavigateBack: ?Function, + renderLeftComponent: SubViewRenderer, + renderRightComponent: SubViewRenderer, + renderTitleComponent: SubViewRenderer, + style?: any, + viewProps?: any, + statusBarHeight: number | Animated.Value, +}; + +type SubViewName = 'left' | 'title' | 'right'; + +const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56; +const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0; +const {PropTypes} = React; + +class NavigationHeader extends React.PureComponent { + props: Props; + + static defaultProps = { + + renderTitleComponent: (props: SubViewProps) => { + const title = String(props.scene.route.title || ''); + return {title}; + }, + + renderLeftComponent: (props: SubViewProps) => { + if (props.scene.index === 0 || !props.onNavigateBack) { + return null; + } + return ( + + ); + }, + + renderRightComponent: (props: SubViewProps) => { + return null; + }, + + statusBarHeight: STATUSBAR_HEIGHT, + }; + + static propTypes = { + ...NavigationPropTypes.SceneRendererProps, + onNavigateBack: PropTypes.func, + renderLeftComponent: PropTypes.func, + renderRightComponent: PropTypes.func, + renderTitleComponent: PropTypes.func, + style: View.propTypes.style, + statusBarHeight: PropTypes.number, + viewProps: PropTypes.shape(View.propTypes), + }; + + _tvEventHandler: TVEventHandler; + + componentDidMount(): void { + this._tvEventHandler = new TVEventHandler(); + this._tvEventHandler.enable(this, function(cmp, evt) { + if (evt && evt.eventType === 'menu') { + cmp.props.onNavigateBack && cmp.props.onNavigateBack(); + } + }); + } + + componentWillUnmount(): void { + if (this._tvEventHandler) { + this._tvEventHandler.disable(); + delete this._tvEventHandler; + } + } + + render(): React.Element { + const { scenes, style, viewProps } = this.props; + + const scenesProps = scenes.map(scene => { + const props = NavigationPropTypes.extractSceneRendererProps(this.props); + props.scene = scene; + return props; + }); + + const barHeight = (this.props.statusBarHeight instanceof Animated.Value) + ? Animated.add(this.props.statusBarHeight, new Animated.Value(APPBAR_HEIGHT)) + : APPBAR_HEIGHT + this.props.statusBarHeight; + + return ( + + {scenesProps.map(this._renderLeft, this)} + {scenesProps.map(this._renderTitle, this)} + {scenesProps.map(this._renderRight, this)} + + ); + } + + _renderLeft = (props: NavigationSceneRendererProps): ?React.Element => { + return this._renderSubView( + props, + 'left', + this.props.renderLeftComponent, + NavigationHeaderStyleInterpolator.forLeft, + ); + }; + + _renderTitle = (props: NavigationSceneRendererProps): ?React.Element => { + return this._renderSubView( + props, + 'title', + this.props.renderTitleComponent, + NavigationHeaderStyleInterpolator.forCenter, + ); + }; + + _renderRight = (props: NavigationSceneRendererProps): ?React.Element => { + return this._renderSubView( + props, + 'right', + this.props.renderRightComponent, + NavigationHeaderStyleInterpolator.forRight, + ); + }; + + _renderSubView( + props: NavigationSceneRendererProps, + name: SubViewName, + renderer: SubViewRenderer, + styleInterpolator: NavigationStyleInterpolator, + ): ?React.Element { + const { + scene, + navigationState, + } = props; + + const { + index, + isStale, + key, + } = scene; + + const offset = navigationState.index - index; + + if (Math.abs(offset) > 2) { + // Scene is far away from the active scene. Hides it to avoid unnecessary + // rendering. + return null; + } + + const subViewProps = {...props, onNavigateBack: this.props.onNavigateBack}; + const subView = renderer(subViewProps); + if (subView === null) { + return null; + } + + const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none'; + return ( + + {subView} + + ); + } + + static HEIGHT = APPBAR_HEIGHT + STATUSBAR_HEIGHT; + static Title = NavigationHeaderTitle; + static BackButton = NavigationHeaderBackButton; + +} + +const styles = StyleSheet.create({ + appbar: { + alignItems: 'center', + backgroundColor: Platform.OS === 'ios' ? '#EFEFF2' : '#FFF', + borderBottomColor: 'rgba(0, 0, 0, .15)', + borderBottomWidth: Platform.OS === 'ios' ? StyleSheet.hairlineWidth : 0, + elevation: 4, + flexDirection: 'row', + justifyContent: 'flex-start', + }, + + title: { + bottom: 0, + left: APPBAR_HEIGHT, + position: 'absolute', + right: APPBAR_HEIGHT, + top: 0, + }, + + left: { + bottom: 0, + left: 0, + position: 'absolute', + top: 0, + }, + + right: { + bottom: 0, + position: 'absolute', + right: 0, + top: 0, + }, +}); + +module.exports = NavigationHeader; diff --git a/src/NavigationExperimental/NavigationHeaderBackButton.js b/src/NavigationExperimental/NavigationHeaderBackButton.js new file mode 100644 index 0000000..3359f31 --- /dev/null +++ b/src/NavigationExperimental/NavigationHeaderBackButton.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2013-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. + * + * 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. + * + * @flow +*/ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); + +const { + I18nManager, + Image, + Platform, + StyleSheet, + TouchableOpacity, +} = ReactNative; + +type Props = { + imageStyle?: any, + onPress: Function, + style?: any, +}; + +const NavigationHeaderBackButton = (props: Props) => ( + + + +); + +NavigationHeaderBackButton.propTypes = { + onPress: React.PropTypes.func.isRequired +}; + +const styles = StyleSheet.create({ + buttonContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + button: { + height: 24, + width: 24, + margin: Platform.OS === 'ios' ? 10 : 16, + resizeMode: 'contain', + transform: [{scaleX: I18nManager.isRTL ? -1 : 1}], + } +}); + +module.exports = NavigationHeaderBackButton; diff --git a/src/NavigationExperimental/NavigationHeaderStyleInterpolator.js b/src/NavigationExperimental/NavigationHeaderStyleInterpolator.js new file mode 100644 index 0000000..232c976 --- /dev/null +++ b/src/NavigationExperimental/NavigationHeaderStyleInterpolator.js @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2013-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. + * + * 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. + * + * @flow + */ +'use strict'; + +const {I18nManager} = require('react-native'); + +import type { + NavigationSceneRendererProps, +} from './NavigationTypeDefinition'; + +/** + * Utility that builds the style for the navigation header. + * + * +-------------+-------------+-------------+ + * | | | | + * | Left | Title | Right | + * | Component | Component | Component | + * | | | | + * +-------------+-------------+-------------+ + */ + +function forLeft(props: NavigationSceneRendererProps): Object { + const {position, scene} = props; + const {index} = scene; + return { + opacity: position.interpolate({ + inputRange: [ index - 1, index, index + 1 ], + outputRange: ([ 0, 1, 0 ]: Array), + }), + }; +} + +function forCenter(props: NavigationSceneRendererProps): Object { + const {position, scene} = props; + const {index} = scene; + return { + opacity:position.interpolate({ + inputRange: [ index - 1, index, index + 1 ], + outputRange: ([ 0, 1, 0 ]: Array), + }), + transform: [ + { + translateX: position.interpolate({ + inputRange: [ index - 1, index + 1 ], + outputRange: I18nManager.isRTL ? + ([ -200, 200 ]: Array) : + ([ 200, -200 ]: Array), + }), + } + ], + }; +} + +function forRight(props: NavigationSceneRendererProps): Object { + const {position, scene} = props; + const {index} = scene; + return { + opacity: position.interpolate({ + inputRange: [ index - 1, index, index + 1 ], + outputRange: ([ 0, 1, 0 ]: Array), + }), + }; +} + +module.exports = { + forCenter, + forLeft, + forRight, +}; diff --git a/src/NavigationExperimental/NavigationHeaderTitle.js b/src/NavigationExperimental/NavigationHeaderTitle.js new file mode 100644 index 0000000..8bd37b5 --- /dev/null +++ b/src/NavigationExperimental/NavigationHeaderTitle.js @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2013-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. + * + * 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. + * + * @flow + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); + +const { + Platform, + StyleSheet, + View, + Text, +} = ReactNative; + +type Props = { + children?: React.Element, + style?: any, + textStyle?: any, + viewProps?: any, +} + +const NavigationHeaderTitle = ({ children, style, textStyle, viewProps }: Props) => ( + + {children} + +); + +const styles = StyleSheet.create({ + title: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + marginHorizontal: 16 + }, + + titleText: { + flex: 1, + fontSize: 18, + fontWeight: '500', + color: 'rgba(0, 0, 0, .9)', + textAlign: Platform.OS === 'ios' ? 'center' : 'left' + } +}); + +NavigationHeaderTitle.propTypes = { + children: React.PropTypes.node.isRequired, + style: View.propTypes.style, + textStyle: Text.propTypes.style +}; + +module.exports = NavigationHeaderTitle; diff --git a/src/NavigationExperimental/NavigationPagerPanResponder.js b/src/NavigationExperimental/NavigationPagerPanResponder.js new file mode 100644 index 0000000..3f5cd87 --- /dev/null +++ b/src/NavigationExperimental/NavigationPagerPanResponder.js @@ -0,0 +1,236 @@ +/** + * Copyright (c) 2013-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 + */ +'use strict'; + +const {Animated, I18nManager} = require('react-native'); +const NavigationAbstractPanResponder = require('./NavigationAbstractPanResponder'); +const NavigationCardStackPanResponder = require('./NavigationCardStackPanResponder'); + +const clamp = require('clamp'); + +import type { + NavigationPanPanHandlers, + NavigationSceneRendererProps, +} from './NavigationTypeDefinition'; + +import type { + NavigationGestureDirection, +} from './NavigationCardStackPanResponder'; + +type Props = NavigationSceneRendererProps & { + onNavigateBack: ?Function, + onNavigateForward: ?Function, +}; + +/** + * Primitive gesture directions. + */ +const { + ANIMATION_DURATION, + POSITION_THRESHOLD, + RESPOND_THRESHOLD, + Directions, +} = NavigationCardStackPanResponder; + +/** + * The threshold (in pixels) to finish the gesture action. + */ +const DISTANCE_THRESHOLD = 50; + +/** + * The threshold to trigger the gesture action. This determines the rate of the + * flick when the action will be triggered + */ +const VELOCITY_THRESHOLD = 1.5; + +/** + * Pan responder that handles gesture for a card in the cards list. + * + * +-------------+-------------+-------------+ + * | | | | + * | | | | + * | | | | + * | Next | Focused | Previous | + * | Card | Card | Card | + * | | | | + * | | | | + * | | | | + * +-------------+-------------+-------------+ + */ +class NavigationPagerPanResponder extends NavigationAbstractPanResponder { + + _isResponding: boolean; + _isVertical: boolean; + _props: Props; + _startValue: number; + + constructor( + direction: NavigationGestureDirection, + props: Props, + ) { + super(); + this._isResponding = false; + this._isVertical = direction === Directions.VERTICAL; + this._props = props; + this._startValue = 0; + } + + onMoveShouldSetPanResponder(event: any, gesture: any): boolean { + const props = this._props; + + if (props.navigationState.index !== props.scene.index) { + return false; + } + + const layout = props.layout; + const isVertical = this._isVertical; + const axis = isVertical ? 'dy' : 'dx'; + const index = props.navigationState.index; + const distance = isVertical ? + layout.height.__getValue() : + layout.width.__getValue(); + + return ( + Math.abs(gesture[axis]) > RESPOND_THRESHOLD && + distance > 0 && + index >= 0 + ); + } + + onPanResponderGrant(): void { + this._isResponding = false; + this._props.position.stopAnimation((value: number) => { + this._isResponding = true; + this._startValue = value; + }); + } + + onPanResponderMove(event: any, gesture: any): void { + if (!this._isResponding) { + return; + } + + const { + layout, + navigationState, + position, + scenes, + } = this._props; + + const isVertical = this._isVertical; + const axis = isVertical ? 'dy' : 'dx'; + const index = navigationState.index; + const distance = isVertical ? + layout.height.__getValue() : + layout.width.__getValue(); + const currentValue = I18nManager.isRTL && axis === 'dx' ? + this._startValue + (gesture[axis] / distance) : + this._startValue - (gesture[axis] / distance); + + const prevIndex = Math.max( + 0, + index - 1, + ); + + const nextIndex = Math.min( + index + 1, + scenes.length - 1, + ); + + const value = clamp( + prevIndex, + currentValue, + nextIndex, + ); + + position.setValue(value); + } + + onPanResponderRelease(event: any, gesture: any): void { + if (!this._isResponding) { + return; + } + + this._isResponding = false; + + const { + navigationState, + onNavigateBack, + onNavigateForward, + position, + } = this._props; + + const isVertical = this._isVertical; + const axis = isVertical ? 'dy' : 'dx'; + const velocityAxis = isVertical ? 'vy' : 'vx'; + const index = navigationState.index; + const distance = I18nManager.isRTL && axis === 'dx' ? + -gesture[axis] : + gesture[axis]; + const moveSpeed = I18nManager.isRTL && velocityAxis === 'vx' ? + -gesture[velocityAxis] : + gesture[velocityAxis]; + + position.stopAnimation((value: number) => { + this._reset(); + if ( + distance > DISTANCE_THRESHOLD || + value <= index - POSITION_THRESHOLD || + moveSpeed > VELOCITY_THRESHOLD + ) { + onNavigateBack && onNavigateBack(); + return; + } + + if ( + distance < -DISTANCE_THRESHOLD || + value >= index + POSITION_THRESHOLD || + moveSpeed < -VELOCITY_THRESHOLD + ) { + onNavigateForward && onNavigateForward(); + } + }); + } + + onPanResponderTerminate(): void { + this._isResponding = false; + this._reset(); + } + + _reset(): void { + const props = this._props; + Animated.timing( + props.position, + { + toValue: props.navigationState.index, + duration: ANIMATION_DURATION, + } + ).start(); + } +} + +function createPanHandlers( + direction: NavigationGestureDirection, + props: Props, +): NavigationPanPanHandlers { + const responder = new NavigationPagerPanResponder(direction, props); + return responder.panHandlers; +} + +function forHorizontal( + props: Props, +): NavigationPanPanHandlers { + return createPanHandlers(Directions.HORIZONTAL, props); +} + +module.exports = { + forHorizontal, +}; diff --git a/src/NavigationExperimental/NavigationPagerStyleInterpolator.js b/src/NavigationExperimental/NavigationPagerStyleInterpolator.js new file mode 100644 index 0000000..55bdedc --- /dev/null +++ b/src/NavigationExperimental/NavigationPagerStyleInterpolator.js @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2013-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. + * + * 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. + * + * @flow + */ +'use strict'; + +const {I18nManager} = require('react-native'); + +import type { + NavigationSceneRendererProps, +} from './NavigationTypeDefinition'; + +/** + * Utility that builds the style for the card in the cards list. + * + * +-------------+-------------+-------------+ + * | | | | + * | | | | + * | | | | + * | Next | Focused | Previous | + * | Card | Card | Card | + * | | | | + * | | | | + * | | | | + * +-------------+-------------+-------------+ + */ + +/** + * Render the initial style when the initial layout isn't measured yet. + */ +function forInitial(props: NavigationSceneRendererProps): Object { + const { + navigationState, + scene, + } = props; + + const focused = navigationState.index === scene.index; + const opacity = focused ? 1 : 0; + // If not focused, move the scene to the far away. + const dir = scene.index > navigationState.index ? 1 : -1; + const translate = focused ? 0 : (1000000 * dir); + return { + opacity, + transform: [ + { translateX: translate }, + { translateY: translate }, + ], + }; +} + +function forHorizontal(props: NavigationSceneRendererProps): Object { + const { + layout, + position, + scene, + } = props; + + if (!layout.isMeasured) { + return forInitial(props); + } + + const index = scene.index; + const inputRange = [index - 1, index, index + 1]; + const width = layout.initWidth; + const outputRange = I18nManager.isRTL ? + ([-width, 0, width]: Array) : + ([width, 0, -width]: Array); + + const translateX = position.interpolate({ + inputRange, + outputRange, + }); + + return { + opacity : 1, + shadowColor: 'transparent', + shadowRadius: 0, + transform: [ + { scale: 1 }, + { translateX }, + { translateY: 0 }, + ], + }; +} + +module.exports = { + forHorizontal, +}; diff --git a/src/NavigationExperimental/NavigationPointerEventsContainer.js b/src/NavigationExperimental/NavigationPointerEventsContainer.js new file mode 100644 index 0000000..022d028 --- /dev/null +++ b/src/NavigationExperimental/NavigationPointerEventsContainer.js @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2013-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. + * + * 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. + * + * @flow + */ +'use strict'; + +const React = require('react'); +const NavigationAnimatedValueSubscription = require('./NavigationAnimatedValueSubscription'); + +const invariant = require('fbjs/lib/invariant'); + +import type { + NavigationSceneRendererProps, +} from './NavigationTypeDefinition'; + +type Props = NavigationSceneRendererProps; + +const MIN_POSITION_OFFSET = 0.01; + +/** + * Create a higher-order component that automatically computes the + * `pointerEvents` property for a component whenever navigation position + * changes. + */ +function create( + Component: ReactClass, +): ReactClass { + + class Container extends React.Component { + + _component: any; + _onComponentRef: (view: any) => void; + _onPositionChange: (data: {value: number}) => void; + _pointerEvents: string; + _positionListener: ?NavigationAnimatedValueSubscription; + + props: Props; + + constructor(props: Props, context: any) { + super(props, context); + this._pointerEvents = this._computePointerEvents(); + } + + componentWillMount(): void { + this._onPositionChange = this._onPositionChange.bind(this); + this._onComponentRef = this._onComponentRef.bind(this); + } + + componentDidMount(): void { + this._bindPosition(this.props); + } + + componentWillUnmount(): void { + this._positionListener && this._positionListener.remove(); + } + + componentWillReceiveProps(nextProps: Props): void { + this._bindPosition(nextProps); + } + + render(): React.Element { + this._pointerEvents = this._computePointerEvents(); + return ( + + ); + } + + _onComponentRef(component: any): void { + this._component = component; + if (component) { + invariant( + typeof component.setNativeProps === 'function', + 'component must implement method `setNativeProps`', + ); + } + } + + _bindPosition(props: NavigationSceneRendererProps): void { + this._positionListener && this._positionListener.remove(); + this._positionListener = new NavigationAnimatedValueSubscription( + props.position, + this._onPositionChange, + ); + } + + _onPositionChange(): void { + if (this._component) { + const pointerEvents = this._computePointerEvents(); + if (this._pointerEvents !== pointerEvents) { + this._pointerEvents = pointerEvents; + this._component.setNativeProps({pointerEvents}); + } + } + } + + _computePointerEvents(): string { + const { + navigationState, + position, + scene, + } = this.props; + + if (scene.isStale || navigationState.index !== scene.index) { + // The scene isn't focused. + return scene.index > navigationState.index ? + 'box-only' : + 'none'; + } + + const offset = position.__getAnimatedValue() - navigationState.index; + if (Math.abs(offset) > MIN_POSITION_OFFSET) { + // The positon is still away from scene's index. + // Scene's children should not receive touches until the position + // is close enough to scene's index. + return 'box-only'; + } + + return 'auto'; + } + } + return Container; +} + +module.exports = { + create, +}; diff --git a/src/NavigationExperimental/NavigationPropTypes.js b/src/NavigationExperimental/NavigationPropTypes.js new file mode 100644 index 0000000..70428c4 --- /dev/null +++ b/src/NavigationExperimental/NavigationPropTypes.js @@ -0,0 +1,123 @@ +/** + * 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 + */ +'use strict'; + +import type { + NavigationSceneRendererProps, +} from './NavigationTypeDefinition'; + +/** + * React component PropTypes Definitions. Consider using this as a supplementary + * measure with `NavigationTypeDefinition`. This helps to capture the propType + * error at run-time, where as `NavigationTypeDefinition` capture the flow + * type check errors at build time. + */ + +const {Animated} = require('react-native'); +const React = require('react'); + +const {PropTypes} = React; + +/* NavigationAction */ +const action = PropTypes.shape({ + type: PropTypes.string.isRequired, +}); + +/* NavigationAnimatedValue */ +const animatedValue = PropTypes.instanceOf(Animated.Value); + +/* NavigationRoute */ +const navigationRoute = PropTypes.shape({ + key: PropTypes.string.isRequired, +}); + +/* NavigationState */ +const navigationState = PropTypes.shape({ + index: PropTypes.number.isRequired, + routes: PropTypes.arrayOf(navigationRoute), +}); + +/* NavigationLayout */ +const layout = PropTypes.shape({ + height: animatedValue, + initHeight: PropTypes.number.isRequired, + initWidth: PropTypes.number.isRequired, + isMeasured: PropTypes.bool.isRequired, + width: animatedValue, +}); + +/* NavigationScene */ +const scene = PropTypes.shape({ + index: PropTypes.number.isRequired, + isActive: PropTypes.bool.isRequired, + isStale: PropTypes.bool.isRequired, + key: PropTypes.string.isRequired, + route: navigationRoute.isRequired, +}); + +/* NavigationSceneRendererProps */ +const SceneRendererProps = { + layout: layout.isRequired, + navigationState: navigationState.isRequired, + position: animatedValue.isRequired, + progress: animatedValue.isRequired, + scene: scene.isRequired, + scenes: PropTypes.arrayOf(scene).isRequired, +}; + +const SceneRenderer = PropTypes.shape(SceneRendererProps); + +/* NavigationPanPanHandlers */ +const panHandlers = PropTypes.shape({ + onMoveShouldSetResponder: PropTypes.func.isRequired, + onMoveShouldSetResponderCapture: PropTypes.func.isRequired, + onResponderEnd: PropTypes.func.isRequired, + onResponderGrant: PropTypes.func.isRequired, + onResponderMove: PropTypes.func.isRequired, + onResponderReject: PropTypes.func.isRequired, + onResponderRelease: PropTypes.func.isRequired, + onResponderStart: PropTypes.func.isRequired, + onResponderTerminate: PropTypes.func.isRequired, + onResponderTerminationRequest: PropTypes.func.isRequired, + onStartShouldSetResponder: PropTypes.func.isRequired, + onStartShouldSetResponderCapture: PropTypes.func.isRequired, +}); + +/** + * Helper function that extracts the props needed for scene renderer. + */ +function extractSceneRendererProps( + props: NavigationSceneRendererProps, +): NavigationSceneRendererProps { + return { + layout: props.layout, + navigationState: props.navigationState, + position: props.position, + progress: props.progress, + scene: props.scene, + scenes: props.scenes, + }; +} + +module.exports = { + // helpers + extractSceneRendererProps, + + // Bundled propTypes. + SceneRendererProps, + + // propTypes + SceneRenderer, + action, + navigationState, + navigationRoute, + panHandlers, +}; diff --git a/src/NavigationExperimental/NavigationScenesReducer.js b/src/NavigationExperimental/NavigationScenesReducer.js new file mode 100644 index 0000000..9eb3544 --- /dev/null +++ b/src/NavigationExperimental/NavigationScenesReducer.js @@ -0,0 +1,209 @@ +/** + * 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 + */ +'use strict'; + +const invariant = require('fbjs/lib/invariant'); +const shallowEqual = require('fbjs/lib/shallowEqual'); + +import type { + NavigationRoute, + NavigationScene, + NavigationState, +} from './NavigationTypeDefinition'; + +const SCENE_KEY_PREFIX = 'scene_'; + +/** + * Helper function to compare route keys (e.g. "9", "11"). + */ +function compareKey(one: string, two: string): number { + const 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.key, + two.key, + ); +} + +/** + * Whether two routes are the same. + */ +function areScenesShallowEqual( + one: NavigationScene, + two: NavigationScene, +): boolean { + return ( + one.key === two.key && + one.index === two.index && + one.isStale === two.isStale && + one.isActive === two.isActive && + areRoutesShallowEqual(one.route, two.route) + ); +} + +/** + * Whether two routes are the same. + */ +function areRoutesShallowEqual( + one: ?NavigationRoute, + two: ?NavigationRoute, +): boolean { + if (!one || !two) { + return one === two; + } + + if (one.key !== two.key) { + return false; + } + + return shallowEqual(one, two); +} + +function NavigationScenesReducer( + scenes: Array, + nextState: NavigationState, + prevState: ?NavigationState, +): Array { + if (prevState === nextState) { + return scenes; + } + + const prevScenes: Map = new Map(); + const freshScenes: Map = new Map(); + const staleScenes: Map = new Map(); + + // Populate stale scenes from previous scenes marked as stale. + scenes.forEach(scene => { + const {key} = scene; + if (scene.isStale) { + staleScenes.set(key, scene); + } + prevScenes.set(key, scene); + }); + + const nextKeys = new Set(); + nextState.routes.forEach((route, index) => { + const key = SCENE_KEY_PREFIX + route.key; + const scene = { + index, + isActive: false, + isStale: false, + key, + route, + }; + invariant( + !nextKeys.has(key), + `navigationState.routes[${index}].key "${key}" conflicts with ` + + 'another route!' + ); + nextKeys.add(key); + + if (staleScenes.has(key)) { + // A previously `stale` scene is now part of the nextState, so we + // revive it by removing it from the stale scene map. + staleScenes.delete(key); + } + freshScenes.set(key, scene); + }); + + if (prevState) { + // Look at the previous routes and classify any removed scenes as `stale`. + prevState.routes.forEach((route: NavigationRoute, index) => { + const key = SCENE_KEY_PREFIX + route.key; + if (freshScenes.has(key)) { + return; + } + staleScenes.set(key, { + index, + isActive: false, + isStale: true, + key, + route, + }); + }); + } + + const nextScenes = []; + + const mergeScene = (nextScene => { + const {key} = nextScene; + const prevScene = prevScenes.has(key) ? prevScenes.get(key) : null; + if (prevScene && areScenesShallowEqual(prevScene, nextScene)) { + // Reuse `prevScene` as `scene` so view can avoid unnecessary re-render. + // This assumes that the scene's navigation state is immutable. + nextScenes.push(prevScene); + } else { + nextScenes.push(nextScene); + } + }); + + staleScenes.forEach(mergeScene); + freshScenes.forEach(mergeScene); + + nextScenes.sort(compareScenes); + + let activeScenesCount = 0; + nextScenes.forEach((scene, ii) => { + const isActive = !scene.isStale && scene.index === nextState.index; + if (isActive !== scene.isActive) { + nextScenes[ii] = { + ...scene, + isActive, + }; + } + if (isActive) { + activeScenesCount++; + } + }); + + invariant( + activeScenesCount === 1, + 'there should always be only one scene active, not %s.', + activeScenesCount, + ); + + if (nextScenes.length !== scenes.length) { + return nextScenes; + } + + if (nextScenes.some( + (scene, index) => !areScenesShallowEqual(scenes[index], scene) + )) { + return nextScenes; + } + + // scenes haven't changed. + return scenes; +} + +module.exports = NavigationScenesReducer; diff --git a/src/NavigationExperimental/NavigationStateUtils.js b/src/NavigationExperimental/NavigationStateUtils.js new file mode 100644 index 0000000..bad0609 --- /dev/null +++ b/src/NavigationExperimental/NavigationStateUtils.js @@ -0,0 +1,214 @@ +/** + * 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 + */ +'use strict'; + +const invariant = require('fbjs/lib/invariant'); + +import type { + NavigationRoute, + NavigationState +} from './NavigationTypeDefinition'; + +/** + * Utilities to perform atomic operation with navigate state and routes. + * + * ```javascript + * const state1 = {key: 'page 1'}; + * const state2 = NavigationStateUtils.push(state1, {key: 'page 2'}); + * ``` + */ +const NavigationStateUtils = { + + /** + * Gets a route by key. If the route isn't found, returns `null`. + */ + get(state: NavigationState, key: string): ?NavigationRoute { + return state.routes.find(route => route.key === key) || null; + }, + + /** + * Returns the first index at which a given route's key can be found in the + * routes of the navigation state, or -1 if it is not present. + */ + indexOf(state: NavigationState, key: string): number { + return state.routes.map(route => route.key).indexOf(key); + }, + + /** + * Returns `true` at which a given route's key can be found in the + * routes of the navigation state. + */ + has(state: NavigationState, key: string): boolean { + return !!state.routes.some(route => route.key === key); + }, + + /** + * Pushes a new route into the navigation state. + * Note that this moves the index to the positon to where the last route in the + * stack is at. + */ + push(state: NavigationState, route: NavigationRoute): NavigationState { + invariant( + NavigationStateUtils.indexOf(state, route.key) === -1, + 'should not push route with duplicated key %s', + route.key, + ); + + const routes = state.routes.slice(); + routes.push(route); + + return { + ...state, + index: routes.length - 1, + routes, + }; + }, + + /** + * Pops out a route from the navigation state. + * Note that this moves the index to the positon to where the last route in the + * stack is at. + */ + pop(state: NavigationState): NavigationState { + if (state.index <= 0) { + // [Note]: Over-popping does not throw error. Instead, it will be no-op. + return state; + } + const routes = state.routes.slice(0, -1); + return { + ...state, + index: routes.length - 1, + routes, + }; + }, + + /** + * Sets the focused route of the navigation state by index. + */ + jumpToIndex(state: NavigationState, index: number): NavigationState { + if (index === state.index) { + return state; + } + + invariant(!!state.routes[index], 'invalid index %s to jump to', index); + + return { + ...state, + index, + }; + }, + + /** + * Sets the focused route of the navigation state by key. + */ + jumpTo(state: NavigationState, key: string): NavigationState { + const index = NavigationStateUtils.indexOf(state, key); + return NavigationStateUtils.jumpToIndex(state, index); + }, + + /** + * Sets the focused route to the previous route. + */ + back(state: NavigationState): NavigationState { + const index = state.index - 1; + const route = state.routes[index]; + return route ? NavigationStateUtils.jumpToIndex(state, index) : state; + }, + + /** + * Sets the focused route to the next route. + */ + forward(state: NavigationState): NavigationState { + const index = state.index + 1; + const route = state.routes[index]; + return route ? NavigationStateUtils.jumpToIndex(state, index) : state; + }, + + /** + * Replace a route by a key. + * Note that this moves the index to the positon to where the new route in the + * stack is at. + */ + replaceAt( + state: NavigationState, + key: string, + route: NavigationRoute, + ): NavigationState { + const index = NavigationStateUtils.indexOf(state, key); + return NavigationStateUtils.replaceAtIndex(state, index, route); + }, + + /** + * Replace a route by a index. + * Note that this moves the index to the positon to where the new route in the + * stack is at. + */ + replaceAtIndex( + state: NavigationState, + index: number, + route: NavigationRoute, + ): NavigationState { + invariant( + !!state.routes[index], + 'invalid index %s for replacing route %s', + index, + route.key, + ); + + if (state.routes[index] === route) { + return state; + } + + const routes = state.routes.slice(); + routes[index] = route; + + return { + ...state, + index, + routes, + }; + }, + + /** + * Resets all routes. + * Note that this moves the index to the positon to where the last route in the + * stack is at if the param `index` isn't provided. + */ + reset( + state: NavigationState, + routes: Array, + index?: number, + ): NavigationState { + invariant( + routes.length && Array.isArray(routes), + 'invalid routes to replace', + ); + + const nextIndex: number = index === undefined ? routes.length - 1 : index; + + if (state.routes.length === routes.length && state.index === nextIndex) { + const compare = (route, ii) => routes[ii] === route; + if (state.routes.every(compare)) { + return state; + } + } + + invariant(!!routes[nextIndex], 'invalid index %s to reset', nextIndex); + + return { + ...state, + index: nextIndex, + routes, + }; + }, +}; + +module.exports = NavigationStateUtils; diff --git a/src/NavigationExperimental/NavigationTransitioner.js b/src/NavigationExperimental/NavigationTransitioner.js new file mode 100644 index 0000000..c232aa4 --- /dev/null +++ b/src/NavigationExperimental/NavigationTransitioner.js @@ -0,0 +1,288 @@ +/** + * 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 + */ +'use strict'; + +const {Animated, Easing, StyleSheet, View} = require('react-native'); +const NavigationPropTypes = require('./NavigationPropTypes'); +const NavigationScenesReducer = require('./NavigationScenesReducer'); +const React = require('react'); + +const invariant = require('fbjs/lib/invariant'); + +import type { + NavigationAnimatedValue, + NavigationLayout, + NavigationScene, + NavigationState, + NavigationTransitionProps, + NavigationTransitionSpec, +} from './NavigationTypeDefinition'; + +type Props = { + configureTransition: ( + a: NavigationTransitionProps, + b: ?NavigationTransitionProps, + ) => NavigationTransitionSpec, + navigationState: NavigationState, + onTransitionEnd: () => void, + onTransitionStart: () => void, + render: (a: NavigationTransitionProps, b: ?NavigationTransitionProps) => any, + style: any, +}; + +type State = { + layout: NavigationLayout, + position: NavigationAnimatedValue, + progress: NavigationAnimatedValue, + scenes: Array, +}; + +const {PropTypes} = React; + +const DefaultTransitionSpec = { + duration: 250, + easing: Easing.inOut(Easing.ease), + timing: Animated.timing, +}; + +class NavigationTransitioner extends React.Component { + _onLayout: (event: any) => void; + _onTransitionEnd: () => void; + _prevTransitionProps: ?NavigationTransitionProps; + _transitionProps: NavigationTransitionProps; + _isMounted: boolean; + + props: Props; + state: State; + + static propTypes = { + configureTransition: PropTypes.func, + navigationState: NavigationPropTypes.navigationState.isRequired, + onTransitionEnd: PropTypes.func, + onTransitionStart: PropTypes.func, + render: PropTypes.func.isRequired, + }; + + constructor(props: Props, context: any) { + super(props, context); + + // The initial layout isn't measured. Measured layout will be only available + // when the component is mounted. + const layout = { + height: new Animated.Value(0), + initHeight: 0, + initWidth: 0, + isMeasured: false, + width: new Animated.Value(0), + }; + + this.state = { + layout, + position: new Animated.Value(this.props.navigationState.index), + progress: new Animated.Value(1), + scenes: NavigationScenesReducer([], this.props.navigationState), + }; + + this._prevTransitionProps = null; + this._transitionProps = buildTransitionProps(props, this.state); + this._isMounted = false; + } + + componentWillMount(): void { + this._onLayout = this._onLayout.bind(this); + this._onTransitionEnd = this._onTransitionEnd.bind(this); + } + + componentDidMount(): void { + this._isMounted = true; + } + + componentWillUnmount(): void { + this._isMounted = false; + } + + componentWillReceiveProps(nextProps: Props): void { + const nextScenes = NavigationScenesReducer( + this.state.scenes, + nextProps.navigationState, + this.props.navigationState + ); + + if (nextScenes === this.state.scenes) { + return; + } + + const nextState = { + ...this.state, + scenes: nextScenes, + }; + + const { + position, + progress, + } = nextState; + + progress.setValue(0); + + this._prevTransitionProps = this._transitionProps; + this._transitionProps = buildTransitionProps(nextProps, nextState); + + // get the transition spec. + const transitionUserSpec = nextProps.configureTransition ? + nextProps.configureTransition( + this._transitionProps, + this._prevTransitionProps, + ) : + null; + + const transitionSpec = { + ...DefaultTransitionSpec, + ...transitionUserSpec, + }; + + const {timing} = transitionSpec; + delete transitionSpec.timing; + + const animations = [ + timing( + progress, + { + ...transitionSpec, + toValue: 1, + }, + ), + ]; + + if (nextProps.navigationState.index !== this.props.navigationState.index) { + animations.push( + timing( + position, + { + ...transitionSpec, + toValue: nextProps.navigationState.index, + }, + ), + ); + } + + // update scenes and play the transition + this.setState(nextState, () => { + nextProps.onTransitionStart && nextProps.onTransitionStart( + this._transitionProps, + this._prevTransitionProps, + ); + Animated.parallel(animations).start(this._onTransitionEnd); + }); + } + + render(): React.Element { + return ( + + {this.props.render(this._transitionProps, this._prevTransitionProps)} + + ); + } + + _onLayout(event: any): void { + const {height, width} = event.nativeEvent.layout; + if (this.state.layout.initWidth === width && + this.state.layout.initHeight === height) { + return; + } + const layout = { + ...this.state.layout, + initHeight: height, + initWidth: width, + isMeasured: true, + }; + + layout.height.setValue(height); + layout.width.setValue(width); + + const nextState = { + ...this.state, + layout, + }; + + this._transitionProps = buildTransitionProps(this.props, nextState); + this.setState(nextState); + } + + _onTransitionEnd(): void { + if (!this._isMounted) { + return; + } + + const prevTransitionProps = this._prevTransitionProps; + this._prevTransitionProps = null; + + const nextState = { + ...this.state, + scenes: this.state.scenes.filter(isSceneNotStale), + }; + + this._transitionProps = buildTransitionProps(this.props, nextState); + + this.setState(nextState, () => { + this.props.onTransitionEnd && this.props.onTransitionEnd( + this._transitionProps, + prevTransitionProps, + ); + }); + } +} + +function buildTransitionProps( + props: Props, + state: State, +): NavigationTransitionProps { + const { + navigationState, + } = props; + + const { + layout, + position, + progress, + scenes, + } = state; + + const scene = scenes.find(isSceneActive); + + invariant(scene, 'No active scene when building navigation transition props.'); + + return { + layout, + navigationState, + position, + progress, + scenes, + scene + }; +} + +function isSceneNotStale(scene: NavigationScene): boolean { + return !scene.isStale; +} + +function isSceneActive(scene: NavigationScene): boolean { + return scene.isActive; +} + +const styles = StyleSheet.create({ + main: { + flex: 1, + }, +}); + +module.exports = NavigationTransitioner; diff --git a/src/NavigationExperimental/NavigationTypeDefinition.js b/src/NavigationExperimental/NavigationTypeDefinition.js new file mode 100644 index 0000000..51f1859 --- /dev/null +++ b/src/NavigationExperimental/NavigationTypeDefinition.js @@ -0,0 +1,120 @@ +/** + * 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 + */ +'use strict'; + +const {Animated} = require('react-native'); + +import type React from 'react'; + +// Object Instances + +export type NavigationAnimatedValue = Animated.Value; + +// Value & Structs. + +export type NavigationGestureDirection = 'horizontal' | 'vertical'; + +export type NavigationRoute = { + key: string, + title?: string +}; + +export type NavigationState = { + index: number, + routes: Array, +}; + +export type NavigationLayout = { + height: NavigationAnimatedValue, + initHeight: number, + initWidth: number, + isMeasured: boolean, + width: NavigationAnimatedValue, +}; + +export type NavigationScene = { + index: number, + isActive: boolean, + isStale: boolean, + key: string, + route: NavigationRoute, +}; + +export type NavigationTransitionProps = { + // The layout of the transitioner of the scenes. + layout: NavigationLayout, + + // The navigation state of the transitioner. + navigationState: NavigationState, + + // The progressive index of the transitioner's navigation state. + position: NavigationAnimatedValue, + + // The value that represents the progress of the transition when navigation + // state changes from one to another. Its numberic value will range from 0 + // to 1. + // progress.__getAnimatedValue() < 1 : transtion is happening. + // progress.__getAnimatedValue() == 1 : transtion completes. + progress: NavigationAnimatedValue, + + // All the scenes of the transitioner. + scenes: Array, + + // The active scene, corresponding to the route at + // `navigationState.routes[navigationState.index]`. + scene: NavigationScene, + + // The gesture distance for `horizontal` and `vertical` transitions + gestureResponseDistance?: ?number, +}; + +// Similar to `NavigationTransitionProps`, except that the prop `scene` +// represents the scene for the renderer to render. +export type NavigationSceneRendererProps = NavigationTransitionProps; + +export type NavigationPanPanHandlers = { + onMoveShouldSetResponder: Function, + onMoveShouldSetResponderCapture: Function, + onResponderEnd: Function, + onResponderGrant: Function, + onResponderMove: Function, + onResponderReject: Function, + onResponderRelease: Function, + onResponderStart: Function, + onResponderTerminate: Function, + onResponderTerminationRequest: Function, + onStartShouldSetResponder: Function, + onStartShouldSetResponderCapture: Function, +}; + +export type NavigationTransitionSpec = { + duration?: number, + // An easing function from `Easing`. + easing?: () => any, + // A timing function such as `Animated.timing`. + timing?: (value: NavigationAnimatedValue, config: any) => any, +}; + +// Functions. + +export type NavigationAnimationSetter = ( + position: NavigationAnimatedValue, + newState: NavigationState, + lastState: NavigationState, +) => void; + +export type NavigationSceneRenderer = ( + props: NavigationSceneRendererProps, +) => ?React.Element; + +export type NavigationStyleInterpolator = ( + props: NavigationSceneRendererProps, +) => Object; diff --git a/src/NavigationExperimental/assets/back-icon@1.5x.android.png b/src/NavigationExperimental/assets/back-icon@1.5x.android.png new file mode 100644 index 0000000..ad03a63 Binary files /dev/null and b/src/NavigationExperimental/assets/back-icon@1.5x.android.png differ diff --git a/src/NavigationExperimental/assets/back-icon@1.5x.ios.png b/src/NavigationExperimental/assets/back-icon@1.5x.ios.png new file mode 100644 index 0000000..e43fa06 Binary files /dev/null and b/src/NavigationExperimental/assets/back-icon@1.5x.ios.png differ diff --git a/src/NavigationExperimental/assets/back-icon@1x.android.png b/src/NavigationExperimental/assets/back-icon@1x.android.png new file mode 100644 index 0000000..083db29 Binary files /dev/null and b/src/NavigationExperimental/assets/back-icon@1x.android.png differ diff --git a/src/NavigationExperimental/assets/back-icon@1x.ios.png b/src/NavigationExperimental/assets/back-icon@1x.ios.png new file mode 100644 index 0000000..4244656 Binary files /dev/null and b/src/NavigationExperimental/assets/back-icon@1x.ios.png differ diff --git a/src/NavigationExperimental/assets/back-icon@2x.android.png b/src/NavigationExperimental/assets/back-icon@2x.android.png new file mode 100644 index 0000000..6de0a1c Binary files /dev/null and b/src/NavigationExperimental/assets/back-icon@2x.android.png differ diff --git a/src/NavigationExperimental/assets/back-icon@2x.ios.png b/src/NavigationExperimental/assets/back-icon@2x.ios.png new file mode 100644 index 0000000..a8b6e9a Binary files /dev/null and b/src/NavigationExperimental/assets/back-icon@2x.ios.png differ diff --git a/src/NavigationExperimental/assets/back-icon@3x.android.png b/src/NavigationExperimental/assets/back-icon@3x.android.png new file mode 100644 index 0000000..15a983a Binary files /dev/null and b/src/NavigationExperimental/assets/back-icon@3x.android.png differ diff --git a/src/NavigationExperimental/assets/back-icon@3x.ios.png b/src/NavigationExperimental/assets/back-icon@3x.ios.png new file mode 100644 index 0000000..07ea37b Binary files /dev/null and b/src/NavigationExperimental/assets/back-icon@3x.ios.png differ diff --git a/src/NavigationExperimental/assets/back-icon@4x.android.png b/src/NavigationExperimental/assets/back-icon@4x.android.png new file mode 100644 index 0000000..17e52e8 Binary files /dev/null and b/src/NavigationExperimental/assets/back-icon@4x.android.png differ diff --git a/src/NavigationExperimental/assets/back-icon@4x.ios.png b/src/NavigationExperimental/assets/back-icon@4x.ios.png new file mode 100644 index 0000000..7899053 Binary files /dev/null and b/src/NavigationExperimental/assets/back-icon@4x.ios.png differ diff --git a/src/NavigationExperimental/index.js b/src/NavigationExperimental/index.js new file mode 100644 index 0000000..2aa3af1 --- /dev/null +++ b/src/NavigationExperimental/index.js @@ -0,0 +1,35 @@ +/** + * 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 + */ +'use strict'; + +const NavigationCard = require('./NavigationCard'); +const NavigationCardStack = require('./NavigationCardStack'); +const NavigationHeader = require('./NavigationHeader'); +const NavigationPropTypes = require('./NavigationPropTypes'); +const NavigationStateUtils = require('./NavigationStateUtils'); +const NavigationTransitioner = require('./NavigationTransitioner'); + +const NavigationExperimental = { + // Core + StateUtils: NavigationStateUtils, + + // Views + Transitioner: NavigationTransitioner, + + // CustomComponents: + Card: NavigationCard, + CardStack: NavigationCardStack, + Header: NavigationHeader, + + PropTypes: NavigationPropTypes, +}; + +module.exports = NavigationExperimental; diff --git a/src/EmitterSubscription.js b/src/Navigator/EmitterSubscription.js similarity index 100% rename from src/EmitterSubscription.js rename to src/Navigator/EmitterSubscription.js diff --git a/src/EventEmitter.js b/src/Navigator/EventEmitter.js similarity index 100% rename from src/EventEmitter.js rename to src/Navigator/EventEmitter.js diff --git a/src/EventSubscription.js b/src/Navigator/EventSubscription.js similarity index 100% rename from src/EventSubscription.js rename to src/Navigator/EventSubscription.js diff --git a/src/EventSubscriptionVendor.js b/src/Navigator/EventSubscriptionVendor.js similarity index 100% rename from src/EventSubscriptionVendor.js rename to src/Navigator/EventSubscriptionVendor.js diff --git a/src/InteractionMixin.js b/src/Navigator/InteractionMixin.js similarity index 100% rename from src/InteractionMixin.js rename to src/Navigator/InteractionMixin.js diff --git a/src/NavigationContext.js b/src/Navigator/NavigationContext.js similarity index 100% rename from src/NavigationContext.js rename to src/Navigator/NavigationContext.js diff --git a/src/NavigationEvent.js b/src/Navigator/NavigationEvent.js similarity index 100% rename from src/NavigationEvent.js rename to src/Navigator/NavigationEvent.js diff --git a/src/NavigationEventEmitter.js b/src/Navigator/NavigationEventEmitter.js similarity index 100% rename from src/NavigationEventEmitter.js rename to src/Navigator/NavigationEventEmitter.js diff --git a/src/NavigationRouteStack.js b/src/Navigator/NavigationRouteStack.js similarity index 100% rename from src/NavigationRouteStack.js rename to src/Navigator/NavigationRouteStack.js diff --git a/src/NavigationTreeNode.js b/src/Navigator/NavigationTreeNode.js similarity index 100% rename from src/NavigationTreeNode.js rename to src/Navigator/NavigationTreeNode.js diff --git a/src/NavigatorBreadcrumbNavigationBar.js b/src/Navigator/NavigatorBreadcrumbNavigationBar.js similarity index 100% rename from src/NavigatorBreadcrumbNavigationBar.js rename to src/Navigator/NavigatorBreadcrumbNavigationBar.js diff --git a/src/NavigatorBreadcrumbNavigationBarStyles.android.js b/src/Navigator/NavigatorBreadcrumbNavigationBarStyles.android.js similarity index 100% rename from src/NavigatorBreadcrumbNavigationBarStyles.android.js rename to src/Navigator/NavigatorBreadcrumbNavigationBarStyles.android.js diff --git a/src/NavigatorBreadcrumbNavigationBarStyles.ios.js b/src/Navigator/NavigatorBreadcrumbNavigationBarStyles.ios.js similarity index 100% rename from src/NavigatorBreadcrumbNavigationBarStyles.ios.js rename to src/Navigator/NavigatorBreadcrumbNavigationBarStyles.ios.js diff --git a/src/NavigatorNavigationBar.js b/src/Navigator/NavigatorNavigationBar.js similarity index 100% rename from src/NavigatorNavigationBar.js rename to src/Navigator/NavigatorNavigationBar.js diff --git a/src/NavigatorNavigationBarStylesAndroid.js b/src/Navigator/NavigatorNavigationBarStylesAndroid.js similarity index 100% rename from src/NavigatorNavigationBarStylesAndroid.js rename to src/Navigator/NavigatorNavigationBarStylesAndroid.js diff --git a/src/NavigatorNavigationBarStylesIOS.js b/src/Navigator/NavigatorNavigationBarStylesIOS.js similarity index 100% rename from src/NavigatorNavigationBarStylesIOS.js rename to src/Navigator/NavigatorNavigationBarStylesIOS.js diff --git a/src/NavigatorSceneConfigs.js b/src/Navigator/NavigatorSceneConfigs.js similarity index 100% rename from src/NavigatorSceneConfigs.js rename to src/Navigator/NavigatorSceneConfigs.js diff --git a/src/Subscribable.js b/src/Navigator/Subscribable.js similarity index 100% rename from src/Subscribable.js rename to src/Navigator/Subscribable.js diff --git a/src/__tests__/NavigationContext-test.js b/src/Navigator/__tests__/NavigationContext-test.js similarity index 100% rename from src/__tests__/NavigationContext-test.js rename to src/Navigator/__tests__/NavigationContext-test.js diff --git a/src/__tests__/NavigationEvent-test.js b/src/Navigator/__tests__/NavigationEvent-test.js similarity index 100% rename from src/__tests__/NavigationEvent-test.js rename to src/Navigator/__tests__/NavigationEvent-test.js diff --git a/src/__tests__/NavigationEventEmitter-test.js b/src/Navigator/__tests__/NavigationEventEmitter-test.js similarity index 100% rename from src/__tests__/NavigationEventEmitter-test.js rename to src/Navigator/__tests__/NavigationEventEmitter-test.js diff --git a/src/__tests__/NavigationRouteStack-test.js b/src/Navigator/__tests__/NavigationRouteStack-test.js similarity index 100% rename from src/__tests__/NavigationRouteStack-test.js rename to src/Navigator/__tests__/NavigationRouteStack-test.js diff --git a/src/__tests__/NavigationTreeNode-test.js b/src/Navigator/__tests__/NavigationTreeNode-test.js similarity index 100% rename from src/__tests__/NavigationTreeNode-test.js rename to src/Navigator/__tests__/NavigationTreeNode-test.js diff --git a/src/buildStyleInterpolator.js b/src/Navigator/buildStyleInterpolator.js similarity index 100% rename from src/buildStyleInterpolator.js rename to src/Navigator/buildStyleInterpolator.js diff --git a/src/flattenStyle.js b/src/Navigator/flattenStyle.js similarity index 100% rename from src/flattenStyle.js rename to src/Navigator/flattenStyle.js diff --git a/src/guid.js b/src/Navigator/guid.js similarity index 100% rename from src/guid.js rename to src/Navigator/guid.js diff --git a/src/Navigator.js b/src/Navigator/index.js similarity index 99% rename from src/Navigator.js rename to src/Navigator/index.js index b714e99..da8c87e 100644 --- a/src/Navigator.js +++ b/src/Navigator/index.js @@ -47,7 +47,7 @@ var React = require('react'); var Subscribable = require('./Subscribable'); var TimerMixin = require('react-timer-mixin'); -var clamp = require('./clamp'); +var clamp = require('clamp'); var invariant = require('fbjs/lib/invariant'); var rebound = require('rebound'); diff --git a/src/merge.js b/src/Navigator/merge.js similarity index 100% rename from src/merge.js rename to src/Navigator/merge.js diff --git a/src/mergeHelpers.js b/src/Navigator/mergeHelpers.js similarity index 100% rename from src/mergeHelpers.js rename to src/Navigator/mergeHelpers.js diff --git a/src/mergeInto.js b/src/Navigator/mergeInto.js similarity index 100% rename from src/mergeInto.js rename to src/Navigator/mergeInto.js