diff --git a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/main/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxThrottledProvider.java b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/main/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxThrottledProvider.java new file mode 100644 index 00000000000..8e67d4cd7c5 --- /dev/null +++ b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/main/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxThrottledProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.combobox.test; + +import java.util.stream.IntStream; + +import com.vaadin.flow.component.combobox.MultiSelectComboBox; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.data.provider.DataProvider; +import com.vaadin.flow.router.Route; + +@Route("multi-select-combo-box-throttled-provider") +public class MultiSelectComboBoxThrottledProvider extends Div { + + public MultiSelectComboBoxThrottledProvider() { + setSizeFull(); + var comboBox = new MultiSelectComboBox(); + comboBox.setItems(DataProvider.fromFilteringCallbacks(query -> { + int offset = query.getOffset(); + int limit = query.getLimit(); + int end = Math.min(offset + limit, 1000); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return IntStream.range(offset, end) + .mapToObj(i -> "Item " + (i + 1)); + }, query -> 1000)); + add(comboBox); + } +} diff --git a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/test/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxThrottledProviderIT.java b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/test/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxThrottledProviderIT.java new file mode 100644 index 00000000000..77f3af4f1f6 --- /dev/null +++ b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/test/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxThrottledProviderIT.java @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.combobox.test; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.Keys; + +import com.vaadin.flow.component.combobox.testbench.MultiSelectComboBoxElement; +import com.vaadin.flow.testutil.TestPath; +import com.vaadin.tests.AbstractComponentIT; + +@TestPath("multi-select-combo-box-throttled-provider") +public class MultiSelectComboBoxThrottledProviderIT + extends AbstractComponentIT { + private MultiSelectComboBoxElement comboBox; + + @Before + public void init() { + open(); + comboBox = $(MultiSelectComboBoxElement.class).waitForFirst(); + } + + @Test + public void selectItem_blurWhileLoading_reopen_itemsCorrectlyLoaded() { + comboBox.openPopup(); + comboBox.waitForLoadingFinished(); + // Add a filter + comboBox.sendKeys("Item"); + comboBox.waitForLoadingFinished(); + // Choose the first item + comboBox.sendKeys(Keys.DOWN, Keys.ENTER, Keys.TAB); + Assert.assertTrue(comboBox.getSelectedTexts().contains("Item 1")); + + comboBox.openPopup(); + comboBox.waitForLoadingFinished(); + Assert.assertFalse(comboBox.getOptions().isEmpty()); + } +} diff --git a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/java/com/vaadin/flow/component/combobox/ComboBoxDataController.java b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/java/com/vaadin/flow/component/combobox/ComboBoxDataController.java index a432d40e1ec..454f1e6afed 100644 --- a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/java/com/vaadin/flow/component/combobox/ComboBoxDataController.java +++ b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/java/com/vaadin/flow/component/combobox/ComboBoxDataController.java @@ -145,6 +145,8 @@ public void initialize() { // provided. private String lastFilter; + private boolean needsForceResetOnReopen = false; + private SerializableConsumer filterSlot = filter -> { // Just ignore when setDataProvider has not been called }; @@ -280,8 +282,12 @@ void setViewportRange(int start, int length, String filter) { // results in it not sending an update. However, the client needs to // receive an update in order to clear the loading state from opening // the combo box. - if (lastFilter == null) { + if (lastFilter == null + || comboBox.isOpened() && needsForceResetOnReopen) { dataCommunicator.reset(); + if (comboBox.isOpened()) { + needsForceResetOnReopen = false; + } } dataCommunicator.setViewportRange(start, length); filterSlot.accept(filter); @@ -587,7 +593,13 @@ private void setClientSideFilter(boolean clientSideFilter) { private void clearFilterOnClose(PropertyChangeEvent event) { if (Boolean.FALSE.equals(event.getValue())) { if (lastFilter != null && !lastFilter.isEmpty()) { + // Clear non-empty filter and set flag to force reset on reopen, + // protecting against late client requests that would update + // lastFilter and prevent normal reset. clearClientSideFilterAndUpdateInMemoryFilter(); + needsForceResetOnReopen = true; + } else { + needsForceResetOnReopen = false; } } }