useHover
A hook to check if the mouse is hovering over an element
Loading...
Installation
npx shadcn@latest add @hooks/use-hoverpnpm dlx shadcn@latest add @hooks/use-hoveryarn dlx shadcn@latest add @hooks/use-hoverbun x shadcn@latest add @hooks/use-hoverCopy and paste the following code into your project.
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 { 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 noop = (...p: any) => void
export type Target = BasicTarget<HTMLElement | Element | Window | Document>
interface Options<T extends Target = Target> {
target?: T
capture?: boolean
once?: boolean
passive?: boolean
enable?: boolean
}
function useEventListener<K extends keyof HTMLElementEventMap>(
eventName: K,
handler: (ev: HTMLElementEventMap[K]) => void,
options?: Options<HTMLElement>,
): void
function useEventListener<K extends keyof ElementEventMap>(
eventName: K,
handler: (ev: ElementEventMap[K]) => void,
options?: Options<Element>,
): void
function useEventListener<K extends keyof DocumentEventMap>(
eventName: K,
handler: (ev: DocumentEventMap[K]) => void,
options?: Options<Document>,
): void
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (ev: WindowEventMap[K]) => void,
options?: Options<Window>,
): void
function useEventListener(
eventName: string | string[],
handler: (event: Event) => void,
options?: Options<Window>,
): void
function useEventListener(
eventName: string | string[],
handler: noop,
options: Options,
): void
function useEventListener(
eventName: string | string[],
handler: noop,
options: Options = {},
) {
const { enable = true } = options
const handlerRef = useLatest(handler)
useEffectWithTarget(
() => {
if (!enable) {
return
}
const targetElement = getTargetElement(options.target, window)
if (!targetElement?.addEventListener) {
return
}
const eventListener = (event: Event) => {
return handlerRef.current(event)
}
const eventNameArray = Array.isArray(eventName) ? eventName : [eventName]
eventNameArray.forEach((event) => {
targetElement.addEventListener(event, eventListener, {
capture: options.capture,
once: options.once,
passive: options.passive,
})
})
return () => {
eventNameArray.forEach((event) => {
targetElement.removeEventListener(event, eventListener, {
capture: options.capture,
})
})
}
},
[eventName, options.capture, options.once, options.passive, enable],
options.target,
)
}
export { useEventListener }import { useMemo } from 'react'
import useToggle from '@/registry/hooks/use-toggle'
export function useBoolean(defaultValue: boolean = false) {
const [state, actions] = useToggle(defaultValue)
return [
state,
useMemo(
() => ({
set: (value: boolean) => actions.set(value),
setTrue: () => actions.set(true),
setFalse: () => actions.set(false),
toggle: () => actions.toggle(),
}),
[actions],
),
] as const
}import { useBoolean } from '@/registry/hooks/use-boolean'
import { useEventListener } from '@/registry/hooks/use-event-listener'
import type { BasicTarget } from '@/registry/lib/create-effect-with-target'
export interface Options {
onEnter?: () => void
onLeave?: () => void
onChange?: (isHovering: boolean) => void
}
export function useHover(target: BasicTarget, options?: Options): boolean {
const { onEnter, onLeave, onChange } = options || {}
const [state, { setTrue, setFalse }] = useBoolean(false)
useEventListener(
'mouseenter',
() => {
onEnter?.()
setTrue()
onChange?.(true)
},
{
target,
},
)
useEventListener(
'mouseleave',
() => {
onLeave?.()
setFalse()
onChange?.(false)
},
{
target,
},
)
return state
}API
/**
* A hook to check if the mouse is hovering over an element
* @param target - The target to check if the mouse is hovering over
* @param options - The options for the hook
* @param options.onEnter - The function to call when the mouse enters the target
* @param options.onLeave - The function to call when the mouse leaves the target
* @param options.onChange - The function to call when the mouse enters or leaves the target
* @returns The boolean value indicating if the mouse is hovering over the target
*/
export function useHover(target: BasicTarget, options?: Options): booleanCredits
Last updated on