UNPKG

react-native-navigation-bottom-sheet

Version:

A performant customizable bottom sheet component made on top of wix react-native-navigation library.

474 lines (368 loc) 21.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var React = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _reactNativeNavigation = require("react-native-navigation"); var _reactNativeReanimated = _interopRequireDefault(require("react-native-reanimated")); var _reactNativeGestureHandler = require("react-native-gesture-handler"); var _events = require("./events"); var _utility = require("./utility"); var _AnimatedStoreScrolling = require("./AnimatedStoreScrolling"); var _AnimatedStoreSheet = require("./AnimatedStoreSheet"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 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 _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 { call, cond, greaterThan, lessThan, neq, clockRunning, abs, not, and, set, sub, or, stopClock, lessOrEq, proc, add, max, min, eq, multiply, block, onChange } = _reactNativeReanimated.default; const { interpolate: interpolateDeprecated, // @ts-ignore: this property is only present in Reanimated 2 interpolateNode } = _reactNativeReanimated.default; // @ts-ignore const interpolate = interpolateNode !== null && interpolateNode !== void 0 ? interpolateNode : interpolateDeprecated; const screenHeight = _reactNative.Dimensions.get('window').height; class BottomSheet extends React.Component { /* BottomSheet state */ /** -------------------------------------------- */ /** Declaration of variables */ /** -------------------------------------------- */ /* Numerical values of snap points which are provided in props */ /* Flag indicating that a command to close the bottom sheet was initiated. */ /* Animated value responsible for an animation of the bottom sheet */ /* Clock used for snapping animations of the bottom sheet */ /* Snap points mapped to Animated values */ /* Value of a current dragging postion */ /* Value of a current speed of dragging */ /* State of a gesture */ /* Basically last snap point */ /* A flag forcing an animation to run; variables below define the animation */ /* Forcefully set starting point of a spring animation */ /* Forcefully set destination point of a spring animation */ /* Gesture mapping */ /* Opacity of the view outside of the bottom sheet */ /* @ts-ignore Dragging animation for bottom sheet, calculated as offset + dragging value */ constructor(props) { var _this$props$enabledCo; super(props); _defineProperty(this, "state", { heightOfHeader: 0, contentHeight: new _reactNativeReanimated.default.Value(0), screenHeight: screenHeight }); _defineProperty(this, "snapPoints", (0, _utility.normalizeSnapPoints)(this.props.snapPoints, screenHeight)); _defineProperty(this, "topSnap", this.snapPoints[this.snapPoints.length - 1]); _defineProperty(this, "closed", false); _defineProperty(this, "_masterTranslateY", void 0); _defineProperty(this, "_clock", new _reactNativeReanimated.default.Clock()); _defineProperty(this, "_snapPoints", []); _defineProperty(this, "_dragY", new _reactNativeReanimated.default.Value(0)); _defineProperty(this, "_velocityY", new _reactNativeReanimated.default.Value(0)); _defineProperty(this, "_panState", new _reactNativeReanimated.default.Value(_reactNativeGestureHandler.State.END)); _defineProperty(this, "_lastBottomSheetHeight", new _reactNativeReanimated.default.Value(this.snapPoints[this.props.initialSnapIndex ? this.props.initialSnapIndex : this.snapPoints.length - 1])); _defineProperty(this, "_forcedSet", new _reactNativeReanimated.default.Value(1)); _defineProperty(this, "_startSnapPoint", new _reactNativeReanimated.default.Value(screenHeight - this.snapPoints[0])); _defineProperty(this, "_endSnapPoint", new _reactNativeReanimated.default.Value(screenHeight - this.snapPoints[this.props.initialSnapIndex ? this.props.initialSnapIndex : this.snapPoints.length - 1])); _defineProperty(this, "_resetValues", new _reactNativeReanimated.default.Value(1)); _defineProperty(this, "_onGestureEvent", _reactNativeReanimated.default.event([{ nativeEvent: { translationY: this._dragY, velocityY: this._velocityY, state: this._panState } }])); _defineProperty(this, "_masterOpacity", void 0); _defineProperty(this, "_draggingAnimation", interpolate(_reactNativeReanimated.default.add(_reactNativeReanimated.default.sub(screenHeight, this._lastBottomSheetHeight), add(this._dragY, _AnimatedStoreSheet.AnimatedStoreSheet._scrollToDragVal)), { inputRange: [screenHeight - this.snapPoints[this.snapPoints.length - 1], screenHeight - this.snapPoints[0]], outputRange: [screenHeight - this.snapPoints[this.snapPoints.length - 1], screenHeight - this.snapPoints[0]], extrapolate: _reactNativeReanimated.default.Extrapolate.CLAMP })); _defineProperty(this, "handleTapEvent", ({ event }) => {// if (!this.props.enabledContentGestureInteraction) { // this._velocityScrollY.setValue(0); // this._velocityY.setValue(0); // } // console.log('Tapped'); }); _defineProperty(this, "unsubscribeDismissBottomSheet", void 0); _defineProperty(this, "unsubscribeSnapTo", void 0); _defineProperty(this, "unsubscribeNavigationListener", void 0); _defineProperty(this, "unsubscribeNavigationBackButtonListener", void 0); _defineProperty(this, "registerListeners", () => { this.unsubscribeSnapTo = (0, _events.listen)('BOTTOM_SHEET_SNAP_TO', index => { this.snapTo(screenHeight - this.snapPoints[index]); }); // Executes when the drawer needs to be dismissed this.unsubscribeDismissBottomSheet = (0, _events.listen)('DISMISS_BOTTOM_SHEET', () => { this.closeBottomSheet(); }); }); _defineProperty(this, "removeListeners", () => { if (this.unsubscribeNavigationListener) this.unsubscribeNavigationListener.remove(); if (this.unsubscribeNavigationBackButtonListener) this.unsubscribeNavigationBackButtonListener.remove(); if (this.unsubscribeSnapTo) this.unsubscribeSnapTo(); if (this.unsubscribeDismissBottomSheet) this.unsubscribeDismissBottomSheet(); }); _defineProperty(this, "touchedOutside", () => { const { dismissWhenTouchOutside } = this.props; if (dismissWhenTouchOutside) { this.closeBottomSheet(); } }); _defineProperty(this, "snapTo", endPoint => { this._endSnapPoint.setValue(endPoint); this._startSnapPoint.setValue(_reactNativeReanimated.default.add(_reactNativeReanimated.default.sub(screenHeight, this._lastBottomSheetHeight), this._dragY)); this._forcedSet.setValue(1); }); _defineProperty(this, "closeBottomSheet", () => { if (this.closed) return; this.closed = true; this.snapTo(screenHeight - this.snapPoints[0]); (0, _events.dispatch)('MARK_CLOSED'); setTimeout(() => { _reactNativeNavigation.Navigation.dismissModal(this.props.componentId); }, 250); }); for (let i = 0; i < this.snapPoints.length; i++) { this._snapPoints.push(new _reactNativeReanimated.default.Value(this.snapPoints[i])); } if (props.animationConfig) (0, _utility.overrideConfig)(props.animationConfig); _AnimatedStoreScrolling.AnimatedStoreScrolling.init((_this$props$enabledCo = this.props.enabledContentGestureInteraction) !== null && _this$props$enabledCo !== void 0 ? _this$props$enabledCo : true, this.snapPoints); // animated_scroll_store._wasStarted.setValue(1); const distance = _reactNativeReanimated.default.proc((bottomPoint, upperPoint) => abs(sub(bottomPoint, upperPoint))); const toss = props.animationConfig && props.animationConfig.toss !== undefined ? props.animationConfig.toss : 0.28; const actualVelocity = cond(eq(_AnimatedStoreScrolling.AnimatedStoreScrolling._lastState, 2), add(_AnimatedStoreSheet.AnimatedStoreSheet._velocityY, this._velocityY), this._velocityY); const currentPoint = sub(this._lastBottomSheetHeight, add(this._dragY, _AnimatedStoreSheet.AnimatedStoreSheet._scrollToDragVal), multiply(toss, actualVelocity)); const rememberAndReturn = _reactNativeReanimated.default.proc(value => block([set(this._lastBottomSheetHeight, value), value])); /** * Recursive function finding closest snap point. */ const getClosestSnapPoint = (index = 0) => { return index === this._snapPoints.length - 1 ? rememberAndReturn(this._snapPoints[index]) : cond(greaterThan(distance(this._snapPoints[index + 1], currentPoint), distance(currentPoint, this._snapPoints[index])), rememberAndReturn(this._snapPoints[index]), getClosestSnapPoint(index + 1)); }; // Does not let the bottom sheet to go higher the highest snap point // and below the lowest snap point const limitedAnimatedValue = value => { return cond(eq(this._lastBottomSheetHeight, this._snapPoints[0]), min(value, sub(screenHeight, this.snapPoints[0])), max(value, sub(screenHeight, this.snapPoints[this.snapPoints.length - 1]))); }; const generalDebug = (val, msg) => { console.log(msg); console.log(val[0]); }; const snapPointUpdated = snapPoint => { if (this.props.onChange) { for (let i = 0; i < this.snapPoints.length; i++) if (this.snapPoints[i] === snapPoint[0]) { this.props.onChange(i); break; } } if (snapPoint[0] === 0) this.closeBottomSheet(); }; const isRun = new _reactNativeReanimated.default.Value(0); const storedResult = new _reactNativeReanimated.default.Value(0); /** * Run a function finding a snap point only once. Save result in storedResult variable. */ const runOnce = func => { return cond(isRun, storedResult, [set(storedResult, func), set(isRun, 1), set(this._dragY, 0), cond(eq(_AnimatedStoreScrolling.AnimatedStoreScrolling._lastState, 2), set(this._velocityY, 0)), set(_AnimatedStoreSheet.AnimatedStoreSheet._scrollToDragVal, 0), set(_AnimatedStoreSheet.AnimatedStoreSheet._velocityY, 0), storedResult]); }; const updateWhenFinished = block([cond(this._forcedSet, [set(this._lastBottomSheetHeight, sub(screenHeight, this._endSnapPoint)), set(this._forcedSet, 0)])]); const currentMovingPoint = new _reactNativeReanimated.default.Value(0); /** * Stop the spring animation and reset some values. */ const __stopClock = block([set(this._lastBottomSheetHeight, sub(screenHeight, currentMovingPoint)), updateWhenFinished, set(this._velocityY, 0), set(_AnimatedStoreScrolling.AnimatedStoreScrolling._velocityScrollY, 0), set(isRun, 0), _reactNativeReanimated.default.stopClock(this._clock)]); const runSpringAnimation = new _reactNativeReanimated.default.Value(0); this._masterTranslateY = _reactNativeReanimated.default.block([ /** * Debugging section. */ onChange(this._lastBottomSheetHeight, call([this._lastBottomSheetHeight], snapPointUpdated)), // onChange( // ASBS._scrollToDragVal, // call([ASBS._scrollToDragVal], (snapPoints: readonly number[]) => // console.log('Changed scrollToDrag: ' + snapPoints[0]) // ) // ), // onChange( // this._velocityY, // call([this._velocityY], (val: any) => generalDebug(val ,"Velocity: ")), // ), /** * Main code section. */ cond(this._resetValues, [set(_AnimatedStoreSheet.AnimatedStoreSheet._scrollToDragVal, 0), set(_AnimatedStoreScrolling.AnimatedStoreScrolling._panScrollState, _reactNativeGestureHandler.State.END), set(_AnimatedStoreScrolling.AnimatedStoreScrolling._scrollY, 0), set(_AnimatedStoreScrolling.AnimatedStoreScrolling._velocityScrollY, 0), set(_AnimatedStoreScrolling.AnimatedStoreScrolling._prevTransY, 0), set(_AnimatedStoreScrolling.AnimatedStoreScrolling._transY, 0), set(this._resetValues, 0)]), /** * Some values do not get reset for some reason, TODO: investigate why * so we reset at the beginning stage when gesture has not * been identified yet. */ cond(or(eq(_AnimatedStoreScrolling.AnimatedStoreScrolling._panScrollState, _reactNativeGestureHandler.State.BEGAN), eq(this._panState, _reactNativeGestureHandler.State.BEGAN)), [set(this._velocityY, 0), set(_AnimatedStoreScrolling.AnimatedStoreScrolling._velocityScrollY, 0), set(_AnimatedStoreSheet.AnimatedStoreSheet._scrollToDragVal, 0)]), /** * Run spring animation when there are no gestures active or * when there is a forced call to run the animation. */ set(runSpringAnimation, or(and(or(eq(this._panState, _reactNativeGestureHandler.State.END), eq(this._panState, _reactNativeGestureHandler.State.CANCELLED), eq(this._panState, _reactNativeGestureHandler.State.FAILED)), or(eq(_AnimatedStoreScrolling.AnimatedStoreScrolling._panScrollState, _reactNativeGestureHandler.State.END), eq(_AnimatedStoreScrolling.AnimatedStoreScrolling._panScrollState, _reactNativeGestureHandler.State.CANCELLED), eq(_AnimatedStoreScrolling.AnimatedStoreScrolling._panScrollState, _reactNativeGestureHandler.State.FAILED))), and(greaterThan(this._forcedSet, 0), neq(this._panState, _reactNativeGestureHandler.State.ACTIVE)))), /** * Determines the current state: whether we need to scroll content or * drag the sheet. */ cond(lessThan(sub(this._snapPoints[this._snapPoints.length - 1], sub(this._lastBottomSheetHeight, _AnimatedStoreSheet.AnimatedStoreSheet._scrollToDragVal, this._dragY)), 0.1), cond(and(greaterThan(_AnimatedStoreScrolling.AnimatedStoreScrolling._velocityScrollY, 0), eq(_AnimatedStoreScrolling.AnimatedStoreScrolling._transY, 0)), [set(_AnimatedStoreScrolling.AnimatedStoreScrolling._lastState, 2)], set(_AnimatedStoreScrolling.AnimatedStoreScrolling._lastState, 1)), set(_AnimatedStoreScrolling.AnimatedStoreScrolling._lastState, 2)), /** * We are always at dragging state when dragging the header. */ cond(or(eq(this._panState, _reactNativeGestureHandler.State.ACTIVE), eq(this._panState, _reactNativeGestureHandler.State.BEGAN)), [set(_AnimatedStoreScrolling.AnimatedStoreScrolling._lastState, 2), stopClock(_AnimatedStoreScrolling.AnimatedStoreScrolling._scrollingClock)]), /** * To cover the special case (when starting dragging at the point that is not at the top), * we update utility variable ASS._startedAtTheTop. */ cond(eq(this._lastBottomSheetHeight, this.topSnap), set(_AnimatedStoreScrolling.AnimatedStoreScrolling._startedAtTheTop, 1), set(_AnimatedStoreScrolling.AnimatedStoreScrolling._startedAtTheTop, 0)), cond(runSpringAnimation, [(0, _utility.runSpring)(this._clock, limitedAnimatedValue(cond(this._forcedSet, this._startSnapPoint, add(sub(screenHeight, this._lastBottomSheetHeight), add(this._dragY, _AnimatedStoreSheet.AnimatedStoreSheet._scrollToDragVal)))), cond(this._forcedSet, this._endSnapPoint, sub(screenHeight, runOnce(getClosestSnapPoint()))), actualVelocity, updateWhenFinished, isRun, currentMovingPoint)], [cond(clockRunning(this._clock), __stopClock), this._draggingAnimation])]); // @ts-ignore this._masterOpacity = interpolate(this._masterTranslateY, { inputRange: [screenHeight - this.snapPoints[this.snapPoints.length - 1], screenHeight - this.snapPoints[0]], outputRange: [this.props.fadeOpacity, 0], extrapolate: _reactNativeReanimated.default.Extrapolate.CLAMP }); this.unsubscribeNavigationListener = _reactNativeNavigation.Navigation.events().bindComponent(this); this.unsubscribeNavigationBackButtonListener = _reactNativeNavigation.Navigation.events().registerNavigationButtonPressedListener(event => { if (event.buttonId === 'RNN.hardwareBackButton' || event.buttonId === 'RNN.hardwareBack') { this.closeBottomSheet(); } }); this.unsubscribeSnapTo = (0, _events.listen)('BOTTOM_SHEET_SNAP_TO', index => { this.snapTo(screenHeight - this.snapPoints[index]); }); // Executes when the drawer needs to be dismissed this.unsubscribeDismissBottomSheet = (0, _events.listen)('DISMISS_BOTTOM_SHEET', () => { this.closeBottomSheet(); }); } /** -------------------------------------------- */ /** Class methods */ /** -------------------------------------------- */ /** * [ react-native-navigation method. ] * * Executed when the component is navigated to view. */ componentDidAppear() {// this.registerListeners(); } /** * [ react-native-navigation method. ] * * Executed when the component is navigated away from view. */ componentDidDisappear() {} componentWillUnmount() { this.removeListeners(); } /** * Registers all the listenrs for this component */ render() { /** Props */ const { style, snapPoints } = this.props; return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(_reactNativeGestureHandler.TapGestureHandler, { maxDurationMs: 100000, onHandlerStateChange: this.handleTapEvent }, /*#__PURE__*/React.createElement(_reactNative.View, { style: styles.containerStyle }, /*#__PURE__*/React.createElement(_reactNative.TouchableWithoutFeedback, { onPress: this.touchedOutside }, /*#__PURE__*/React.createElement(_reactNativeReanimated.default.View, { style: [styles.overlayStyle, { opacity: this._masterOpacity }] })), /*#__PURE__*/React.createElement(_reactNativeReanimated.default.View, { style: [{ backgroundColor: this.props.backgroundColor, borderTopLeftRadius: this.props.borderRadius, borderTopRightRadius: this.props.borderRadius, transform: [{ translateY: this._masterTranslateY }], height: this.topSnap, width: '100%' }, style] }, /*#__PURE__*/React.createElement(_reactNativeGestureHandler.PanGestureHandler, { onGestureEvent: this._onGestureEvent, onHandlerStateChange: this._onGestureEvent }, /*#__PURE__*/React.createElement(_reactNativeReanimated.default.View, { onLayout: _AnimatedStoreScrolling.AnimatedStoreScrolling.handleLayoutHeader, style: { zIndex: 101 } }, this.props.renderHeader && this.props.renderHeader())), /*#__PURE__*/React.createElement(_reactNativeReanimated.default.View, { style: { overflow: 'hidden', width: '100%', height: '100%', borderTopLeftRadius: this.props.borderRadius, borderTopRightRadius: this.props.borderRadius } }, /*#__PURE__*/React.createElement(_reactNativeGestureHandler.PanGestureHandler, { onGestureEvent: _AnimatedStoreScrolling.AnimatedStoreScrolling._onGestureEventScrolling, onHandlerStateChange: _AnimatedStoreScrolling.AnimatedStoreScrolling._onGestureEventScrolling }, /*#__PURE__*/React.createElement(_reactNativeReanimated.default.View, { style: { height: '100%' } }, /*#__PURE__*/React.createElement(_reactNativeReanimated.default.View, { onLayout: _AnimatedStoreScrolling.AnimatedStoreScrolling.handleLayoutContent, style: [{ transform: [{ translateY: _AnimatedStoreScrolling.AnimatedStoreScrolling._masterScrollY }] }] }, this.props.renderContent && this.props.renderContent())))))))); } } _defineProperty(BottomSheet, "defaultProps", { snapPoints: [0, 100, 300, 500], backgroundColor: '#FFF', dismissWhenTouchOutside: true, fadeOpacity: 0.7, borderRadius: 0, enabledContentGestureInteraction: true }); var _default = (0, _reactNativeGestureHandler.gestureHandlerRootHOC)(BottomSheet); exports.default = _default; const styles = _reactNative.StyleSheet.create({ containerStyle: { flex: 1, flexDirection: 'row', justifyContent: "center" }, overlayStyle: { position: 'absolute', width: '100%', height: '100%', backgroundColor: '#000' } }); //# sourceMappingURL=BottomSheet.js.map