@sanity/ui
Version:
The Sanity UI components.
118 lines (94 loc) • 2.74 kB
text/typescript
import {useEffect, useRef, useState} from 'react'
import {EMPTY_ARRAY} from '../constants'
/**
* @public
*/
export type ClickOutsideListener = (event: MouseEvent) => void
/**
* @public
*/
export type ClickOutsideElements = (HTMLElement | null | (HTMLElement | null)[])[]
function _getElements(
element: HTMLElement | null,
elementsArg: ClickOutsideElements,
): HTMLElement[] {
const ret = [element]
for (const el of elementsArg) {
if (Array.isArray(el)) {
ret.push(...el)
} else {
ret.push(el)
}
}
return ret.filter(Boolean) as HTMLElement[]
}
/**
* @public
* @deprecated replaced by the new `useClickOutsideEvent` hook, instead of:
* ```tsx
* const [button, setButtonElement] = useState(null)
* useClickOutside((event) => {}, [button])
* return <button ref={setButtonElement} />
* ```
* do:
* ```tsx
* const buttonRef = useRef()
* useClickOutsideEvent((event) => {}, () => [buttonRef.current])
* return <button ref={buttonRef} />
* ```
*/
export function useClickOutside(
listener: ClickOutsideListener,
elementsArg: ClickOutsideElements = EMPTY_ARRAY,
boundaryElement?: HTMLElement | null,
): (el: HTMLElement | null) => void {
const [element, setElement] = useState<HTMLElement | null>(null)
const [elements, setElements] = useState(() => _getElements(element, elementsArg))
const elementsRef = useRef(elements)
useEffect(() => {
const prevElements = elementsRef.current
const nextElements = _getElements(element, elementsArg)
if (prevElements.length !== nextElements.length) {
setElements(nextElements)
elementsRef.current = nextElements
return
}
for (const el of prevElements) {
if (!nextElements.includes(el)) {
setElements(nextElements)
elementsRef.current = nextElements
return
}
}
for (const el of nextElements) {
if (!prevElements.includes(el)) {
setElements(nextElements)
elementsRef.current = nextElements
return
}
}
}, [element, elementsArg])
useEffect(() => {
if (!listener) return undefined
const handleWindowMouseDown = (evt: MouseEvent) => {
const target = evt.target
if (!(target instanceof Node)) {
return
}
if (boundaryElement && !boundaryElement.contains(target)) {
return
}
for (const el of elements) {
if (target === el || el.contains(target)) {
return
}
}
listener(evt)
}
window.addEventListener('mousedown', handleWindowMouseDown)
return () => {
window.removeEventListener('mousedown', handleWindowMouseDown)
}
}, [boundaryElement, listener, elements])
return setElement
}