-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update useMediaQuery documentation #128
- Loading branch information
1 parent
3776274
commit b32eda4
Showing
5 changed files
with
259 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { Meta } from '@storybook/blocks'; | ||
import { useRef } from 'react'; | ||
import { useClickedOutside } from './useClickedOutside'; | ||
|
||
<Meta title="hooks/useClickedOutside" /> | ||
|
||
# useClickedOutside | ||
|
||
This hook registers a click event listener on the document that triggers a callback function when | ||
the click event occurs outside of the specified element. | ||
|
||
## Reference | ||
|
||
```ts | ||
function useClickedOutside( | ||
target: Unreffable<Element>, | ||
listener: (mouseEvent: MouseEvent) => void, | ||
options?: EventListenerOptions, | ||
): void; | ||
``` | ||
|
||
## Example Usage | ||
|
||
Using the `useClickedOutside` hook with a RefObject: | ||
|
||
```jsx | ||
import { useRef } from 'react'; | ||
import { useClickedOutside } from './useClickedOutside'; | ||
|
||
function MyComponent() { | ||
const ref = useRef(null); | ||
|
||
useClickedOutside(ref, () => { | ||
console.log('Clicked outside!'); | ||
}); | ||
|
||
return <div ref={ref}>Click outside of this element to trigger the callback.</div>; | ||
} | ||
``` | ||
|
||
Using the `useClickedOutside` hook with an element from state: | ||
|
||
```jsx | ||
import { useRef } from 'react'; | ||
import { useClickedOutside } from './useClickedOutside'; | ||
|
||
function MyComponent() { | ||
const [element, setElement] = useState<HTMLDivElement>(); | ||
|
||
useClickedOutside(element, () => { | ||
console.log('Clicked outside!'); | ||
}); | ||
|
||
return <div ref={element}>Click outside of this element to trigger the callback.</div>; | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
/* eslint-disable react-hooks/rules-of-hooks */ | ||
/* eslint-disable react/no-multi-comp */ | ||
/* eslint-disable react/jsx-no-literals */ | ||
import { type Meta, type StoryObj } from '@storybook/react'; | ||
import { useRef, useState, type ReactElement } from 'react'; | ||
import { useClickedOutside } from './useClickedOutside.js'; | ||
|
||
export default { | ||
title: 'hooks/useClickedOutside', | ||
} satisfies Meta; | ||
|
||
export const RefObject = { | ||
render(): ReactElement { | ||
const ref = useRef(null); | ||
|
||
useClickedOutside(ref, () => { | ||
// eslint-disable-next-line no-console | ||
console.log('Clicked outside!'); | ||
}); | ||
|
||
return <div ref={ref}>Click outside of this element to trigger the callback.</div>; | ||
}, | ||
} satisfies StoryObj; | ||
|
||
export const State = { | ||
render(): ReactElement { | ||
const [element, setElement] = useState<HTMLDivElement | null>(null); | ||
|
||
useClickedOutside(element, () => { | ||
// eslint-disable-next-line no-console | ||
console.log('Clicked outside!'); | ||
}); | ||
|
||
return <div ref={setElement}>Click outside of this element to trigger the callback.</div>; | ||
}, | ||
} satisfies StoryObj; | ||
|
||
export const CustomOptions = { | ||
render(): ReactElement { | ||
const ref = useRef<HTMLDivElement>(null); | ||
|
||
useClickedOutside( | ||
ref, | ||
() => { | ||
// eslint-disable-next-line no-console | ||
console.log('Clicked outside!'); | ||
}, | ||
{ capture: true }, | ||
); | ||
|
||
return <div ref={ref}>Click outside of this element to trigger the callback.</div>; | ||
}, | ||
} satisfies StoryObj; | ||
|
||
export const MultipleElements = { | ||
render(): ReactElement { | ||
const ref1 = useRef(null); | ||
const ref2 = useRef(null); | ||
|
||
useClickedOutside(ref1, () => { | ||
// eslint-disable-next-line no-console | ||
console.log('Clicked outside of element 1!'); | ||
}); | ||
|
||
useClickedOutside(ref2, () => { | ||
// eslint-disable-next-line no-console | ||
console.log('Clicked outside of element 2!'); | ||
}); | ||
|
||
return ( | ||
<> | ||
<div ref={ref1}>Click outside of this element to trigger the callback for element 1.</div> | ||
<div ref={ref2}>Click outside of this element to trigger the callback for element 2.</div> | ||
</> | ||
); | ||
}, | ||
} satisfies StoryObj; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/* eslint-disable react/no-multi-comp, react/jsx-no-literals */ | ||
import { jest } from '@jest/globals'; | ||
import { fireEvent, render } from '@testing-library/react'; | ||
import { useRef, type ReactElement } from 'react'; | ||
import { useClickedOutside } from './useClickedOutside.js'; | ||
|
||
describe('useClickedOutside', () => { | ||
it('should call the callback function when clicked outside of the element', () => { | ||
const callback = jest.fn(); | ||
|
||
function MyComponent(): ReactElement { | ||
const ref = useRef(null); | ||
useClickedOutside(ref, callback); | ||
|
||
return ( | ||
<div ref={ref} style={{ inlineSize: 100, blockSize: 100 }}> | ||
My Component | ||
</div> | ||
); | ||
} | ||
|
||
render(<MyComponent />); | ||
|
||
fireEvent.click(document.body, { clientX: 150, clientY: 150 }); | ||
|
||
expect(callback).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should call the callback function when clicked inside of the element', () => { | ||
const callback = jest.fn(); | ||
|
||
function MyComponent(): ReactElement { | ||
const ref = useRef(null); | ||
useClickedOutside(ref, callback); | ||
|
||
return ( | ||
<div ref={ref} style={{ inlineSize: 100, blockSize: 100 }}> | ||
My Component | ||
</div> | ||
); | ||
} | ||
|
||
render(<MyComponent />); | ||
|
||
fireEvent.click(document.body, { clientX: 50, clientY: 50 }); | ||
|
||
expect(callback).toHaveBeenCalledTimes(1); | ||
expect(callback).toHaveBeenCalledWith(expect.objectContaining({ clientX: 50, clientY: 50 })); | ||
}); | ||
|
||
it('should call the callback function when clicked inside of the element stored in state', () => { | ||
const callback = jest.fn(); | ||
|
||
function MyComponent(): ReactElement { | ||
const ref = useRef(null); | ||
useClickedOutside(ref, callback); | ||
|
||
return ( | ||
<div ref={ref} style={{ inlineSize: 100, blockSize: 100 }}> | ||
My Component | ||
</div> | ||
); | ||
} | ||
|
||
render(<MyComponent />); | ||
|
||
fireEvent.click(document.body, { clientX: 50, clientY: 50 }); | ||
|
||
expect(callback).toHaveBeenCalledTimes(1); | ||
expect(callback).toHaveBeenCalledWith(expect.objectContaining({ clientX: 50, clientY: 50 })); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { useCallback, useMemo } from 'react'; | ||
import { unref, type Unreffable } from '../../utils/unref/unref.js'; | ||
import { useEventListener } from '../useEventListener/useEventListener.js'; | ||
|
||
/** | ||
* Registers a click event listener on the document that triggers a callback function when the | ||
* click event occurs outside of the specified element. | ||
* | ||
* @param ref | ||
* @param listener | ||
* @param options | ||
*/ | ||
export function useClickedOutside( | ||
target: Unreffable<Element | null>, | ||
listener: (mouseEvent: MouseEvent) => void, | ||
options?: EventListenerOptions, | ||
): void { | ||
const memoizedOptions = useMemo( | ||
() => options, | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[...Object.keys(options ?? {}), ...Object.values(options ?? {})], | ||
); | ||
|
||
const onClick = useCallback( | ||
(event: Event) => { | ||
// Using element bounds because a click on a shadow element (e.g. dialog backdrop) is also | ||
// considered to be a click outside an element | ||
const { | ||
top = 0, | ||
right = 0, | ||
bottom = 0, | ||
left = 0, | ||
} = unref(target)?.getBoundingClientRect() ?? {}; | ||
|
||
if (event instanceof MouseEvent) { | ||
const { clientX, clientY } = event; | ||
|
||
if (clientX >= left && clientX <= right && clientY >= top && clientY <= bottom) { | ||
// Clicked inside bounding box of the element | ||
return; | ||
} | ||
} else { | ||
throw new TypeError('Expected MouseEvent'); | ||
} | ||
|
||
listener(event); | ||
}, | ||
[listener, target], | ||
); | ||
|
||
useEventListener(globalThis.document, 'click', onClick, memoizedOptions); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters