Skip to content

Commit

Permalink
Make autocomplete autoscroll on focus mobile only
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Abban committed Sep 13, 2024
1 parent 8265a48 commit ea52ef7
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 24 deletions.
9 changes: 2 additions & 7 deletions src/components/shared/form_fields/CityAutocompleteField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -85,6 +86,7 @@ const ariaDescribedby = useAriaDescribedby(
`${props.inputId}-error`,
computed<boolean>( () => props.showError )
);
const scrollIntoView = useAutocompleteScrollIntoViewOnFocus( props.scrollTargetId, autoscrollMaxWidth );
const placeholder = computed( () => {
if ( cities.value.length > 0 ) {
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -94,18 +95,12 @@ const ariaDescribedby = useAriaDescribedby(
`${props.inputId}-error`,
computed<boolean>( () => 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 = '';
Expand Down
9 changes: 2 additions & 7 deletions src/components/shared/form_fields/StreetAutocompleteField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -112,6 +113,7 @@ const ariaDescribedby = useAriaDescribedby(
`${props.inputIdStreetName}-error`,
computed<boolean>( () => props.showError )
);
const scrollIntoView = useAutocompleteScrollIntoViewOnFocus( props.scrollTargetId, autoscrollMaxWidth );
const filteredStreets = computed<Array<string>>( () => {
const streetList = streets.value.filter( ( streetItem: string ) => {
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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' } );
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>( '#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<HTMLInputElement>( '#city' ).trigger( 'focus' );

expect( scrollElement.scrollIntoView ).not.toHaveBeenCalled();
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>( '#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<HTMLInputElement>( '#country' ).trigger( 'focus' );

expect( scrollElement.scrollIntoView ).not.toHaveBeenCalled();
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>( '#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<HTMLInputElement>( '#street' ).trigger( 'focus' );

expect( scrollElement.scrollIntoView ).not.toHaveBeenCalled();
} );
} );

0 comments on commit ea52ef7

Please sign in to comment.