Switch from Select to Combobox for Country and State Inputs (https://github.com/woocommerce/woocommerce-blocks/pull/4369)

* Add combobox control

* Implement in country and state

* mobile styling

* styling across themes

* Remove validated select component

* Use focus-within

* Update tests

* Use @wordpress/compose

* Move field clearing to effect hook

* Patch combobox component

PR https://github.com/WordPress/gutenberg/pull/33928

* patch package after install

* update package

* Prevent autofill handling impacting manual input

* Add todo

* combo requires option to be selected
This commit is contained in:
Mike Jolley 2021-08-12 17:30:42 +01:00 committed by GitHub
parent 946e05d70b
commit a40893ae3a
14 changed files with 787 additions and 357 deletions

View File

@ -112,6 +112,18 @@ const AddressForm = ( {
);
}, [ currentFields, fieldConfig, values.country ] );
// Clear values for hidden fields.
useEffect( () => {
addressFormFields.forEach( ( field ) => {
if ( field.hidden && values[ field.key ] ) {
onChange( {
...values,
[ field.key ]: '',
} );
}
} );
}, [ addressFormFields, onChange, values ] );
useEffect( () => {
if ( type === 'shipping' ) {
validateShippingCountry(
@ -161,8 +173,6 @@ const AddressForm = ( {
...values,
country: newValue,
state: '',
city: '',
postcode: '',
} )
}
errorId={

View File

@ -21,7 +21,7 @@ const renderInCheckoutProvider = ( ui, options = {} ) => {
// Countries used in testing addresses must be in the wcSettings global.
// See: tests/js/setup-globals.js
const primaryAddress = {
country: 'United Kingdom (UK)',
country: 'United Kingdom',
countryKey: 'GB',
city: 'London',
state: 'Greater London',
@ -46,25 +46,22 @@ const cityRegExp = /city/i;
const stateRegExp = /county|province|state/i;
const postalCodeRegExp = /postal code|postcode|zip/i;
const inputAddress = ( {
const inputAddress = async ( {
country = null,
city = null,
state = null,
postcode = null,
} ) => {
if ( country ) {
const countryButton = screen.getByRole( 'button', {
name: countryRegExp,
} );
userEvent.click( countryButton );
userEvent.click( screen.getByRole( 'option', { name: country } ) );
const countryInput = screen.getByLabelText( countryRegExp );
userEvent.type( countryInput, country + '{arrowdown}{enter}' );
}
if ( city ) {
const cityInput = screen.getByLabelText( cityRegExp );
userEvent.type( cityInput, city );
}
if ( state ) {
const stateButton = screen.queryByRole( 'button', {
const stateButton = screen.queryByRole( 'combobox', {
name: stateRegExp,
} );
// State input might be a select or a text input.
@ -162,17 +159,11 @@ describe( 'AddressForm Component', () => {
inputAddress( secondaryAddress );
// Only update `country` to verify other values are reset.
inputAddress( { country: primaryAddress.country } );
expect( screen.getByLabelText( cityRegExp ).value ).toBe( '' );
expect( screen.getByLabelText( stateRegExp ).value ).toBe( '' );
expect( screen.getByLabelText( postalCodeRegExp ).value ).toBe( '' );
// Repeat the test with an address which has a select for the state.
inputAddress( tertiaryAddress );
inputAddress( { country: primaryAddress.country } );
expect( screen.getByLabelText( cityRegExp ).value ).toBe( '' );
expect( screen.getByLabelText( stateRegExp ).value ).toBe( '' );
expect( screen.getByLabelText( postalCodeRegExp ).value ).toBe( '' );
} );
} );

View File

@ -0,0 +1,150 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import { __ } from '@wordpress/i18n';
import { useEffect, useRef } from '@wordpress/element';
import { withInstanceId } from '@wordpress/compose';
import { ComboboxControl } from 'wordpress-components';
import {
ValidationInputError,
useValidationContext,
} from '@woocommerce/base-context';
import { isObject } from '@woocommerce/types';
/**
* Internal dependencies
*/
import './style.scss';
export interface ComboboxControlOption {
label: string;
value: string;
}
/**
* Wrapper for the WordPress ComboboxControl which supports validation.
*/
const Combobox = ( {
id,
className,
label,
onChange,
options,
value,
required = false,
errorMessage = __(
'Please select a value.',
'woo-gutenberg-products-block'
),
errorId: incomingErrorId,
instanceId = '0',
autoComplete = 'off',
}: {
id: string;
className: string;
label: string;
onChange: ( filterValue: string ) => void;
options: ComboboxControlOption[];
value: string;
required: boolean;
errorMessage: string;
errorId: string;
instanceId: string;
autoComplete: string;
} ): JSX.Element => {
const {
getValidationError,
setValidationErrors,
clearValidationError,
} = useValidationContext();
const controlRef = useRef< HTMLDivElement >( null );
const controlId = id || 'control-' + instanceId;
const errorId = incomingErrorId || controlId;
const error = ( getValidationError( errorId ) || {
message: '',
hidden: false,
} ) as {
message: string;
hidden: boolean;
};
useEffect( () => {
if ( ! required || value ) {
clearValidationError( errorId );
} else {
setValidationErrors( {
[ errorId ]: {
message: errorMessage,
hidden: true,
},
} );
}
return () => {
clearValidationError( errorId );
};
}, [
clearValidationError,
value,
errorId,
errorMessage,
required,
setValidationErrors,
] );
// @todo Remove patch for ComboboxControl once https://github.com/WordPress/gutenberg/pull/33928 is released
return (
<div
id={ controlId }
className={ classnames( 'wc-block-components-combobox', className, {
'is-active': value,
'has-error': error.message && ! error.hidden,
} ) }
ref={ controlRef }
>
<ComboboxControl
className={ 'wc-block-components-combobox-control' }
label={ label }
onChange={ onChange }
onFilterValueChange={ ( filterValue: string ) => {
if ( filterValue.length ) {
// If we have a value and the combobox is not focussed, this could be from browser autofill.
const activeElement = isObject( controlRef.current )
? controlRef.current.ownerDocument.activeElement
: undefined;
if (
activeElement &&
isObject( controlRef.current ) &&
controlRef.current.contains( activeElement )
) {
return;
}
// Try to match.
const normalizedFilterValue = filterValue.toLocaleUpperCase();
const foundOption = options.find(
( option ) =>
option.label
.toLocaleUpperCase()
.startsWith( normalizedFilterValue ) ||
option.value.toLocaleUpperCase() ===
normalizedFilterValue
);
if ( foundOption ) {
onChange( foundOption.value );
}
}
} }
options={ options }
value={ value || '' }
allowReset={ false }
autoComplete={ autoComplete }
/>
<ValidationInputError propertyName={ errorId } />
</div>
);
};
export default withInstanceId( Combobox );

View File

@ -0,0 +1,156 @@
.wc-block-components-form .wc-block-components-combobox,
.wc-block-components-combobox {
.wc-block-components-combobox-control {
@include reset-typography();
@include reset-box();
.components-base-control__field {
@include reset-box();
}
.components-combobox-control__suggestions-container {
@include reset-typography();
@include reset-box();
position: relative;
}
input.components-combobox-control__input {
@include reset-typography();
@include font-size(regular);
box-sizing: border-box;
outline: inherit;
border: 1px solid $input-border-gray;
background: #fff;
box-shadow: none;
color: $input-text-active;
font-family: inherit;
font-weight: normal;
height: 3em;
letter-spacing: inherit;
line-height: 1;
padding: em($gap-large) $gap em($gap-smallest);
text-align: left;
text-overflow: ellipsis;
text-transform: none;
white-space: nowrap;
width: 100%;
opacity: initial;
border-radius: 4px;
&[aria-expanded="true"],
&:focus {
background-color: #fff;
color: $input-text-active;
}
&[aria-expanded="true"] {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.has-dark-controls & {
background-color: $input-background-dark;
border-color: $input-border-dark;
color: $input-text-dark;
&:focus {
background-color: $input-background-dark;
color: $input-text-dark;
}
}
}
.components-form-token-field__suggestions-list {
position: absolute;
z-index: 10;
background-color: $select-dropdown-light;
border: 1px solid $input-border-gray;
border-top: 0;
margin: 3em 0 0 0;
padding: 0;
max-height: 300px;
min-width: 100%;
overflow: auto;
color: $input-text-active;
.has-dark-controls & {
background-color: $select-dropdown-dark;
color: $input-text-dark;
}
.components-form-token-field__suggestion {
@include font-size(regular);
color: $gray-700;
cursor: default;
list-style: none;
margin: 0;
padding: em($gap-smallest) $gap;
&.is-selected {
background-color: $gray-300;
.has-dark-controls & {
background-color: $select-item-dark;
}
}
&:hover,
&:focus,
&.is-highlighted,
&:active {
background-color: #00669e;
color: #fff;
}
}
}
label.components-base-control__label {
@include reset-typography();
@include font-size(regular);
line-height: 1.375; // =22px when font-size is 16px.
position: absolute;
transform: translateY(0.75em);
transform-origin: top left;
transition: all 200ms ease;
color: $gray-700;
z-index: 1;
margin: 0 0 0 #{$gap + 1px};
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - #{2 * $gap});
white-space: nowrap;
.has-dark-controls & {
color: $input-placeholder-dark;
}
@media screen and (prefers-reduced-motion: reduce) {
transition: none;
}
}
}
&.is-active,
&:focus-within {
.wc-block-components-combobox-control label.components-base-control__label {
transform: translateY(#{$gap-smallest}) scale(0.75);
}
}
&.has-error {
.wc-block-components-combobox-control {
label.components-base-control__label {
color: $alert-red;
}
input.components-combobox-control__input {
&,
&:hover,
&:focus,
&:active {
border-color: $alert-red;
}
&:focus {
outline: 1px dotted $alert-red;
outline-offset: 2px;
}
}
}
}
}

View File

@ -9,7 +9,7 @@ import classnames from 'classnames';
/**
* Internal dependencies
*/
import { ValidatedSelect } from '../select';
import Combobox from '../combobox';
import './style.scss';
import type { CountryInputWithCountriesProps } from './CountryInputProps';
@ -31,8 +31,8 @@ const CountryInput = ( {
const options = useMemo(
() =>
Object.keys( countries ).map( ( key ) => ( {
key,
name: decodeEntities( countries[ key ] ),
value: key,
label: decodeEntities( countries[ key ] ),
} ) ),
[ countries ]
);
@ -44,15 +44,16 @@ const CountryInput = ( {
'wc-block-components-country-input'
) }
>
<ValidatedSelect
<Combobox
id={ id }
label={ label }
onChange={ onChange }
options={ options }
value={ options.find( ( option ) => option.key === value ) }
value={ value }
errorId={ errorId }
errorMessage={ errorMessage }
required={ required }
autoComplete={ autoComplete }
/>
{ autoComplete !== 'off' && (
<input
@ -61,11 +62,17 @@ const CountryInput = ( {
autoComplete={ autoComplete }
value={ value }
onChange={ ( event ) => {
const textValue = event.target.value;
const textValue = event.target.value.toLocaleUpperCase();
const foundOption = options.find(
( option ) => option.key === textValue
( option ) =>
( textValue.length !== 2 &&
option.label.toLocaleUpperCase() ===
textValue ) ||
( textValue.length === 2 &&
option.value.toLocaleUpperCase() ===
textValue )
);
onChange( foundOption ? foundOption.key : '' );
onChange( foundOption ? foundOption.value : '' );
} }
style={ {
minHeight: '0',

View File

@ -1,2 +0,0 @@
export { default as Select } from './select';
export { default as ValidatedSelect } from './validated';

View File

@ -1,60 +0,0 @@
/**
* External dependencies
*/
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { CustomSelectControl } from 'wordpress-components';
/**
* Internal dependencies
*/
import './style.scss';
const Select = ( {
className,
feedback,
id,
label,
onChange,
options,
value,
} ) => {
return (
<div
id={ id }
className={ classnames( 'wc-block-components-select', className, {
'is-active': value,
} ) }
>
<CustomSelectControl
label={ label }
onChange={ ( { selectedItem } ) => {
onChange( selectedItem.key );
} }
options={ options }
value={ value || null }
/>
{ feedback }
</div>
);
};
Select.propTypes = {
onChange: PropTypes.func.isRequired,
options: PropTypes.arrayOf(
PropTypes.shape( {
key: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
} ).isRequired
).isRequired,
className: PropTypes.string,
feedback: PropTypes.node,
id: PropTypes.string,
label: PropTypes.string,
value: PropTypes.shape( {
key: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
} ),
};
export default Select;

View File

@ -1,162 +0,0 @@
.wc-block-components-form .wc-block-components-select,
.wc-block-components-select {
height: 3em;
position: relative;
label.components-custom-select-control__label {
@include reset-typography();
@include font-size(regular);
line-height: 1.375; // =22px when font-size is 16px.
position: absolute;
transform: translateY(0.75em);
transform-origin: top left;
transition: all 200ms ease;
color: $gray-700;
z-index: 1;
margin: 0 0 0 #{$gap + 1px};
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - #{2 * $gap});
white-space: nowrap;
.has-dark-controls & {
color: $input-placeholder-dark;
}
@media screen and (prefers-reduced-motion: reduce) {
transition: none;
}
}
&.is-active label {
transform: translateY(#{$gap-smallest}) scale(0.75);
}
&.has-error {
.components-custom-select-control__button {
&,
&:hover,
&:focus,
&:active {
border-color: $alert-red;
}
&:focus {
outline: 1px dotted $alert-red;
outline-offset: 2px;
}
}
}
&.has-error label {
color: $alert-red;
}
.components-custom-select-control__button {
&,
&:hover,
&:focus,
&:active {
@include font-size(regular);
background: #fff;
box-shadow: none;
color: $input-text-active;
font-family: inherit;
font-weight: normal;
height: 3em;
letter-spacing: inherit;
line-height: 1;
overflow: hidden;
padding: em($gap-large) $gap em($gap-smallest);
text-align: left;
text-overflow: ellipsis;
text-transform: none;
white-space: nowrap;
width: 100%;
opacity: initial;
border-radius: 4px;
.has-dark-controls & {
background: $input-background-dark;
border-color: $input-border-dark;
color: $input-text-dark;
}
}
}
.components-custom-select-control__button-icon {
right: #{$gap - 4px};
.has-dark-controls & {
fill: $input-text-dark;
}
}
.components-custom-select-control__menu {
background-color: $select-dropdown-light;
margin: 0;
max-height: 300px;
overflow: auto;
color: $input-text-active;
// Required by IE11.
&:empty {
display: none;
}
.has-dark-controls & {
background-color: $select-dropdown-dark;
color: $input-text-dark;
}
}
.components-custom-select-control__item {
@include font-size(regular);
margin-left: 0;
padding-left: $gap;
&:hover,
&:focus,
&.is-highlighted {
.has-dark-controls & {
background-color: $select-item-dark;
}
}
}
.components-custom-select-control__item-icon {
display: none;
}
}
.theme-twentytwentyone {
// Extra classes for specificity.
&.theme-twentytwentyone.theme-twentytwentyone .components-custom-select-control__button {
background-color: #fff;
color: $input-text-active;
}
&.is-dark-theme {
// If the theme is in dark mode, as well as the block, then this selector will match.
.has-dark-controls {
.components-custom-select-control__item {
color: $input-text-dark;
}
}
// If the theme is in dark mode, but the block isn't, then this selector will match.
.components-custom-select-control__item {
color: $input-text-active;
}
}
}
.theme-twentyseventeen {
// Extra classes for specificity.
&.theme-twentyseventeen.theme-twentyseventeen {
.components-custom-select-control__button {
background-color: $select-dropdown-light;
color: $input-text-active;
}
.has-dark-controls {
.components-custom-select-control__button {
background-color: $select-dropdown-dark;
color: $input-text-dark;
}
}
}
}

View File

@ -1,100 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useEffect } from 'react';
import { useShallowEqual } from '@woocommerce/base-hooks';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withInstanceId } from '@wordpress/compose';
import {
ValidationInputError,
useValidationContext,
} from '@woocommerce/base-context';
/**
* Internal dependencies
*/
import Select from './select';
import './style.scss';
const ValidatedSelect = ( {
className,
id,
value,
instanceId,
required,
errorId,
errorMessage = __(
'Please select a value.',
'woo-gutenberg-products-block'
),
...rest
} ) => {
const selectId = id || 'select-' + instanceId;
errorId = errorId || selectId;
// Prevents re-renders when value is an object, e.g. {key: "NY", name: "New York"}
const currentValue = useShallowEqual( value );
const {
getValidationError,
setValidationErrors,
clearValidationError,
} = useValidationContext();
useEffect( () => {
if ( ! required || currentValue ) {
clearValidationError( errorId );
} else {
setValidationErrors( {
[ errorId ]: {
message: errorMessage,
hidden: true,
},
} );
}
}, [
clearValidationError,
currentValue,
errorId,
errorMessage,
required,
setValidationErrors,
] );
// Remove validation errors when unmounted.
useEffect( () => {
return () => {
clearValidationError( errorId );
};
}, [ clearValidationError, errorId ] );
const error = getValidationError( errorId ) || {};
return (
<Select
id={ selectId }
className={ classnames( className, {
'has-error': error.message && ! error.hidden,
} ) }
feedback={ <ValidationInputError propertyName={ errorId } /> }
value={ currentValue }
{ ...rest }
/>
);
};
ValidatedSelect.propTypes = {
className: PropTypes.string,
errorId: PropTypes.string,
errorMessage: PropTypes.string,
id: PropTypes.string,
required: PropTypes.bool,
value: PropTypes.shape( {
key: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
} ),
};
export default withInstanceId( ValidatedSelect );

View File

@ -10,7 +10,7 @@ import classnames from 'classnames';
* Internal dependencies
*/
import { ValidatedTextInput } from '../text-input';
import { ValidatedSelect } from '../select';
import Combobox from '../combobox';
import './style.scss';
import type { StateInputWithStatesProps } from './StateInputProps';
@ -30,8 +30,8 @@ const StateInput = ( {
() =>
countryStates
? Object.keys( countryStates ).map( ( key ) => ( {
key,
name: decodeEntities( countryStates[ key ] ),
value: key,
label: decodeEntities( countryStates[ key ] ),
} ) )
: [],
[ countryStates ]
@ -47,10 +47,12 @@ const StateInput = ( {
if ( options.length > 0 ) {
const foundOption = options.find(
( option ) =>
option.key === stateValue || option.name === stateValue
option.label.toLocaleUpperCase() ===
stateValue.toLocaleUpperCase() ||
option.value.toLocaleUpperCase() ===
stateValue.toLocaleUpperCase()
);
onChange( foundOption ? foundOption.key : '' );
onChange( foundOption ? foundOption.value : '' );
return;
}
onChange( stateValue );
@ -61,7 +63,7 @@ const StateInput = ( {
if ( options.length > 0 ) {
return (
<>
<ValidatedSelect
<Combobox
className={ classnames(
className,
'wc-block-components-state-input'
@ -70,12 +72,13 @@ const StateInput = ( {
label={ label }
onChange={ onChangeState }
options={ options }
value={ options.find( ( option ) => option.key === value ) }
value={ value }
errorMessage={ __(
'Please select a state.',
'woo-gutenberg-products-block'
) }
required={ required }
autoComplete={ autoComplete }
/>
{ autoComplete !== 'off' && (
<input

View File

@ -91,6 +91,8 @@ const entries = {
'./node_modules/wordpress-components/src/spinner/style.scss',
'snackbar-notice-style':
'./node_modules/wordpress-components/src/snackbar/style.scss',
'combobox-control-style':
'./node_modules/wordpress-components/src/combobox-control/style.scss',
'general-style': glob.sync( './assets/**/*.scss', {
ignore: [

View File

@ -9682,6 +9682,12 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true
},
"@yarnpkg/lockfile": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
"dev": true
},
"abab": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
@ -11042,6 +11048,16 @@
"dev": true,
"optional": true
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@ -16835,6 +16851,13 @@
}
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true
},
"filelist": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz",
@ -17007,6 +17030,60 @@
"locate-path": "^3.0.0"
}
},
"find-yarn-workspace-root": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
"dev": true,
"requires": {
"micromatch": "^4.0.2"
},
"dependencies": {
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
}
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.2.3"
}
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": {
"is-number": "^7.0.0"
}
}
}
},
"findup-sync": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz",
@ -21488,6 +21565,15 @@
"graceful-fs": "^4.1.9"
}
},
"klaw-sync": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.11"
}
},
"kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@ -23559,6 +23645,13 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
"nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"dev": true,
"optional": true
},
"nanoid": {
"version": "3.1.23",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
@ -24601,6 +24694,75 @@
"integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
"dev": true
},
"patch-package": {
"version": "6.4.7",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.4.7.tgz",
"integrity": "sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ==",
"dev": true,
"requires": {
"@yarnpkg/lockfile": "^1.1.0",
"chalk": "^2.4.2",
"cross-spawn": "^6.0.5",
"find-yarn-workspace-root": "^2.0.0",
"fs-extra": "^7.0.1",
"is-ci": "^2.0.0",
"klaw-sync": "^6.0.0",
"minimist": "^1.2.0",
"open": "^7.4.2",
"rimraf": "^2.6.3",
"semver": "^5.6.0",
"slash": "^2.0.0",
"tmp": "^0.0.33"
},
"dependencies": {
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"fs-extra": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6"
}
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true
}
}
},
"path": {
"version": "0.12.7",
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
@ -33366,7 +33528,11 @@
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
"optional": true
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
},
"glob-parent": {
"version": "3.1.0",

View File

@ -51,6 +51,7 @@
"package-plugin:zip-only": "rimraf woocommerce-gutenberg-products-block.zip && ./bin/build-plugin-zip.sh -z",
"package-plugin:deploy": "npm run build:deploy && npm run package-plugin:zip-only",
"phpunit": "docker-compose up -d db && docker-compose up -d --build wordpress-unit-tests && docker exec --workdir /var/www/html/wp-content/plugins/woocommerce-gutenberg-products-block wordpress_test php ./vendor/bin/phpunit",
"postinstall": "patch-package",
"reformat-files": "prettier --ignore-path .eslintignore --write \"**/*.{js,jsx,json,ts,tsx}\"",
"release": "sh ./bin/wordpress-deploy.sh",
"start": "rimraf build/* && cross-env BABEL_ENV=default CHECK_CIRCULAR_DEPS=true webpack --watch --info-verbosity none",
@ -157,6 +158,7 @@
"lodash": "4.17.21",
"merge-config": "2.0.0",
"mini-css-extract-plugin": "1.3.6",
"patch-package": "^6.4.7",
"postcss": "8.2.10",
"postcss-loader": "4.2.0",
"prettier": "npm:wp-prettier@2.0.5",

View File

@ -0,0 +1,267 @@
diff --git a/node_modules/wordpress-components/build-module/combobox-control/index.js b/node_modules/wordpress-components/build-module/combobox-control/index.js
index 51c59c1..61bda14 100644
--- a/node_modules/wordpress-components/build-module/combobox-control/index.js
+++ b/node_modules/wordpress-components/build-module/combobox-control/index.js
@@ -1,19 +1,10 @@
-import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
-import _classCallCheck from "@babel/runtime/helpers/esm/classCallCheck";
-import _createClass from "@babel/runtime/helpers/esm/createClass";
-import _inherits from "@babel/runtime/helpers/esm/inherits";
-import _possibleConstructorReturn from "@babel/runtime/helpers/esm/possibleConstructorReturn";
-import _getPrototypeOf from "@babel/runtime/helpers/esm/getPrototypeOf";
import { createElement } from "@wordpress/element";
-function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
-
-function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
-
/**
* External dependencies
*/
import classnames from 'classnames';
+import { noop, deburr } from 'lodash';
/**
* WordPress dependencies
*/
@@ -34,77 +25,46 @@ import BaseControl from '../base-control';
import Button from '../button';
import { Flex, FlexBlock, FlexItem } from '../flex';
import withFocusOutside from '../higher-order/with-focus-outside';
-var DetectOutside = withFocusOutside( /*#__PURE__*/function (_Component) {
- _inherits(_class, _Component);
-
- var _super = _createSuper(_class);
-
- function _class() {
- _classCallCheck(this, _class);
-
- return _super.apply(this, arguments);
+const DetectOutside = withFocusOutside(class extends Component {
+ handleFocusOutside(event) {
+ this.props.onFocusOutside(event);
}
- _createClass(_class, [{
- key: "handleFocusOutside",
- value: function handleFocusOutside(event) {
- this.props.onFocusOutside(event);
- }
- }, {
- key: "render",
- value: function render() {
- return this.props.children;
- }
- }]);
-
- return _class;
-}(Component));
+ render() {
+ return this.props.children;
+ }
-function ComboboxControl(_ref) {
+});
+
+function ComboboxControl({
+ value,
+ label,
+ options,
+ onChange,
+ onFilterValueChange = noop,
+ hideLabelFromVision,
+ help,
+ allowReset = true,
+ className,
+ messages = {
+ selected: __('Item selected.')
+ }
+}) {
var _currentOption$label;
- var value = _ref.value,
- label = _ref.label,
- options = _ref.options,
- onChange = _ref.onChange,
- onFilterValueChange = _ref.onFilterValueChange,
- hideLabelFromVision = _ref.hideLabelFromVision,
- help = _ref.help,
- _ref$allowReset = _ref.allowReset,
- allowReset = _ref$allowReset === void 0 ? true : _ref$allowReset,
- className = _ref.className,
- _ref$messages = _ref.messages,
- messages = _ref$messages === void 0 ? {
- selected: __('Item selected.')
- } : _ref$messages;
- var instanceId = useInstanceId(ComboboxControl);
-
- var _useState = useState(null),
- _useState2 = _slicedToArray(_useState, 2),
- selectedSuggestion = _useState2[0],
- setSelectedSuggestion = _useState2[1];
-
- var _useState3 = useState(false),
- _useState4 = _slicedToArray(_useState3, 2),
- isExpanded = _useState4[0],
- setIsExpanded = _useState4[1];
-
- var _useState5 = useState(''),
- _useState6 = _slicedToArray(_useState5, 2),
- inputValue = _useState6[0],
- setInputValue = _useState6[1];
-
- var inputContainer = useRef();
- var currentOption = options.find(function (option) {
- return option.value === value;
- });
- var currentLabel = (_currentOption$label = currentOption === null || currentOption === void 0 ? void 0 : currentOption.label) !== null && _currentOption$label !== void 0 ? _currentOption$label : '';
- var matchingSuggestions = useMemo(function () {
- var startsWithMatch = [];
- var containsMatch = [];
- var match = inputValue.toLocaleLowerCase();
- options.forEach(function (option) {
- var index = option.label.toLocaleLowerCase().indexOf(match);
+ const currentOption = options.find(option => option.value === value);
+ const currentLabel = (_currentOption$label = currentOption === null || currentOption === void 0 ? void 0 : currentOption.label) !== null && _currentOption$label !== void 0 ? _currentOption$label : '';
+ const instanceId = useInstanceId(ComboboxControl);
+ const [selectedSuggestion, setSelectedSuggestion] = useState(currentOption || null);
+ const [isExpanded, setIsExpanded] = useState(false);
+ const [inputValue, setInputValue] = useState('');
+ const inputContainer = useRef();
+ const matchingSuggestions = useMemo(() => {
+ const startsWithMatch = [];
+ const containsMatch = [];
+ const match = deburr(inputValue.toLocaleLowerCase());
+ options.forEach(option => {
+ const index = deburr(option.label).toLocaleLowerCase().indexOf(match);
if (index === 0) {
startsWithMatch.push(option);
@@ -115,7 +75,7 @@ function ComboboxControl(_ref) {
return startsWithMatch.concat(containsMatch);
}, [inputValue, options, value]);
- var onSuggestionSelected = function onSuggestionSelected(newSelectedSuggestion) {
+ const onSuggestionSelected = newSelectedSuggestion => {
onChange(newSelectedSuggestion.value);
speak(messages.selected, 'assertive');
setSelectedSuggestion(newSelectedSuggestion);
@@ -123,10 +83,9 @@ function ComboboxControl(_ref) {
setIsExpanded(false);
};
- var handleArrowNavigation = function handleArrowNavigation() {
- var offset = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
- var index = matchingSuggestions.indexOf(selectedSuggestion);
- var nextIndex = index + offset;
+ const handleArrowNavigation = (offset = 1) => {
+ const index = matchingSuggestions.indexOf(selectedSuggestion);
+ let nextIndex = index + offset;
if (nextIndex < 0) {
nextIndex = matchingSuggestions.length - 1;
@@ -138,8 +97,12 @@ function ComboboxControl(_ref) {
setIsExpanded(true);
};
- var onKeyDown = function onKeyDown(event) {
- var preventDefault = false;
+ const onKeyDown = event => {
+ let preventDefault = false;
+
+ if (event.defaultPrevented) {
+ return;
+ }
switch (event.keyCode) {
case ENTER:
@@ -164,7 +127,6 @@ function ComboboxControl(_ref) {
setIsExpanded(false);
setSelectedSuggestion(null);
preventDefault = true;
- event.stopPropagation();
break;
default:
@@ -176,34 +138,44 @@ function ComboboxControl(_ref) {
}
};
- var onFocus = function onFocus() {
+ const onFocus = () => {
setIsExpanded(true);
onFilterValueChange('');
setInputValue('');
};
- var onFocusOutside = function onFocusOutside() {
+ const onFocusOutside = () => {
setIsExpanded(false);
};
- var onInputChange = function onInputChange(event) {
- var text = event.value;
+ const onInputChange = event => {
+ const text = event.value;
setInputValue(text);
onFilterValueChange(text);
setIsExpanded(true);
};
- var handleOnReset = function handleOnReset() {
+ const handleOnReset = () => {
onChange(null);
inputContainer.current.input.focus();
- }; // Announcements
+ }; // Update current selections when the filter input changes.
- useEffect(function () {
- var hasMatchingSuggestions = matchingSuggestions.length > 0;
+ useEffect(() => {
+ const hasMatchingSuggestions = matchingSuggestions.length > 0;
+ const hasSelectedMatchingSuggestions = matchingSuggestions.indexOf(selectedSuggestion) > 0;
+
+ if (hasMatchingSuggestions && !hasSelectedMatchingSuggestions) {
+ // If the current selection isn't present in the list of suggestions, then automatically select the first item from the list of suggestions.
+ setSelectedSuggestion(matchingSuggestions[0]);
+ }
+ }, [matchingSuggestions, selectedSuggestion]); // Announcements
+
+ useEffect(() => {
+ const hasMatchingSuggestions = matchingSuggestions.length > 0;
if (isExpanded) {
- var message = hasMatchingSuggestions ? sprintf(
+ const message = hasMatchingSuggestions ? sprintf(
/* translators: %d: number of results. */
_n('%d result found, use up and down arrow keys to navigate.', '%d results found, use up and down arrow keys to navigate.', matchingSuggestions.length), matchingSuggestions.length) : __('No results.');
speak(message, 'polite');
@@ -220,7 +192,7 @@ function ComboboxControl(_ref) {
className: classnames(className, 'components-combobox-control'),
tabIndex: "-1",
label: label,
- id: "components-form-token-input-".concat(instanceId),
+ id: `components-form-token-input-${instanceId}`,
hideLabelFromVision: hideLabelFromVision,
help: help
}, createElement("div", {
@@ -232,7 +204,7 @@ function ComboboxControl(_ref) {
instanceId: instanceId,
ref: inputContainer,
value: isExpanded ? inputValue : currentLabel,
- "aria-label": currentLabel ? "".concat(currentLabel, ", ").concat(label) : null,
+ "aria-label": currentLabel ? `${currentLabel}, ${label}` : null,
onFocus: onFocus,
isExpanded: isExpanded,
selectedSuggestionIndex: matchingSuggestions.indexOf(selectedSuggestion),
@@ -248,9 +220,7 @@ function ComboboxControl(_ref) {
match: {
label: inputValue
},
- displayTransform: function displayTransform(suggestion) {
- return suggestion.label;
- },
+ displayTransform: suggestion => suggestion.label,
suggestions: matchingSuggestions,
selectedIndex: matchingSuggestions.indexOf(selectedSuggestion),
onHover: setSelectedSuggestion,