hooks_use-focus-trap.js

import { React } from '@gravityforms/libraries';
import { setupFocusManager } from './helpers/focus-manager';
import { focusSelector, focusable, tabbable } from './helpers/tabbable';
import scopeTab from './helpers/scope-tab';
import { createAriaHider } from './helpers/aria-hider';

const { useCallback, useEffect, useRef } = React;

/**
 * @module useFocusTrap
 * @description A hook to trap focus inside a specific element so the user can't tab outside it.
 *
 * @author https://github.com/charlie-tango
 *
 * @since 1.1.3
 *
 * @param {boolean} active  Whether the element that will trap the focus is active or not.
 * @param {object}  options Options for the focus trap hook. @see https://www.npmjs.com/package/@charlietango/use-focus-trap
 *
 * @return {object} A reference to the element that will trap the focus.
 *
 * @example
 * import React from 'react'
 * import useFocusTrap from '@gravityforms/react-utils';
 *
 * const Component = () => {
 *  const ref = useFocusTrap()
 *  return (
 *   <div ref={ref}>
 *     <button>Trapped to the button</button>
 *   </div>
 *  )
 * }
 *
 * export default Component
 *
 */
function useFocusTrap( active = true, options = {} ) {
	const ref = useRef();
	const restoreAria = useRef( null );
	const {
		markForFocusLater,
		returnFocus,
		setupScopedFocus,
		teardownScopedFocus,
	} = setupFocusManager();

	const setRef = useCallback( ( node ) => {
		if ( restoreAria.current ) {
			restoreAria.current();
		}
		if ( ref.current ) {
			returnFocus();
			teardownScopedFocus();
		}
		if ( active && node ) {
			setupScopedFocus( node );
			markForFocusLater();
			const processNode = ( activeNode ) => {
				// See if we should disable aria
				restoreAria.current = ! options.disableAriaHider
					? createAriaHider( activeNode )
					: null;
				// Find the initial focus element
				let focusElement = null;
				if ( options.focusSelector ) {
					focusElement = typeof options.focusSelector === 'string' ? activeNode.querySelector( options.focusSelector ) : options.focusSelector;
				}
				if ( ! focusElement ) {
					const children = Array.from( activeNode.querySelectorAll( focusSelector ) );
					focusElement =
                        // Prefer tabbable elements, But fallback to any focusable element
                        children.find( tabbable ) ||
                            // But fallback to any focusable element
                            children.find( focusable ) ||
                            // Nothing found
                            null;
					// If everything else fails, see if the node itself can handle focus
					if ( ! focusElement && focusable( activeNode ) ) {
						focusElement = activeNode;
					}
				}
				if ( focusElement ) {
					// Set the initial focus inside the traps
					focusElement.focus();
				} else if ( process.env.RUN_MODE === 'development' ) {
					// eslint-disable-next-line no-console
					console.warn( '[useFocusTrap]: Failed to find a focusable element after activating the focus trap. Make sure to include at an element that can recieve focus. As a fallback, you can also set "tabIndex={-1}" on the focus trap node.', activeNode );
				}
			};
			// Delay processing the HTML node by a frame. This ensures focus is assigned correctly.
			setTimeout( () => {
				if ( node.ownerDocument ) {
					processNode( node );
				} else if ( process.env.RUN_MODE === 'development' ) {
					// eslint-disable-next-line no-console
					console.warn( '[useFocusTrap]: The focus trap is not part of the DOM yet, so it is unable to correctly set focus. Make sure to render the ref node.', node );
				}
			} );
			ref.current = node;
		} else {
			ref.current = null;
		}
	}, [ active, options.focusSelector, options.disableAriaHider ] );
	useEffect( () => {
		if ( ! active ) {
			return undefined;
		}
		const handleKeyDown = ( event ) => {
			if ( event.key === 'Tab' && ref.current ) {
				scopeTab( ref.current, event );
			}
		};
		document.addEventListener( 'keydown', handleKeyDown );
		return () => {
			document.removeEventListener( 'keydown', handleKeyDown );
		};
	}, [ active ] );
	return setRef;
}
export default useFocusTrap;