From 2f2c8ce85f4e2f27d466e3c43708889a81490c26 Mon Sep 17 00:00:00 2001 From: iyad Date: Wed, 8 Aug 2018 18:50:12 +0300 Subject: [PATCH 01/22] feat: Menu component --- example/src/ExampleList.js | 4 +++- src/index.js | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/example/src/ExampleList.js b/example/src/ExampleList.js index db2b159451..f051d9a460 100644 --- a/example/src/ExampleList.js +++ b/example/src/ExampleList.js @@ -21,6 +21,7 @@ import FABExample from './Examples/FABExample'; import IconButtonExample from './Examples/IconButtonExample'; import ListAccordionExample from './Examples/ListAccordionExample'; import ListSectionExample from './Examples/ListSectionExample'; +import MenuExample from './Examples/MenuExample'; import ProgressBarExample from './Examples/ProgressBarExample'; import RadioButtonExample from './Examples/RadioButtonExample'; import RadioButtonGroupExample from './Examples/RadioButtonGroupExample'; @@ -31,7 +32,7 @@ import SwitchExample from './Examples/SwitchExample'; import TextExample from './Examples/TextExample'; import TextInputExample from './Examples/TextInputExample'; import ToggleButtonExample from './Examples/ToggleButtonExample'; -import TouchableRippleExample from './Examples/TouchableRippleExample'; +import TouchableRippleExample from './Examurlples/TouchableRippleExample'; type Props = { theme: Theme, @@ -56,6 +57,7 @@ export const examples = { iconButton: IconButtonExample, listAccordion: ListAccordionExample, listSection: ListSectionExample, + menu: MenuExample, progressbar: ProgressBarExample, radio: RadioButtonExample, radioGroup: RadioButtonGroupExample, diff --git a/src/index.js b/src/index.js index f8e067c6ae..6b5dbfcb34 100644 --- a/src/index.js +++ b/src/index.js @@ -34,6 +34,7 @@ export { default as FAB } from './components/FAB/FAB'; export { default as HelperText } from './components/HelperText'; export { default as IconButton } from './components/IconButton'; export { default as Modal } from './components/Modal'; +export { default as Menu } from './components/Menu/Menu'; export { default as Portal } from './components/Portal/Portal'; export { default as ProgressBar } from './components/ProgressBar/ProgressBar'; export { default as RadioButton } from './components/RadioButton'; From 558500d0ed671186e3c1492426b84e10147d2dd9 Mon Sep 17 00:00:00 2001 From: iyad Date: Wed, 8 Aug 2018 19:00:53 +0300 Subject: [PATCH 02/22] feat: Menu component --- docs/assets/screenshots/menu-1.png | Bin 0 -> 16559 bytes docs/assets/screenshots/menu-2.png | Bin 0 -> 14246 bytes example/src/MenuExample.js | 112 +++++++++ src/components/Menu/AnimatedSurface.js | 81 +++++++ src/components/Menu/Menu.js | 305 +++++++++++++++++++++++++ src/components/Menu/MenuItem.js | 125 ++++++++++ 6 files changed, 623 insertions(+) create mode 100644 docs/assets/screenshots/menu-1.png create mode 100644 docs/assets/screenshots/menu-2.png create mode 100644 example/src/MenuExample.js create mode 100644 src/components/Menu/AnimatedSurface.js create mode 100644 src/components/Menu/Menu.js create mode 100644 src/components/Menu/MenuItem.js diff --git a/docs/assets/screenshots/menu-1.png b/docs/assets/screenshots/menu-1.png new file mode 100644 index 0000000000000000000000000000000000000000..2eb0778b5191f90cfff45275030b537c9203167c GIT binary patch literal 16559 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 E7k@N00ssI2 literal 0 HcmV?d00001 diff --git a/example/src/MenuExample.js b/example/src/MenuExample.js new file mode 100644 index 0000000000..4da5ea7d8f --- /dev/null +++ b/example/src/MenuExample.js @@ -0,0 +1,112 @@ +/* @flow */ + +import * as React from 'react'; +import { View, StyleSheet, Platform } from 'react-native'; +import { Menu, Appbar, withTheme, Divider, Button } from 'react-native-paper'; +import type { Theme } from 'react-native-paper/types'; + +type State = { + visible1: boolean, + visible2: boolean, +}; + +type Props = { + theme: Theme, +}; + +const MORE_ICON = Platform.OS === 'ios' ? 'more-horiz' : 'more-vert'; + +class MenuExample extends React.Component { + static navigationOptions = { + header: null, + }; + + state = { + visible1: false, + visible2: false, + }; + + static title = 'Menu'; + + _openMenu1 = () => this.setState({ visible1: true }); + _openMenu2 = () => this.setState({ visible2: true }); + + _closeMenu1 = () => this.setState({ visible1: false }); + _closeMenu2 = () => this.setState({ visible2: false }); + + render() { + const { + theme: { + colors: { background }, + }, + } = this.props; + + return ( + + + this.props.navigation.goBack()} /> + +

+ } + > + {}} title="Undo" /> + {}} title="Redo" /> + + {}} title="Cut" disabled /> + {}} title="Copy" disabled /> + {}} title="Paste" /> + + + + + Menu with icons + + } + > + {}} title="Undo" /> + {}} title="Redo" /> + + {}} + title="Cut" + disabled + /> + {}} + title="Copy" + disabled + /> + {}} title="Paste" /> + + + + ); + } +} + +const styles = StyleSheet.create({ + screen: { + flex: 1, + }, + container: { + flex: 1, + alignItems: 'center', + paddingTop: 48, + }, +}); + +export default withTheme(MenuExample); diff --git a/src/components/Menu/AnimatedSurface.js b/src/components/Menu/AnimatedSurface.js new file mode 100644 index 0000000000..7630d2f103 --- /dev/null +++ b/src/components/Menu/AnimatedSurface.js @@ -0,0 +1,81 @@ +/* @flow */ + +import * as React from 'react'; +import { Animated, StyleSheet } from 'react-native'; +import shadow from '../../styles/shadow'; +import { withTheme } from '../../core/theming'; +import * as Colors from '../../styles/colors'; +import type { Theme } from '../../types'; + +type Props = { + /** + * Content of the `Surface`. + */ + children: React.Node, + style?: any, + /** + * @optional + */ + theme: Theme, +}; + +/** + * Surface is a basic container that can give depth to an element with elevation shadow. + * A shadow can be applied by specifying the `elevation` property both on Android and iOS. + * + *
+ * + * + * + *
+ * + * ## Usage + * ```js + * import * as React from 'react'; + * import { Surface, Text } from 'react-native-paper'; + * + * const MyComponent = () => ( + * + * Surface + * + * ); + * + * const styles = StyleSheet.create({ + * surface: { + * padding: 8, + * height: 80, + * width: 80, + * alignItems: 'center', + * justifyContent: 'center', + * elevation: 4, + * }, + * }); + * ``` + */ +class Surface extends React.Component { + render() { + const { style, theme, ...rest } = this.props; + const flattenedStyles = StyleSheet.flatten(style) || {}; + const { elevation } = flattenedStyles; + + return ( + + ); + } +} + +export default withTheme(Surface); + +const styles = StyleSheet.create({ + surface: { + backgroundColor: Colors.white, + }, +}); diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js new file mode 100644 index 0000000000..eeb9defa3f --- /dev/null +++ b/src/components/Menu/Menu.js @@ -0,0 +1,305 @@ +/* @flow */ + +import * as React from 'react'; +import { + StyleSheet, + Animated, + View, + Easing, + Modal, + Dimensions, + TouchableWithoutFeedback, + I18nManager, + BackHandler, +} from 'react-native'; +import { withTheme } from '../../core/theming'; +import type { Theme } from '../../types'; +import AnimatedSurface from './AnimatedSurface'; +import MenuItem from './MenuItem'; + +type Props = { + /** + * Whether the Menu is currently visible. + */ + visible: boolean, + /** + * The button to trigger the menu. + */ + button: React.Node, + /** + * Callback called when Menu is dismissed. The `visible` prop needs to be updated when this is called. + */ + onDismiss: () => mixed, + /** + * Content of the `Menu`. + */ + children: React.Node, + style?: any, + /** + * @optional + */ + theme: Theme, +}; + +/** + * Menus display a list of choices on temporary surfaces. Their placement varies based on the element that opens them. + * + *
+ * + * + *
+ * + * ## Usage + * ```js + * import * as React from 'react'; + * import { View } from 'react-native'; + * import { Button, Paragraph, Menu, Divider } from 'react-native-paper'; + * + * export default class MyComponent extends React.Component { + * state = { + * visible: false, + * }; + * + * _openMenu = () => this.setState({ visible: true }); + * + * _closeMenu = () => this.setState({ visible: false }); + * + * render() { + * return ( + * + * Show menu + * } + * > + * {}} title="Item 1" /> + * {}} title="Item 2" /> + * + * {}} title="Item 3" /> + * + * + * ); + * } + * } + * ``` + */ + +type State = { + top: number, + left: number, + menuWidth: number, + menuHeight: number, + buttonWidth: number, + buttonHeight: number, + opacityAnimation: Animated.Value, + menuSizeAnimation: Animated.ValueXY, + menuState: 'hidden' | 'animating' | 'shown', +}; + +const ANIMATION_DURATION = 300; +const EASING = Easing.bezier(0.4, 0, 0.2, 1); +const SCREEN_INDENT = 8; + +class Menu extends React.Component { + // @component ./MenuItem.js + static Item = MenuItem; + + state = { + menuState: 'hidden', + top: 0, + left: 0, + menuWidth: 0, + menuHeight: 0, + buttonWidth: 0, + buttonHeight: 0, + opacityAnimation: new Animated.Value(0), + menuSizeAnimation: new Animated.ValueXY({ x: 0, y: 0 }), + }; + + componentDidUpdate(prevProps) { + if (prevProps.visible !== this.props.visible) { + this._toggle(); + } + } + + _container: any; + + _setContainerRef = ref => { + this._container = ref; + }; + + // Start menu animation + _onMenuLayout = e => { + if (this.state.menuState === 'animating') { + return; + } + + const { width, height } = e.nativeEvent.layout; + + this.setState( + { + menuState: 'animating', + menuWidth: width, + menuHeight: height, + }, + () => { + Animated.parallel([ + Animated.timing(this.state.menuSizeAnimation, { + toValue: { x: width, y: height }, + duration: ANIMATION_DURATION, + easing: EASING, + }), + Animated.timing(this.state.opacityAnimation, { + toValue: 1, + duration: ANIMATION_DURATION, + easing: EASING, + }), + ]).start(); + } + ); + }; + + // Save button width and height for menu layout + _onButtonLayout = e => { + const { width, height } = e.nativeEvent.layout; + this.setState({ buttonWidth: width, buttonHeight: height }); + }; + + _toggle = () => { + if (this.props.visible) { + this._show(); + } else { + this._hide(); + } + }; + + _show = () => { + BackHandler.addEventListener('hardwareBackPress', this._hide); + this._container.measureInWindow((x, y) => { + const top = Math.max(SCREEN_INDENT, y); + const left = Math.max(SCREEN_INDENT, x); + this.setState({ menuState: 'shown', top, left }); + }); + }; + + _hide = () => { + BackHandler.removeEventListener('hardwareBackPress', this._hide); + Animated.timing(this.state.opacityAnimation, { + toValue: 0, + duration: ANIMATION_DURATION, + easing: EASING, + }).start(() => { + if (this.props.visible && this.props.onDismiss) { + this.props.onDismiss(); + } + if (this.props.visible) { + this._show(); + } else { + this.setState({ + menuState: 'hidden', + menuSizeAnimation: new Animated.ValueXY({ x: 0, y: 0 }), + opacityAnimation: new Animated.Value(0), + }); + } + }); + }; + + render() { + const { visible, button, style, children, theme } = this.props; + + const { + menuState, + menuWidth, + menuHeight, + buttonWidth, + buttonHeight, + opacityAnimation, + menuSizeAnimation, + } = this.state; + + const menuSize = { + width: menuSizeAnimation.x, + height: menuSizeAnimation.y, + }; + + // Adjust position of menu + let { left, top } = this.state; + const transforms = []; + + const dimensions = Dimensions.get('screen'); + + // Flip by X axis if menu hits right screen border + if (left > dimensions.width - menuWidth - SCREEN_INDENT) { + transforms.push({ + translateX: Animated.multiply(menuSizeAnimation.x, -1), + }); + + left = Math.min(dimensions.width - SCREEN_INDENT, left + buttonWidth); + } + + // Flip by Y axis if menu hits bottom screen border + if (top > dimensions.height - menuHeight - SCREEN_INDENT) { + transforms.push({ + translateY: Animated.multiply(menuSizeAnimation.y, -1), + }); + + top = Math.min(dimensions.height - SCREEN_INDENT, top + buttonHeight); + } + + // RTL support + const leftOrRight = I18nManager.isRTL ? { right: left } : { left }; + + const shadowMenuContainerStyle = { + opacity: opacityAnimation, + transform: transforms, + borderRadius: theme.roundness, + ...leftOrRight, + top, + }; + + const animationStarted = menuState === 'animating'; + const modalVisible = menuState === 'shown' || animationStarted || visible; + + return ( + + {button} + + + + + + + + {children} + + + + + ); + } +} + +const styles = StyleSheet.create({ + shadowMenuContainer: { + position: 'absolute', + opacity: 0, + paddingTop: 8, + elevation: 8, + }, + menuContainer: { + overflow: 'hidden', + }, +}); + +export default withTheme(Menu); diff --git a/src/components/Menu/MenuItem.js b/src/components/Menu/MenuItem.js new file mode 100644 index 0000000000..ed4d341a5b --- /dev/null +++ b/src/components/Menu/MenuItem.js @@ -0,0 +1,125 @@ +/* @flow */ + +import color from 'color'; +import * as React from 'react'; +import { View, StyleSheet } from 'react-native'; +import Icon from '../Icon'; +import TouchableRipple from '../TouchableRipple'; +import Text from '../Typography/Text'; +import { withTheme } from '../../core/theming'; +import { black, white } from '../../styles/colors'; +import type { Theme } from '../../types'; +import type { IconSource } from '../Icon'; + +type Props = { + /** + * Title text for the `MenuItem`. + */ + title: React.Node, + /** + * Icon to display for the `MenuItem`. + */ + icon?: IconSource, + /** + * Whether the `MenuItem` is disabled. A disabled `MenuItem` is greyed out and `onPress` is not called on touch. + */ + disabled?: boolean, + /** + * Function to execute on press. + */ + onPress?: () => mixed, + /** + * @optional + */ + theme: Theme, + style?: any, +}; + +/** + * A component to show list of options inside a Menu. + * + */ + +class MenuItem extends React.Component { + static displayName = 'Menu.Item'; + + render() { + const { icon, title, disabled, onPress, theme, style } = this.props; + + let titleColor = color(theme.colors.text) + .alpha(0.87) + .rgb() + .string(); + let iconColor = color(theme.colors.text) + .alpha(0.54) + .rgb() + .string(); + + if (disabled) { + iconColor = titleColor = color(theme.dark ? white : black) + .alpha(0.32) + .rgb() + .string(); + } + + return ( + + + {icon ? ( + + + + ) : null} + + + {title} + + + + + ); + } +} + +const minWidth = 112; +const maxWidth = 280; + +const styles = StyleSheet.create({ + container: { + padding: 8, + minWidth, + maxWidth, + }, + row: { + flexDirection: 'row', + }, + icon: { + width: 40, + }, + title: { + fontSize: 16, + }, + item: { + margin: 8, + }, + content: { + justifyContent: 'center', + minWidth: minWidth - 16, + maxWidth: maxWidth - 16, + }, + widthWithIcon: { + maxWidth: maxWidth - (8 * 6 + 40), + }, +}); + +export default withTheme(MenuItem); From eff98cde2391cdf559601324aaa50c89d9b89ee9 Mon Sep 17 00:00:00 2001 From: iyad Date: Wed, 8 Aug 2018 19:08:42 +0300 Subject: [PATCH 03/22] fix: fixed flow errors --- example/src/MenuExample.js | 1 + 1 file changed, 1 insertion(+) diff --git a/example/src/MenuExample.js b/example/src/MenuExample.js index 4da5ea7d8f..eff1155fb4 100644 --- a/example/src/MenuExample.js +++ b/example/src/MenuExample.js @@ -12,6 +12,7 @@ type State = { type Props = { theme: Theme, + navigation: any, }; const MORE_ICON = Platform.OS === 'ios' ? 'more-horiz' : 'more-vert'; From 3a6e7b6c88b6954406be0a25a2a7557459cc5898 Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Sun, 17 Feb 2019 20:38:19 +0000 Subject: [PATCH 04/22] fix: import path which was broke during rebase --- example/src/ExampleList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/ExampleList.js b/example/src/ExampleList.js index f051d9a460..150c3606c9 100644 --- a/example/src/ExampleList.js +++ b/example/src/ExampleList.js @@ -32,7 +32,7 @@ import SwitchExample from './Examples/SwitchExample'; import TextExample from './Examples/TextExample'; import TextInputExample from './Examples/TextInputExample'; import ToggleButtonExample from './Examples/ToggleButtonExample'; -import TouchableRippleExample from './Examurlples/TouchableRippleExample'; +import TouchableRippleExample from './Examples/TouchableRippleExample'; type Props = { theme: Theme, From e0c1adcfd8c62d1cf9313f365dc17cb654b35d3b Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Sun, 17 Feb 2019 20:40:57 +0000 Subject: [PATCH 05/22] fix: moved example to new 'Examples' directory --- example/src/{ => Examples}/MenuExample.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) rename example/src/{ => Examples}/MenuExample.js (95%) diff --git a/example/src/MenuExample.js b/example/src/Examples/MenuExample.js similarity index 95% rename from example/src/MenuExample.js rename to example/src/Examples/MenuExample.js index eff1155fb4..8ffe9a1fca 100644 --- a/example/src/MenuExample.js +++ b/example/src/Examples/MenuExample.js @@ -2,8 +2,14 @@ import * as React from 'react'; import { View, StyleSheet, Platform } from 'react-native'; -import { Menu, Appbar, withTheme, Divider, Button } from 'react-native-paper'; -import type { Theme } from 'react-native-paper/types'; +import { + Menu, + Appbar, + Divider, + Button, + withTheme, + type Theme, +} from 'react-native-paper'; type State = { visible1: boolean, From 226a73bf595cbb47087179d1c52780ff5f2c8370 Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Sun, 17 Feb 2019 20:42:28 +0000 Subject: [PATCH 06/22] fix: remove unneeded AnimatedSurface --- src/components/Menu/AnimatedSurface.js | 81 -------------------------- src/components/Menu/Menu.js | 6 +- 2 files changed, 3 insertions(+), 84 deletions(-) delete mode 100644 src/components/Menu/AnimatedSurface.js diff --git a/src/components/Menu/AnimatedSurface.js b/src/components/Menu/AnimatedSurface.js deleted file mode 100644 index 7630d2f103..0000000000 --- a/src/components/Menu/AnimatedSurface.js +++ /dev/null @@ -1,81 +0,0 @@ -/* @flow */ - -import * as React from 'react'; -import { Animated, StyleSheet } from 'react-native'; -import shadow from '../../styles/shadow'; -import { withTheme } from '../../core/theming'; -import * as Colors from '../../styles/colors'; -import type { Theme } from '../../types'; - -type Props = { - /** - * Content of the `Surface`. - */ - children: React.Node, - style?: any, - /** - * @optional - */ - theme: Theme, -}; - -/** - * Surface is a basic container that can give depth to an element with elevation shadow. - * A shadow can be applied by specifying the `elevation` property both on Android and iOS. - * - *
- * - * - * - *
- * - * ## Usage - * ```js - * import * as React from 'react'; - * import { Surface, Text } from 'react-native-paper'; - * - * const MyComponent = () => ( - * - * Surface - * - * ); - * - * const styles = StyleSheet.create({ - * surface: { - * padding: 8, - * height: 80, - * width: 80, - * alignItems: 'center', - * justifyContent: 'center', - * elevation: 4, - * }, - * }); - * ``` - */ -class Surface extends React.Component { - render() { - const { style, theme, ...rest } = this.props; - const flattenedStyles = StyleSheet.flatten(style) || {}; - const { elevation } = flattenedStyles; - - return ( - - ); - } -} - -export default withTheme(Surface); - -const styles = StyleSheet.create({ - surface: { - backgroundColor: Colors.white, - }, -}); diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js index eeb9defa3f..e82ac17302 100644 --- a/src/components/Menu/Menu.js +++ b/src/components/Menu/Menu.js @@ -14,7 +14,7 @@ import { } from 'react-native'; import { withTheme } from '../../core/theming'; import type { Theme } from '../../types'; -import AnimatedSurface from './AnimatedSurface'; +import Surface from '../Surface'; import MenuItem from './MenuItem'; type Props = { @@ -270,7 +270,7 @@ class Menu extends React.Component { - { > {children} - + ); From e93581bb4f409dacf6ed24fc52d7cf088206a96b Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Sun, 17 Feb 2019 22:33:15 +0000 Subject: [PATCH 07/22] feat: initial attempt at creating ts typings --- typings/components/Menu.d.ts | 25 +++++++++++++++++++++++++ typings/index.d.ts | 1 + 2 files changed, 26 insertions(+) create mode 100644 typings/components/Menu.d.ts diff --git a/typings/components/Menu.d.ts b/typings/components/Menu.d.ts new file mode 100644 index 0000000000..e2187fb2d3 --- /dev/null +++ b/typings/components/Menu.d.ts @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { StyleProp, ViewStyle, ViewProps } from 'react-native'; +import { ThemeShape, IconSource } from '../types'; + +export interface MenuProps { + visible: boolean; + button: React.ReactNode; + onDismiss: () => mixed; + children: React.ReactNode; + style?: any; + theme?: ThemeShape; +} + +export interface ItemProps { + title: React.ReactNode; + icon?: IconSource; + disabled?: boolean; + onPress?: () => mixed; + theme?: ThemeShape; + style?: any; +} + +export declare class Menu extends React.Component { + static Item: React.ComponentType; +} diff --git a/typings/index.d.ts b/typings/index.d.ts index ea72f7171f..be18d6cef2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -20,6 +20,7 @@ export * from './components/Divider'; export * from './components/FAB'; export * from './components/HelperText'; export * from './components/IconButton'; +export * from './components/Menu' export * from './components/Modal'; export * from './components/Portal'; export * from './components/ProgressBar'; From 7107764e66465404c9ca5054e88fa22dcc29bc85 Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Mon, 18 Feb 2019 06:34:31 +0000 Subject: [PATCH 08/22] fix: import order to be in alphabetical order --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 6b5dbfcb34..792c8e7389 100644 --- a/src/index.js +++ b/src/index.js @@ -33,8 +33,8 @@ export { default as Divider } from './components/Divider'; export { default as FAB } from './components/FAB/FAB'; export { default as HelperText } from './components/HelperText'; export { default as IconButton } from './components/IconButton'; -export { default as Modal } from './components/Modal'; export { default as Menu } from './components/Menu/Menu'; +export { default as Modal } from './components/Modal'; export { default as Portal } from './components/Portal/Portal'; export { default as ProgressBar } from './components/ProgressBar/ProgressBar'; export { default as RadioButton } from './components/RadioButton'; From 0d23148791a7533abd9d4a125bdad8918b9ad45a Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Mon, 18 Feb 2019 20:13:52 +0000 Subject: [PATCH 09/22] fix: moved docs markup to be above the class definition --- src/components/Menu/Menu.js | 68 ++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js index e82ac17302..b242a10b3c 100644 --- a/src/components/Menu/Menu.js +++ b/src/components/Menu/Menu.js @@ -41,6 +41,22 @@ type Props = { theme: Theme, }; +type State = { + top: number, + left: number, + menuWidth: number, + menuHeight: number, + buttonWidth: number, + buttonHeight: number, + opacityAnimation: Animated.Value, + menuSizeAnimation: Animated.ValueXY, + menuState: 'hidden' | 'animating' | 'shown', +}; + +const ANIMATION_DURATION = 300; +const EASING = Easing.bezier(0.4, 0, 0.2, 1); +const SCREEN_INDENT = 8; + /** * Menus display a list of choices on temporary surfaces. Their placement varies based on the element that opens them. * @@ -85,23 +101,6 @@ type Props = { * } * ``` */ - -type State = { - top: number, - left: number, - menuWidth: number, - menuHeight: number, - buttonWidth: number, - buttonHeight: number, - opacityAnimation: Animated.Value, - menuSizeAnimation: Animated.ValueXY, - menuState: 'hidden' | 'animating' | 'shown', -}; - -const ANIMATION_DURATION = 300; -const EASING = Easing.bezier(0.4, 0, 0.2, 1); -const SCREEN_INDENT = 8; - class Menu extends React.Component { // @component ./MenuItem.js static Item = MenuItem; @@ -120,7 +119,7 @@ class Menu extends React.Component { componentDidUpdate(prevProps) { if (prevProps.visible !== this.props.visible) { - this._toggle(); + this._updateVisibility(); } } @@ -167,7 +166,7 @@ class Menu extends React.Component { this.setState({ buttonWidth: width, buttonHeight: height }); }; - _toggle = () => { + _updateVisibility = () => { if (this.props.visible) { this._show(); } else { @@ -219,43 +218,38 @@ class Menu extends React.Component { menuSizeAnimation, } = this.state; - const menuSize = { - width: menuSizeAnimation.x, - height: menuSizeAnimation.y, - }; - // Adjust position of menu let { left, top } = this.state; const transforms = []; - const dimensions = Dimensions.get('screen'); + const { width: screenWidth, height: screenHeight } = Dimensions.get( + 'screen' + ); // Flip by X axis if menu hits right screen border - if (left > dimensions.width - menuWidth - SCREEN_INDENT) { + if (left > screenWidth - menuWidth - SCREEN_INDENT) { transforms.push({ translateX: Animated.multiply(menuSizeAnimation.x, -1), }); - left = Math.min(dimensions.width - SCREEN_INDENT, left + buttonWidth); + left = Math.min(screenWidth - SCREEN_INDENT, left + buttonWidth); } // Flip by Y axis if menu hits bottom screen border - if (top > dimensions.height - menuHeight - SCREEN_INDENT) { + if (top > screenHeight - menuHeight - SCREEN_INDENT) { transforms.push({ translateY: Animated.multiply(menuSizeAnimation.y, -1), }); - top = Math.min(dimensions.height - SCREEN_INDENT, top + buttonHeight); + top = Math.min(screenHeight - SCREEN_INDENT, top + buttonHeight); } - // RTL support - const leftOrRight = I18nManager.isRTL ? { right: left } : { left }; - const shadowMenuContainerStyle = { opacity: opacityAnimation, transform: transforms, borderRadius: theme.roundness, - ...leftOrRight, + right: I18nManager.isRTL ? left : {}, + left: I18nManager.isRTL ? {} : left, top, }; @@ -279,7 +273,13 @@ class Menu extends React.Component { ]} > {children} From cb5459aa1385dc2f91c5d5b88efc4f2c7b7ca172 Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Mon, 18 Feb 2019 21:04:13 +0000 Subject: [PATCH 10/22] fix: update docs wording for more clarity --- src/components/Menu/Menu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js index b242a10b3c..27f1b3d20b 100644 --- a/src/components/Menu/Menu.js +++ b/src/components/Menu/Menu.js @@ -58,7 +58,7 @@ const EASING = Easing.bezier(0.4, 0, 0.2, 1); const SCREEN_INDENT = 8; /** - * Menus display a list of choices on temporary surfaces. Their placement varies based on the element that opens them. + * Menus display a list of choices on temporary elevated surfaces. Their placement varies based on the element that opens them. * *
* From dd97efcc46bdf2703928cc8295bac17a595e151c Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Mon, 18 Feb 2019 21:10:09 +0000 Subject: [PATCH 11/22] docs: more fixes to use the correct wording for clarity --- src/components/Menu/MenuItem.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Menu/MenuItem.js b/src/components/Menu/MenuItem.js index ed4d341a5b..c7c93db018 100644 --- a/src/components/Menu/MenuItem.js +++ b/src/components/Menu/MenuItem.js @@ -21,7 +21,7 @@ type Props = { */ icon?: IconSource, /** - * Whether the `MenuItem` is disabled. A disabled `MenuItem` is greyed out and `onPress` is not called on touch. + * Whether the 'item' is disabled. A disabled 'item' is greyed out and `onPress` is not called on touch. */ disabled?: boolean, /** @@ -36,7 +36,7 @@ type Props = { }; /** - * A component to show list of options inside a Menu. + * A component to show a single list item inside a Menu. * */ From 7e7d0a82228338230294ae9c1b4f35155b0a4223 Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Tue, 19 Feb 2019 20:24:21 +0000 Subject: [PATCH 12/22] fix: add some constants to make values more clear --- src/components/Menu/MenuItem.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Menu/MenuItem.js b/src/components/Menu/MenuItem.js index c7c93db018..fa54d8d314 100644 --- a/src/components/Menu/MenuItem.js +++ b/src/components/Menu/MenuItem.js @@ -93,6 +93,7 @@ class MenuItem extends React.Component { const minWidth = 112; const maxWidth = 280; +const iconWidth = 40; const styles = StyleSheet.create({ container: { @@ -104,7 +105,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', }, icon: { - width: 40, + width: iconWidth, }, title: { fontSize: 16, @@ -118,7 +119,7 @@ const styles = StyleSheet.create({ maxWidth: maxWidth - 16, }, widthWithIcon: { - maxWidth: maxWidth - (8 * 6 + 40), + maxWidth: maxWidth - (iconWidth + 48), }, }); From 52582e8c544a423ae8110465e0f93531720d5684 Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Tue, 19 Feb 2019 20:35:07 +0000 Subject: [PATCH 13/22] fix: use consts for disabled colors --- src/components/Menu/MenuItem.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/components/Menu/MenuItem.js b/src/components/Menu/MenuItem.js index fa54d8d314..c4426b4cf2 100644 --- a/src/components/Menu/MenuItem.js +++ b/src/components/Menu/MenuItem.js @@ -46,21 +46,24 @@ class MenuItem extends React.Component { render() { const { icon, title, disabled, onPress, theme, style } = this.props; - let titleColor = color(theme.colors.text) - .alpha(0.87) - .rgb() - .string(); - let iconColor = color(theme.colors.text) - .alpha(0.54) + const disabledColor = color(theme.dark ? white : black) + .alpha(0.32) .rgb() .string(); - if (disabled) { - iconColor = titleColor = color(theme.dark ? white : black) - .alpha(0.32) - .rgb() - .string(); - } + const titleColor = disabled + ? disabledColor + : color(theme.colors.text) + .alpha(0.87) + .rgb() + .string(); + + const iconColor = disabled + ? disabledColor + : color(theme.colors.text) + .alpha(0.54) + .rgb() + .string(); return ( Date: Wed, 20 Feb 2019 21:26:36 +0000 Subject: [PATCH 14/22] fix: change prop name from button to anchor --- example/src/Examples/MenuExample.js | 4 ++-- src/components/Menu/Menu.js | 32 ++++++++++++++--------------- typings/components/Menu.d.ts | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/example/src/Examples/MenuExample.js b/example/src/Examples/MenuExample.js index 8ffe9a1fca..c68a1dd2fe 100644 --- a/example/src/Examples/MenuExample.js +++ b/example/src/Examples/MenuExample.js @@ -56,7 +56,7 @@ class MenuExample extends React.Component { { Menu with icons diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js index 27f1b3d20b..8543d64668 100644 --- a/src/components/Menu/Menu.js +++ b/src/components/Menu/Menu.js @@ -23,9 +23,9 @@ type Props = { */ visible: boolean, /** - * The button to trigger the menu. + * The anchor to open the menu from. In most cases, it will be a button that opens the manu. */ - button: React.Node, + anchor: React.Node, /** * Callback called when Menu is dismissed. The `visible` prop needs to be updated when this is called. */ @@ -46,8 +46,8 @@ type State = { left: number, menuWidth: number, menuHeight: number, - buttonWidth: number, - buttonHeight: number, + anchorWidth: number, + anchorHeight: number, opacityAnimation: Animated.Value, menuSizeAnimation: Animated.ValueXY, menuState: 'hidden' | 'animating' | 'shown', @@ -86,7 +86,7 @@ const SCREEN_INDENT = 8; * Show menu * } * > @@ -111,8 +111,8 @@ class Menu extends React.Component { left: 0, menuWidth: 0, menuHeight: 0, - buttonWidth: 0, - buttonHeight: 0, + anchorWidth: 0, + anchorHeight: 0, opacityAnimation: new Animated.Value(0), menuSizeAnimation: new Animated.ValueXY({ x: 0, y: 0 }), }; @@ -160,10 +160,10 @@ class Menu extends React.Component { ); }; - // Save button width and height for menu layout - _onButtonLayout = e => { + // Save anchor width and height for menu layout + _onAnchorLayout = e => { const { width, height } = e.nativeEvent.layout; - this.setState({ buttonWidth: width, buttonHeight: height }); + this.setState({ anchorWidth: width, anchorHeight: height }); }; _updateVisibility = () => { @@ -206,14 +206,14 @@ class Menu extends React.Component { }; render() { - const { visible, button, style, children, theme } = this.props; + const { visible, anchor, style, children, theme } = this.props; const { menuState, menuWidth, menuHeight, - buttonWidth, - buttonHeight, + anchorWidth, + anchorHeight, opacityAnimation, menuSizeAnimation, } = this.state; @@ -232,7 +232,7 @@ class Menu extends React.Component { translateX: Animated.multiply(menuSizeAnimation.x, -1), }); - left = Math.min(screenWidth - SCREEN_INDENT, left + buttonWidth); + left = Math.min(screenWidth - SCREEN_INDENT, left + anchorWidth); } // Flip by Y axis if menu hits bottom screen border @@ -241,7 +241,7 @@ class Menu extends React.Component { translateY: Animated.multiply(menuSizeAnimation.y, -1), }); - top = Math.min(screenHeight - SCREEN_INDENT, top + buttonHeight); + top = Math.min(screenHeight - SCREEN_INDENT, top + anchorHeight); } const shadowMenuContainerStyle = { @@ -258,7 +258,7 @@ class Menu extends React.Component { return ( - {button} + {anchor} diff --git a/typings/components/Menu.d.ts b/typings/components/Menu.d.ts index e2187fb2d3..e217d165e5 100644 --- a/typings/components/Menu.d.ts +++ b/typings/components/Menu.d.ts @@ -4,7 +4,7 @@ import { ThemeShape, IconSource } from '../types'; export interface MenuProps { visible: boolean; - button: React.ReactNode; + anchor: React.ReactNode; onDismiss: () => mixed; children: React.ReactNode; style?: any; From 19bf0ba5a04639c829ef137176a702e97bd1d6a9 Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Wed, 20 Feb 2019 21:58:02 +0000 Subject: [PATCH 15/22] fix: use any instead of mixed in type definition --- typings/components/Menu.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/components/Menu.d.ts b/typings/components/Menu.d.ts index e217d165e5..3ecf9c964e 100644 --- a/typings/components/Menu.d.ts +++ b/typings/components/Menu.d.ts @@ -5,7 +5,7 @@ import { ThemeShape, IconSource } from '../types'; export interface MenuProps { visible: boolean; anchor: React.ReactNode; - onDismiss: () => mixed; + onDismiss: () => any; children: React.ReactNode; style?: any; theme?: ThemeShape; @@ -15,7 +15,7 @@ export interface ItemProps { title: React.ReactNode; icon?: IconSource; disabled?: boolean; - onPress?: () => mixed; + onPress?: () => any; theme?: ThemeShape; style?: any; } From 6c7f9323cccf62b2e009cf257ccaa065c59e878f Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Thu, 21 Feb 2019 22:17:33 +0000 Subject: [PATCH 16/22] fix: use the correct types in the definitions --- src/components/Menu/Menu.js | 2 +- typings/components/Menu.d.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js index 8543d64668..9bc4c687a0 100644 --- a/src/components/Menu/Menu.js +++ b/src/components/Menu/Menu.js @@ -123,7 +123,7 @@ class Menu extends React.Component { } } - _container: any; + _container: ?View; _setContainerRef = ref => { this._container = ref; diff --git a/typings/components/Menu.d.ts b/typings/components/Menu.d.ts index 3ecf9c964e..bf5081d7b6 100644 --- a/typings/components/Menu.d.ts +++ b/typings/components/Menu.d.ts @@ -5,9 +5,9 @@ import { ThemeShape, IconSource } from '../types'; export interface MenuProps { visible: boolean; anchor: React.ReactNode; - onDismiss: () => any; + onDismiss: () => void; children: React.ReactNode; - style?: any; + style?: StyleProp; theme?: ThemeShape; } @@ -15,9 +15,9 @@ export interface ItemProps { title: React.ReactNode; icon?: IconSource; disabled?: boolean; - onPress?: () => any; + onPress?: () => void; theme?: ThemeShape; - style?: any; + style?: StyleProp; } export declare class Menu extends React.Component { From c448c7ce99a946b1f8f82555a97a94e014d61f74 Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Thu, 21 Feb 2019 22:20:09 +0000 Subject: [PATCH 17/22] fix: use simpler turnery operation so 'right' or 'left' won't be empty --- src/components/Menu/Menu.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js index 9bc4c687a0..3165205c79 100644 --- a/src/components/Menu/Menu.js +++ b/src/components/Menu/Menu.js @@ -248,9 +248,8 @@ class Menu extends React.Component { opacity: opacityAnimation, transform: transforms, borderRadius: theme.roundness, - right: I18nManager.isRTL ? left : {}, - left: I18nManager.isRTL ? {} : left, top, + ...(I18nManager.isRTL ? { right: left } : { left }), }; const animationStarted = menuState === 'animating'; From 5ac0fadc8870ae2ba561122d49193f7c0fdeaa6e Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Thu, 21 Feb 2019 22:46:38 +0000 Subject: [PATCH 18/22] docs: added comments for more clarity on origin of constants --- src/components/Menu/Menu.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js index 3165205c79..7b615232c1 100644 --- a/src/components/Menu/Menu.js +++ b/src/components/Menu/Menu.js @@ -53,10 +53,14 @@ type State = { menuState: 'hidden' | 'animating' | 'shown', }; -const ANIMATION_DURATION = 300; -const EASING = Easing.bezier(0.4, 0, 0.2, 1); +// Minimum padding between the edge of the screen and the menu const SCREEN_INDENT = 8; +// From https://material.io/design/motion/speed.html#duration +const ANIMATION_DURATION = 250; +// From the 'Standard easing' section of https://material.io/design/motion/speed.html#easing +const EASING = Easing.bezier(0.4, 0, 0.2, 1); + /** * Menus display a list of choices on temporary elevated surfaces. Their placement varies based on the element that opens them. * From b7456b9931cc1228eda90b6323ce220e02783dd0 Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Sat, 2 Mar 2019 18:07:06 +0100 Subject: [PATCH 19/22] use portal --- src/components/Menu/Menu.js | 108 ++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js index 7b615232c1..ef9de6ab94 100644 --- a/src/components/Menu/Menu.js +++ b/src/components/Menu/Menu.js @@ -6,7 +6,6 @@ import { Animated, View, Easing, - Modal, Dimensions, TouchableWithoutFeedback, I18nManager, @@ -14,6 +13,7 @@ import { } from 'react-native'; import { withTheme } from '../../core/theming'; import type { Theme } from '../../types'; +import Portal from '../Portal/Portal'; import Surface from '../Surface'; import MenuItem from './MenuItem'; @@ -44,10 +44,8 @@ type Props = { type State = { top: number, left: number, - menuWidth: number, - menuHeight: number, - anchorWidth: number, - anchorHeight: number, + menuLayout: { height: number, width: number }, + anchorLayout: { height: number, width: number }, opacityAnimation: Animated.Value, menuSizeAnimation: Animated.ValueXY, menuState: 'hidden' | 'animating' | 'shown', @@ -113,10 +111,8 @@ class Menu extends React.Component { menuState: 'hidden', top: 0, left: 0, - menuWidth: 0, - menuHeight: 0, - anchorWidth: 0, - anchorHeight: 0, + menuLayout: { width: 0, height: 0 }, + anchorLayout: { width: 0, height: 0 }, opacityAnimation: new Animated.Value(0), menuSizeAnimation: new Animated.ValueXY({ x: 0, y: 0 }), }; @@ -129,10 +125,6 @@ class Menu extends React.Component { _container: ?View; - _setContainerRef = ref => { - this._container = ref; - }; - // Start menu animation _onMenuLayout = e => { if (this.state.menuState === 'animating') { @@ -144,8 +136,7 @@ class Menu extends React.Component { this.setState( { menuState: 'animating', - menuWidth: width, - menuHeight: height, + menuLayout: { width, height }, }, () => { Animated.parallel([ @@ -167,7 +158,8 @@ class Menu extends React.Component { // Save anchor width and height for menu layout _onAnchorLayout = e => { const { width, height } = e.nativeEvent.layout; - this.setState({ anchorWidth: width, anchorHeight: height }); + + this.setState({ anchorLayout: { width, height } }); }; _updateVisibility = () => { @@ -180,15 +172,19 @@ class Menu extends React.Component { _show = () => { BackHandler.addEventListener('hardwareBackPress', this._hide); - this._container.measureInWindow((x, y) => { - const top = Math.max(SCREEN_INDENT, y); - const left = Math.max(SCREEN_INDENT, x); - this.setState({ menuState: 'shown', top, left }); - }); + + if (this._container) { + this._container.measureInWindow((x, y) => { + const top = Math.max(SCREEN_INDENT, y); + const left = Math.max(SCREEN_INDENT, x); + this.setState({ menuState: 'shown', top, left }); + }); + } }; _hide = () => { BackHandler.removeEventListener('hardwareBackPress', this._hide); + Animated.timing(this.state.opacityAnimation, { toValue: 0, duration: ANIMATION_DURATION, @@ -214,10 +210,8 @@ class Menu extends React.Component { const { menuState, - menuWidth, - menuHeight, - anchorWidth, - anchorHeight, + menuLayout, + anchorLayout, opacityAnimation, menuSizeAnimation, } = this.state; @@ -231,21 +225,21 @@ class Menu extends React.Component { ); // Flip by X axis if menu hits right screen border - if (left > screenWidth - menuWidth - SCREEN_INDENT) { + if (left > screenWidth - menuLayout.width - SCREEN_INDENT) { transforms.push({ translateX: Animated.multiply(menuSizeAnimation.x, -1), }); - left = Math.min(screenWidth - SCREEN_INDENT, left + anchorWidth); + left = Math.min(screenWidth - SCREEN_INDENT, left + anchorLayout.width); } // Flip by Y axis if menu hits bottom screen border - if (top > screenHeight - menuHeight - SCREEN_INDENT) { + if (top > screenHeight - menuLayout.height - SCREEN_INDENT) { transforms.push({ translateY: Animated.multiply(menuSizeAnimation.y, -1), }); - top = Math.min(screenHeight - SCREEN_INDENT, top + anchorHeight); + top = Math.min(screenHeight - SCREEN_INDENT, top + anchorLayout.height); } const shadowMenuContainerStyle = { @@ -257,37 +251,43 @@ class Menu extends React.Component { }; const animationStarted = menuState === 'animating'; - const modalVisible = menuState === 'shown' || animationStarted || visible; + const menuVisible = menuState === 'shown' || animationStarted || visible; return ( - + { + this._container = c; + }} + collapsable={false} + > {anchor} - - - - - - - + + + + - {children} - - - + + {children} + + + + ) : null} ); } From 1e1344611b38d68d5b321ea2df266b841bf5f070 Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Wed, 6 Mar 2019 20:46:21 +0000 Subject: [PATCH 20/22] fix: add status bar padding to top --- src/components/Menu/Menu.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js index ef9de6ab94..644b36ebfe 100644 --- a/src/components/Menu/Menu.js +++ b/src/components/Menu/Menu.js @@ -10,6 +10,7 @@ import { TouchableWithoutFeedback, I18nManager, BackHandler, + StatusBar, } from 'react-native'; import { withTheme } from '../../core/theming'; import type { Theme } from '../../types'; @@ -175,8 +176,13 @@ class Menu extends React.Component { if (this._container) { this._container.measureInWindow((x, y) => { - const top = Math.max(SCREEN_INDENT, y); + let top = Math.max(SCREEN_INDENT, y); const left = Math.max(SCREEN_INDENT, x); + + if (StatusBar.currentHeight && top < StatusBar.currentHeight) { + top += StatusBar.currentHeight; + } + this.setState({ menuState: 'shown', top, left }); }); } From 6ea16bbb1d7929750dfad26c557d83ab0df1171a Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Sun, 10 Mar 2019 15:08:51 +0000 Subject: [PATCH 21/22] refactor: remove inner view --- src/components/Menu/Menu.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js index 644b36ebfe..bdb8090f2e 100644 --- a/src/components/Menu/Menu.js +++ b/src/components/Menu/Menu.js @@ -265,8 +265,9 @@ class Menu extends React.Component { this._container = c; }} collapsable={false} + onLayout={this._onAnchorLayout} > - {anchor} + {anchor} {menuVisible ? ( From 9adb5d07fc75c395e7b8caeeab4213f778cb0cc9 Mon Sep 17 00:00:00 2001 From: Waquid Valiya Peedikakkal Date: Sun, 10 Mar 2019 16:31:49 +0000 Subject: [PATCH 22/22] fix: add status bar height for all cases --- src/components/Menu/Menu.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js index bdb8090f2e..3887327db6 100644 --- a/src/components/Menu/Menu.js +++ b/src/components/Menu/Menu.js @@ -176,13 +176,9 @@ class Menu extends React.Component { if (this._container) { this._container.measureInWindow((x, y) => { - let top = Math.max(SCREEN_INDENT, y); + const top = Math.max(SCREEN_INDENT, y) + StatusBar.currentHeight; const left = Math.max(SCREEN_INDENT, x); - if (StatusBar.currentHeight && top < StatusBar.currentHeight) { - top += StatusBar.currentHeight; - } - this.setState({ menuState: 'shown', top, left }); }); }