From 805c73c3bd2fa0e48326361cb44c2b8455ea996e Mon Sep 17 00:00:00 2001 From: colmb Date: Tue, 3 Jul 2018 09:12:32 +1200 Subject: [PATCH 1/3] When the menu position is set to `left` but React Native is running in RTL mode, the menu does not correctly render. It should render at the start of the viewport, which would be on the right in RTL mode. Instead it continues to render on the left. `left` and `right` positions are misleading in this library. The intention of `left` and `right` (as per how its coded) is to behave more like `start` and `end` in React Native. - This commit adds some new menuPosition props options for `start` and `end` which are intended to deprecate `left` and `right`. - It also adds a new function to allow calculating whether we should consider the menuPosition as at the 'start' of the viewport which will be left in LTR mode and right in RTL mode. - It also adds a function which uses this new calculation to decide what side the menu should be rendered on. --- README.md | 2 +- index.js | 67 ++++++++++++++++++++++++++++++++++++---------------- package.json | 2 +- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 7b65b12..2b63bbc 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ class Application extends React.Component { | onChange | none | Function | Callback on menu open/close. Is passed isOpen as an argument | | onMove | none | Function | Callback on menu move. Is passed left as an argument | | onSliding | none | Function | Callback when menu is sliding. It returns a decimal from 0 to 1 which represents the percentage of menu offset between hiddenMenuOffset and openMenuOffset.| -| menuPosition | left | String | either 'left' or 'right' | +| menuPosition | start | String | either 'start' or 'end' ('left' and 'right' supported but deprecated) | | animationFunction | none | (Function -> Object) | Function that accept 2 arguments (prop, value) and return an object:
- `prop` you should use at the place you specify parameter to animate
- `value` you should use to specify the final value of prop | | onAnimationComplete | none | (Function -> Void) | Function that accept 1 optional argument (event):
- `event` you should this to capture the animation event after the animation has successfully completed | | animationStyle | none | (Function -> Object) | Function that accept 1 argument (value) and return an object:
- `value` you should use at the place you need current value of animated parameter (left offset of content view) | diff --git a/index.js b/index.js index 0d481e4..cfe10d8 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ import { Dimensions, Animated, TouchableWithoutFeedback, + I18nManager, } from 'react-native'; import PropTypes from 'prop-types'; import styles from './styles'; @@ -17,7 +18,7 @@ type Props = { edgeHitWidth: number, toleranceX: number, toleranceY: number, - menuPosition: 'left' | 'right', + menuPosition: 'left' | 'right' | 'start' | 'end', onChange: Function, onMove: Function, onSliding: Function, @@ -58,6 +59,22 @@ function shouldOpenMenu(dx: number): boolean { return dx > barrierForward; } +// returns `true` is menu is positioned `left` or `start`, false otherwise. +function isMenuPositionedAtStartOfViewport(menuPosition: string): boolean { + return menuPosition === 'left' || menuPosition === 'start'; +} + +// return 1 multiplier if menu position is `start|left` AND +// LTR or `end|right` AND RTL, return -1 otherwise. +function menuPositionMultiplier(menuPosition) { + const start = isMenuPositionedAtStartOfViewport(menuPosition); + if ((start && !I18nManager.isRTL) || (!start && I18nManager.isRTL)) { + return 1; + } else { + return -1; + } +} + export default class SideMenu extends React.Component { onLayoutChange: Function; onStartShouldSetResponderCapture: Function; @@ -75,7 +92,7 @@ export default class SideMenu extends React.Component { this.prevLeft = 0; this.isOpen = !!props.isOpen; - const initialMenuPositionMultiplier = props.menuPosition === 'right' ? -1 : 1; + const initialMenuPositionMultiplier = menuPositionMultiplier(props.menuPosition); const openOffsetMenuPercentage = props.openMenuOffset / deviceScreen.width; const hiddenMenuOffsetPercentage = props.hiddenMenuOffset / deviceScreen.width; const left: Animated.Value = new Animated.Value( @@ -142,12 +159,12 @@ export default class SideMenu extends React.Component { ); } - const { width, height } = this.state; + const { width, height, left } = this.state; const ref = sideMenu => (this.sideMenu = sideMenu); const style = [ styles.frontView, - { width, height }, - this.props.animationStyle(this.state.left), + { width, height, }, + this.props.animationStyle(left), ]; return ( @@ -159,7 +176,7 @@ export default class SideMenu extends React.Component { } moveLeft(offset: number) { - const newOffset = this.menuPositionMultiplier() * offset; + const newOffset = menuPositionMultiplier(this.props.menuPosition) * offset; this.props .animationFunction(this.state.left, newOffset) @@ -168,16 +185,12 @@ export default class SideMenu extends React.Component { this.prevLeft = newOffset; } - menuPositionMultiplier(): -1 | 1 { - return this.props.menuPosition === 'right' ? -1 : 1; - } - handlePanResponderMove(e: Object, gestureState: Object) { - if (this.state.left.__getValue() * this.menuPositionMultiplier() >= 0) { + if (this.state.left.__getValue() * menuPositionMultiplier(this.props.menuPosition) >= 0) { let newLeft = this.prevLeft + gestureState.dx; if (!this.props.bounceBackOnOverdraw && Math.abs(newLeft) > this.state.openMenuOffset) { - newLeft = this.menuPositionMultiplier() * this.state.openMenuOffset; + newLeft = menuPositionMultiplier(this.props.menuPosition) * this.state.openMenuOffset; } this.props.onMove(newLeft); @@ -186,7 +199,7 @@ export default class SideMenu extends React.Component { } handlePanResponderEnd(e: Object, gestureState: Object) { - const offsetLeft = this.menuPositionMultiplier() * + const offsetLeft = menuPositionMultiplier(this.props.menuPosition) * (this.state.left.__getValue() + gestureState.dx); this.openMenu(shouldOpenMenu(offsetLeft)); @@ -203,11 +216,13 @@ export default class SideMenu extends React.Component { return touchMoved; } - const withinEdgeHitWidth = this.props.menuPosition === 'right' ? + const start = isMenuPositionedAtStartOfViewport(this.props.menuPosition); + // If `right|end` OR `left|start` and RTL then calculate edgeHitWidth using screen width. + const withinEdgeHitWidth = (!start || (start && I18nManager.isRTL)) ? gestureState.moveX > (deviceScreen.width - this.props.edgeHitWidth) : gestureState.moveX < this.props.edgeHitWidth; - const swipingToOpen = this.menuPositionMultiplier() * gestureState.dx > 0; + const swipingToOpen = menuPositionMultiplier(this.props.menuPosition) * gestureState.dx > 0; return withinEdgeHitWidth && touchMoved && swipingToOpen; } @@ -233,10 +248,22 @@ export default class SideMenu extends React.Component { return !disableGestures; } + getBoundryStyleByDirection(): Object { + const boundryEdge = this.state.width - this.state.openMenuOffset; + const start = isMenuPositionedAtStartOfViewport(this.props.menuPosition); + // If the RTL setting matches the menuPosition prop + // value, then return start and end values which are + // responsive to RTL direction for menu boundry. + if (start) { + return { start: 0, end: boundryEdge }; + } + else { + return { end: 0, start: boundryEdge }; + } + } + render(): React.Element { - const boundryStyle = this.props.menuPosition === 'right' ? - { left: this.state.width - this.state.openMenuOffset } : - { right: this.state.width - this.state.openMenuOffset }; + const boundryStyle = this.getBoundryStyleByDirection(); const menu = ( @@ -260,7 +287,7 @@ SideMenu.propTypes = { edgeHitWidth: PropTypes.number, toleranceX: PropTypes.number, toleranceY: PropTypes.number, - menuPosition: PropTypes.oneOf(['left', 'right']), + menuPosition: PropTypes.oneOf(['left', 'right', 'start', 'end']), onChange: PropTypes.func, onMove: PropTypes.func, children: PropTypes.node, @@ -304,4 +331,4 @@ SideMenu.defaultProps = { isOpen: false, bounceBackOnOverdraw: true, autoClosing: true, -}; +}; \ No newline at end of file diff --git a/package.json b/package.json index bf23d4a..b548fe4 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,6 @@ "eslint-plugin-react": "^7.1.0", "flow-bin": "0.49", "react": "16.0.0-alpha.12", - "react-native": "^0.46.3" + "react-native": "^0.51.0" } } From d03b93efa05a2b675eec935919069a7a50299cd0 Mon Sep 17 00:00:00 2001 From: colmb Date: Wed, 4 Jul 2018 21:41:08 +1200 Subject: [PATCH 2/3] Ensure that hiddenMenuOffset prop respects the menuPosition multiplier calculation --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index cfe10d8..6f2a080 100644 --- a/index.js +++ b/index.js @@ -98,7 +98,7 @@ export default class SideMenu extends React.Component { const left: Animated.Value = new Animated.Value( props.isOpen ? props.openMenuOffset * initialMenuPositionMultiplier - : props.hiddenMenuOffset, + : props.hiddenMenuOffset * initialMenuPositionMultiplier ); this.onLayoutChange = this.onLayoutChange.bind(this); From b22a63b7fee93d3f1a51b3e395ccb391f7b9587e Mon Sep 17 00:00:00 2001 From: colmb Date: Mon, 27 Aug 2018 15:24:33 +1200 Subject: [PATCH 3/3] Add ability to recalculate the drawer offsets from layout and state changes. This makes the drawer component responsive. --- index.js | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 6f2a080..bf5adb7 100644 --- a/index.js +++ b/index.js @@ -134,16 +134,46 @@ export default class SideMenu extends React.Component { componentWillReceiveProps(props: Props): void { if (typeof props.isOpen !== 'undefined' && this.isOpen !== props.isOpen && (props.autoClosing || this.isOpen === false)) { this.openMenu(props.isOpen); + } else { + // This below code is taken from an Open PR into React Native Side Menu. + // See https://github.com/react-native-community/react-native-side-menu/pull/356/commits/89bb710a8a2458db4b8163c94d81d38fb9c95927 + const { openMenuOffset, hiddenMenuOffset } = props; + // if openMenuOffset or hiddenMenuOffset has changed + if ((this.state.openMenuOffset != openMenuOffset) || (this.state.hiddenMenuOffset != hiddenMenuOffset)) { + this.setState({ + ...this.state, + openMenuOffset, hiddenMenuOffset + }); + this.moveLeft(this.isOpen ? openMenuOffset : hiddenMenuOffset); + } } } onLayoutChange(e: Event) { - const { width, height } = e.nativeEvent.layout; - const openMenuOffset = width * this.state.openOffsetMenuPercentage; - const hiddenMenuOffset = width * this.state.hiddenMenuOffsetPercentage; - this.setState({ width, height, openMenuOffset, hiddenMenuOffset }); + // This below code is taken from an Open PR into React Native Side Menu. + // https://github.com/react-native-community/react-native-side-menu/pull/343/commits/1bf58bc701a560b3d5221dff762e6730641b16fd + const sizes = e.nativeEvent.layout; + this.changeOffset(sizes); } + changeOffset = ({ width, height }) => { + const { + openMenuOffset = width * DEFAULT_MULTIPLIER, + hiddenMenuOffset, + } = this.props; + const openOffsetMenuPercentage = openMenuOffset / width; + const hiddenMenuOffsetPercentage = hiddenMenuOffset / width; + this.setState({ + width, + height, + openMenuOffset, + hiddenMenuOffset, + openOffsetMenuPercentage, + hiddenMenuOffsetPercentage, + }); + this.moveLeft(this.isOpen ? openMenuOffset : hiddenMenuOffset); + } + /** * Get content view. This view will be rendered over menu * @return {React.Component} @@ -219,7 +249,7 @@ export default class SideMenu extends React.Component { const start = isMenuPositionedAtStartOfViewport(this.props.menuPosition); // If `right|end` OR `left|start` and RTL then calculate edgeHitWidth using screen width. const withinEdgeHitWidth = (!start || (start && I18nManager.isRTL)) ? - gestureState.moveX > (deviceScreen.width - this.props.edgeHitWidth) : + gestureState.moveX > (this.state.width - this.props.edgeHitWidth) : gestureState.moveX < this.props.edgeHitWidth; const swipingToOpen = menuPositionMultiplier(this.props.menuPosition) * gestureState.dx > 0;