# react-native-modal-popover

Pure JS popover component for react-native

![Android](https://raw.githubusercontent.com/doomsower/react-native-modal-popover/master/gifs/android.gif)
![iOS](https://raw.githubusercontent.com/doomsower/react-native-modal-popover/master/gifs/ios.gif)

## About this module

The original [react-native-popover](https://github.com/jeanregisser/react-native-popover) is now outdated,
so I decided to publish my own module to avoid using github url in my package.json. Something got lost in
the process of rewriting, but now it uses `Modal` and native animation drivers, and also has cool helper
to use with Touchables. Thanks to @jeanregisser and to the authors of hanging PRs for their code.

## Requirements

Previously (version `0.0.6`) this module required react version `>16.2.0` to work (which corresponds to react-native version `>0.52.0`).

Version `0.0.7` does not reqire `React.Fragment` anymore, so you can use with reasonably old versions of react and react-native.

## Install

```sh
yarn add react-native-modal-popover
```

## Usage

This module exports two react components, `Popover` and `PopoverController`, and one react hook, `usePopover`.
`Popover` works pretty much like original `Popover`, and `PopoverController` is a convenience component that uses [React Render Props](https://github.com/jaredpalmer/awesome-react-render-props) pattern.

**Important** this example uses `React.Fragment` to wrap children, but if you use `react-native` version older than `0.52`, then you should reaplce `React.Fragment` with `View`

### Using hook

`usePopover` is preferred modern way to have popover in your app.

```jsx
import React from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import { Popover, usePopover } from 'react-native-modal-popover';

const styles = StyleSheet.create({
  app: {
    ...StyleSheet.absoluteFillObject,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#c2ffd2',
  },
  content: {
    padding: 16,
    backgroundColor: 'pink',
    borderRadius: 8,
  },
  arrow: {
    borderTopColor: 'pink',
  },
  background: {
    backgroundColor: 'rgba(0, 0, 255, 0.5)',
  },
});

const App = () => {
  const {
    openPopover,
    closePopover,
    popoverVisible,
    touchableRef,
    popoverAnchorRect,
  } = usePopover();
  return (
    <View style={styles.app}>
      <Button title="Press me!" ref={touchableRef} onPress={openPopover} />
      <Popover
        contentStyle={styles.content}
        arrowStyle={styles.arrow}
        backgroundStyle={styles.background}
        visible={popoverVisible}
        onClose={closePopover}
        fromRect={popoverAnchorRect}
        supportedOrientations={['portrait', 'landscape']}>
        <Text>Hello from inside popover!</Text>
      </Popover>
    </View>
  );
};

export default App;
```

### Using PopoverController

Use `PopoverController` if you cannot use hooks for some reason.

```jsx
import React from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import { Popover, PopoverController } from 'react-native-modal-popover';

const styles = StyleSheet.create({
  app: {
    ...StyleSheet.absoluteFillObject,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#c2ffd2',
  },
  content: {
    padding: 16,
    backgroundColor: 'pink',
    borderRadius: 8,
  },
  arrow: {
    borderTopColor: 'pink',
  },
  background: {
    backgroundColor: 'rgba(0, 0, 255, 0.5)',
  },
});

const App = () => (
  <View style={styles.app}>
    <PopoverController>
      {({
        openPopover,
        closePopover,
        popoverVisible,
        setPopoverAnchor,
        popoverAnchorRect,
      }) => (
        <React.Fragment>
          <Button
            title="Press me!"
            ref={setPopoverAnchor}
            onPress={openPopover}
          />
          <Popover
            contentStyle={styles.content}
            arrowStyle={styles.arrow}
            backgroundStyle={styles.background}
            visible={popoverVisible}
            onClose={closePopover}
            fromRect={popoverAnchorRect}
            supportedOrientations={['portrait', 'landscape']}>
            <Text>Hello from inside popover!</Text>
          </Popover>
        </React.Fragment>
      )}
    </PopoverController>
  </View>
);

export default App;
```

## Props

### `Popover`

| Prop                  | Type                                                                                                | Optional | Default                                                                     | Description                                                                                                                                                                                                                                           |
| --------------------- | --------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| visible               | bool                                                                                                | Yes      | false                                                                       | Show/Hide the popover                                                                                                                                                                                                                                 |
| fromRect              | Rect                                                                                                | No\*     |                                                                             | Rectangle at which to anchor the popover. **Optional** when used inside `PopoverTouchable`, **required** when used standalone. If you set this property, you should also change it when screen orientation changes.                                   |
| displayArea           | Rect                                                                                                | Yes      | Screen - 10px padding                                                       | Area where the popover is allowed to be displayed. **Important note:** if you use non-default value here and you want to handle screen orientation changes, it is your responsibility to change this value when screen orientation changes.           |
| placement             | string                                                                                              | Yes      | 'auto'                                                                      | How to position the popover - top &#124; bottom &#124; start &#124; end &#124; auto. When 'auto' is specified, it will determine the ideal placement so that the popover is fully visible within `displayArea`.                                       |
| onClose               | function                                                                                            | Yes      |                                                                             | Callback to be fired when the user closes the popover                                                                                                                                                                                                 |
| onDismiss             | function                                                                                            | Yes      |                                                                             | Callback to be fired after the popup closes                                                                                                                                                                                                           |
| backgroundStyle       | ViewStyle                                                                                           | Yes      |                                                                             | Custom style to be applied to background overlay                                                                                                                                                                                                      |
| contentStyle          | ViewStyle                                                                                           | Yes      |                                                                             | Custom style to be applied to popover reactangle. Use it to set round corners, background color, etc.                                                                                                                                                 |
| arrowStyle            | ViewStyle                                                                                           | Yes      |                                                                             | Custom style to be applied to popover arrow. Use `borderTopColor` to match content backgroundColor                                                                                                                                                    |
| duration              | number                                                                                              | Yes      | 300                                                                         | Animation duration                                                                                                                                                                                                                                    |
| easing                | (show: boolean) => (value: number) => number                                                        | Yes      | show => show ? Easing.out(Easing.back(1.70158)) : Easing.inOut(Easing.quad) | Function that returns easing function for show or hide animation, depending on `show` argument                                                                                                                                                        |
| useNativeDriver       | bool                                                                                                | Yes      | false                                                                       | Defines if animations should use native driver                                                                                                                                                                                                        |
| supportedOrientations | array of enum('portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right') | Yes      |                                                                             | This prop is passed to react-native `Modal`, see [react-native docs](https://facebook.github.io/react-native/docs/modal.html#supportedorientations). Set this to `['portrait', 'landscape']` if you want your popover to resprect screen orientation. |
| calculateStatusBar    | bool                                                                                                | Yes      | false                                                                       | Defines if while use status bar height while calculating "Y" origin of anchor.                                                                                                                                                                        |

### `PopoverController` and `usePopover` hook

`PopoverController` accepts function as children. This function is called with one argument of type `PopoverControllerRenderProps` and returns react element. The children of this element are your UI handle to open popover (`Button`, `Toggle`, whatever) and `Popover` element itself. Pass properties to you handle and `Popover`, and `PopoverController` will make them work together behind the scenes. All the props are required to make controller work.

`usePopover` returns object with same props as `PopoverControllerRenderProps`, except that ref has different name: `touchableRef`.

#### `PopoverControllerRenderProps`:

| Prop                                                                 | Type         | Description                                                                                                                                                                                                                                                          |
| -------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| openPopover                                                          | () => void   | Call this function when you want to open popover, e.g. pass to `onPress` of a `Button`                                                                                                                                                                               |
| closePopover                                                         | () => void   | Call this function when you want to close popover. Typically you pass this as `onClose` prop of `Popover`, which will make popover close when tapped outside. If you have a button inside popover which should close the popover, pass this function to this button. |
| popoverVisible                                                       | boolean      | Pass this to `visible` prop of `Popover` component                                                                                                                                                                                                                   |
| setPopoverAnchor (`PopoverController`) / touchableRef (`usePopover`) | ref function | Pass this as `ref` to popover UI handle. This will bind popover display position to the position of this UI handle.                                                                                                                                                  |
| popoverAnchorRect                                                    | Rect         | Pass this as `fromRect` prop of `Popover` component                                                                                                                                                                                                                  |

### `Rect`

Rect is an object with the following properties: `{x: number, y: number, width: number, height: number}`

## Using without `PopoverController`

In this case you have to handle refs, measure UI handle and manage popover visibility manually:

```jsx
import React from 'react';
import {
  findNodeHandle,
  NativeModules,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import Button from './Button';
import Popover from './popover';

const styles = StyleSheet.create({
  app: {
    ...StyleSheet.absoluteFillObject,
    padding: 10,
    backgroundColor: '#c2ffd2',
    alignItems: 'center',
  },
});

export default class App2 extends React.Component {
  state = {
    showPopover: false,
    popoverAnchor: { x: 0, y: 0, width: 0, height: 0 },
  };

  setButton = (e) => {
    const handle = findNodeHandle(this.button);
    if (handle) {
      NativeModules.UIManager.measure(handle, (x0, y0, width, height, x, y) => {
        this.setState({ popoverAnchor: { x, y, width, height } });
      });
    }
  };

  openPopover = () => {
    this.setState({ showPopover: true });
  };

  closePopover = () => this.setState({ showPopover: false });

  render() {
    return (
      <View style={styles.app}>
        <Button
          ref={(r) => {
            this.button = r;
          }}
          icon="arrow-up"
          onPress={this.openPopover}
          onLayout={this.setButton}
        />
        <Popover
          visible={this.state.showPopover}
          fromRect={this.state.popoverAnchor}
          onClose={this.closePopover}
          placement="bottom">
          <Text>Hi</Text>
        </Popover>
      </View>
    );
  }
}
```

## Contributing

If you want to add some features, feel free to submit PR.
