initial commit
This commit is contained in:
@ -0,0 +1,212 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import { useCallback, useRef } from '@wordpress/element';
|
||||
import classNames from 'classnames';
|
||||
import Downshift from 'downshift';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import DropdownSelectorInput from './input';
|
||||
import DropdownSelectorInputWrapper from './input-wrapper';
|
||||
import DropdownSelectorMenu from './menu';
|
||||
import DropdownSelectorSelectedChip from './selected-chip';
|
||||
import DropdownSelectorSelectedValue from './selected-value';
|
||||
import './style.scss';
|
||||
|
||||
/**
|
||||
* Component used to show an input box with a dropdown with suggestions.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
const DropdownSelector = ( {
|
||||
attributeLabel = '',
|
||||
className,
|
||||
checked = [],
|
||||
inputLabel = '',
|
||||
isDisabled = false,
|
||||
isLoading = false,
|
||||
multiple = false,
|
||||
onChange = () => {},
|
||||
options = [],
|
||||
} ) => {
|
||||
const inputRef = useRef( null );
|
||||
|
||||
const classes = classNames(
|
||||
className,
|
||||
'wc-block-dropdown-selector',
|
||||
'wc-block-components-dropdown-selector',
|
||||
{
|
||||
'is-disabled': isDisabled,
|
||||
'is-loading': isLoading,
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 ]
|
||||
);
|
||||
|
||||
return (
|
||||
<Downshift
|
||||
onChange={ onChange }
|
||||
selectedItem={ null }
|
||||
stateReducer={ stateReducer }
|
||||
>
|
||||
{ ( {
|
||||
getInputProps,
|
||||
getItemProps,
|
||||
getLabelProps,
|
||||
getMenuProps,
|
||||
highlightedIndex,
|
||||
inputValue,
|
||||
isOpen,
|
||||
openMenu,
|
||||
} ) => (
|
||||
<div
|
||||
className={ classNames( classes, {
|
||||
'is-multiple': multiple,
|
||||
'is-single': ! multiple,
|
||||
'has-checked': checked.length > 0,
|
||||
'is-open': isOpen,
|
||||
} ) }
|
||||
>
|
||||
{ /* eslint-disable-next-line jsx-a11y/label-has-for */ }
|
||||
<label
|
||||
{ ...getLabelProps( {
|
||||
className: 'screen-reader-text',
|
||||
} ) }
|
||||
>
|
||||
{ inputLabel }
|
||||
</label>
|
||||
<DropdownSelectorInputWrapper
|
||||
isOpen={ isOpen }
|
||||
onClick={ () => inputRef.current.focus() }
|
||||
>
|
||||
{ checked.map( ( value ) => {
|
||||
const option = options.find(
|
||||
( o ) => o.value === value
|
||||
);
|
||||
const onRemoveItem = ( val ) => {
|
||||
onChange( val );
|
||||
inputRef.current.focus();
|
||||
};
|
||||
return multiple ? (
|
||||
<DropdownSelectorSelectedChip
|
||||
key={ value }
|
||||
onRemoveItem={ onRemoveItem }
|
||||
option={ option }
|
||||
/>
|
||||
) : (
|
||||
<DropdownSelectorSelectedValue
|
||||
key={ value }
|
||||
onClick={ () => inputRef.current.focus() }
|
||||
onRemoveItem={ onRemoveItem }
|
||||
option={ option }
|
||||
/>
|
||||
);
|
||||
} ) }
|
||||
<DropdownSelectorInput
|
||||
checked={ checked }
|
||||
getInputProps={ getInputProps }
|
||||
inputRef={ inputRef }
|
||||
isDisabled={ isDisabled }
|
||||
onFocus={ openMenu }
|
||||
onRemoveItem={ ( val ) => {
|
||||
onChange( val );
|
||||
inputRef.current.focus();
|
||||
} }
|
||||
placeholder={
|
||||
checked.length > 0 && multiple
|
||||
? null
|
||||
: sprintf(
|
||||
/* translators: %s attribute name. */
|
||||
__(
|
||||
'Any %s',
|
||||
'woocommerce'
|
||||
),
|
||||
attributeLabel
|
||||
)
|
||||
}
|
||||
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'
|
||||
}
|
||||
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;
|
@ -0,0 +1,13 @@
|
||||
const DropdownSelectorInputWrapper = ( { children, onClick } ) => {
|
||||
return (
|
||||
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
|
||||
<div
|
||||
className="wc-block-dropdown-selector__input-wrapper wc-block-components-dropdown-selector__input-wrapper"
|
||||
onClick={ onClick }
|
||||
>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownSelectorInputWrapper;
|
@ -0,0 +1,36 @@
|
||||
const DropdownSelectorInput = ( {
|
||||
checked,
|
||||
getInputProps,
|
||||
inputRef,
|
||||
isDisabled,
|
||||
onFocus,
|
||||
onRemoveItem,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
value,
|
||||
} ) => {
|
||||
return (
|
||||
<input
|
||||
{ ...getInputProps( {
|
||||
ref: inputRef,
|
||||
className:
|
||||
'wc-block-dropdown-selector__input wc-block-components-dropdown-selector__input',
|
||||
disabled: isDisabled,
|
||||
onFocus,
|
||||
onKeyDown( e ) {
|
||||
if (
|
||||
e.key === 'Backspace' &&
|
||||
! value &&
|
||||
checked.length > 0
|
||||
) {
|
||||
onRemoveItem( checked[ checked.length - 1 ] );
|
||||
}
|
||||
},
|
||||
placeholder,
|
||||
tabIndex,
|
||||
} ) }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownSelectorInput;
|
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const DropdownSelectorMenu = ( {
|
||||
checked,
|
||||
getItemProps,
|
||||
getMenuProps,
|
||||
highlightedIndex,
|
||||
options,
|
||||
} ) => {
|
||||
return (
|
||||
<ul
|
||||
{ ...getMenuProps( {
|
||||
className:
|
||||
'wc-block-dropdown-selector__list wc-block-components-dropdown-selector__list',
|
||||
} ) }
|
||||
>
|
||||
{ options.map( ( option, index ) => {
|
||||
const selected = checked.includes( option.value );
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<li
|
||||
{ ...getItemProps( {
|
||||
key: option.value,
|
||||
className: classNames(
|
||||
'wc-block-dropdown-selector__list-item',
|
||||
'wc-block-components-dropdown-selector__list-item',
|
||||
{
|
||||
'is-selected': selected,
|
||||
'is-highlighted':
|
||||
highlightedIndex === index,
|
||||
}
|
||||
),
|
||||
index,
|
||||
item: option.value,
|
||||
'aria-label': selected
|
||||
? sprintf(
|
||||
/* translators: %s is referring to the filter option being removed. */
|
||||
__(
|
||||
'Remove %s filter',
|
||||
'woocommerce'
|
||||
),
|
||||
option.name
|
||||
)
|
||||
: null,
|
||||
} ) }
|
||||
>
|
||||
{ option.label }
|
||||
</li>
|
||||
);
|
||||
} ) }
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownSelectorMenu;
|
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { RemovableChip } from '@woocommerce/base-components/chip';
|
||||
|
||||
const DropdownSelectorSelectedChip = ( { onRemoveItem, option } ) => {
|
||||
return (
|
||||
<RemovableChip
|
||||
className="wc-block-dropdown-selector__selected-chip wc-block-components-dropdown-selector__selected-chip"
|
||||
removeOnAnyClick={ true }
|
||||
onRemove={ () => {
|
||||
onRemoveItem( option.value );
|
||||
} }
|
||||
ariaLabel={ sprintf(
|
||||
/* translators: %s is referring to the filter option being removed. */
|
||||
__( 'Remove %s filter', 'woocommerce' ),
|
||||
option.name
|
||||
) }
|
||||
text={ option.label }
|
||||
radius="large"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownSelectorSelectedChip;
|
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useEffect, useRef } from '@wordpress/element';
|
||||
import { Icon, noAlt } from '@woocommerce/icons';
|
||||
|
||||
const DropdownSelectorSelectedValue = ( { onClick, onRemoveItem, option } ) => {
|
||||
const labelRef = useRef( null );
|
||||
|
||||
useEffect( () => {
|
||||
labelRef.current.focus();
|
||||
}, [ labelRef ] );
|
||||
|
||||
return (
|
||||
<div className="wc-block-dropdown-selector__selected-value wc-block-components-dropdown-selector__selected-value">
|
||||
<button
|
||||
ref={ labelRef }
|
||||
className="wc-block-dropdown-selector__selected-value__label wc-block-components-dropdown-selector__selected-value__label"
|
||||
onClick={ ( e ) => {
|
||||
e.stopPropagation();
|
||||
onClick( option.value );
|
||||
} }
|
||||
aria-label={ sprintf(
|
||||
/* translators: %s attribute value used in the filter. For example: yellow, green, small, large. */
|
||||
__(
|
||||
'Replace current %s filter',
|
||||
'woocommerce'
|
||||
),
|
||||
option.name
|
||||
) }
|
||||
>
|
||||
{ option.label }
|
||||
</button>
|
||||
<button
|
||||
className="wc-block-dropdown-selector__selected-value__remove wc-block-components-dropdown-selector__selected-value__remove"
|
||||
onClick={ () => {
|
||||
onRemoveItem( option.value );
|
||||
} }
|
||||
onKeyDown={ ( e ) => {
|
||||
if ( e.key === 'Backspace' || e.key === 'Delete' ) {
|
||||
onRemoveItem( option.value );
|
||||
}
|
||||
} }
|
||||
aria-label={ sprintf(
|
||||
/* translators: %s attribute value used in the filter. For example: yellow, green, small, large. */
|
||||
__( 'Remove %s filter', 'woocommerce' ),
|
||||
option.name
|
||||
) }
|
||||
>
|
||||
<Icon srcElement={ noAlt } size={ 16 } />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownSelectorSelectedValue;
|
@ -0,0 +1,176 @@
|
||||
// 18px is the minimum input field line-height and 14px is the font-size of
|
||||
// the drop down selector elements.
|
||||
|
||||
$dropdown-selector-line-height: math.div(18, 14);
|
||||
|
||||
.wc-block-components-dropdown-selector {
|
||||
max-width: 300px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wc-block-components-dropdown-selector__input-wrapper {
|
||||
background: #fff;
|
||||
border: 1px solid $input-border-gray;
|
||||
color: $input-text-active;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
cursor: text;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 2px $gap-smaller;
|
||||
|
||||
.is-disabled & {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
.is-multiple.has-checked > & {
|
||||
padding: 2px $gap-smallest;
|
||||
}
|
||||
|
||||
.is-open > & {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-dropdown-selector__input {
|
||||
@include font-size(small);
|
||||
line-height: $dropdown-selector-line-height;
|
||||
margin: em($gap-small*0.25) 0;
|
||||
min-width: 0;
|
||||
padding: em($gap-smallest * 0.75) 0 em($gap-smallest * 0.75);
|
||||
|
||||
.is-single & {
|
||||
width: 100%;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.is-single.has-checked.is-open & {
|
||||
margin-bottom: 1.5px;
|
||||
margin-top: 1.5px;
|
||||
}
|
||||
|
||||
.is-single.has-checked:not(.is-open) & {
|
||||
@include visually-hidden();
|
||||
// Fixes an issue in Firefox that `flex: wrap` in the container was making
|
||||
// this element to still occupy one line.
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.is-multiple & {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Visually hide the input
|
||||
.is-single .wc-block-components-dropdown-selector__input:first-child,
|
||||
.is-multiple .wc-block-components-dropdown-selector__input {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-dropdown-selector {
|
||||
// Reset <button> styles
|
||||
.wc-block-components-dropdown-selector__selected-value__label,
|
||||
.wc-block-components-dropdown-selector__selected-value__remove {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
text-transform: initial;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background-color: transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-dropdown-selector__selected-value {
|
||||
@include font-size(small);
|
||||
align-items: center;
|
||||
color: $gray-700;
|
||||
display: inline-flex;
|
||||
margin: em($gap-small*0.25) 0;
|
||||
padding: em($gap-smallest * 0.75) 0 em($gap-smallest * 0.75);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wc-block-components-dropdown-selector__selected-value__label {
|
||||
flex-grow: 1;
|
||||
line-height: $dropdown-selector-line-height;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.wc-block-components-dropdown-selector__selected-value__remove {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
padding: 0 0 0 0.3em;
|
||||
|
||||
> svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-dropdown-selector__selected-chip {
|
||||
@include font-size(small);
|
||||
margin-top: em($gap-small*0.25);
|
||||
margin-bottom: em($gap-small*0.25);
|
||||
line-height: $dropdown-selector-line-height;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-dropdown-selector__list {
|
||||
background-color: #fff;
|
||||
margin: -1px 0 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 1;
|
||||
|
||||
&:not(:empty) {
|
||||
border: 1px solid #9f9f9f;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-dropdown-selector__list-item {
|
||||
@include font-size(small);
|
||||
color: $gray-700;
|
||||
cursor: default;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 $gap-smallest;
|
||||
|
||||
&.is-selected {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.is-highlighted,
|
||||
&:active {
|
||||
background-color: #00669e;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user