Repository: callstack/react-native-paper
Pull request #965 (2019-03-30 15:21:10)

feat: custom icon for clear in seachbar
<!-- Please provide enough information so that others can review your pull request. -->
<!-- Keep pull requests small and focused on a single change. -->

### Motivation

Closes https://github.com/callstack/react-native-paper/issues/707

<!-- What existing problem does the pull request solve? Can you solve the issue with a different approach? -->

### Test plan

```
<Searchbar
      placeholder="Search"
      value="Test"
      clearIcon="custom-icon-name"
/>
```

<!-- List the steps with which we can test this change. Provide screenshots if this changes anything visual. -->


callstack-bot: Hey @raajnadar, thank you for your pull request 🤗. The documentation from this branch can be viewed [here](https://9348-71323749-gh.circle-artifacts.com/0/docs/index.html). Please remember to update Typescript types if you changed API.

Patch:
diff --git a/README.md b/README.md
index 1290229e1..14f653dfc 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,8 @@
   <img alt="react-native-paper" src="docs/assets/images/paper-logo.svg?sanitize=true" width="300">
 </p>
 <p align="center">
-  Material design for React Native.
+  Material design for React Native.<br/>
+  <a href="https://reactnativepaper.com">reactnativepaper.com</a>
 </p>
 
 ---
diff --git a/docs/component-docs.config.js b/docs/component-docs.config.js
index 8771e79bf..bd3e2f733 100644
--- a/docs/component-docs.config.js
+++ b/docs/component-docs.config.js
@@ -13,7 +13,7 @@ const assets = [
 ];
 const styles = [path.join(__dirname, 'assets', 'styles.css')];
 const scripts = [path.join(__dirname, 'assets', 'snack.js')];
-const github = 'https://github.com/callstack/react-native-paper/edit/master';
+const github = 'https://github.com/callstack/react-native-paper/edit/master/';
 
 if (!fs.existsSync(dist)) {
   fs.mkdirSync(dist);
diff --git a/package.json b/package.json
index 388955d5b..c41ec841c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "react-native-paper",
-  "version": "2.15.0",
+  "version": "2.15.2",
   "description": "Material design for React Native",
   "main": "src/index.js",
   "typings": "typings/index.d.ts",
diff --git a/src/components/Chip.js b/src/components/Chip.js
index 0c5ec199b..5c693d795 100644
--- a/src/components/Chip.js
+++ b/src/components/Chip.js
@@ -152,6 +152,7 @@ class Chip extends React.Component<Props, State> {
         : dark
           ? '#383838'
           : '#ebebeb',
+      borderRadius = 16,
     } = StyleSheet.flatten(style) || {};
 
     const borderColor =
@@ -214,6 +215,7 @@ class Chip extends React.Component<Props, State> {
               ? selectedBackgroundColor
               : backgroundColor,
             borderColor,
+            borderRadius,
           },
           style,
         ]}
@@ -222,7 +224,7 @@ class Chip extends React.Component<Props, State> {
         <TouchableRipple
           borderless
           delayPressIn={0}
-          style={styles.touchable}
+          style={{ borderRadius }}
           onPress={onPress}
           onPressIn={this._handlePressIn}
           onPressOut={this._handlePressOut}
@@ -296,13 +298,9 @@ class Chip extends React.Component<Props, State> {
 
 const styles = StyleSheet.create({
   container: {
-    borderRadius: 16,
     borderWidth: StyleSheet.hairlineWidth,
     borderStyle: 'solid',
   },
-  touchable: {
-    borderRadius: 16,
-  },
   content: {
     flexDirection: 'row',
     alignItems: 'center',
diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js
index 3887327db..92a21bbb3 100644
--- a/src/components/Menu/Menu.js
+++ b/src/components/Menu/Menu.js
@@ -10,7 +10,7 @@ import {
   TouchableWithoutFeedback,
   I18nManager,
   BackHandler,
-  StatusBar,
+  Platform,
 } from 'react-native';
 import { withTheme } from '../../core/theming';
 import type { Theme } from '../../types';
@@ -48,13 +48,11 @@ type State = {
   menuLayout: { height: number, width: number },
   anchorLayout: { height: number, width: number },
   opacityAnimation: Animated.Value,
-  menuSizeAnimation: Animated.ValueXY,
-  menuState: 'hidden' | 'animating' | 'shown',
+  scaleAnimation: Animated.ValueXY,
 };
 
 // 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
@@ -72,7 +70,7 @@ const EASING = Easing.bezier(0.4, 0, 0.2, 1);
  * ```js
  * import * as React from 'react';
  * import { View } from 'react-native';
- * import { Button, Paragraph, Menu, Divider } from 'react-native-paper';
+ * import { Button, Paragraph, Menu, Divider, Provider } from 'react-native-paper';
  *
  * export default class MyComponent extends React.Component {
  *   state = {
@@ -85,20 +83,27 @@ const EASING = Easing.bezier(0.4, 0, 0.2, 1);
  *
  *   render() {
  *     return (
- *       <View>
- *         <Menu
- *           visible={this.state.visible}
- *           onDismiss={this._closeMenu}
- *           anchor={
- *             <Button onPress={this._openMenu}>Show menu</Button>
- *           }
- *         >
- *           <Menu.Item onPress={() => {}} title="Item 1" />
- *           <Menu.Item onPress={() => {}} title="Item 2" />
- *           <Divider />
- *           <Menu.Item onPress={() => {}} title="Item 3" />
- *         </Menu>
- *       </View>
+ *       <Provider>
+ *         <View
+ *           style={{
+ *             paddingTop: 50,
+ *             flexDirection: 'row',
+ *             justifyContent: 'center'
+ *           }}>
+ *           <Menu
+ *             visible={this.state.visible}
+ *             onDismiss={this._closeMenu}
+ *             anchor={
+ *               <Button onPress={this._openMenu}>Show menu</Button>
+ *             }
+ *           >
+ *             <Menu.Item onPress={() => {}} title="Item 1" />
+ *             <Menu.Item onPress={() => {}} title="Item 2" />
+ *             <Divider />
+ *             <Menu.Item onPress={() => {}} title="Item 3" />
+ *           </Menu>
+ *         </View>
+ *       </Provider>
  *     );
  *   }
  * }
@@ -109,13 +114,12 @@ class Menu extends React.Component<Props, State> {
   static Item = MenuItem;
 
   state = {
-    menuState: 'hidden',
     top: 0,
     left: 0,
     menuLayout: { width: 0, height: 0 },
     anchorLayout: { width: 0, height: 0 },
     opacityAnimation: new Animated.Value(0),
-    menuSizeAnimation: new Animated.ValueXY({ x: 0, y: 0 }),
+    scaleAnimation: new Animated.ValueXY({ x: 0, y: 0 }),
   };
 
   componentDidUpdate(prevProps) {
@@ -124,188 +128,285 @@ class Menu extends React.Component<Props, State> {
     }
   }
 
-  _container: ?View;
+  componentWillUnmount() {
+    BackHandler.removeEventListener('hardwareBackPress', this.props.onDismiss);
+  }
 
-  // Start menu animation
-  _onMenuLayout = e => {
-    if (this.state.menuState === 'animating') {
-      return;
+  _anchor: ?View;
+  _menu: ?View;
+
+  _measureMenuLayout = () =>
+    new Promise(resolve => {
+      if (this._menu) {
+        this._menu.measureInWindow((x, y, width, height) => {
+          resolve({ x, y, width, height });
+        });
+      }
+    });
+
+  _measureAnchorLayout = () =>
+    new Promise(resolve => {
+      if (this._anchor) {
+        this._anchor.measureInWindow((x, y, width, height) => {
+          resolve({ x, y, width, height });
+        });
+      }
+    });
+
+  _updateVisibility = () => {
+    if (this.props.visible) {
+      this._show();
+    } else {
+      this._hide();
     }
+  };
 
-    const { width, height } = e.nativeEvent.layout;
+  _show = async () => {
+    BackHandler.addEventListener('hardwareBackPress', this.props.onDismiss);
+
+    const [menuLayout, anchorLayout] = await Promise.all([
+      this._measureMenuLayout(),
+      this._measureAnchorLayout(),
+    ]);
+
+    // When visible is true for first render
+    // native views can be still not rendered and
+    // measureMenuLayout/measureAnchorLayout functions
+    // return wrong values e.g { x:0, y: 0, width: 0, height: 0 }
+    // so we have to wait until views are ready
+    // and rerun this function to show menu
+    if (
+      !menuLayout.width ||
+      !menuLayout.height ||
+      !anchorLayout.width ||
+      !anchorLayout.height
+    ) {
+      BackHandler.removeEventListener(
+        'hardwareBackPress',
+        this.props.onDismiss
+      );
+      setTimeout(() => {
+        this._show();
+      }, ANIMATION_DURATION);
+      return;
+    }
 
     this.setState(
       {
-        menuState: 'animating',
-        menuLayout: { width, height },
+        left: anchorLayout.x,
+        top: anchorLayout.y,
+        anchorLayout: {
+          height: anchorLayout.height,
+          width: anchorLayout.width,
+        },
+        menuLayout: {
+          width: menuLayout.width,
+          height: menuLayout.height,
+        },
       },
       () => {
         Animated.parallel([
-          Animated.timing(this.state.menuSizeAnimation, {
-            toValue: { x: width, y: height },
+          Animated.timing(this.state.scaleAnimation, {
+            toValue: { x: menuLayout.width, y: menuLayout.height },
             duration: ANIMATION_DURATION,
             easing: EASING,
+            useNativeDriver: true,
           }),
           Animated.timing(this.state.opacityAnimation, {
             toValue: 1,
             duration: ANIMATION_DURATION,
             easing: EASING,
+            useNativeDriver: true,
           }),
         ]).start();
       }
     );
   };
 
-  // Save anchor width and height for menu layout
-  _onAnchorLayout = e => {
-    const { width, height } = e.nativeEvent.layout;
-
-    this.setState({ anchorLayout: { width, height } });
-  };
-
-  _updateVisibility = () => {
-    if (this.props.visible) {
-      this._show();
-    } else {
-      this._hide();
-    }
-  };
-
-  _show = () => {
-    BackHandler.addEventListener('hardwareBackPress', this._hide);
-
-    if (this._container) {
-      this._container.measureInWindow((x, y) => {
-        const top = Math.max(SCREEN_INDENT, y) + StatusBar.currentHeight;
-        const left = Math.max(SCREEN_INDENT, x);
-
-        this.setState({ menuState: 'shown', top, left });
-      });
-    }
-  };
-
   _hide = () => {
-    BackHandler.removeEventListener('hardwareBackPress', this._hide);
+    BackHandler.removeEventListener('hardwareBackPress', this.props.onDismiss);
 
     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),
-        });
+      useNativeDriver: true,
+    }).start(finished => {
+      if (finished) {
+        this.state.scaleAnimation.setValue({ x: 0, y: 0 });
       }
     });
   };
 
   render() {
-    const { visible, anchor, style, children, theme } = this.props;
+    const { visible, anchor, style, children, theme, onDismiss } = this.props;
 
     const {
-      menuState,
       menuLayout,
       anchorLayout,
       opacityAnimation,
-      menuSizeAnimation,
+      scaleAnimation,
     } = this.state;
 
-    // Adjust position of menu
+    // I don't know why but on Android measure function is wrong by 24
+    const additionalVerticalValue = Platform.select({
+      android: 24,
+      default: 0,
+    });
+
     let { left, top } = this.state;
-    const transforms = [];
+
+    const scaleTransforms = [
+      {
+        scaleX: scaleAnimation.x.interpolate({
+          inputRange: [0, menuLayout.width],
+          outputRange: [0, 1],
+        }),
+      },
+      {
+        scaleY: scaleAnimation.y.interpolate({
+          inputRange: [0, menuLayout.height],
+          outputRange: [0, 1],
+        }),
+      },
+    ];
+
+    // 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'
     );
 
-    // Flip by X axis if menu hits right screen border
-    if (left > screenWidth - menuLayout.width - SCREEN_INDENT) {
-      transforms.push({
-        translateX: Animated.multiply(menuSizeAnimation.x, -1),
+    // Check if menu fits horizontally and if not align it to right.
+    if (left <= screenWidth - menuLayout.width - SCREEN_INDENT) {
+      positionTransforms.push({
+        translateX: scaleAnimation.x.interpolate({
+          inputRange: [0, menuLayout.width],
+          outputRange: [-(menuLayout.width / 2), 0],
+        }),
+      });
+
+      // Check if menu position has enough space from left side
+      if (left >= 0 && left < SCREEN_INDENT) {
+        left = SCREEN_INDENT;
+      }
+    } else {
+      positionTransforms.push({
+        translateX: scaleAnimation.x.interpolate({
+          inputRange: [0, menuLayout.width],
+          outputRange: [menuLayout.width / 2, 0],
+        }),
       });
 
-      left = Math.min(screenWidth - SCREEN_INDENT, left + anchorLayout.width);
+      left += anchorLayout.width - menuLayout.width;
+
+      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;
+      }
     }
 
-    // Flip by Y axis if menu hits bottom screen border
-    if (top > screenHeight - menuLayout.height - SCREEN_INDENT) {
-      transforms.push({
-        translateY: Animated.multiply(menuSizeAnimation.y, -1),
+    // Check if menu fits vertically and if not align it to bottom.
+    if (top <= screenHeight - menuLayout.height - SCREEN_INDENT) {
+      positionTransforms.push({
+        translateY: scaleAnimation.y.interpolate({
+          inputRange: [0, menuLayout.height],
+          outputRange: [-(menuLayout.height / 2), 0],
+        }),
       });
 
-      top = Math.min(screenHeight - SCREEN_INDENT, top + anchorLayout.height);
+      // Check if menu position has enough space from top side
+      if (top >= 0 && top < SCREEN_INDENT) {
+        top = SCREEN_INDENT;
+      }
+    } else {
+      positionTransforms.push({
+        translateY: scaleAnimation.y.interpolate({
+          inputRange: [0, menuLayout.height],
+          outputRange: [menuLayout.height / 2, 0],
+        }),
+      });
+
+      top += anchorLayout.height - menuLayout.height;
+
+      const bottom = top + menuLayout.height + additionalVerticalValue;
+      // Check if menu position has enough space from bottom side
+      if (bottom <= screenHeight && bottom > screenHeight - SCREEN_INDENT) {
+        top =
+          screenHeight -
+          SCREEN_INDENT -
+          menuLayout.height -
+          additionalVerticalValue;
+      }
     }
 
     const shadowMenuContainerStyle = {
       opacity: opacityAnimation,
-      transform: transforms,
+      transform: scaleTransforms,
       borderRadius: theme.roundness,
-      top,
-      ...(I18nManager.isRTL ? { right: left } : { left }),
     };
 
-    const animationStarted = menuState === 'animating';
-    const menuVisible = menuState === 'shown' || animationStarted || visible;
+    const positionStyle = {
+      top: top + additionalVerticalValue,
+      ...(I18nManager.isRTL ? { right: left } : { left }),
+    };
 
     return (
       <View
-        ref={c => {
-          this._container = c;
+        ref={ref => {
+          this._anchor = ref;
         }}
         collapsable={false}
-        onLayout={this._onAnchorLayout}
       >
         {anchor}
-        {menuVisible ? (
-          <Portal>
-            <TouchableWithoutFeedback onPress={this._hide}>
+        <Portal>
+          {visible ? (
+            <TouchableWithoutFeedback onPress={onDismiss}>
               <View style={StyleSheet.absoluteFill} />
             </TouchableWithoutFeedback>
-            <Surface
-              onLayout={this._onMenuLayout}
-              style={[
-                styles.shadowMenuContainer,
-                shadowMenuContainerStyle,
-                style,
-              ]}
-            >
-              <Animated.View
-                style={[
-                  styles.menuContainer,
-                  animationStarted && {
-                    width: menuSizeAnimation.x,
-                    height: menuSizeAnimation.y,
-                  },
-                ]}
+          ) : null}
+          <View
+            ref={ref => {
+              // This hack is needed to properly show menu
+              // when visible is `true` initially
+              // because in componentDidMount _menu ref is undefined
+              // because it's rendered in portal
+              if (!this._menu) {
+                this._menu = ref;
+                if (visible) {
+                  this._show();
+                }
+              }
+            }}
+            collapsable={false}
+            pointerEvents={visible ? 'auto' : 'none'}
+            style={[styles.wrapper, positionStyle, style]}
+          >
+            <Animated.View style={{ transform: positionTransforms }}>
+              <Surface
+                style={[styles.shadowMenuContainer, shadowMenuContainerStyle]}
               >
                 {children}
-              </Animated.View>
-            </Surface>
-          </Portal>
-        ) : null}
+              </Surface>
+            </Animated.View>
+          </View>
+        </Portal>
       </View>
     );
   }
 }
 
 const styles = StyleSheet.create({
-  shadowMenuContainer: {
+  wrapper: {
     position: 'absolute',
+  },
+  shadowMenuContainer: {
     opacity: 0,
-    paddingTop: 8,
+    paddingVertical: 8,
     elevation: 8,
   },
-  menuContainer: {
-    overflow: 'hidden',
-  },
 });
 
 export default withTheme(Menu);
diff --git a/src/components/Searchbar.js b/src/components/Searchbar.js
index 38a1c5533..b1d590726 100644
--- a/src/components/Searchbar.js
+++ b/src/components/Searchbar.js
@@ -44,6 +44,10 @@ type Props = React.ElementConfig<typeof TextInput> & {|
    * Custom color for icon, default will be derived from theme
    */
   iconColor?: string,
+  /**
+   * Custom icon for clear button, default will be icon close
+   */
+  clearIcon?: IconSource,
 |};
 
 /**
@@ -128,6 +132,7 @@ class Searchbar extends React.Component<Props> {
       theme,
       style,
       iconColor: customIconColor,
+      clearIcon,
       inputStyle,
       ...rest
     } = this.props;
@@ -184,7 +189,7 @@ class Searchbar extends React.Component<Props> {
           color={value ? iconColor : 'rgba(255, 255, 255, 0)'}
           rippleColor={rippleColor}
           onPress={this._handleClearPress}
-          icon="close"
+          icon={clearIcon || 'close'}
           accessibilityTraits="button"
           accessibilityComponentType="button"
           accessibilityRole="button"
diff --git a/src/components/TextInput/TextInput.js b/src/components/TextInput/TextInput.js
index efc95b5df..0f9fc85a9 100644
--- a/src/components/TextInput/TextInput.js
+++ b/src/components/TextInput/TextInput.js
@@ -248,7 +248,11 @@ class TextInput extends React.Component<TextInputProps, State> {
     Animated.timing(this.state.error, {
       toValue: 1,
       duration: FOCUS_ANIMATION_DURATION,
-      useNativeDriver: true,
+      // To prevent this - https://github.com/callstack/react-native-paper/issues/941
+      useNativeDriver: Platform.select({
+        ios: false,
+        default: true,
+      }),
     }).start(this._showPlaceholder);
   };
 
@@ -256,7 +260,11 @@ class TextInput extends React.Component<TextInputProps, State> {
     Animated.timing(this.state.error, {
       toValue: 0,
       duration: BLUR_ANIMATION_DURATION,
-      useNativeDriver: true,
+      // To prevent this - https://github.com/callstack/react-native-paper/issues/941
+      useNativeDriver: Platform.select({
+        ios: false,
+        default: true,
+      }),
     }).start();
   };
 
diff --git a/typings/components/Searchbar.d.ts b/typings/components/Searchbar.d.ts
index c6659b7de..5381d8977 100644
--- a/typings/components/Searchbar.d.ts
+++ b/typings/components/Searchbar.d.ts
@@ -5,6 +5,7 @@ import { ThemeShape, IconSource } from '../types';
 export interface SearchbarProps extends TextInputProps {
   icon?: IconSource;
   theme?: ThemeShape;
+  clearIcon?: IconSource;
   onIconPress?: () => any;
   onChangeText?: (query: string) => void;
 }


Test patch:
diff --git a/src/components/__tests__/Menu.test.js b/src/components/__tests__/Menu.test.js
new file mode 100644
index 000000000..6591f8ce9
--- /dev/null
+++ b/src/components/__tests__/Menu.test.js
@@ -0,0 +1,40 @@
+/* @flow */
+
+import * as React from 'react';
+import renderer from 'react-test-renderer';
+import Menu from '../Menu/Menu';
+import Button from '../Button';
+
+it('renders visible menu', () => {
+  const tree = renderer
+    .create(
+      <Menu
+        visible
+        onDismiss={jest.fn()}
+        anchor={<Button mode="outlined">Open menu</Button>}
+      >
+        <Menu.Item onPress={jest.fn()} title="Undo" />
+        <Menu.Item onPress={jest.fn()} title="Redo" />
+      </Menu>
+    )
+    .toJSON();
+
+  expect(tree).toMatchSnapshot();
+});
+
+it('renders not visible menu', () => {
+  const tree = renderer
+    .create(
+      <Menu
+        visible={false}
+        onDismiss={jest.fn()}
+        anchor={<Button mode="outlined">Open menu</Button>}
+      >
+        <Menu.Item onPress={jest.fn()} title="Undo" />
+        <Menu.Item onPress={jest.fn()} title="Redo" />
+      </Menu>
+    )
+    .toJSON();
+
+  expect(tree).toMatchSnapshot();
+});
diff --git a/src/components/__tests__/__snapshots__/Menu.test.js.snap b/src/components/__tests__/__snapshots__/Menu.test.js.snap
new file mode 100644
index 000000000..57226cbcd
--- /dev/null
+++ b/src/components/__tests__/__snapshots__/Menu.test.js.snap
@@ -0,0 +1,189 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders not visible menu 1`] = `
+<View
+  collapsable={false}
+>
+  <View
+    style={
+      Object {
+        "backgroundColor": "transparent",
+        "borderColor": "rgba(0, 0, 0, 0.29)",
+        "borderRadius": 4,
+        "borderStyle": "solid",
+        "borderWidth": 0.5,
+        "elevation": 0,
+        "minWidth": 64,
+        "shadowColor": "#000000",
+        "shadowOffset": Object {
+          "height": 0,
+          "width": 0,
+        },
+        "shadowOpacity": 0.24,
+        "shadowRadius": 0,
+      }
+    }
+  >
+    <View
+      accessibilityRole="button"
+      accessible={true}
+      isTVSelectable={true}
+      onResponderGrant={[Function]}
+      onResponderMove={[Function]}
+      onResponderRelease={[Function]}
+      onResponderTerminate={[Function]}
+      onResponderTerminationRequest={[Function]}
+      onStartShouldSetResponder={[Function]}
+      style={
+        Array [
+          Object {
+            "overflow": "hidden",
+          },
+          Object {
+            "borderRadius": 4,
+          },
+        ]
+      }
+    >
+      <View
+        style={
+          Array [
+            Object {
+              "alignItems": "center",
+              "flexDirection": "row",
+              "justifyContent": "center",
+            },
+            undefined,
+          ]
+        }
+      >
+        <Text
+          numberOfLines={1}
+          style={
+            Array [
+              Object {
+                "color": "#000000",
+                "fontFamily": "Helvetica Neue",
+                "textAlign": "left",
+                "writingDirection": "ltr",
+              },
+              Array [
+                Object {
+                  "letterSpacing": 1,
+                  "marginHorizontal": 16,
+                  "marginVertical": 9,
+                  "textAlign": "center",
+                },
+                undefined,
+                Object {
+                  "color": "#6200ee",
+                  "fontFamily": "HelveticaNeue-Medium",
+                },
+                Object {
+                  "fontFamily": "HelveticaNeue-Medium",
+                },
+              ],
+            ]
+          }
+        >
+          OPEN MENU
+        </Text>
+      </View>
+    </View>
+  </View>
+</View>
+`;
+
+exports[`renders visible menu 1`] = `
+<View
+  collapsable={false}
+>
+  <View
+    style={
+      Object {
+        "backgroundColor": "transparent",
+        "borderColor": "rgba(0, 0, 0, 0.29)",
+        "borderRadius": 4,
+        "borderStyle": "solid",
+        "borderWidth": 0.5,
+        "elevation": 0,
+        "minWidth": 64,
+        "shadowColor": "#000000",
+        "shadowOffset": Object {
+          "height": 0,
+          "width": 0,
+        },
+        "shadowOpacity": 0.24,
+        "shadowRadius": 0,
+      }
+    }
+  >
+    <View
+      accessibilityRole="button"
+      accessible={true}
+      isTVSelectable={true}
+      onResponderGrant={[Function]}
+      onResponderMove={[Function]}
+      onResponderRelease={[Function]}
+      onResponderTerminate={[Function]}
+      onResponderTerminationRequest={[Function]}
+      onStartShouldSetResponder={[Function]}
+      style={
+        Array [
+          Object {
+            "overflow": "hidden",
+          },
+          Object {
+            "borderRadius": 4,
+          },
+        ]
+      }
+    >
+      <View
+        style={
+          Array [
+            Object {
+              "alignItems": "center",
+              "flexDirection": "row",
+              "justifyContent": "center",
+            },
+            undefined,
+          ]
+        }
+      >
+        <Text
+          numberOfLines={1}
+          style={
+            Array [
+              Object {
+                "color": "#000000",
+                "fontFamily": "Helvetica Neue",
+                "textAlign": "left",
+                "writingDirection": "ltr",
+              },
+              Array [
+                Object {
+                  "letterSpacing": 1,
+                  "marginHorizontal": 16,
+                  "marginVertical": 9,
+                  "textAlign": "center",
+                },
+                undefined,
+                Object {
+                  "color": "#6200ee",
+                  "fontFamily": "HelveticaNeue-Medium",
+                },
+                Object {
+                  "fontFamily": "HelveticaNeue-Medium",
+                },
+              ],
+            ]
+          }
+        >
+          OPEN MENU
+        </Text>
+      </View>
+    </View>
+  </View>
+</View>
+`;