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,
	};
}