├── .babelrc
├── .eslintignore
├── .eslintrc
├── .github
├── ISSUE_TEMPLATE.md
└── workflows
│ └── main.yml
├── .gitignore
├── .npmignore
├── Jenkinsfile
├── LICENSE
├── README.md
├── __mocks__
├── .eslintrc
└── react-native.js
├── __tests__
├── .eslintrc
├── Backdrop-test.js
├── Menu-test.js
├── MenuOption-test.js
├── MenuOptions-test.js
├── MenuProvider-test.js
├── MenuTrigger-test.js
├── helpers-test.js
├── helpers.js
├── menuRegistry-test.js
└── renderers
│ ├── ContextMenu-test.js
│ ├── MenuOutside-test.js
│ ├── NotAnimatedContextMenu-test.js
│ ├── Popover-test.js
│ └── SlideInMenu-test.js
├── android.demo-popover.gif
├── android.demo.gif
├── build
├── rnpm.js
└── rnpm.js.map
├── doc
├── api.md
├── examples.md
├── extensions.md
└── img
│ ├── basic.png
│ ├── checked.png
│ ├── context-menu.png
│ └── styled.png
├── examples
├── .expo
│ ├── packager-info.json
│ └── settings.json
├── .watchmanconfig
├── App.js
├── BasicExample.js
├── CloseOnBackExample.js
├── ControlledExample.js
├── Demo.js
├── Example.js
├── ExtensionExample.js
├── FlatListExample.js
├── InFlatListExample.js
├── MenuMethodsExample.js
├── ModalExample.js
├── NavigatorExample.js
├── NonRootExample.js
├── PopoverExample.js
├── StylingExample.js
├── TouchableExample.js
├── __tests__
│ ├── Basic-test.js
│ └── __snapshots__
│ │ └── Basic-test.js.snap
├── app.json
├── assets
│ ├── icon.png
│ └── splash.png
├── babel.config.js
├── package-lock.json
└── package.json
├── package.json
├── rollup.config.babel.js
├── setup-jasmine-env.js
├── src
├── Backdrop.js
├── Menu.js
├── MenuOption.js
├── MenuOptions.js
├── MenuPlaceholder.js
├── MenuProvider.js
├── MenuTrigger.js
├── constants.js
├── helpers.js
├── index.d.ts
├── index.js
├── logger.js
├── menuRegistry.js
├── polyfills.js
├── renderers
│ ├── ContextMenu.js
│ ├── MenuOutside.js
│ ├── NotAnimatedContextMenu.js
│ ├── Popover.js
│ └── SlideInMenu.js
└── with-context.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["module:metro-react-native-babel-preset"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | coverage/*
3 | examples/node_modules/*
4 | examples/android/*
5 | examples/ios/*
6 | build/*
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": ["eslint:recommended", "plugin:react/recommended"],
4 | "parserOptions": {
5 | "ecmaVersion": 6,
6 | "sourceType": "module",
7 | "ecmaFeatures": {
8 | "jsx": true,
9 | "modules": true
10 | }
11 | },
12 | "plugins": ["react"],
13 |
14 | "env": {
15 | "node": true,
16 | "es6": true,
17 | "browser": true
18 | },
19 |
20 | "rules": {
21 | "no-console": 0,
22 | "comma-dangle": ["error", "always-multiline"],
23 | "no-unused-vars": ["error", { "ignoreRestSiblings": true }],
24 | "react/prop-types": 0,
25 | "react/no-did-mount-set-state": 0,
26 | "react/no-deprecated": 0
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | If you have question about the usage of the library, please ask question on StackOverflow with tag [react-native-popup-menu](http://stackoverflow.com/questions/tagged/react-native-popup-menu). We are subscribed to it but it also allows community to help you if we are busy.
2 |
3 | If you have found a bug, please describe your problem and expected behaviour. Additionally let us know library version, platform on which the problem happens and if possible some code snipets reproducing problem.
4 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | strategy:
11 | matrix:
12 | node-version: [10.x]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Use Node.js ${{ matrix.node-version }}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - run: yarn
21 | - run: yarn lint
22 | - run: yarn test
23 | env:
24 | CI: true
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | coverage
4 | target
5 | .expo
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | examples
2 | .babelrc
3 | Jenkinsfile
4 | coverage/
5 | target/
6 | __tests__/
7 | __mocks__/
8 | *.demo.gif
9 | doc/
10 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | node {
2 | stage 'Fetch source code'
3 | git url: 'https://github.com/instea/react-native-popup-menu.git', branch: 'master'
4 | stage 'Install dependencies'
5 | sh 'npm install'
6 | stage 'Run tests'
7 | try {
8 | sh 'npm test'
9 | } finally {
10 | step([$class: 'JUnitResultArchiver', testResults: 'target/*.xml'])
11 | step([$class: 'ArtifactArchiver', artifacts: 'coverage/**/*', fingerprint: true])
12 | step([$class: 'Mailer', notifyEveryUnstableBuild: true, recipients: "${env.DEV_MAIL}", sendToIndividuals: true])
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | ISC License
2 |
3 | Copyright (c) 2016, instea
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted, provided that the above
7 | copyright notice and this permission notice appear in all copies.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-popup-menu
2 | [](https://www.npmjs.com/package/react-native-popup-menu)
3 |
4 | Extensible popup menu component for React Native for Android, iOS and (unofficially) UWP and react-native-web.
5 |
6 | Features:
7 | * Simple to use popup/context menu
8 | * Multiple modes: animated, not animated, slide in from bottom or popover
9 | * By default opening and closing animations
10 | * Optional back button handling
11 | * Easy styling
12 | * Customizable on various levels - menu options, positioning, animations
13 | * Can work as controlled as well as uncontrolled component
14 | * Different lifecycle hooks
15 | * RTL layout support
16 |
17 | Community driven features:
18 | * Support for UWP, react-native-web and react-native-desktop
19 | * Typescript definitions
20 |
21 | We thank our community for maintaining features that goes over our scope.
22 |
23 | | Context Menu, Slide-in Menu | Popover |
24 | |---|---|
25 | |||
26 |
27 | ## Installation
28 |
29 | ```
30 | npm install react-native-popup-menu --save
31 | ```
32 | If you are using **older RN versions** check our compatibility table.
33 |
34 | ## Basic Usage
35 | Wrap your application inside `MenuProvider` and then simply use `Menu` component where you need it. Below you can find a simple example.
36 |
37 | For more detailed documentation check [API](./doc/api.md).
38 |
39 | ```js
40 | // your entry point
41 | import { MenuProvider } from 'react-native-popup-menu';
42 |
43 | export const App = () => (
44 |
45 |
46 |
47 | );
48 |
49 | // somewhere in your app
50 | import {
51 | Menu,
52 | MenuOptions,
53 | MenuOption,
54 | MenuTrigger,
55 | } from 'react-native-popup-menu';
56 |
57 | export const YourComponent = () => (
58 |
59 | Hello world!
60 |
70 |
71 | );
72 |
73 | ```
74 |
75 | ## Documentation
76 |
77 | - [Examples](doc/examples.md)
78 | - [API](doc/api.md)
79 | - [Extension points](doc/extensions.md)
80 |
81 | ## Contributing
82 | Contributions are welcome! Just open an issues with any idea or pull-request if it is no-brainer. Make sure all tests and linting rules pass.
83 |
84 | ## React Native Compatibility
85 | We keep compatibility on best effort basis.
86 |
87 | | popup-menu version | min RN (React) version |
88 | | ------------------ | -------------- |
89 | | 0.13 | 0.55 (16.3.1) |
90 | | 0.9 | 0.40 |
91 | | 0.8 | 0.38 |
92 | | 0.7 | 0.18 |
93 |
--------------------------------------------------------------------------------
/__mocks__/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "jest": true,
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/__mocks__/react-native.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const ReactNative = React;
3 |
4 | ReactNative.StyleSheet = {
5 | create: function create(styles) {
6 | return styles;
7 | },
8 | };
9 |
10 | class View extends React.Component {
11 | render() { return false; }
12 | }
13 |
14 | View.propTypes = {
15 | style: () => null,
16 | };
17 |
18 | class ListView extends React.Component {
19 | static DataSource() {
20 | }
21 | }
22 |
23 | class AppRegistry {
24 | static registerComponent () {
25 | }
26 | }
27 |
28 | const Animated = {
29 | timing: () => ({ start: () => undefined }),
30 | Value: () => ({ interpolate: () => false }),
31 | View: View,
32 | };
33 |
34 | const I18nManager = {
35 | isRTL: false,
36 | };
37 |
38 | const Text = () => "Text";
39 | const TouchableHighlight = () => false;
40 | const TouchableWithoutFeedback = () => false;
41 | const TouchableNativeFeedback = () => false;
42 | const TouchableOpacity = () => false;
43 | const ToolbarAndroid = () => false;
44 | const Image = () => false;
45 | const ScrollView = () => false;
46 | const Platform = {
47 | select: jest.fn(o => o.ios),
48 | };
49 | const PixelRatio = {
50 | roundToNearestPixel: n => n,
51 | }
52 | const BackHandler = {
53 | addEventListener: jest.fn(),
54 | removeEventListener: jest.fn(),
55 | }
56 |
57 | ReactNative.View = View;
58 | ReactNative.ScrollView = ScrollView;
59 | ReactNative.ListView = ListView;
60 | ReactNative.Text = Text;
61 | ReactNative.TouchableOpacity = TouchableOpacity;
62 | ReactNative.TouchableHighlight = TouchableHighlight;
63 | ReactNative.TouchableNativeFeedback = TouchableNativeFeedback;
64 | ReactNative.TouchableWithoutFeedback = TouchableWithoutFeedback;
65 | ReactNative.ToolbarAndroid = ToolbarAndroid;
66 | ReactNative.Image = Image;
67 | ReactNative.AppRegistry = AppRegistry;
68 | ReactNative.Animated = Animated;
69 | ReactNative.I18nManager = I18nManager;
70 | ReactNative.Platform = Platform;
71 | ReactNative.PixelRatio = PixelRatio;
72 | ReactNative.BackHandler = BackHandler;
73 |
74 | module.exports = ReactNative;
75 |
--------------------------------------------------------------------------------
/__tests__/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "options": true,
4 | "describe": true,
5 | "it": true,
6 | "before": true,
7 | "beforeEach": true,
8 | "after": true,
9 | "afterEach": true,
10 | "jest": true,
11 | "expect": true,
12 | "jasmine": true
13 | },
14 | "rules": {
15 | "react/jsx-key": 0,
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/__tests__/Backdrop-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TouchableWithoutFeedback, View } from 'react-native';
3 | import { render } from './helpers';
4 |
5 | jest.dontMock('../src/Backdrop');
6 | const Backdrop = require('../src/Backdrop').default;
7 |
8 | const { createSpy } = jasmine;
9 |
10 | describe('Backdrop', () => {
11 |
12 | it('should render component', () => {
13 | const { output } = render(
14 |
15 | );
16 | expect(output.type).toEqual(TouchableWithoutFeedback);
17 | const view = output.props.children;
18 | expect(view.type).toEqual(View);
19 | });
20 |
21 | it('should trigger on press event', () => {
22 | const onPressSpy = createSpy();
23 | const { output } = render(
24 |
25 | );
26 | expect(output.type).toEqual(TouchableWithoutFeedback);
27 | expect(typeof output.props.onPress).toEqual('function');
28 | output.props.onPress();
29 | expect(onPressSpy).toHaveBeenCalled();
30 | });
31 |
32 | });
33 |
--------------------------------------------------------------------------------
/__tests__/Menu-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Text } from 'react-native';
3 | import { render } from './helpers';
4 |
5 | import { MenuTrigger, MenuOptions } from '../src/index';
6 |
7 | jest.mock('../src/helpers', () => ({
8 | makeName: () => 'generated-name',
9 | deprecatedComponent: jest.fn(() => jest.fn()),
10 | }));
11 |
12 | jest.dontMock('../src/Menu');
13 | const exported = require('../src/Menu');
14 | const { Menu, default: ExportedMenu } = exported;
15 |
16 | const { objectContaining, createSpy, any } = jasmine;
17 |
18 | describe('Menu', () => {
19 |
20 | function renderMenu(element) {
21 | const ctx = createMockContext();
22 | const result = render(element, ctx);
23 | result.ctx = ctx;
24 | return result;
25 | }
26 |
27 | function createMockContext() {
28 | return {
29 | menuRegistry : {
30 | subscribe: createSpy(),
31 | unsubscribe: createSpy(),
32 | },
33 | menuActions: {
34 | _notify: createSpy(),
35 | },
36 | }
37 | }
38 |
39 | it('should export api', () => {
40 | expect(typeof ExportedMenu.debug).toEqual('boolean');
41 | expect(typeof ExportedMenu.setDefaultRenderer).toEqual('function');
42 | });
43 |
44 | it('should render component and preserve children order', () => {
45 | const { output } = renderMenu(
46 |
52 | );
53 | expect(output.type).toEqual(View);
54 | expect(output.props.children.length).toEqual(3);
55 | // React.Children.toArray modifies components keys
56 | // using the same function to create expected children
57 | const expectedChildren = React.Children.toArray([
58 | Some text,
59 | , // trigger will be modified
60 | , // options will be removed
61 | Some other text,
62 | ]);
63 | expect(output.props.children[0]).toEqual(expectedChildren[0]);
64 | expect(output.props.children[1]).toEqual(objectContaining({
65 | type: MenuTrigger,
66 | props: objectContaining({
67 | onRef: any(Function),
68 | }),
69 | }));
70 | expect(output.props.children[2]).toEqual(expectedChildren[3]);
71 | });
72 |
73 | it('should subscribe menu and notify context', () => {
74 | const { ctx, instance } = renderMenu(
75 |
79 | );
80 | instance.componentDidMount();
81 | expect(ctx.menuRegistry.subscribe).toHaveBeenCalledWith(instance);
82 | expect(ctx.menuActions._notify).toHaveBeenCalled();
83 | });
84 |
85 | it('should not subscribe menu because of missing options', () => {
86 | const { instance, renderer, ctx } = renderMenu(
87 |
91 | );
92 | instance.componentDidMount();
93 | expect(ctx.menuRegistry.subscribe).not.toHaveBeenCalled();
94 | const output = renderer.getRenderOutput();
95 | expect(output.type).toEqual(View);
96 | const expectedChildren = React.Children.toArray([
97 | ,
98 | Some text,
99 | ]);
100 | expect(output.props.children[0]).toEqual(
101 | objectContaining({
102 | type: MenuTrigger,
103 | })
104 | );
105 | expect(output.props.children[1]).toEqual(expectedChildren[1]);
106 | });
107 |
108 | it('should not subscribe menu because of missing trigger', () => {
109 | const { instance, renderer, ctx } = renderMenu(
110 |
114 | );
115 | instance.componentDidMount();
116 | expect(ctx.menuRegistry.subscribe).not.toHaveBeenCalled();
117 | const output = renderer.getRenderOutput();
118 | expect(output.type).toEqual(View);
119 | expect(output.props.children).toEqual(React.Children.toArray(
120 | Some text
121 | ));
122 | });
123 |
124 | it('should not fail without any children', () => {
125 | const { instance, renderer } = renderMenu(
126 |
127 | );
128 | instance.componentDidMount();
129 | const output = renderer.getRenderOutput();
130 | expect(output.type).toEqual(View);
131 | expect(output.props.children).toEqual([]);
132 | instance.componentWillUnmount();
133 | });
134 |
135 | it('should autogenerate name if not provided', () => {
136 | const { instance } = renderMenu(
137 |
138 | );
139 | expect(instance.getName()).toEqual('generated-name');
140 | });
141 |
142 | it('should use name from props if provided', () => {
143 | const { instance } = renderMenu(
144 |
145 | );
146 | expect(instance.getName()).toEqual('prop-name');
147 | });
148 |
149 | it('should unsubscribe menu', () => {
150 | const { instance, ctx } = renderMenu(
151 |
155 | );
156 | instance.componentWillUnmount();
157 | expect(ctx.menuRegistry.unsubscribe).toHaveBeenCalledWith(instance);
158 | });
159 |
160 | it('should notify context if updated', () => {
161 | const { instance, ctx } = renderMenu(
162 |
166 | );
167 | instance.componentDidUpdate({});
168 | expect(ctx.menuActions._notify).toHaveBeenCalled();
169 | });
170 |
171 | it('should get menu options', () => {
172 | const onSelect = () => 0;
173 | const { instance } = renderMenu(
174 |
178 | );
179 | const options = instance._getOptions();
180 | expect(options.type).toEqual(MenuOptions);
181 | });
182 |
183 | it('declarative opened takes precedence over imperative', () => {
184 | const { instance } = renderMenu(
185 |