95 lines
		
	
	
	
		
			2.7 KiB
			
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			95 lines
		
	
	
	
		
			2.7 KiB
			
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
// Copyright 2021 Signal Messenger, LLC
 | 
						|
// SPDX-License-Identifier: AGPL-3.0-only
 | 
						|
 | 
						|
import * as React from 'react';
 | 
						|
 | 
						|
type CallbackType = (toFocus: HTMLElement | null | undefined) => void;
 | 
						|
 | 
						|
// Restore focus on teardown
 | 
						|
export const useRestoreFocus = (): Array<CallbackType> => {
 | 
						|
  const toFocusRef = React.useRef<HTMLElement | null>(null);
 | 
						|
  const lastFocusedRef = React.useRef<HTMLElement | null>(null);
 | 
						|
 | 
						|
  // We need to use a callback here because refs aren't necessarily populated on first
 | 
						|
  //   render. For example, ModalHost makes a top-level parent div first, and then renders
 | 
						|
  //   into it. And the children you pass it don't have access to that root div.
 | 
						|
  const setFocusRef = React.useCallback(
 | 
						|
    (toFocus: HTMLElement | null | undefined) => {
 | 
						|
      if (!toFocus) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      // We only want to do this once.
 | 
						|
      if (toFocusRef.current) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      toFocusRef.current = toFocus;
 | 
						|
 | 
						|
      // Remember last-focused element, focus this new target element.
 | 
						|
      lastFocusedRef.current = document.activeElement as HTMLElement;
 | 
						|
      toFocus.focus();
 | 
						|
    },
 | 
						|
    []
 | 
						|
  );
 | 
						|
 | 
						|
  React.useEffect(() => {
 | 
						|
    return () => {
 | 
						|
      // On unmount, returned focus to element focused before we set the focus
 | 
						|
      setTimeout(() => {
 | 
						|
        if (lastFocusedRef.current && lastFocusedRef.current.focus) {
 | 
						|
          lastFocusedRef.current.focus();
 | 
						|
        }
 | 
						|
      });
 | 
						|
    };
 | 
						|
  }, []);
 | 
						|
 | 
						|
  return [setFocusRef];
 | 
						|
};
 | 
						|
 | 
						|
// Panels are initially rendered outside the DOM, and then added to it. We need to
 | 
						|
//   delay our attempts to set focus.
 | 
						|
// Just like the above hook, but with a debounce.
 | 
						|
export const useDelayedRestoreFocus = (): Array<CallbackType> => {
 | 
						|
  const toFocusRef = React.useRef<HTMLElement | null>(null);
 | 
						|
  const lastFocusedRef = React.useRef<HTMLElement | null>(null);
 | 
						|
 | 
						|
  const setFocusRef = React.useCallback(
 | 
						|
    (toFocus: HTMLElement | null | undefined) => {
 | 
						|
      function setFocus() {
 | 
						|
        if (!toFocus) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        // We only want to do this once.
 | 
						|
        if (toFocusRef.current) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        toFocusRef.current = toFocus;
 | 
						|
 | 
						|
        // Remember last-focused element, focus this new target element.
 | 
						|
        lastFocusedRef.current = document.activeElement as HTMLElement;
 | 
						|
        toFocus.focus();
 | 
						|
      }
 | 
						|
 | 
						|
      const timeout = setTimeout(setFocus, 250);
 | 
						|
 | 
						|
      return () => {
 | 
						|
        clearTimeout(timeout);
 | 
						|
      };
 | 
						|
    },
 | 
						|
    []
 | 
						|
  );
 | 
						|
 | 
						|
  React.useEffect(() => {
 | 
						|
    return () => {
 | 
						|
      // On unmount, returned focus to element focused before we set the focus
 | 
						|
      setTimeout(() => {
 | 
						|
        if (lastFocusedRef.current && lastFocusedRef.current.focus) {
 | 
						|
          lastFocusedRef.current.focus();
 | 
						|
        }
 | 
						|
      });
 | 
						|
    };
 | 
						|
  }, []);
 | 
						|
 | 
						|
  return [setFocusRef];
 | 
						|
};
 |