Skip to content

Commit

Permalink
fix: dismiss menu on window layout change. closes #1005 (#1026)
Browse files Browse the repository at this point in the history
  • Loading branch information
satya164 authored and Trancever committed Apr 25, 2019
1 parent e6d8ccf commit 1472877
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 37 deletions.
14 changes: 3 additions & 11 deletions src/components/Appbar/AppbarHeader.js
@@ -1,11 +1,12 @@
/* @flow */

import * as React from 'react';
import { View, Platform, SafeAreaView, StyleSheet } from 'react-native';
import { View, SafeAreaView, StyleSheet } from 'react-native';

import Appbar, { DEFAULT_APPBAR_HEIGHT } from './Appbar';
import shadow from '../../styles/shadow';
import { withTheme } from '../../core/theming';
import { APPROX_STATUSBAR_HEIGHT } from '../../constants';
import type { Theme } from '../../types';

type Props = React.ElementConfig<typeof Appbar> & {|
Expand All @@ -31,15 +32,6 @@ type Props = React.ElementConfig<typeof Appbar> & {|
style?: any,
|};

const DEFAULT_STATUSBAR_HEIGHT_EXPO =
global.__expo && global.__expo.Constants
? global.__expo.Constants.statusBarHeight
: 0;
const DEFAULT_STATUSBAR_HEIGHT = Platform.select({
android: DEFAULT_STATUSBAR_HEIGHT_EXPO,
ios: Platform.Version < 11 ? DEFAULT_STATUSBAR_HEIGHT_EXPO : 0,
});

/**
* A component to use as a header at the top of the screen.
* It can contain the screen title, controls such as navigation buttons, menu button etc.
Expand Down Expand Up @@ -91,7 +83,7 @@ class AppbarHeader extends React.Component<Props> {
render() {
const {
// Don't use default props since we check it to know whether we should use SafeAreaView
statusBarHeight = DEFAULT_STATUSBAR_HEIGHT,
statusBarHeight = APPROX_STATUSBAR_HEIGHT,
style,
...rest
} = this.props;
Expand Down
88 changes: 62 additions & 26 deletions src/components/Menu/Menu.js
Expand Up @@ -17,6 +17,7 @@ import type { Theme } from '../../types';
import Portal from '../Portal/Portal';
import Surface from '../Surface';
import MenuItem from './MenuItem';
import { APPROX_STATUSBAR_HEIGHT } from '../../constants';

type Props = {
/**
Expand All @@ -27,6 +28,13 @@ type Props = {
* The anchor to open the menu from. In most cases, it will be a button that opens the manu.
*/
anchor: React.Node,
/**
* Extra margin to add at the top of the menu to account for translucent status bar on Android.
* If you are using Expo, we assume translucent status bar and set a height for status bar automatically.
* Pass `0` or a custom value to and customize it.
* This is automatically handled on iOS.
*/
statusBarHeight: number,
/**
* Callback called when Menu is dismissed. The `visible` prop needs to be updated when this is called.
*/
Expand All @@ -42,14 +50,15 @@ type Props = {
theme: Theme,
};

type State = {
type State = {|
top: number,
left: number,
menuLayout: { height: number, width: number },
anchorLayout: { height: number, width: number },
windowLayout: {| height: number, width: number |},
menuLayout: {| height: number, width: number |},
anchorLayout: {| height: number, width: number |},
opacityAnimation: Animated.Value,
scaleAnimation: Animated.ValueXY,
};
|};

// Minimum padding between the edge of the screen and the menu
const SCREEN_INDENT = 8;
Expand Down Expand Up @@ -113,9 +122,14 @@ class Menu extends React.Component<Props, State> {
// @component ./MenuItem.js
static Item = MenuItem;

static defaultProps = {
statusBarHeight: APPROX_STATUSBAR_HEIGHT,
};

state = {
top: 0,
left: 0,
windowLayout: { width: 0, height: 0 },
menuLayout: { width: 0, height: 0 },
anchorLayout: { width: 0, height: 0 },
opacityAnimation: new Animated.Value(0),
Expand All @@ -129,7 +143,8 @@ class Menu extends React.Component<Props, State> {
}

componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.props.onDismiss);
BackHandler.removeEventListener('hardwareBackPress', this._handleDismiss);
Dimensions.removeEventListener('change', this._handleDismiss);
}

_anchor: ?View;
Expand Down Expand Up @@ -161,9 +176,17 @@ class Menu extends React.Component<Props, State> {
}
};

_handleDismiss = () => {
if (this.props.visible) {
this.props.onDismiss();
}
};

_show = async () => {
BackHandler.addEventListener('hardwareBackPress', this.props.onDismiss);
BackHandler.addEventListener('hardwareBackPress', this._handleDismiss);
Dimensions.addEventListener('change', this._handleDismiss);

const windowLayout = Dimensions.get('window');
const [menuLayout, anchorLayout] = await Promise.all([
this._measureMenuLayout(),
this._measureAnchorLayout(),
Expand All @@ -176,25 +199,26 @@ class Menu extends React.Component<Props, State> {
// so we have to wait until views are ready
// and rerun this function to show menu
if (
!windowLayout.width ||
!windowLayout.height ||
!menuLayout.width ||
!menuLayout.height ||
!anchorLayout.width ||
!anchorLayout.height
) {
BackHandler.removeEventListener(
'hardwareBackPress',
this.props.onDismiss
);
setTimeout(() => {
this._show();
}, ANIMATION_DURATION);
BackHandler.removeEventListener('hardwareBackPress', this._handleDismiss);
setTimeout(this._show, ANIMATION_DURATION);
return;
}

this.setState(
{
left: anchorLayout.x,
top: anchorLayout.y,
windowLayout: {
height: windowLayout.height,
width: windowLayout.width,
},
anchorLayout: {
height: anchorLayout.height,
width: anchorLayout.width,
Expand Down Expand Up @@ -224,7 +248,8 @@ class Menu extends React.Component<Props, State> {
};

_hide = () => {
BackHandler.removeEventListener('hardwareBackPress', this.props.onDismiss);
BackHandler.removeEventListener('hardwareBackPress', this._handleDismiss);
Dimensions.removeEventListener('change', this._handleDismiss);

Animated.timing(this.state.opacityAnimation, {
toValue: 0,
Expand All @@ -239,9 +264,18 @@ class Menu extends React.Component<Props, State> {
};

render() {
const { visible, anchor, style, children, theme, onDismiss } = this.props;
const {
visible,
anchor,
style,
children,
theme,
statusBarHeight,
onDismiss,
} = this.props;

const {
windowLayout,
menuLayout,
anchorLayout,
opacityAnimation,
Expand All @@ -250,7 +284,7 @@ class Menu extends React.Component<Props, State> {

// I don't know why but on Android measure function is wrong by 24
const additionalVerticalValue = Platform.select({
android: 24,
android: statusBarHeight,
default: 0,
});

Expand All @@ -274,12 +308,8 @@ class Menu extends React.Component<Props, State> {
// We need to translate menu while animating scale to imitate transform origin for scale animation
const positionTransforms = [];

const { width: screenWidth, height: screenHeight } = Dimensions.get(
'screen'
);

// Check if menu fits horizontally and if not align it to right.
if (left <= screenWidth - menuLayout.width - SCREEN_INDENT) {
if (left <= windowLayout.width - menuLayout.width - SCREEN_INDENT) {
positionTransforms.push({
translateX: scaleAnimation.x.interpolate({
inputRange: [0, menuLayout.width],
Expand All @@ -303,13 +333,16 @@ class Menu extends React.Component<Props, State> {

const right = left + menuLayout.width;
// Check if menu position has enough space from right side
if (right <= screenWidth && right > screenWidth - SCREEN_INDENT) {
left = screenWidth - SCREEN_INDENT - menuLayout.width;
if (
right <= windowLayout.width &&
right > windowLayout.width - SCREEN_INDENT
) {
left = windowLayout.width - SCREEN_INDENT - menuLayout.width;
}
}

// Check if menu fits vertically and if not align it to bottom.
if (top <= screenHeight - menuLayout.height - SCREEN_INDENT) {
if (top <= windowLayout.width - menuLayout.height - SCREEN_INDENT) {
positionTransforms.push({
translateY: scaleAnimation.y.interpolate({
inputRange: [0, menuLayout.height],
Expand All @@ -333,9 +366,12 @@ class Menu extends React.Component<Props, State> {

const bottom = top + menuLayout.height + additionalVerticalValue;
// Check if menu position has enough space from bottom side
if (bottom <= screenHeight && bottom > screenHeight - SCREEN_INDENT) {
if (
bottom <= windowLayout.height &&
bottom > windowLayout.height - SCREEN_INDENT
) {
top =
screenHeight -
windowLayout.height -
SCREEN_INDENT -
menuLayout.height -
additionalVerticalValue;
Expand Down
13 changes: 13 additions & 0 deletions src/constants.js
@@ -0,0 +1,13 @@
/* @flow */

import { Platform } from 'react-native';

const DEFAULT_STATUSBAR_HEIGHT_EXPO =
global.__expo && global.__expo.Constants
? global.__expo.Constants.statusBarHeight
: 0;

export const APPROX_STATUSBAR_HEIGHT = Platform.select({
android: DEFAULT_STATUSBAR_HEIGHT_EXPO,
ios: Platform.Version < 11 ? DEFAULT_STATUSBAR_HEIGHT_EXPO : 0,
});
1 change: 1 addition & 0 deletions typings/components/Menu.d.ts
Expand Up @@ -7,6 +7,7 @@ export interface MenuProps {
anchor: React.ReactNode;
onDismiss: () => void;
children: React.ReactNode;
statusBarHeight?: number,
style?: StyleProp<ViewStyle>;
theme?: ThemeShape;
}
Expand Down

0 comments on commit 1472877

Please sign in to comment.