/** * External dependencies */ import { __ } from '@wordpress/i18n'; import { BACKSPACE, DOWN, UP } from '@wordpress/keycodes'; import { createElement, Component, createRef } from '@wordpress/element'; import { Icon, search } from '@wordpress/icons'; import classnames from 'classnames'; import { isArray } from 'lodash'; import { RefObject, ChangeEvent, FocusEvent, KeyboardEvent, InputHTMLAttributes, } from 'react'; /** * Internal dependencies */ import Tags from './tags'; import { Selected, Option } from './types'; type Props = { /** * Bool to determine if tags should be rendered. */ hasTags?: boolean; /** * Help text to be appended beneath the input. */ help?: string | JSX.Element; /** * Render tags inside input, otherwise render below input. */ inlineTags?: boolean; /** * Allow the select options to be filtered by search input. */ isSearchable?: boolean; /** * ID of the main SelectControl instance. */ instanceId?: number; /** * A label to use for the main input. */ label?: string; /** * ID used for a11y in the listbox. */ listboxId?: string; /** * Function called when the input is blurred. */ onBlur?: () => void; /** * Function called when selected results change, passed result list. */ onChange: ( selected: Option[] ) => void; /** * Function called when input field is changed or focused. */ onSearch: ( query: string ) => void; /** * A placeholder for the search input. */ placeholder?: string; /** * Search query entered by user. */ query?: string | null; /** * An array of objects describing selected values. If the label of the selected * value is omitted, the Tag of that value will not be rendered inside the * search box. */ selected?: Selected; /** * Show all options on focusing, even if a query exists. */ showAllOnFocus?: boolean; /** * Control input autocomplete field, defaults: off. */ autoComplete?: string; /** * Function to execute when the control should be expanded or collapsed. */ setExpanded: ( expanded: boolean ) => void; /** * Function to execute when the search value changes. */ updateSearchOptions: ( query: string ) => void; /** * Function to execute when keyboard navigation should decrement the selected index. */ decrementSelectedIndex: () => void; /** * Function to execute when keyboard navigation should increment the selected index. */ incrementSelectedIndex: () => void; /** * Multi-select mode allows multiple options to be selected. */ multiple?: boolean; /** * Is the control currently focused. */ isFocused?: boolean; /** * ID for accessibility purposes. aria-activedescendant will be set to this value. */ activeId?: string; /** * Disable the control. */ disabled?: boolean; /** * Is the control currently expanded. This is for accessibility purposes. */ isExpanded?: boolean; /** * The type of input to use for the search field. */ searchInputType?: InputHTMLAttributes< HTMLInputElement >[ 'type' ]; /** * The aria label for the search input. */ ariaLabel?: string; /** * Class name to be added to the input. */ className?: string; /** * Show the clear button. */ showClearButton?: boolean; }; type State = { isActive: boolean; }; /** * A search control to allow user input to filter the options. */ class Control extends Component< Props, State > { input: RefObject< HTMLInputElement >; constructor( props: Props ) { super( props ); this.state = { isActive: false, }; this.input = createRef(); this.updateSearch = this.updateSearch.bind( this ); this.onFocus = this.onFocus.bind( this ); this.onBlur = this.onBlur.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); } updateSearch( onSearch: ( query: string ) => void ) { return ( event: ChangeEvent< HTMLInputElement > ) => { onSearch( event.target.value ); }; } onFocus( onSearch: ( query: string ) => void ) { const { isSearchable, setExpanded, showAllOnFocus, updateSearchOptions, } = this.props; return ( event: FocusEvent< HTMLInputElement > ) => { this.setState( { isActive: true } ); if ( isSearchable && showAllOnFocus ) { event.target.select(); updateSearchOptions( '' ); } else if ( isSearchable ) { onSearch( event.target.value ); } else { setExpanded( true ); } }; } onBlur() { const { onBlur } = this.props; if ( typeof onBlur === 'function' ) { onBlur(); } this.setState( { isActive: false } ); } onKeyDown( event: KeyboardEvent< HTMLInputElement > ) { const { decrementSelectedIndex, incrementSelectedIndex, selected, onChange, query, setExpanded, } = this.props; if ( BACKSPACE === event.keyCode && ! query && isArray( selected ) && selected.length ) { onChange( [ ...selected.slice( 0, -1 ) ] ); } if ( DOWN === event.keyCode ) { incrementSelectedIndex(); setExpanded( true ); event.preventDefault(); event.stopPropagation(); } if ( UP === event.keyCode ) { decrementSelectedIndex(); setExpanded( true ); event.preventDefault(); event.stopPropagation(); } } renderButton() { const { multiple, selected } = this.props; if ( multiple || ! isArray( selected ) || ! selected.length ) { return null; } return (
{ help }
) }