diff --git a/projects/packages/forms/changelog/add-forms-fancy-country-dropdown b/projects/packages/forms/changelog/add-forms-fancy-country-dropdown new file mode 100644 index 0000000000000..8e60351e60572 --- /dev/null +++ b/projects/packages/forms/changelog/add-forms-fancy-country-dropdown @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Forms: add searchable country selector dropdown (combobox) diff --git a/projects/packages/forms/src/blocks/contact-form/class-contact-form-block.php b/projects/packages/forms/src/blocks/contact-form/class-contact-form-block.php index 351b66ca93a4d..0db0a12b4659d 100644 --- a/projects/packages/forms/src/blocks/contact-form/class-contact-form-block.php +++ b/projects/packages/forms/src/blocks/contact-form/class-contact-form-block.php @@ -260,6 +260,7 @@ public static function register_child_blocks() { 'jetpack/field-prefix-options', 'jetpack/field-prefix-default', 'jetpack/field-prefix-onChange', + 'jetpack/field-phone-country-toggle', ), ) ); diff --git a/projects/packages/forms/src/blocks/contact-form/editor.scss b/projects/packages/forms/src/blocks/contact-form/editor.scss index bea235171cbb8..b3614ca57818d 100644 --- a/projects/packages/forms/src/blocks/contact-form/editor.scss +++ b/projects/packages/forms/src/blocks/contact-form/editor.scss @@ -955,7 +955,7 @@ .jetpack-field__input, .jetpack-field__textarea, .jetpack-field-dropdown__toggle { - padding-top: 1.4em; + padding-top: max(var(--jetpack--contact-form--input-padding-top, 0), 1.4em); padding-left: var(--jetpack--contact-form--animated-left-offset); } diff --git a/projects/packages/forms/src/blocks/contact-form/util/form-styles.js b/projects/packages/forms/src/blocks/contact-form/util/form-styles.js index 385434db3f7d6..525f4bda54490 100644 --- a/projects/packages/forms/src/blocks/contact-form/util/form-styles.js +++ b/projects/packages/forms/src/blocks/contact-form/util/form-styles.js @@ -152,7 +152,7 @@ window.jetpackForms.generateStyleVariables = function ( formNode ) { '--jetpack--contact-form--input-height': inputHeight, '--jetpack--contact-form--font-size': fontSize, '--jetpack--contact-form--font-family': fontFamily, - '--jetpack--contact-form--line-height': lineHeight, + '--jetpack--contact-form--line-height': lineHeight === 'normal' ? '1.2em' : lineHeight, '--jetpack--contact-form--button-primary--color': buttonPrimaryColor, '--jetpack--contact-form--button-primary--background-color': buttonPrimaryBackgroundColor, '--jetpack--contact-form--button-primary--border': buttonPrimaryBorder, diff --git a/projects/packages/forms/src/blocks/field-telephone/country-names-translated.js b/projects/packages/forms/src/blocks/field-telephone/country-names-translated.js new file mode 100644 index 0000000000000..df579b76906c6 --- /dev/null +++ b/projects/packages/forms/src/blocks/field-telephone/country-names-translated.js @@ -0,0 +1,257 @@ +import { __ } from '@wordpress/i18n'; + +/** + * Translated country names for internationalization. + * This provides human-readable, translatable country names + * that can be used alongside the country-list.js data. + */ +export const translatedCountryNames = { + AF: __( 'Afghanistan', 'jetpack-forms' ), + AL: __( 'Albania', 'jetpack-forms' ), + DZ: __( 'Algeria', 'jetpack-forms' ), + AS: __( 'American Samoa', 'jetpack-forms' ), + AD: __( 'Andorra', 'jetpack-forms' ), + AO: __( 'Angola', 'jetpack-forms' ), + AI: __( 'Anguilla', 'jetpack-forms' ), + AG: __( 'Antigua and Barbuda', 'jetpack-forms' ), + AR: __( 'Argentina', 'jetpack-forms' ), + AM: __( 'Armenia', 'jetpack-forms' ), + AW: __( 'Aruba', 'jetpack-forms' ), + AU: __( 'Australia', 'jetpack-forms' ), + AT: __( 'Austria', 'jetpack-forms' ), + AZ: __( 'Azerbaijan', 'jetpack-forms' ), + BS: __( 'Bahamas', 'jetpack-forms' ), + BH: __( 'Bahrain', 'jetpack-forms' ), + BD: __( 'Bangladesh', 'jetpack-forms' ), + BB: __( 'Barbados', 'jetpack-forms' ), + BY: __( 'Belarus', 'jetpack-forms' ), + BE: __( 'Belgium', 'jetpack-forms' ), + BZ: __( 'Belize', 'jetpack-forms' ), + BJ: __( 'Benin', 'jetpack-forms' ), + BM: __( 'Bermuda', 'jetpack-forms' ), + BT: __( 'Bhutan', 'jetpack-forms' ), + BO: __( 'Bolivia', 'jetpack-forms' ), + BA: __( 'Bosnia and Herzegovina', 'jetpack-forms' ), + BW: __( 'Botswana', 'jetpack-forms' ), + BR: __( 'Brazil', 'jetpack-forms' ), + IO: __( 'British Indian Ocean Territory', 'jetpack-forms' ), + VG: __( 'British Virgin Islands', 'jetpack-forms' ), + BN: __( 'Brunei', 'jetpack-forms' ), + BG: __( 'Bulgaria', 'jetpack-forms' ), + BF: __( 'Burkina Faso', 'jetpack-forms' ), + BI: __( 'Burundi', 'jetpack-forms' ), + KH: __( 'Cambodia', 'jetpack-forms' ), + CM: __( 'Cameroon', 'jetpack-forms' ), + CA: __( 'Canada', 'jetpack-forms' ), + CV: __( 'Cape Verde', 'jetpack-forms' ), + KY: __( 'Cayman Islands', 'jetpack-forms' ), + CF: __( 'Central African Republic', 'jetpack-forms' ), + TD: __( 'Chad', 'jetpack-forms' ), + CL: __( 'Chile', 'jetpack-forms' ), + CN: __( 'China', 'jetpack-forms' ), + CX: __( 'Christmas Island', 'jetpack-forms' ), + CC: __( 'Cocos (Keeling) Islands', 'jetpack-forms' ), + CO: __( 'Colombia', 'jetpack-forms' ), + KM: __( 'Comoros', 'jetpack-forms' ), + CG: __( 'Congo - Brazzaville', 'jetpack-forms' ), + CD: __( 'Congo - Kinshasa', 'jetpack-forms' ), + CK: __( 'Cook Islands', 'jetpack-forms' ), + CR: __( 'Costa Rica', 'jetpack-forms' ), + HR: __( 'Croatia', 'jetpack-forms' ), + CU: __( 'Cuba', 'jetpack-forms' ), + CY: __( 'Cyprus', 'jetpack-forms' ), + CZ: __( 'Czech Republic', 'jetpack-forms' ), + CI: __( "Côte d'Ivoire", 'jetpack-forms' ), + DK: __( 'Denmark', 'jetpack-forms' ), + DJ: __( 'Djibouti', 'jetpack-forms' ), + DM: __( 'Dominica', 'jetpack-forms' ), + DO: __( 'Dominican Republic', 'jetpack-forms' ), + EC: __( 'Ecuador', 'jetpack-forms' ), + EG: __( 'Egypt', 'jetpack-forms' ), + SV: __( 'El Salvador', 'jetpack-forms' ), + GQ: __( 'Equatorial Guinea', 'jetpack-forms' ), + ER: __( 'Eritrea', 'jetpack-forms' ), + EE: __( 'Estonia', 'jetpack-forms' ), + SZ: __( 'Eswatini', 'jetpack-forms' ), + ET: __( 'Ethiopia', 'jetpack-forms' ), + FK: __( 'Falkland Islands', 'jetpack-forms' ), + FO: __( 'Faroe Islands', 'jetpack-forms' ), + FJ: __( 'Fiji', 'jetpack-forms' ), + FI: __( 'Finland', 'jetpack-forms' ), + FR: __( 'France', 'jetpack-forms' ), + GF: __( 'French Guiana', 'jetpack-forms' ), + PF: __( 'French Polynesia', 'jetpack-forms' ), + GA: __( 'Gabon', 'jetpack-forms' ), + GM: __( 'Gambia', 'jetpack-forms' ), + GE: __( 'Georgia', 'jetpack-forms' ), + DE: __( 'Germany', 'jetpack-forms' ), + GH: __( 'Ghana', 'jetpack-forms' ), + GI: __( 'Gibraltar', 'jetpack-forms' ), + GR: __( 'Greece', 'jetpack-forms' ), + GL: __( 'Greenland', 'jetpack-forms' ), + GD: __( 'Grenada', 'jetpack-forms' ), + GP: __( 'Guadeloupe', 'jetpack-forms' ), + GU: __( 'Guam', 'jetpack-forms' ), + GT: __( 'Guatemala', 'jetpack-forms' ), + GG: __( 'Guernsey', 'jetpack-forms' ), + GN: __( 'Guinea', 'jetpack-forms' ), + GW: __( 'Guinea-Bissau', 'jetpack-forms' ), + GY: __( 'Guyana', 'jetpack-forms' ), + HT: __( 'Haiti', 'jetpack-forms' ), + HN: __( 'Honduras', 'jetpack-forms' ), + HK: __( 'Hong Kong', 'jetpack-forms' ), + HU: __( 'Hungary', 'jetpack-forms' ), + IS: __( 'Iceland', 'jetpack-forms' ), + IN: __( 'India', 'jetpack-forms' ), + ID: __( 'Indonesia', 'jetpack-forms' ), + IR: __( 'Iran', 'jetpack-forms' ), + IQ: __( 'Iraq', 'jetpack-forms' ), + IE: __( 'Ireland', 'jetpack-forms' ), + IM: __( 'Isle of Man', 'jetpack-forms' ), + IL: __( 'Israel', 'jetpack-forms' ), + IT: __( 'Italy', 'jetpack-forms' ), + JM: __( 'Jamaica', 'jetpack-forms' ), + JP: __( 'Japan', 'jetpack-forms' ), + JE: __( 'Jersey', 'jetpack-forms' ), + JO: __( 'Jordan', 'jetpack-forms' ), + KZ: __( 'Kazakhstan', 'jetpack-forms' ), + KE: __( 'Kenya', 'jetpack-forms' ), + KI: __( 'Kiribati', 'jetpack-forms' ), + XK: __( 'Kosovo', 'jetpack-forms' ), + KW: __( 'Kuwait', 'jetpack-forms' ), + KG: __( 'Kyrgyzstan', 'jetpack-forms' ), + LA: __( 'Laos', 'jetpack-forms' ), + LV: __( 'Latvia', 'jetpack-forms' ), + LB: __( 'Lebanon', 'jetpack-forms' ), + LS: __( 'Lesotho', 'jetpack-forms' ), + LR: __( 'Liberia', 'jetpack-forms' ), + LY: __( 'Libya', 'jetpack-forms' ), + LI: __( 'Liechtenstein', 'jetpack-forms' ), + LT: __( 'Lithuania', 'jetpack-forms' ), + LU: __( 'Luxembourg', 'jetpack-forms' ), + MO: __( 'Macao', 'jetpack-forms' ), + MG: __( 'Madagascar', 'jetpack-forms' ), + MW: __( 'Malawi', 'jetpack-forms' ), + MY: __( 'Malaysia', 'jetpack-forms' ), + MV: __( 'Maldives', 'jetpack-forms' ), + ML: __( 'Mali', 'jetpack-forms' ), + MT: __( 'Malta', 'jetpack-forms' ), + MH: __( 'Marshall Islands', 'jetpack-forms' ), + MQ: __( 'Martinique', 'jetpack-forms' ), + MR: __( 'Mauritania', 'jetpack-forms' ), + MU: __( 'Mauritius', 'jetpack-forms' ), + YT: __( 'Mayotte', 'jetpack-forms' ), + MX: __( 'Mexico', 'jetpack-forms' ), + FM: __( 'Micronesia', 'jetpack-forms' ), + MD: __( 'Moldova', 'jetpack-forms' ), + MC: __( 'Monaco', 'jetpack-forms' ), + MN: __( 'Mongolia', 'jetpack-forms' ), + ME: __( 'Montenegro', 'jetpack-forms' ), + MS: __( 'Montserrat', 'jetpack-forms' ), + MA: __( 'Morocco', 'jetpack-forms' ), + MZ: __( 'Mozambique', 'jetpack-forms' ), + MM: __( 'Myanmar', 'jetpack-forms' ), + NA: __( 'Namibia', 'jetpack-forms' ), + NR: __( 'Nauru', 'jetpack-forms' ), + NP: __( 'Nepal', 'jetpack-forms' ), + NL: __( 'Netherlands', 'jetpack-forms' ), + NC: __( 'New Caledonia', 'jetpack-forms' ), + NZ: __( 'New Zealand', 'jetpack-forms' ), + NI: __( 'Nicaragua', 'jetpack-forms' ), + NE: __( 'Niger', 'jetpack-forms' ), + NG: __( 'Nigeria', 'jetpack-forms' ), + NU: __( 'Niue', 'jetpack-forms' ), + NF: __( 'Norfolk Island', 'jetpack-forms' ), + KP: __( 'North Korea', 'jetpack-forms' ), + MK: __( 'North Macedonia', 'jetpack-forms' ), + MP: __( 'Northern Mariana Islands', 'jetpack-forms' ), + NO: __( 'Norway', 'jetpack-forms' ), + OM: __( 'Oman', 'jetpack-forms' ), + PK: __( 'Pakistan', 'jetpack-forms' ), + PW: __( 'Palau', 'jetpack-forms' ), + PS: __( 'Palestine', 'jetpack-forms' ), + PA: __( 'Panama', 'jetpack-forms' ), + PG: __( 'Papua New Guinea', 'jetpack-forms' ), + PY: __( 'Paraguay', 'jetpack-forms' ), + PE: __( 'Peru', 'jetpack-forms' ), + PH: __( 'Philippines', 'jetpack-forms' ), + PN: __( 'Pitcairn Islands', 'jetpack-forms' ), + PL: __( 'Poland', 'jetpack-forms' ), + PT: __( 'Portugal', 'jetpack-forms' ), + PR: __( 'Puerto Rico', 'jetpack-forms' ), + QA: __( 'Qatar', 'jetpack-forms' ), + RO: __( 'Romania', 'jetpack-forms' ), + RU: __( 'Russia', 'jetpack-forms' ), + RW: __( 'Rwanda', 'jetpack-forms' ), + RE: __( 'Réunion', 'jetpack-forms' ), + BL: __( 'Saint Barthélemy', 'jetpack-forms' ), + SH: __( 'Saint Helena', 'jetpack-forms' ), + KN: __( 'Saint Kitts and Nevis', 'jetpack-forms' ), + LC: __( 'Saint Lucia', 'jetpack-forms' ), + MF: __( 'Saint Martin', 'jetpack-forms' ), + PM: __( 'Saint Pierre and Miquelon', 'jetpack-forms' ), + VC: __( 'Saint Vincent and the Grenadines', 'jetpack-forms' ), + WS: __( 'Samoa', 'jetpack-forms' ), + SM: __( 'San Marino', 'jetpack-forms' ), + SA: __( 'Saudi Arabia', 'jetpack-forms' ), + SN: __( 'Senegal', 'jetpack-forms' ), + RS: __( 'Serbia', 'jetpack-forms' ), + SC: __( 'Seychelles', 'jetpack-forms' ), + SL: __( 'Sierra Leone', 'jetpack-forms' ), + SG: __( 'Singapore', 'jetpack-forms' ), + SK: __( 'Slovakia', 'jetpack-forms' ), + SI: __( 'Slovenia', 'jetpack-forms' ), + SB: __( 'Solomon Islands', 'jetpack-forms' ), + SO: __( 'Somalia', 'jetpack-forms' ), + ZA: __( 'South Africa', 'jetpack-forms' ), + GS: __( 'South Georgia and the South Sandwich Islands', 'jetpack-forms' ), + KR: __( 'South Korea', 'jetpack-forms' ), + ES: __( 'Spain', 'jetpack-forms' ), + LK: __( 'Sri Lanka', 'jetpack-forms' ), + SD: __( 'Sudan', 'jetpack-forms' ), + SR: __( 'Suriname', 'jetpack-forms' ), + SJ: __( 'Svalbard and Jan Mayen', 'jetpack-forms' ), + SE: __( 'Sweden', 'jetpack-forms' ), + CH: __( 'Switzerland', 'jetpack-forms' ), + SY: __( 'Syria', 'jetpack-forms' ), + ST: __( 'São Tomé and Príncipe', 'jetpack-forms' ), + TW: __( 'Taiwan', 'jetpack-forms' ), + TJ: __( 'Tajikistan', 'jetpack-forms' ), + TZ: __( 'Tanzania', 'jetpack-forms' ), + TH: __( 'Thailand', 'jetpack-forms' ), + TL: __( 'Timor-Leste', 'jetpack-forms' ), + TG: __( 'Togo', 'jetpack-forms' ), + TK: __( 'Tokelau', 'jetpack-forms' ), + TO: __( 'Tonga', 'jetpack-forms' ), + TT: __( 'Trinidad and Tobago', 'jetpack-forms' ), + TN: __( 'Tunisia', 'jetpack-forms' ), + TR: __( 'Turkey', 'jetpack-forms' ), + TM: __( 'Turkmenistan', 'jetpack-forms' ), + TC: __( 'Turks and Caicos Islands', 'jetpack-forms' ), + TV: __( 'Tuvalu', 'jetpack-forms' ), + VI: __( 'U.S. Virgin Islands', 'jetpack-forms' ), + UG: __( 'Uganda', 'jetpack-forms' ), + UA: __( 'Ukraine', 'jetpack-forms' ), + AE: __( 'United Arab Emirates', 'jetpack-forms' ), + GB: __( 'United Kingdom', 'jetpack-forms' ), + US: __( 'United States', 'jetpack-forms' ), + UY: __( 'Uruguay', 'jetpack-forms' ), + UZ: __( 'Uzbekistan', 'jetpack-forms' ), + VU: __( 'Vanuatu', 'jetpack-forms' ), + VA: __( 'Vatican City', 'jetpack-forms' ), + VE: __( 'Venezuela', 'jetpack-forms' ), + VN: __( 'Vietnam', 'jetpack-forms' ), + WF: __( 'Wallis and Futuna', 'jetpack-forms' ), + YE: __( 'Yemen', 'jetpack-forms' ), + ZM: __( 'Zambia', 'jetpack-forms' ), + ZW: __( 'Zimbabwe', 'jetpack-forms' ), +}; + +/** + * Helper function to get translated country name by country code + * @param {string} countryCode - Two-letter country code (e.g., 'US', 'CA') + * @return {string} Translated country name or the original code if not found + */ +export const getTranslatedCountryName = countryCode => { + return translatedCountryNames[ countryCode ] || countryCode; +}; diff --git a/projects/packages/forms/src/blocks/field-telephone/edit.js b/projects/packages/forms/src/blocks/field-telephone/edit.js index 18c60ef25ac5d..91137aec1d621 100644 --- a/projects/packages/forms/src/blocks/field-telephone/edit.js +++ b/projects/packages/forms/src/blocks/field-telephone/edit.js @@ -3,19 +3,26 @@ import { useBlockProps, useInnerBlocksProps, BlockContextProvider, + BlockControls, } from '@wordpress/block-editor'; -import { PanelBody, ToggleControl } from '@wordpress/components'; +import { PanelBody, ToggleControl, ToolbarButton, ToolbarGroup } from '@wordpress/components'; import { useCallback, useEffect, useMemo, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { globe } from '@wordpress/icons'; import clsx from 'clsx'; import JetpackFieldControls from '../shared/components/jetpack-field-controls'; import useFieldSelected from '../shared/hooks/use-field-selected'; import useFormWrapper from '../shared/hooks/use-form-wrapper'; import useJetpackFieldStyles from '../shared/hooks/use-jetpack-field-styles'; import { countries } from './country-list'; +import { getTranslatedCountryName } from './country-names-translated'; const EMPTY_ARRAY = []; +const isBoolean = value => { + return value === true || value === false; +}; + export default function PhoneFieldEdit( props ) { const { setAttributes, attributes, clientId, isSelected } = props; const { @@ -32,16 +39,11 @@ export default function PhoneFieldEdit( props ) { const { isInnerBlockSelected, hasPlaceholder } = useFieldSelected( clientId ); const { blockStyle } = useJetpackFieldStyles( attributes ); const blockProps = useBlockProps( { - className: clsx( - 'jetpack-field', - 'jetpack-field-phone', - 'jetpack-field-telephone', - width ? ` jetpack-field__width-${ width }` : '', - { - 'is-selected': isSelected || isInnerBlockSelected, - 'has-placeholder': hasPlaceholder, - } - ), + className: clsx( 'jetpack-field', 'jetpack-field-phone', 'jetpack-field-telephone', { + [ `jetpack-field__width-${ width }` ]: width, + 'is-selected': isSelected || isInnerBlockSelected, + 'has-placeholder': hasPlaceholder, + } ), style: blockStyle, } ); @@ -49,12 +51,16 @@ export default function PhoneFieldEdit( props ) { const countryPairs = useMemo( () => { return countries.map( country => ( { - label: country.country + ' ' + country.label, - value: country.code, + ...country, + country: getTranslatedCountryName( country.code ), } ) ); }, [] ); const onChangeShowCountrySelector = value => { + if ( ! isBoolean( value ) ) { + // if not a boolean (ie, event object), toggle the value + value = ! showCountrySelector; + } setAttributes( { showCountrySelector: value, } ); @@ -89,8 +95,7 @@ export default function PhoneFieldEdit( props ) { // Handler is provided as context from edit as index.js can't pass it as a prop. const onChangeDefaultCountry = useCallback( event => { - const value = event.target.value; - setAttributes( { default: value } ); + setAttributes( { default: event.target.value } ); }, [ setAttributes ] ); @@ -106,6 +111,17 @@ export default function PhoneFieldEdit( props ) {
+ + + + + + { const { 'jetpack/field-share-attributes': isSynced } = context; + const [ comboboxOpen, setComboboxOpen ] = useState( false ); + const inputRef = useRef( null ); useSyncedAttributes( 'jetpack/input', @@ -45,36 +50,58 @@ const PhoneInputEdit = ( { attributes, clientId, isSelected, name, setAttributes ); // Prefix/Country selector - const prefixOptions = context?.[ 'jetpack/field-prefix-options' ] || []; const defaultPrefix = context?.[ 'jetpack/field-prefix-default' ] || 'US'; - const onChangeDefaultPrefix = context?.[ 'jetpack/field-prefix-onChange' ] || ( () => {} ); const showCountrySelector = context?.[ 'jetpack/field-phone-country-toggle' ] || false; + const handleChangeDefaultPrefix = useCallback( + event => { + const onChangeDefaultPrefix = context?.[ 'jetpack/field-prefix-onChange' ] || ( () => {} ); + onChangeDefaultPrefix( event ); + setComboboxOpen( false ); + // Focus on the input element after closing the combobox + setTimeout( () => { + if ( inputRef.current ) { + inputRef.current.focus(); + } + }, 0 ); + }, + [ context, setComboboxOpen ] + ); + + // ensure the combobox is closed when the block is not selected + useEffect( () => { + if ( isSelected ) { + return; + } + setComboboxOpen( isSelected ); + }, [ isSelected ] ); + + const countries = useMemo( () => { + return context?.[ 'jetpack/field-prefix-options' ] || []; + }, [ context ] ); + return ( <>
- { showCountrySelector && prefixOptions.length === 1 && ( -
{ prefixOptions[ 0 ].label }
- ) } - { showCountrySelector && prefixOptions.length > 1 && ( + { showCountrySelector && countries.length > 1 && defaultPrefix && (
- +
) } setComboboxOpen( false ) } type="text" value={ isSelected ? placeholder : '' } placeholder={ placeholder } diff --git a/projects/packages/forms/src/blocks/input-phone/editor.scss b/projects/packages/forms/src/blocks/input-phone/editor.scss index 177c9ea48eab7..99d503e25ef84 100644 --- a/projects/packages/forms/src/blocks/input-phone/editor.scss +++ b/projects/packages/forms/src/blocks/input-phone/editor.scss @@ -4,7 +4,7 @@ .jetpack-field__input, .jetpack-field__textarea { box-sizing: border-box; - border-color: var(--jetpack--contact-form--border-color); + border-color: var(--jetpack--contact-form--border-color, currentColor); border-radius: var(--jetpack--contact-form--border-radius); border-width: var(--jetpack--contact-form--border-size); font-size: var(--jetpack--contact-form--font-size); @@ -68,6 +68,8 @@ color: inherit; display: flex; max-width: 40%; + background-color: inherit; + align-items: center; // these class is used to style select elements .jetpack-field__input-element { @@ -86,7 +88,7 @@ &:where(select) { // compensate spacing for downarrow // TODO: misbehaves with :focus, fixed on next version of Gutenberg - padding-right: 24px; + padding-right: 20px; } &:focus { @@ -100,6 +102,10 @@ .is-style-animated .jetpack-contact-form .jetpack-field.jetpack-field-phone, .is-style-animated .jetpack-contact-form .jetpack-field.jetpack-field-telephone { + .jetpack-field__input { + padding-top: max(var(--jetpack--contact-form--input-padding-top, 0), 1.4em); + } + &:not(.has-placeholder):not(.is-selected), &:not(.has-placeholder):not(.has-child-selected) { @@ -120,6 +126,13 @@ .is-style-outlined .jetpack-contact-form .jetpack-field.jetpack-field-phone, .is-style-outlined .jetpack-contact-form .jetpack-field.jetpack-field-telephone { + .jetpack-field__input { + + &.is-selected { + box-shadow: unset; + } + } + &:not(.has-placeholder):not(.is-selected), &:not(.has-placeholder):not(.has-child-selected) { diff --git a/projects/packages/forms/src/blocks/input-phone/index.js b/projects/packages/forms/src/blocks/input-phone/index.js index 91eea00298918..f3f91e37f6244 100644 --- a/projects/packages/forms/src/blocks/input-phone/index.js +++ b/projects/packages/forms/src/blocks/input-phone/index.js @@ -7,11 +7,8 @@ import save from './save'; const name = 'phone-input'; const settings = { apiVersion: 3, - title: __( 'Phone Input', 'jetpack-forms' ), - description: __( - 'A compound input for phone numbers with international support.', - 'jetpack-forms' - ), + title: __( 'Phone input', 'jetpack-forms' ), + description: __( 'Phone number input.', 'jetpack-forms' ), category: 'contact-form', icon: { src: renderMaterialIcon( diff --git a/projects/packages/forms/src/blocks/shared/components/searchable-combobox.js b/projects/packages/forms/src/blocks/shared/components/searchable-combobox.js new file mode 100644 index 0000000000000..951b3eb9e2db6 --- /dev/null +++ b/projects/packages/forms/src/blocks/shared/components/searchable-combobox.js @@ -0,0 +1,334 @@ +import { KeyboardShortcuts, SVG, Path } from '@wordpress/components'; +import { useCallback, useRef, useState, useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import clsx from 'clsx'; + +const downArrowIcon = () => { + return ( + + + + ); +}; +/** + * Combobox Component + * + * A reusable combobox component for selecting options with search functionality. + * Handles its own state management for open/closed state, filtering, and selection. + * Uses WordPress KeyboardShortcuts component for proper block editor keyboard event handling. + * Automatically scrolls to the selected option when the combobox opens. + * + * @param {object} props - The component props + * @param {Array} props.options - Array of options objects with code, country, flag, and value properties + * @param {string} props.selectedOptionCode - The currently selected option code + * @param {Function} props.onOptionChange - Callback function called when an option is selected + * @param {boolean} props.isOpen - External control for combobox open state + * @param {Function} props.onOpenChange - Callback function for open state changes + * @param {string} props.className - Additional CSS class names + * @param {boolean} props.disabled - Whether the combobox is disabled + * @param {string} props.placeholder - The placeholder text for the search input + * @param {object} props.parentStyle - The parent style + * @return {Element|null} The SearchableCombobox component or null if no options/selectedOption + */ +const SearchableCombobox = ( { + options = [], + selectedOptionCode = null, + onOptionChange, + isOpen: externalIsOpen = false, + onOpenChange, + className = '', + disabled = false, + placeholder = __( 'Search…', 'jetpack-forms' ), + parentStyle = {}, +} ) => { + const [ internalIsOpen, setInternalIsOpen ] = useState( false ); + const [ filteredOptions, setFilteredOptions ] = useState( [] ); + const [ searchTerm, setSearchTerm ] = useState( '' ); + const [ selectedOption, setSelectedOption ] = useState( null ); + const [ focusedOptionIndex, setFocusedOptionIndex ] = useState( -1 ); + const searchInputRef = useRef( null ); + const optionsRef = useRef( [] ); + + // Use external open state if provided, otherwise use internal state + const isOpen = onOpenChange ? externalIsOpen : internalIsOpen; + const setIsOpen = onOpenChange || setInternalIsOpen; + + // Initialize filtered options when options change + useEffect( () => { + setFilteredOptions( options ); + }, [ options ] ); + + // Update selected option when selectedOptionCode or options change + useEffect( () => { + if ( ! selectedOptionCode || ! options.length ) { + setSelectedOption( null ); + return; + } + + const option = options.find( opt => opt.code === selectedOptionCode ); + setSelectedOption( option || null ); + }, [ selectedOptionCode, options ] ); + + // Filter options based on search term + useEffect( () => { + if ( ! searchTerm ) { + setFilteredOptions( options ); + return; + } + + const filtered = options.filter( + option => + option.country.toLowerCase().includes( searchTerm.toLowerCase() ) || + option.value.toLowerCase().includes( searchTerm.toLowerCase() ) || + option.code.toLowerCase().includes( searchTerm.toLowerCase() ) + ); + setFilteredOptions( filtered ); + }, [ searchTerm, options ] ); + + // Focus search input when combobox opens + useEffect( () => { + if ( isOpen && searchInputRef.current ) { + setTimeout( () => { + searchInputRef.current.focus(); + }, 0 ); + } + }, [ isOpen ] ); + + // Clear search term and reset focus when combobox closes + useEffect( () => { + if ( ! isOpen ) { + setSearchTerm( '' ); + setFocusedOptionIndex( -1 ); + } + }, [ isOpen ] ); + + // Reset focused option when filtered options change + useEffect( () => { + setFocusedOptionIndex( -1 ); + }, [ filteredOptions ] ); + + // Scroll focused option into view + useEffect( () => { + if ( focusedOptionIndex >= 0 && optionsRef.current[ focusedOptionIndex ] ) { + optionsRef.current[ focusedOptionIndex ].scrollIntoView( { + block: 'nearest', + behavior: 'auto', + } ); + } + }, [ focusedOptionIndex ] ); + + // Scroll to selected option when combobox opens + useEffect( () => { + if ( isOpen && selectedOption && filteredOptions.length > 0 ) { + // Find the index of the selected option in the filtered options + const selectedIndex = filteredOptions.findIndex( + option => option.code === selectedOption.code + ); + + // Scroll to the selected option if found + if ( selectedIndex >= 0 && optionsRef.current[ selectedIndex ] ) { + // Use setTimeout to ensure the DOM has been rendered + setTimeout( () => { + optionsRef.current[ selectedIndex ].scrollIntoView( { + block: 'nearest', + container: 'nearest', + behavior: 'instant', + } ); + }, 0 ); + } + } + }, [ isOpen, selectedOption, filteredOptions ] ); + + const handleToggle = useCallback( () => { + if ( disabled ) { + return; + } + setIsOpen( ! isOpen ); + }, [ isOpen, setIsOpen, disabled ] ); + + const handleOptionSelect = useCallback( + event => { + if ( onOptionChange ) { + onOptionChange( event ); + } + setIsOpen( false ); + }, + [ onOptionChange, setIsOpen ] + ); + + const handleSearchChange = useCallback( event => { + setSearchTerm( event.target.value ); + }, [] ); + + // Keyboard shortcut handlers - using consistent function references as required by KeyboardShortcuts + const handleArrowDown = useCallback( + event => { + if ( ! isOpen ) { + return; + } + event.preventDefault(); + setFocusedOptionIndex( prevIndex => { + const nextIndex = prevIndex < filteredOptions.length - 1 ? prevIndex + 1 : 0; + return nextIndex; + } ); + }, + [ isOpen, filteredOptions.length, setFocusedOptionIndex ] + ); + + const handleArrowUp = useCallback( + event => { + if ( ! isOpen ) { + return; + } + event.preventDefault(); + setFocusedOptionIndex( prevIndex => { + const nextIndex = prevIndex > 0 ? prevIndex - 1 : filteredOptions.length - 1; + return nextIndex; + } ); + }, + [ isOpen, filteredOptions.length, setFocusedOptionIndex ] + ); + + const handleEnter = useCallback( + event => { + if ( ! isOpen ) { + return; + } + event.preventDefault(); + let focusedOption = null; + if ( focusedOptionIndex >= 0 && focusedOptionIndex < filteredOptions.length ) { + focusedOption = filteredOptions[ focusedOptionIndex ]; + } else if ( filteredOptions.length > 0 && searchTerm ) { + focusedOption = filteredOptions[ 0 ]; + } + if ( focusedOption ) { + const mockEvent = { + target: { value: focusedOption.code }, + currentTarget: { value: focusedOption.code }, + }; + if ( onOptionChange ) { + onOptionChange( mockEvent ); + } + setIsOpen( false ); + } + }, + [ isOpen, focusedOptionIndex, filteredOptions, onOptionChange, setIsOpen, searchTerm ] + ); + + const handleEscape = useCallback( + event => { + if ( ! isOpen ) { + return; + } + event.preventDefault(); + setIsOpen( false ); + }, + [ isOpen, setIsOpen ] + ); + + // Keyboard shortcuts object for KeyboardShortcuts component + const shortcuts = { + down: handleArrowDown, + up: handleArrowUp, + enter: handleEnter, + esc: handleEscape, + }; + + // Don't render if no options or no selected option + if ( ! options.length || ! selectedOption ) { + return null; + } + + const triggerArrowClass = clsx( 'jetpack-combobox-trigger-arrow', { + 'is-open': isOpen, + } ); + + return ( +
+ + + { isOpen && ( + +
+ = 0 + ? `option-${ filteredOptions[ focusedOptionIndex ]?.code }` + : undefined + } + /> +
+ { filteredOptions.map( ( { country, flag, value, code }, index ) => { + const isFocused = index === focusedOptionIndex; + const isSelected = selectedOption?.code === code; + + return ( + + ); + } ) } +
+
+
+ ) } +
+ ); +}; + +export default SearchableCombobox; diff --git a/projects/packages/forms/src/contact-form/class-contact-form-field.php b/projects/packages/forms/src/contact-form/class-contact-form-field.php index c89ff65fdaadb..c715b951a839d 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form-field.php +++ b/projects/packages/forms/src/contact-form/class-contact-form-field.php @@ -1003,6 +1003,10 @@ public function render_telephone_field( $id, $label, $value, $class, $required, $this->enqueue_phone_field_assets(); + // $class is ill-formed, so we need to fix it + // Strip 'class=' and quotes to get just the class names + $class_names = preg_replace( "/^class=['\"]([^'\"]*)['\"].*$/", '$1', $class ); + $link_label_id = $id . '-number'; $this->set_invalid_message( 'phone', __( 'Please enter a valid phone number', 'jetpack-forms' ) ); @@ -1011,11 +1015,21 @@ public function render_telephone_field( $id, $label, $value, $class, $required, $value = ''; } + $translated_countries = $this->get_translatable_countries(); + $global_config = array( + 'i18n' => array( + 'countryNames' => $translated_countries, + ), + ); + wp_interactivity_config( 'jetpack/field-phone', $global_config ); ob_start(); ?> -
'', 'phoneCountryCode' => $default_country, - 'countryList' => array(), 'fullPhoneNumber' => '', 'countryPrefix' => '', + // combobox state + 'useCombobox' => true, + 'comboboxOpen' => false, + 'searchTerm' => '', + 'allCountries' => array(), + 'filteredCountries' => array(), + 'selectedCountry' => array(), ) ); ?> > -
- - +
+
+ + +
+ +
+ +
+
+
- + type="tel" required="true" @@ -1065,9 +1121,10 @@ public function render_telephone_field( $id, $label, $value, $class, $required, data-wp-bind--aria-invalid='state.fieldHasErrors' data-wp-bind--value='context.phoneNumber' aria-errormessage="-phone-error-message" - data-wp-on--input='actions.onPhoneNumberChange' + data-wp-on--input='actions.phoneNumberInputHandler' data-wp-on--blur='actions.onFieldBlur' - data-wp-class--has-value='state.hasFieldValue' + data-wp-on--focus='actions.phoneNumberFocusHandler' + data-wp-class--has-value='context.phoneNumber' /> get_error_div( $id, 'phone' ); + $field = $label . $input . $this->get_error_div( $id, 'telephone' ); return $field; } @@ -2824,16 +2881,275 @@ private function enqueue_slider_field_assets() { ); } + /** + * Gets an array of translatable country names indexed by their two-letter country codes. + * + * @since $$next-version$$ + * + * @return array Array of country names with two-letter country codes as keys. + */ + public function get_translatable_countries() { + return array( + 'AF' => __( 'Afghanistan', 'jetpack-forms' ), + 'AL' => __( 'Albania', 'jetpack-forms' ), + 'DZ' => __( 'Algeria', 'jetpack-forms' ), + 'AS' => __( 'American Samoa', 'jetpack-forms' ), + 'AD' => __( 'Andorra', 'jetpack-forms' ), + 'AO' => __( 'Angola', 'jetpack-forms' ), + 'AI' => __( 'Anguilla', 'jetpack-forms' ), + 'AG' => __( 'Antigua and Barbuda', 'jetpack-forms' ), + 'AR' => __( 'Argentina', 'jetpack-forms' ), + 'AM' => __( 'Armenia', 'jetpack-forms' ), + 'AW' => __( 'Aruba', 'jetpack-forms' ), + 'AU' => __( 'Australia', 'jetpack-forms' ), + 'AT' => __( 'Austria', 'jetpack-forms' ), + 'AZ' => __( 'Azerbaijan', 'jetpack-forms' ), + 'BS' => __( 'Bahamas', 'jetpack-forms' ), + 'BH' => __( 'Bahrain', 'jetpack-forms' ), + 'BD' => __( 'Bangladesh', 'jetpack-forms' ), + 'BB' => __( 'Barbados', 'jetpack-forms' ), + 'BY' => __( 'Belarus', 'jetpack-forms' ), + 'BE' => __( 'Belgium', 'jetpack-forms' ), + 'BZ' => __( 'Belize', 'jetpack-forms' ), + 'BJ' => __( 'Benin', 'jetpack-forms' ), + 'BM' => __( 'Bermuda', 'jetpack-forms' ), + 'BT' => __( 'Bhutan', 'jetpack-forms' ), + 'BO' => __( 'Bolivia', 'jetpack-forms' ), + 'BA' => __( 'Bosnia and Herzegovina', 'jetpack-forms' ), + 'BW' => __( 'Botswana', 'jetpack-forms' ), + 'BR' => __( 'Brazil', 'jetpack-forms' ), + 'IO' => __( 'British Indian Ocean Territory', 'jetpack-forms' ), + 'VG' => __( 'British Virgin Islands', 'jetpack-forms' ), + 'BN' => __( 'Brunei', 'jetpack-forms' ), + 'BG' => __( 'Bulgaria', 'jetpack-forms' ), + 'BF' => __( 'Burkina Faso', 'jetpack-forms' ), + 'BI' => __( 'Burundi', 'jetpack-forms' ), + 'KH' => __( 'Cambodia', 'jetpack-forms' ), + 'CM' => __( 'Cameroon', 'jetpack-forms' ), + 'CA' => __( 'Canada', 'jetpack-forms' ), + 'CV' => __( 'Cape Verde', 'jetpack-forms' ), + 'KY' => __( 'Cayman Islands', 'jetpack-forms' ), + 'CF' => __( 'Central African Republic', 'jetpack-forms' ), + 'TD' => __( 'Chad', 'jetpack-forms' ), + 'CL' => __( 'Chile', 'jetpack-forms' ), + 'CN' => __( 'China', 'jetpack-forms' ), + 'CX' => __( 'Christmas Island', 'jetpack-forms' ), + 'CC' => __( 'Cocos (Keeling) Islands', 'jetpack-forms' ), + 'CO' => __( 'Colombia', 'jetpack-forms' ), + 'KM' => __( 'Comoros', 'jetpack-forms' ), + 'CG' => __( 'Congo - Brazzaville', 'jetpack-forms' ), + 'CD' => __( 'Congo - Kinshasa', 'jetpack-forms' ), + 'CK' => __( 'Cook Islands', 'jetpack-forms' ), + 'CR' => __( 'Costa Rica', 'jetpack-forms' ), + 'HR' => __( 'Croatia', 'jetpack-forms' ), + 'CU' => __( 'Cuba', 'jetpack-forms' ), + 'CY' => __( 'Cyprus', 'jetpack-forms' ), + 'CZ' => __( 'Czech Republic', 'jetpack-forms' ), + 'CI' => __( "Côte d'Ivoire", 'jetpack-forms' ), + 'DK' => __( 'Denmark', 'jetpack-forms' ), + 'DJ' => __( 'Djibouti', 'jetpack-forms' ), + 'DM' => __( 'Dominica', 'jetpack-forms' ), + 'DO' => __( 'Dominican Republic', 'jetpack-forms' ), + 'EC' => __( 'Ecuador', 'jetpack-forms' ), + 'EG' => __( 'Egypt', 'jetpack-forms' ), + 'SV' => __( 'El Salvador', 'jetpack-forms' ), + 'GQ' => __( 'Equatorial Guinea', 'jetpack-forms' ), + 'ER' => __( 'Eritrea', 'jetpack-forms' ), + 'EE' => __( 'Estonia', 'jetpack-forms' ), + 'SZ' => __( 'Eswatini', 'jetpack-forms' ), + 'ET' => __( 'Ethiopia', 'jetpack-forms' ), + 'FK' => __( 'Falkland Islands', 'jetpack-forms' ), + 'FO' => __( 'Faroe Islands', 'jetpack-forms' ), + 'FJ' => __( 'Fiji', 'jetpack-forms' ), + 'FI' => __( 'Finland', 'jetpack-forms' ), + 'FR' => __( 'France', 'jetpack-forms' ), + 'GF' => __( 'French Guiana', 'jetpack-forms' ), + 'PF' => __( 'French Polynesia', 'jetpack-forms' ), + 'GA' => __( 'Gabon', 'jetpack-forms' ), + 'GM' => __( 'Gambia', 'jetpack-forms' ), + 'GE' => __( 'Georgia', 'jetpack-forms' ), + 'DE' => __( 'Germany', 'jetpack-forms' ), + 'GH' => __( 'Ghana', 'jetpack-forms' ), + 'GI' => __( 'Gibraltar', 'jetpack-forms' ), + 'GR' => __( 'Greece', 'jetpack-forms' ), + 'GL' => __( 'Greenland', 'jetpack-forms' ), + 'GD' => __( 'Grenada', 'jetpack-forms' ), + 'GP' => __( 'Guadeloupe', 'jetpack-forms' ), + 'GU' => __( 'Guam', 'jetpack-forms' ), + 'GT' => __( 'Guatemala', 'jetpack-forms' ), + 'GG' => __( 'Guernsey', 'jetpack-forms' ), + 'GN' => __( 'Guinea', 'jetpack-forms' ), + 'GW' => __( 'Guinea-Bissau', 'jetpack-forms' ), + 'GY' => __( 'Guyana', 'jetpack-forms' ), + 'HT' => __( 'Haiti', 'jetpack-forms' ), + 'HN' => __( 'Honduras', 'jetpack-forms' ), + 'HK' => __( 'Hong Kong', 'jetpack-forms' ), + 'HU' => __( 'Hungary', 'jetpack-forms' ), + 'IS' => __( 'Iceland', 'jetpack-forms' ), + 'IN' => __( 'India', 'jetpack-forms' ), + 'ID' => __( 'Indonesia', 'jetpack-forms' ), + 'IR' => __( 'Iran', 'jetpack-forms' ), + 'IQ' => __( 'Iraq', 'jetpack-forms' ), + 'IE' => __( 'Ireland', 'jetpack-forms' ), + 'IM' => __( 'Isle of Man', 'jetpack-forms' ), + 'IL' => __( 'Israel', 'jetpack-forms' ), + 'IT' => __( 'Italy', 'jetpack-forms' ), + 'JM' => __( 'Jamaica', 'jetpack-forms' ), + 'JP' => __( 'Japan', 'jetpack-forms' ), + 'JE' => __( 'Jersey', 'jetpack-forms' ), + 'JO' => __( 'Jordan', 'jetpack-forms' ), + 'KZ' => __( 'Kazakhstan', 'jetpack-forms' ), + 'KE' => __( 'Kenya', 'jetpack-forms' ), + 'KI' => __( 'Kiribati', 'jetpack-forms' ), + 'XK' => __( 'Kosovo', 'jetpack-forms' ), + 'KW' => __( 'Kuwait', 'jetpack-forms' ), + 'KG' => __( 'Kyrgyzstan', 'jetpack-forms' ), + 'LA' => __( 'Laos', 'jetpack-forms' ), + 'LV' => __( 'Latvia', 'jetpack-forms' ), + 'LB' => __( 'Lebanon', 'jetpack-forms' ), + 'LS' => __( 'Lesotho', 'jetpack-forms' ), + 'LR' => __( 'Liberia', 'jetpack-forms' ), + 'LY' => __( 'Libya', 'jetpack-forms' ), + 'LI' => __( 'Liechtenstein', 'jetpack-forms' ), + 'LT' => __( 'Lithuania', 'jetpack-forms' ), + 'LU' => __( 'Luxembourg', 'jetpack-forms' ), + 'MO' => __( 'Macao', 'jetpack-forms' ), + 'MG' => __( 'Madagascar', 'jetpack-forms' ), + 'MW' => __( 'Malawi', 'jetpack-forms' ), + 'MY' => __( 'Malaysia', 'jetpack-forms' ), + 'MV' => __( 'Maldives', 'jetpack-forms' ), + 'ML' => __( 'Mali', 'jetpack-forms' ), + 'MT' => __( 'Malta', 'jetpack-forms' ), + 'MH' => __( 'Marshall Islands', 'jetpack-forms' ), + 'MQ' => __( 'Martinique', 'jetpack-forms' ), + 'MR' => __( 'Mauritania', 'jetpack-forms' ), + 'MU' => __( 'Mauritius', 'jetpack-forms' ), + 'YT' => __( 'Mayotte', 'jetpack-forms' ), + 'MX' => __( 'Mexico', 'jetpack-forms' ), + 'FM' => __( 'Micronesia', 'jetpack-forms' ), + 'MD' => __( 'Moldova', 'jetpack-forms' ), + 'MC' => __( 'Monaco', 'jetpack-forms' ), + 'MN' => __( 'Mongolia', 'jetpack-forms' ), + 'ME' => __( 'Montenegro', 'jetpack-forms' ), + 'MS' => __( 'Montserrat', 'jetpack-forms' ), + 'MA' => __( 'Morocco', 'jetpack-forms' ), + 'MZ' => __( 'Mozambique', 'jetpack-forms' ), + 'MM' => __( 'Myanmar', 'jetpack-forms' ), + 'NA' => __( 'Namibia', 'jetpack-forms' ), + 'NR' => __( 'Nauru', 'jetpack-forms' ), + 'NP' => __( 'Nepal', 'jetpack-forms' ), + 'NL' => __( 'Netherlands', 'jetpack-forms' ), + 'NC' => __( 'New Caledonia', 'jetpack-forms' ), + 'NZ' => __( 'New Zealand', 'jetpack-forms' ), + 'NI' => __( 'Nicaragua', 'jetpack-forms' ), + 'NE' => __( 'Niger', 'jetpack-forms' ), + 'NG' => __( 'Nigeria', 'jetpack-forms' ), + 'NU' => __( 'Niue', 'jetpack-forms' ), + 'NF' => __( 'Norfolk Island', 'jetpack-forms' ), + 'KP' => __( 'North Korea', 'jetpack-forms' ), + 'MK' => __( 'North Macedonia', 'jetpack-forms' ), + 'MP' => __( 'Northern Mariana Islands', 'jetpack-forms' ), + 'NO' => __( 'Norway', 'jetpack-forms' ), + 'OM' => __( 'Oman', 'jetpack-forms' ), + 'PK' => __( 'Pakistan', 'jetpack-forms' ), + 'PW' => __( 'Palau', 'jetpack-forms' ), + 'PS' => __( 'Palestine', 'jetpack-forms' ), + 'PA' => __( 'Panama', 'jetpack-forms' ), + 'PG' => __( 'Papua New Guinea', 'jetpack-forms' ), + 'PY' => __( 'Paraguay', 'jetpack-forms' ), + 'PE' => __( 'Peru', 'jetpack-forms' ), + 'PH' => __( 'Philippines', 'jetpack-forms' ), + 'PN' => __( 'Pitcairn Islands', 'jetpack-forms' ), + 'PL' => __( 'Poland', 'jetpack-forms' ), + 'PT' => __( 'Portugal', 'jetpack-forms' ), + 'PR' => __( 'Puerto Rico', 'jetpack-forms' ), + 'QA' => __( 'Qatar', 'jetpack-forms' ), + 'RO' => __( 'Romania', 'jetpack-forms' ), + 'RU' => __( 'Russia', 'jetpack-forms' ), + 'RW' => __( 'Rwanda', 'jetpack-forms' ), + 'RE' => __( 'Réunion', 'jetpack-forms' ), + 'BL' => __( 'Saint Barthélemy', 'jetpack-forms' ), + 'SH' => __( 'Saint Helena', 'jetpack-forms' ), + 'KN' => __( 'Saint Kitts and Nevis', 'jetpack-forms' ), + 'LC' => __( 'Saint Lucia', 'jetpack-forms' ), + 'MF' => __( 'Saint Martin', 'jetpack-forms' ), + 'PM' => __( 'Saint Pierre and Miquelon', 'jetpack-forms' ), + 'VC' => __( 'Saint Vincent and the Grenadines', 'jetpack-forms' ), + 'WS' => __( 'Samoa', 'jetpack-forms' ), + 'SM' => __( 'San Marino', 'jetpack-forms' ), + 'SA' => __( 'Saudi Arabia', 'jetpack-forms' ), + 'SN' => __( 'Senegal', 'jetpack-forms' ), + 'RS' => __( 'Serbia', 'jetpack-forms' ), + 'SC' => __( 'Seychelles', 'jetpack-forms' ), + 'SL' => __( 'Sierra Leone', 'jetpack-forms' ), + 'SG' => __( 'Singapore', 'jetpack-forms' ), + 'SK' => __( 'Slovakia', 'jetpack-forms' ), + 'SI' => __( 'Slovenia', 'jetpack-forms' ), + 'SB' => __( 'Solomon Islands', 'jetpack-forms' ), + 'SO' => __( 'Somalia', 'jetpack-forms' ), + 'ZA' => __( 'South Africa', 'jetpack-forms' ), + 'GS' => __( 'South Georgia and the South Sandwich Islands', 'jetpack-forms' ), + 'KR' => __( 'South Korea', 'jetpack-forms' ), + 'ES' => __( 'Spain', 'jetpack-forms' ), + 'LK' => __( 'Sri Lanka', 'jetpack-forms' ), + 'SD' => __( 'Sudan', 'jetpack-forms' ), + 'SR' => __( 'Suriname', 'jetpack-forms' ), + 'SJ' => __( 'Svalbard and Jan Mayen', 'jetpack-forms' ), + 'SE' => __( 'Sweden', 'jetpack-forms' ), + 'CH' => __( 'Switzerland', 'jetpack-forms' ), + 'SY' => __( 'Syria', 'jetpack-forms' ), + 'ST' => __( 'São Tomé and Príncipe', 'jetpack-forms' ), + 'TW' => __( 'Taiwan', 'jetpack-forms' ), + 'TJ' => __( 'Tajikistan', 'jetpack-forms' ), + 'TZ' => __( 'Tanzania', 'jetpack-forms' ), + 'TH' => __( 'Thailand', 'jetpack-forms' ), + 'TL' => __( 'Timor-Leste', 'jetpack-forms' ), + 'TG' => __( 'Togo', 'jetpack-forms' ), + 'TK' => __( 'Tokelau', 'jetpack-forms' ), + 'TO' => __( 'Tonga', 'jetpack-forms' ), + 'TT' => __( 'Trinidad and Tobago', 'jetpack-forms' ), + 'TN' => __( 'Tunisia', 'jetpack-forms' ), + 'TR' => __( 'Turkey', 'jetpack-forms' ), + 'TM' => __( 'Turkmenistan', 'jetpack-forms' ), + 'TC' => __( 'Turks and Caicos Islands', 'jetpack-forms' ), + 'TV' => __( 'Tuvalu', 'jetpack-forms' ), + 'VI' => __( 'U.S. Virgin Islands', 'jetpack-forms' ), + 'UG' => __( 'Uganda', 'jetpack-forms' ), + 'UA' => __( 'Ukraine', 'jetpack-forms' ), + 'AE' => __( 'United Arab Emirates', 'jetpack-forms' ), + 'GB' => __( 'United Kingdom', 'jetpack-forms' ), + 'US' => __( 'United States', 'jetpack-forms' ), + 'UY' => __( 'Uruguay', 'jetpack-forms' ), + 'UZ' => __( 'Uzbekistan', 'jetpack-forms' ), + 'VU' => __( 'Vanuatu', 'jetpack-forms' ), + 'VA' => __( 'Vatican City', 'jetpack-forms' ), + 'VE' => __( 'Venezuela', 'jetpack-forms' ), + 'VN' => __( 'Vietnam', 'jetpack-forms' ), + 'WF' => __( 'Wallis and Futuna', 'jetpack-forms' ), + 'YE' => __( 'Yemen', 'jetpack-forms' ), + 'ZM' => __( 'Zambia', 'jetpack-forms' ), + 'ZW' => __( 'Zimbabwe', 'jetpack-forms' ), + ); + } + /** * Enqueues scripts and styles needed for the slider field. * - * @since 5.1.0 + * @since $$next-version$$ * * @return void */ private function enqueue_phone_field_assets() { $version = defined( 'JETPACK__VERSION' ) ? \JETPACK__VERSION : '0.1'; + // combobox styles + \wp_enqueue_style( + 'jetpack-form-combobox', + plugins_url( '../../dist/contact-form/css/combobox.css', __FILE__ ), + array(), + $version + ); + \wp_enqueue_style( 'jetpack-form-phone-field', plugins_url( '../../dist/contact-form/css/phone-field.css', __FILE__ ), diff --git a/projects/packages/forms/src/contact-form/class-contact-form-plugin.php b/projects/packages/forms/src/contact-form/class-contact-form-plugin.php index 7b15940968a4d..83491a1c3cd8a 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form-plugin.php +++ b/projects/packages/forms/src/contact-form/class-contact-form-plugin.php @@ -515,6 +515,10 @@ public static function block_attributes_to_shortcode_attributes( $atts, $type, $ unset( $atts['default'] ); } + if ( ! isset( $atts['showCountrySelector'] ) || ! $atts['showCountrySelector'] ) { + unset( $atts['default'] ); + } + $input_attrs = self::get_block_support_classes_and_styles( $block_name, $inner_block['attrs'] ); $atts['inputclasses'] = 'wp-block-jetpack-input jetpack-field__input-element'; $atts['inputclasses'] .= isset( $input_attrs['class'] ) ? ' ' . $input_attrs['class'] : ''; diff --git a/projects/packages/forms/src/contact-form/css/combobox.scss b/projects/packages/forms/src/contact-form/css/combobox.scss new file mode 100644 index 0000000000000..12b88cd3fc775 --- /dev/null +++ b/projects/packages/forms/src/contact-form/css/combobox.scss @@ -0,0 +1,167 @@ +// Custom Combobox Styles for Country Selector +.jetpack-custom-combobox { + letter-spacing: normal; // don't let input attributes rule over this + background-color: inherit; + + // keyboardShortcuts adds a wrapping div, so we need to style it + > div { + color: currentColor; + background-color: inherit; + } + + .jetpack-combobox-trigger { + width: 100%; + border: 0 solid #ddd; + border-radius: 4px; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + font: inherit; + line-height: inherit; + padding-block: 0; + padding-inline: 0; + gap: 0.4em; + color: inherit; + + &:focus { + outline: 0; + text-decoration: none; + box-shadow: none; + } + + &:hover { + text-decoration: none; + } + + .jetpack-combobox-trigger-arrow { + transition: transform 0.2s; + display: flex; + align-items: center; + justify-content: center; + width: 0.6em; + margin-left: 4px; + + svg { + width: 100%; + height: 100%; + color: inherit; + } + + &.is-open { + transform: scaleY(-1); + } + } + } + + .jetpack-combobox-dropdown { + position: absolute; + left: -1px; + top: 100%; + width: -webkit-fill-available; + min-width: 60%; + + background-color: var(--jetpack--contact-form--input-background-fallback); + border-color: var(--jetpack--contact-form--border-color, currentColor); + border-width: var(--jetpack--contact-form--border-size, 1px); + border-radius: var(--jetpack--contact-form--border-radius, 4px); + // some line-heights are too small, + //use max() to ensure a minimum height of 8 * 25px + max-height: max(calc(var(--jetpack--contact-form--line-height, 25px) * 8), 200px); + z-index: 1000; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; + + &.jetpack-combobox-open { + display: block; + } + + .jetpack-combobox-search { + width: 100%; + padding: 8px 12px; + border: none; + border-bottom: 1px solid #eee; + outline: none; + font-size: 14px; + position: sticky; + top: 0; + color: inherit; + background-color: inherit; + z-index: 1; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + + &:focus { + outline: none; + box-shadow: none; + } + + } + // for styles on search input, themes keep finding ways of messing with this + input[type="text"].jetpack-combobox-search { + background-color: inherit !important; + } + + .jetpack-combobox-options { + max-height: calc(var(--jetpack--contact-form--line-height, 25px) * 7); + overflow-y: auto; + color: currentColor; + } + + .jetpack-combobox-option { + padding: 8px 12px; + cursor: pointer; + display: flex; + align-items: flex-start; + font-size: 14px; + transition: background-color 0.1s; + gap: 8px; + background-color: transparent; + border: none; + line-height: inherit; + color: currentColor; + + &:hover, + &.is-focused { + background-color: var(--wp--preset--color--secondary, #f5f5f5); + color: var(--wp--preset--color--background, #000); + } + + &.jetpack-combobox-option-selected { + background-color: var(--wp--preset--color--secondary, #f5f5f5); + font-weight: 600; + color: var(--wp--preset--color--background, #000); + + &:hover { + background-color: var(--wp--preset--color--secondary, #f5f5f5); + } + } + } + } +} + + +.jetpack-combobox-option-icon { + color: currentColor; + pointer-events: none; +} + +.jetpack-combobox-option-value { + font-weight: 500; + pointer-events: none; + color: currentColor; +} + +.jetpack-combobox-option-description { + pointer-events: none; + color: currentColor; +} + +// Editor styles +.wp-block .jetpack-custom-combobox { + + .jetpack-combobox-option { + width: 100%; + } +} diff --git a/projects/packages/forms/src/contact-form/css/grunion.scss b/projects/packages/forms/src/contact-form/css/grunion.scss index 00c4a5ca92040..618e4ea5aca5d 100644 --- a/projects/packages/forms/src/contact-form/css/grunion.scss +++ b/projects/packages/forms/src/contact-form/css/grunion.scss @@ -69,7 +69,7 @@ that needs to mimic the input element styles */ font: inherit; border: 1px solid #8c8f94; border-radius: 0; - background-color: var(--jetpack--contact-form--background-color); + background-color: var(--jetpack--contact-form--input-background); } :where(.contact-form textarea) { @@ -1212,7 +1212,7 @@ on production builds, the attributes are being reordered, causing side-effects align-items: baseline; } -.contact-form :is([type="submit"],button:not([type="reset"])) { +.contact-form :is([type="submit"]) { display: inline-flex; justify-content: center; align-items: center; diff --git a/projects/packages/forms/src/contact-form/css/phone-field.scss b/projects/packages/forms/src/contact-form/css/phone-field.scss index 2cbdb43adb49c..f832addfd463b 100644 --- a/projects/packages/forms/src/contact-form/css/phone-field.scss +++ b/projects/packages/forms/src/contact-form/css/phone-field.scss @@ -9,7 +9,7 @@ display: flex; gap: 8px; background-color: var(--jetpack--contact-form--input-background); - border-color: var(--jetpack--contact-form--border-color); + border-color: var(--jetpack--contact-form--border-color, currentColor); border-width: var(--jetpack--contact-form--border-size); border-style: var(--jetpack--contact-form--border-style); border-radius: var(--jetpack--contact-form--border-radius); @@ -17,27 +17,28 @@ font-size: var(--jetpack--contact-form--font-size); padding: var(--jetpack--contact-form--input-padding); line-height: var(--jetpack--contact-form--line-height); + align-items: center; &:has(.jetpack-field__input-element:focus) { outline-width: 2px; outline-style: solid; - // mimics default focus outline color, need to fix this + // mimics default focus outline color, this is something we cannot + // fully control as the probe would fail to capture :focus styles outline-color: rgb(0, 95, 204); } .jetpack-field__input-prefix:not([hidden]) { - display: flex; - width: 40%; + background-color: inherit; .jetpack-field__input-element { + width: 100%; + text-overflow: ellipsis; + white-space: nowrap; @media (max-width: #{ (gb.$break-mobile) }) { max-width: calc(gb.$break-mobile / 3); } - width: 100%; - text-overflow: ellipsis; - white-space: nowrap; } } @@ -75,11 +76,15 @@ .is-style-animated & { .jetpack-field__input-phone-wrapper { - padding-top: 1.4em; + padding-top: max(var(--jetpack--contact-form--input-padding-top, 0), 1.4em); padding-left: var(--jetpack--contact-form--animated-left-offset); padding-right: var(--jetpack--contact-form--animated-right-offset); - &:not(:has(*:focus, *:active)) { + &:has(.jetpack-field__input-element:focus) { + outline: unset; + } + + &:not(:has(*:focus, *:active)):not(.is-combobox-open) { .jetpack-field__input-prefix:not(:has(~ .has-placeholder),:has(~ .has-value)) { pointer-events: none; @@ -88,23 +93,34 @@ } } - .animated-label__label:has(~* .jetpack-field__input-element:focus), - .animated-label__label:has(~* .jetpack-field__input-element.has-value), - .animated-label__label:has(~* .jetpack-field__input-element.has-placeholder) { - transform: translateY(0); - top: calc(2px + var(--jetpack--contact-form--border-top-size, var(--jetpack--contact-form--border-size, 1px))); + .animated-label__label { - .grunion-label-text { - font-size: 0.75em; + &:has(~.jetpack-field__input-phone-wrapper.is-combobox-open), + &:has(~.jetpack-field__input-phone-wrapper:focus-within), + &:has(~* .jetpack-field__input-element:focus), + &:has(~* .jetpack-field__input-element.has-value), + &:has(~* .jetpack-field__input-element.has-placeholder) { + transform: translateY(0); + top: calc(2px + var(--jetpack--contact-form--border-top-size, var(--jetpack--contact-form--border-size, 1px))); + + .grunion-label-text { + font-size: 0.75em; + } } } + } .is-style-outlined & { .jetpack-field__input-phone-wrapper { + z-index: unset; - &:not(:has(*:focus, *:active)) { + &:has(.jetpack-field__input-element:focus) { + outline: unset; + } + + &:not(:has(*:focus, *:active)):not(.is-combobox-open) { // notched label is game of superpositions with z-index, // while no input element is selected, force transparent so label is visible @@ -119,13 +135,18 @@ } } - .notched-label:has(~* .jetpack-field__input-element:focus) .notched-label__label, - .notched-label:has(~* .jetpack-field__input-element.has-value) .notched-label__label, - .notched-label:has(~* .jetpack-field__input-element.has-placeholder) .notched-label__label { - top: calc(var(--jetpack--contact-form--border-top-size, var(--jetpack--contact-form--border-size)) * -1); + .notched-label { + + &:has(~.jetpack-field__input-phone-wrapper.is-combobox-open) .notched-label__label, + &:has(~.jetpack-field__input-phone-wrapper:focus-within) .notched-label__label, + &:has(~* .jetpack-field__input-element:focus) .notched-label__label, + &:has(~* .jetpack-field__input-element.has-value) .notched-label__label, + &:has(~* .jetpack-field__input-element.has-placeholder) .notched-label__label { + top: calc(var(--jetpack--contact-form--border-top-size, var(--jetpack--contact-form--border-size)) * -1); - .grunion-label-text { - font-size: 0.8em; + .grunion-label-text { + font-size: 0.8em; + } } } diff --git a/projects/packages/forms/src/modules/field-phone/view.js b/projects/packages/forms/src/modules/field-phone/view.js index 9b091ae4460c8..07cf9168e86f6 100644 --- a/projects/packages/forms/src/modules/field-phone/view.js +++ b/projects/packages/forms/src/modules/field-phone/view.js @@ -1,10 +1,28 @@ -import { store, getContext } from '@wordpress/interactivity'; +import { store, getContext, getConfig, getElement, withSyncEvent } from '@wordpress/interactivity'; import parsePhoneNumber, { AsYouType } from 'libphonenumber-js'; import { countries } from '../../blocks/field-telephone/country-list'; import { isEmptyValue } from '../../contact-form/js/validate-helper'; const NAMESPACE = 'jetpack/form'; const asYouTypes = {}; +const phoneInputRefs = {}; +const searchInputRefs = {}; +const optionsListRefs = {}; +const updateSelection = selectedCountry => { + const context = getContext(); + context.phoneCountryCode = selectedCountry.code; + context.countryPrefix = selectedCountry.value; + context.fullPhoneNumber = context.countryPrefix + ' ' + context.phoneNumber; + asYouTypes[ context.fieldId ] = new AsYouType( context.phoneCountryCode ); + context.filteredCountries = context.filteredCountries.map( country => ( { + ...country, + selected: country.code === selectedCountry.code, + } ) ); + context.allCountries = context.allCountries.map( country => ( { + ...country, + selected: country.code === selectedCountry.code, + } ) ); +}; const { actions } = store( NAMESPACE, { state: { @@ -50,7 +68,7 @@ const { actions } = store( NAMESPACE, { context.phoneCountryCode = context.defaultCountry; context.phoneNumber = ''; }, - onPhoneNumberChange( event ) { + phoneNumberInputHandler( event ) { const context = getContext(); const fieldId = context.fieldId; const value = event.target.value; @@ -63,39 +81,155 @@ const { actions } = store( NAMESPACE, { asYouTypes[ fieldId ].reset(); asYouTypes[ fieldId ].input( groomedValue ); if ( asYouTypes[ fieldId ].getCountry() ) { - context.phoneCountryCode = asYouTypes[ fieldId ].getCountry(); + const countryCode = asYouTypes[ fieldId ].getCountry(); context.phoneNumber = asYouTypes[ fieldId ].getNationalNumber(); - asYouTypes[ fieldId ] = new AsYouType( context.phoneCountryCode ); - context.countryPrefix = countries.find( - item => item.code === context.phoneCountryCode - )?.value; + context.selectedCountry = context.allCountries.find( + country => country.code === countryCode + ); + updateSelection( context.selectedCountry ); } else { context.phoneNumber = value; } - context.fullPhoneNumber = context.countryPrefix + ' ' + context.phoneNumber; + actions.updateField( fieldId, value ); }, - onPhoneCountryChange( event ) { + phoneCountryChangeHandler() { + const context = getContext(); + // this context.filtered is from the template iterator + context.selectedCountry = { ...context.filtered }; + updateSelection( context.selectedCountry ); + context.comboboxOpen = false; + phoneInputRefs[ context.fieldId ]?.focus?.(); + }, + phoneComboboxInputHandler( event ) { + const context = getContext(); + const searchTerm = event.target.value.toLowerCase(); + context.filteredCountries = context.allCountries.filter( + country => + country.country.toLowerCase().includes( searchTerm ) || + country.code.toLowerCase().includes( searchTerm ) || + country.value.includes( searchTerm ) + ); + optionsListRefs[ context.fieldId ].scrollTo?.( { top: 0, behavior: 'instant' } ); + }, + phoneComboboxKeydownHandler: withSyncEvent( event => { + const context = getContext(); + if ( event.key === 'Escape' ) { + context.comboboxOpen = false; + } else if ( event.key === 'Enter' ) { + event.preventDefault(); + // Select either the currently selected country or the first filtered option if available + if ( context.filteredCountries.length > 0 ) { + const selectedCountry = + context.filteredCountries.find( country => country.selected ) || + context.filteredCountries[ 0 ]; + context.selectedCountry = selectedCountry; + updateSelection( context.selectedCountry ); + context.comboboxOpen = false; + // Focus on the ref input + phoneInputRefs[ context.fieldId ]?.focus?.(); + } + } else if ( event.key === 'ArrowDown' ) { + event.preventDefault(); + if ( context.filteredCountries.length > 0 ) { + // Find index of currently selected country in filtered list + const selectedIndex = context.filteredCountries.findIndex( country => country.selected ); + + // If there's a next country in filtered list, select it, otherwise wrap to first + const nextIndex = + selectedIndex === context.filteredCountries.length - 1 ? 0 : selectedIndex + 1; + context.selectedCountry = context.filteredCountries[ nextIndex ]; + updateSelection( context.selectedCountry ); + setTimeout( () => { + // Find and scroll the newly selected option into view + const selectedOption = optionsListRefs[ context.fieldId ].querySelector( + '.jetpack-combobox-option-selected' + ); + selectedOption?.scrollIntoView?.( { + block: 'nearest', + container: 'nearest', + behavior: 'instant', + } ); + }, 0 ); + } + } else if ( event.key === 'ArrowUp' ) { + event.preventDefault(); + if ( context.filteredCountries.length > 0 ) { + // Find index of currently selected country in filtered list + const selectedIndex = context.filteredCountries.findIndex( country => country.selected ); + + // If there's a previous country in filtered list, select it, otherwise wrap to last + const prevIndex = + selectedIndex <= 0 ? context.filteredCountries.length - 1 : selectedIndex - 1; + context.selectedCountry = context.filteredCountries[ prevIndex ]; + updateSelection( context.selectedCountry ); + setTimeout( () => { + // Find and scroll the newly selected option into view + const selectedOption = optionsListRefs[ context.fieldId ].querySelector( + '.jetpack-combobox-option-selected' + ); + selectedOption?.scrollIntoView?.( { + block: 'nearest', + container: 'nearest', + behavior: 'instant', + } ); + }, 0 ); + } + } + } ), + phoneNumberFocusHandler() { const context = getContext(); - context.phoneCountryCode = event?.target?.value || context.defaultCountry; - context.countryPrefix = countries.find( - item => item.code === context.phoneCountryCode - )?.value; - asYouTypes[ context.fieldId ] = new AsYouType( context.phoneCountryCode ); - context.fullPhoneNumber = context.countryPrefix + ' ' + context.phoneNumber; + context.comboboxOpen = false; + }, + phoneComboboxToggle() { + const context = getContext(); + context.comboboxOpen = ! context.comboboxOpen; + if ( context.comboboxOpen ) { + setTimeout( () => { + searchInputRefs[ context.fieldId ]?.focus?.(); + optionsListRefs[ context.fieldId ] + .querySelector( '.jetpack-combobox-option-selected' ) + ?.scrollIntoView?.( { block: 'nearest', container: 'nearest' } ); + }, 0 ); + } + }, + phoneComboboxDocumentClickHandler( event ) { + const { ref } = getElement(); + if ( ref.contains( event.target ) ) { + return; + } + const context = getContext(); + context.comboboxOpen = false; }, }, callbacks: { - initializeCountrySelector() { + initializePhoneField() { + const element = getElement().ref; + const context = getContext(); + // store refs for quick access later and less intensive dom scouting + phoneInputRefs[ context.fieldId ] = element.querySelector( 'input[type="tel"]' ); + searchInputRefs[ context.fieldId ] = element.parentElement.querySelector( + '.jetpack-combobox-search' + ); + optionsListRefs[ context.fieldId ] = element.parentElement.querySelector( + '.jetpack-combobox-options' + ); + }, + initializePhoneFieldCustomComboBox() { const context = getContext(); - if ( context.showCountrySelector ) { - context.countryList = countries.map( country => ( { - ...country, - label: country.country + ' ' + country.flag + ' ' + country.value, - value: country.code, - selected: country.code === context.defaultCountry, - } ) ); + if ( ! context.showCountrySelector ) { + return; } + const config = getConfig( 'jetpack/field-phone' ); + context.allCountries = countries.map( country => ( { + ...country, + country: config?.i18n?.countryNames?.[ country.code ] || '', + selected: country.code === context.defaultCountry, + } ) ); + context.filteredCountries = [ ...context.allCountries ]; + context.selectedCountry = context.filteredCountries.find( + country => country.code === context.defaultCountry + ); asYouTypes[ context.fieldId ] = new AsYouType( context.defaultCountry ); }, }, diff --git a/projects/packages/forms/tests/php/contact-form/Contact_Form_Test.php b/projects/packages/forms/tests/php/contact-form/Contact_Form_Test.php index 87e44ae072265..95ccb5735c7fc 100644 --- a/projects/packages/forms/tests/php/contact-form/Contact_Form_Test.php +++ b/projects/packages/forms/tests/php/contact-form/Contact_Form_Test.php @@ -1703,9 +1703,9 @@ public function assertValidPhoneField( $html, $attributes ) { // Get label. $label = $this->getFirstElement( $wrapper_div, 'label' ); - // Inputs. - $visible_input = $this->getFirstElement( $wrapper_div, 'input', 0 ); - $input = $this->getFirstElement( $wrapper_div, 'input', 1 ); + // Inputs. (0 is the comboxbox search input, 1 is the visible input and 2 is the hidden, actual, input) + $visible_input = $this->getFirstElement( $wrapper_div, 'input', 1 ); + $input = $this->getFirstElement( $wrapper_div, 'input', 2 ); // Label matches for matches input ID. $this->assertEquals( @@ -1728,8 +1728,8 @@ public function assertValidPhoneField( $html, $attributes ) { $this->assertEquals( $input->getAttribute( 'value' ), $attributes['default'], 'value and default doesn\'t match' ); $this->assertEquals( + 'jetpack-field__input-element', $visible_input->getAttribute( 'class' ), - "{$attributes['type']} {$attributes['class']} grunion-field", 'input class attribute doesn\'t match' ); } diff --git a/projects/plugins/jetpack/changelog/add-forms-country-combobox b/projects/plugins/jetpack/changelog/add-forms-country-combobox new file mode 100644 index 0000000000000..ff3461477521a --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-forms-country-combobox @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Forms: phone field can now carry a country selector combobox