├── .gitignore ├── jest.config.js ├── renovate.json ├── tsconfig.json ├── __tests__ ├── useFocus.test.ts ├── useActive.test.ts ├── useTouch.test.ts ├── useHover.test.ts ├── useWindowResize.test.tsx ├── useMousePosition.test.ts ├── useResizeObserver.test.tsx └── useClickOutside.test.tsx ├── src ├── index.ts ├── useFocus │ └── index.tsx ├── useActive │ └── index.tsx ├── useMousePosition │ └── index.tsx ├── useTouch │ └── index.tsx ├── useHover │ └── index.tsx ├── useWindowResize │ └── index.tsx ├── useResizeObserver │ └── index.tsx └── useClickOutside │ └── index.tsx ├── docs ├── useActive.mdx ├── useHover.mdx ├── useTouch.mdx ├── useWindowResize.mdx ├── useFocus.mdx ├── useMousePosition.mdx ├── useResizeObserver.mdx ├── intro.mdx └── useClickOutside.mdx ├── .github └── workflows │ └── npmpublish.yml ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | pkg 4 | .docz -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "jsdom", 3 | }; 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "major": { 7 | "automerge": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "jsx": "react", 7 | "strict": true, 8 | "allowSyntheticDefaultImports": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /__tests__/useFocus.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from "@testing-library/react"; 2 | import { useFocus } from "../src"; 3 | 4 | test("useFocus should react on focus/blur events", () => { 5 | const { result } = renderHook(() => useFocus()); 6 | 7 | expect(result.current[0]).toBeFalsy(); 8 | act(() => result.current[1].onFocus({} as React.FocusEvent)); 9 | expect(result.current[0]).toBeTruthy(); 10 | act(() => result.current[1].onBlur({} as React.FocusEvent)); 11 | expect(result.current[0]).toBeFalsy(); 12 | }); 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useActive } from './useActive/index'; 2 | export { default as useClickOutside } from './useClickOutside/index'; 3 | export { default as useFocus } from './useFocus/index'; 4 | export { default as useHover } from './useHover/index'; 5 | export { default as useMousePosition } from './useMousePosition/index'; 6 | export { default as useResizeObserver } from './useResizeObserver/index'; 7 | export { default as useTouch } from './useTouch/index'; 8 | export { default as useWindowResize } from './useWindowResize/index'; 9 | -------------------------------------------------------------------------------- /__tests__/useActive.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from "@testing-library/react"; 2 | import { useActive } from "../src"; 3 | 4 | test("useActive should react on mouseDown/mouseUp events", () => { 5 | const { result } = renderHook(() => useActive()); 6 | 7 | expect(result.current[0]).toBeFalsy(); 8 | act(() => result.current[1].onMouseDown({} as React.MouseEvent)); 9 | expect(result.current[0]).toBeTruthy(); 10 | act(() => result.current[1].onMouseUp({} as React.MouseEvent)); 11 | expect(result.current[0]).toBeFalsy(); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/useTouch.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from "@testing-library/react"; 2 | import { useTouch } from "../src"; 3 | 4 | test("useTouch should react on touchStart/touchEnd events", () => { 5 | const { result } = renderHook(() => useTouch()); 6 | 7 | expect(result.current[0]).toBeFalsy(); 8 | act(() => result.current[1].onTouchStart({} as React.TouchEvent)); 9 | expect(result.current[0]).toBeTruthy(); 10 | act(() => result.current[1].onTouchEnd({} as React.TouchEvent)); 11 | expect(result.current[0]).toBeFalsy(); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/useHover.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from "@testing-library/react"; 2 | import { useHover } from "../src"; 3 | 4 | test("useHover should react on mouseEnter/mouseLeave events", () => { 5 | const { result } = renderHook(() => useHover()); 6 | 7 | expect(result.current[0]).toBeFalsy(); 8 | act(() => result.current[1].onMouseEnter({} as React.MouseEvent)); 9 | expect(result.current[0]).toBeTruthy(); 10 | act(() => result.current[1].onMouseLeave({} as React.MouseEvent)); 11 | expect(result.current[0]).toBeFalsy(); 12 | }); 13 | -------------------------------------------------------------------------------- /src/useFocus/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function useFocus(): [ 4 | boolean, 5 | { 6 | onFocus: (e: React.FocusEvent) => void; 7 | onBlur: (e: React.FocusEvent) => void; 8 | } 9 | ] { 10 | const [isFocused, setFocused] = React.useState(false); 11 | 12 | const bind = React.useMemo( 13 | () => ({ 14 | onFocus: (e: React.FocusEvent) => void setFocused(true), 15 | onBlur: (e: React.FocusEvent) => void setFocused(false), 16 | }), 17 | [] 18 | ); 19 | 20 | return [isFocused, bind]; 21 | } 22 | 23 | export default useFocus; 24 | -------------------------------------------------------------------------------- /src/useActive/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function useActive(): [ 4 | boolean, 5 | { 6 | onMouseDown: (e: React.MouseEvent) => void; 7 | onMouseUp: (e: React.MouseEvent) => void; 8 | } 9 | ] { 10 | const [isActive, setActive] = React.useState(false); 11 | 12 | const bind = React.useMemo( 13 | () => ({ 14 | onMouseDown: (e: React.MouseEvent) => void setActive(true), 15 | onMouseUp: (e: React.MouseEvent) => void setActive(false), 16 | }), 17 | [] 18 | ); 19 | 20 | return [isActive, bind]; 21 | } 22 | 23 | export default useActive; 24 | -------------------------------------------------------------------------------- /src/useMousePosition/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function useMousePosition(): [ 4 | number, 5 | number, 6 | { onMouseMove: (e: React.MouseEvent) => void } 7 | ] { 8 | const [x, setX] = React.useState(0); 9 | const [y, setY] = React.useState(0); 10 | 11 | const bind = React.useMemo( 12 | () => ({ 13 | onMouseMove: (e: React.MouseEvent) => { 14 | setX(e.nativeEvent.offsetX); 15 | setY(e.nativeEvent.offsetY); 16 | }, 17 | }), 18 | [] 19 | ); 20 | 21 | return [x, y, bind]; 22 | } 23 | 24 | export default useMousePosition; 25 | -------------------------------------------------------------------------------- /src/useTouch/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function useTouch(): [ 4 | boolean, 5 | { 6 | onTouchStart: (e: React.TouchEvent) => void; 7 | onTouchEnd: (e: React.TouchEvent) => void; 8 | } 9 | ] { 10 | const [isTouched, setTouched] = React.useState(false); 11 | 12 | const bind = React.useMemo( 13 | () => ({ 14 | onTouchStart: (e: React.TouchEvent) => void setTouched(true), 15 | onTouchEnd: (e: React.TouchEvent) => void setTouched(false), 16 | }), 17 | [] 18 | ); 19 | 20 | return [isTouched, bind]; 21 | } 22 | 23 | export default useTouch; 24 | -------------------------------------------------------------------------------- /src/useHover/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function useHover(): [ 4 | boolean, 5 | { 6 | onMouseEnter: (e: React.MouseEvent) => void; 7 | onMouseLeave: (e: React.MouseEvent) => void; 8 | } 9 | ] { 10 | const [isHovered, setHovered] = React.useState(false); 11 | 12 | const bind = React.useMemo( 13 | () => ({ 14 | onMouseEnter: (e: React.MouseEvent) => void setHovered(true), 15 | onMouseLeave: (e: React.MouseEvent) => void setHovered(false), 16 | }), 17 | [] 18 | ); 19 | 20 | return [isHovered, bind]; 21 | } 22 | 23 | export default useHover; 24 | -------------------------------------------------------------------------------- /src/useWindowResize/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function useWindowResize(): [number, number] { 4 | const [width, setWidth] = React.useState(window.innerWidth); 5 | const [height, setHeight] = React.useState(window.innerHeight); 6 | 7 | const resize = React.useCallback(() => { 8 | setWidth(window.innerWidth); 9 | setHeight(window.innerHeight); 10 | }, []); 11 | 12 | React.useEffect(() => { 13 | window.addEventListener('resize', resize); 14 | return () => void window.removeEventListener('resize', resize); 15 | }, [resize]); 16 | 17 | return [width, height]; 18 | } 19 | 20 | export default useWindowResize; 21 | -------------------------------------------------------------------------------- /docs/useActive.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: useActive 3 | --- 4 | 5 | import { Playground } from 'docz'; 6 | import { useActive } from '../src'; 7 | 8 | # useActive 9 | 10 | ```js 11 | import { useActive } from 'use-events'; 12 | ``` 13 | 14 | ```jsx 15 | const Example = () => { 16 | const [isActive, bind] = useActive(); 17 | 18 | return ( 19 |
20 | You are {isActive ? 'clicking' : 'not clicking'} this div 21 |
22 | ); 23 | }; 24 | ``` 25 | 26 | 27 | {() => { 28 | const [isActive, bind] = useActive(); 29 | return ( 30 |
31 | You are {isActive ? 'clicking' : 'not clicking'} this div 32 |
33 | ); 34 | }} 35 |
36 | -------------------------------------------------------------------------------- /docs/useHover.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: useHover 3 | --- 4 | 5 | import { Playground } from 'docz'; 6 | import { useHover } from '../src'; 7 | 8 | # useHover 9 | 10 | ```js 11 | import { useHover } from 'use-events'; 12 | ``` 13 | 14 | ```jsx 15 | const Example = () => { 16 | const [isHovered, bind] = useHover(); 17 | 18 | return ( 19 |
20 | You are {isHovered ? 'hovering' : 'not hovering'} this div 21 |
22 | ); 23 | }; 24 | ``` 25 | 26 | 27 | {() => { 28 | const [isHovered, bind] = useHover(); 29 | return ( 30 |
31 | You are {isHovered ? 'hovering' : 'not hovering'} this div 32 |
33 | ); 34 | }} 35 |
36 | -------------------------------------------------------------------------------- /docs/useTouch.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: useTouch 3 | --- 4 | 5 | import { Playground } from 'docz'; 6 | import { useTouch } from '../src'; 7 | 8 | # useTouch 9 | 10 | ```js 11 | import { useTouch } from 'use-events'; 12 | ``` 13 | 14 | ```jsx 15 | const Example = () => { 16 | const [isTouched, bind] = useTouch(); 17 | 18 | return ( 19 |
20 | You are {isTouched ? 'touching' : 'not touching'} this div 21 |
22 | ); 23 | }; 24 | ``` 25 | 26 | 27 | {() => { 28 | const [isTouched, bind] = useTouch(); 29 | return ( 30 |
31 | You are {isTouched ? 'touching' : 'not touching'} this div 32 |
33 | ); 34 | }} 35 |
36 | -------------------------------------------------------------------------------- /docs/useWindowResize.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: useWindowResize 3 | --- 4 | 5 | import { Playground } from 'docz'; 6 | import { useWindowResize } from '../src'; 7 | 8 | # useWindowResize 9 | 10 | ```js 11 | import { useWindowResize } from 'use-events'; 12 | ``` 13 | 14 | ```jsx 15 | const Example = () => { 16 | const [width, height] = useWindowResize(); 17 | 18 | return ( 19 |
20 | width: {width} 21 | height: {height} 22 |
23 | ); 24 | }; 25 | ``` 26 | 27 | 28 | {() => { 29 | const [width, height] = useWindowResize(); 30 | return ( 31 |
32 | width: {width} 33 | height: {height} 34 |
35 | ); 36 | }} 37 |
38 | -------------------------------------------------------------------------------- /__tests__/useWindowResize.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent } from "@testing-library/react"; 2 | import { act, renderHook } from "@testing-library/react"; 3 | import { useWindowResize } from "../src"; 4 | 5 | const resize = (width: number, height: number) => { 6 | // @ts-ignore 7 | window.innerWidth = width; 8 | // @ts-ignore 9 | window.innerHeight = height; 10 | fireEvent(window, new Event("resize")); 11 | }; 12 | 13 | test("useWindowResize should react on window resize event", () => { 14 | const { result } = renderHook(() => useWindowResize()); 15 | 16 | act(() => resize(100, 100)); 17 | expect(result.current[0]).toBe(100); 18 | expect(result.current[1]).toBe(100); 19 | 20 | act(() => resize(200, 200)); 21 | expect(result.current[0]).toBe(200); 22 | expect(result.current[1]).toBe(200); 23 | }); 24 | -------------------------------------------------------------------------------- /docs/useFocus.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: useFocus 3 | --- 4 | 5 | import { Playground } from 'docz'; 6 | import { useFocus } from '../src'; 7 | 8 | # useFocus 9 | 10 | ```js 11 | import { useFocus } from 'use-events'; 12 | ``` 13 | 14 | ```jsx 15 | const Example = () => { 16 | const [isFocused, bind] = useFocus(); 17 | 18 | return ( 19 |
20 | 21 |
You are {isFocused ? 'focusing' : 'not focusing'} the input
22 |
23 | ); 24 | }; 25 | ``` 26 | 27 | 28 | {() => { 29 | const [isFocused, bind] = useFocus(); 30 | return ( 31 |
32 | 33 |
You are {isFocused ? 'focusing' : 'not focusing'} the input
34 |
35 | ); 36 | }} 37 |
38 | -------------------------------------------------------------------------------- /__tests__/useMousePosition.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from "@testing-library/react"; 2 | import { useMousePosition } from "../src"; 3 | 4 | test("useTouch should react on mouseMove event", () => { 5 | const { result } = renderHook(() => useMousePosition()); 6 | 7 | expect(result.current[0]).toBe(0); 8 | expect(result.current[1]).toBe(0); 9 | act(() => { 10 | return result.current[2].onMouseMove({ 11 | nativeEvent: { offsetX: 1, offsetY: 2 }, 12 | } as React.MouseEvent); 13 | }); 14 | expect(result.current[0]).toBe(1); 15 | expect(result.current[1]).toBe(2); 16 | act(() => { 17 | return result.current[2].onMouseMove({ 18 | nativeEvent: { offsetX: 3, offsetY: 4 }, 19 | } as React.MouseEvent); 20 | }); 21 | expect(result.current[0]).toBe(3); 22 | expect(result.current[1]).toBe(4); 23 | }); 24 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: NPM publish 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | publish-npm: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@master 16 | 17 | - name: Set Node.js 20.x 18 | uses: actions/setup-node@v4 19 | 20 | with: 21 | node-version: 20 22 | registry-url: https://registry.npmjs.org/ 23 | 24 | - name: Install dependencies 25 | run: yarn 26 | 27 | - name: Typechecking 28 | run: yarn tsc 29 | 30 | - name: Test 31 | run: yarn test 32 | 33 | - name: Build 34 | run: yarn build 35 | 36 | - name: Publish 37 | run: npm publish 38 | working-directory: ./pkg 39 | env: 40 | NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}} 41 | -------------------------------------------------------------------------------- /docs/useMousePosition.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: useMousePosition 3 | --- 4 | 5 | import { Playground } from 'docz'; 6 | import { useMousePosition } from '../src'; 7 | 8 | # useMousePosition 9 | 10 | ```js 11 | import { useMousePosition } from 'use-events'; 12 | ``` 13 | 14 | ```jsx 15 | const Example = () => { 16 | const [x, y, bind] = useMousePosition(); 17 | 18 | return ( 19 |
20 | {x} - {y} 21 |
22 | ); 23 | }; 24 | ``` 25 | 26 | 27 | {() => { 28 | const [x, y, bind] = useMousePosition(); 29 | return ( 30 |
42 | x: {x} y: {y} 43 |
44 | ); 45 | }} 46 |
47 | -------------------------------------------------------------------------------- /__tests__/useResizeObserver.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/react"; 2 | import React from "react"; 3 | import ResizeObserver from "resize-observer-polyfill"; 4 | import { useResizeObserver } from "../src"; 5 | 6 | // @ts-ignore 7 | jest.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => cb()); 8 | jest.mock("resize-observer-polyfill"); 9 | 10 | const TestComponent = () => { 11 | const ref = React.useRef(null); 12 | const [width, height] = useResizeObserver(ref); 13 | return
{width + height}
; 14 | }; 15 | 16 | const resize = (width: number, height: number) => { 17 | // @ts-ignore 18 | ResizeObserver.mockReset(); 19 | // @ts-ignore 20 | ResizeObserver.mockImplementation((cb) => { 21 | cb([{ contentRect: { width, height } }]); 22 | return { observe: jest.fn, disconnect: jest.fn }; 23 | }); 24 | 25 | const { container } = render(); 26 | return container.textContent; 27 | }; 28 | 29 | test("useResizeObserver", () => { 30 | expect(resize(100, 100)).toBe("200"); 31 | expect(resize(200, 200)).toBe("400"); 32 | }); 33 | -------------------------------------------------------------------------------- /docs/useResizeObserver.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: useResizeObserver 3 | --- 4 | 5 | import { Playground } from 'docz'; 6 | import { useResizeObserver } from '../src'; 7 | 8 | # useResizeObserver 9 | 10 | ```js 11 | import { useResizeObserver } from 'use-events'; 12 | ``` 13 | 14 | ```jsx 15 | const Example = () => { 16 | const ref = React.useRef(null); 17 | const [width, height] = useResizeObserver(ref); 18 | 19 | return ( 20 |
21 | width: {width} 22 | height: {height} 23 |
24 | ); 25 | }; 26 | ``` 27 | 28 | 29 | {() => { 30 | const ref = React.useRef(null); 31 | const [width, height] = useResizeObserver(ref); 32 | return ( 33 |
44 | width: {width} 45 | height: {height} 46 |
47 | ); 48 | 49 | }} 50 | 51 |
52 | -------------------------------------------------------------------------------- /docs/intro.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Introduction 3 | route: / 4 | order: 1 5 | --- 6 | 7 | # Introduction 8 | 9 | **Use Events** - Events turned into React Hooks. 10 | Read about [Hooks](https://reactjs.org/docs/hooks-intro.html) feature. 11 | 12 | ## Install 13 | 14 | > Note: React 16.8+ is required for Hooks. 15 | 16 | ### With npm 17 | 18 | ```sh 19 | npm i use-events 20 | ``` 21 | 22 | ### Or with yarn 23 | 24 | ```sh 25 | yarn add use-events 26 | ``` 27 | 28 | exposed as `UseEvents` 29 | 30 | ## Quick Examples 31 | 32 | ```jsx 33 | import { useActive, useHover, useMousePosition } from 'use-events'; 34 | ``` 35 | 36 | ```jsx 37 | const App = () => { 38 | const [isActive, bind] = useActive(); 39 | 40 | return ( 41 | 44 | ); 45 | }; 46 | ``` 47 | 48 | ```jsx 49 | const App = () => { 50 | const [isHovered, hoverBind] = useHover(); 51 | const [x, y, mousePositionBind] = useMousePosition(); 52 | 53 | return ( 54 |
55 | {isHovered && ( 56 | 57 | {x} - {y} 58 | 59 | )} 60 |
61 | ); 62 | }; 63 | ``` 64 | -------------------------------------------------------------------------------- /src/useResizeObserver/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ResizeObserver from "resize-observer-polyfill"; 3 | 4 | function useResizeObserver( 5 | ref: React.RefObject, 6 | ): [number, number] { 7 | const animationFrameID = React.useRef(0); 8 | const [width, setWidth] = React.useState(0); 9 | const [height, setHeight] = React.useState(0); 10 | 11 | React.useLayoutEffect(() => { 12 | // https://github.com/microsoft/TypeScript/issues/37861 13 | // @ts-ignore 14 | const resizeObserver = new ResizeObserver((entries) => { 15 | if (!Array.isArray(entries) || entries.length === 0) { 16 | return; 17 | } 18 | 19 | const { width, height } = entries[0].contentRect; 20 | 21 | // https://github.com/WICG/resize-observer/issues/38 22 | animationFrameID.current = requestAnimationFrame(() => { 23 | setWidth(width); 24 | setHeight(height); 25 | }); 26 | }); 27 | 28 | if (ref.current !== null) { 29 | resizeObserver.observe(ref.current); 30 | } 31 | 32 | return () => { 33 | if (animationFrameID.current) { 34 | cancelAnimationFrame(animationFrameID.current); 35 | } 36 | 37 | resizeObserver.disconnect(); 38 | }; 39 | }, [ref]); 40 | 41 | return [width, height]; 42 | } 43 | 44 | export default useResizeObserver; 45 | -------------------------------------------------------------------------------- /src/useClickOutside/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function useClickOutside( 4 | refs: React.RefObject[], 5 | onClickOutside: (e: MouseEvent) => void, 6 | ): [boolean] { 7 | const [isActive, setActive] = React.useState(false); 8 | 9 | const isOutside = React.useCallback( 10 | (e: MouseEvent) => { 11 | const test = refs.map((ref) => { 12 | return ( 13 | ref.current !== null && !ref.current.contains(e.target as HTMLElement) 14 | ); 15 | }); 16 | 17 | return test.every(Boolean); 18 | }, 19 | [refs], 20 | ); 21 | 22 | const mousedown = React.useCallback( 23 | (e: MouseEvent) => { 24 | if (isOutside(e)) { 25 | setActive(true); 26 | onClickOutside(e); 27 | } 28 | }, 29 | [isOutside, onClickOutside], 30 | ); 31 | 32 | const mouseup = React.useCallback( 33 | (e: MouseEvent) => { 34 | if (isOutside(e)) { 35 | setActive(false); 36 | } 37 | }, 38 | [isOutside], 39 | ); 40 | 41 | React.useEffect(() => { 42 | document.addEventListener("mousedown", mousedown); 43 | document.addEventListener("mouseup", mouseup); 44 | 45 | return () => { 46 | document.removeEventListener("mousedown", mousedown); 47 | document.removeEventListener("mouseup", mouseup); 48 | }; 49 | }, [refs, onClickOutside]); 50 | 51 | return [isActive]; 52 | } 53 | 54 | export default useClickOutside; 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Use Events 3 |

4 | 5 | ![npm](https://img.shields.io/npm/dt/use-events.svg) 6 | ![npm](https://img.shields.io/npm/v/use-events.svg) 7 | ![NpmLicense](https://img.shields.io/npm/l/use-events.svg) 8 | 9 | **Use Events** - Events turned into React Hooks. 10 | Read about [Hooks](https://reactjs.org/docs/hooks-intro.html) feature. 11 | 12 | ## Documentation 13 | 14 | https://sandiiarov.github.io/use-events 15 | 16 | ### List of hooks 17 | 18 | - [useActive](https://sandiiarov.github.io/use-events/#/docs-use-active) 19 | - [useClickOutside](https://sandiiarov.github.io/use-events/#/docs-use-click-outside) 20 | - [useFocus](https://sandiiarov.github.io/use-events/#/docs-use-focus) 21 | - [useHover](https://sandiiarov.github.io/use-events/#/docs-use-hover) 22 | - [useMousePosition](https://sandiiarov.github.io/use-events/#/docs-use-mouse-position) 23 | - [useTouch](https://sandiiarov.github.io/use-events/#/docs-use-touch) 24 | - [useWindowResize](https://sandiiarov.github.io/use-events/#/docs-use-window-resize) 25 | - [useResizeObserver](https://sandiiarov.github.io/use-events/#/docs-use-resize-observer) 26 | 27 | ## Installation 28 | 29 | > Note: React 16.8+ is required for Hooks. 30 | 31 | ### With npm 32 | 33 | ```sh 34 | npm i use-events 35 | ``` 36 | 37 | ### Or with yarn 38 | 39 | ```sh 40 | yarn add use-events 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/useClickOutside.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: useClickOutside 3 | --- 4 | 5 | import { Playground } from 'docz'; 6 | import { useClickOutside } from '../src'; 7 | 8 | # useClickOutside 9 | 10 | ```js 11 | import { useClickOutside } from 'use-events'; 12 | ``` 13 | 14 | ```jsx 15 | const Example = () => { 16 | const ref1 = React.useRef(null); 17 | const ref2 = React.useRef(null); 18 | const [isActive] = useClickOutside([ref1, ref2], event => console.log(event)); 19 | 20 | return ( 21 |
22 |
23 | You are {isActive ? 'clicking' : 'not clicking'} outside of this div 24 |
25 |
26 |
27 | You are {isActive ? 'clicking' : 'not clicking'} outside of this div 28 |
29 |
30 | ); 31 | }; 32 | ``` 33 | 34 | 35 | {() => { 36 | const ref1 = React.useRef(null); 37 | const ref2 = React.useRef(null); 38 | const [isActive] = useClickOutside([ref1, ref2], event => 39 | console.log(event) 40 | ); 41 | return ( 42 |
43 |
44 | You are {isActive ? 'clicking' : 'not clicking'} outside of this div 45 |
46 |
47 |
48 | You are {isActive ? 'clicking' : 'not clicking'} outside of this div 49 |
50 |
51 | ); 52 | }} 53 |
54 | -------------------------------------------------------------------------------- /__tests__/useClickOutside.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render } from "@testing-library/react"; 2 | import React from "react"; 3 | import { useClickOutside } from "../src"; 4 | 5 | test("useClickOutside should react on click outside and call callback", () => { 6 | const mockFn = jest.fn(); 7 | 8 | const TestComponent = () => { 9 | const ref1 = React.useRef(null); 10 | const ref2 = React.useRef(null); 11 | const [isActive] = useClickOutside([ref1, ref2], mockFn); 12 | return ( 13 |
14 |
15 | {isActive ? "Foo" : "Baz"} 16 |
17 |
18 | {isActive ? "Foo" : "Baz"} 19 |
20 |
21 | ); 22 | }; 23 | 24 | const { container, getByTestId } = render(); 25 | 26 | const firstEl = getByTestId("1"); 27 | const secondEl = getByTestId("2"); 28 | 29 | expect(firstEl.textContent).toBe("Baz"); 30 | expect(secondEl.textContent).toBe("Baz"); 31 | 32 | fireEvent.mouseDown(container); 33 | expect(firstEl.textContent).toBe("Foo"); 34 | expect(secondEl.textContent).toBe("Foo"); 35 | fireEvent.mouseUp(container); 36 | 37 | fireEvent.mouseDown(firstEl); 38 | expect(firstEl.textContent).toBe("Baz"); 39 | expect(secondEl.textContent).toBe("Baz"); 40 | fireEvent.mouseUp(firstEl); 41 | 42 | fireEvent.mouseDown(secondEl); 43 | expect(firstEl.textContent).toBe("Baz"); 44 | expect(secondEl.textContent).toBe("Baz"); 45 | fireEvent.mouseUp(secondEl); 46 | 47 | expect(mockFn).toBeCalledTimes(1); 48 | }); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-events", 3 | "version": "1.4.5", 4 | "license": "MIT", 5 | "description": "Event Hooks", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/sandiiarov/use-events.git" 9 | }, 10 | "scripts": { 11 | "build": "pika build", 12 | "test": "jest", 13 | "tsc": "tsc --noEmit" 14 | }, 15 | "babel": { 16 | "presets": [ 17 | "@babel/preset-typescript", 18 | "@babel/preset-react" 19 | ], 20 | "env": { 21 | "test": { 22 | "presets": [ 23 | "@babel/preset-env" 24 | ] 25 | } 26 | } 27 | }, 28 | "@pika/pack": { 29 | "pipeline": [ 30 | [ 31 | "@pika/plugin-standard-pkg" 32 | ], 33 | [ 34 | "@pika/plugin-build-node" 35 | ], 36 | [ 37 | "@pika/plugin-build-web" 38 | ], 39 | [ 40 | "@pika/plugin-build-types" 41 | ] 42 | ] 43 | }, 44 | "peerDependencies": { 45 | "react": ">=16.8.1" 46 | }, 47 | "dependencies": { 48 | "resize-observer-polyfill": "1.5.1" 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "7.6.4", 52 | "@babel/preset-env": "7.6.3", 53 | "@babel/preset-react": "7.6.3", 54 | "@babel/preset-typescript": "7.6.0", 55 | "@pika/pack": "0.5.0", 56 | "@pika/plugin-build-node": "0.6.1", 57 | "@pika/plugin-build-types": "0.6.1", 58 | "@pika/plugin-build-web": "0.6.1", 59 | "@pika/plugin-standard-pkg": "0.6.1", 60 | "@testing-library/dom": "10.4.0", 61 | "@testing-library/react": "16.2.0", 62 | "@types/jest": "24.0.18", 63 | "@types/react": "19.0.8", 64 | "@types/react-dom": "19.0.3", 65 | "jest": "29.7.0", 66 | "jest-environment-jsdom": "29.7.0", 67 | "react": "19.0.0", 68 | "react-dom": "19.0.0", 69 | "typescript": "5.7.3" 70 | } 71 | } 72 | --------------------------------------------------------------------------------