@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;