From 2c14cd6b6097c999e6bd666a01e302c6554c9a47 Mon Sep 17 00:00:00 2001 From: Stephen Callender Date: Wed, 11 Mar 2026 10:05:23 -0400 Subject: [PATCH 1/2] Auto-select country/state if only 1 option. Fix autofill state for Chrome mobile --- .../_components/app/address-region-field.twig | 22 ++++++++++++++++ .../base/input-select-searchable.twig | 1 + src/web/assets/checkout/dist/js/alpine.js | 25 +++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/src/templates/_components/app/address-region-field.twig b/src/templates/_components/app/address-region-field.twig index a625167..fca37d8 100644 --- a/src/templates/_components/app/address-region-field.twig +++ b/src/templates/_components/app/address-region-field.twig @@ -27,6 +27,7 @@ regions: {{ countriesAndRegions|json_encode }}, initialRegions: [], currentRegions: [], + autofillValue: '', }" x-init=" initialRegions = regions[countryCode] ?? []; @@ -41,6 +42,27 @@ } " > + {# Autofill catcher: always in the DOM (never display:none) so Chrome mobile can autofill it. + The main searchable select input is inside an x-show div that's hidden until a country is + selected, and Chrome mobile skips autofilling hidden inputs. This off-screen input captures + the autofilled state value so we can apply it once options load. #} + {% if context|default %} + {% set regionCatcherId = ('administrativeArea')|namespaceInputId(context) ~ '-autofill-catcher' %} + {% else %} + {% set regionCatcherId = 'administrativeArea-autofill-catcher' %} + {% endif %} + +
{% include 'foster-checkout/_components/base/input-select-searchable' with { context: context, diff --git a/src/templates/_components/base/input-select-searchable.twig b/src/templates/_components/base/input-select-searchable.twig index 12b2741..106336d 100644 --- a/src/templates/_components/base/input-select-searchable.twig +++ b/src/templates/_components/base/input-select-searchable.twig @@ -99,6 +99,7 @@ @keydown.enter.prevent="open ? selectActiveOption() : toggleListbox()" @keydown="onTriggerKeydown($event)" @input="onTriggerInput($event)" + @change="onTriggerInput($event)" class="w-full py-[11px] pl-3 pr-10 rounded-lg cursor-default border bg-white text-left focus:!ring-0 focus:!outline-none transition-color duration-300 caret-transparent" :class="errors.length ? 'border-red-500 focus:border-red-500' : 'border-gray-250 focus:border-black'" aria-haspopup="listbox" diff --git a/src/web/assets/checkout/dist/js/alpine.js b/src/web/assets/checkout/dist/js/alpine.js index 4de85a6..b586cc7 100644 --- a/src/web/assets/checkout/dist/js/alpine.js +++ b/src/web/assets/checkout/dist/js/alpine.js @@ -121,6 +121,24 @@ const SearchableSelect = (props) => { if (input && input.value && !this.selectedOption) { this.selectByValue(input.value); } + + // Check for autofill catcher value (mobile Chrome workaround). + // The catcher input sits outside the x-show wrapper so it's always + // visible to the browser's autofill engine. Walk past our own x-data + // to find the parent region-field wrapper. + if (!this.selectedOption) { + const catcher = this.$el.parentElement?.closest('[x-data]')?.querySelector('[id$="-autofill-catcher"]'); + if (catcher && catcher.value) { + if (this.selectByValue(catcher.value)) { + catcher.value = ''; + } + } + } + + // Auto-select if there's only one option + if (!this.selectedOption && o.length === 1) { + this.selectedOption = o[0]; + } }); }); @@ -147,6 +165,13 @@ const SearchableSelect = (props) => { }) } }); + + // Auto-select if there's only one option (deferred so watchers are active) + this.$nextTick(() => { + if (!this.selectedOption && this.options.length === 1) { + this.selectedOption = this.options[0]; + } + }); }, /** From 80b1ff156bc8798b61e96c6fe3b32c8d6573baf2 Mon Sep 17 00:00:00 2001 From: Stephen Callender Date: Wed, 11 Mar 2026 10:17:50 -0400 Subject: [PATCH 2/2] Simpler route for fixing chrome mobile --- .../_components/app/address-region-field.twig | 58 ++++++------------- src/web/assets/checkout/dist/js/alpine.js | 13 ----- 2 files changed, 19 insertions(+), 52 deletions(-) diff --git a/src/templates/_components/app/address-region-field.twig b/src/templates/_components/app/address-region-field.twig index fca37d8..12800f9 100644 --- a/src/templates/_components/app/address-region-field.twig +++ b/src/templates/_components/app/address-region-field.twig @@ -27,7 +27,6 @@ regions: {{ countriesAndRegions|json_encode }}, initialRegions: [], currentRegions: [], - autofillValue: '', }" x-init=" initialRegions = regions[countryCode] ?? []; @@ -41,43 +40,24 @@ }); } " + {# Use h-0/overflow-hidden instead of x-show (display:none) or invisible (visibility:hidden) + so Chrome mobile can still autofill the input inside — it skips both display:none and + visibility:hidden inputs. #} + :class="currentRegions.length > 0 ? '' : 'h-0 overflow-hidden'" > - {# Autofill catcher: always in the DOM (never display:none) so Chrome mobile can autofill it. - The main searchable select input is inside an x-show div that's hidden until a country is - selected, and Chrome mobile skips autofilling hidden inputs. This off-screen input captures - the autofilled state value so we can apply it once options load. #} - {% if context|default %} - {% set regionCatcherId = ('administrativeArea')|namespaceInputId(context) ~ '-autofill-catcher' %} - {% else %} - {% set regionCatcherId = 'administrativeArea-autofill-catcher' %} - {% endif %} - - -
- {% include 'foster-checkout/_components/base/input-select-searchable' with { - context: context, - id: 'administrativeArea', - model: 'administrativeArea', - name: 'administrativeArea', - options: 'initialRegions', - eventName: 'addressregions', - fallbackOptions: fallbackOptions, - label: 'addressFields.stateLabel'|t('foster-checkout'), - placeholder: 'addressFields.select'|t('foster-checkout'), - required: true, - value: address ? address.administrativeArea : '', - errors: errors.administrativeArea ?? [], - autocomplete: 'address-level1' - } %} -
+ {% include 'foster-checkout/_components/base/input-select-searchable' with { + context: context, + id: 'administrativeArea', + model: 'administrativeArea', + name: 'administrativeArea', + options: 'initialRegions', + eventName: 'addressregions', + fallbackOptions: fallbackOptions, + label: 'addressFields.stateLabel'|t('foster-checkout'), + placeholder: 'addressFields.select'|t('foster-checkout'), + required: true, + value: address ? address.administrativeArea : '', + errors: errors.administrativeArea ?? [], + autocomplete: 'address-level1' + } %}
diff --git a/src/web/assets/checkout/dist/js/alpine.js b/src/web/assets/checkout/dist/js/alpine.js index b586cc7..70bc9a8 100644 --- a/src/web/assets/checkout/dist/js/alpine.js +++ b/src/web/assets/checkout/dist/js/alpine.js @@ -122,19 +122,6 @@ const SearchableSelect = (props) => { this.selectByValue(input.value); } - // Check for autofill catcher value (mobile Chrome workaround). - // The catcher input sits outside the x-show wrapper so it's always - // visible to the browser's autofill engine. Walk past our own x-data - // to find the parent region-field wrapper. - if (!this.selectedOption) { - const catcher = this.$el.parentElement?.closest('[x-data]')?.querySelector('[id$="-autofill-catcher"]'); - if (catcher && catcher.value) { - if (this.selectByValue(catcher.value)) { - catcher.value = ''; - } - } - } - // Auto-select if there's only one option if (!this.selectedOption && o.length === 1) { this.selectedOption = o[0];