use-click-away

PreviousNext

A hook to click away from an element

Docs
hookshook

Preview

Loading preview…
registry/hooks/use-click-away.ts
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,
  )
}

Installation

npx shadcn@latest add @hooks/use-click-away

Usage

import { UseClickAway } from "@/hooks/use-click-away"
const value = UseClickAway()