├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ └── main.yml
├── .gitignore
├── .watchmanconfig
├── .yarnrc
├── CHANGELOGS.md
├── LICENSE
├── README.md
├── babel.config.js
├── example
├── App.js
├── app.json
├── assets
│ ├── adaptive-icon.png
│ ├── favicon.png
│ ├── icon.png
│ └── splash.png
├── babel.config.js
├── metro.config.js
├── package.json
├── src
│ └── App.tsx
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
├── lefthook.yml
├── package.json
├── scripts
└── bootstrap.js
├── src
├── components
│ ├── container.tsx
│ ├── event-provider.tsx
│ └── outside-press-handler.tsx
├── event-context.ts
├── hooks
│ ├── use-event-store.ts
│ └── use-event.ts
├── index.ts
└── utils
│ └── deep-clone.ts
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: dcangulo
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | day: "saturday"
8 | time: "04:00"
9 | timezone: "Asia/Manila"
10 | - package-ecosystem: "npm"
11 | directory: "/"
12 | schedule:
13 | interval: "weekly"
14 | day: "saturday"
15 | time: "04:00"
16 | timezone: "Asia/Manila"
17 | open-pull-requests-limit: 99
18 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: main
2 | on: [push]
3 | jobs:
4 | main:
5 | name: main
6 | runs-on: ubuntu-latest
7 | steps:
8 | - name: Check out code
9 | uses: actions/checkout@v4
10 | - name: Setup environment
11 | uses: actions/setup-node@v4
12 | - name: Install dependencies
13 | run: yarn
14 | - name: Check types
15 | run: yarn typecheck
16 | - name: Run lint
17 | run: yarn lint
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # XDE
6 | .expo/
7 |
8 | # VSCode
9 | .vscode/
10 | jsconfig.json
11 |
12 | # Xcode
13 | #
14 | build/
15 | *.pbxuser
16 | !default.pbxuser
17 | *.mode1v3
18 | !default.mode1v3
19 | *.mode2v3
20 | !default.mode2v3
21 | *.perspectivev3
22 | !default.perspectivev3
23 | xcuserdata
24 | *.xccheckout
25 | *.moved-aside
26 | DerivedData
27 | *.hmap
28 | *.ipa
29 | *.xcuserstate
30 | project.xcworkspace
31 |
32 | # Android/IJ
33 | #
34 | .classpath
35 | .cxx
36 | .gradle
37 | .idea
38 | .project
39 | .settings
40 | local.properties
41 | android.iml
42 |
43 | # Cocoapods
44 | #
45 | example/ios/Pods
46 |
47 | # Ruby
48 | example/vendor/
49 |
50 | # node.js
51 | #
52 | node_modules/
53 | npm-debug.log
54 | yarn-debug.log
55 | yarn-error.log
56 |
57 | # BUCK
58 | buck-out/
59 | \.buckd/
60 | android/app/libs
61 | android/keystores/debug.keystore
62 |
63 | # Expo
64 | .expo/
65 |
66 | # Turborepo
67 | .turbo/
68 |
69 | # generated by bob
70 | lib/
71 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | # Override Yarn command so we can automatically setup the repo on running `yarn`
2 |
3 | yarn-path "scripts/bootstrap.js"
4 |
--------------------------------------------------------------------------------
/CHANGELOGS.md:
--------------------------------------------------------------------------------
1 | # Changelogs
2 |
3 | ## 1.2.2 (2024-01-02)
4 | * Removed `engines` from `package.json`. ([#327](https://github.com/dcangulo/react-native-outside-press/pull/327) by [@gulewei](https://github.com/gulewei))
5 |
6 | ## 1.2.1 (2023-08-19)
7 | * Updated documentation.
8 | * Added default style to container.
9 | * Make `disabled` prop optional.
10 |
11 | ## 1.2.0 (2023-08-14)
12 | * Updated project template.
13 | * Fixed outside press event not firing on web.
14 |
15 | ## 1.1.0 (2022-08-10)
16 | * Added `disabled` prop to `OutsidePressHandler`.
17 |
18 | ## 1.0.2 (2022-07-12)
19 | * Fixed unexpected behavior on iOS and Android.
20 |
21 | ## 1.0.1 (2022-07-11)
22 | * Fixed a bug where a dev dependency is in dependencies.
23 |
24 | ## 1.0.0 (2022-07-10)
25 | * Initial release.
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 David Angulo
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native Outside Press
2 | [](https://www.npmjs.com/package/react-native-outside-press)
3 | [](LICENSE)
4 | [](https://github.com/dcangulo/react-native-outside-press/pulls)
5 |
6 | [airbnb/react-outside-click-handler](https://github.com/airbnb/react-outside-click-handler) but for React Native.
7 |
8 | ## Compatibility
9 | | iOS | Android | Web | Windows | macOS | Expo |
10 | |--------------------|--------------------|--------------------|--------------------|--------------------|--------------------|
11 | | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
12 |
13 | ## Installation
14 | ```bash
15 | yarn add react-native-outside-press
16 | ```
17 |
18 | ## Usage
19 |
20 | ### EventProvider
21 | Wrap your app with `EventProvider`.
22 |
23 | ```js
24 | import { EventProvider } from 'react-native-outside-press';
25 |
26 | export default function App() {
27 | return (
28 |
29 |
30 |
31 | );
32 | }
33 | ```
34 |
35 | #### Props
36 |
37 | | Name | Description | Type | Default | Required? |
38 | |-------------|---------------------|------------------------------------------------------------|---------------|-----------|
39 | | `style` | | [ViewStyle](https://reactnative.dev/docs/view-style-props) | `{ flex: 1 }` | `false` |
40 | | `ViewProps` | Inherits ViewProps. | [ViewProps](https://reactnative.dev/docs/view#props) | | `false` |
41 |
42 | ### OutsidePressHandler
43 | Wrap every component you want to detect outside press with `OutsidePressHandler`.
44 |
45 | ```js
46 | import { View } from 'react-native';
47 | import OutsidePressHandler from 'react-native-outside-press';
48 |
49 | export default function MyComponent() {
50 | return (
51 | {
53 | console.log('Pressed outside the box!');
54 | }}
55 | >
56 |
57 |
58 | );
59 | }
60 | ```
61 |
62 | #### Props
63 |
64 | | Name | Description | Type | Default | Required? |
65 | |------------------|------------------------------------------------------|------------------------------------------------------|---------------|-----------|
66 | | `onOutsidePress` | Function to run when pressed outside of component. | function | | `true` |
67 | | `disabled` | Controls whether `onOutsidePress` should run or not. | boolean | `false` | `false` |
68 | | `ViewProps` | Inherits ViewProps. | [ViewProps](https://reactnative.dev/docs/view#props) | | `false` |
69 |
70 | ## Changelogs
71 | See [CHANGELOGS.md](CHANGELOGS.md)
72 |
73 | ## License
74 | Copyright © 2023 David Angulo, released under the MIT license, see [LICENSE](LICENSE).
75 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/example/App.js:
--------------------------------------------------------------------------------
1 | export { default } from './src/App';
2 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "example",
4 | "slug": "example",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "splash": {
10 | "image": "./assets/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#ffffff"
13 | },
14 | "assetBundlePatterns": [
15 | "**/*"
16 | ],
17 | "ios": {
18 | "supportsTablet": true
19 | },
20 | "android": {
21 | "adaptiveIcon": {
22 | "foregroundImage": "./assets/adaptive-icon.png",
23 | "backgroundColor": "#ffffff"
24 | }
25 | },
26 | "web": {
27 | "favicon": "./assets/favicon.png"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/example/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dcangulo/react-native-outside-press/fbeac099fa29631b693d8d5db8fcfee24d23dd9b/example/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/example/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dcangulo/react-native-outside-press/fbeac099fa29631b693d8d5db8fcfee24d23dd9b/example/assets/favicon.png
--------------------------------------------------------------------------------
/example/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dcangulo/react-native-outside-press/fbeac099fa29631b693d8d5db8fcfee24d23dd9b/example/assets/icon.png
--------------------------------------------------------------------------------
/example/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dcangulo/react-native-outside-press/fbeac099fa29631b693d8d5db8fcfee24d23dd9b/example/assets/splash.png
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const pak = require('../package.json');
3 |
4 | module.exports = function (api) {
5 | api.cache(true);
6 |
7 | return {
8 | presets: ['babel-preset-expo'],
9 | plugins: [
10 | [
11 | 'module-resolver',
12 | {
13 | extensions: ['.tsx', '.ts', '.js', '.json'],
14 | alias: {
15 | // For development, we want to alias the library to the source
16 | [pak.name]: path.join(__dirname, '..', pak.source),
17 | },
18 | },
19 | ],
20 | ],
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/example/metro.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const escape = require('escape-string-regexp');
3 | const { getDefaultConfig } = require('@expo/metro-config');
4 | const exclusionList = require('metro-config/src/defaults/exclusionList');
5 | const pak = require('../package.json');
6 |
7 | const root = path.resolve(__dirname, '..');
8 | const modules = Object.keys({ ...pak.peerDependencies });
9 |
10 | const defaultConfig = getDefaultConfig(__dirname);
11 |
12 | /**
13 | * Metro configuration
14 | * https://facebook.github.io/metro/docs/configuration
15 | *
16 | * @type {import('metro-config').MetroConfig}
17 | */
18 | const config = {
19 | ...defaultConfig,
20 |
21 | projectRoot: __dirname,
22 | watchFolders: [root],
23 |
24 | // We need to make sure that only one version is loaded for peerDependencies
25 | // So we block them at the root, and alias them to the versions in example's node_modules
26 | resolver: {
27 | ...defaultConfig.resolver,
28 |
29 | blacklistRE: exclusionList(
30 | modules.map(
31 | (m) =>
32 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`)
33 | )
34 | ),
35 |
36 | extraNodeModules: modules.reduce((acc, name) => {
37 | acc[name] = path.join(__dirname, 'node_modules', name);
38 | return acc;
39 | }, {}),
40 | },
41 | };
42 |
43 | module.exports = config;
44 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "expo": "~49.0.7",
13 | "expo-status-bar": "~1.6.0",
14 | "react": "18.2.0",
15 | "react-native": "0.72.3",
16 | "react-dom": "18.2.0",
17 | "react-native-web": "~0.19.6"
18 | },
19 | "devDependencies": {
20 | "@babel/core": "^7.20.0",
21 | "babel-plugin-module-resolver": "^5.0.0",
22 | "@expo/webpack-config": "^18.0.1",
23 | "babel-loader": "^8.1.0"
24 | },
25 | "private": true
26 | }
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { View, StyleSheet, Text } from 'react-native';
3 | import OutsidePressHandler, { EventProvider } from 'react-native-outside-press';
4 |
5 | export default function App() {
6 | return (
7 |
8 | {
10 | console.log('Pressed outside the black box!');
11 | }}
12 | >
13 |
14 | console.log('Pressed inside blackbox!')}
17 | >
18 | Press Me
19 |
20 |
21 |
22 | console.log('Pressed!')}>Press Me
23 | {
25 | console.log('Pressed outside the red box!');
26 | }}
27 | >
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | const styles = StyleSheet.create({
35 | container: {
36 | flex: 1,
37 | },
38 | blackBox: {
39 | height: 200,
40 | backgroundColor: 'black',
41 | },
42 | redBox: {
43 | height: 200,
44 | backgroundColor: 'red',
45 | },
46 | text: {
47 | color: 'white',
48 | },
49 | });
50 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig",
3 | "compilerOptions": {
4 | // Avoid expo-cli auto-generating a tsconfig
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config');
3 | const { resolver } = require('./metro.config');
4 |
5 | const root = path.resolve(__dirname, '..');
6 | const node_modules = path.join(__dirname, 'node_modules');
7 |
8 | module.exports = async function (env, argv) {
9 | const config = await createExpoWebpackConfigAsync(env, argv);
10 |
11 | config.module.rules.push({
12 | test: /\.(js|jsx|ts|tsx)$/,
13 | include: path.resolve(root, 'src'),
14 | use: 'babel-loader',
15 | });
16 |
17 | // We need to make sure that only one version is loaded for peerDependencies
18 | // So we alias them to the versions in example's node_modules
19 | Object.assign(config.resolve.alias, {
20 | ...resolver.extraNodeModules,
21 | 'react-native-web': path.join(node_modules, 'react-native-web'),
22 | });
23 |
24 | return config;
25 | };
26 |
--------------------------------------------------------------------------------
/lefthook.yml:
--------------------------------------------------------------------------------
1 | pre-commit:
2 | parallel: true
3 | commands:
4 | lint:
5 | files: git diff --name-only @{push}
6 | glob: "*.{js,ts,jsx,tsx}"
7 | run: npx eslint {files}
8 | types:
9 | files: git diff --name-only @{push}
10 | glob: "*.{js,ts, jsx, tsx}"
11 | run: npx tsc --noEmit
12 | commit-msg:
13 | parallel: true
14 | commands:
15 | commitlint:
16 | run: npx commitlint --edit
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-outside-press",
3 | "version": "1.2.2",
4 | "description": "airbnb/react-outside-click-handler but for React Native",
5 | "main": "lib/commonjs/index",
6 | "module": "lib/module/index",
7 | "types": "lib/typescript/index.d.ts",
8 | "react-native": "src/index",
9 | "source": "src/index",
10 | "files": [
11 | "src",
12 | "lib",
13 | "android",
14 | "ios",
15 | "cpp",
16 | "*.podspec",
17 | "!lib/typescript/example",
18 | "!ios/build",
19 | "!android/build",
20 | "!android/gradle",
21 | "!android/gradlew",
22 | "!android/gradlew.bat",
23 | "!android/local.properties",
24 | "!**/__tests__",
25 | "!**/__fixtures__",
26 | "!**/__mocks__",
27 | "!**/.*"
28 | ],
29 | "scripts": {
30 | "typecheck": "tsc --noEmit",
31 | "lint": "eslint \"**/*.{js,ts,tsx}\"",
32 | "prepack": "bob build",
33 | "example": "yarn --cwd example",
34 | "bootstrap": "yarn example && yarn install"
35 | },
36 | "keywords": [
37 | "react-native",
38 | "ios",
39 | "android"
40 | ],
41 | "repository": "https://github.com/dcangulo/react-native-outside-press",
42 | "author": "David Angulo (https://www.davidangulo.xyz)",
43 | "license": "MIT",
44 | "bugs": {
45 | "url": "https://github.com/dcangulo/react-native-outside-press/issues"
46 | },
47 | "homepage": "https://github.com/dcangulo/react-native-outside-press#readme",
48 | "devDependencies": {
49 | "@commitlint/config-conventional": "^18.0.0",
50 | "@evilmartians/lefthook": "^1.4.8",
51 | "@react-native-community/eslint-config": "^3.0.2",
52 | "@types/react": "~18.2.20",
53 | "@types/react-native": "0.72.6",
54 | "commitlint": "^18.0.0",
55 | "del-cli": "^5.0.0",
56 | "eslint": "^8.4.1",
57 | "eslint-config-prettier": "^9.0.0",
58 | "eslint-plugin-prettier": "^4.0.0",
59 | "pod-install": "^0.1.0",
60 | "prettier": "^2.0.5",
61 | "react": "18.2.0",
62 | "react-native": "0.72.6",
63 | "react-native-builder-bob": "^0.23.1",
64 | "typescript": "^5.0.2"
65 | },
66 | "resolutions": {
67 | "@types/react": "17.0.21"
68 | },
69 | "peerDependencies": {
70 | "react": "*",
71 | "react-native": "*"
72 | },
73 | "commitlint": {
74 | "extends": [
75 | "@commitlint/config-conventional"
76 | ]
77 | },
78 | "eslintConfig": {
79 | "root": true,
80 | "extends": [
81 | "@react-native-community",
82 | "prettier"
83 | ],
84 | "rules": {
85 | "prettier/prettier": [
86 | "error",
87 | {
88 | "quoteProps": "consistent",
89 | "singleQuote": true,
90 | "tabWidth": 2,
91 | "trailingComma": "es5",
92 | "useTabs": false
93 | }
94 | ],
95 | "react-hooks/exhaustive-deps": 0
96 | }
97 | },
98 | "eslintIgnore": [
99 | "node_modules/",
100 | "lib/"
101 | ],
102 | "prettier": {
103 | "quoteProps": "consistent",
104 | "singleQuote": true,
105 | "tabWidth": 2,
106 | "trailingComma": "es5",
107 | "useTabs": false
108 | },
109 | "react-native-builder-bob": {
110 | "source": "src",
111 | "output": "lib",
112 | "targets": [
113 | "commonjs",
114 | "module",
115 | [
116 | "typescript",
117 | {
118 | "project": "tsconfig.build.json"
119 | }
120 | ]
121 | ]
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/scripts/bootstrap.js:
--------------------------------------------------------------------------------
1 | const os = require('os');
2 | const path = require('path');
3 | const child_process = require('child_process');
4 |
5 | const root = path.resolve(__dirname, '..');
6 | const args = process.argv.slice(2);
7 | const options = {
8 | cwd: process.cwd(),
9 | env: process.env,
10 | stdio: 'inherit',
11 | encoding: 'utf-8',
12 | };
13 |
14 | if (os.type() === 'Windows_NT') {
15 | options.shell = true;
16 | }
17 |
18 | let result;
19 |
20 | if (process.cwd() !== root || args.length) {
21 | // We're not in the root of the project, or additional arguments were passed
22 | // In this case, forward the command to `yarn`
23 | result = child_process.spawnSync('yarn', args, options);
24 | } else {
25 | // If `yarn` is run without arguments, perform bootstrap
26 | result = child_process.spawnSync('yarn', ['bootstrap'], options);
27 | }
28 |
29 | process.exitCode = result.status;
30 |
--------------------------------------------------------------------------------
/src/components/container.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Platform, View } from 'react-native';
3 | import type { ViewProps } from 'react-native';
4 | import useEvent from '../hooks/use-event';
5 | import type { IEvent } from '../hooks/use-event-store';
6 | import deepClone from '../utils/deep-clone';
7 |
8 | export default function Container(props: ViewProps) {
9 | const { events, skippedEventId, setSkippedEventId } = useEvent();
10 | const runEvents = () => {
11 | events.forEach((event: IEvent) => {
12 | if (event.id === (global as any).rnopSkippedEventId) return;
13 | if (event.disabled) return;
14 |
15 | event.onOutsidePress();
16 | });
17 |
18 | if ((global as any).rnopSkippedEventId) setSkippedEventId('');
19 | };
20 |
21 | useEffect(() => {
22 | if (skippedEventId) runEvents();
23 | }, [skippedEventId]);
24 |
25 | return Platform.select({
26 | web: (
27 |
33 | {deepClone(props.children, runEvents)}
34 |
35 | ),
36 | default: (
37 |
38 | {props.children}
39 |
40 | ),
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/event-provider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import type { ViewProps } from 'react-native';
4 | import useEventStore from '../hooks/use-event-store';
5 | import EventContext from '../event-context';
6 | import Container from './container';
7 |
8 | export default function EventProvider(props: ViewProps) {
9 | const { style, ...rest } = props;
10 | const eventStore = useEventStore();
11 |
12 | return (
13 |
14 |
15 | {props.children}
16 |
17 |
18 | );
19 | }
20 |
21 | const styles = StyleSheet.create({
22 | container: {
23 | flex: 1,
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/src/components/outside-press-handler.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect } from 'react';
2 | import { View, Platform } from 'react-native';
3 | import type { ViewProps } from 'react-native';
4 | import useEvent from '../hooks/use-event';
5 | import deepClone from '../utils/deep-clone';
6 |
7 | interface IOutsidePressHandlerProps extends ViewProps {
8 | onOutsidePress: () => void;
9 | disabled?: boolean;
10 | }
11 |
12 | export default function OutsidePressHandler(props: IOutsidePressHandlerProps) {
13 | const { children, onOutsidePress, disabled = false } = props;
14 | const id: string = useRef(Math.random().toString()).current;
15 | const { appendEvent, removeEvent, setSkippedEventId } = useEvent();
16 | const setSkippedEventIdFunc = () => setSkippedEventId(id);
17 |
18 | useEffect(() => {
19 | appendEvent({ id, onOutsidePress, disabled });
20 |
21 | return () => removeEvent(id);
22 | }, [onOutsidePress, disabled]);
23 |
24 | return Platform.select({
25 | web: (
26 |
32 | {deepClone(children, setSkippedEventIdFunc)}
33 |
34 | ),
35 | default: (
36 |
37 | {children}
38 |
39 | ),
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/src/event-context.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 | import type { IEvent } from './hooks/use-event-store';
3 |
4 | export type EventContextType = {
5 | events: IEvent[];
6 | appendEvent: (newEvent: IEvent) => void;
7 | removeEvent: (id: string) => void;
8 | skippedEventId: string;
9 | setSkippedEventId: (id: string) => void;
10 | };
11 |
12 | const EventContext = createContext(null);
13 |
14 | export default EventContext;
15 |
--------------------------------------------------------------------------------
/src/hooks/use-event-store.ts:
--------------------------------------------------------------------------------
1 | import { useState, useMemo } from 'react';
2 |
3 | export interface IEvent {
4 | id: string;
5 | onOutsidePress: () => void;
6 | disabled: boolean;
7 | }
8 |
9 | export default function useEventStore() {
10 | const [events, setEvents] = useState([]);
11 | const [skippedEventId, setSkippedEventId] = useState('');
12 | const eventActions = useMemo(
13 | () => ({
14 | events,
15 | appendEvent: (newEvent: IEvent) =>
16 | setEvents((state) => [...state, newEvent]),
17 | removeEvent: (id: string) =>
18 | setEvents((state) => state.filter((event) => event.id !== id)),
19 | skippedEventId,
20 | setSkippedEventId: (id: string) => {
21 | (global as any).rnopSkippedEventId = id;
22 | setSkippedEventId(id);
23 | },
24 | }),
25 | [skippedEventId, events]
26 | );
27 |
28 | return eventActions;
29 | }
30 |
--------------------------------------------------------------------------------
/src/hooks/use-event.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import EventContext from '../event-context';
3 | import type { EventContextType } from '../event-context';
4 |
5 | export default function useEvent() {
6 | const eventContext = useContext(EventContext) as EventContextType;
7 |
8 | return eventContext;
9 | }
10 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import EventProvider from './components/event-provider';
2 | import OutsidePressHandler from './components/outside-press-handler';
3 |
4 | export { EventProvider };
5 | export default OutsidePressHandler;
6 |
--------------------------------------------------------------------------------
/src/utils/deep-clone.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function deepClone(children: React.ReactNode, func: () => void) {
4 | return React.Children.map(
5 | children,
6 | (child: React.ReactNode): React.ReactNode => {
7 | if (!React.isValidElement(child)) return child;
8 |
9 | const props: any = { ...child.props };
10 |
11 | if (typeof child.props.onPress === 'function') {
12 | props.onPress = () => {
13 | func();
14 | return child.props.onPress();
15 | };
16 | }
17 |
18 | return React.cloneElement(
19 | child,
20 | props,
21 | deepClone(child.props.children, func)
22 | );
23 | }
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "extends": "./tsconfig",
4 | "exclude": ["example"]
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "react-native-outside-press": ["./src/index"]
6 | },
7 | "allowUnreachableCode": false,
8 | "allowUnusedLabels": false,
9 | "esModuleInterop": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "jsx": "react",
12 | "lib": ["esnext"],
13 | "module": "esnext",
14 | "moduleResolution": "node",
15 | "noFallthroughCasesInSwitch": true,
16 | "noImplicitReturns": true,
17 | "noImplicitUseStrict": false,
18 | "noStrictGenericChecks": false,
19 | "noUncheckedIndexedAccess": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "resolveJsonModule": true,
23 | "skipLibCheck": true,
24 | "strict": true,
25 | "target": "esnext",
26 | "verbatimModuleSyntax": true
27 | }
28 | }
29 |
--------------------------------------------------------------------------------