Implement browser autocomplete for checkout address fields (https://github.com/woocommerce/woocommerce-blocks/pull/1755)
* Add autocomplete support for textinput * Add autocomplete fields to forms * Prefix default ids * Hack for autocomplete on custom select components * Restore labels and avoid reset of state * State field autocomplete * Fix calculator autocomplete * Simplify existance of hidden field * move label on autofill preview in chrome * Put back state clearance Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com>
This commit is contained in:
parent
7b53486be3
commit
2a25cfd0ed
|
@ -15,6 +15,7 @@ const CountryInput = ( {
|
||||||
label,
|
label,
|
||||||
onChange,
|
onChange,
|
||||||
value = '',
|
value = '',
|
||||||
|
autoComplete = 'off',
|
||||||
} ) => {
|
} ) => {
|
||||||
const options = Object.keys( countries ).map( ( key ) => ( {
|
const options = Object.keys( countries ).map( ( key ) => ( {
|
||||||
key,
|
key,
|
||||||
|
@ -22,13 +23,36 @@ const CountryInput = ( {
|
||||||
} ) );
|
} ) );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<>
|
||||||
className={ className }
|
<Select
|
||||||
label={ label }
|
className={ className }
|
||||||
onChange={ onChange }
|
label={ label }
|
||||||
options={ options }
|
onChange={ onChange }
|
||||||
value={ options.find( ( option ) => option.key === value ) }
|
options={ options }
|
||||||
/>
|
value={ options.find( ( option ) => option.key === value ) }
|
||||||
|
/>
|
||||||
|
{ autoComplete !== 'off' && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
aria-hidden={ true }
|
||||||
|
autoComplete={ autoComplete }
|
||||||
|
value={ value }
|
||||||
|
onChange={ ( event ) => {
|
||||||
|
const textValue = event.target.value;
|
||||||
|
const foundOption = options.find(
|
||||||
|
( option ) => option.key === textValue
|
||||||
|
);
|
||||||
|
onChange( foundOption ? foundOption.key : '' );
|
||||||
|
} }
|
||||||
|
style={ {
|
||||||
|
height: '0',
|
||||||
|
border: '0',
|
||||||
|
padding: '0',
|
||||||
|
position: 'absolute',
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -38,6 +62,7 @@ CountryInput.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
|
autoComplete: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CountryInput;
|
export default CountryInput;
|
||||||
|
|
|
@ -19,7 +19,7 @@ const ShippingCalculatorAddress = ( { address: initialAddress, onUpdate } ) => {
|
||||||
const [ address, setAddress ] = useState( initialAddress );
|
const [ address, setAddress ] = useState( initialAddress );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="wc-block-shipping-calculator-address">
|
<form className="wc-block-shipping-calculator-address">
|
||||||
<ShippingCountryInput
|
<ShippingCountryInput
|
||||||
className="wc-block-shipping-calculator-address__input"
|
className="wc-block-shipping-calculator-address__input"
|
||||||
label={ __(
|
label={ __(
|
||||||
|
@ -27,6 +27,7 @@ const ShippingCalculatorAddress = ( { address: initialAddress, onUpdate } ) => {
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
value={ address.country }
|
value={ address.country }
|
||||||
|
autoComplete="country"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setAddress( {
|
setAddress( {
|
||||||
...address,
|
...address,
|
||||||
|
@ -40,6 +41,7 @@ const ShippingCalculatorAddress = ( { address: initialAddress, onUpdate } ) => {
|
||||||
country={ address.country }
|
country={ address.country }
|
||||||
label={ __( 'State / County', 'woo-gutenberg-products-block' ) }
|
label={ __( 'State / County', 'woo-gutenberg-products-block' ) }
|
||||||
value={ address.state }
|
value={ address.state }
|
||||||
|
autoComplete="address-level1"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setAddress( {
|
setAddress( {
|
||||||
...address,
|
...address,
|
||||||
|
@ -51,6 +53,7 @@ const ShippingCalculatorAddress = ( { address: initialAddress, onUpdate } ) => {
|
||||||
className="wc-block-shipping-calculator-address__input"
|
className="wc-block-shipping-calculator-address__input"
|
||||||
label={ __( 'City', 'woo-gutenberg-products-block' ) }
|
label={ __( 'City', 'woo-gutenberg-products-block' ) }
|
||||||
value={ address.city }
|
value={ address.city }
|
||||||
|
autoComplete="address-level2"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setAddress( {
|
setAddress( {
|
||||||
...address,
|
...address,
|
||||||
|
@ -62,6 +65,7 @@ const ShippingCalculatorAddress = ( { address: initialAddress, onUpdate } ) => {
|
||||||
className="wc-block-shipping-calculator-address__input"
|
className="wc-block-shipping-calculator-address__input"
|
||||||
label={ __( 'Postal code', 'woo-gutenberg-products-block' ) }
|
label={ __( 'Postal code', 'woo-gutenberg-products-block' ) }
|
||||||
value={ address.postcode }
|
value={ address.postcode }
|
||||||
|
autoComplete="postal-code"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setAddress( {
|
setAddress( {
|
||||||
...address,
|
...address,
|
||||||
|
@ -76,7 +80,7 @@ const ShippingCalculatorAddress = ( { address: initialAddress, onUpdate } ) => {
|
||||||
>
|
>
|
||||||
{ __( 'Update', 'woo-gutenberg-products-block' ) }
|
{ __( 'Update', 'woo-gutenberg-products-block' ) }
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { decodeEntities } from '@wordpress/html-entities';
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
|
import { useCallback } from '@wordpress/element';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -16,32 +17,73 @@ const StateInput = ( {
|
||||||
country,
|
country,
|
||||||
label,
|
label,
|
||||||
onChange,
|
onChange,
|
||||||
|
autoComplete = 'off',
|
||||||
value = '',
|
value = '',
|
||||||
} ) => {
|
} ) => {
|
||||||
const countryCounties = counties[ country ];
|
const countryCounties = counties[ country ];
|
||||||
if ( ! countryCounties || Object.keys( countryCounties ).length === 0 ) {
|
const options =
|
||||||
return (
|
countryCounties && Object.keys( countryCounties ).length > 0
|
||||||
<TextInput
|
? Object.keys( countryCounties ).map( ( key ) => ( {
|
||||||
|
key,
|
||||||
|
name: decodeEntities( countryCounties[ key ] ),
|
||||||
|
} ) )
|
||||||
|
: [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles state selection onChange events. Finds a matching state by key or value.
|
||||||
|
*
|
||||||
|
* @param {Object} event event data.
|
||||||
|
*/
|
||||||
|
const onChangeState = useCallback(
|
||||||
|
( stateValue ) => {
|
||||||
|
if ( options.length > 0 ) {
|
||||||
|
const foundOption = options.find(
|
||||||
|
( option ) =>
|
||||||
|
option.key === stateValue || option.name === stateValue
|
||||||
|
);
|
||||||
|
|
||||||
|
onChange( foundOption ? foundOption.key : '' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChange( stateValue );
|
||||||
|
},
|
||||||
|
[ onChange, options ]
|
||||||
|
);
|
||||||
|
|
||||||
|
return options.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
className={ className }
|
className={ className }
|
||||||
label={ label }
|
label={ label }
|
||||||
onChange={ onChange }
|
onChange={ onChangeState }
|
||||||
value={ value }
|
options={ options }
|
||||||
|
value={ options.find( ( option ) => option.key === value ) }
|
||||||
/>
|
/>
|
||||||
);
|
{ autoComplete !== 'off' && (
|
||||||
}
|
<input
|
||||||
|
type="text"
|
||||||
const options = Object.keys( countryCounties ).map( ( key ) => ( {
|
aria-hidden={ true }
|
||||||
key,
|
autoComplete={ autoComplete }
|
||||||
name: decodeEntities( countryCounties[ key ] ),
|
value={ value }
|
||||||
} ) );
|
onChange={ ( event ) =>
|
||||||
|
onChangeState( event.target.value )
|
||||||
return (
|
}
|
||||||
<Select
|
style={ {
|
||||||
|
height: '0',
|
||||||
|
border: '0',
|
||||||
|
padding: '0',
|
||||||
|
position: 'absolute',
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<TextInput
|
||||||
className={ className }
|
className={ className }
|
||||||
label={ label }
|
label={ label }
|
||||||
onChange={ onChange }
|
onChange={ onChangeState }
|
||||||
options={ options }
|
autoComplete={ autoComplete }
|
||||||
value={ options.find( ( option ) => option.key === value ) }
|
value={ value }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -54,6 +96,7 @@ StateInput.propTypes = {
|
||||||
] )
|
] )
|
||||||
).isRequired,
|
).isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
autoComplete: PropTypes.string,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
country: PropTypes.string,
|
country: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
|
|
|
@ -22,12 +22,13 @@ const TextInput = ( {
|
||||||
screenReaderLabel,
|
screenReaderLabel,
|
||||||
disabled,
|
disabled,
|
||||||
help,
|
help,
|
||||||
|
autoComplete = 'off',
|
||||||
value = '',
|
value = '',
|
||||||
onChange,
|
onChange,
|
||||||
} ) => {
|
} ) => {
|
||||||
const [ isActive, setIsActive ] = useState( false );
|
const [ isActive, setIsActive ] = useState( false );
|
||||||
const onChangeValue = ( event ) => onChange( event.target.value );
|
const onChangeValue = ( event ) => onChange( event.target.value );
|
||||||
const textInputId = id || componentId;
|
const textInputId = id || 'textinput-' + componentId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -35,19 +36,11 @@ const TextInput = ( {
|
||||||
'is-active': isActive || value,
|
'is-active': isActive || value,
|
||||||
} ) }
|
} ) }
|
||||||
>
|
>
|
||||||
<Label
|
|
||||||
label={ label }
|
|
||||||
screenReaderLabel={ screenReaderLabel || label }
|
|
||||||
wrapperElement="label"
|
|
||||||
wrapperProps={ {
|
|
||||||
htmlFor: textInputId,
|
|
||||||
} }
|
|
||||||
htmlFor={ textInputId }
|
|
||||||
/>
|
|
||||||
<input
|
<input
|
||||||
type={ type }
|
type={ type }
|
||||||
id={ textInputId }
|
id={ textInputId }
|
||||||
value={ value }
|
value={ value }
|
||||||
|
autoComplete={ autoComplete }
|
||||||
onChange={ onChangeValue }
|
onChange={ onChangeValue }
|
||||||
onFocus={ () => setIsActive( true ) }
|
onFocus={ () => setIsActive( true ) }
|
||||||
onBlur={ () => setIsActive( false ) }
|
onBlur={ () => setIsActive( false ) }
|
||||||
|
@ -57,6 +50,15 @@ const TextInput = ( {
|
||||||
!! help ? textInputId + '__help' : undefined
|
!! help ? textInputId + '__help' : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Label
|
||||||
|
label={ label }
|
||||||
|
screenReaderLabel={ screenReaderLabel || label }
|
||||||
|
wrapperElement="label"
|
||||||
|
wrapperProps={ {
|
||||||
|
htmlFor: textInputId,
|
||||||
|
} }
|
||||||
|
htmlFor={ textInputId }
|
||||||
|
/>
|
||||||
{ !! help && (
|
{ !! help && (
|
||||||
<p
|
<p
|
||||||
id={ textInputId + '__help' }
|
id={ textInputId + '__help' }
|
||||||
|
@ -78,6 +80,7 @@ TextInput.propTypes = {
|
||||||
screenReaderLabel: PropTypes.string,
|
screenReaderLabel: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
help: PropTypes.string,
|
help: PropTypes.string,
|
||||||
|
autoComplete: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withComponentId( TextInput );
|
export default withComponentId( TextInput );
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
.wc-block-text-input label {
|
.wc-block-text-input label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translateY(#{$gap-small});
|
transform: translateY(#{$gap-small});
|
||||||
|
left: 0;
|
||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
@ -20,7 +21,9 @@
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.wc-block-text-input input:-webkit-autofill + label {
|
||||||
|
transform: translateY(#{$gap-smallest}) scale(0.75);
|
||||||
|
}
|
||||||
.wc-block-text-input.is-active label {
|
.wc-block-text-input.is-active label {
|
||||||
transform: translateY(#{$gap-smallest}) scale(0.75);
|
transform: translateY(#{$gap-smallest}) scale(0.75);
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,7 @@ const Block = ( { shippingMethods = [], isEditor = false } ) => {
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
value={ contactFields.email }
|
value={ contactFields.email }
|
||||||
|
autoComplete="email"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setContactFields( {
|
setContactFields( {
|
||||||
...contactFields,
|
...contactFields,
|
||||||
|
@ -115,6 +116,7 @@ const Block = ( { shippingMethods = [], isEditor = false } ) => {
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
value={ shippingFields.firstName }
|
value={ shippingFields.firstName }
|
||||||
|
autoComplete="given-name"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setShippingFields( {
|
setShippingFields( {
|
||||||
...shippingFields,
|
...shippingFields,
|
||||||
|
@ -128,6 +130,7 @@ const Block = ( { shippingMethods = [], isEditor = false } ) => {
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
value={ shippingFields.lastName }
|
value={ shippingFields.lastName }
|
||||||
|
autoComplete="family-name"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setShippingFields( {
|
setShippingFields( {
|
||||||
...shippingFields,
|
...shippingFields,
|
||||||
|
@ -142,6 +145,7 @@ const Block = ( { shippingMethods = [], isEditor = false } ) => {
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
value={ shippingFields.streetAddress }
|
value={ shippingFields.streetAddress }
|
||||||
|
autoComplete="address-line1"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setShippingFields( {
|
setShippingFields( {
|
||||||
...shippingFields,
|
...shippingFields,
|
||||||
|
@ -155,6 +159,7 @@ const Block = ( { shippingMethods = [], isEditor = false } ) => {
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
value={ shippingFields.apartment }
|
value={ shippingFields.apartment }
|
||||||
|
autoComplete="address-line2"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setShippingFields( {
|
setShippingFields( {
|
||||||
...shippingFields,
|
...shippingFields,
|
||||||
|
@ -169,6 +174,7 @@ const Block = ( { shippingMethods = [], isEditor = false } ) => {
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
value={ shippingFields.country }
|
value={ shippingFields.country }
|
||||||
|
autoComplete="country"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setShippingFields( {
|
setShippingFields( {
|
||||||
...shippingFields,
|
...shippingFields,
|
||||||
|
@ -183,6 +189,7 @@ const Block = ( { shippingMethods = [], isEditor = false } ) => {
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
value={ shippingFields.city }
|
value={ shippingFields.city }
|
||||||
|
autoComplete="address-level2"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setShippingFields( {
|
setShippingFields( {
|
||||||
...shippingFields,
|
...shippingFields,
|
||||||
|
@ -199,6 +206,7 @@ const Block = ( { shippingMethods = [], isEditor = false } ) => {
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
value={ shippingFields.state }
|
value={ shippingFields.state }
|
||||||
|
autoComplete="address-level1"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setShippingFields( {
|
setShippingFields( {
|
||||||
...shippingFields,
|
...shippingFields,
|
||||||
|
@ -212,6 +220,7 @@ const Block = ( { shippingMethods = [], isEditor = false } ) => {
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
value={ shippingFields.postcode }
|
value={ shippingFields.postcode }
|
||||||
|
autoComplete="postal-code"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setShippingFields( {
|
setShippingFields( {
|
||||||
...shippingFields,
|
...shippingFields,
|
||||||
|
@ -224,6 +233,7 @@ const Block = ( { shippingMethods = [], isEditor = false } ) => {
|
||||||
type="tel"
|
type="tel"
|
||||||
label={ __( 'Phone', 'woo-gutenberg-products-block' ) }
|
label={ __( 'Phone', 'woo-gutenberg-products-block' ) }
|
||||||
value={ shippingFields.phone }
|
value={ shippingFields.phone }
|
||||||
|
autoComplete="tel"
|
||||||
onChange={ ( newValue ) =>
|
onChange={ ( newValue ) =>
|
||||||
setShippingFields( {
|
setShippingFields( {
|
||||||
...shippingFields,
|
...shippingFields,
|
||||||
|
|
Loading…
Reference in New Issue