Skip to content

Commit

Permalink
Search with autocomplete: Work around Enter edge case
Browse files Browse the repository at this point in the history
The `accessible-autocomplete` component has an edge case where when the
menu is visible, it `preventDefault()`s on the Enter key event, even if
the user hasn't put keyboard focus on a suggestion. This results in a
scenario where the user types something, does _not_ interact with the
autocomplete menu at all, and then hits Enter to try to submit the form
- but it isn't submitted.

This is not something we can fix upstream as it is usually desirable
behaviour for the library, so we implement a workaround that adds our
own event listener and ensure `requestSubmit` is run for the form if it
hasn't been already.

- Add event listener for `keydown` event and submit the containing form
  ourselves if the key is `Enter`
- Split out code meant to run on accepting a suggestion (`onConfirm`)
  from form submission logic (`submitContainingForm`) so we can reuse
  the latter
- Add a check to `submitContainingForm` that it hasn't already been
  called for the component anyway
  • Loading branch information
csutter committed Nov 7, 2024
1 parent 671ef33 commit f8264b1
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

* Add GA4 tracking for search autocomplete ([PR #4371](https://github.com/alphagov/govuk_publishing_components/pull/4371))
* Append no-actions class to rows without actions in Summary Cards ([PR #4368](https://github.com/alphagov/govuk_publishing_components/pull/4368))
* Search with autocomplete: Work around Enter edge case ([PR #4372](https://github.com/alphagov/govuk_publishing_components/pull/4372))

## 45.0.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};

this.sourceUrl = this.$module.getAttribute('data-source-url')
this.sourceKey = this.$module.getAttribute('data-source-key')

this.isSubmitting = false
}

init () {
Expand All @@ -28,7 +30,7 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
confirmOnBlur: false,
showNoOptionsFound: false,
source: this.getResults.bind(this),
onConfirm: this.submitContainingForm.bind(this),
onConfirm: this.onConfirm.bind(this),
templates: {
suggestion: this.constructSuggestionHTMLString.bind(this)
},
Expand All @@ -54,6 +56,19 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
this.$autocompleteInput.setAttribute('type', 'search')
// Remove the original input from the DOM
this.$originalInput.parentNode.removeChild(this.$originalInput)

// The accessible-autocomplete component has an edge case where when the menu is visible, it
// prevents default on the Enter key event, even if the user hasn't put keyboard focus on a
// suggestion. This results in a scenario where the user types something, does _not_ interact
// with the autocomplete menu at all, and then hits Enter to try to submit the form - but it
// isn't submitted.
//
// This manually triggers our form submission logic when the Enter key is pressed as a
// workaround (which will do nothing if the form is already in the process of submitting
// through `onConfirm` because the user has accepted a suggestion).
this.$autocompleteInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') this.submitContainingForm()
})
}

// Callback used by accessible-autocomplete to generate the HTML for each suggestion based on
Expand Down Expand Up @@ -109,16 +124,24 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};

// Callback used by accessible-autocomplete to submit the containing form when a suggestion is
// confirmed by the user (e.g. by pressing Enter or clicking on it)
submitContainingForm (value) {
onConfirm (value) {
// The accessible-autocomplete component calls this callback _before_ it updates its
// internal state, so the value of the input field is not yet updated when this callback is
// called. We need to force the value to be updated before submitting the form, but the rest
// of the state can catch up later.
this.$autocompleteInput.value = value

this.$autocompleteInput.dataset.autocompleteAccepted = true
this.submitContainingForm()
}

if (this.$form) {
// The accessible-autocomplete component calls this callback _before_ it updates its
// internal state, so the value of the input field is not yet updated when this callback is
// called. We need to force the value to be updated before submitting the form, but the rest
// of the state can catch up later.
this.$autocompleteInput.value = value
// Submit the containing form, if one exists and the component is not already in the process of
// submitting
submitContainingForm () {
if (this.isSubmitting) return
this.isSubmitting = true

if (this.$form) {
if (this.$form.requestSubmit) {
this.$form.requestSubmit()
} else {
Expand Down
23 changes: 21 additions & 2 deletions spec/javascripts/components/search-with-autocomplete-spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-env jasmine */
/* global GOVUK, Event, FormData */
/* global GOVUK, Event, FormData, KeyboardEvent */

describe('Search with autocomplete component', () => {
let autocomplete, fixture
Expand Down Expand Up @@ -209,13 +209,32 @@ describe('Search with autocomplete component', () => {
const form = fixture.querySelector('form')
const submitSpy = spyOn(form, 'requestSubmit')

autocomplete.submitContainingForm('updated value')
autocomplete.onConfirm('updated value')

const formData = new FormData(form)
expect(formData.get('q')).toEqual('updated value')
expect(submitSpy).toHaveBeenCalled()
})

it('triggers a requestSubmit if Enter is pressed in the search field to work around library bug', (done) => {
const form = fixture.querySelector('form')
const input = fixture.querySelector('input')
const submitSpy = spyOn(form, 'requestSubmit')

stubSuccessfulFetch(['i am an undesirable result'])
performInput(input, 'i just want to search the old-fashioned way', () => {
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
bubbles: true,
cancelable: true
})
input.dispatchEvent(enterEvent)

expect(submitSpy).toHaveBeenCalled()
done()
})
})

describe('analytics data attributes', () => {
it('sets data attributes on the input when suggestions are returned', (done) => {
const input = fixture.querySelector('input')
Expand Down

0 comments on commit f8264b1

Please sign in to comment.