/** @format */ /** * External dependencies */ import { Button } from '@wordpress/components'; import classnames from 'classnames'; import { Component, createRef } from '@wordpress/element'; import { isEqual } from 'lodash'; import { ENTER, ESCAPE, UP, DOWN, LEFT, RIGHT } from '@wordpress/keycodes'; import PropTypes from 'prop-types'; /** * A list box that displays filtered options after search. */ class List extends Component { constructor() { super( ...arguments ); this.handleKeyDown = this.handleKeyDown.bind( this ); this.select = this.select.bind( this ); this.optionRefs = {}; this.listbox = createRef(); } componentDidUpdate( prevProps ) { const { options, selectedIndex } = this.props; // Remove old option refs to avoid memory leaks. if ( ! isEqual( options, prevProps.options ) ) { this.optionRefs = {}; } if ( selectedIndex !== prevProps.selectedIndex ) { this.scrollToOption( selectedIndex ); } } getOptionRef( index ) { if ( ! this.optionRefs.hasOwnProperty( index ) ) { this.optionRefs[ index ] = createRef(); } return this.optionRefs[ index ]; } select( option ) { const { onSelect } = this.props; if ( option.isDisabled ) { return; } onSelect( option ); } scrollToOption( index ) { const listbox = this.listbox.current; if ( listbox.scrollHeight <= listbox.clientHeight ) { return; } if ( ! this.optionRefs[ index ] ) { return; } const option = this.optionRefs[ index ].current; const scrollBottom = listbox.clientHeight + listbox.scrollTop; const elementBottom = option.offsetTop + option.offsetHeight; if ( elementBottom > scrollBottom ) { listbox.scrollTop = elementBottom - listbox.clientHeight; } else if ( option.offsetTop < listbox.scrollTop ) { listbox.scrollTop = option.offsetTop; } } handleKeyDown( event ) { const { decrementSelectedIndex, incrementSelectedIndex, options, onSearch, selectedIndex, setExpanded, } = this.props; if ( options.length === 0 ) { return; } switch ( event.keyCode ) { case UP: decrementSelectedIndex(); event.preventDefault(); event.stopPropagation(); break; case DOWN: incrementSelectedIndex(); event.preventDefault(); event.stopPropagation(); break; case ENTER: this.select( options[ selectedIndex ] ); event.preventDefault(); event.stopPropagation(); break; case LEFT: case RIGHT: setExpanded( false ); break; case ESCAPE: setExpanded( false ); onSearch( null ); return; default: return; } } toggleKeyEvents( isListening ) { const { node } = this.props; // This exists because we must capture ENTER key presses before RichText. // It seems that react fires the simulated capturing events after the // native browser event has already bubbled so we can't stopPropagation // and avoid RichText getting the event from TinyMCE, hence we must // register a native event handler. const handler = isListening ? 'addEventListener' : 'removeEventListener'; node[ handler ]( 'keydown', this.handleKeyDown, true ); } componentDidMount() { this.toggleKeyEvents( true ); } componentWillUnmount() { this.toggleKeyEvents( false ); } render() { const { instanceId, listboxId, options, selectedIndex, staticList } = this.props; const listboxClasses = classnames( 'woocommerce-select-control__listbox', { 'is-static': staticList, } ); return (