2019-12-03 13:39:11 +00:00
|
|
|
/**
|
|
|
|
* External dependencies
|
|
|
|
*/
|
|
|
|
import PropTypes from 'prop-types';
|
2019-12-05 13:58:44 +00:00
|
|
|
import { useCallback, useRef } from '@wordpress/element';
|
2019-12-03 13:39:11 +00:00
|
|
|
import classNames from 'classnames';
|
|
|
|
import Downshift from 'downshift';
|
2019-12-05 13:58:44 +00:00
|
|
|
import { __, sprintf } from '@wordpress/i18n';
|
2019-12-03 13:39:11 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal dependencies
|
|
|
|
*/
|
|
|
|
import DropdownSelectorInput from './input';
|
|
|
|
import DropdownSelectorInputWrapper from './input-wrapper';
|
|
|
|
import DropdownSelectorMenu from './menu';
|
|
|
|
import DropdownSelectorSelectedChip from './selected-chip';
|
2019-12-05 13:58:44 +00:00
|
|
|
import DropdownSelectorSelectedValue from './selected-value';
|
2019-12-03 13:39:11 +00:00
|
|
|
import './style.scss';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component used to show an input box with a dropdown with suggestions.
|
2020-09-20 23:54:08 +00:00
|
|
|
*
|
|
|
|
* @param {Object} props Incoming props for the component.
|
|
|
|
* @param {string} props.attributeLabel Label for the attributes.
|
|
|
|
* @param {string} props.className CSS class used.
|
|
|
|
* @param {Array} props.checked Which items are checked.
|
|
|
|
* @param {string} props.inputLabel Label used for the input.
|
|
|
|
* @param {boolean} props.isDisabled Whether the input is disabled or not.
|
|
|
|
* @param {boolean} props.isLoading Whether the input is loading.
|
|
|
|
* @param {boolean} props.multiple Whether multi-select is allowed.
|
|
|
|
* @param {function():any} props.onChange Function to be called when onChange event fires.
|
|
|
|
* @param {Array} props.options The option values to show in the select.
|
2019-12-03 13:39:11 +00:00
|
|
|
*/
|
|
|
|
const DropdownSelector = ( {
|
|
|
|
attributeLabel = '',
|
|
|
|
className,
|
|
|
|
checked = [],
|
|
|
|
inputLabel = '',
|
|
|
|
isDisabled = false,
|
|
|
|
isLoading = false,
|
2019-12-05 13:58:44 +00:00
|
|
|
multiple = false,
|
2019-12-03 13:39:11 +00:00
|
|
|
onChange = () => {},
|
|
|
|
options = [],
|
|
|
|
} ) => {
|
|
|
|
const inputRef = useRef( null );
|
|
|
|
|
2020-06-17 09:53:42 +00:00
|
|
|
const classes = classNames(
|
|
|
|
className,
|
|
|
|
'wc-block-dropdown-selector',
|
|
|
|
'wc-block-components-dropdown-selector',
|
|
|
|
{
|
|
|
|
'is-disabled': isDisabled,
|
|
|
|
'is-loading': isLoading,
|
|
|
|
}
|
|
|
|
);
|
2019-12-03 13:39:11 +00:00
|
|
|
|
2019-12-05 13:58:44 +00:00
|
|
|
/**
|
|
|
|
* State reducer for the downshift component.
|
|
|
|
* See: https://github.com/downshift-js/downshift#statereducer
|
|
|
|
*/
|
|
|
|
const stateReducer = useCallback(
|
|
|
|
( state, changes ) => {
|
|
|
|
switch ( changes.type ) {
|
|
|
|
case Downshift.stateChangeTypes.keyDownEnter:
|
|
|
|
case Downshift.stateChangeTypes.clickItem:
|
|
|
|
return {
|
|
|
|
...changes,
|
|
|
|
highlightedIndex: state.highlightedIndex,
|
|
|
|
isOpen: multiple,
|
|
|
|
inputValue: '',
|
|
|
|
};
|
|
|
|
case Downshift.stateChangeTypes.blurInput:
|
|
|
|
case Downshift.stateChangeTypes.mouseUp:
|
|
|
|
return {
|
|
|
|
...changes,
|
|
|
|
inputValue: state.inputValue,
|
|
|
|
};
|
|
|
|
default:
|
|
|
|
return changes;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[ multiple ]
|
|
|
|
);
|
2019-12-03 13:39:11 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Downshift
|
|
|
|
onChange={ onChange }
|
|
|
|
selectedItem={ null }
|
|
|
|
stateReducer={ stateReducer }
|
|
|
|
>
|
|
|
|
{ ( {
|
|
|
|
getInputProps,
|
|
|
|
getItemProps,
|
|
|
|
getLabelProps,
|
|
|
|
getMenuProps,
|
|
|
|
highlightedIndex,
|
|
|
|
inputValue,
|
|
|
|
isOpen,
|
|
|
|
openMenu,
|
|
|
|
} ) => (
|
2019-12-05 13:58:44 +00:00
|
|
|
<div
|
|
|
|
className={ classNames( classes, {
|
|
|
|
'is-multiple': multiple,
|
|
|
|
'is-single': ! multiple,
|
2020-07-10 09:09:49 +00:00
|
|
|
'has-checked': checked.length > 0,
|
|
|
|
'is-open': isOpen,
|
2019-12-05 13:58:44 +00:00
|
|
|
} ) }
|
|
|
|
>
|
2019-12-03 13:39:11 +00:00
|
|
|
{ /* eslint-disable-next-line jsx-a11y/label-has-for */ }
|
|
|
|
<label
|
|
|
|
{ ...getLabelProps( {
|
|
|
|
className: 'screen-reader-text',
|
|
|
|
} ) }
|
|
|
|
>
|
|
|
|
{ inputLabel }
|
|
|
|
</label>
|
|
|
|
<DropdownSelectorInputWrapper
|
|
|
|
isOpen={ isOpen }
|
2019-12-05 13:58:44 +00:00
|
|
|
onClick={ () => inputRef.current.focus() }
|
2019-12-03 13:39:11 +00:00
|
|
|
>
|
|
|
|
{ checked.map( ( value ) => {
|
|
|
|
const option = options.find(
|
|
|
|
( o ) => o.value === value
|
|
|
|
);
|
2019-12-05 13:58:44 +00:00
|
|
|
const onRemoveItem = ( val ) => {
|
|
|
|
onChange( val );
|
|
|
|
inputRef.current.focus();
|
|
|
|
};
|
|
|
|
return multiple ? (
|
2019-12-03 13:39:11 +00:00
|
|
|
<DropdownSelectorSelectedChip
|
|
|
|
key={ value }
|
2019-12-05 13:58:44 +00:00
|
|
|
onRemoveItem={ onRemoveItem }
|
|
|
|
option={ option }
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<DropdownSelectorSelectedValue
|
|
|
|
key={ value }
|
|
|
|
onClick={ () => inputRef.current.focus() }
|
|
|
|
onRemoveItem={ onRemoveItem }
|
2019-12-03 13:39:11 +00:00
|
|
|
option={ option }
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
} ) }
|
|
|
|
<DropdownSelectorInput
|
|
|
|
checked={ checked }
|
|
|
|
getInputProps={ getInputProps }
|
|
|
|
inputRef={ inputRef }
|
|
|
|
isDisabled={ isDisabled }
|
|
|
|
onFocus={ openMenu }
|
|
|
|
onRemoveItem={ ( val ) => {
|
|
|
|
onChange( val );
|
2019-12-05 13:58:44 +00:00
|
|
|
inputRef.current.focus();
|
2019-12-03 13:39:11 +00:00
|
|
|
} }
|
2019-12-05 13:58:44 +00:00
|
|
|
placeholder={
|
|
|
|
checked.length > 0 && multiple
|
|
|
|
? null
|
|
|
|
: sprintf(
|
|
|
|
// Translators: %s attribute name.
|
|
|
|
__(
|
|
|
|
'Any %s',
|
|
|
|
'woo-gutenberg-products-block'
|
|
|
|
),
|
|
|
|
attributeLabel
|
|
|
|
)
|
|
|
|
}
|
2020-01-30 10:04:39 +00:00
|
|
|
tabIndex={
|
|
|
|
// When it's a single selector and there is one element selected,
|
|
|
|
// we make the input non-focusable with the keyboard because it's
|
|
|
|
// visually hidden. The input is still rendered, though, because it
|
|
|
|
// must be possible to focus it when pressing the select value chip.
|
|
|
|
! multiple && checked.length > 0 ? '-1' : '0'
|
|
|
|
}
|
2019-12-03 13:39:11 +00:00
|
|
|
value={ inputValue }
|
|
|
|
/>
|
|
|
|
</DropdownSelectorInputWrapper>
|
|
|
|
{ isOpen && ! isDisabled && (
|
|
|
|
<DropdownSelectorMenu
|
|
|
|
checked={ checked }
|
|
|
|
getItemProps={ getItemProps }
|
|
|
|
getMenuProps={ getMenuProps }
|
|
|
|
highlightedIndex={ highlightedIndex }
|
|
|
|
options={ options.filter(
|
|
|
|
( option ) =>
|
|
|
|
! inputValue ||
|
|
|
|
option.value.startsWith( inputValue )
|
|
|
|
) }
|
|
|
|
/>
|
|
|
|
) }
|
|
|
|
</div>
|
|
|
|
) }
|
|
|
|
</Downshift>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
DropdownSelector.propTypes = {
|
|
|
|
attributeLabel: PropTypes.string,
|
|
|
|
checked: PropTypes.array,
|
|
|
|
className: PropTypes.string,
|
|
|
|
inputLabel: PropTypes.string,
|
|
|
|
isDisabled: PropTypes.bool,
|
|
|
|
isLoading: PropTypes.bool,
|
|
|
|
limit: PropTypes.number,
|
|
|
|
onChange: PropTypes.func,
|
|
|
|
options: PropTypes.arrayOf(
|
|
|
|
PropTypes.shape( {
|
|
|
|
label: PropTypes.node.isRequired,
|
|
|
|
value: PropTypes.string.isRequired,
|
|
|
|
} )
|
|
|
|
),
|
|
|
|
};
|
|
|
|
|
|
|
|
export default DropdownSelector;
|