diff --git a/packages/snap-controller/src/Autocomplete/AutocompleteController.test.ts b/packages/snap-controller/src/Autocomplete/AutocompleteController.test.ts index 7a3be78ef..da8c4be44 100644 --- a/packages/snap-controller/src/Autocomplete/AutocompleteController.test.ts +++ b/packages/snap-controller/src/Autocomplete/AutocompleteController.test.ts @@ -11,6 +11,7 @@ import { AutocompleteController, INPUT_DELAY as _INPUT_DELAY } from './Autocompl import { waitFor } from '@testing-library/preact'; import { MockClient } from '@searchspring/snap-shared'; +import deepmerge from 'deepmerge'; const KEY_ENTER = 13; const KEY_ESCAPE = 27; @@ -226,6 +227,35 @@ describe('Autocomplete Controller', () => { expect(controller.urlManager.state.query).toBe(undefined); }); + it('can opt out of binding input event', async () => { + const bindingConfig = deepmerge(acConfig, { settings: { bind: { input: false } } }); + + const controller = new AutocompleteController(bindingConfig, { + client: new MockClient(globals, {}), + store: new AutocompleteStore(bindingConfig, services), + urlManager, + eventManager: new EventManager(), + profiler: new Profiler(), + logger: new Logger(), + tracker: new Tracker(globals), + }); + + await controller.bind(); + + let inputEl: HTMLInputElement | null; + + await waitFor(() => { + inputEl = document.querySelector(controller.config.selector); + expect(inputEl).toBeDefined(); + }); + + const query = 'bumpers'; + inputEl!.value = query; + inputEl!.focus(); + inputEl!.dispatchEvent(new Event('input')); + expect(controller.urlManager.state.query).toBe(undefined); + }); + it('can bind to input after input has been focused', async () => { const controller = new AutocompleteController(acConfig, { client: new MockClient(globals, {}), @@ -672,6 +702,85 @@ describe('Autocomplete Controller', () => { beforeSubmitfn.mockClear(); }); + it('can opt out of submit event', async () => { + document.body.innerHTML = '
'; + + const bindingConfig = deepmerge(acConfig, { settings: { bind: { submit: false } } }); + + const controller = new AutocompleteController(bindingConfig, { + client: new MockClient(globals, {}), + store: new AutocompleteStore(bindingConfig, services), + urlManager, + eventManager: new EventManager(), + profiler: new Profiler(), + logger: new Logger(), + tracker: new Tracker(globals), + }); + + await controller.bind(); + (controller.client as MockClient).mockData.updateConfig({ autocomplete: 'autocomplete.query.bumpers' }); + + const inputEl: HTMLInputElement | null = document.querySelector(controller.config.selector); + + const query = 'bumpers'; + inputEl!.value = query; + + const form = inputEl!.form; + const beforeSubmitfn = jest.spyOn(controller.eventManager, 'fire'); + const handlerSubmitfn = jest.spyOn(controller.handlers.input, 'formSubmit'); + + form?.dispatchEvent(new Event('submit', { bubbles: true })); + //this timeout seems to be needed. Cant replace with waitFor + await new Promise((resolve) => setTimeout(resolve, INPUT_DELAY)); + + expect(beforeSubmitfn).not.toHaveBeenCalledWith('beforeSubmit', { + controller, + input: inputEl!, + }); + + expect(handlerSubmitfn).not.toHaveBeenCalled(); + + beforeSubmitfn.mockClear(); + }); + + it('can opt out of submit event (with no form)', async () => { + const bindingConfig = deepmerge(acConfig, { action: '/search', settings: { bind: { submit: false } } }); + + const controller = new AutocompleteController(bindingConfig, { + client: new MockClient(globals, {}), + store: new AutocompleteStore(bindingConfig, services), + urlManager, + eventManager: new EventManager(), + profiler: new Profiler(), + logger: new Logger(), + tracker: new Tracker(globals), + }); + + await controller.bind(); + (controller.client as MockClient).mockData.updateConfig({ autocomplete: 'autocomplete.query.bumpers' }); + + const beforeSubmitfn = jest.spyOn(controller.eventManager, 'fire'); + const enterKeyfn = jest.spyOn(controller.handlers.input, 'enterKey'); + const inputEl: HTMLInputElement | null = document.querySelector(controller.config.selector); + + const query = 'bumpers'; + inputEl!.value = query; + + inputEl!.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, keyCode: KEY_ENTER })); + + // this timeout seems to be needed. Cant replace with waitFor + await new Promise((resolve) => setTimeout(resolve, INPUT_DELAY)); + + expect(beforeSubmitfn).not.toHaveBeenCalledWith('beforeSubmit', { + controller, + input: inputEl!, + }); + + expect(enterKeyfn).not.toHaveBeenCalled(); + + beforeSubmitfn.mockClear(); + }); + it('adds fallback query when integrated spell correct setting is enabled', async () => { let acConfig2 = { ...acConfig, settings: { integratedSpellCorrection: true } }; diff --git a/packages/snap-controller/src/Autocomplete/AutocompleteController.ts b/packages/snap-controller/src/Autocomplete/AutocompleteController.ts index d452007bc..f724dc847 100644 --- a/packages/snap-controller/src/Autocomplete/AutocompleteController.ts +++ b/packages/snap-controller/src/Autocomplete/AutocompleteController.ts @@ -35,6 +35,10 @@ const defaultConfig: AutocompleteControllerConfig = { merchandising: true, singleResult: false, }, + bind: { + input: true, + submit: true, + }, }, }; @@ -445,7 +449,7 @@ export class AutocompleteController extends AbstractController { input.setAttribute(INPUT_ATTRIBUTE, ''); - input.addEventListener('input', this.handlers.input.input); + this.config.settings?.bind?.input && input.addEventListener('input', this.handlers.input.input); if (this.config?.settings?.initializeFromUrl && !input.value && this.store.state.input) { input.value = this.store.state.input; @@ -458,10 +462,10 @@ export class AutocompleteController extends AbstractController { let formActionUrl: string | undefined; if (this.config.action) { - input.addEventListener('keydown', this.handlers.input.enterKey); + this.config.settings?.bind?.submit && input.addEventListener('keydown', this.handlers.input.enterKey); formActionUrl = this.config.action; } else if (form) { - form.addEventListener('submit', this.handlers.input.formSubmit as unknown as EventListener); + this.config.settings?.bind?.submit && form.addEventListener('submit', this.handlers.input.formSubmit as unknown as EventListener); formActionUrl = form.action || ''; // serializeForm will include additional form element in our urlManager as globals diff --git a/packages/snap-controller/src/Autocomplete/README.md b/packages/snap-controller/src/Autocomplete/README.md index bdb0045cc..f0f7dd983 100644 --- a/packages/snap-controller/src/Autocomplete/README.md +++ b/packages/snap-controller/src/Autocomplete/README.md @@ -22,6 +22,8 @@ The `AutocompleteController` is used when making queries to the API `autocomplet | settings.history.showResults | if history limit is set and there is no input, the first term results will be displayed | false | | | settings.redirects.merchandising | boolean to disable merchandising redirects when ac form is submitted | true | | | settings.redirects.singleResult | enable redirect to product detail page if search yields 1 result count | false | | +| settings.bind.input | boolean to disable binding of the input element (selector) | true | | +| settings.bind.submit | boolean to disable binding of the submit event (form submission of enter key press) | true | | | settings.variants.field | used to set the field in which to grab the variant data from | ➖ | | | settings.variants.realtime.enabled | enable real time variant updates | ➖ | | | settings.variants.realtime.filters | specify which filters to use to determine which results are updated | ➖ | | diff --git a/packages/snap-store-mobx/src/types.ts b/packages/snap-store-mobx/src/types.ts index 5c0770cab..bac85f308 100644 --- a/packages/snap-store-mobx/src/types.ts +++ b/packages/snap-store-mobx/src/types.ts @@ -124,6 +124,10 @@ export type AutocompleteStoreConfigSettings = { merchandising?: boolean; singleResult?: boolean; }; + bind?: { + input?: boolean; + submit?: boolean; + }; }; // Autocomplete config