diff --git a/src/Autocomplete/assets/dist/controller.d.ts b/src/Autocomplete/assets/dist/controller.d.ts index a60f3e94880..587407f8237 100644 --- a/src/Autocomplete/assets/dist/controller.d.ts +++ b/src/Autocomplete/assets/dist/controller.d.ts @@ -19,6 +19,7 @@ declare class export_default extends Controller { minCharacters: NumberConstructor; tomSelectOptions: ObjectConstructor; preload: StringConstructor; + resetOnFocus: BooleanConstructor; }; readonly urlValue: string; readonly optionsAsHtmlValue: boolean; @@ -31,6 +32,7 @@ declare class export_default extends Controller { readonly tomSelectOptionsValue: object; readonly hasPreloadValue: boolean; readonly preloadValue: string; + readonly resetOnFocusValue: boolean; tomSelect: TomSelect | undefined; private mutationObserver; private isObserving; diff --git a/src/Autocomplete/assets/dist/controller.js b/src/Autocomplete/assets/dist/controller.js index 514e73dcf9f..264b2b3208b 100644 --- a/src/Autocomplete/assets/dist/controller.js +++ b/src/Autocomplete/assets/dist/controller.js @@ -290,6 +290,16 @@ function _createAutocompleteWithRemoteData(autocompleteEndpointUrl, minCharacter return `
${this.createOptionTextValue.replace("%placeholder%", `${escapeData(data.input)}`)}
`; } }, + onFocus: () => { + if (this.resetOnFocusValue && this.tomSelect) { + if (this.tomSelect.control_input.value.trim() === "") { + this.tomSelect.clearOptions(); + this.tomSelect.loadedSearches = {}; + if (typeof this.tomSelect.clearPagination === "function") this.tomSelect.clearPagination(); + this.tomSelect.load(""); + } + } + }, preload: this.preload }); return _assertClassBrand(_Class_brand, this, _createTomSelect).call(this, config); @@ -333,6 +343,7 @@ _Class.values = { createOptionText: String, minCharacters: Number, tomSelectOptions: Object, - preload: String + preload: String, + resetOnFocus: Boolean }; export { _Class as default }; diff --git a/src/Autocomplete/assets/src/controller.ts b/src/Autocomplete/assets/src/controller.ts index 646e084bb1e..d0f04d067b1 100644 --- a/src/Autocomplete/assets/src/controller.ts +++ b/src/Autocomplete/assets/src/controller.ts @@ -33,6 +33,7 @@ export default class extends Controller { minCharacters: Number, tomSelectOptions: Object, preload: String, + resetOnFocus: Boolean, }; declare readonly urlValue: string; @@ -46,6 +47,7 @@ export default class extends Controller { declare readonly tomSelectOptionsValue: object; declare readonly hasPreloadValue: boolean; declare readonly preloadValue: string; + declare readonly resetOnFocusValue: boolean; tomSelect: TomSelect | undefined; private mutationObserver: MutationObserver; @@ -321,6 +323,19 @@ export default class extends Controller { return `
${this.createOptionTextValue.replace('%placeholder%', `${escapeData(data.input)}`)}
`; }, }, + onFocus: () => { + if (this.resetOnFocusValue && this.tomSelect) { + const query = this.tomSelect.control_input.value.trim(); + if (query === '') { + this.tomSelect.clearOptions(); + (this.tomSelect as any).loadedSearches = {}; + if (typeof (this.tomSelect as any).clearPagination === 'function') { + (this.tomSelect as any).clearPagination(); + } + this.tomSelect.load(''); + } + } + }, preload: this.preload, }); diff --git a/src/Autocomplete/assets/test/unit/controller.test.ts b/src/Autocomplete/assets/test/unit/controller.test.ts index ccc4f0fd71c..554c85f42e0 100644 --- a/src/Autocomplete/assets/test/unit/controller.test.ts +++ b/src/Autocomplete/assets/test/unit/controller.test.ts @@ -1206,4 +1206,103 @@ describe('AutocompleteController', () => { // but the absence of "already initialized" error is the key indicator) expect(newSelect).toBeInTheDocument(); }); + + it('reloads options on focus when resetOnFocus is enabled', async () => { + const { container, tomSelect } = await startAutocompleteTest(` + + + `); + + // first focus: initial load + fetchMock.mockResponseOnce( + JSON.stringify({ + results: [ + { value: 1, text: 'pizza' }, + { value: 2, text: 'popcorn' }, + ], + }) + ); + + const controlInput = tomSelect.control_input; + + userEvent.click(controlInput); + await waitFor(() => { + expect(container.querySelectorAll('.option[data-selectable]')).toHaveLength(2); + }); + + expect(fetchMock.requests().length).toEqual(1); + expect(fetchMock.requests()[0].url).toEqual('/path/to/autocomplete?query='); + + // simulate blur then re-focus: should reload + fetchMock.mockResponseOnce( + JSON.stringify({ + results: [ + { value: 1, text: 'pizza' }, + { value: 2, text: 'popcorn' }, + { value: 3, text: 'salad' }, + ], + }) + ); + + // trigger TomSelect's focus event which calls our onFocus callback + tomSelect.trigger('focus'); + await waitFor(() => { + expect(fetchMock.requests().length).toEqual(2); + }); + + expect(fetchMock.requests()[1].url).toEqual('/path/to/autocomplete?query='); + + // open the dropdown to render the new options + tomSelect.open(); + await waitFor(() => { + expect(container.querySelectorAll('.option[data-selectable]')).toHaveLength(3); + }); + }); + + it('does not reload options on focus when resetOnFocus is not set', async () => { + const { container, tomSelect } = await startAutocompleteTest(` + + + `); + + // first focus: initial load (preload: 'focus' is the default) + fetchMock.mockResponseOnce( + JSON.stringify({ + results: [ + { value: 1, text: 'pizza' }, + { value: 2, text: 'popcorn' }, + ], + }) + ); + + const controlInput = tomSelect.control_input; + + userEvent.click(controlInput); + await waitFor(() => { + expect(container.querySelectorAll('.option[data-selectable]')).toHaveLength(2); + }); + + expect(fetchMock.requests().length).toEqual(1); + + // blur and re-focus: should NOT make a new request + tomSelect.blur(); + await shortDelay(10); + + userEvent.click(controlInput); + await shortDelay(50); + + // still only 1 request — no reload on re-focus + expect(fetchMock.requests().length).toEqual(1); + }); }); diff --git a/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php b/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php index 711ef2e401d..5a97af2059a 100644 --- a/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php +++ b/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php @@ -94,6 +94,10 @@ public function finishView(FormView $view, FormInterface $form, array $options): $values['create-option-text'] = $this->trans($options['create_option_text']); $values['preload'] = $options['preload']; + if ($options['reset_on_focus']) { + $values['reset-on-focus'] = ''; + } + foreach ($values as $name => $value) { $attr['data-'.$controllerName.'-'.$name.'-value'] = $value; } @@ -152,6 +156,7 @@ public function configureOptions(OptionsResolver $resolver): void 'min_characters' => null, 'max_results' => 10, 'preload' => 'focus', + 'reset_on_focus' => false, 'extra_options' => [], ]); diff --git a/src/Autocomplete/tests/Fixtures/Form/ProductType.php b/src/Autocomplete/tests/Fixtures/Form/ProductType.php index c2c5b35bf18..a34c046481f 100644 --- a/src/Autocomplete/tests/Fixtures/Form/ProductType.php +++ b/src/Autocomplete/tests/Fixtures/Form/ProductType.php @@ -54,6 +54,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'createOnBlur' => true, ], ]) + ->add('portionSizeResetOnFocus', ChoiceType::class, [ + 'choices' => [ + 'small' => 's', + 'medium' => 'm', + 'large' => 'l', + ], + 'autocomplete' => true, + 'reset_on_focus' => true, + 'mapped' => false, + ]) ; } diff --git a/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php b/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php index 126ac7280b4..0d5294975e5 100644 --- a/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php +++ b/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php @@ -38,6 +38,9 @@ public function testFieldsRenderWithStimulusController() ->assertElementAttributeContains('#product_portionSize', 'data-controller', 'symfony--ux-autocomplete--autocomplete') ->assertElementAttributeContains('#product_tags', 'data-controller', 'symfony--ux-autocomplete--autocomplete') ->assertElementAttributeContains('#product_tags', 'data-symfony--ux-autocomplete--autocomplete-tom-select-options-value', 'createOnBlur') + + ->assertElementAttributeContains('#product_portionSizeResetOnFocus', 'data-controller', 'symfony--ux-autocomplete--autocomplete') + ->assertElementAttributeContains('#product_portionSizeResetOnFocus', 'data-symfony--ux-autocomplete--autocomplete-reset-on-focus-value', '') ; }