Skip to content

Commit 17c1f81

Browse files
satya164Trancever
authored andcommitted
fix: dismiss menu on window layout change. closes #1005 (#1026)
1 parent 4974b8c commit 17c1f81

3 files changed

Lines changed: 82 additions & 43 deletions

File tree

src/components/Appbar/AppbarHeader.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
StyleSheet,
44
StyleProp,
55
View,
6-
Platform,
76
SafeAreaView,
87
ViewStyle,
98
} from 'react-native';
@@ -12,6 +11,7 @@ import Appbar, { DEFAULT_APPBAR_HEIGHT } from './Appbar';
1211
import shadow from '../../styles/shadow';
1312
import { withTheme } from '../../core/theming';
1413
import { Theme } from '../../types';
14+
import { APPROX_STATUSBAR_HEIGHT } from '../../constants';
1515

1616
type Props = React.ComponentProps<typeof Appbar> & {
1717
/**
@@ -37,16 +37,6 @@ type Props = React.ComponentProps<typeof Appbar> & {
3737
__expo?: any;
3838
};
3939

40-
// @ts-ignore
41-
const expo = global.__expo;
42-
43-
const DEFAULT_STATUSBAR_HEIGHT_EXPO =
44-
expo && expo.Constants ? expo.Constants.statusBarHeight : 0;
45-
const DEFAULT_STATUSBAR_HEIGHT = Platform.select({
46-
android: DEFAULT_STATUSBAR_HEIGHT_EXPO,
47-
ios: Platform.Version < 11 ? DEFAULT_STATUSBAR_HEIGHT_EXPO : 0,
48-
});
49-
5040
/**
5141
* A component to use as a header at the top of the screen.
5242
* It can contain the screen title, controls such as navigation buttons, menu button etc.
@@ -98,7 +88,7 @@ class AppbarHeader extends React.Component<Props> {
9888
render() {
9989
const {
10090
// Don't use default props since we check it to know whether we should use SafeAreaView
101-
statusBarHeight = DEFAULT_STATUSBAR_HEIGHT,
91+
statusBarHeight = APPROX_STATUSBAR_HEIGHT,
10292
style,
10393
...rest
10494
} = this.props;

src/components/Menu/Menu.tsx

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Portal from '../Portal/Portal';
2020
import Surface from '../Surface';
2121
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2222
import MenuItem, { MenuItem as _MenuItem } from './MenuItem';
23+
import { APPROX_STATUSBAR_HEIGHT } from '../../constants';
2324

2425
type Props = {
2526
/**
@@ -30,6 +31,13 @@ type Props = {
3031
* The anchor to open the menu from. In most cases, it will be a button that opens the menu.
3132
*/
3233
anchor: React.ReactNode;
34+
/**
35+
* Extra margin to add at the top of the menu to account for translucent status bar on Android.
36+
* If you are using Expo, we assume translucent status bar and set a height for status bar automatically.
37+
* Pass `0` or a custom value to and customize it.
38+
* This is automatically handled on iOS.
39+
*/
40+
statusBarHeight?: number;
3341
/**
3442
* Callback called when Menu is dismissed. The `visible` prop needs to be updated when this is called.
3543
*/
@@ -45,11 +53,14 @@ type Props = {
4553
theme: Theme;
4654
};
4755

56+
type Layout = Omit<Omit<LayoutRectangle, 'x'>, 'y'>;
57+
4858
type State = {
4959
top: number;
5060
left: number;
51-
menuLayout: LayoutRectangle;
52-
anchorLayout: LayoutRectangle;
61+
windowLayout: Layout;
62+
menuLayout: Layout;
63+
anchorLayout: Layout;
5364
opacityAnimation: Animated.Value;
5465
scaleAnimation: Animated.ValueXY;
5566
};
@@ -116,11 +127,16 @@ class Menu extends React.Component<Props, State> {
116127
// @component ./MenuItem.tsx
117128
static Item = MenuItem;
118129

130+
static defaultProps = {
131+
statusBarHeight: APPROX_STATUSBAR_HEIGHT,
132+
};
133+
119134
state = {
120135
top: 0,
121136
left: 0,
122-
menuLayout: { width: 0, height: 0, x: 0, y: 0 },
123-
anchorLayout: { width: 0, height: 0, x: 0, y: 0 },
137+
windowLayout: { width: 0, height: 0 },
138+
menuLayout: { width: 0, height: 0 },
139+
anchorLayout: { width: 0, height: 0 },
124140
opacityAnimation: new Animated.Value(0),
125141
scaleAnimation: new Animated.ValueXY({ x: 0, y: 0 }),
126142
};
@@ -132,7 +148,8 @@ class Menu extends React.Component<Props, State> {
132148
}
133149

134150
componentWillUnmount() {
135-
BackHandler.removeEventListener('hardwareBackPress', this.props.onDismiss);
151+
BackHandler.removeEventListener('hardwareBackPress', this._handleDismiss);
152+
Dimensions.removeEventListener('change', this._handleDismiss);
136153
}
137154

138155
_anchor?: View | null;
@@ -164,8 +181,17 @@ class Menu extends React.Component<Props, State> {
164181
}
165182
};
166183

184+
_handleDismiss = () => {
185+
if (this.props.visible) {
186+
this.props.onDismiss();
187+
}
188+
};
189+
167190
_show = async () => {
168-
BackHandler.addEventListener('hardwareBackPress', this.props.onDismiss);
191+
BackHandler.addEventListener('hardwareBackPress', this._handleDismiss);
192+
Dimensions.addEventListener('change', this._handleDismiss);
193+
194+
const windowLayout = Dimensions.get('window');
169195
const [menuLayout, anchorLayout] = await Promise.all([
170196
this._measureMenuLayout(),
171197
this._measureAnchorLayout(),
@@ -178,34 +204,31 @@ class Menu extends React.Component<Props, State> {
178204
// so we have to wait until views are ready
179205
// and rerun this function to show menu
180206
if (
207+
!windowLayout.width ||
208+
!windowLayout.height ||
181209
!menuLayout.width ||
182210
!menuLayout.height ||
183211
!anchorLayout.width ||
184212
!anchorLayout.height
185213
) {
186-
BackHandler.removeEventListener(
187-
'hardwareBackPress',
188-
this.props.onDismiss
189-
);
190-
setTimeout(() => {
191-
this._show();
192-
}, ANIMATION_DURATION);
214+
BackHandler.removeEventListener('hardwareBackPress', this._handleDismiss);
215+
setTimeout(this._show, ANIMATION_DURATION);
193216
return;
194217
}
195218

196219
this.setState(
197-
state => ({
220+
() => ({
198221
left: anchorLayout.x,
199222
top: anchorLayout.y,
223+
windowLayout: {
224+
height: windowLayout.height,
225+
width: windowLayout.width,
226+
},
200227
anchorLayout: {
201-
x: state.anchorLayout.x,
202-
y: state.anchorLayout.y,
203228
height: anchorLayout.height,
204229
width: anchorLayout.width,
205230
},
206231
menuLayout: {
207-
x: state.menuLayout.x,
208-
y: state.menuLayout.y,
209232
width: menuLayout.width,
210233
height: menuLayout.height,
211234
},
@@ -230,7 +253,8 @@ class Menu extends React.Component<Props, State> {
230253
};
231254

232255
_hide = () => {
233-
BackHandler.removeEventListener('hardwareBackPress', this.props.onDismiss);
256+
BackHandler.removeEventListener('hardwareBackPress', this._handleDismiss);
257+
Dimensions.removeEventListener('change', this._handleDismiss);
234258

235259
Animated.timing(this.state.opacityAnimation, {
236260
toValue: 0,
@@ -245,9 +269,18 @@ class Menu extends React.Component<Props, State> {
245269
};
246270

247271
render() {
248-
const { visible, anchor, style, children, theme, onDismiss } = this.props;
272+
const {
273+
visible,
274+
anchor,
275+
style,
276+
children,
277+
theme,
278+
statusBarHeight,
279+
onDismiss,
280+
} = this.props;
249281

250282
const {
283+
windowLayout,
251284
menuLayout,
252285
anchorLayout,
253286
opacityAnimation,
@@ -256,7 +289,7 @@ class Menu extends React.Component<Props, State> {
256289

257290
// I don't know why but on Android measure function is wrong by 24
258291
const additionalVerticalValue = Platform.select({
259-
android: 24,
292+
android: statusBarHeight,
260293
default: 0,
261294
});
262295

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

283-
const { width: screenWidth, height: screenHeight } = Dimensions.get(
284-
'screen'
285-
);
286-
287316
// Check if menu fits horizontally and if not align it to right.
288-
if (left <= screenWidth - menuLayout.width - SCREEN_INDENT) {
317+
if (left <= windowLayout.width - menuLayout.width - SCREEN_INDENT) {
289318
positionTransforms.push({
290319
translateX: scaleAnimation.x.interpolate({
291320
inputRange: [0, menuLayout.width],
@@ -309,13 +338,16 @@ class Menu extends React.Component<Props, State> {
309338

310339
const right = left + menuLayout.width;
311340
// Check if menu position has enough space from right side
312-
if (right <= screenWidth && right > screenWidth - SCREEN_INDENT) {
313-
left = screenWidth - SCREEN_INDENT - menuLayout.width;
341+
if (
342+
right <= windowLayout.width &&
343+
right > windowLayout.width - SCREEN_INDENT
344+
) {
345+
left = windowLayout.width - SCREEN_INDENT - menuLayout.width;
314346
}
315347
}
316348

317349
// Check if menu fits vertically and if not align it to bottom.
318-
if (top <= screenHeight - menuLayout.height - SCREEN_INDENT) {
350+
if (top <= windowLayout.width - menuLayout.height - SCREEN_INDENT) {
319351
positionTransforms.push({
320352
translateY: scaleAnimation.y.interpolate({
321353
inputRange: [0, menuLayout.height],
@@ -339,9 +371,12 @@ class Menu extends React.Component<Props, State> {
339371

340372
const bottom = top + menuLayout.height + additionalVerticalValue;
341373
// Check if menu position has enough space from bottom side
342-
if (bottom <= screenHeight && bottom > screenHeight - SCREEN_INDENT) {
374+
if (
375+
bottom <= windowLayout.height &&
376+
bottom > windowLayout.height - SCREEN_INDENT
377+
) {
343378
top =
344-
screenHeight -
379+
windowLayout.height -
345380
SCREEN_INDENT -
346381
menuLayout.height -
347382
additionalVerticalValue;

src/constants.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* @flow */
2+
3+
import { Platform } from 'react-native';
4+
5+
// @ts-ignore
6+
const expo = global.__expo;
7+
8+
const DEFAULT_STATUSBAR_HEIGHT_EXPO =
9+
expo && expo.Constants ? expo.Constants.statusBarHeight : 0;
10+
11+
export const APPROX_STATUSBAR_HEIGHT = Platform.select({
12+
android: DEFAULT_STATUSBAR_HEIGHT_EXPO,
13+
ios: Platform.Version < 11 ? DEFAULT_STATUSBAR_HEIGHT_EXPO : 0,
14+
});

0 commit comments

Comments
 (0)