useClickAway
A hook to click away from an element
Loading...
Installation
npx shadcn@latest add @hooks/use-click-awaypnpm dlx shadcn@latest add @hooks/use-click-awayyarn dlx shadcn@latest add @hooks/use-click-awaybun x shadcn@latest add @hooks/use-click-awayCopy and paste the following code into your project.
import { useRef } from 'react'
import { useIsomorphicLayoutEffect } from '@/registry/hooks/use-isomorphic-layout-effect'
export function useLatest<T>(value: T) {
const ref = useRef(value)
useIsomorphicLayoutEffect(() => {
ref.current = value
})
return ref
}import { isBrowser, isEqual, isFunction } from 'es-toolkit'
import { useRef } from 'react'
import { useUnmount } from '@/registry/hooks/use-unmount'
import type {
DependencyList,
EffectCallback,
RefObject,
useEffect,
useLayoutEffect,
} from 'react'
type TargetValue<T> = T | undefined | null
type TargetType = HTMLElement | Element | Window | Document
export type BasicTarget<T extends TargetType = Element> =
| (() => TargetValue<T>)
| TargetValue<T>
| RefObject<TargetValue<T>>
export function getTargetElement<T extends TargetType>(
target: BasicTarget<T>,
defaultElement?: T,
) {
if (!isBrowser) {
return undefined
}
if (!target) {
return defaultElement
}
let targetElement: TargetValue<T>
if (isFunction(target)) {
targetElement = target()
} else if ('current' in target) {
targetElement = target.current
} else {
targetElement = target
}
return targetElement
}
export function createEffectWithTarget(
useEffectType: typeof useEffect | typeof useLayoutEffect,
) {
/**
*
* @param effect
* @param deps
* @param target target should compare ref.current vs ref.current, dom vs dom, ()=>dom vs ()=>dom
*/
const useEffectWithTarget = (
effect: EffectCallback,
deps: DependencyList,
target: BasicTarget<any> | BasicTarget<any>[],
) => {
const hasInitRef = useRef(false)
const lastElementRef = useRef<(Element | null)[]>([])
const lastDepsRef = useRef<DependencyList>([])
const unLoadRef = useRef<any>(undefined)
useEffectType(() => {
const targets = Array.isArray(target) ? target : [target]
const els = targets.map((item) => getTargetElement(item))
// init run
if (!hasInitRef.current) {
hasInitRef.current = true
lastElementRef.current = els
lastDepsRef.current = deps
unLoadRef.current = effect()
return
}
if (
els.length !== lastElementRef.current.length ||
!isEqual(lastElementRef.current, els) ||
!isEqual(lastDepsRef.current, deps)
) {
unLoadRef.current?.()
lastElementRef.current = els
lastDepsRef.current = deps
unLoadRef.current = effect()
}
})
useUnmount(() => {
unLoadRef.current?.()
// for react-refresh
hasInitRef.current = false
})
}
return useEffectWithTarget
}import { useEffect } from 'react'
import { createEffectWithTarget } from '@/registry/lib/create-effect-with-target'
export const useEffectWithTarget = createEffectWithTarget(useEffect)import { useEffectWithTarget } from '@/registry/hooks/use-effect-with-target'
import { useLatest } from '@/registry/hooks/use-latest'
import { getTargetElement } from '@/registry/lib/create-effect-with-target'
import type { BasicTarget } from '@/registry/lib/create-effect-with-target'
type DocumentEventKey = keyof DocumentEventMap
declare type TargetValue<T> = T | undefined | null
const checkIfAllInShadow = (targets: BasicTarget[]) => {
return targets.every((item) => {
const targetElement = getTargetElement(item)
if (!targetElement) {
return false
}
if (targetElement.getRootNode() instanceof ShadowRoot) {
return true
}
return false
})
}
const getShadow = (node: TargetValue<Element>) => {
if (!node) {
return document
}
return node.getRootNode()
}
const getDocumentOrShadow = (
target: BasicTarget | BasicTarget[],
): Document | Node => {
if (!target || !document.getRootNode) {
return document
}
const targets = Array.isArray(target) ? target : [target]
if (checkIfAllInShadow(targets)) {
return getShadow(getTargetElement(targets[0]))
}
return document
}
export function useClickAway<T extends Event = Event>(
onClickAway: (event: T) => void,
target: BasicTarget | BasicTarget[],
eventName: DocumentEventKey | DocumentEventKey[] = 'click',
) {
const onClickAwayRef = useLatest(onClickAway)
useEffectWithTarget(
() => {
const handler = (event: any) => {
const targets = Array.isArray(target) ? target : [target]
if (
targets.some((item) => {
const targetElement = getTargetElement(item)
return !targetElement || targetElement.contains(event.target)
})
) {
return
}
onClickAwayRef.current(event)
}
const documentOrShadow = getDocumentOrShadow(target)
const eventNames = Array.isArray(eventName) ? eventName : [eventName]
eventNames.forEach((event) =>
documentOrShadow.addEventListener(event, handler),
)
return () => {
eventNames.forEach((event) =>
documentOrShadow.removeEventListener(event, handler),
)
}
},
Array.isArray(eventName) ? eventName : [eventName],
target,
)
}API
type TargetValue<T> = T | undefined | null
type TargetType = HTMLElement | Element | Window | Document
type BasicTarget<T extends TargetType = Element> =
| (() => TargetValue<T>)
| TargetValue<T>
| RefObject<TargetValue<T>>
type DocumentEventKey = keyof DocumentEventMap
/**
* A hook to click away from an element
* @param onClickAway - The function to call when the user clicks away from the target
* @param target - The target to click away from
* @param eventName - The event to listen for
/**
* A hook to click away from an element
* @param onClickAway - The function to call when the user clicks away from the target
* @param target - The target to click away from
* @param eventName - The event to listen for
*/
*/
export function useClickAway<T extends Event = Event>(
onClickAway: (event: T) => void,
target: BasicTarget | BasicTarget[],
eventName?: DocumentEventKey | DocumentEventKey[],
): voidCredits
Last updated on