This repository has been archived by the owner on Feb 23, 2024. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Switch from Select to Combobox for Country and State Inputs (#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 WordPress/gutenberg#33928 * patch package after install * update package * Prevent autofill handling impacting manual input * Add todo * combo requires option to be selected
- Loading branch information
1 parent
ec54e9b
commit 56476e1
Showing
14 changed files
with
787 additions
and
357 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.