initial commit

This commit is contained in:
2021-12-10 12:03:04 +00:00
commit c46c7ddbf0
3643 changed files with 582794 additions and 0 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}