UNPKG

react-navigation-drawer

Version:

Drawer navigator component for React Navigation

420 lines (349 loc) 17.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var React = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _reactNativeGestureHandler = require("react-native-gesture-handler"); var _reactNativeReanimated = _interopRequireDefault(require("react-native-reanimated")); var _DrawerProgressContext = _interopRequireDefault(require("../utils/DrawerProgressContext")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (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 _extends() { _extends = Object.assign || 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); } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } const { Clock, Value, onChange, clockRunning, startClock, stopClock, interpolate, spring, abs, add, and, block, call, cond, divide, eq, event, greaterThan, lessThan, max, min, multiply, neq, or, set, sub } = _reactNativeReanimated.default; const TRUE = 1; const FALSE = 0; const NOOP = 0; const UNSET = -1; const PROGRESS_EPSILON = 0.05; const DIRECTION_LEFT = 1; const DIRECTION_RIGHT = -1; const SWIPE_DISTANCE_THRESHOLD_DEFAULT = 60; const SWIPE_DISTANCE_MINIMUM = 5; const SPRING_CONFIG = { stiffness: 1000, damping: 500, mass: 3, overshootClamping: true, restDisplacementThreshold: 0.01, restSpeedThreshold: 0.01 }; class Drawer extends React.PureComponent { constructor(...args) { super(...args); _defineProperty(this, "clock", new Clock()); _defineProperty(this, "isDrawerTypeFront", new Value(this.props.drawerType === 'front' ? TRUE : FALSE)); _defineProperty(this, "isOpen", new Value(this.props.open ? TRUE : FALSE)); _defineProperty(this, "nextIsOpen", new Value(UNSET)); _defineProperty(this, "isSwiping", new Value(FALSE)); _defineProperty(this, "gestureState", new Value(_reactNativeGestureHandler.State.UNDETERMINED)); _defineProperty(this, "touchX", new Value(0)); _defineProperty(this, "velocityX", new Value(0)); _defineProperty(this, "gestureX", new Value(0)); _defineProperty(this, "offsetX", new Value(0)); _defineProperty(this, "position", new Value(0)); _defineProperty(this, "containerWidth", new Value(0)); _defineProperty(this, "drawerWidth", new Value(0)); _defineProperty(this, "drawerOpacity", new Value(0)); _defineProperty(this, "drawerPosition", new Value(this.props.drawerPosition === 'right' ? DIRECTION_RIGHT : DIRECTION_LEFT)); _defineProperty(this, "touchDistanceFromDrawer", cond(this.isDrawerTypeFront, cond(eq(this.drawerPosition, DIRECTION_LEFT), max( // Distance of touch start from left screen edge - Drawer width sub(sub(this.touchX, this.gestureX), this.drawerWidth), 0), min(multiply( // Distance of drawer from left screen edge - Touch start point sub(sub(this.containerWidth, this.drawerWidth), sub(this.touchX, this.gestureX)), DIRECTION_RIGHT), 0)), 0)); _defineProperty(this, "swipeDistanceThreshold", new Value(this.props.swipeDistanceThreshold !== undefined ? this.props.swipeDistanceThreshold : SWIPE_DISTANCE_THRESHOLD_DEFAULT)); _defineProperty(this, "swipeVelocityThreshold", new Value(this.props.swipeVelocityThreshold)); _defineProperty(this, "currentOpenValue", this.props.open); _defineProperty(this, "pendingOpenValue", void 0); _defineProperty(this, "isStatusBarHidden", false); _defineProperty(this, "manuallyTriggerSpring", new Value(FALSE)); _defineProperty(this, "transitionTo", isOpen => { const toValue = new Value(0); const frameTime = new Value(0); const state = { position: this.position, time: new Value(0), finished: new Value(FALSE), velocity: new Value(0) }; return block([cond(clockRunning(this.clock), NOOP, [// Animation wasn't running before // Set the initial values and start the clock set(toValue, multiply(isOpen, this.drawerWidth, this.drawerPosition)), set(frameTime, 0), set(state.time, 0), set(state.finished, FALSE), set(state.velocity, this.velocityX), set(this.isOpen, isOpen), startClock(this.clock), set(this.manuallyTriggerSpring, FALSE)]), spring(this.clock, state, { ...SPRING_CONFIG, toValue }), cond(state.finished, [// Reset gesture and velocity from previous gesture set(this.touchX, 0), set(this.gestureX, 0), set(this.velocityX, 0), set(this.offsetX, 0), // When the animation finishes, stop the clock stopClock(this.clock), call([this.isOpen], ([value]) => { const open = Boolean(value); if (open !== this.props.open) { // Sync drawer's state after animation finished // This shouldn't be necessary, but there seems to be an issue on iOS this.toggleDrawer(this.props.open); } })])]); }); _defineProperty(this, "dragX", block([onChange(this.isOpen, call([this.isOpen], ([value]) => { const open = Boolean(value); this.currentOpenValue = open; // Without this check, the drawer can go to an infinite update <-> animate loop for sync updates if (open !== this.props.open) { // If the mode changed, update state if (open) { this.props.onOpen(); } else { this.props.onClose(); } this.pendingOpenValue = open; // Force componentDidUpdate to fire, whether user does a setState or not // This allows us to detect when the user drops the update and revert back // It's necessary to make sure that the state stays in sync this.forceUpdate(); } })), onChange(this.nextIsOpen, cond(neq(this.nextIsOpen, UNSET), [// Stop any running animations cond(clockRunning(this.clock), stopClock(this.clock)), // Update the open value to trigger the transition set(this.isOpen, this.nextIsOpen), set(this.gestureX, 0), set(this.nextIsOpen, UNSET)])), // This block must be after the this.isOpen listener since we check for current value onChange(this.isSwiping, // Listen to updates for this value only when it changes // Without `onChange`, this will fire even if the value didn't change // We don't want to call the listeners if the value didn't change call([this.isSwiping], ([value]) => { const { keyboardDismissMode } = this.props; if (value === TRUE) { if (keyboardDismissMode === 'on-drag') { _reactNative.Keyboard.dismiss(); } this.toggleStatusBar(true); } else { this.toggleStatusBar(this.currentOpenValue); } })), cond(eq(this.gestureState, _reactNativeGestureHandler.State.ACTIVE), [cond(this.isSwiping, NOOP, [// We weren't dragging before, set it to true set(this.isSwiping, TRUE), // Also update the drag offset to the last position set(this.offsetX, this.position)]), // Update position with previous offset + gesture distance set(this.position, add(this.offsetX, this.gestureX, this.touchDistanceFromDrawer)), // Stop animations while we're dragging stopClock(this.clock)], [set(this.isSwiping, FALSE), set(this.touchX, 0), this.transitionTo(cond(this.manuallyTriggerSpring, this.isOpen, cond(or(and(greaterThan(abs(this.gestureX), SWIPE_DISTANCE_MINIMUM), greaterThan(abs(this.velocityX), this.swipeVelocityThreshold)), greaterThan(abs(this.gestureX), this.swipeDistanceThreshold)), cond(eq(this.drawerPosition, DIRECTION_LEFT), // If swiped to right, open the drawer, otherwise close it greaterThan(cond(eq(this.velocityX, 0), this.gestureX, this.velocityX), 0), // If swiped to left, open the drawer, otherwise close it lessThan(cond(eq(this.velocityX, 0), this.gestureX, this.velocityX), 0)), this.isOpen)))]), this.position])); _defineProperty(this, "translateX", cond(eq(this.drawerPosition, DIRECTION_RIGHT), min(max(multiply(this.drawerWidth, -1), this.dragX), 0), max(min(this.drawerWidth, this.dragX), 0))); _defineProperty(this, "progress", cond( // Check if the drawer width is available to avoid division by zero eq(this.drawerWidth, 0), 0, abs(divide(this.translateX, this.drawerWidth)))); _defineProperty(this, "handleGestureEvent", event([{ nativeEvent: { x: this.touchX, translationX: this.gestureX, velocityX: this.velocityX } }])); _defineProperty(this, "handleGestureStateChange", event([{ nativeEvent: { state: s => set(this.gestureState, s) } }])); _defineProperty(this, "handleTapStateChange", event([{ nativeEvent: { oldState: s => cond(eq(s, _reactNativeGestureHandler.State.ACTIVE), set(this.manuallyTriggerSpring, TRUE)) } }])); _defineProperty(this, "handleContainerLayout", e => this.containerWidth.setValue(e.nativeEvent.layout.width)); _defineProperty(this, "handleDrawerLayout", e => { this.drawerWidth.setValue(e.nativeEvent.layout.width); this.toggleDrawer(this.props.open); // Until layout is available, drawer is hidden with opacity: 0 by default // Show it in the next frame when layout is available // If we don't delay it until the next frame, there's a visible flicker requestAnimationFrame(() => requestAnimationFrame(() => this.drawerOpacity.setValue(1))); }); _defineProperty(this, "toggleDrawer", open => { if (this.currentOpenValue !== open) { this.nextIsOpen.setValue(open ? TRUE : FALSE); // This value will also be set shortly after as changing this.nextIsOpen changes this.isOpen // However, there's a race condition on Android, so we need to set a bit earlier this.currentOpenValue = open; } }); _defineProperty(this, "toggleStatusBar", hidden => { const { hideStatusBar, statusBarAnimation } = this.props; if (hideStatusBar && this.isStatusBarHidden !== hidden) { this.isStatusBarHidden = hidden; _reactNative.StatusBar.setHidden(hidden, statusBarAnimation); } }); } componentDidUpdate(prevProps) { const { open, drawerPosition, drawerType, swipeDistanceThreshold, swipeVelocityThreshold, hideStatusBar } = this.props; if ( // If we're not in the middle of a transition, sync the drawer's open state typeof this.pendingOpenValue !== 'boolean' || open !== this.pendingOpenValue) { this.toggleDrawer(open); } this.pendingOpenValue = undefined; if (open !== prevProps.open && hideStatusBar) { this.toggleStatusBar(open); } if (prevProps.drawerPosition !== drawerPosition) { this.drawerPosition.setValue(drawerPosition === 'right' ? DIRECTION_RIGHT : DIRECTION_LEFT); } if (prevProps.drawerType !== drawerType) { this.isDrawerTypeFront.setValue(drawerType === 'front' ? TRUE : FALSE); } if (prevProps.swipeDistanceThreshold !== swipeDistanceThreshold) { this.swipeDistanceThreshold.setValue(swipeDistanceThreshold !== undefined ? swipeDistanceThreshold : SWIPE_DISTANCE_THRESHOLD_DEFAULT); } if (prevProps.swipeVelocityThreshold !== swipeVelocityThreshold) { this.swipeVelocityThreshold.setValue(swipeVelocityThreshold); } } componentWillUnmount() { this.toggleStatusBar(false); } render() { const { open, gestureEnabled, drawerPosition, drawerType, swipeEdgeWidth, sceneContainerStyle, drawerStyle, overlayStyle, onGestureRef, renderDrawerContent, renderSceneContent, gestureHandlerProps } = this.props; const right = drawerPosition === 'right'; const contentTranslateX = drawerType === 'front' ? 0 : this.translateX; const drawerTranslateX = drawerType === 'back' ? _reactNative.I18nManager.isRTL ? multiply(this.drawerWidth, DIRECTION_RIGHT) : this.drawerWidth : this.translateX; const offset = _reactNative.I18nManager.isRTL ? '100%' : multiply(this.drawerWidth, -1); // FIXME: Currently hitSlop is broken when on Android when drawer is on right // https://github.com/kmagiera/react-native-gesture-handler/issues/569 const hitSlop = right ? // Extend hitSlop to the side of the screen when drawer is closed // This lets the user drag the drawer from the side of the screen { right: 0, width: open ? undefined : swipeEdgeWidth } : { left: 0, width: open ? undefined : swipeEdgeWidth }; return /*#__PURE__*/React.createElement(_DrawerProgressContext.default.Provider, { value: this.progress }, /*#__PURE__*/React.createElement(_reactNativeGestureHandler.PanGestureHandler, _extends({ ref: onGestureRef, activeOffsetX: [-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM], failOffsetY: [-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM], onGestureEvent: this.handleGestureEvent, onHandlerStateChange: this.handleGestureStateChange, hitSlop: hitSlop, enabled: gestureEnabled }, gestureHandlerProps), /*#__PURE__*/React.createElement(_reactNativeReanimated.default.View, { onLayout: this.handleContainerLayout, style: styles.main }, /*#__PURE__*/React.createElement(_reactNativeReanimated.default.View, { style: [styles.content, { transform: [{ translateX: contentTranslateX }] }, sceneContainerStyle], importantForAccessibility: open ? 'no-hide-descendants' : 'yes' }, renderSceneContent({ progress: this.progress }), /*#__PURE__*/React.createElement(_reactNativeGestureHandler.TapGestureHandler, { enabled: gestureEnabled, onHandlerStateChange: this.handleTapStateChange }, /*#__PURE__*/React.createElement(_reactNativeReanimated.default.View, { style: [styles.overlay, { opacity: interpolate(this.progress, { inputRange: [PROGRESS_EPSILON, 1], outputRange: [0, 1] }), // We don't want the user to be able to press through the overlay when drawer is open // One approach is to adjust the pointerEvents based on the progress // But we can also send the overlay behind the screen, which works, and is much less code zIndex: cond(greaterThan(this.progress, PROGRESS_EPSILON), 0, -1) }, overlayStyle] }))), /*#__PURE__*/React.createElement(_reactNativeReanimated.default.Code, { exec: block([onChange(this.manuallyTriggerSpring, [cond(eq(this.manuallyTriggerSpring, TRUE), [set(this.nextIsOpen, FALSE), call([], () => this.currentOpenValue = false)])])]) }), /*#__PURE__*/React.createElement(_reactNativeReanimated.default.View, { accessibilityViewIsModal: open, removeClippedSubviews: _reactNative.Platform.OS !== 'ios', onLayout: this.handleDrawerLayout, style: [styles.container, right ? { right: offset } : { left: offset }, { transform: [{ translateX: drawerTranslateX }], opacity: this.drawerOpacity, zIndex: drawerType === 'back' ? -1 : 0 }, drawerStyle] }, renderDrawerContent({ progress: this.progress }))))); } } exports.default = Drawer; _defineProperty(Drawer, "defaultProps", { gestureEnabled: true, drawerPostion: _reactNative.I18nManager.isRTL ? 'left' : 'right', drawerType: 'front', swipeEdgeWidth: 32, swipeVelocityThreshold: 500, keyboardDismissMode: 'on-drag', hideStatusBar: false, statusBarAnimation: 'slide' }); const styles = _reactNative.StyleSheet.create({ container: { backgroundColor: 'white', position: 'absolute', top: 0, bottom: 0, width: '80%', maxWidth: '100%' }, overlay: { ..._reactNative.StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.5)' }, content: { flex: 1 }, main: { flex: 1, overflow: 'hidden' } }); //# sourceMappingURL=Drawer.js.map