├── .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 | --------------------------------------------------------------------------------