Files
mattermost-mobile/app/components/sidebars/drawer_layout/drawer_layout.js
Mattermost Build 49f86826e1 Update dependencies (#5686) (#5687)
* Update dependencies

* Fix unsigned builds

(cherry picked from commit ada6be9b7a)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-09-22 14:00:45 -03:00

530 lines
16 KiB
JavaScript

/* eslint-disable */
// Original work: https://github.com/react-native-community/react-native-drawer-layout .
// Modified work: Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, { Component } from 'react';
import {
Animated,
Dimensions,
Easing,
Keyboard,
PanResponder,
Platform,
StyleSheet,
TouchableWithoutFeedback,
View,
I18nManager,
} from 'react-native';
import PropTypes from 'prop-types';
const MIN_SWIPE_DISTANCE = 3;
const VX_MAX = 0.1;
const IDLE = 'Idle';
const DRAGGING = 'Dragging';
const SETTLING = 'Settling';
const emptyObject = {};
export default class DrawerLayout extends Component {
static propTypes = {
children: PropTypes.any,
drawerBackgroundColor: PropTypes.string,
drawerLockMode: PropTypes.oneOf(['unlocked', 'locked-closed', 'locked-open']),
drawerPosition: PropTypes.oneOf(['left', 'right']),
drawerWidth: PropTypes.number,
keyboardDismissMode: PropTypes.oneOf(['none', 'on-drag']),
onDrawerClose: PropTypes.func,
onDrawerOpen: PropTypes.func,
onDrawerSlide: PropTypes.func,
onDrawerStateChanged: PropTypes.func,
renderNavigationView: PropTypes.func,
statusBarBackgroundColor: PropTypes.string,
useNativeAnimations: PropTypes.bool,
isTablet: PropTypes.bool,
}
static defaultProps = {
drawerWidth: 0,
drawerPosition: 'left',
useNativeAnimations: false,
isTablet: false,
renderNavigationView: () => true,
};
static positions = {
Left: 'left',
Right: 'right',
};
constructor(props, context) {
super(props, context);
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder: this._shouldSetPanResponder,
onPanResponderGrant: this._panResponderGrant,
onPanResponderMove: this._panResponderMove,
onPanResponderTerminationRequest: () => false,
onPanResponderRelease: this._panResponderRelease,
onPanResponderTerminate: () => {},
});
this.canClose = true;
this.openValue = new Animated.Value(0);
const deviceWidth = parseFloat(Dimensions.get('window').width);
this.state = {
accessibilityViewIsModal: false,
deviceWidth,
drawerShown: false,
threshold: deviceWidth / 2,
};
this.openValue.addListener(this.handleOpenValueChanged);
}
componentDidMount() {
this.dimensionsListener = Dimensions.addEventListener('change', this.handleDimensionsChange);
}
componentWillUnmount() {
if (this.openValue) {
this.openValue.removeListener(this.handleOpenValueChanged);
}
this.dimensionsListener?.remove();
}
handleDimensionsChange = ({window}) => {
const deviceWidth = parseFloat(window.width);
this.setState({
deviceWidth,
threshold: deviceWidth / 2,
});
};
handleOpenValueChanged = ({ value }) => {
const drawerShown = value > 0;
const accessibilityViewIsModal = drawerShown;
if (drawerShown !== this.state.drawerShown) {
this.setState({ drawerShown, accessibilityViewIsModal });
}
if (this.props.keyboardDismissMode === 'on-drag' || drawerShown) {
Keyboard.dismiss();
}
this._lastOpenValue = value;
if (this.props.onDrawerSlide) {
this.props.onDrawerSlide({ nativeEvent: { offset: value } });
}
};
getDrawerPosition() {
const { drawerPosition } = this.props;
const rtl = I18nManager.isRTL;
return rtl
? drawerPosition === 'left' ? 'right' : 'left' // invert it
: drawerPosition;
}
renderDrawer = () => {
if (this.props.isTablet) {
return null;
}
const { accessibilityViewIsModal, drawerShown } = this.state;
const {
drawerBackgroundColor,
drawerWidth,
drawerPosition,
} = this.props;
/**
* We need to use the "original" drawer position here
* as RTL turns position left and right on its own
**/
const dynamicDrawerStyles = {
backgroundColor: drawerBackgroundColor,
width: drawerWidth,
left: drawerPosition === 'left' ? 0 : null,
right: drawerPosition === 'right' ? 0 : null,
};
/* Drawer styles */
let outputRange;
let translateDistance = drawerWidth;
if (Platform.OS === 'ios') {
// ios main sidebar sits mostly under the main screen, with slight move
translateDistance = drawerPosition === 'left' ?
Math.floor(drawerWidth * 0.2) : Math.floor(drawerWidth * 1.2)
}
if (this.getDrawerPosition() === 'left') {
outputRange = [-translateDistance, 0];
} else {
outputRange = [translateDistance, 0];
}
const drawerTranslateX = this.openValue.interpolate({
inputRange: [0, 1],
outputRange,
extrapolate: 'clamp',
});
const animatedDrawerStyles = {
transform: [{ translateX: drawerTranslateX }],
};
// 0 - ios main drawer
// 1 - main | tablet
// 2 - overlay
// 3 - android main drawer | settings drawer
const drawerZIndex = drawerPosition === 'left' ? 0 : 3
return (
<React.Fragment>
<Animated.View
accessibilityViewIsModal={accessibilityViewIsModal}
style={[
StyleFactory.drawer(drawerZIndex),
dynamicDrawerStyles,
animatedDrawerStyles,
]}
>
{this.props.renderNavigationView(drawerWidth)}
</Animated.View>
</React.Fragment>
);
};
renderDrawerForTablet = () => {
const {accessibilityViewIsModal} = this.state;
const {
drawerWidth: width,
isTablet,
} = this.props;
if (isTablet) {
return (
<Animated.View
accessibilityViewIsModal={accessibilityViewIsModal}
style={[styles.tablet, {width}]}
>
{this.props.renderNavigationView(width)}
</Animated.View>
);
}
return null;
};
render() {
const {drawerPosition, drawerWidth, isTablet} = this.props;
const panHandlers = isTablet ? emptyObject : this._panResponder.panHandlers;
const containerStyles = [styles.container];
if (isTablet) {
containerStyles.push(styles.tabletContainer);
}
const mainStyles = [styles.main]
if (drawerPosition === 'left') {
/* Drawer styles */
let outputRange;
if (this.getDrawerPosition() === 'left') {
outputRange = [0, drawerWidth];
} else {
outputRange = [drawerWidth, 0];
}
const drawerTranslateX = this.openValue.interpolate({
inputRange: [0, 1],
outputRange,
extrapolate: 'clamp',
});
const animatedDrawerStyles = {
transform: [{ translateX: drawerTranslateX }],
};
mainStyles.push(animatedDrawerStyles)
}
/* Overlay styles */
const overlayOpacity = this.openValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 0.5],
extrapolate: 'clamp',
});
const animatedOverlayStyles = { opacity: overlayOpacity };
const pointerEvents = this.state.drawerShown ? 'auto' : 'none';
return (
<View
testID={this.props.testID}
style={containerStyles}
{...panHandlers}
>
{this.renderDrawerForTablet()}
<Animated.View style={mainStyles}>
<TouchableWithoutFeedback
pointerEvents={pointerEvents}
onPress={this._onOverlayClick}
>
<Animated.View
pointerEvents={pointerEvents}
style={[styles.overlay, animatedOverlayStyles]}
/>
</TouchableWithoutFeedback>
{this.props.children}
</Animated.View>
{this.renderDrawer()}
</View>
);
}
_onOverlayClick = (e) => {
e.stopPropagation();
if (!this._isLockedClosed() && !this._isLockedOpen()) {
this.closeDrawer();
}
};
_emitStateChanged = (newState) => {
if (this.props.onDrawerStateChanged) {
this.props.onDrawerStateChanged(newState);
}
};
openDrawer = (options = {}) => {
if (!this.props.isTablet) {
this._emitStateChanged(SETTLING);
Animated.timing(this.openValue, {
toValue: 1,
duration: 150,
easing: Easing.out(Easing.cubic),
useNativeDriver: this.props.useNativeAnimations,
...options,
}).start(() => {
this.handleOpenValueChanged({value: 1});
if (this.props.onDrawerOpen) {
this.props.onDrawerOpen();
}
this._emitStateChanged(IDLE);
});
}
};
closeDrawer = (options = {}) => {
this._emitStateChanged(SETTLING);
Animated.timing(this.openValue, {
toValue: 0,
duration: 150,
easing: Easing.out(Easing.cubic),
useNativeDriver: this.props.useNativeAnimations,
...options,
}).start(() => {
this.handleOpenValueChanged({value: 0});
if (this.props.onDrawerClose) {
this.props.onDrawerClose();
this.canClose = true;
}
this._emitStateChanged(IDLE);
});
};
_handleDrawerOpen = () => {
if (this.props.onDrawerOpen) {
this.props.onDrawerOpen();
}
};
_handleDrawerClose = () => {
if (this.props.onDrawerClose) {
this.props.onDrawerClose();
}
};
_shouldSetPanResponder = (e, { moveX, dx, dy }) => {
if (!dx || !dy || Math.abs(dx) < MIN_SWIPE_DISTANCE) {
return false;
}
if (this._isLockedClosed() || this._isLockedOpen() || !this.canClose) {
return false;
}
const {deviceWidth} = this.state;
const overlayArea = deviceWidth -
(deviceWidth - this.props.drawerWidth);
if (this.getDrawerPosition() === 'left') {
if (this._lastOpenValue === 1) {
if (
(dx < 0 && Math.abs(dx) > Math.abs(dy) * 3) ||
moveX > overlayArea
) {
this._isClosing = true;
this._closingAnchorValue = this._getOpenValueForX(moveX);
return true;
}
} else {
const filter = moveX > 0 && dx > 35;
if (filter) {
this._isClosing = false;
return true;
}
return false;
}
} else {
if (this._lastOpenValue === 1) {
if (
(dx > 0 && Math.abs(dx) > Math.abs(dy) * 3) ||
moveX < overlayArea
) {
this._isClosing = true;
this._closingAnchorValue = this._getOpenValueForX(moveX);
return true;
}
} else {
if (moveX >= deviceWidth - 35 && dx < 0) {
this._isClosing = false;
return true;
}
return false;
}
}
};
_panResponderGrant = () => {
this._emitStateChanged(DRAGGING);
};
_panResponderMove = (e, { moveX, dx }) => {
const useDx = this.getDrawerPosition() === 'left' && !this._isClosing;
let openValue = this._getOpenValueForX(useDx ? dx : moveX);
if (this._isClosing) {
openValue = 1 - (this._closingAnchorValue - openValue);
}
if (openValue > 1) {
openValue = 1;
} else if (openValue < 0) {
openValue = 0;
}
this.openValue.setValue(openValue);
};
_panResponderRelease = (e, { moveX, vx }) => {
const {threshold} = this.state;
const previouslyOpen = this._isClosing;
const isWithinVelocityThreshold = vx < VX_MAX && vx > -VX_MAX;
if (this.getDrawerPosition() === 'left') {
if (
(vx > 0 && moveX > threshold) ||
vx >= VX_MAX ||
(isWithinVelocityThreshold &&
previouslyOpen &&
moveX > threshold)
) {
this.openDrawer({ velocity: vx });
} else if (
(vx < 0 && moveX < threshold) ||
vx < -VX_MAX ||
(isWithinVelocityThreshold && !previouslyOpen)
) {
this.closeDrawer({ velocity: vx });
} else if (previouslyOpen) {
this.openDrawer();
} else {
this.closeDrawer();
}
} else {
if (
(vx < 0 && moveX < threshold) ||
vx <= -VX_MAX ||
(isWithinVelocityThreshold &&
previouslyOpen &&
moveX < threshold)
) {
this.openDrawer({ velocity: (-1) * vx });
} else if (
(vx > 0 && moveX > threshold) ||
vx > VX_MAX ||
(isWithinVelocityThreshold && !previouslyOpen)
) {
this.closeDrawer({ velocity: (-1) * vx });
} else if (previouslyOpen) {
this.openDrawer();
} else {
this.closeDrawer();
}
}
};
_isLockedClosed = () => {
return this.props.drawerLockMode === 'locked-closed' &&
!this.state.drawerShown;
};
_isLockedOpen = () => {
return this.props.drawerLockMode === 'locked-open' &&
this.state.drawerShown;
};
_getOpenValueForX(x) {
const { drawerWidth } = this.props;
const { deviceWidth } = this.state;
if (this.getDrawerPosition() === 'left') {
return x / drawerWidth;
}
// position === 'right'
return (deviceWidth - x) / drawerWidth;
}
}
class StyleFactory {
static drawer(zIndex) {
return ({
position: 'absolute',
top: 0,
bottom: 0,
zIndex,
});
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'transparent',
},
tabletContainer: {
flexDirection: 'row',
},
tablet: {
height: '100%',
zIndex: 1,
},
main: {
flex: 1,
zIndex: 1,
position: 'relative', // so overlay pins to this View
},
overlay: {
backgroundColor: '#000',
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
zIndex: 2,
},
});