diff --git a/__tests__/renderers/Popover-test.js b/__tests__/renderers/Popover-test.js
new file mode 100644
index 0000000..47f0b98
--- /dev/null
+++ b/__tests__/renderers/Popover-test.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import { View, Animated, Text } from 'react-native';
+import { render } from '../helpers';
+
+jest.dontMock('../../src/renderers/Popover');
+const { default: Popover } = require('../../src/renderers/Popover');
+
+describe('NotAnimatedContextMenu', () => {
+
+ const defaultLayouts = {
+ windowLayout: { width: 400, height: 600 },
+ triggerLayout: { width: 50, height: 50, x: 10, y: 10 },
+ optionsLayout: { width: 200, height: 100 },
+ };
+
+ describe('renderer', () => {
+ it('should render component', () => {
+ const { output } = render(
+
+ Some text
+ Other text
+
+ );
+ expect(output.type).toEqual(Animated.View);
+ const anchor = output.props.children[0]
+ expect(anchor.type).toEqual(View);
+ const content = output.props.children[1]
+ expect(content.type).toEqual(View);
+ expect(content.props.children).toEqual([
+ Some text,
+ Other text
+ ]);
+ });
+ });
+
+});
diff --git a/doc/api.md b/doc/api.md
index a442549..3c2dfa8 100644
--- a/doc/api.md
+++ b/doc/api.md
@@ -194,6 +194,7 @@ The `renderers` module provides following renderers
* `ContextMenu` (default) - opens (animated) context menu over the trigger position. The `ContextMenu.computePosition` exports function for position calculation in case you would like to implement your own renderer (without special position calculation).
* `NotAnimatedContextMenu` - same as ContextMenu but without any animation.
* `SlideInMenu` - slides in the menu from the bottom of the screen.
+* `Popover` - display menu as a popover.
It is possible to extend menu and use custom renderer (see implementation of existing renderers or [extension guide](extensions.md)).
diff --git a/examples/Demo.js b/examples/Demo.js
index 1b3b05a..fb05945 100644
--- a/examples/Demo.js
+++ b/examples/Demo.js
@@ -15,6 +15,7 @@ import TouchableExample from './TouchableExample';
import MenuMethodsExample from './MenuMethodsExample';
import CloseOnBackExample from './CloseOnBackExample';
import FlatListExample from './FlatListExample';
+import PopoverExample from './PopoverExample';
const demos = [
{ Component: BasicExample, name: 'Basic example' },
@@ -30,6 +31,7 @@ const demos = [
{ Component: NavigatorExample, name: 'Example with react-native-router-flux' },
{ Component: CloseOnBackExample, name: 'Close on back button press example' },
{ Component: FlatListExample, name: 'Using FlatList' },
+ { Component: PopoverExample, name: 'Popover renderer' },
];
// show debug messages for demos.
diff --git a/examples/PopoverExample.js b/examples/PopoverExample.js
new file mode 100644
index 0000000..231e551
--- /dev/null
+++ b/examples/PopoverExample.js
@@ -0,0 +1,73 @@
+import {
+ Menu,
+ MenuContext,
+ MenuOptions,
+ MenuTrigger,
+ renderers,
+} from 'react-native-popup-menu';
+import { Text, View, StyleSheet } from 'react-native';
+import React from 'react';
+
+const { Popover } = renderers
+
+const MyPopover = () => (
+
+)
+
+const Row = () => (
+
+
+
+
+
+
+
+
+)
+
+const PopoverExample = () => (
+
+
+
+
+
+
+
+
+
+
+);
+
+const styles = StyleSheet.create({
+ container: {
+ padding: 10,
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ },
+ row: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ backdrop: {
+ backgroundColor: 'black',
+ opacity: 0.3,
+ },
+ menuOptions: {
+ padding: 50,
+ },
+ menuTrigger: {
+ padding: 5,
+ },
+ triggerText: {
+ fontSize: 20,
+ },
+})
+
+export default PopoverExample;
diff --git a/src/index.js b/src/index.js
index 043d7fc..f962b61 100644
--- a/src/index.js
+++ b/src/index.js
@@ -9,6 +9,7 @@ import MenuTrigger from './MenuTrigger';
import ContextMenu from './renderers/ContextMenu';
import NotAnimatedContextMenu from './renderers/NotAnimatedContextMenu';
import SlideInMenu from './renderers/SlideInMenu';
-const renderers = { ContextMenu, SlideInMenu, NotAnimatedContextMenu };
+import Popover from './renderers/Popover';
+const renderers = { ContextMenu, SlideInMenu, NotAnimatedContextMenu, Popover };
export { Menu as default, Menu, MenuContext, MenuOption, MenuOptions, MenuTrigger, renderers };
diff --git a/src/renderers/Popover.js b/src/renderers/Popover.js
new file mode 100644
index 0000000..3b91d07
--- /dev/null
+++ b/src/renderers/Popover.js
@@ -0,0 +1,266 @@
+import { Animated, Easing, StyleSheet, View } from 'react-native';
+import React from 'react';
+
+import { OPEN_ANIM_DURATION, CLOSE_ANIM_DURATION } from '../constants.js';
+
+const popoverPadding = 7;
+const anchorSize = 15;
+const anchorHyp = Math.sqrt(anchorSize*anchorSize + anchorSize*anchorSize);
+const anchorOffset = (anchorHyp + anchorSize) / 2 - popoverPadding;
+
+const POSITIVE_DIRECTION = 1;
+const NEGATIVE_DIRECTION = -1;
+
+/**
+ * Computes position properties of popover when trying to align it to the triger side.
+ * It consideres window boundaries.
+ * Returns object with keys:
+ * - position: Absolute position - top/left,
+ * - direction: Positive if position is above/left, negative if position is below/right the trigger
+ */
+function axisSidePositionProperties({ oDim, wDim, tPos, tDim }) {
+ // if options are bigger than window dimension, then render at 0
+ if (oDim > wDim) {
+ return { position: 0, direction: POSITIVE_DIRECTION };
+ }
+ // render above trigger
+ if (tPos - oDim >= 0) {
+ return { position: tPos - oDim, direction: POSITIVE_DIRECTION };
+ }
+ // render under trigger
+ if (tPos + tDim + oDim <= wDim) {
+ return { position: tPos + tDim, direction: NEGATIVE_DIRECTION };
+ }
+ // compute center position
+ let pos = tPos + (tDim / 2) - (oDim / 2);
+ // check top boundary
+ if (pos < 0) {
+ return { position: 0, direction: NEGATIVE_DIRECTION };
+ }
+ // check bottom boundary
+ if (pos + oDim > wDim) {
+ return { position: wDim - oDim, direction: POSITIVE_DIRECTION };
+ }
+ // if everything ok, render in center position
+ return { position: pos, direction: POSITIVE_DIRECTION };
+}
+
+// computes offsets (off screen overlap) of popover when trying to align it to the center
+function centeringProperties({ oDim, wDim, tPos, tDim }) {
+ const center = Math.round(tPos + (tDim / 2));
+ const leftOffset = (oDim / 2) - center;
+ const rightOffset = center + (oDim / 2) - wDim;
+ return { center, leftOffset, rightOffset };
+}
+
+/**
+ * Computes position and offset of popover when trying to align it to the triger center.
+ * It consideres window boundaries.
+ * Returns object with keys:
+ * - position: Absolute position - top/left,
+ * - offset: window overlapping size if window boundaries were not considered
+ */
+function axisCenteredPositionProperties(options) {
+ const { oDim, wDim } = options;
+ const { center, leftOffset, rightOffset } = centeringProperties(options);
+ if (leftOffset > 0 || rightOffset > 0) {
+ // right/bottom position is better
+ if (leftOffset < rightOffset) {
+ return { offset: rightOffset, position: wDim - oDim };
+ }
+ // left/top position is better
+ if (rightOffset < leftOffset) {
+ return { offset: -leftOffset, position: 0 };
+ }
+ }
+ // centered position
+ return { offset: 0, position: center - oDim / 2 };
+}
+
+// picks max offset for popover
+function maxCenterOffset(options) {
+ const { leftOffset, rightOffset } = centeringProperties(options);
+ return Math.max(0, leftOffset, rightOffset);
+}
+
+/**
+ * Computes properties needed for drawing popover.
+ * Returns object with keys:
+ * - position: