* initial refactor

* Fix auto fill and write tests

* Removed autofill from country/region and added support for abbreviation regions

* Add changelog

* Add changelog for the component package

* Fix clear form for autofill
This commit is contained in:
louwie17 2021-08-23 14:36:00 -03:00 committed by GitHub
parent 7cb06f00fd
commit c6abb21840
6 changed files with 207 additions and 14 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: Fix
Update country region typeahead for better autofill support. #7497

View File

@ -4,7 +4,7 @@
import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
import { escapeRegExp } from 'lodash';
import { Fragment, useEffect, useMemo, useState } from '@wordpress/element';
import { useEffect, useMemo, useState, useRef } from '@wordpress/element';
import { getSetting } from '@woocommerce/wc-admin-settings';
import { SelectControl, TextControl } from '@woocommerce/components';
@ -85,33 +85,63 @@ export function getCountryStateOptions() {
export function useGetCountryStateAutofill( options, countryState, setValue ) {
const [ autofillCountry, setAutofillCountry ] = useState( '' );
const [ autofillState, setAutofillState ] = useState( '' );
const isAutofillChange = useRef();
useEffect( () => {
const option = options.find( ( opt ) => opt.key === countryState );
const labels = option ? option.label.split( /\u2013|\u2014|\-/ ) : [];
const newCountry = ( labels[ 0 ] || '' ).trim();
const newState = ( labels[ 1 ] || '' ).trim();
if (
! isAutofillChange.current &&
( newCountry !== autofillCountry || newState !== autofillState )
) {
setAutofillCountry( newCountry );
setAutofillState( newState );
}
isAutofillChange.current = false;
}, [ countryState ] );
useEffect( () => {
if ( ! autofillCountry && ! autofillState && countryState ) {
// Clear form.
isAutofillChange.current = true;
setValue( 'countryState', '' );
}
let filteredOptions = [];
const countrySearch = new RegExp(
escapeRegExp( autofillCountry ),
'i'
);
const stateSearch = new RegExp(
escapeRegExp( autofillState.replace( /\s/g, '' ) ) + '$', // Always match the end of string for region.
'i'
);
if ( autofillState.length || autofillCountry.length ) {
filteredOptions = options.filter( ( option ) =>
countrySearch.test( option.label )
( autofillCountry.length ? countrySearch : stateSearch ).test(
option.label
)
);
}
if ( autofillCountry.length && autofillState.length ) {
const stateSearch = new RegExp(
escapeRegExp( autofillState.replace( /\s/g, '' ) ),
'i'
);
const isStateAbbreviation = autofillState.length < 3;
filteredOptions = filteredOptions.filter( ( option ) =>
stateSearch.test(
option.label.replace( '-', '' ).replace( /\s/g, '' )
( isStateAbbreviation ? option.key : option.label )
.replace( '-', '' )
.replace( /\s/g, '' )
)
);
const isCountryAbbreviation = autofillCountry.length < 3;
if ( filteredOptions.length > 1 ) {
let countryKeyOptions = [];
countryKeyOptions = filteredOptions.filter( ( option ) =>
countrySearch.test( option.key )
countrySearch.test(
isCountryAbbreviation ? option.key : option.label
)
);
if ( countryKeyOptions.length > 0 ) {
@ -122,7 +152,11 @@ export function useGetCountryStateAutofill( options, countryState, setValue ) {
if ( filteredOptions.length > 1 ) {
let stateKeyOptions = [];
stateKeyOptions = filteredOptions.filter( ( option ) =>
stateSearch.test( option.key )
stateSearch.test(
( isStateAbbreviation ? option.key : option.label )
.replace( '-', '' )
.replace( /\s/g, '' )
)
);
if ( stateKeyOptions.length === 1 ) {
@ -135,12 +169,13 @@ export function useGetCountryStateAutofill( options, countryState, setValue ) {
filteredOptions.length === 1 &&
countryState !== filteredOptions[ 0 ].key
) {
isAutofillChange.current = true;
setValue( 'countryState', filteredOptions[ 0 ].key );
}
}, [ autofillCountry, autofillState, countryState, options, setValue ] );
}, [ autofillCountry, autofillState, options, setValue ] );
return (
<Fragment>
<>
<input
onChange={ ( event ) =>
setAutofillCountry( event.target.value )
@ -162,7 +197,7 @@ export function useGetCountryStateAutofill( options, countryState, setValue ) {
tabIndex="-1"
autoComplete="address-level1"
/>
</Fragment>
</>
);
}
@ -200,6 +235,7 @@ export function StoreAddress( props ) {
<SelectControl
label={ __( 'Country / Region', 'woocommerce-admin' ) }
required
autoComplete="new-password" // disable autocomplete and autofill
options={ countryStateOptions }
excludeSelectedOptions={ false }
showAllOnFocus

View File

@ -0,0 +1,142 @@
/**
* External dependencies
*/
import { useState, useEffect, useCallback } from '@wordpress/element';
import { render, fireEvent } from '@testing-library/react';
/**
* Internal dependencies
*/
import { useGetCountryStateAutofill } from '../store-address';
const AutofillWrapper = ( { options, value, onChange } ) => {
const [ values, setValues ] = useState( { countryState: value || '' } );
const setCountryState = useCallback( ( key, newValue ) => {
setValues( {
...values,
[ key ]: newValue,
} );
}, [] );
useEffect( () => {
setValues( { countryState: value } );
}, [ value ] );
useEffect( () => {
if ( onChange ) {
onChange( values );
}
}, [ values ] );
const countryStateAutofill = useGetCountryStateAutofill(
options,
values.countryState,
setCountryState
);
return countryStateAutofill;
};
const DEFAULT_OPTIONS = [
{ key: 'KH', label: 'Cambodia' },
{ key: 'CM', label: 'Cameroon' },
{ key: 'CA:AB', label: 'Canada — Alberta' },
{ key: 'CA:BC', label: 'Canada — British Columbia' },
{ key: 'CA:MB', label: 'Canada — Manitoba' },
{ key: 'US:CA', label: 'United States - California' },
];
describe( 'useGetCountryStateAutofill', () => {
it( 'should render a country and state inputs with autoComplete', () => {
const { queryAllByRole } = render(
<AutofillWrapper options={ [ ...DEFAULT_OPTIONS ] } />
);
const inputs = queryAllByRole( 'textbox' );
expect( inputs.length ).toBe( 2 );
expect( inputs[ 0 ].autocomplete ).toEqual( 'country' );
expect( inputs[ 1 ].autocomplete ).toEqual( 'address-level1' );
} );
it( 'should set autocomplete fields if a value is selected', () => {
const { queryAllByRole } = render(
<AutofillWrapper options={ [ ...DEFAULT_OPTIONS ] } value="CA:MB" />
);
const inputs = queryAllByRole( 'textbox' );
expect( inputs.length ).toBe( 2 );
expect( inputs[ 0 ].value ).toEqual( 'Canada' );
expect( inputs[ 1 ].value ).toEqual( 'Manitoba' );
} );
it( 'should select region by key if abbreviation is used', () => {
const onChange = jest.fn();
const { queryAllByRole } = render(
<AutofillWrapper
options={ [ ...DEFAULT_OPTIONS ] }
onChange={ onChange }
/>
);
const inputs = queryAllByRole( 'textbox' );
fireEvent.change( inputs[ 0 ], { target: { value: 'United States' } } );
fireEvent.change( inputs[ 1 ], {
target: { value: 'CA' },
} );
expect( onChange ).toHaveBeenCalledWith( { countryState: 'US:CA' } );
} );
it( 'should update the value if the auto complete fields changed', () => {
const onChange = jest.fn();
const { queryAllByRole } = render(
<AutofillWrapper
options={ [ ...DEFAULT_OPTIONS ] }
onChange={ onChange }
/>
);
const inputs = queryAllByRole( 'textbox' );
fireEvent.change( inputs[ 0 ], { target: { value: 'Canada' } } );
fireEvent.change( inputs[ 1 ], {
target: { value: 'British Columbia' },
} );
expect( onChange ).toHaveBeenCalledWith( { countryState: 'CA:BC' } );
} );
it( 'should update the value if the auto complete fields changed and value was already set', () => {
const onChange = jest.fn();
const { queryAllByRole } = render(
<AutofillWrapper
options={ [ ...DEFAULT_OPTIONS ] }
onChange={ onChange }
value="CM"
/>
);
const inputs = queryAllByRole( 'textbox' );
expect( inputs[ 0 ].value ).toEqual( 'Cameroon' );
onChange.mockClear();
fireEvent.change( inputs[ 0 ], { target: { value: 'Canada' } } );
fireEvent.change( inputs[ 1 ], {
target: { value: 'British Columbia' },
} );
expect( onChange ).toHaveBeenCalledWith( { countryState: 'CA:BC' } );
} );
it( 'should update the auto complete inputs when value changed and inputs already set', () => {
const onChange = jest.fn();
const options = [ ...DEFAULT_OPTIONS ];
const { rerender, queryAllByRole } = render(
<AutofillWrapper options={ options } onChange={ onChange } />
);
let inputs = queryAllByRole( 'textbox' );
fireEvent.change( inputs[ 0 ], { target: { value: 'Canada' } } );
fireEvent.change( inputs[ 1 ], {
target: { value: 'British Columbia' },
} );
expect( onChange ).toHaveBeenCalledWith( { countryState: 'CA:BC' } );
rerender(
<AutofillWrapper
options={ options }
onChange={ onChange }
value="KH"
/>
);
inputs = queryAllByRole( 'textbox' );
expect( inputs[ 0 ].value ).toEqual( 'Cambodia' );
expect( inputs[ 1 ].value ).toEqual( '' );
} );
} );

View File

@ -1,4 +1,5 @@
# Unreleased
- Add `autoComplete` prop to the `SelectControl` component. #7497
# 8.1.0

View File

@ -124,12 +124,13 @@ class Control extends Component {
onSearch,
placeholder,
searchInputType,
autoComplete,
} = this.props;
const { isActive } = this.state;
return (
<input
autoComplete="off"
autoComplete={ autoComplete || 'off' }
className="woocommerce-select-control__control-input"
id={ `woocommerce-select-control-${ instanceId }__control-input` }
ref={ this.input }
@ -329,6 +330,10 @@ Control.propTypes = {
* Show all options on focusing, even if a query exists.
*/
showAllOnFocus: PropTypes.bool,
/**
* Control input autocomplete field, defaults: off.
*/
autoComplete: PropTypes.string,
};
export default Control;

View File

@ -3,7 +3,7 @@
*/
import { __, _n, sprintf } from '@wordpress/i18n';
import classnames from 'classnames';
import { createElement, Component } from '@wordpress/element';
import { Component, createElement } from '@wordpress/element';
import { debounce, escapeRegExp, identity, noop } from 'lodash';
import PropTypes from 'prop-types';
import { withFocusOutside, withSpokenMessages } from '@wordpress/components';
@ -539,6 +539,10 @@ SelectControl.propTypes = {
* Render results list positioned statically instead of absolutely.
*/
staticList: PropTypes.bool,
/**
* autocomplete prop for the Control input field.
*/
autoComplete: PropTypes.string,
};
SelectControl.defaultProps = {
@ -559,6 +563,7 @@ SelectControl.defaultProps = {
showClearButton: false,
hideBeforeSearch: false,
staticList: false,
autoComplete: 'off',
};
export default compose( [