LCOV - code coverage report
Current view: top level - dashql-app/src/ui/foundations - focus.ts Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 90 90
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react'
       2              : import { iterateFocusableElements } from '@primer/behaviors/utils'
       3              : import { focusTrap, focusZone, FocusZoneSettings } from '@primer/behaviors';
       4              : 
       5              : export type UseOpenAndCloseFocusArgs = {
       6              :     initialFocusRef?: React.RefObject<HTMLElement | null>
       7              :     containerRef: React.RefObject<HTMLElement | null>
       8              :     returnFocusRef?: React.RefObject<HTMLElement | null>
       9              :     preventFocusOnOpen?: boolean
      10              : }
      11              : 
      12            1 : export function useOpenAndCloseFocus(args: UseOpenAndCloseFocusArgs): void {
      13            1 :     React.useEffect(() => {
      14            1 :         if (args.preventFocusOnOpen) {
      15            1 :             return;
      16              :         }
      17            1 :         const returnRef = args.returnFocusRef?.current
      18            1 :         if (args.initialFocusRef && args.initialFocusRef.current) {
      19            1 :             args.initialFocusRef.current.focus()
      20            1 :         } else if (args.containerRef.current) {
      21            1 :             const firstItem = iterateFocusableElements(args.containerRef.current).next().value
      22            1 :             firstItem?.focus()
      23              :         }
      24            1 :         return function() {
      25            1 :             returnRef?.focus()
      26            1 :         }
      27            1 :     }, [args.initialFocusRef, args.returnFocusRef, args.containerRef, args.preventFocusOnOpen])
      28            1 : }
      29              : 
      30              : 
      31              : export interface FocusTrapHookArgs {
      32              :     /// Ref that will be used for the trapping container. If not provided, one will
      33              :     /// be created by this hook and returned.
      34              :     containerRef?: React.RefObject<HTMLElement>
      35              :     /// Ref for the element that should receive focus when the focus trap is first enabled. If
      36              :     /// not provided, one will be created by this hook and returned. Its use is optional.
      37              :     initialFocusRef?: React.RefObject<HTMLElement>
      38              :     /// Set to true to disable the focus trap and clean up listeners. Can be re-enabled at
      39              :     /// any time.
      40              :     disabled?: boolean
      41              :     /// If true, when this focus trap is cleaned up, restore focus to the element that had
      42              :     /// focus immediately before the focus trap was enabled. (Default: false)
      43              :     restoreFocusOnCleanUp?: boolean
      44              :     /// If passed, when this focus trap is cleaned up, restore focus to this element instead
      45              :     /// of element with focus immediately before the focus trap was enabled.
      46              :     returnFocusRef?: React.RefObject<HTMLElement>
      47              : }
      48              : 
      49              : /// Hook used to trap focus inside a container.
      50              : /// Returns a ref that can be added to the container that should trap focus.
      51            1 : export function useFocusTrap(
      52            1 :     args?: FocusTrapHookArgs,
      53            1 :     dependencies: React.DependencyList = [],
      54            1 : ): { containerRef: React.RefObject<HTMLElement | null>; initialFocusRef: React.RefObject<HTMLElement | null> } {
      55            1 :     const altContainerRef = React.useRef<HTMLElement>(null);
      56            1 :     const altInitialFocusRef = React.useRef<HTMLElement>(null);
      57              : 
      58            1 :     const containerRef = args?.containerRef ?? altContainerRef;
      59            1 :     const initialFocusRef = args?.initialFocusRef ?? altInitialFocusRef;
      60            1 :     const disabled = args?.disabled;
      61            1 :     const abortController = React.useRef<AbortController | null>(null);
      62            1 :     const previousFocusedElement = React.useRef<Element | null>(null);
      63              : 
      64              :     // If we are enabling a focus trap and haven't already stored the previously focused element
      65              :     // go ahead an do that so we can restore later when the trap is disabled.
      66            1 :     if (!previousFocusedElement.current && !args?.disabled) {
      67            1 :         previousFocusedElement.current = document.activeElement
      68              :     }
      69              :     // This function removes the event listeners that enable the focus trap and restores focus
      70              :     // to the previously-focused element (if necessary).
      71            1 :     function disableTrap() {
      72            1 :         abortController.current?.abort()
      73            1 :         if (args?.returnFocusRef && args.returnFocusRef.current instanceof HTMLElement) {
      74            1 :             args.returnFocusRef.current.focus()
      75            1 :         } else if (args?.restoreFocusOnCleanUp && previousFocusedElement.current instanceof HTMLElement) {
      76            1 :             previousFocusedElement.current.focus()
      77            1 :             previousFocusedElement.current = null
      78            1 :         }
      79            1 :     }
      80              : 
      81            1 :     React.useEffect(
      82            1 :         () => {
      83            1 :             if (containerRef.current instanceof HTMLElement) {
      84            1 :                 if (!disabled) {
      85            1 :                     abortController.current = focusTrap(containerRef.current, initialFocusRef.current ?? undefined) ?? null;
      86            1 :                     return () => {
      87            1 :                         disableTrap()
      88            1 :                     }
      89            1 :                 } else {
      90            1 :                     disableTrap()
      91              :                 }
      92            1 :             }
      93            1 :         },
      94            1 :         [containerRef, initialFocusRef, disabled, ...dependencies],
      95            1 :     )
      96            1 :     return { containerRef, initialFocusRef };
      97            1 : }
      98              : 
      99              : 
     100              : export interface FocusZoneHookArgs extends Omit<FocusZoneSettings, 'activeDescendantControl'> {
     101              :     /// Optional ref for the container that holds all elements participating in arrow key focus.
     102              :     /// If one is not passed, we will create one for you and return it from the hook.
     103              :     containerRef?: React.RefObject<HTMLElement>
     104              :     /// If using the "active descendant" focus pattern, pass `true` or a ref to the controlling
     105              :     /// element. If a ref object is not passed, we will create one for you.
     106              :     activeDescendantFocus?: boolean | React.RefObject<HTMLElement>
     107              :     /// Set to true to disable the focus zone and clean up listeners. Can be re-enabled at any time.
     108              :     disabled?: boolean
     109              : }
     110              : 
     111            1 : export function useFocusZone(
     112            1 :     settings: FocusZoneHookArgs = {},
     113            1 :     dependencies: React.DependencyList = [],
     114            1 : ): { containerRef: React.RefObject<HTMLElement | null>; activeDescendantControlRef: React.RefObject<HTMLElement | null> } {
     115            1 :     const altContainerRef = React.useRef<HTMLElement>(null);
     116            1 :     const altActiveDescFocusRef = React.useRef<HTMLElement>(null);
     117              : 
     118            1 :     const containerRef = settings.containerRef ?? altContainerRef;
     119            1 :     const useActiveDescendant = !!settings.activeDescendantFocus
     120              :     const passedActiveDescendantRef =
     121            1 :         typeof settings.activeDescendantFocus === 'boolean' || !settings.activeDescendantFocus
     122            1 :             ? undefined
     123            1 :             : settings.activeDescendantFocus
     124            1 :     const activeDescendantControlRef = passedActiveDescendantRef ?? altActiveDescFocusRef;
     125            1 :     const disabled = settings.disabled
     126            1 :     const abortController = React.useRef<AbortController | null>(null);
     127              : 
     128            1 :     React.useEffect(
     129            1 :         () => {
     130            1 :             if (
     131            1 :                 containerRef.current instanceof HTMLElement &&
     132            1 :                 (!useActiveDescendant || activeDescendantControlRef.current instanceof HTMLElement)
     133            1 :             ) {
     134            1 :                 if (!disabled) {
     135            1 :                     const vanillaSettings: FocusZoneSettings = {
     136            1 :                         ...settings,
     137            1 :                         activeDescendantControl: activeDescendantControlRef.current ?? undefined,
     138            1 :                     }
     139            1 :                     abortController.current = focusZone(containerRef.current, vanillaSettings)
     140            1 :                     return () => {
     141            1 :                         abortController.current?.abort()
     142            1 :                     }
     143            1 :                 } else {
     144            1 :                     abortController.current?.abort()
     145              :                 }
     146            1 :             }
     147            1 :         },
     148            1 :         [disabled, ...dependencies],
     149            1 :     )
     150            1 :     return { containerRef, activeDescendantControlRef }
     151            1 : }
        

Generated by: LCOV version 2.0-1