Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this be more beneficial if this was a callback?
Usually a click outside is an action, when we click outside an element, we want to trigger a callback.
For example, a simple modal or a dropdown menu, clicking outside those elements would trigger a close.

Suggested change
setActive(false);
callback();

I'm not seeing a good case for using a state @luisdralves, but maybe you have a more explicit example.
WDYT @rafaelcruzazevedo?

}
};

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

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

return [active, setActive];
}