Shadcn Hooks

useFullscreen

A hook to manage fullscreen state

Loading...

Installation

npx shadcn@latest add @hooks/use-fullscreen
pnpm dlx shadcn@latest add @hooks/use-fullscreen
yarn dlx shadcn@latest add @hooks/use-fullscreen
bun x shadcn@latest add @hooks/use-fullscreen

Copy and paste the following code into your project.

use-event-listener.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 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 }

use-isomorphic-layout-effect.ts
import { useEffect, useLayoutEffect } from 'react'
import { isBrowser } from '@/registry/lib/is-browser'

/**
 * Custom hook that uses either `useLayoutEffect` or `useEffect` based on the environment (client-side or server-side).
 * @param {Function} effect - The effect function to be executed.
 * @param {Array<any>} [dependencies] - An array of dependencies for the effect (optional).
 * @public
 * @see [Documentation](https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect)
 * @example
 * ```tsx
 * useIsomorphicLayoutEffect(() => {
 *   // Code to be executed during the layout phase on the client side
 * }, [dependency1, dependency2]);
 * ```
 */
export const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect

use-unmount.ts
import { useEffect } from 'react'
import { useLatest } from '@/registry/hooks/use-latest'

export function useUnmount(fn: () => void) {
  const fnRef = useLatest(fn)

  useEffect(
    () => () => {
      fnRef.current()
    },
    [],
  )
}

use-latest.ts
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
}

use-memoized-fn.ts
import { useMemo, useRef } from 'react'

type noop = (this: any, ...args: any[]) => any

type PickFunction<T extends noop> = (
  this: ThisParameterType<T>,
  ...args: Parameters<T>
) => ReturnType<T>

export function useMemoizedFn<T extends noop>(fn: T) {
  const fnRef = useRef<T>(fn)

  // why not write `fnRef.current = fn`?
  // https://github.com/alibaba/hooks/issues/728
  fnRef.current = useMemo<T>(() => fn, [fn])

  const memoizedFn = useRef<PickFunction<T>>(undefined)

  if (!memoizedFn.current) {
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args)
    }
  }

  return memoizedFn.current
}

use-fullscreen.ts
import { useRef, useState } from 'react'
import { useEffectWithTarget } from '@/registry/hooks/use-effect-with-target'
import { useEventListener } from '@/registry/hooks/use-event-listener'
import { useIsomorphicLayoutEffect } from '@/registry/hooks/use-isomorphic-layout-effect'
import { useMemoizedFn } from '@/registry/hooks/use-memoized-fn'
import { useUnmount } from '@/registry/hooks/use-unmount'
import { getTargetElement as getTargetElementUtil } from '@/registry/lib/create-effect-with-target'
import { isBrowser } from '@/registry/lib/is-browser'
import type { BasicTarget } from '@/registry/lib/create-effect-with-target'

export interface UseFullscreenOptions {
  /**
   * Automatically exit fullscreen when component is unmounted
   *
   * @default false
   */
  autoExit?: boolean
}

const eventHandlers = [
  'fullscreenchange',
  'webkitfullscreenchange',
  'webkitendfullscreen',
  'mozfullscreenchange',
  'MSFullscreenChange',
] as const

type RequestMethod =
  | 'requestFullscreen'
  | 'webkitRequestFullscreen'
  | 'webkitEnterFullscreen'
  | 'webkitEnterFullScreen'
  | 'webkitRequestFullScreen'
  | 'mozRequestFullScreen'
  | 'msRequestFullscreen'

type ExitMethod =
  | 'exitFullscreen'
  | 'webkitExitFullscreen'
  | 'webkitExitFullScreen'
  | 'webkitCancelFullScreen'
  | 'mozCancelFullScreen'
  | 'msExitFullscreen'

type FullscreenEnabledProperty =
  | 'fullScreen'
  | 'webkitIsFullScreen'
  | 'webkitDisplayingFullscreen'
  | 'mozFullScreen'
  | 'msFullscreenElement'

type FullscreenElementProperty =
  | 'fullscreenElement'
  | 'webkitFullscreenElement'
  | 'mozFullScreenElement'
  | 'msFullscreenElement'

function getTargetElement(target: BasicTarget<any>) {
  return getTargetElementUtil(target, document.documentElement)
}

function getProperties(target: BasicTarget<any>) {
  const targetElement = getTargetElement(target)

  const getRequestMethod = () => {
    const methods: RequestMethod[] = [
      'requestFullscreen',
      'webkitRequestFullscreen',
      'webkitEnterFullscreen',
      'webkitEnterFullScreen',
      'webkitRequestFullScreen',
      'mozRequestFullScreen',
      'msRequestFullscreen',
    ]

    return methods.find(
      (method) =>
        (targetElement && method in targetElement) ||
        (document && method in document),
    )
  }

  const getExitMethod = () => {
    const methods: ExitMethod[] = [
      'exitFullscreen',
      'webkitExitFullscreen',
      'webkitExitFullScreen',
      'webkitCancelFullScreen',
      'mozCancelFullScreen',
      'msExitFullscreen',
    ]

    return methods.find(
      (method) =>
        (targetElement && method in targetElement) ||
        (document && method in document),
    )
  }

  const getFullscreenEnabledProperty = () => {
    const properties: FullscreenEnabledProperty[] = [
      'fullScreen',
      'webkitIsFullScreen',
      'webkitDisplayingFullscreen',
      'mozFullScreen',
      'msFullscreenElement',
    ]

    return properties.find(
      (property) =>
        (document && property in document) ||
        (targetElement && property in targetElement),
    )
  }

  const getFullscreenElementProperty = () => {
    const properties: FullscreenElementProperty[] = [
      'fullscreenElement',
      'webkitFullscreenElement',
      'mozFullScreenElement',
      'msFullscreenElement',
    ]

    return properties.find(
      (property) =>
        (document && property in document) ||
        (targetElement && property in targetElement),
    )
  }

  return {
    requestMethod: getRequestMethod(),
    exitMethod: getExitMethod(),
    fullscreenEnabledProperty: getFullscreenEnabledProperty(),
    fullscreenElementProperty: getFullscreenElementProperty(),
  }
}

function getIsSupported(
  target: BasicTarget<any>,
  properties: ReturnType<typeof getProperties>,
) {
  const targetElement = getTargetElement(target)

  const { requestMethod, exitMethod, fullscreenEnabledProperty } = properties
  return !!(
    targetElement &&
    document &&
    requestMethod !== undefined &&
    exitMethod !== undefined &&
    fullscreenEnabledProperty !== undefined
  )
}

/**
 * Reactive Fullscreen API.
 *
 * @param target - The target element to make fullscreen. If not provided, uses document.documentElement
 * @param options - Configuration options
 */
export function useFullscreen(
  target?: BasicTarget<any>,
  options: UseFullscreenOptions = {},
) {
  const { autoExit = false } = options

  const properties = useRef<{
    requestMethod: RequestMethod | undefined
    exitMethod: ExitMethod | undefined
    fullscreenEnabledProperty: FullscreenEnabledProperty | undefined
    fullscreenElementProperty: FullscreenElementProperty | undefined
  }>({
    requestMethod: undefined,
    exitMethod: undefined,
    fullscreenEnabledProperty: undefined,
    fullscreenElementProperty: undefined,
  })
  const [isSupported, setIsSupported] = useState(() => {
    if (!isBrowser) return false

    return getIsSupported(target, getProperties(target))
  })
  const [isFullscreen, setIsFullscreen] = useState(false)

  const exit = useMemoizedFn(async () => {
    const { exitMethod } = properties.current
    if (!isSupported || !isFullscreen) return

    const element = getTargetElement(target)
    const doc = document as any

    if (exitMethod) {
      if (doc[exitMethod] != null) {
        await doc[exitMethod]()
      } else if (element && (element as any)[exitMethod] != null) {
        // Fallback for Safari iOS
        await (element as any)[exitMethod]()
      }
    }

    setIsFullscreen(false)
  })

  useEffectWithTarget(
    () => {
      if (!isBrowser) {
        return
      }

      properties.current = getProperties(target)

      setIsSupported(getIsSupported(target, properties.current))
    },
    [],
    target,
  )

  const isCurrentElementFullScreen = useMemoizedFn((): boolean => {
    const { fullscreenElementProperty } = properties.current
    if (!fullscreenElementProperty || !isBrowser) return false

    const element = getTargetElement(target)

    return document[fullscreenElementProperty as keyof Document] === element
  })

  const isElementFullScreen = useMemoizedFn((): boolean => {
    const { fullscreenEnabledProperty } = properties.current
    if (!fullscreenEnabledProperty || !isBrowser) return false

    const element = getTargetElement(target)
    const doc = document as any

    if (doc[fullscreenEnabledProperty] != null) {
      return Boolean(doc[fullscreenEnabledProperty])
    }

    // Fallback for WebKit and iOS Safari browsers
    if (element && (element as any)[fullscreenEnabledProperty] != null) {
      return Boolean((element as any)[fullscreenEnabledProperty])
    }

    return false
  })

  const enter = useMemoizedFn(async () => {
    const { requestMethod } = properties.current
    if (!isSupported || isFullscreen) return

    if (isElementFullScreen()) {
      await exit()
    }

    const element = getTargetElement(target)
    if (requestMethod && element && (element as any)[requestMethod] != null) {
      await (element as any)[requestMethod]()
      setIsFullscreen(true)
    }
  })

  const toggle = useMemoizedFn(async () => {
    await (isFullscreen ? exit() : enter())
  })

  const handlerCallback = useMemoizedFn(() => {
    const isElementFullScreenValue = isElementFullScreen()

    if (
      !isElementFullScreenValue ||
      (isElementFullScreenValue && isCurrentElementFullScreen())
    ) {
      setIsFullscreen(isElementFullScreenValue)
    }
  })

  const listenerOptions = { capture: false, passive: true }
  // Listen to fullscreen change events on document
  useEventListener(eventHandlers as any, handlerCallback, {
    target: () => document,
    ...listenerOptions,
  })

  // Listen to fullscreen change events on target element
  useEventListener(eventHandlers as any, handlerCallback, {
    target: () => getTargetElement(target),
    ...listenerOptions,
  })

  // Check initial state on mount
  useIsomorphicLayoutEffect(() => {
    if (isBrowser) {
      handlerCallback()
    }
  }, [])

  useUnmount(() => {
    if (autoExit) exit()
  })

  return {
    isSupported,
    isFullscreen,
    enter,
    exit,
    toggle,
  }
}

export type UseFullscreenReturn = ReturnType<typeof useFullscreen>

API

export interface UseFullscreenOptions {
  /**
   * Automatically exit fullscreen when component is unmounted
   *
   * @default false
   */
  autoExit?: boolean
}

/**
 * A hook to manage fullscreen state
 * @param target - The target element to make fullscreen. If not provided, uses document.documentElement
 * @param options - Configuration options
 */
export function useFullscreen(
  target?: BasicTarget<HTMLElement | Element>,
  options?: UseFullscreenOptions,
): {
  isSupported: boolean
  isFullscreen: boolean
  enter: PickFunction<() => Promise<void>>
  exit: PickFunction<() => Promise<void>>
  toggle: PickFunction<() => Promise<void>>
}

Credits

Last updated on

On this page