/** @format */ /** * External dependencies */ import { __, sprintf } from '@wordpress/i18n'; import { Component, createRef } from '@wordpress/element'; import { withInstanceId } from '@wordpress/compose'; import { findIndex, noop } from 'lodash'; import Gridicon from 'gridicons'; import PropTypes from 'prop-types'; import classnames from 'classnames'; /** * Internal dependencies */ import Autocomplete from './autocomplete'; import { product, productCategory, coupons } from './autocompleters'; import Tag from 'components/tag'; import './style.scss'; /** * A search box which autocompletes results while typing, allowing for the user to select an existing object * (product, order, customer, etc). Currently only products are supported. */ class Search extends Component { constructor( props ) { super( props ); this.state = { value: '', isActive: false, }; this.input = createRef(); this.selectResult = this.selectResult.bind( this ); this.removeResult = this.removeResult.bind( this ); this.updateSearch = this.updateSearch.bind( this ); this.onFocus = this.onFocus.bind( this ); this.onBlur = this.onBlur.bind( this ); } selectResult( value ) { const { selected, onChange } = this.props; // Check if this is already selected const isSelected = findIndex( selected, { id: value.id } ); if ( -1 === isSelected ) { this.setState( { value: '' } ); onChange( [ ...selected, value ] ); } } removeResult( id ) { return () => { const { selected, onChange } = this.props; const i = findIndex( selected, { id } ); onChange( [ ...selected.slice( 0, i ), ...selected.slice( i + 1 ) ] ); }; } updateSearch( onChange ) { return event => { const value = event.target.value || ''; this.setState( { value } ); onChange( event ); }; } getAutocompleter() { switch ( this.props.type ) { case 'products': return product; case 'product_cats': return productCategory; case 'coupons': return coupons; default: return {}; } } renderTags() { const { selected } = this.props; return selected.length ? (
{ selected.map( ( item, i ) => { const screenReaderLabel = sprintf( __( '%1$s (%2$s of %3$s)', 'wc-admin' ), item.label, i + 1, selected.length ); return ( ); } ) }
) : null; } onFocus() { this.setState( { isActive: true } ); } onBlur() { this.setState( { isActive: false } ); } render() { const autocompleter = this.getAutocompleter(); const { placeholder, inlineTags, selected, instanceId } = this.props; const { value = '', isActive } = this.state; const aria = { 'aria-labelledby': this.props[ 'aria-labelledby' ], 'aria-label': this.props[ 'aria-label' ], }; return (
{ ( { listBoxId, activeId, onChange } ) => // Disable reason: The div below visually simulates an input field. Its // child input is the actual input and responds accordingly to all keyboard // events, but click events need to be passed onto the child input. There // is no appropriate aria role for describing this situation, which is only // for the benefit of sighted users. /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */ inlineTags ? (
{ this.input.current.focus(); } } > { this.renderTags() } { __( 'Move backward for selected items' ) }
) : ( ) }
{ ! inlineTags && this.renderTags() }
); /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */ } } Search.propTypes = { /** * Function called when selected results change, passed result list. */ onChange: PropTypes.func, /** * The object type to be used in searching. */ type: PropTypes.oneOf( [ 'products', 'product_cats', 'orders', 'customers', 'coupons' ] ) .isRequired, /** * A placeholder for the search input. */ placeholder: PropTypes.string, /** * An array of objects describing selected values. */ selected: PropTypes.arrayOf( PropTypes.shape( { id: PropTypes.number.isRequired, label: PropTypes.string.isRequired, } ) ), /** * Render tags inside input, otherwise render below input */ inlineTags: PropTypes.bool, }; Search.defaultProps = { onChange: noop, selected: [], inlineTags: false, }; export default withInstanceId( Search );