@gravityforms/components
Version: 
UI components for use in Gravity Forms development. Both React and vanilla js flavors.
183 lines (174 loc) • 5.45 kB
JavaScript
import { ARROW_UP, ARROW_DOWN, HOME, PAGE_UP, END, PAGE_DOWN, SPACE, ENTER, ESCAPE } from '../../../utils/keymap';
/**
 * @function useDropdownKeyDown
 * @description Adds the keydown handlers to props.
 *
 * @since 4.5.0
 *
 * @param {object}   props    The props to add handlers to.
 * @param {Function} useStore The useStore hook.
 *
 * @return {object} The modified props.
 */
const useDropdownKeyDown = ( props = {}, useStore = () => {} ) => {
	const {
		hasSearch = false,
		multi = false,
		open = () => {},
		resetAndClose = () => {},
		selectItem = () => {},
		selectMultipleItems = () => {},
	} = props;
	const activeItem = useStore( ( state ) => state.activeItem );
	const selectedItem = useStore( ( state ) => state.selectedItem );
	const listItems = useStore( ( state ) => state.listItems );
	const dropdownOpen = useStore( ( state ) => state.open );
	const setActiveItem = useStore( ( state ) => state.setActiveItem );
	const setSelectedItem = useStore( ( state ) => state.setSelectedItem );
	const baseElRef = useStore( ( state ) => state.baseElRef );
	const popoverRef = useStore( ( state ) => state.popoverRef );
	const triggerRef = useStore( ( state ) => state.triggerRef );
	const allowedKeys = [
		ARROW_UP,
		ARROW_DOWN,
		HOME,
		PAGE_UP,
		END,
		PAGE_DOWN,
		ENTER,
	];
	if ( ! hasSearch ) {
		allowedKeys.push( SPACE );
	}
	if ( multi ) {
		allowedKeys.push( 'a' );
	}
	/**
	 * @function handleEscKeyDown
	 * @description Handles the keydown event for the escape key.
	 *
	 * @since 4.5.0
	 *
	 * @param {Event} event The event object.
	 */
	const handleEscKeyDown = ( event ) => {
		if ( event.key !== ESCAPE ) {
			return;
		}
		// Close dropdown if escape is pressed.
		resetAndClose( event );
		triggerRef?.current?.focus();
	};
	/**
	 * @function handleTriggerKeyDown
	 * @description Handles the keydown event for the dropdown trigger.
	 *
	 * @since 4.5.0
	 *
	 * @param {Event} event The event object.
	 */
	const handleTriggerKeyDown = ( event ) => {
		// Return early if not arrow up or down key.
		if ( ! [ ARROW_UP, ARROW_DOWN ].includes( event.key ) ) {
			return;
		}
		if ( ! dropdownOpen ) {
			open();
		}
		// Current target is not the base element, focus on the base element.
		baseElRef?.current?.focus();
	};
	/**
	 * @function handleListKeyDown
	 * @description Handles the keydown event for the dropdown list.
	 *
	 * @since 4.5.0
	 *
	 * @param {Event} event The event object.
	 */
	const handleListKeyDown = ( event ) => {
		// If default prevented from earlier propagation, return early.
		if ( event.defaultPrevented ) {
			return;
		}
		// If not one of the allowed keys, return early.
		if ( ! allowedKeys.includes( event.key ) ) {
			return;
		}
		// Prevent default to prevent multiple calls of this event handler.
		if ( ! multi || event.key !== 'a' ) {
			event.preventDefault();
		}
		// Handle if arrow up or arrow down is pressed.
		if ( [ ARROW_UP, ARROW_DOWN ].includes( event.key ) ) {
			const activeIndex = listItems.ids.indexOf( activeItem.id );
			if ( activeIndex === -1 ) {
				return;
			}
			let nextIndex;
			if ( event.key === ARROW_UP ) {
				nextIndex = activeIndex - 1;
				if ( nextIndex === -1 ) {
					nextIndex = listItems.ids.length - 1;
				}
			}
			if ( event.key === ARROW_DOWN ) {
				nextIndex = activeIndex + 1;
				if ( nextIndex === listItems.ids.length ) {
					nextIndex = 0;
				}
			}
			const nextItem = listItems.items[ nextIndex ];
			setActiveItem( nextItem );
			if ( nextItem.component !== 'search' && event.shiftKey ) {
				selectItem( nextItem )( event );
			}
			popoverRef?.current?.querySelector( `#${ nextItem.id }` )?.focus();
		}
		// Set active item to first item if home or page up is pressed.
		if ( [ HOME, PAGE_UP ].includes( event.key ) ) {
			const nextIndex = 0;
			const nextItem = listItems.items[ nextIndex ];
			if ( event.shiftKey && event.ctrlKey ) {
				const activeIndex = listItems.ids.indexOf( activeItem.id );
				const items = listItems.items.slice( nextIndex, activeIndex + 1 );
				// Select items in reverse order as we are jumping to the top.
				selectMultipleItems( items.reverse() );
			}
			setActiveItem( nextItem );
		}
		// Set active item to last item if end or page down is pressed.
		if ( [ END, PAGE_DOWN ].includes( event.key ) ) {
			const nextIndex = hasSearch ? listItems.ids.length - 2 : listItems.ids.length - 1;
			const nextItem = listItems.items[ nextIndex ];
			if ( event.shiftKey && event.ctrlKey ) {
				const activeIndex = listItems.ids.indexOf( activeItem.id );
				const items = listItems.items.slice( activeIndex, nextIndex + 1 );
				selectMultipleItems( items );
			}
			setActiveItem( nextItem );
		}
		if ( event.key === 'a' && event.ctrlKey ) {
			// If selected items are already all items, deselect all items.
			if ( selectedItem.length === listItems.items.length - ( hasSearch ? 1 : 0 ) ) {
				setSelectedItem( [] );
				return;
			}
			// Select all items except search.
			const items = listItems.items.filter( ( item ) => item.component !== 'search' );
			selectMultipleItems( items );
		}
		// Set active item to selected item.
		// If search is active, only listen for ENTER key, as space is allowed in the input.
		if ( [ ENTER, ...( hasSearch ? [] : [ SPACE ] ) ].includes( event.key ) ) {
			selectItem( activeItem )( event );
		}
	};
	return {
		...props,
		handleEscKeyDown,
		handleTriggerKeyDown,
		handleListKeyDown,
	};
};
export default useDropdownKeyDown;