├── .babelrc.js
├── .gitignore
├── .huskyrc
├── .lintstagedrc
├── .npmrc
├── .prettierrc
├── README.md
├── SECURITY.md
├── package.json
├── src
└── index.ts
└── tsconfig.json
/.babelrc.js:
--------------------------------------------------------------------------------
1 | const loose = true
2 |
3 | module.exports = {
4 | presets: [
5 | [
6 | '@babel/env',
7 | {
8 | loose,
9 | modules: false,
10 | },
11 | ],
12 | '@babel/typescript',
13 | ],
14 | }
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | types
4 |
--------------------------------------------------------------------------------
/.huskyrc:
--------------------------------------------------------------------------------
1 | {
2 | "hooks": {
3 | "pre-commit": "lint-staged"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "*.{js,ts,json,md}": [
3 | "prettier --write",
4 | "git add"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "trailingComma": "all"
5 | }
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # use-onclickoutside
2 |
3 | React hook for listening for clicks outside of an element.
4 |
5 | ## Usage
6 |
7 | ```js
8 | import * as React from 'react'
9 | import useOnClickOutside from 'use-onclickoutside'
10 |
11 | export default function Modal({ close }) {
12 | const ref = React.useRef(null)
13 | useOnClickOutside(ref, close)
14 |
15 | return
{'Modal content'}
16 | }
17 | ```
18 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security policy
2 |
3 | ## Supported versions
4 |
5 | The latest version of the project is currently supported with security updates.
6 |
7 | ## Reporting a vulnerability
8 |
9 | You can report a vulnerability by contacting maintainers via email.
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "use-onclickoutside",
3 | "version": "0.4.1",
4 | "description": "React hook for listening for clicks outside of an element.",
5 | "main": "dist/use-onclickoutside.cjs.js",
6 | "module": "dist/use-onclickoutside.esm.js",
7 | "types": "./dist/declarations/src/index.d.ts",
8 | "browser": {
9 | "./dist/use-onclickoutside.cjs.js": "./dist/use-onclickoutside.browser.cjs.js",
10 | "./dist/use-onclickoutside.esm.js": "./dist/use-onclickoutside.browser.esm.js"
11 | },
12 | "files": [
13 | "dist"
14 | ],
15 | "scripts": {
16 | "test": "echo \"Warning: no test specified\" || jest --env=node",
17 | "build": "preconstruct build",
18 | "preversion": "npm test",
19 | "prepare": "npm run build"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/Andarist/use-onclickoutside.git"
24 | },
25 | "author": "",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/Andarist/use-onclickoutside/issues"
29 | },
30 | "homepage": "https://github.com/Andarist/use-onclickoutside#readme",
31 | "peerDependencies": {
32 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
33 | },
34 | "devDependencies": {
35 | "@babel/core": "^7.18.5",
36 | "@babel/preset-env": "^7.18.2",
37 | "@babel/preset-typescript": "^7.17.12",
38 | "@preconstruct/cli": "^2.1.5",
39 | "@types/react": "^18.0.12",
40 | "husky": "^1.1.3",
41 | "jest": "^28.1.1",
42 | "lint-staged": "^8.0.4",
43 | "prettier": "^2.7.0",
44 | "react": "^18.1.0",
45 | "rimraf": "^3.0.2",
46 | "typescript": "^4.7.3"
47 | },
48 | "dependencies": {
49 | "are-passive-events-supported": "^1.1.1",
50 | "use-latest": "^1.2.1"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import arePassiveEventsSupported from 'are-passive-events-supported'
3 | import useLatest from 'use-latest'
4 |
5 | const MOUSEDOWN = 'mousedown'
6 | const TOUCHSTART = 'touchstart'
7 |
8 | type HandledEvents = [typeof MOUSEDOWN, typeof TOUCHSTART]
9 | type HandledEventsType = HandledEvents[number]
10 | type PossibleEvent = {
11 | [Type in HandledEventsType]: HTMLElementEventMap[Type]
12 | }[HandledEventsType]
13 | type Handler = (event: PossibleEvent) => void
14 |
15 | const events: HandledEvents = [MOUSEDOWN, TOUCHSTART]
16 |
17 | const getAddOptions = (
18 | event: HandledEventsType,
19 | ): AddEventListenerOptions | undefined => {
20 | if (event === TOUCHSTART && arePassiveEventsSupported()) {
21 | return { passive: true }
22 | }
23 | }
24 |
25 | const currentDocument = typeof document !== 'undefined' ? document : undefined
26 |
27 | export default function useOnClickOutside(
28 | ref: React.RefObject,
29 | handler: Handler | null,
30 | { document = currentDocument } = {},
31 | ) {
32 | if (typeof document === 'undefined') {
33 | return
34 | }
35 |
36 | const handlerRef = useLatest(handler)
37 |
38 | useEffect(() => {
39 | if (!handler) {
40 | return
41 | }
42 |
43 | const listener = (event: PossibleEvent) => {
44 | if (
45 | !ref.current ||
46 | !handlerRef.current ||
47 | ref.current.contains(event.target as Node)
48 | ) {
49 | return
50 | }
51 |
52 | handlerRef.current(event)
53 | }
54 |
55 | events.forEach(event => {
56 | document.addEventListener(event, listener, getAddOptions(event))
57 | })
58 |
59 | return () => {
60 | events.forEach(event => {
61 | document.removeEventListener(event, listener)
62 | })
63 | }
64 | }, [!handler])
65 | }
66 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "forceConsistentCasingInFileNames": true,
4 | "lib": ["dom", "esnext"],
5 | "module": "esnext",
6 | "moduleResolution": "node",
7 | "resolveJsonModule": true,
8 | "rootDir": "./src",
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "target": "esnext"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------