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;