How to create a More-Popup Menu in React-Native

Categories:

React-Native has pretty much all components you need, but the one that seems to be missing is a standard More-Popup Menu (also called DropDown Menu). I'm talking about the "three dots menu" you see in every application showing additional actions when you click on it:

react native more popup dropdown menu

There are already several NPM packages that try to recreate it by using buttons toggling a modal which renders a custom view representing the menu, but they all come with some layout problems, mostly when the More-Button is close to a border, it just overflows and looks hideous. If you look through the React-Native components or even search the doc for "Popup"/"Dropdown" nothing shows up. But ...

react-native-has-a-popup-component

If you dig deep down the github rabbit hole as mentioned here, you can find an undocumented UIManager.java class that allows you to create Popups with its showPopupMenu method:

(UIManagerModule.showPopupMenu is only available for Android - there is currently no similar behavior for iOS.)

  /**
   * Show a PopupMenu.
   *
   * @param reactTag the tag of the anchor view (the PopupMenu is displayed next to this view); this
   *        needs to be the tag of a native view (shadow views can not be anchors)
   * @param items the menu items as an array of strings
   * @param error will be called if there is an error displaying the menu
   * @param success will be called with the position of the selected item as the first argument, or
   *        no arguments if the menu is dismissed
   */
  @ReactMethod
  public void showPopupMenu(int reactTag, ReadableArray items, Callback error, Callback success) {
    mUIImplementation.showPopupMenu(reactTag, items, error, success);
  }

The description is straight-forward, so let's see how to build a React component around it. We will use react-native-vector-icons to display an Icon with three vertical dots, and let the component take two properties:

  1. An actions array representing the menu items as strings
  2. An onPress callback handling clicks on these menu items: It gets called with the event name and the index of the pressed menu item. (The event names are either "itemSelected" or "dismissed".)

The implementation can be seen here.

import React, { Component, PropTypes } from 'react'
import { View, UIManager, findNodeHandle, TouchableOpacity } from 'react-native'
import Icon from 'react-native-vector-icons/MaterialIcons'

const ICON_SIZE = 24

export default class PopupMenu extends Component {
  static propTypes = {
    // array of strings, will be list items of Menu
    actions:  PropTypes.arrayOf(PropTypes.string).isRequired,
    onPress: PropTypes.func.isRequired
  }

  constructor (props) {
    super(props)
    this.state = {
      icon: null
    }
  }

  onError () {
    console.log('Popup Error')
  }

  onPress = () => {
    if (this.state.icon) {
      UIManager.showPopupMenu(
        findNodeHandle(this.state.icon),
        this.props.actions,
        this.onError,
        this.props.onPress
      )
    }
  }

  render () {
    return (
      <View>
        <TouchableOpacity onPress={this.onPress}>
          <Icon
            name='more-vert'
            size={ICON_SIZE}
            color={'grey'}
            ref={this.onRef} />
        </TouchableOpacity>
      </View>
    )
  }

  onRef = icon => {
    if (!this.state.icon) {
      this.setState({icon})
    }
  }
}

Here's an example usage of this React-Native Popup Component:

  render () {
    return (
      <View>
        <PopupMenu actions={['Edit', 'Remove']} onPress={this.onPopupEvent} />
      </View>
    )
  }

  onPopupEvent = (eventName, index) => {
    if (eventName !== 'itemSelected') return
    if (index === 0) this.onEdit()
    else this.onRemove()
  }