woocommerce/plugins/woocommerce-admin/client/components/search/index.js

225 lines
6.1 KiB
JavaScript
Raw Normal View History

/** @format */
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
2018-10-10 03:58:34 +00:00
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';
2018-10-10 03:58:34 +00:00
import classnames from 'classnames';
/**
* Internal dependencies
*/
import Autocomplete from './autocomplete';
2018-09-17 02:26:34 +00:00
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: '',
2018-10-10 03:58:34 +00:00
isActive: false,
};
2018-10-10 03:58:34 +00:00
this.input = createRef();
this.selectResult = this.selectResult.bind( this );
this.removeResult = this.removeResult.bind( this );
this.updateSearch = this.updateSearch.bind( this );
2018-10-10 03:58:34 +00:00
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;
2018-09-17 02:26:34 +00:00
case 'coupons':
return coupons;
default:
return {};
}
}
2018-10-10 03:58:34 +00:00
renderTags() {
const { selected } = this.props;
return selected.length ? (
<div className="woocommerce-search__token-list">
{ selected.map( ( item, i ) => {
const screenReaderLabel = sprintf(
__( '%1$s (%2$s of %3$s)', 'wc-admin' ),
item.label,
i + 1,
selected.length
);
return (
<Tag
key={ item.id }
id={ item.id }
label={ item.label }
remove={ this.removeResult }
removeLabel={ __( 'Remove product', 'wc-admin' ) }
screenReaderLabel={ screenReaderLabel }
/>
);
} ) }
</div>
) : null;
}
onFocus() {
this.setState( { isActive: true } );
}
onBlur() {
this.setState( { isActive: false } );
}
render() {
const autocompleter = this.getAutocompleter();
2018-10-10 03:58:34 +00:00
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 (
<div className="woocommerce-search">
<Gridicon className="woocommerce-search__icon" icon="search" size={ 18 } />
<Autocomplete completer={ autocompleter } onSelect={ this.selectResult }>
2018-10-10 03:58:34 +00:00
{ ( { 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 ? (
<div
className={ classnames( 'woocommerce-search__inline-container', {
'is-active': isActive,
} ) }
onClick={ () => {
this.input.current.focus();
} }
>
{ this.renderTags() }
<input
ref={ this.input }
type="text"
size={
( ( value.length === 0 && placeholder && placeholder.length ) ||
value.length ) + 1
}
value={ value }
placeholder={ ( selected.length === 0 && placeholder ) || '' }
className="woocommerce-search__inline-input"
onChange={ this.updateSearch( onChange ) }
aria-owns={ listBoxId }
aria-activedescendant={ activeId }
onFocus={ this.onFocus }
onBlur={ this.onBlur }
aria-describedby={
selected.length ? `search-inline-input-${ instanceId }` : null
}
{ ...aria }
/>
2018-10-10 03:58:34 +00:00
<span id={ `search-inline-input-${ instanceId }` } className="screen-reader-text">
{ __( 'Move backward for selected items' ) }
</span>
</div>
) : (
<input
type="search"
value={ value }
placeholder={ placeholder }
className="woocommerce-search__input"
onChange={ this.updateSearch( onChange ) }
aria-owns={ listBoxId }
aria-activedescendant={ activeId }
{ ...aria }
/>
)
}
</Autocomplete>
{ ! inlineTags && this.renderTags() }
</div>
);
2018-10-10 03:58:34 +00:00
/* 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.
*/
2018-09-17 02:26:34 +00:00
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,
} )
),
2018-10-10 03:58:34 +00:00
/**
* Render tags inside input, otherwise render below input
*/
inlineTags: PropTypes.bool,
};
Search.defaultProps = {
onChange: noop,
selected: [],
2018-10-10 03:58:34 +00:00
inlineTags: false,
};
2018-10-10 03:58:34 +00:00
export default withInstanceId( Search );