Skip to content

Commit

Permalink
Add use-detect-outside-click hook
Browse files Browse the repository at this point in the history
  • Loading branch information
luisdralves committed Jul 15, 2022
1 parent a72dcee commit 38ee5d5
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 1 deletion.
2 changes: 1 addition & 1 deletion packages/hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"jest": {
"preset": "ts-jest",
"testEnvironment": "jsdom",
"testRegex": "(src/.*\\.test.ts)$"
"testRegex": "(src/.*\\.test.tsx?)$"
},
"devDependencies": {
"@testing-library/react": "12.1.2",
Expand Down
75 changes: 75 additions & 0 deletions packages/hooks/src/use-detect-outside-click/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Module dependencies.
*/

import { fireEvent, render, screen } from '@testing-library/react';
import { useDetectOutsideClick } from './';
import React, { useRef } from 'react';

/**
* `Mock` component.
*/

const Mock = ({ initiallyActive }: { initiallyActive?: boolean }) => {
const ref = useRef<HTMLDivElement>(null);
const [active, setActive] = useDetectOutsideClick(ref, initiallyActive);

return (
<>
<div
onClick={() => setActive(true)}
ref={ref}
>
{active ? 'active' : 'inactive'}
</div>

<div>{'outside'}</div>
</>
);
};

/**
* Test `useDetectOutsideClick` hook.
*/

describe(`'useDetectOutsideClick' hook`, () => {
it('should lose focus', () => {
render(<Mock initiallyActive />);

expect(screen.queryByText('active')).toBeTruthy();

fireEvent.click(screen.getByText('outside'));

expect(screen.queryByText('inactive')).toBeTruthy();
});

it('should gain focus', () => {
render(<Mock />);

expect(screen.queryByText('inactive')).toBeTruthy();

fireEvent.click(screen.getByText('inactive'));

expect(screen.queryByText('active')).toBeTruthy();
});

it('should keep focused', () => {
render(<Mock initiallyActive />);

expect(screen.queryByText('active')).toBeTruthy();

fireEvent.click(screen.getByText('active'));

expect(screen.queryByText('active')).toBeTruthy();
});

it('should keep unfocused', () => {
render(<Mock />);

expect(screen.queryByText('inactive')).toBeTruthy();

fireEvent.click(screen.getByText('outside'));

expect(screen.queryByText('inactive')).toBeTruthy();
});
});
48 changes: 48 additions & 0 deletions packages/hooks/src/use-detect-outside-click/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Module dependencies.
*/

import {
Dispatch,
RefObject,
SetStateAction,
useEffect,
useState
} from 'react';

/**
* `Result` Props.
*/

type Result = [boolean, Dispatch<SetStateAction<boolean>>];

/**
* Export `useDetectOutsideClick` hook.
*/

export function useDetectOutsideClick(
ref: RefObject<HTMLElement> | null | undefined,
initialState = false
): Result {
const [active, setActive] = useState(initialState);

useEffect(() => {
const handleClickOutside = ({ target }: MouseEvent) => {
if (
ref?.current &&
target instanceof Element &&
!ref?.current?.contains(target)
) {
setActive(false);
}
};

document.addEventListener('click', handleClickOutside, true);

return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, [ref]);

return [active, setActive];
}
1 change: 1 addition & 0 deletions packages/hooks/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"jsx": "react",
"allowSyntheticDefaultImports": true,
"declaration": true,
"esModuleInterop": true,
Expand Down

0 comments on commit 38ee5d5

Please sign in to comment.