Skip to content

Commit

Permalink
fix: dismiss menu on window layout change. closes callstack#1005 (cal…
Browse files Browse the repository at this point in the history
  • Loading branch information
satya164 authored and Trancever committed Jun 27, 2019
1 parent 4974b8c commit 17c1f81
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 43 deletions.
14 changes: 2 additions & 12 deletions src/components/Appbar/AppbarHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
StyleSheet,
StyleProp,
View,
Platform,
SafeAreaView,
ViewStyle,
} from 'react-native';
Expand All @@ -12,6 +11,7 @@ import Appbar, { DEFAULT_APPBAR_HEIGHT } from './Appbar';
import shadow from '../../styles/shadow';
import { withTheme } from '../../core/theming';
import { Theme } from '../../types';
import { APPROX_STATUSBAR_HEIGHT } from '../../constants';

type Props = React.ComponentProps<typeof Appbar> & {
/**
Expand All @@ -37,16 +37,6 @@ type Props = React.ComponentProps<typeof Appbar> & {
__expo?: any;
};

// @ts-ignore
const expo = global.__expo;

const DEFAULT_STATUSBAR_HEIGHT_EXPO =
expo && expo.Constants ? 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 @@ -98,7 +88,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
97 changes: 66 additions & 31 deletions src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Portal from '../Portal/Portal';
import Surface from '../Surface';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import MenuItem, { MenuItem as _MenuItem } from './MenuItem';
import { APPROX_STATUSBAR_HEIGHT } from '../../constants';

type Props = {
/**
Expand All @@ -30,6 +31,13 @@ type Props = {
* The anchor to open the menu from. In most cases, it will be a button that opens the menu.
*/
anchor: React.ReactNode;
/**
* 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 @@ -45,11 +53,14 @@ type Props = {
theme: Theme;
};

type Layout = Omit<Omit<LayoutRectangle, 'x'>, 'y'>;

type State = {
top: number;
left: number;
menuLayout: LayoutRectangle;
anchorLayout: LayoutRectangle;
windowLayout: Layout;
menuLayout: Layout;
anchorLayout: Layout;
opacityAnimation: Animated.Value;
scaleAnimation: Animated.ValueXY;
};
Expand Down Expand Up @@ -116,11 +127,16 @@ class Menu extends React.Component<Props, State> {
// @component ./MenuItem.tsx
static Item = MenuItem;

static defaultProps = {
statusBarHeight: APPROX_STATUSBAR_HEIGHT,
};

state = {
top: 0,
left: 0,
menuLayout: { width: 0, height: 0, x: 0, y: 0 },
anchorLayout: { width: 0, height: 0, x: 0, y: 0 },
windowLayout: { width: 0, height: 0 },
menuLayout: { width: 0, height: 0 },
anchorLayout: { width: 0, height: 0 },
opacityAnimation: new Animated.Value(0),
scaleAnimation: new Animated.ValueXY({ x: 0, y: 0 }),
};
Expand All @@ -132,7 +148,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 | null;
Expand Down Expand Up @@ -164,8 +181,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 @@ -178,34 +204,31 @@ 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(
state => ({
() => ({
left: anchorLayout.x,
top: anchorLayout.y,
windowLayout: {
height: windowLayout.height,
width: windowLayout.width,
},
anchorLayout: {
x: state.anchorLayout.x,
y: state.anchorLayout.y,
height: anchorLayout.height,
width: anchorLayout.width,
},
menuLayout: {
x: state.menuLayout.x,
y: state.menuLayout.y,
width: menuLayout.width,
height: menuLayout.height,
},
Expand All @@ -230,7 +253,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 @@ -245,9 +269,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 @@ -256,7 +289,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 @@ -280,12 +313,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 @@ -309,13 +338,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 @@ -339,9 +371,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
14 changes: 14 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* @flow */

import { Platform } from 'react-native';

// @ts-ignore
const expo = global.__expo;

const DEFAULT_STATUSBAR_HEIGHT_EXPO =
expo && expo.Constants ? 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,
});

0 comments on commit 17c1f81

Please sign in to comment.