Shadcn Hooks

useActiveElement

A hook to track the currently focused element in the document

Loading...

Installation

npx shadcn@latest add @shadcnhooks/use-active-element
pnpm dlx shadcn@latest add @shadcnhooks/use-active-element
yarn dlx shadcn@latest add @shadcnhooks/use-active-element
bun x shadcn@latest add @shadcnhooks/use-active-element

Copy and paste the following code into your project.

use-active-element.ts
import { useCallback, useSyncExternalStore } from 'react'

export interface UseActiveElementOptions {
  /**
   * Whether to resolve the deepest focused node inside open shadow roots.
   * @default true
   */
  deep?: boolean
  /**
   * Whether to re-check active element when DOM nodes are removed.
   * @default false
   */
  triggerOnRemoval?: boolean
}

function resolveActiveElement(deep: boolean): Element | null {
  if (typeof document === 'undefined') {
    return null
  }

  let activeElement: Element | null = document.activeElement

  if (!deep) {
    return activeElement
  }

  while (activeElement?.shadowRoot?.activeElement) {
    activeElement = activeElement.shadowRoot.activeElement
  }

  return activeElement
}

export function useActiveElement<T extends Element = HTMLElement>(
  options: UseActiveElementOptions = {},
): T | null {
  const { deep = true, triggerOnRemoval = false } = options

  const subscribe = useCallback(
    (onStoreChange: () => void) => {
      if (typeof window === 'undefined') {
        return () => {}
      }

      const onFocus = () => {
        onStoreChange()
      }
      const onBlur = (event: FocusEvent) => {
        if (event.relatedTarget !== null) {
          return
        }
        onStoreChange()
      }

      window.addEventListener('focus', onFocus, true)
      window.addEventListener('blur', onBlur, true)

      const observer =
        triggerOnRemoval && typeof MutationObserver !== 'undefined'
          ? new MutationObserver(() => {
              onStoreChange()
            })
          : null

      observer?.observe(document, {
        childList: true,
        subtree: true,
      })

      return () => {
        window.removeEventListener('focus', onFocus, true)
        window.removeEventListener('blur', onBlur, true)
        observer?.disconnect()
      }
    },
    [triggerOnRemoval],
  )

  const getSnapshot = useCallback(() => {
    return resolveActiveElement(deep) as T | null
  }, [deep])

  const getServerSnapshot = useCallback(() => {
    return null as T | null
  }, [])

  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
}

API

export interface UseActiveElementOptions {
  /**
   * Search active element deeply inside shadow dom.
   * @default true
   */
  deep?: boolean
  /**
   * Track active element when it's removed from the DOM
   * Using a MutationObserver under the hood
   * @default false
   */
  triggerOnRemoval?: boolean
}

export function useActiveElement<T extends Element = HTMLElement>(
  options?: UseActiveElementOptions,
): T | null

Credits

Last updated on

On this page