From ea52ef77ab1399bc8c48f3279586ef4dded0e40b Mon Sep 17 00:00:00 2001 From: Abban Dunne Date: Thu, 12 Sep 2024 07:14:19 +0200 Subject: [PATCH] Make autocomplete autoscroll on focus mobile only We added an on focus autoscroll to the autocomplete fields to stop the dropdown menu being hidden behind the on screen keyboard. This works well on mobile but is annoying on desktop, so this makes the autoscroll only happen on smaller sizes. I also moved the autoscroll functionality into a composable as it's shared between 3 fields. Ticket: https://phabricator.wikimedia.org/T374419 --- .../shared/form_fields/CityAutocompleteField.vue | 9 ++------- .../form_fields/CountryAutocompleteField.vue | 9 ++------- .../form_fields/StreetAutocompleteField.vue | 9 ++------- .../useAutocompleteScrollIntoViewOnFocus.ts | 15 +++++++++++++++ .../form_fields/CityAutocompleteField.spec.ts | 12 +++++++++++- .../form_fields/CountryAutocompleteField.spec.ts | 12 +++++++++++- .../form_fields/StreetAutocompleteField.spec.ts | 12 +++++++++++- 7 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 src/components/shared/form_fields/useAutocompleteScrollIntoViewOnFocus.ts diff --git a/src/components/shared/form_fields/CityAutocompleteField.vue b/src/components/shared/form_fields/CityAutocompleteField.vue index b4d0fbf9e..9ef29db27 100644 --- a/src/components/shared/form_fields/CityAutocompleteField.vue +++ b/src/components/shared/form_fields/CityAutocompleteField.vue @@ -54,6 +54,7 @@ import { CityAutocompleteResource, NullCityAutocompleteResource } from '@src/api import TextFormInput from '@src/components/shared/form_elements/TextFormInput.vue'; import { updateAutocompleteScrollPosition } from '@src/components/shared/form_fields/updateAutocompleteScrollPosition'; import { useAriaDescribedby } from '@src/components/shared/form_fields/useAriaDescribedby'; +import { autoscrollMaxWidth, useAutocompleteScrollIntoViewOnFocus } from '@src/components/shared/form_fields/useAutocompleteScrollIntoViewOnFocus'; enum InteractionState { Typing, @@ -85,6 +86,7 @@ const ariaDescribedby = useAriaDescribedby( `${props.inputId}-error`, computed( () => props.showError ) ); +const scrollIntoView = useAutocompleteScrollIntoViewOnFocus( props.scrollTargetId, autoscrollMaxWidth ); const placeholder = computed( () => { if ( cities.value.length > 0 ) { @@ -93,13 +95,6 @@ const placeholder = computed( () => { return 'form_for_example'; } ); -const scrollIntoView = (): void => { - const scrollIntoViewElement = document.getElementById( props.scrollTargetId ); - if ( scrollIntoViewElement ) { - scrollIntoViewElement.scrollIntoView( { behavior: 'smooth' } ); - } -}; - const onFocus = ( event: Event ) => { autocompleteIsActive.value = true; scrollIntoView(); diff --git a/src/components/shared/form_fields/CountryAutocompleteField.vue b/src/components/shared/form_fields/CountryAutocompleteField.vue index 9e8a43de7..525ca4a28 100644 --- a/src/components/shared/form_fields/CountryAutocompleteField.vue +++ b/src/components/shared/form_fields/CountryAutocompleteField.vue @@ -58,6 +58,7 @@ import TextFormInput from '@src/components/shared/form_elements/TextFormInput.vu import { computed, nextTick, ref } from 'vue'; import { updateAutocompleteScrollPosition } from '@src/components/shared/form_fields/updateAutocompleteScrollPosition'; import { useAriaDescribedby } from '@src/components/shared/form_fields/useAriaDescribedby'; +import { autoscrollMaxWidth, useAutocompleteScrollIntoViewOnFocus } from '@src/components/shared/form_fields/useAutocompleteScrollIntoViewOnFocus'; enum InteractionState { Typing, @@ -94,18 +95,12 @@ const ariaDescribedby = useAriaDescribedby( `${props.inputId}-error`, computed( () => props.showError ) ); +const scrollIntoView = useAutocompleteScrollIntoViewOnFocus( props.scrollTargetId, autoscrollMaxWidth ); const isFirstFocusOnDefaultValue = (): boolean => { return !wasFocusedBefore.value && !props.wasRestored; }; -const scrollIntoView = (): void => { - const scrollIntoViewElement = document.getElementById( props.scrollTargetId ); - if ( scrollIntoViewElement ) { - scrollIntoViewElement.scrollIntoView( { behavior: 'smooth' } ); - } -}; - const onFocus = ( event: Event ) => { if ( isFirstFocusOnDefaultValue() ) { countryName.value = ''; diff --git a/src/components/shared/form_fields/StreetAutocompleteField.vue b/src/components/shared/form_fields/StreetAutocompleteField.vue index 393433efb..606945e32 100644 --- a/src/components/shared/form_fields/StreetAutocompleteField.vue +++ b/src/components/shared/form_fields/StreetAutocompleteField.vue @@ -79,6 +79,7 @@ import { useStreetsResource } from '@src/components/shared/form_fields/useStreet import TextFormInput from '@src/components/shared/form_elements/TextFormInput.vue'; import { updateAutocompleteScrollPosition } from '@src/components/shared/form_fields/updateAutocompleteScrollPosition'; import ValueEqualsPlaceholderWarning from '@src/components/shared/ValueEqualsPlaceholderWarning.vue'; +import { autoscrollMaxWidth, useAutocompleteScrollIntoViewOnFocus } from '@src/components/shared/form_fields/useAutocompleteScrollIntoViewOnFocus'; enum InteractionState { Typing, @@ -112,6 +113,7 @@ const ariaDescribedby = useAriaDescribedby( `${props.inputIdStreetName}-error`, computed( () => props.showError ) ); +const scrollIntoView = useAutocompleteScrollIntoViewOnFocus( props.scrollTargetId, autoscrollMaxWidth ); const filteredStreets = computed>( () => { const streetList = streets.value.filter( ( streetItem: string ) => { @@ -127,13 +129,6 @@ const onUpdateModel = (): void => { emit( 'update:modelValue', joinStreetAndBuildingNumber( streetNameModel.value, buildingNumberModel.value ) ); }; -const scrollIntoView = (): void => { - const scrollIntoViewElement = document.getElementById( props.scrollTargetId ); - if ( scrollIntoViewElement ) { - scrollIntoViewElement.scrollIntoView( { behavior: 'smooth' } ); - } -}; - const onStreetNameFocus = ( event: Event ) => { autocompleteIsActive.value = true; scrollIntoView(); diff --git a/src/components/shared/form_fields/useAutocompleteScrollIntoViewOnFocus.ts b/src/components/shared/form_fields/useAutocompleteScrollIntoViewOnFocus.ts new file mode 100644 index 000000000..5ac0d0f84 --- /dev/null +++ b/src/components/shared/form_fields/useAutocompleteScrollIntoViewOnFocus.ts @@ -0,0 +1,15 @@ +export const autoscrollMaxWidth = 769; + +export function useAutocompleteScrollIntoViewOnFocus( target: string, maxWidth: number ): () => void { + return (): void => { + + if ( window.innerWidth > maxWidth ) { + return; + } + + const scrollIntoViewElement = document.getElementById( target ); + if ( scrollIntoViewElement ) { + scrollIntoViewElement.scrollIntoView( { behavior: 'smooth' } ); + } + }; +} diff --git a/tests/unit/components/shared/form_fields/CityAutocompleteField.spec.ts b/tests/unit/components/shared/form_fields/CityAutocompleteField.spec.ts index 76bdc3ba4..9f292284d 100644 --- a/tests/unit/components/shared/form_fields/CityAutocompleteField.spec.ts +++ b/tests/unit/components/shared/form_fields/CityAutocompleteField.spec.ts @@ -231,11 +231,21 @@ describe( 'CityAutocompleteField.vue', () => { expect( field.attributes( 'aria-describedby' ) ).toStrictEqual( 'city-selected city-error' ); } ); - it( 'scrolls field into view when focused', async () => { + it( 'scrolls field into view on small size when focused', async () => { const wrapper = getWrapper(); + Object.defineProperty( window, 'innerWidth', { writable: true, configurable: true, value: 769 } ); await wrapper.find( '#city' ).trigger( 'focus' ); expect( scrollElement.scrollIntoView ).toHaveBeenCalled(); } ); + + it( 'does not scroll field into view on large size when focused', async () => { + const wrapper = getWrapper(); + Object.defineProperty( window, 'innerWidth', { writable: true, configurable: true, value: 770 } ); + + await wrapper.find( '#city' ).trigger( 'focus' ); + + expect( scrollElement.scrollIntoView ).not.toHaveBeenCalled(); + } ); } ); diff --git a/tests/unit/components/shared/form_fields/CountryAutocompleteField.spec.ts b/tests/unit/components/shared/form_fields/CountryAutocompleteField.spec.ts index 6a4a255d5..4ed060e0b 100644 --- a/tests/unit/components/shared/form_fields/CountryAutocompleteField.spec.ts +++ b/tests/unit/components/shared/form_fields/CountryAutocompleteField.spec.ts @@ -288,11 +288,21 @@ describe( 'CountryAutocompleteField.vue', () => { expect( field.attributes( 'aria-describedby' ) ).toStrictEqual( 'country-selected country-error' ); } ); - it( 'scrolls field into view when focused', async () => { + it( 'scrolls field into view on small size when focused', async () => { const wrapper = getWrapper(); + Object.defineProperty( window, 'innerWidth', { writable: true, configurable: true, value: 769 } ); await wrapper.find( '#country' ).trigger( 'focus' ); expect( scrollElement.scrollIntoView ).toHaveBeenCalled(); } ); + + it( 'does not scroll field into view on large size when focused', async () => { + const wrapper = getWrapper(); + Object.defineProperty( window, 'innerWidth', { writable: true, configurable: true, value: 770 } ); + + await wrapper.find( '#country' ).trigger( 'focus' ); + + expect( scrollElement.scrollIntoView ).not.toHaveBeenCalled(); + } ); } ); diff --git a/tests/unit/components/shared/form_fields/StreetAutocompleteField.spec.ts b/tests/unit/components/shared/form_fields/StreetAutocompleteField.spec.ts index d51cc7c86..1e7e4c6a8 100644 --- a/tests/unit/components/shared/form_fields/StreetAutocompleteField.spec.ts +++ b/tests/unit/components/shared/form_fields/StreetAutocompleteField.spec.ts @@ -265,11 +265,21 @@ describe( 'StreetAutocompleteField.vue', () => { expect( field.attributes( 'aria-describedby' ) ).toStrictEqual( 'street-selected street-error' ); } ); - it( 'scrolls field into view when focused', async () => { + it( 'scrolls field into view on small size when focused', async () => { const wrapper = getWrapper( '', '12345' ); + Object.defineProperty( window, 'innerWidth', { writable: true, configurable: true, value: 769 } ); await wrapper.find( '#street' ).trigger( 'focus' ); expect( scrollElement.scrollIntoView ).toHaveBeenCalled(); } ); + + it( 'does not scroll field into view on large size when focused', async () => { + const wrapper = getWrapper( '', '12345' ); + Object.defineProperty( window, 'innerWidth', { writable: true, configurable: true, value: 770 } ); + + await wrapper.find( '#street' ).trigger( 'focus' ); + + expect( scrollElement.scrollIntoView ).not.toHaveBeenCalled(); + } ); } );