a11y_focus-loop.js

import getFocusable from '../dom/get-focusable';

/**
 * @module focusLoop
 * @description Loop through focusable els inside a container. Bound to a keydown listener usually.
 *
 * @since 1.0.0
 *
 * @param {KeyboardEvent} e         The event object from the keyDown/keyUp listener using this.
 * @param {HTMLElement}   trigger   The trigger that the user clicked to launch the element using focus loop.
 * @param {HTMLElement}   container The container the trigger focused.
 * @param {Function}      onEscape  The function to call if the esc key is used.
 *
 * @requires getFocusable
 *
 * @return {void}
 *
 * @example
 * import { focusLoop } from "@gravityforms/utils";
 *
 * const exampleContainer = document.getElementById( 'example-container' );
 * const exampleTrigger = document.getElementById( 'example-trigger' );
 *
 * closeExample = () => {
 *     // close the container.
 * }
 *
 * handleKeyEvents = ( e ) =>
 * 		focusLoop(
 * 			e,
 * 			exampleTrigger,
 * 			exampleContainer,
 * 			closeExample
 * 		);
 *
 * bindEvents = () => {
 *      exampleContainer.addEventListener( 'keydown', handleKeyEvents );
 * };
 *
 * bindEvents();
 *
 */
export default function focusLoop(
	e = {},
	trigger = null,
	container = null,
	onEscape = () => {}
) {
	if ( ! container || ! trigger ) {
		console.error(
			'You need to pass a container and trigger node to focusLoop.'
		);
		return;
	}
	// esc key, refocus the settings trigger in the editor preview for the active field
	if ( e.keyCode === 27 ) {
		trigger.focus();
		onEscape();
		return;
	}
	// not tab key, exit
	if ( e.keyCode !== 9 ) {
		return;
	}
	// get visible focusable items
	const focusable = getFocusable( container );
	// store first and last visible item
	const firstFocusableEl = focusable[ 0 ];
	const lastFocusableEl = focusable[ focusable.length - 1 ];

	// shiftkey was involved, we're going backwards, focus last el if we are leaving first
	if ( e.shiftKey ) {
		/* shift + tab */
		if ( document.activeElement === firstFocusableEl ) {
			lastFocusableEl.focus();
			e.preventDefault();
		}
		// regular tabbing direction, bring us back to first el at reaching end
	} /* tab */ else if ( document.activeElement === lastFocusableEl ) {
		firstFocusableEl.focus();
		e.preventDefault();
	}
}