Skip to content
This repository has been archived by the owner on Sep 13, 2020. It is now read-only.

Address RTL issues with SideMenu library #355

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <br /> - `prop` you should use at the place you specify parameter to animate <br /> - `value` you should use to specify the final value of prop |
| onAnimationComplete | none | (Function -> Void) | Function that accept 1 optional argument (event): <br /> - `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: <br /> - `value` you should use at the place you need current value of animated parameter (left offset of content view) |
Expand Down
109 changes: 83 additions & 26 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Dimensions,
Animated,
TouchableWithoutFeedback,
I18nManager,
} from 'react-native';
import PropTypes from 'prop-types';
import styles from './styles';
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -75,13 +92,13 @@ 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(
props.isOpen
? props.openMenuOffset * initialMenuPositionMultiplier
: props.hiddenMenuOffset,
: props.hiddenMenuOffset * initialMenuPositionMultiplier
);

this.onLayoutChange = this.onLayoutChange.bind(this);
Expand Down Expand Up @@ -117,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}
Expand All @@ -142,12 +189,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 (
Expand All @@ -159,7 +206,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)
Expand All @@ -168,16 +215,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);
Expand All @@ -186,7 +229,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));
Expand All @@ -203,11 +246,13 @@ export default class SideMenu extends React.Component {
return touchMoved;
}

const withinEdgeHitWidth = this.props.menuPosition === 'right' ?
gestureState.moveX > (deviceScreen.width - this.props.edgeHitWidth) :
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 > (this.state.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;
}

Expand All @@ -233,10 +278,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<void, void> {
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 = (
<View style={[styles.menu, boundryStyle]}>
Expand All @@ -260,7 +317,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,
Expand Down Expand Up @@ -304,4 +361,4 @@ SideMenu.defaultProps = {
isOpen: false,
bounceBackOnOverdraw: true,
autoClosing: true,
};
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}