UNPKG

@react-navigation/stack

Version:

Stack navigator component for iOS and Android with animated transitions and gestures

476 lines (471 loc) 17.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getIsModalPresentation = exports.default = void 0; var _color = _interopRequireDefault(require("color")); var React = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _CardStyleInterpolators = require("../../TransitionConfigs/CardStyleInterpolators"); var _CardAnimationContext = _interopRequireDefault(require("../../utils/CardAnimationContext")); var _getDistanceForDirection = _interopRequireDefault(require("../../utils/getDistanceForDirection")); var _getInvertedMultiplier = _interopRequireDefault(require("../../utils/getInvertedMultiplier")); var _memoize = _interopRequireDefault(require("../../utils/memoize")); var _GestureHandler = require("../GestureHandler"); var _ModalStatusBarManager = _interopRequireDefault(require("../ModalStatusBarManager")); var _CardSheet = _interopRequireDefault(require("./CardSheet")); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } const GESTURE_VELOCITY_IMPACT = 0.3; const TRUE = 1; const FALSE = 0; /** * The distance of touch start from the edge of the screen where the gesture will be recognized */ const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 50; const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135; const useNativeDriver = _reactNative.Platform.OS !== 'web'; const hasOpacityStyle = style => { if (style) { const flattenedStyle = _reactNative.StyleSheet.flatten(style); return flattenedStyle.opacity != null; } return false; }; class Card extends React.Component { static defaultProps = { shadowEnabled: false, gestureEnabled: true, gestureVelocityImpact: GESTURE_VELOCITY_IMPACT, overlay: _ref => { let { style } = _ref; return style ? /*#__PURE__*/React.createElement(_reactNative.Animated.View, { pointerEvents: "none", style: [styles.overlay, style] }) : null; } }; componentDidMount() { this.animate({ closing: this.props.closing }); this.isCurrentlyMounted = true; } componentDidUpdate(prevProps) { const { layout, gestureDirection, closing } = this.props; const { width, height } = layout; if (width !== prevProps.layout.width) { this.layout.width.setValue(width); } if (height !== prevProps.layout.height) { this.layout.height.setValue(height); } if (gestureDirection !== prevProps.gestureDirection) { this.inverted.setValue((0, _getInvertedMultiplier.default)(gestureDirection)); } const toValue = this.getAnimateToValue(this.props); if (this.getAnimateToValue(prevProps) !== toValue || this.lastToValue !== toValue) { // We need to trigger the animation when route was closed // Thr route might have been closed by a `POP` action or by a gesture // When route was closed due to a gesture, the animation would've happened already // It's still important to trigger the animation so that `onClose` is called // If `onClose` is not called, cleanup step won't be performed for gestures this.animate({ closing }); } } componentWillUnmount() { this.props.gesture.stopAnimation(); this.isCurrentlyMounted = false; this.handleEndInteraction(); } isCurrentlyMounted = false; isClosing = new _reactNative.Animated.Value(FALSE); inverted = new _reactNative.Animated.Value((0, _getInvertedMultiplier.default)(this.props.gestureDirection)); layout = { width: new _reactNative.Animated.Value(this.props.layout.width), height: new _reactNative.Animated.Value(this.props.layout.height) }; isSwiping = new _reactNative.Animated.Value(FALSE); animate = _ref2 => { let { closing, velocity } = _ref2; const { gesture, transitionSpec, onOpen, onClose, onTransition } = this.props; const toValue = this.getAnimateToValue({ ...this.props, closing }); this.lastToValue = toValue; this.isClosing.setValue(closing ? TRUE : FALSE); const spec = closing ? transitionSpec.close : transitionSpec.open; const animation = spec.animation === 'spring' ? _reactNative.Animated.spring : _reactNative.Animated.timing; this.setPointerEventsEnabled(!closing); this.handleStartInteraction(); clearTimeout(this.pendingGestureCallback); onTransition === null || onTransition === void 0 ? void 0 : onTransition({ closing, gesture: velocity !== undefined }); animation(gesture, { ...spec.config, velocity, toValue, useNativeDriver, isInteraction: false }).start(_ref3 => { let { finished } = _ref3; this.handleEndInteraction(); clearTimeout(this.pendingGestureCallback); if (finished) { if (closing) { onClose(); } else { onOpen(); } if (this.isCurrentlyMounted) { // Make sure to re-open screen if it wasn't removed this.forceUpdate(); } } }); }; getAnimateToValue = _ref4 => { let { closing, layout, gestureDirection } = _ref4; if (!closing) { return 0; } return (0, _getDistanceForDirection.default)(layout, gestureDirection); }; setPointerEventsEnabled = enabled => { var _this$ref$current; const pointerEvents = enabled ? 'box-none' : 'none'; (_this$ref$current = this.ref.current) === null || _this$ref$current === void 0 ? void 0 : _this$ref$current.setPointerEvents(pointerEvents); }; handleStartInteraction = () => { if (this.interactionHandle === undefined) { this.interactionHandle = _reactNative.InteractionManager.createInteractionHandle(); } }; handleEndInteraction = () => { if (this.interactionHandle !== undefined) { _reactNative.InteractionManager.clearInteractionHandle(this.interactionHandle); this.interactionHandle = undefined; } }; handleGestureStateChange = _ref5 => { let { nativeEvent } = _ref5; const { layout, onClose, onGestureBegin, onGestureCanceled, onGestureEnd, gestureDirection, gestureVelocityImpact } = this.props; switch (nativeEvent.state) { case _GestureHandler.GestureState.ACTIVE: this.isSwiping.setValue(TRUE); this.handleStartInteraction(); onGestureBegin === null || onGestureBegin === void 0 ? void 0 : onGestureBegin(); break; case _GestureHandler.GestureState.CANCELLED: { this.isSwiping.setValue(FALSE); this.handleEndInteraction(); const velocity = gestureDirection === 'vertical' || gestureDirection === 'vertical-inverted' ? nativeEvent.velocityY : nativeEvent.velocityX; this.animate({ closing: this.props.closing, velocity }); onGestureCanceled === null || onGestureCanceled === void 0 ? void 0 : onGestureCanceled(); break; } case _GestureHandler.GestureState.END: { this.isSwiping.setValue(FALSE); let distance; let translation; let velocity; if (gestureDirection === 'vertical' || gestureDirection === 'vertical-inverted') { distance = layout.height; translation = nativeEvent.translationY; velocity = nativeEvent.velocityY; } else { distance = layout.width; translation = nativeEvent.translationX; velocity = nativeEvent.velocityX; } const closing = (translation + velocity * gestureVelocityImpact) * (0, _getInvertedMultiplier.default)(gestureDirection) > distance / 2 ? velocity !== 0 || translation !== 0 : this.props.closing; this.animate({ closing, velocity }); if (closing) { // We call onClose with a delay to make sure that the animation has already started // This will make sure that the state update caused by this doesn't affect start of animation this.pendingGestureCallback = setTimeout(() => { onClose(); // Trigger an update after we dispatch the action to remove the screen // This will make sure that we check if the screen didn't get removed so we can cancel the animation this.forceUpdate(); }, 32); } onGestureEnd === null || onGestureEnd === void 0 ? void 0 : onGestureEnd(); break; } } }; // Memoize this to avoid extra work on re-render getInterpolatedStyle = (0, _memoize.default)((styleInterpolator, animation) => styleInterpolator(animation)); // Keep track of the animation context when deps changes. getCardAnimation = (0, _memoize.default)((interpolationIndex, current, next, layout, insetTop, insetRight, insetBottom, insetLeft) => ({ index: interpolationIndex, current: { progress: current }, next: next && { progress: next }, closing: this.isClosing, swiping: this.isSwiping, inverted: this.inverted, layouts: { screen: layout }, insets: { top: insetTop, right: insetRight, bottom: insetBottom, left: insetLeft } })); gestureActivationCriteria() { const { layout, gestureDirection, gestureResponseDistance } = this.props; const enableTrackpadTwoFingerGesture = true; const distance = gestureResponseDistance !== undefined ? gestureResponseDistance : gestureDirection === 'vertical' || gestureDirection === 'vertical-inverted' ? GESTURE_RESPONSE_DISTANCE_VERTICAL : GESTURE_RESPONSE_DISTANCE_HORIZONTAL; if (gestureDirection === 'vertical') { return { maxDeltaX: 15, minOffsetY: 5, hitSlop: { bottom: -layout.height + distance }, enableTrackpadTwoFingerGesture }; } else if (gestureDirection === 'vertical-inverted') { return { maxDeltaX: 15, minOffsetY: -5, hitSlop: { top: -layout.height + distance }, enableTrackpadTwoFingerGesture }; } else { const hitSlop = -layout.width + distance; const invertedMultiplier = (0, _getInvertedMultiplier.default)(gestureDirection); if (invertedMultiplier === 1) { return { minOffsetX: 5, maxDeltaY: 20, hitSlop: { right: hitSlop }, enableTrackpadTwoFingerGesture }; } else { return { minOffsetX: -5, maxDeltaY: 20, hitSlop: { left: hitSlop }, enableTrackpadTwoFingerGesture }; } } } ref = /*#__PURE__*/React.createRef(); render() { const { styleInterpolator, interpolationIndex, current, gesture, next, layout, insets, overlay, overlayEnabled, shadowEnabled, gestureEnabled, gestureDirection, pageOverflowEnabled, headerDarkContent, children, containerStyle: customContainerStyle, contentStyle, ...rest } = this.props; const interpolationProps = this.getCardAnimation(interpolationIndex, current, next, layout, insets.top, insets.right, insets.bottom, insets.left); const interpolatedStyle = this.getInterpolatedStyle(styleInterpolator, interpolationProps); const { containerStyle, cardStyle, overlayStyle, shadowStyle } = interpolatedStyle; const handleGestureEvent = gestureEnabled ? _reactNative.Animated.event([{ nativeEvent: gestureDirection === 'vertical' || gestureDirection === 'vertical-inverted' ? { translationY: gesture } : { translationX: gesture } }], { useNativeDriver }) : undefined; const { backgroundColor } = _reactNative.StyleSheet.flatten(contentStyle || {}); const isTransparent = typeof backgroundColor === 'string' ? (0, _color.default)(backgroundColor).alpha() === 0 : false; return /*#__PURE__*/React.createElement(_CardAnimationContext.default.Provider, { value: interpolationProps }, // StatusBar messes with translucent status bar on Android // So we should only enable it on iOS _reactNative.Platform.OS === 'ios' && overlayEnabled && next && getIsModalPresentation(styleInterpolator) ? /*#__PURE__*/React.createElement(_ModalStatusBarManager.default, { dark: headerDarkContent, layout: layout, insets: insets, style: cardStyle }) : null, /*#__PURE__*/React.createElement(_reactNative.Animated.View, { style: { // This is a dummy style that doesn't actually change anything visually. // Animated needs the animated value to be used somewhere, otherwise things don't update properly. // If we disable animations and hide header, it could end up making the value unused. // So we have this dummy style that will always be used regardless of what else changed. opacity: current } // Make sure that this view isn't removed. If this view is removed, our style with animated value won't apply , collapsable: false }), /*#__PURE__*/React.createElement(_reactNative.View, _extends({ pointerEvents: "box-none" // Make sure this view is not removed on the new architecture, as it causes focus loss during navigation on Android. // This can happen when the view flattening results in different trees - due to `overflow` style changing in a parent. , collapsable: false }, rest), overlayEnabled ? /*#__PURE__*/React.createElement(_reactNative.View, { pointerEvents: "box-none", style: _reactNative.StyleSheet.absoluteFill }, overlay({ style: overlayStyle })) : null, /*#__PURE__*/React.createElement(_reactNative.Animated.View, { style: [styles.container, containerStyle, customContainerStyle], pointerEvents: "box-none" }, /*#__PURE__*/React.createElement(_GestureHandler.PanGestureHandler, _extends({ enabled: layout.width !== 0 && gestureEnabled, onGestureEvent: handleGestureEvent, onHandlerStateChange: this.handleGestureStateChange }, this.gestureActivationCriteria()), /*#__PURE__*/React.createElement(_reactNative.Animated.View, { needsOffscreenAlphaCompositing: hasOpacityStyle(cardStyle), style: [styles.container, cardStyle] }, shadowEnabled && shadowStyle && !isTransparent ? /*#__PURE__*/React.createElement(_reactNative.Animated.View, { style: [styles.shadow, gestureDirection === 'horizontal' ? [styles.shadowHorizontal, styles.shadowLeft] : gestureDirection === 'horizontal-inverted' ? [styles.shadowHorizontal, styles.shadowRight] : gestureDirection === 'vertical' ? [styles.shadowVertical, styles.shadowTop] : [styles.shadowVertical, styles.shadowBottom], { backgroundColor }, shadowStyle], pointerEvents: "none" }) : null, /*#__PURE__*/React.createElement(_CardSheet.default, { ref: this.ref, enabled: pageOverflowEnabled, layout: layout, style: contentStyle }, children)))))); } } exports.default = Card; const getIsModalPresentation = cardStyleInterpolator => { return cardStyleInterpolator === _CardStyleInterpolators.forModalPresentationIOS || // Handle custom modal presentation interpolators as well cardStyleInterpolator.name === 'forModalPresentationIOS'; }; exports.getIsModalPresentation = getIsModalPresentation; const styles = _reactNative.StyleSheet.create({ container: { flex: 1 }, overlay: { flex: 1, backgroundColor: '#000' }, shadow: { position: 'absolute', shadowRadius: 5, shadowColor: '#000', shadowOpacity: 0.3 }, shadowHorizontal: { top: 0, bottom: 0, width: 3, shadowOffset: { width: -1, height: 1 } }, shadowLeft: { left: 0 }, shadowRight: { right: 0 }, shadowVertical: { left: 0, right: 0, height: 3, shadowOffset: { width: 1, height: -1 } }, shadowTop: { top: 0 }, shadowBottom: { bottom: 0 } }); //# sourceMappingURL=Card.js.map