Fix country/region selection not preserved in store details task (https://github.com/woocommerce/woocommerce-admin/pull/8228)
* Fix country/region selection not preserved in store details task * Update store-adress test cases * Add changelog * Check alphabets regx for isStateAbbreviation * Update comments * Use unknown type for locale * Add Disable reason
This commit is contained in:
parent
ea6b50f241
commit
e6f19d0d91
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: Fix
|
||||
|
||||
Fix country/region selection not preserved in store details task. #8228
|
|
@ -16,6 +16,9 @@ import { useSelect } from '@wordpress/data';
|
|||
import { getAdminSetting } from '~/utils/admin-settings';
|
||||
|
||||
const { countries } = getAdminSetting( 'dataEndpoints', { countries: {} } );
|
||||
|
||||
type Option = { key: string; label: string };
|
||||
|
||||
/**
|
||||
* Check if a given address field is required for the locale.
|
||||
*
|
||||
|
@ -23,9 +26,12 @@ const { countries } = getAdminSetting( 'dataEndpoints', { countries: {} } );
|
|||
* @param {Object} locale Locale data.
|
||||
* @return {boolean} Field requirement.
|
||||
*/
|
||||
export function isAddressFieldRequired( fieldName, locale = {} ) {
|
||||
export function isAddressFieldRequired(
|
||||
fieldName: string,
|
||||
locale: unknown = {}
|
||||
): boolean {
|
||||
if ( locale[ fieldName ]?.hasOwnProperty( 'required' ) ) {
|
||||
return locale[ fieldName ]?.required;
|
||||
return locale[ fieldName ]?.required as boolean;
|
||||
}
|
||||
|
||||
if ( fieldName === 'address_2' ) {
|
||||
|
@ -122,6 +128,58 @@ export function getCountryStateOptions() {
|
|||
return countryStateOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize state string for matching.
|
||||
*
|
||||
* @param {string} state The state to normalize.
|
||||
* @return {Function} filter function.
|
||||
*/
|
||||
export const normalizeState = ( state: string ): string => {
|
||||
return state.replace( /\s/g, '' ).toLowerCase();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get state filter
|
||||
*
|
||||
* @param {string} isStateAbbreviation Whether to use state abbreviation or not.
|
||||
* @param {string} normalizedAutofillState The value of the autofillState field.
|
||||
* @return {Function} filter function.
|
||||
*/
|
||||
export const getStateFilter = (
|
||||
isStateAbbreviation: boolean,
|
||||
normalizedAutofillState: string
|
||||
): ( ( option: Option ) => boolean ) => ( option: Option ) => {
|
||||
const countryStateArray = isStateAbbreviation
|
||||
? option.key.split( ':' )
|
||||
: option.label.split( '—' );
|
||||
|
||||
// No region options in the country
|
||||
if ( countryStateArray.length <= 1 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const state = countryStateArray[ 1 ];
|
||||
// Handle special case, for example: China — Beijing / 北京
|
||||
if ( state.includes( '/' ) ) {
|
||||
const stateStrList = state.split( '/' );
|
||||
return (
|
||||
normalizeState( stateStrList[ 0 ] ) === normalizedAutofillState ||
|
||||
normalizeState( stateStrList[ 1 ] ) === normalizedAutofillState
|
||||
);
|
||||
}
|
||||
|
||||
// Handle special case, for example: Iran — Alborz (البرز)
|
||||
if ( state.includes( '(' ) && state.includes( ')' ) ) {
|
||||
const stateStrList = state.replace( ')', '' ).split( '(' );
|
||||
return (
|
||||
normalizeState( stateStrList[ 0 ] ) === normalizedAutofillState ||
|
||||
normalizeState( stateStrList[ 1 ] ) === normalizedAutofillState
|
||||
);
|
||||
}
|
||||
|
||||
return normalizeState( state ) === normalizedAutofillState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the autofill countryState fields and set value from filtered options.
|
||||
*
|
||||
|
@ -130,91 +188,94 @@ export function getCountryStateOptions() {
|
|||
* @param {Function} setValue Set value of the countryState input.
|
||||
* @return {Object} React component.
|
||||
*/
|
||||
export function useGetCountryStateAutofill( options, countryState, setValue ) {
|
||||
export function useGetCountryStateAutofill(
|
||||
options: Option[],
|
||||
countryState: string,
|
||||
setValue: ( key: string, value: string ) => void
|
||||
): JSX.Element {
|
||||
const [ autofillCountry, setAutofillCountry ] = useState( '' );
|
||||
const [ autofillState, setAutofillState ] = useState( '' );
|
||||
const isAutofillChange: {
|
||||
current: boolean;
|
||||
} = useRef();
|
||||
|
||||
// Sync the autofill fields on first render and the countryState value changes.
|
||||
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 ) {
|
||||
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 );
|
||||
if (
|
||||
newCountry !== autofillCountry ||
|
||||
newState !== autofillState
|
||||
) {
|
||||
setAutofillCountry( newCountry );
|
||||
setAutofillState( newState );
|
||||
}
|
||||
}
|
||||
isAutofillChange.current = false;
|
||||
}, [ countryState ] );
|
||||
// Disable reason: If we include autofillCountry/autofillState in the dependency array, we will have an unnecessary function call because we also update them in this function.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ countryState, options ] );
|
||||
|
||||
// Sync the countryState value the autofill fields changes
|
||||
useEffect( () => {
|
||||
// Skip on first render since we only want to update the value when the autofill fields changes.
|
||||
if ( isAutofillChange.current === undefined ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! autofillCountry && ! autofillState && countryState ) {
|
||||
// Clear form.
|
||||
// Clear form
|
||||
isAutofillChange.current = true;
|
||||
setValue( 'countryState', '' );
|
||||
return;
|
||||
}
|
||||
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 ) =>
|
||||
( autofillCountry.length ? countrySearch : stateSearch ).test(
|
||||
option.label
|
||||
)
|
||||
);
|
||||
}
|
||||
const isCountryAbbreviation = autofillCountry.length < 3;
|
||||
const isStateAbbreviation =
|
||||
autofillState.length < 3 && !! autofillState.match( /^[\w]+$/ );
|
||||
let filteredOptions = [];
|
||||
|
||||
if ( autofillCountry.length && autofillState.length ) {
|
||||
const isStateAbbreviation = autofillState.length < 3;
|
||||
filteredOptions = filteredOptions.filter( ( option ) =>
|
||||
stateSearch.test(
|
||||
( isStateAbbreviation ? option.key : option.label )
|
||||
.replace( '-', '' )
|
||||
.replace( /\s/g, '' )
|
||||
filteredOptions = options.filter( ( option ) =>
|
||||
countrySearch.test(
|
||||
isCountryAbbreviation ? option.key : option.label
|
||||
)
|
||||
);
|
||||
|
||||
const isCountryAbbreviation = autofillCountry.length < 3;
|
||||
// no country matches so use all options for state filter.
|
||||
if ( ! filteredOptions.length ) {
|
||||
filteredOptions = [ ...options ];
|
||||
}
|
||||
if ( filteredOptions.length > 1 ) {
|
||||
let countryKeyOptions = [];
|
||||
countryKeyOptions = filteredOptions.filter( ( option ) =>
|
||||
countrySearch.test(
|
||||
isCountryAbbreviation ? option.key : option.label
|
||||
filteredOptions = filteredOptions.filter(
|
||||
getStateFilter(
|
||||
isStateAbbreviation,
|
||||
normalizeState( autofillState )
|
||||
)
|
||||
);
|
||||
|
||||
if ( countryKeyOptions.length > 0 ) {
|
||||
filteredOptions = countryKeyOptions;
|
||||
}
|
||||
}
|
||||
|
||||
if ( filteredOptions.length > 1 ) {
|
||||
let stateKeyOptions = [];
|
||||
stateKeyOptions = filteredOptions.filter( ( option ) =>
|
||||
stateSearch.test(
|
||||
( isStateAbbreviation ? option.key : option.label )
|
||||
.replace( '-', '' )
|
||||
.replace( /\s/g, '' )
|
||||
)
|
||||
);
|
||||
|
||||
if ( stateKeyOptions.length === 1 ) {
|
||||
filteredOptions = stateKeyOptions;
|
||||
}
|
||||
}
|
||||
} else if ( autofillCountry.length ) {
|
||||
filteredOptions = options.filter( ( option ) =>
|
||||
countrySearch.test(
|
||||
isCountryAbbreviation ? option.key : option.label
|
||||
)
|
||||
);
|
||||
} else if ( autofillState.length ) {
|
||||
filteredOptions = options.filter(
|
||||
getStateFilter(
|
||||
isStateAbbreviation,
|
||||
normalizeState( autofillState )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
filteredOptions.length === 1 &&
|
||||
countryState !== filteredOptions[ 0 ].key
|
||||
|
@ -222,6 +283,8 @@ export function useGetCountryStateAutofill( options, countryState, setValue ) {
|
|||
isAutofillChange.current = true;
|
||||
setValue( 'countryState', filteredOptions[ 0 ].key );
|
||||
}
|
||||
// Disable reason: If we include countryState in the dependency array, we will have an unnecessary function call because we also update it in this function.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ autofillCountry, autofillState, options, setValue ] );
|
||||
|
||||
return (
|
||||
|
@ -234,7 +297,7 @@ export function useGetCountryStateAutofill( options, countryState, setValue ) {
|
|||
name="country"
|
||||
type="text"
|
||||
className="woocommerce-select-control__autofill-input"
|
||||
tabIndex="-1"
|
||||
tabIndex={ -1 }
|
||||
autoComplete="country"
|
||||
/>
|
||||
|
||||
|
@ -244,21 +307,32 @@ export function useGetCountryStateAutofill( options, countryState, setValue ) {
|
|||
name="state"
|
||||
type="text"
|
||||
className="woocommerce-select-control__autofill-input"
|
||||
tabIndex="-1"
|
||||
tabIndex={ -1 }
|
||||
autoComplete="address-level1"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type StoreAddressProps = {
|
||||
// Disable reason: The getInputProps type are not provided by the caller and source.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getInputProps: any;
|
||||
setValue: ( key: string, value: string ) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Store address fields.
|
||||
*
|
||||
* @param {Object} props Props for input components.
|
||||
* @param {Function} props.getInputProps Get input props.
|
||||
* @param {Function} props.setValue Set value of the countryState input.
|
||||
* @return {Object} -
|
||||
*/
|
||||
export function StoreAddress( props ) {
|
||||
const { getInputProps, setValue, onChange } = props;
|
||||
export function StoreAddress( {
|
||||
getInputProps,
|
||||
setValue,
|
||||
}: StoreAddressProps ): JSX.Element {
|
||||
const countryState = getInputProps( 'countryState' ).value;
|
||||
const { locale, hasFinishedResolution } = useSelect( ( select ) => {
|
||||
return {
|
||||
|
@ -274,7 +348,6 @@ export function StoreAddress( props ) {
|
|||
countryState,
|
||||
setValue
|
||||
);
|
||||
|
||||
if ( ! hasFinishedResolution ) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { render, fireEvent } from '@testing-library/react';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useGetCountryStateAutofill } from '../store-address';
|
||||
import { useGetCountryStateAutofill, getStateFilter } from '../store-address';
|
||||
|
||||
const AutofillWrapper = ( { options, value, onChange } ) => {
|
||||
const [ values, setValues ] = useState( { countryState: value || '' } );
|
||||
|
@ -41,7 +41,12 @@ const DEFAULT_OPTIONS = [
|
|||
{ key: 'CA:BC', label: 'Canada — British Columbia' },
|
||||
{ key: 'CA:MB', label: 'Canada — Manitoba' },
|
||||
{ key: 'US:CA', label: 'United States - California' },
|
||||
{ key: 'US:AR', label: 'United States (US) — Arkansas' },
|
||||
{ key: 'US:KS', label: 'United States (US) — Kansas' },
|
||||
{ key: 'CN:CN2', label: 'China — Beijing / 北京' },
|
||||
{ key: 'IR:THR', label: 'Iran — Tehran (تهران)' },
|
||||
];
|
||||
|
||||
describe( 'useGetCountryStateAutofill', () => {
|
||||
it( 'should render a country and state inputs with autoComplete', () => {
|
||||
const { queryAllByRole } = render(
|
||||
|
@ -54,7 +59,23 @@ describe( 'useGetCountryStateAutofill', () => {
|
|||
expect( inputs[ 1 ].autocomplete ).toEqual( 'address-level1' );
|
||||
} );
|
||||
|
||||
it( 'should set autocomplete fields if a value is selected', () => {
|
||||
it( 'should set countryState value if a value is provided', () => {
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<AutofillWrapper
|
||||
options={ [ ...DEFAULT_OPTIONS ] }
|
||||
value="US:KS"
|
||||
onChange={ onChange }
|
||||
/>
|
||||
);
|
||||
|
||||
// check the most recent call values
|
||||
expect( onChange.mock.calls.pop() ).toEqual( [
|
||||
{ countryState: 'US:KS' },
|
||||
] );
|
||||
} );
|
||||
|
||||
it( 'should set autocomplete fields if the countryState is not empty', () => {
|
||||
const { queryAllByRole } = render(
|
||||
<AutofillWrapper options={ [ ...DEFAULT_OPTIONS ] } value="CA:MB" />
|
||||
);
|
||||
|
@ -65,7 +86,7 @@ describe( 'useGetCountryStateAutofill', () => {
|
|||
expect( inputs[ 1 ].value ).toEqual( 'Manitoba' );
|
||||
} );
|
||||
|
||||
it( 'should select region by key if abbreviation is used', () => {
|
||||
it( 'should set countryState if auto complete fields are changed and abbreviation is used', () => {
|
||||
const onChange = jest.fn();
|
||||
const { queryAllByRole } = render(
|
||||
<AutofillWrapper
|
||||
|
@ -81,7 +102,7 @@ describe( 'useGetCountryStateAutofill', () => {
|
|||
expect( onChange ).toHaveBeenCalledWith( { countryState: 'US:CA' } );
|
||||
} );
|
||||
|
||||
it( 'should update the value if the auto complete fields changed', () => {
|
||||
it( 'should set countryState if auto complete fields are changed and abbreviation is not used', () => {
|
||||
const onChange = jest.fn();
|
||||
const { queryAllByRole } = render(
|
||||
<AutofillWrapper
|
||||
|
@ -97,7 +118,7 @@ describe( 'useGetCountryStateAutofill', () => {
|
|||
expect( onChange ).toHaveBeenCalledWith( { countryState: 'CA:BC' } );
|
||||
} );
|
||||
|
||||
it( 'should update the value if the auto complete fields changed and value was already set', () => {
|
||||
it( 'should update the countryState if the auto complete fields changed and countryState was already set', () => {
|
||||
const onChange = jest.fn();
|
||||
const { queryAllByRole } = render(
|
||||
<AutofillWrapper
|
||||
|
@ -116,7 +137,7 @@ describe( 'useGetCountryStateAutofill', () => {
|
|||
expect( onChange ).toHaveBeenCalledWith( { countryState: 'CA:BC' } );
|
||||
} );
|
||||
|
||||
it( 'should update the auto complete inputs when value changed and inputs already set', () => {
|
||||
it( 'should update the auto complete fields when countryState is changed and inputs already set', () => {
|
||||
const onChange = jest.fn();
|
||||
const options = [ ...DEFAULT_OPTIONS ];
|
||||
const { rerender, queryAllByRole } = render(
|
||||
|
@ -140,3 +161,53 @@ describe( 'useGetCountryStateAutofill', () => {
|
|||
expect( inputs[ 1 ].value ).toEqual( '' );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getStateFilter', () => {
|
||||
test.each( [
|
||||
{
|
||||
isStateAbbreviation: false,
|
||||
normalizedAutofillState: 'britishcolumbia',
|
||||
expected: { key: 'CA:BC', label: 'Canada — British Columbia' },
|
||||
},
|
||||
{
|
||||
isStateAbbreviation: true,
|
||||
normalizedAutofillState: 'ks',
|
||||
expected: {
|
||||
key: 'US:KS',
|
||||
label: 'United States (US) — Kansas',
|
||||
},
|
||||
},
|
||||
{
|
||||
isStateAbbreviation: false,
|
||||
normalizedAutofillState: '北京',
|
||||
expected: { key: 'CN:CN2', label: 'China — Beijing / 北京' },
|
||||
},
|
||||
{
|
||||
isStateAbbreviation: false,
|
||||
normalizedAutofillState: 'beijing',
|
||||
expected: { key: 'CN:CN2', label: 'China — Beijing / 北京' },
|
||||
},
|
||||
{
|
||||
isStateAbbreviation: false,
|
||||
normalizedAutofillState: 'تهران',
|
||||
expected: { key: 'IR:THR', label: 'Iran — Tehran (تهران)' },
|
||||
},
|
||||
{
|
||||
isStateAbbreviation: false,
|
||||
normalizedAutofillState: 'tehran',
|
||||
expected: { key: 'IR:THR', label: 'Iran — Tehran (تهران)' },
|
||||
},
|
||||
] )(
|
||||
'should filter state matches with isStateAbbreviation=$isStateAbbreviation and normalizedAutofillState=$normalizedAutofillState',
|
||||
( { isStateAbbreviation, normalizedAutofillState, expected } ) => {
|
||||
expect(
|
||||
DEFAULT_OPTIONS.filter(
|
||||
getStateFilter(
|
||||
isStateAbbreviation,
|
||||
normalizedAutofillState
|
||||
)
|
||||
)
|
||||
).toEqual( [ expected ] );
|
||||
}
|
||||
);
|
||||
} );
|
Loading…
Reference in New Issue