hooks_helpers_focus-manager.js
import { findTabbableDescendants } from './tabbable';
/**
* Global map of trap ID to element that should receive focus when the trap closes.
* Used by setReturnFocusTarget, markForFocusLater, and returnFocus.
* @type {Map<string|symbol, HTMLElement>}
*/
const returnFocusTargetsByTrapId = new Map();
/**
* @function setReturnFocusTarget
* @description Sets the element to return focus to when the focus trap with the given ID closes.
* Call from outside the trap to override the default; pass null for element to clear.
*
* @since 5.2.1
*
* @param {HTMLElement|null} element Element to focus when the trap closes, or null to clear.
* @param {string|symbol} id Focus trap ID (must match the focusTrapId passed to useFocusTrap).
*
* @return {void}
*/
export function setReturnFocusTarget( element, id ) {
if ( id === undefined || id === null ) {
return;
}
if ( element !== undefined && element !== null ) {
returnFocusTargetsByTrapId.set( id, element );
} else {
returnFocusTargetsByTrapId.delete( id );
}
}
/**
* @function getReturnFocusTarget
* @description Returns the element currently stored as the return-focus target for the given trap ID.
*
* @since 5.2.1
*
* @param {string|symbol} id Focus trap ID.
*
* @return {HTMLElement|undefined} The element to return focus to, or undefined if none is set.
*/
export function getReturnFocusTarget( id ) {
return returnFocusTargetsByTrapId.get( id );
}
/**
* @ignore
*/
export function setupFocusManager() {
let focusElement = null;
let needToFocus = false;
/**
* @ignore
*/
function handleBlur() {
needToFocus = true;
}
/**
* @ignore
*/
function handleFocus() {
if ( ! needToFocus ) {
return;
}
needToFocus = false;
if ( ! focusElement ) {
return;
}
if ( focusElement.contains( document.activeElement ) ) {
return;
}
const el = findTabbableDescendants( focusElement )[ 0 ] || focusElement;
el.focus();
}
/**
* @ignore
*
* @param {string|symbol} id Trap ID. Stores the current activeElement in the map as the return target.
*/
function markForFocusLater( id ) {
returnFocusTargetsByTrapId.set( id, document.activeElement );
}
/**
* @ignore
*
* @param {string|symbol} id Trap ID. Focuses the element stored for this ID and removes it from the map.
*/
function returnFocus( id ) {
if ( ! returnFocusTargetsByTrapId.has( id ) ) {
return;
}
const toFocus = returnFocusTargetsByTrapId.get( id );
returnFocusTargetsByTrapId.delete( id );
try {
if ( toFocus ) {
setTimeout( () => toFocus.focus() );
}
} catch ( e ) {
// eslint-disable-next-line no-console
console.warn( [
'You tried to return focus to',
toFocus,
'but it is not in the DOM anymore',
].join( ' ' ) );
}
}
/**
* @ignore
*/
function setupScopedFocus( element ) {
focusElement = element;
focusElement.addEventListener( 'focusout', handleBlur, false );
focusElement.addEventListener( 'focusin', handleFocus, true );
}
/**
* @ignore
*/
function teardownScopedFocus() {
if ( ! focusElement ) {
return;
}
focusElement.removeEventListener( 'focusout', handleBlur );
focusElement.removeEventListener( 'focusin', handleFocus );
focusElement = null;
}
return {
markForFocusLater,
returnFocus,
setupScopedFocus,
teardownScopedFocus,
};
}