diff --git a/CHANGELOG.md b/CHANGELOG.md index 888acb1e7b..7db0d8d2a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/assets/javascripts/govuk_publishing_components/components/search-with-autocomplete.js b/app/assets/javascripts/govuk_publishing_components/components/search-with-autocomplete.js index bb9be2c02a..9e39d47e08 100644 --- a/app/assets/javascripts/govuk_publishing_components/components/search-with-autocomplete.js +++ b/app/assets/javascripts/govuk_publishing_components/components/search-with-autocomplete.js @@ -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 () { @@ -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) }, @@ -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 @@ -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 { diff --git a/spec/javascripts/components/search-with-autocomplete-spec.js b/spec/javascripts/components/search-with-autocomplete-spec.js index 6ecf3e554b..6fa15c8365 100644 --- a/spec/javascripts/components/search-with-autocomplete-spec.js +++ b/spec/javascripts/components/search-with-autocomplete-spec.js @@ -1,5 +1,5 @@ /* eslint-env jasmine */ -/* global GOVUK, Event, FormData */ +/* global GOVUK, Event, FormData, KeyboardEvent */ describe('Search with autocomplete component', () => { let autocomplete, fixture @@ -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')