From 07299ae9184fca8a5562d9343e3a175b6740a71c Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Wed, 27 Nov 2024 16:55:45 +0200 Subject: [PATCH 01/10] feat: virtual list item selection --- .../pom.xml | 5 + .../tests/VirtualListSelectionPage.java | 77 +++++ .../vaadin-virtual-list-flow/pom.xml | 5 + .../component/virtuallist/VirtualList.java | 313 ++++++++++++++++++ .../VirtualListMultiSelectionModel.java | 264 +++++++++++++++ .../VirtualListNoneSelectionModel.java | 68 ++++ .../VirtualListSingleSelectionModel.java | 160 +++++++++ .../frontend/virtualListConnector.js | 41 ++- .../tests/VirtualListMultiSelectionTest.java | 190 +++++++++++ .../tests/VirtualListNoneSelectionTest.java | 60 ++++ .../tests/VirtualListSelectionTest.java | 56 ++++ .../tests/VirtualListSingleSelectionTest.java | 106 ++++++ 12 files changed, 1341 insertions(+), 4 deletions(-) create mode 100644 vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java create mode 100644 vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListMultiSelectionModel.java create mode 100644 vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListNoneSelectionModel.java create mode 100644 vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java create mode 100644 vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListMultiSelectionTest.java create mode 100644 vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListNoneSelectionTest.java create mode 100644 vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionTest.java create mode 100644 vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/pom.xml b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/pom.xml index 68ac366ae7a..5a77402eeab 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/pom.xml +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/pom.xml @@ -27,6 +27,11 @@ com.vaadin flow-html-components + + com.vaadin + vaadin-lumo-theme + ${project.version} + com.vaadin flow-data diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java new file mode 100644 index 00000000000..959cdfb5780 --- /dev/null +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java @@ -0,0 +1,77 @@ +/* + * Copyright 2000-2024 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.virtuallist.tests; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.virtuallist.VirtualList; +import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; +import com.vaadin.flow.data.provider.DataProvider; +import com.vaadin.flow.data.renderer.LitRenderer; +import com.vaadin.flow.router.Route; + +/** + * Test view for {@link VirtualList} + * + * @author Vaadin Ltd. + */ +@Route("vaadin-virtual-list/virtual-list-selection") +public class VirtualListSelectionPage extends Div { + + public VirtualListSelectionPage() { + var list = new VirtualList(); + list.setHeight("200px"); + + var items = createItems(); + list.setDataProvider(DataProvider.ofCollection(items)); + + list.setRenderer(LitRenderer. of("
${item.name}
") + .withProperty("name", item -> item.name)); + + list.setSelectionMode(SelectionMode.MULTI); + + list.addSelectionListener(e -> { + System.out.println("Selected items: " + e.getAllSelectedItems()); + }); + + add(list); + + var button = new NativeButton("Select second item", e -> { + list.select(items.get(1)); + }); + add(button); + + var printSelectionButton = new NativeButton("Print selection", e -> { + System.out.println("Selected item: " + + list.getSelectionModel().getSelectedItems()); + }); + add(printSelectionButton); + + } + + private List createItems() { + return IntStream.range(0, 1000).mapToObj(i -> new Item("Item " + i, i)) + .collect(Collectors.toList()); + } + + public static record Item(String name, int value) { + }; + +} diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/pom.xml b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/pom.xml index 1c5b7a9253d..37e6be9de79 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/pom.xml +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/pom.xml @@ -21,6 +21,11 @@ vaadin-renderer-flow ${project.version}
+ + org.mockito + mockito-core + test + com.vaadin flow-test-generic diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java index a6f28ab6f42..6f66c36ed93 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java @@ -19,9 +19,14 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import com.vaadin.flow.component.ClientCallable; import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.ComponentUtil; import com.vaadin.flow.component.Focusable; import com.vaadin.flow.component.HasSize; import com.vaadin.flow.component.HasStyle; @@ -30,6 +35,7 @@ import com.vaadin.flow.component.dependency.JsModule; import com.vaadin.flow.component.dependency.NpmPackage; import com.vaadin.flow.component.virtuallist.paging.PagelessDataCommunicator; +import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.data.binder.HasDataProvider; import com.vaadin.flow.data.provider.ArrayUpdater; import com.vaadin.flow.data.provider.ArrayUpdater.Update; @@ -39,6 +45,15 @@ import com.vaadin.flow.data.renderer.ComponentRenderer; import com.vaadin.flow.data.renderer.LitRenderer; import com.vaadin.flow.data.renderer.Renderer; +import com.vaadin.flow.data.selection.MultiSelect; +import com.vaadin.flow.data.selection.MultiSelectionEvent; +import com.vaadin.flow.data.selection.MultiSelectionListener; +import com.vaadin.flow.data.selection.SelectionListener; +import com.vaadin.flow.data.selection.SelectionModel; +import com.vaadin.flow.data.selection.SelectionModel.Single; +import com.vaadin.flow.data.selection.SingleSelect; +import com.vaadin.flow.data.selection.SingleSelectionEvent; +import com.vaadin.flow.data.selection.SingleSelectionListener; import com.vaadin.flow.dom.DisabledUpdateMode; import com.vaadin.flow.function.ValueProvider; import com.vaadin.flow.internal.JsonUtils; @@ -46,6 +61,7 @@ import com.vaadin.flow.shared.Registration; import elemental.json.Json; +import elemental.json.JsonArray; import elemental.json.JsonValue; /** @@ -119,6 +135,9 @@ public void initialize() { private Renderer renderer; + private SelectionMode selectionMode; + private SelectionModel, T> selectionModel; + private final CompositeDataGenerator dataGenerator = new CompositeDataGenerator<>(); private final List renderingRegistrations = new ArrayList<>(); private transient T placeholderItem; @@ -134,6 +153,11 @@ public void initialize() { public VirtualList() { setRenderer((ValueProvider) String::valueOf); addAttachListener((e) -> this.setPlaceholderItem(this.placeholderItem)); + + // Use NONE selection mode by default + setSelectionMode(SelectionMode.NONE); + + initSelection(); } private void initConnector() { @@ -144,6 +168,38 @@ private void initConnector() { getElement()); } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void initSelection() { + // Generate "selected" property for selected items + dataGenerator.addDataGenerator((item, jsonObject) -> { + if (this.getSelectionModel().isSelected(item)) { + jsonObject.put("selected", true); + } + }); + + // Set up SingleSelectionEvent and MultiSelectionEvent listeners to + // refresh the items in data communicator when selection changes. This + // is to ensure the data generator labels selected items with "selected" + // property on selection change. + ComponentUtil.addListener(this, SingleSelectionEvent.class, + (ComponentEventListener) ((ComponentEventListener, T>>) event -> { + Stream.of(event.getValue(), event.getOldValue()) + .filter(Objects::nonNull) + .forEach(item -> getDataCommunicator() + .refresh((T) item)); + })); + + ComponentUtil.addListener(this, MultiSelectionEvent.class, + (ComponentEventListener) ((ComponentEventListener, T>>) event -> { + Stream.concat(event.getAddedSelection().stream(), + event.getRemovedSelection().stream()) + .filter(Objects::nonNull) + .forEach(item -> getDataCommunicator() + .refresh((T) item)); + })); + + } + @Override public void setDataProvider(DataProvider dataProvider) { Objects.requireNonNull(dataProvider, "The dataProvider cannot be null"); @@ -324,4 +380,261 @@ public void scrollToStart() { public void scrollToEnd() { scrollToIndex(Integer.MAX_VALUE); } + + /** + * Returns the selection mode for this virtual list. + * + * @return the selection mode, not null + */ + public SelectionMode getSelectionMode() { + assert selectionMode != null : "No selection mode set by " + + getClass().getName() + " constructor"; + return selectionMode; + } + + /** + * Sets the virtual list's selection mode. + * + * @param selectionMode + * the selection mode to switch to, not {@code null} + * @return the used selection model + * + * @see SelectionMode + * @see VirtualListSingleSelectionModel + * @see VirtualListMultiSelectionModel + * @see VirtualListNoneSelectionModel + */ + public SelectionModel, T> setSelectionMode( + SelectionMode selectionMode) { + Objects.requireNonNull(selectionMode, "Selection mode cannot be null."); + + if (selectionMode == SelectionMode.SINGLE) { + setSelectionModel(new VirtualListSingleSelectionModel<>(this), + selectionMode); + } else if (selectionMode == SelectionMode.MULTI) { + setSelectionModel(new VirtualListMultiSelectionModel<>(this), + selectionMode); + } else { + setSelectionModel(new VirtualListNoneSelectionModel<>(), + selectionMode); + } + return selectionModel; + } + + private Set getItemsFromKeys(JsonArray keys) { + return JsonUtils.stream(keys).map( + key -> getDataCommunicator().getKeyMapper().get(key.asString())) + .filter(Objects::nonNull).collect(Collectors.toSet()); + } + + @SuppressWarnings("unchecked") + @ClientCallable + void updateSelection(JsonArray addedKeys, JsonArray removedKeys) { + var addedItems = getItemsFromKeys(addedKeys); + var removedItems = getItemsFromKeys(removedKeys); + + if (selectionModel instanceof VirtualListSingleSelectionModel model) { + model.setSelectedItem( + addedItems.isEmpty() ? null : addedItems.iterator().next()); + } else if (selectionModel instanceof VirtualListMultiSelectionModel model) { + model.updateSelection(addedItems, removedItems); + } + } + + /** + * Sets the selection model for the virtual list. + *

+ * The default selection model is {@link VirtualListNoneSelectionModel}. + * + * @param model + * the selection model to use, not {@code null} + * @param selectionMode + * the selection mode this selection model corresponds to, not + * {@code null} + * + * @see #setSelectionMode(SelectionMode) + */ + private void setSelectionModel(SelectionModel, T> model, + SelectionMode selectionMode) { + Objects.requireNonNull(model, "selection model cannot be null"); + Objects.requireNonNull(selectionMode, "selection mode cannot be null"); + + selectionModel = model; + this.selectionMode = selectionMode; + + getElement().setProperty("selectionMode", + SelectionMode.NONE.equals(selectionMode) ? null + : selectionMode.name().toLowerCase()); + + } + + /** + * Adds a selection listener to the current selection model. + *

+ * This is a shorthand for + * {@code virtualList.getSelectionModel().addSelectionListener()}. To get + * more detailed selection events, use {@link #getSelectionModel()} and + * either + * {@link VirtualListSingleSelectionModel#addSingleSelectionListener(SingleSelectionListener)} + * or + * {@link VirtualListMultiSelectionModel#addMultiSelectionListener(MultiSelectionListener)} + * depending on the used selection mode. + * + * @param listener + * the listener to add + * @return a registration handle to remove the listener + * @throws UnsupportedOperationException + * if {@link SelectionMode#NONE} is in use + */ + public Registration addSelectionListener( + SelectionListener, T> listener) { + return getSelectionModel().addSelectionListener(listener); + } + + /** + * Use this virtual list as a single select in {@link Binder}. + *

+ * Throws {@link IllegalStateException} if the virtual list is not using a + * {@link VirtualListSingleSelectionModel}. + * + * @return the single select wrapper that can be used in binder + * @throws IllegalStateException + * if not using a single selection model + */ + public SingleSelect, T> asSingleSelect() { + var model = getSelectionModel(); + if (!(model instanceof VirtualListSingleSelectionModel)) { + throw new IllegalStateException( + "VirtualList is not in single select mode, " + + "it needs to be explicitly set to such with " + + "setSelectionMode(SelectionMode.SINGLE) before " + + "being able to use single selection features."); + } + return ((VirtualListSingleSelectionModel) model).asSingleSelect(); + } + + /** + * Use this virtual list as a multiselect in {@link Binder}. + *

+ * Throws {@link IllegalStateException} if the virtual list is not using a + * {@link VirtualListMultiSelectionModel}. + * + * @return the multiselect wrapper that can be used in binder + * @throws IllegalStateException + * if not using a multiselection model + */ + public MultiSelect, T> asMultiSelect() { + var model = getSelectionModel(); + if (!(model instanceof VirtualListMultiSelectionModel)) { + throw new IllegalStateException( + "VirtualList is not in multi select mode, " + + "it needs to be explicitly set to such with " + + "setSelectionMode(SelectionMode.MULTI) before " + + "being able to use multi selection features."); + } + return ((VirtualListMultiSelectionModel) model).asMultiSelect(); + } + + /** + * This method is a shorthand that delegates to the currently set selection + * model. + * + * @see #getSelectionModel() + * @see VirtualListSingleSelectionModel#getSelectedItems() + * @see VirtualListMultiSelectionModel#getSelectedItems() + * + * @return a set with the selected items, never null + */ + public Set getSelectedItems() { + return getSelectionModel().getSelectedItems(); + } + + /** + * This method is a shorthand that delegates to the currently set selection + * model. + * + * @param item + * the item to select, not null + * + * @see #getSelectionModel() + * @see VirtualListSingleSelectionModel#select(Object) + * @see VirtualListMultiSelectionModel#select(Object) + */ + public void select(T item) { + getSelectionModel().select(item); + } + + /** + * This method is a shorthand that delegates to the currently set selection + * model. + * + * @param item + * the item to deselect, not null + * + * @see #getSelectionModel() + * @see VirtualListSingleSelectionModel#deselect(Object) + * @see VirtualListMultiSelectionModel#deselect(Object) + */ + public void deselect(T item) { + getSelectionModel().deselect(item); + } + + /** + * This method is a shorthand that delegates to the currently set selection + * model. + * + * @see #getSelectionModel() + * @see VirtualListSingleSelectionModel#deselectAll() + * @see VirtualListMultiSelectionModel#deselectAll() + */ + public void deselectAll() { + getSelectionModel().deselectAll(); + } + + /** + * Returns the selection model for this virtual list. + * + * @return the selection model, not null + */ + public SelectionModel, T> getSelectionModel() { + assert selectionModel != null : "No selection model set by " + + getClass().getName() + " constructor"; + return selectionModel; + } + + /** + * Selection mode representing the built-in selection models in virtual + * list. + *

+ * These enums can be used in + * {@link VirtualList#setSelectionMode(SelectionMode)} to easily switch + * between the built-in selection models. + * + * @see VirtualList#setSelectionMode(SelectionMode) + * @see VirtualList#setSelectionModel(SelectionModel, SelectionMode) + */ + public enum SelectionMode { + + /** + * Single selection mode that maps to built-in {@link Single}. + * + * @see VirtualListSingleSelectionModel + */ + SINGLE, + + /** + * Multiselection mode that maps to built-in + * {@link SelectionModel.Multi}. + * + * @see VirtualListMultiSelectionModelπ + */ + MULTI, + + /** + * Selection model that doesn't allow selection. + * + * @see VirtualListNoneSelectionModel + */ + NONE; + } } diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListMultiSelectionModel.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListMultiSelectionModel.java new file mode 100644 index 00000000000..066dd3511a7 --- /dev/null +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListMultiSelectionModel.java @@ -0,0 +1,264 @@ +/* + * Copyright 2000-2024 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.virtuallist; + +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.vaadin.flow.component.AbstractField.ComponentValueChangeEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.data.selection.MultiSelect; +import com.vaadin.flow.data.selection.MultiSelectionEvent; +import com.vaadin.flow.data.selection.MultiSelectionListener; +import com.vaadin.flow.data.selection.SelectionEvent; +import com.vaadin.flow.data.selection.SelectionListener; +import com.vaadin.flow.data.selection.SelectionModel; +import com.vaadin.flow.dom.Element; +import com.vaadin.flow.shared.Registration; + +/** + * Implementation of a SelectionModel.Multi. + * + * @param + * the virtual list bean type + * @author Vaadin Ltd. + */ +public class VirtualListMultiSelectionModel + implements SelectionModel.Multi, T> { + + private final Map selected; + private VirtualList list; + + /** + * Constructor for passing a reference of the virtual list to this + * implementation. + * + * @param list + * reference to the virtual list for which this selection model + * is created + */ + public VirtualListMultiSelectionModel(VirtualList list) { + this.list = list; + selected = new LinkedHashMap<>(); + } + + @Override + public Set getSelectedItems() { + /* + * A new LinkedHashSet is created to avoid + * ConcurrentModificationExceptions when changing the selection during + * an iteration + */ + return Collections + .unmodifiableSet(new LinkedHashSet<>(selected.values())); + } + + /** + * Returns an unmodifiable view of the selected item ids. + *

+ * Exposed to be overridden within subclasses. + *

+ * The returned Set may be a direct view of the internal data structures of + * this class. A defensive copy should be made by callers when iterating + * over this Set and modifying the selection during iteration to avoid + * ConcurrentModificationExceptions. + * + * @return An unmodifiable view of the selected item ids. Updates in the + * selection may or may not be directly reflected in the Set. + */ + protected Set getSelectedItemIds() { + return Collections.unmodifiableSet(this.selected.keySet()); + } + + @Override + public Optional getFirstSelectedItem() { + return selected.values().stream().findFirst(); + } + + @Override + public void select(T item) { + if (isSelected(item)) { + return; + } + Set selected = new HashSet<>(); + if (item != null) { + selected.add(item); + } + + doUpdateSelection(selected, Collections.emptySet(), false); + } + + @Override + public void deselect(T item) { + if (!isSelected(item)) { + return; + } + Set deselected = new HashSet<>(); + if (item != null) { + deselected.add(item); + } + doUpdateSelection(Collections.emptySet(), deselected, false); + } + + @Override + public void selectAll() { + updateSelection( + (Set) list.getDataCommunicator().getDataProvider() + .fetch(list.getDataCommunicator().buildQuery(0, + Integer.MAX_VALUE)) + .collect(Collectors.toSet()), + Collections.emptySet()); + } + + @Override + public void deselectAll() { + updateSelection(Collections.emptySet(), getSelectedItems()); + } + + @Override + public void updateSelection(Set addedItems, Set removedItems) { + Objects.requireNonNull(addedItems, "added items cannot be null"); + Objects.requireNonNull(removedItems, "removed items cannot be null"); + doUpdateSelection(addedItems, removedItems, false); + } + + private Map mapItemsById(Set items) { + return items.stream().collect(LinkedHashMap::new, + (map, item) -> map.put(this.getItemId(item), item), + Map::putAll); + } + + private void doUpdateSelection(Set addedItems, Set removedItems, + boolean userOriginated) { + Map addedItemsMap = mapItemsById(addedItems); + Map removedItemsMap = mapItemsById(removedItems); + addedItemsMap.keySet().stream().filter(removedItemsMap::containsKey) + .collect(Collectors.toList()).forEach(key -> { + addedItemsMap.remove(key); + removedItemsMap.remove(key); + }); + doUpdateSelection(addedItemsMap, removedItemsMap, userOriginated); + } + + private void doUpdateSelection(Map addedItems, + Map removedItems, boolean userOriginated) { + if (selected.keySet().containsAll(addedItems.keySet()) && Collections + .disjoint(selected.keySet(), removedItems.keySet())) { + return; + } + + Set oldSelection = getSelectedItems(); + removedItems.keySet().forEach(selected::remove); + selected.putAll(addedItems); + + ComponentUtil.fireEvent(list, new MultiSelectionEvent<>(list, + asMultiSelect(), oldSelection, userOriginated)); + } + + @Override + public boolean isSelected(T item) { + return selected.containsKey(getItemId(item)); + } + + public void setSelectedItems(Set items) { + var oldValue = getSelectedItems(); + selected.clear(); + items.forEach(item -> selected.put(getItemId(item), item)); + + // TODO: This should not be here + ComponentUtil.fireEvent(list, new MultiSelectionEvent<>(list, + asMultiSelect(), oldValue, true)); + + } + + public MultiSelect, T> asMultiSelect() { + return new MultiSelect, T>() { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Registration addValueChangeListener( + ValueChangeListener, Set>> listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + + ComponentEventListener componentEventListener = event -> listener + .valueChanged( + (ComponentValueChangeEvent, Set>) event); + + return ComponentUtil.addListener(list, + MultiSelectionEvent.class, componentEventListener); + } + + @Override + public Registration addSelectionListener( + MultiSelectionListener, T> listener) { + return addMultiSelectionListener(listener); + } + + @Override + public void deselectAll() { + VirtualListMultiSelectionModel.this.deselectAll(); + } + + @Override + public void updateSelection(Set addedItems, + Set removedItems) { + VirtualListMultiSelectionModel.this.updateSelection(addedItems, + removedItems); + } + + @Override + public Element getElement() { + return list.getElement(); + } + + @Override + public Set getSelectedItems() { + return VirtualListMultiSelectionModel.this.getSelectedItems(); + } + }; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Registration addSelectionListener( + SelectionListener, T> listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + return ComponentUtil.addListener(list, MultiSelectionEvent.class, + (ComponentEventListener) (event -> listener + .selectionChange((SelectionEvent) event))); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Registration addMultiSelectionListener( + MultiSelectionListener, T> listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + return ComponentUtil.addListener(list, MultiSelectionEvent.class, + (ComponentEventListener) (event -> listener + .selectionChange((MultiSelectionEvent) event))); + } + + private Object getItemId(T item) { + return list.getDataCommunicator().getDataProvider().getId(item); + } +} diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListNoneSelectionModel.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListNoneSelectionModel.java new file mode 100644 index 00000000000..ee66ce63cc4 --- /dev/null +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListNoneSelectionModel.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2024 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.virtuallist; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import com.vaadin.flow.data.selection.SelectionListener; +import com.vaadin.flow.data.selection.SelectionModel; +import com.vaadin.flow.shared.Registration; + +/** + * Selection model implementation for disabling selection in VirtualList. + * + * @param + * the virtual list bean type + */ +public class VirtualListNoneSelectionModel + implements SelectionModel, T> { + + @Override + public Set getSelectedItems() { + return Collections.emptySet(); + } + + @Override + public Optional getFirstSelectedItem() { + return Optional.empty(); + } + + @Override + public void select(T item) { + // NO-OP + } + + @Override + public void deselect(T item) { + // NO-OP + } + + @Override + public void deselectAll() { + // NO-OP + } + + @Override + public Registration addSelectionListener( + SelectionListener, T> listener) { + throw new UnsupportedOperationException( + "This selection model doesn't allow selection, cannot add selection listeners to it. " + + "Please set suitable selection mode with virtualList.setSelectionMode"); + } + +} diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java new file mode 100644 index 00000000000..29c586b7a73 --- /dev/null +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java @@ -0,0 +1,160 @@ +/* + * Copyright 2000-2024 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.virtuallist; + +import java.util.Objects; +import java.util.Optional; + +import com.vaadin.flow.component.AbstractField.ComponentValueChangeEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.data.selection.SelectionEvent; +import com.vaadin.flow.data.selection.SelectionListener; +import com.vaadin.flow.data.selection.SelectionModel; +import com.vaadin.flow.data.selection.SingleSelect; +import com.vaadin.flow.data.selection.SingleSelectionEvent; +import com.vaadin.flow.data.selection.SingleSelectionListener; +import com.vaadin.flow.dom.Element; +import com.vaadin.flow.shared.Registration; + +/** + * Implementation of a SelectionModel.Single. + * + * @param + * the virtual list bean type + * @author Vaadin Ltd. + */ +public class VirtualListSingleSelectionModel + implements SelectionModel.Single, T> { + + private T selectedItem; + private boolean deselectAllowed = true; + private VirtualList list; + + /** + * Constructor for passing a reference of the virtual list to this + * implementation. + * + * @param list + * reference to the virtual list for which this selection model + * is created + */ + public VirtualListSingleSelectionModel(VirtualList list) { + this.list = list; + } + + @Override + public void select(T item) { + if (isSelected(item)) { + return; + } + doSelect(item, false); + } + + @Override + public void deselect(T item) { + if (isSelected(item)) { + select(null); + } + } + + @Override + public boolean isSelected(T item) { + return Objects.equals(getItemId(item), getItemId(selectedItem)); + } + + @Override + public Optional getSelectedItem() { + return Optional.ofNullable(selectedItem); + } + + @Override + public void setDeselectAllowed(boolean deselectAllowed) { + this.deselectAllowed = deselectAllowed; + } + + @Override + public boolean isDeselectAllowed() { + return deselectAllowed; + } + + public SingleSelect, T> asSingleSelect() { + return new SingleSelect, T>() { + + @Override + public void setValue(T value) { + setSelectedItem(value); + } + + @Override + public T getValue() { + return getSelectedItem().orElse(getEmptyValue()); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Registration addValueChangeListener( + ValueChangeListener, T>> listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + ComponentEventListener componentEventListener = event -> listener + .valueChanged( + (ComponentValueChangeEvent, T>) event); + + return ComponentUtil.addListener(list, + SingleSelectionEvent.class, componentEventListener); + } + + @Override + public Element getElement() { + return list.getElement(); + } + }; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Registration addSelectionListener( + SelectionListener, T> listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + + return ComponentUtil.addListener(list, SingleSelectionEvent.class, + (ComponentEventListener) (event -> listener + .selectionChange((SelectionEvent) event))); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Registration addSingleSelectionListener( + SingleSelectionListener, T> listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + return ComponentUtil.addListener(list, SingleSelectionEvent.class, + (ComponentEventListener) (event -> listener + .selectionChange((SingleSelectionEvent) event))); + } + + private void doSelect(T item, boolean userOriginated) { + T oldValue = selectedItem; + selectedItem = item; + if (oldValue != selectedItem) { + ComponentUtil.fireEvent(list, new SingleSelectionEvent<>(list, + asSingleSelect(), oldValue, true)); + } + } + + private Object getItemId(T item) { + return item == null ? null + : list.getDataCommunicator().getDataProvider().getId(item); + } +} diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js index 9089923d397..98a99950962 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js @@ -8,6 +8,7 @@ window.Vaadin.Flow.virtualListConnector = { return; } + list.itemIdPath = 'key'; const extraItemsBuffer = 20; let lastRequestedRange = [0, 0]; @@ -105,7 +106,7 @@ window.Vaadin.Flow.virtualListConnector = { list.$connector.set = function (index, items) { list.items.splice(index, items.length, ...items); - list.items = [...list.items]; + list.$connector.updateItems([...list.items]); }; list.$connector.clear = function (index, length) { @@ -120,7 +121,7 @@ window.Vaadin.Flow.virtualListConnector = { return map; }, {}); - list.items = list.items.map((item) => { + const newItems = list.items.map((item) => { // Items can be undefined if they are outside the viewport if (!item) { return item; @@ -129,14 +130,15 @@ window.Vaadin.Flow.virtualListConnector = { // return existing item as fallback if it was not updated return updatedItemsMap[item.key] || item; }); + list.$connector.updateItems(newItems); }; list.$connector.updateSize = function (newSize) { const delta = newSize - list.items.length; if (delta > 0) { - list.items = [...list.items, ...Array(delta)]; + list.$connector.updateItems([...list.items, ...Array(delta)]); } else if (delta < 0) { - list.items = list.items.slice(0, newSize); + list.$connector.updateItems(list.items.slice(0, newSize)); } }; @@ -146,5 +148,36 @@ window.Vaadin.Flow.virtualListConnector = { const nodeId = Object.entries(placeholderItem).find(([key]) => key.endsWith('_nodeid')); list.$connector.placeholderElement = nodeId ? Vaadin.Flow.clients[appId].getByNodeId(nodeId[1]) : null; }; + + list.$connector.updateItems = function (items) { + // Update the virtual list's items + list.items = items; + + // Update the virtual list's selectedItems + list.$connector.__updatingSelectedItemsFromServer = true; + list.selectedItems = items.filter((item) => item && item.selected); + list.$connector.__updatingSelectedItemsFromServer = false; + }; + + let previousSelectedKeys = []; + + list.addEventListener('selected-items-changed', function (event) { + const selectedKeys = event.detail.value.map((item) => item.key); + const addedKeys = selectedKeys.filter((key) => !previousSelectedKeys.includes(key)); + const removedKeys = previousSelectedKeys.filter((key) => !selectedKeys.includes(key)); + previousSelectedKeys = selectedKeys; + + if (list.$connector.__updatingSelectedItemsFromServer) { + // Items are being updated from the server, don't send the selection changes back + return; + } + + // If server sends partial updates while still making selections, other items might get temporarily + // de-selected / selected if their state is now yet synced from the server. + // Workaround the issue by updating the item selection state immediately on the client. + list.items.filter((item) => item).forEach((item) => (item.selected = selectedKeys.includes(item.key))); + + list.$server.updateSelection(addedKeys, removedKeys); + }); } }; diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListMultiSelectionTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListMultiSelectionTest.java new file mode 100644 index 00000000000..ba5bf991178 --- /dev/null +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListMultiSelectionTest.java @@ -0,0 +1,190 @@ +/* + * Copyright 2000-2024 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.virtuallist.tests; + +import java.util.Collections; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.vaadin.flow.component.virtuallist.VirtualList; +import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; +import com.vaadin.flow.data.selection.MultiSelect; +import com.vaadin.flow.data.selection.MultiSelectionListener; + +/** + * Tests using selection via VirtualList's MultiSelect API. + */ +public class VirtualListMultiSelectionTest { + + private VirtualList list; + private MultiSelect, String> multiSelect; + private MultiSelectionListener, String> selectionListenerSpy; + + @SuppressWarnings("unchecked") + @Before + public void setUp() { + list = new VirtualList<>(); + list.setItems("1", "2", "3", "4", "5"); + list.setSelectionMode(SelectionMode.MULTI); + multiSelect = list.asMultiSelect(); + selectionListenerSpy = Mockito.mock(MultiSelectionListener.class); + multiSelect.addSelectionListener(selectionListenerSpy); + } + + @Test + public void isSelected() { + multiSelect.select("2", "3"); + + Assert.assertTrue(multiSelect.isSelected("2")); + Assert.assertTrue(multiSelect.isSelected("3")); + + Assert.assertFalse(multiSelect.isSelected("1")); + Assert.assertFalse(multiSelect.isSelected("4")); + Assert.assertFalse(multiSelect.isSelected("5")); + Assert.assertFalse(multiSelect.isSelected("99")); + } + + @Test + public void getSelectedItems() { + multiSelect.select("2", "3"); + + Assert.assertEquals(Set.of("2", "3"), multiSelect.getSelectedItems()); + } + + @Test + public void setValue_updatesSelectionAndTriggersSelectionListener() { + multiSelect.setValue(Set.of("2", "3")); + + Assert.assertEquals(Set.of("2", "3"), multiSelect.getValue()); + Assert.assertEquals(Set.of("2", "3"), multiSelect.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void setValue_setExistingValue_noChanges() { + multiSelect.setValue(Set.of("2", "3")); + Mockito.reset(selectionListenerSpy); + multiSelect.setValue(Set.of("2", "3")); + + Assert.assertEquals(Set.of("2", "3"), multiSelect.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .selectionChange(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void setValue_setDifferentValue_selectionChanged() { + multiSelect.setValue(Set.of("2", "3")); + Mockito.reset(selectionListenerSpy); + multiSelect.setValue(Set.of("1", "2")); + + Assert.assertEquals(Set.of("1", "2"), multiSelect.getValue()); + Assert.assertEquals(Set.of("1", "2"), multiSelect.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void changeSelection_updatesSelectionAndValueAndTriggersSelectionListener() { + multiSelect.select("1", "2", "3"); + Assert.assertEquals(Set.of("1", "2", "3"), + multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1", "2", "3"), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + Mockito.reset(selectionListenerSpy); + + multiSelect.deselect("2", "3"); + Assert.assertEquals(Set.of("1"), multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1"), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + Mockito.reset(selectionListenerSpy); + + multiSelect.deselectAll(); + Assert.assertEquals(Set.of(), multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of(), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + Mockito.reset(selectionListenerSpy); + } + + @SuppressWarnings("unchecked") + @Test + public void updateSelection_updatesSelectionAndValueAndTriggersSelectionListener() { + multiSelect.updateSelection(Set.of("1", "2", "3"), + Collections.emptySet()); + Assert.assertEquals(Set.of("1", "2", "3"), + multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1", "2", "3"), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + Mockito.reset(selectionListenerSpy); + + multiSelect.updateSelection(Collections.emptySet(), Set.of("2", "3")); + Assert.assertEquals(Set.of("1"), multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1"), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + Mockito.reset(selectionListenerSpy); + } + + @SuppressWarnings("unchecked") + @Test + public void selectExistingItems_noChanges() { + multiSelect.select("1", "2", "3"); + Mockito.reset(selectionListenerSpy); + + multiSelect.select(); + multiSelect.select("1"); + multiSelect.select("1", "2"); + multiSelect.select("1", "2", "3"); + Assert.assertEquals(Set.of("1", "2", "3"), + multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1", "2", "3"), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .selectionChange(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void deselectUnselectedItems_noChanges() { + multiSelect.select("1", "2", "3"); + Mockito.reset(selectionListenerSpy); + + multiSelect.deselect(); + multiSelect.deselect("4", "5"); + Assert.assertEquals(Set.of("1", "2", "3"), + multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1", "2", "3"), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .selectionChange(Mockito.any()); + } + + @Test + public void emptySelection_deselectAll_noChanges() { + multiSelect.deselectAll(); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .selectionChange(Mockito.any()); + } +} diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListNoneSelectionTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListNoneSelectionTest.java new file mode 100644 index 00000000000..929938ab1d6 --- /dev/null +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListNoneSelectionTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2000-2024 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.virtuallist.tests; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.flow.component.virtuallist.VirtualList; + +public class VirtualListNoneSelectionTest { + + private VirtualList list; + + @Before + public void setup() { + list = new VirtualList<>(); + list.setItems("foo", "bar", "baz"); + } + + @Test(expected = UnsupportedOperationException.class) + public void addSelectionListener_throws() { + list.addSelectionListener(e -> { + }); + } + + @Test(expected = IllegalStateException.class) + public void asSingleSelect_throws() { + list.asSingleSelect(); + } + + @Test(expected = IllegalStateException.class) + public void asMultiSelect_throws() { + list.asMultiSelect(); + } + + @Test + public void select_getSelectedItems_empty() { + list.select("foo"); + Assert.assertTrue(list.getSelectedItems().isEmpty()); + } + + @Test + public void clientSelectionMode() { + Assert.assertNull(list.getElement().getProperty("selectionMode")); + } +} diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionTest.java new file mode 100644 index 00000000000..f93a37c6324 --- /dev/null +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2024 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.virtuallist.tests; + +import java.util.Set; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.vaadin.flow.component.virtuallist.VirtualList; +import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; +import com.vaadin.flow.data.selection.SelectionListener; + +/** + * Tests using selection via VirtualList's API. + */ +public class VirtualListSelectionTest { + + private VirtualList list; + private SelectionListener, String> selectionListenerSpy; + + @SuppressWarnings("unchecked") + @Before + public void setUp() { + list = new VirtualList<>(); + list.setItems("1", "2", "3", "4", "5"); + list.setSelectionMode(SelectionMode.MULTI); + selectionListenerSpy = Mockito.mock(SelectionListener.class); + list.addSelectionListener(selectionListenerSpy); + } + + @Test + public void select_updatesSelectionAndTriggersSelectionListener() { + list.select("2"); + + Assert.assertEquals(Set.of("2"), list.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + } + +} diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java new file mode 100644 index 00000000000..26f63472e82 --- /dev/null +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2000-2024 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.virtuallist.tests; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.vaadin.flow.component.HasValue.ValueChangeEvent; +import com.vaadin.flow.component.HasValue.ValueChangeListener; +import com.vaadin.flow.component.virtuallist.VirtualList; +import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; +import com.vaadin.flow.data.selection.SingleSelect; + +/** + * Tests using selection via VirtualList's SingleSelect API. + */ +public class VirtualListSingleSelectionTest { + + private VirtualList list; + private SingleSelect, String> singleSelect; + private ValueChangeListener> selectionListenerSpy; + + @SuppressWarnings("unchecked") + @Before + public void setUp() { + list = new VirtualList<>(); + list.setItems("1", "2", "3", "4", "5"); + list.setSelectionMode(SelectionMode.SINGLE); + singleSelect = list.asSingleSelect(); + selectionListenerSpy = Mockito.mock(ValueChangeListener.class); + singleSelect.addValueChangeListener(selectionListenerSpy); + } + + @Test + public void getValue() { + singleSelect.setValue("2"); + + Assert.assertEquals("2", singleSelect.getValue()); + } + + @Test + public void setValue_triggersSelectionListener() { + singleSelect.setValue("2"); + + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .valueChanged(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void setValue_setExistingValue_noChanges() { + singleSelect.setValue("2"); + Mockito.reset(selectionListenerSpy); + singleSelect.setValue("2"); + + Assert.assertEquals("2", singleSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .valueChanged(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void setValue_setDifferentValue_selectionChanged() { + singleSelect.setValue("2"); + Mockito.reset(selectionListenerSpy); + singleSelect.setValue("3"); + + Assert.assertEquals("3", singleSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .valueChanged(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void clear_updatesSelectionAndValueAndTriggersSelectionListener() { + singleSelect.setValue("2"); + Mockito.reset(selectionListenerSpy); + + singleSelect.clear(); + Assert.assertEquals(null, singleSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .valueChanged(Mockito.any()); + } + + @Test + public void emptySelection_clear_noChanges() { + singleSelect.clear(); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .valueChanged(Mockito.any()); + } +} From 3a72694c64c29dfbadf9fe75023f4f5e781b1107 Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Thu, 28 Nov 2024 12:11:15 +0200 Subject: [PATCH 02/10] tests --- .../component/virtuallist/VirtualList.java | 2 +- .../tests/VirtualListAsMultiSelectTest.java | 189 ++++++++++++++++++ .../tests/VirtualListAsSingleSelectTest.java | 105 ++++++++++ .../tests/VirtualListMultiSelectionTest.java | 185 +++++++++-------- .../tests/VirtualListSingleSelectionTest.java | 159 ++++++++++++--- .../tests/VirtualListTestHelpers.java | 71 +++++++ 6 files changed, 590 insertions(+), 121 deletions(-) create mode 100644 vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsMultiSelectTest.java create mode 100644 vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsSingleSelectTest.java create mode 100644 vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListTestHelpers.java diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java index 6f66c36ed93..9a15c68131c 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java @@ -429,7 +429,7 @@ private Set getItemsFromKeys(JsonArray keys) { @SuppressWarnings("unchecked") @ClientCallable - void updateSelection(JsonArray addedKeys, JsonArray removedKeys) { + private void updateSelection(JsonArray addedKeys, JsonArray removedKeys) { var addedItems = getItemsFromKeys(addedKeys); var removedItems = getItemsFromKeys(removedKeys); diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsMultiSelectTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsMultiSelectTest.java new file mode 100644 index 00000000000..dd3119615dc --- /dev/null +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsMultiSelectTest.java @@ -0,0 +1,189 @@ +/* + * Copyright 2000-2024 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.virtuallist.tests; + +import java.util.Collections; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.vaadin.flow.component.virtuallist.VirtualList; +import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; +import com.vaadin.flow.data.selection.MultiSelect; +import com.vaadin.flow.data.selection.MultiSelectionListener; + +/** + * Tests using selection via VirtualList's MultiSelect API. + */ +public class VirtualListAsMultiSelectTest { + + private MultiSelect, String> multiSelect; + private MultiSelectionListener, String> selectionListenerSpy; + + @SuppressWarnings("unchecked") + @Before + public void setUp() { + var list = new VirtualList(); + list.setItems("1", "2", "3", "4", "5"); + list.setSelectionMode(SelectionMode.MULTI); + multiSelect = list.asMultiSelect(); + selectionListenerSpy = Mockito.mock(MultiSelectionListener.class); + multiSelect.addSelectionListener(selectionListenerSpy); + } + + @Test + public void isSelected() { + multiSelect.select("2", "3"); + + Assert.assertTrue(multiSelect.isSelected("2")); + Assert.assertTrue(multiSelect.isSelected("3")); + + Assert.assertFalse(multiSelect.isSelected("1")); + Assert.assertFalse(multiSelect.isSelected("4")); + Assert.assertFalse(multiSelect.isSelected("5")); + Assert.assertFalse(multiSelect.isSelected("99")); + } + + @Test + public void getSelectedItems() { + multiSelect.select("2", "3"); + + Assert.assertEquals(Set.of("2", "3"), multiSelect.getSelectedItems()); + } + + @Test + public void setValue_updatesSelectionAndTriggersSelectionListener() { + multiSelect.setValue(Set.of("2", "3")); + + Assert.assertEquals(Set.of("2", "3"), multiSelect.getValue()); + Assert.assertEquals(Set.of("2", "3"), multiSelect.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void setValue_setExistingValue_noChanges() { + multiSelect.setValue(Set.of("2", "3")); + Mockito.reset(selectionListenerSpy); + multiSelect.setValue(Set.of("2", "3")); + + Assert.assertEquals(Set.of("2", "3"), multiSelect.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .selectionChange(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void setValue_setDifferentValue_selectionChanged() { + multiSelect.setValue(Set.of("2", "3")); + Mockito.reset(selectionListenerSpy); + multiSelect.setValue(Set.of("1", "2")); + + Assert.assertEquals(Set.of("1", "2"), multiSelect.getValue()); + Assert.assertEquals(Set.of("1", "2"), multiSelect.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void changeSelection_updatesSelectionAndValueAndTriggersSelectionListener() { + multiSelect.select("1", "2", "3"); + Assert.assertEquals(Set.of("1", "2", "3"), + multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1", "2", "3"), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + Mockito.reset(selectionListenerSpy); + + multiSelect.deselect("2", "3"); + Assert.assertEquals(Set.of("1"), multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1"), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + Mockito.reset(selectionListenerSpy); + + multiSelect.deselectAll(); + Assert.assertEquals(Set.of(), multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of(), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + Mockito.reset(selectionListenerSpy); + } + + @SuppressWarnings("unchecked") + @Test + public void updateSelection_updatesSelectionAndValueAndTriggersSelectionListener() { + multiSelect.updateSelection(Set.of("1", "2", "3"), + Collections.emptySet()); + Assert.assertEquals(Set.of("1", "2", "3"), + multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1", "2", "3"), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + Mockito.reset(selectionListenerSpy); + + multiSelect.updateSelection(Collections.emptySet(), Set.of("2", "3")); + Assert.assertEquals(Set.of("1"), multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1"), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + Mockito.reset(selectionListenerSpy); + } + + @SuppressWarnings("unchecked") + @Test + public void selectExistingItems_noChanges() { + multiSelect.select("1", "2", "3"); + Mockito.reset(selectionListenerSpy); + + multiSelect.select(); + multiSelect.select("1"); + multiSelect.select("1", "2"); + multiSelect.select("1", "2", "3"); + Assert.assertEquals(Set.of("1", "2", "3"), + multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1", "2", "3"), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .selectionChange(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void deselectUnselectedItems_noChanges() { + multiSelect.select("1", "2", "3"); + Mockito.reset(selectionListenerSpy); + + multiSelect.deselect(); + multiSelect.deselect("4", "5"); + Assert.assertEquals(Set.of("1", "2", "3"), + multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1", "2", "3"), multiSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .selectionChange(Mockito.any()); + } + + @Test + public void emptySelection_deselectAll_noChanges() { + multiSelect.deselectAll(); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .selectionChange(Mockito.any()); + } +} diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsSingleSelectTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsSingleSelectTest.java new file mode 100644 index 00000000000..5f8b36bbf86 --- /dev/null +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsSingleSelectTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2000-2024 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.virtuallist.tests; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.vaadin.flow.component.HasValue.ValueChangeEvent; +import com.vaadin.flow.component.HasValue.ValueChangeListener; +import com.vaadin.flow.component.virtuallist.VirtualList; +import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; +import com.vaadin.flow.data.selection.SingleSelect; + +/** + * Tests using selection via VirtualList's SingleSelect API. + */ +public class VirtualListAsSingleSelectTest { + + private SingleSelect, String> singleSelect; + private ValueChangeListener> selectionListenerSpy; + + @SuppressWarnings("unchecked") + @Before + public void setUp() { + var list = new VirtualList(); + list.setItems("1", "2", "3", "4", "5"); + list.setSelectionMode(SelectionMode.SINGLE); + singleSelect = list.asSingleSelect(); + selectionListenerSpy = Mockito.mock(ValueChangeListener.class); + singleSelect.addValueChangeListener(selectionListenerSpy); + } + + @Test + public void getValue() { + singleSelect.setValue("2"); + + Assert.assertEquals("2", singleSelect.getValue()); + } + + @Test + public void setValue_triggersSelectionListener() { + singleSelect.setValue("2"); + + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .valueChanged(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void setValue_setExistingValue_noChanges() { + singleSelect.setValue("2"); + Mockito.reset(selectionListenerSpy); + singleSelect.setValue("2"); + + Assert.assertEquals("2", singleSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .valueChanged(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void setValue_setDifferentValue_selectionChanged() { + singleSelect.setValue("2"); + Mockito.reset(selectionListenerSpy); + singleSelect.setValue("3"); + + Assert.assertEquals("3", singleSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .valueChanged(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void clear_updatesSelectionAndValueAndTriggersSelectionListener() { + singleSelect.setValue("2"); + Mockito.reset(selectionListenerSpy); + + singleSelect.clear(); + Assert.assertEquals(null, singleSelect.getValue()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .valueChanged(Mockito.any()); + } + + @Test + public void emptySelection_clear_noChanges() { + singleSelect.clear(); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .valueChanged(Mockito.any()); + } +} diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListMultiSelectionTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListMultiSelectionTest.java index ba5bf991178..3968b1b282f 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListMultiSelectionTest.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListMultiSelectionTest.java @@ -15,7 +15,8 @@ */ package com.vaadin.flow.component.virtuallist.tests; -import java.util.Collections; +import static com.vaadin.flow.component.virtuallist.tests.VirtualListTestHelpers.*; + import java.util.Set; import org.junit.Assert; @@ -25,17 +26,19 @@ import com.vaadin.flow.component.virtuallist.VirtualList; import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; -import com.vaadin.flow.data.selection.MultiSelect; -import com.vaadin.flow.data.selection.MultiSelectionListener; +import com.vaadin.flow.data.provider.CompositeDataGenerator; +import com.vaadin.flow.data.provider.DataGenerator; +import com.vaadin.flow.data.selection.SelectionListener; /** - * Tests using selection via VirtualList's MultiSelect API. + * Tests multi-selectable VirtualList */ public class VirtualListMultiSelectionTest { private VirtualList list; - private MultiSelect, String> multiSelect; - private MultiSelectionListener, String> selectionListenerSpy; + private SelectionListener, String> selectionListenerSpy; + private CompositeDataGenerator dataGenerator; + private DataGenerator dataGeneratorSpy; @SuppressWarnings("unchecked") @Before @@ -43,148 +46,156 @@ public void setUp() { list = new VirtualList<>(); list.setItems("1", "2", "3", "4", "5"); list.setSelectionMode(SelectionMode.MULTI); - multiSelect = list.asMultiSelect(); - selectionListenerSpy = Mockito.mock(MultiSelectionListener.class); - multiSelect.addSelectionListener(selectionListenerSpy); + + selectionListenerSpy = Mockito.mock(SelectionListener.class); + list.addSelectionListener(selectionListenerSpy); + + dataGenerator = getDataGenerator(list); + dataGeneratorSpy = Mockito.mock(DataGenerator.class); + dataGenerator.addDataGenerator(dataGeneratorSpy); } @Test - public void isSelected() { - multiSelect.select("2", "3"); + public void setsWebComponentSelectionMode() { + Assert.assertEquals("multi", + list.getElement().getProperty("selectionMode")); + } - Assert.assertTrue(multiSelect.isSelected("2")); - Assert.assertTrue(multiSelect.isSelected("3")); + @Test + public void getSelectionMode_returnsMode() { + Assert.assertEquals(SelectionMode.MULTI, list.getSelectionMode()); + } - Assert.assertFalse(multiSelect.isSelected("1")); - Assert.assertFalse(multiSelect.isSelected("4")); - Assert.assertFalse(multiSelect.isSelected("5")); - Assert.assertFalse(multiSelect.isSelected("99")); + @Test + public void setSelectionMode_returnsModel() { + var model = list.setSelectionMode(SelectionMode.MULTI); + Assert.assertEquals(list.getSelectionModel(), model); + } + + @Test(expected = IllegalStateException.class) + public void asSingleSelect_throwsIfSelectionModeIsMulti() { + list.asSingleSelect(); } @Test public void getSelectedItems() { - multiSelect.select("2", "3"); + list.select("2"); + list.select("3"); - Assert.assertEquals(Set.of("2", "3"), multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("2", "3"), list.getSelectedItems()); } @Test - public void setValue_updatesSelectionAndTriggersSelectionListener() { - multiSelect.setValue(Set.of("2", "3")); + public void select_triggersSelectionListener() { + list.select("1"); - Assert.assertEquals(Set.of("2", "3"), multiSelect.getValue()); - Assert.assertEquals(Set.of("2", "3"), multiSelect.getSelectedItems()); Mockito.verify(selectionListenerSpy, Mockito.times(1)) .selectionChange(Mockito.any()); } @SuppressWarnings("unchecked") @Test - public void setValue_setExistingValue_noChanges() { - multiSelect.setValue(Set.of("2", "3")); + public void select_selectExistingValue_noChanges() { + list.select("1"); Mockito.reset(selectionListenerSpy); - multiSelect.setValue(Set.of("2", "3")); + list.select("1"); - Assert.assertEquals(Set.of("2", "3"), multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1"), list.getSelectedItems()); Mockito.verify(selectionListenerSpy, Mockito.times(0)) .selectionChange(Mockito.any()); } @SuppressWarnings("unchecked") @Test - public void setValue_setDifferentValue_selectionChanged() { - multiSelect.setValue(Set.of("2", "3")); + public void select_selectDifferentValue_selectionChanged() { + list.select("1"); Mockito.reset(selectionListenerSpy); - multiSelect.setValue(Set.of("1", "2")); + list.select("2"); - Assert.assertEquals(Set.of("1", "2"), multiSelect.getValue()); - Assert.assertEquals(Set.of("1", "2"), multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1", "2"), list.getSelectedItems()); Mockito.verify(selectionListenerSpy, Mockito.times(1)) .selectionChange(Mockito.any()); } @SuppressWarnings("unchecked") @Test - public void changeSelection_updatesSelectionAndValueAndTriggersSelectionListener() { - multiSelect.select("1", "2", "3"); - Assert.assertEquals(Set.of("1", "2", "3"), - multiSelect.getSelectedItems()); - Assert.assertEquals(Set.of("1", "2", "3"), multiSelect.getValue()); - Mockito.verify(selectionListenerSpy, Mockito.times(1)) - .selectionChange(Mockito.any()); + public void select_deselect_selectionChanged() { + list.select("1"); Mockito.reset(selectionListenerSpy); + list.deselect("1"); - multiSelect.deselect("2", "3"); - Assert.assertEquals(Set.of("1"), multiSelect.getSelectedItems()); - Assert.assertEquals(Set.of("1"), multiSelect.getValue()); + Assert.assertEquals(Set.of(), list.getSelectedItems()); Mockito.verify(selectionListenerSpy, Mockito.times(1)) .selectionChange(Mockito.any()); - Mockito.reset(selectionListenerSpy); - - multiSelect.deselectAll(); - Assert.assertEquals(Set.of(), multiSelect.getSelectedItems()); - Assert.assertEquals(Set.of(), multiSelect.getValue()); - Mockito.verify(selectionListenerSpy, Mockito.times(1)) - .selectionChange(Mockito.any()); - Mockito.reset(selectionListenerSpy); } @SuppressWarnings("unchecked") @Test - public void updateSelection_updatesSelectionAndValueAndTriggersSelectionListener() { - multiSelect.updateSelection(Set.of("1", "2", "3"), - Collections.emptySet()); - Assert.assertEquals(Set.of("1", "2", "3"), - multiSelect.getSelectedItems()); - Assert.assertEquals(Set.of("1", "2", "3"), multiSelect.getValue()); - Mockito.verify(selectionListenerSpy, Mockito.times(1)) - .selectionChange(Mockito.any()); + public void selecti_deselectAll_selectionChanged() { + list.select("1"); Mockito.reset(selectionListenerSpy); - - multiSelect.updateSelection(Collections.emptySet(), Set.of("2", "3")); - Assert.assertEquals(Set.of("1"), multiSelect.getSelectedItems()); - Assert.assertEquals(Set.of("1"), multiSelect.getValue()); + list.deselectAll(); + Assert.assertEquals(Set.of(), list.getSelectedItems()); Mockito.verify(selectionListenerSpy, Mockito.times(1)) .selectionChange(Mockito.any()); - Mockito.reset(selectionListenerSpy); } - @SuppressWarnings("unchecked") @Test - public void selectExistingItems_noChanges() { - multiSelect.select("1", "2", "3"); - Mockito.reset(selectionListenerSpy); - - multiSelect.select(); - multiSelect.select("1"); - multiSelect.select("1", "2"); - multiSelect.select("1", "2", "3"); - Assert.assertEquals(Set.of("1", "2", "3"), - multiSelect.getSelectedItems()); - Assert.assertEquals(Set.of("1", "2", "3"), multiSelect.getValue()); + public void emptySelection_deselectAll_noChanges() { + list.deselectAll(); Mockito.verify(selectionListenerSpy, Mockito.times(0)) .selectionChange(Mockito.any()); } + @Test + public void select_generateItemSelected() { + list.select("1"); + Assert.assertTrue(generatesSelected(dataGenerator, "1")); + Assert.assertFalse(generatesSelected(dataGenerator, "2")); + } + + @Test + public void deselect_generateItemSelected() { + list.select("1"); + list.deselect("1"); + Assert.assertFalse(generatesSelected(dataGenerator, "1")); + } + + @Test + public void select_generateItemData() { + list.select("1"); + Mockito.verify(dataGeneratorSpy, Mockito.times(1)) + .refreshData(Mockito.eq("1")); + } + @SuppressWarnings("unchecked") @Test - public void deselectUnselectedItems_noChanges() { - multiSelect.select("1", "2", "3"); - Mockito.reset(selectionListenerSpy); + public void deselect_generateItemData() { + list.select("1"); + Mockito.reset(dataGeneratorSpy); + list.deselect("1"); + Mockito.verify(dataGeneratorSpy, Mockito.times(1)) + .refreshData(Mockito.eq("1")); + } - multiSelect.deselect(); - multiSelect.deselect("4", "5"); - Assert.assertEquals(Set.of("1", "2", "3"), - multiSelect.getSelectedItems()); - Assert.assertEquals(Set.of("1", "2", "3"), multiSelect.getValue()); - Mockito.verify(selectionListenerSpy, Mockito.times(0)) + @Test + public void updateSelectionFromClient_itemsSelected() { + updateSelectionFromClient(list, Set.of("1", "2"), Set.of()); + Assert.assertEquals(Set.of("1", "2"), list.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) .selectionChange(Mockito.any()); } + @SuppressWarnings("unchecked") @Test - public void emptySelection_deselectAll_noChanges() { - multiSelect.deselectAll(); - Mockito.verify(selectionListenerSpy, Mockito.times(0)) + public void updateSelectionFromClient_itemsChanged() { + list.select("1"); + list.select("2"); + Mockito.reset(selectionListenerSpy); + updateSelectionFromClient(list, Set.of("3"), Set.of("1")); + Assert.assertEquals(Set.of("2", "3"), list.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) .selectionChange(Mockito.any()); } + } diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java index 26f63472e82..68c8c760603 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java @@ -15,25 +15,30 @@ */ package com.vaadin.flow.component.virtuallist.tests; +import static com.vaadin.flow.component.virtuallist.tests.VirtualListTestHelpers.*; + +import java.util.Set; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; -import com.vaadin.flow.component.HasValue.ValueChangeEvent; -import com.vaadin.flow.component.HasValue.ValueChangeListener; import com.vaadin.flow.component.virtuallist.VirtualList; import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; -import com.vaadin.flow.data.selection.SingleSelect; +import com.vaadin.flow.data.provider.CompositeDataGenerator; +import com.vaadin.flow.data.provider.DataGenerator; +import com.vaadin.flow.data.selection.SelectionListener; /** - * Tests using selection via VirtualList's SingleSelect API. + * Tests single-selectable VirtualList */ public class VirtualListSingleSelectionTest { private VirtualList list; - private SingleSelect, String> singleSelect; - private ValueChangeListener> selectionListenerSpy; + private SelectionListener, String> selectionListenerSpy; + private CompositeDataGenerator dataGenerator; + private DataGenerator dataGeneratorSpy; @SuppressWarnings("unchecked") @Before @@ -41,66 +46,154 @@ public void setUp() { list = new VirtualList<>(); list.setItems("1", "2", "3", "4", "5"); list.setSelectionMode(SelectionMode.SINGLE); - singleSelect = list.asSingleSelect(); - selectionListenerSpy = Mockito.mock(ValueChangeListener.class); - singleSelect.addValueChangeListener(selectionListenerSpy); + + selectionListenerSpy = Mockito.mock(SelectionListener.class); + list.addSelectionListener(selectionListenerSpy); + + dataGenerator = getDataGenerator(list); + dataGeneratorSpy = Mockito.mock(DataGenerator.class); + dataGenerator.addDataGenerator(dataGeneratorSpy); + } + + @Test + public void setsWebComponentSelectionMode() { + Assert.assertEquals("single", + list.getElement().getProperty("selectionMode")); + } + + @Test + public void getSelectionMode_returnsMode() { + Assert.assertEquals(SelectionMode.SINGLE, list.getSelectionMode()); + } + + @Test + public void setSelectionMode_returnsModel() { + var model = list.setSelectionMode(SelectionMode.SINGLE); + Assert.assertEquals(list.getSelectionModel(), model); + } + + @Test(expected = IllegalStateException.class) + public void asMultiSelect_throwsIfSelectionModeIsSingle() { + list.asMultiSelect(); } @Test - public void getValue() { - singleSelect.setValue("2"); + public void getSelectedItems() { + list.select("2"); + list.select("3"); - Assert.assertEquals("2", singleSelect.getValue()); + Assert.assertEquals(Set.of("3"), list.getSelectedItems()); } @Test - public void setValue_triggersSelectionListener() { - singleSelect.setValue("2"); + public void select_triggersSelectionListener() { + list.select("1"); Mockito.verify(selectionListenerSpy, Mockito.times(1)) - .valueChanged(Mockito.any()); + .selectionChange(Mockito.any()); } @SuppressWarnings("unchecked") @Test - public void setValue_setExistingValue_noChanges() { - singleSelect.setValue("2"); + public void select_selectExistingValue_noChanges() { + list.select("1"); Mockito.reset(selectionListenerSpy); - singleSelect.setValue("2"); + list.select("1"); - Assert.assertEquals("2", singleSelect.getValue()); + Assert.assertEquals(Set.of("1"), list.getSelectedItems()); Mockito.verify(selectionListenerSpy, Mockito.times(0)) - .valueChanged(Mockito.any()); + .selectionChange(Mockito.any()); } @SuppressWarnings("unchecked") @Test - public void setValue_setDifferentValue_selectionChanged() { - singleSelect.setValue("2"); + public void select_selectDifferentValue_selectionChanged() { + list.select("1"); Mockito.reset(selectionListenerSpy); - singleSelect.setValue("3"); + list.select("2"); - Assert.assertEquals("3", singleSelect.getValue()); + Assert.assertEquals(Set.of("2"), list.getSelectedItems()); Mockito.verify(selectionListenerSpy, Mockito.times(1)) - .valueChanged(Mockito.any()); + .selectionChange(Mockito.any()); } @SuppressWarnings("unchecked") @Test - public void clear_updatesSelectionAndValueAndTriggersSelectionListener() { - singleSelect.setValue("2"); + public void select_deselect_selectionChanged() { + list.select("1"); Mockito.reset(selectionListenerSpy); + list.deselect("1"); - singleSelect.clear(); - Assert.assertEquals(null, singleSelect.getValue()); + Assert.assertEquals(Set.of(), list.getSelectedItems()); Mockito.verify(selectionListenerSpy, Mockito.times(1)) - .valueChanged(Mockito.any()); + .selectionChange(Mockito.any()); } + @SuppressWarnings("unchecked") @Test - public void emptySelection_clear_noChanges() { - singleSelect.clear(); + public void selecti_deselectAll_selectionChanged() { + list.select("1"); + Mockito.reset(selectionListenerSpy); + list.deselectAll(); + Assert.assertEquals(Set.of(), list.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + } + + @Test + public void emptySelection_deselectAll_noChanges() { + list.deselectAll(); Mockito.verify(selectionListenerSpy, Mockito.times(0)) - .valueChanged(Mockito.any()); + .selectionChange(Mockito.any()); + } + + @Test + public void select_generateItemSelected() { + list.select("1"); + Assert.assertTrue(generatesSelected(dataGenerator, "1")); + Assert.assertFalse(generatesSelected(dataGenerator, "2")); + } + + @Test + public void deselect_generateItemSelected() { + list.select("1"); + list.deselect("1"); + Assert.assertFalse(generatesSelected(dataGenerator, "1")); + } + + @Test + public void select_generateItemData() { + list.select("1"); + Mockito.verify(dataGeneratorSpy, Mockito.times(1)) + .refreshData(Mockito.eq("1")); + } + + @SuppressWarnings("unchecked") + @Test + public void deselect_generateItemData() { + list.select("1"); + Mockito.reset(dataGeneratorSpy); + list.deselect("1"); + Mockito.verify(dataGeneratorSpy, Mockito.times(1)) + .refreshData(Mockito.eq("1")); + } + + @Test + public void updateSelectionFromClient_itemsSelected() { + updateSelectionFromClient(list, Set.of("1"), Set.of()); + Assert.assertEquals(Set.of("1"), list.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void updateSelectionFromClient_itemsChanged() { + list.select("1"); + Mockito.reset(selectionListenerSpy); + updateSelectionFromClient(list, Set.of("3"), Set.of("1")); + Assert.assertEquals(Set.of("3"), list.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); } } diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListTestHelpers.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListTestHelpers.java new file mode 100644 index 00000000000..0b3f288ee3e --- /dev/null +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListTestHelpers.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2024 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.virtuallist.tests; + +import java.util.Set; + +import com.vaadin.flow.component.virtuallist.VirtualList; +import com.vaadin.flow.data.provider.CompositeDataGenerator; +import com.vaadin.flow.data.provider.DataGenerator; +import com.vaadin.flow.internal.JsonUtils; + +import elemental.json.Json; +import elemental.json.JsonArray; + +public class VirtualListTestHelpers { + + public static boolean generatesSelected(DataGenerator dataGenerator, + T item) { + var jsonObject = Json.createObject(); + dataGenerator.generateData(item, jsonObject); + return jsonObject.hasKey("selected"); + } + + @SuppressWarnings("unchecked") + public static CompositeDataGenerator getDataGenerator( + VirtualList list) { + try { + var dataGenerator = VirtualList.class + .getDeclaredField("dataGenerator"); + dataGenerator.setAccessible(true); + return (CompositeDataGenerator) dataGenerator.get(list); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private static JsonArray getKeysFromItems(VirtualList list, + Set items) { + return JsonUtils.listToJson(items.stream().map( + item -> list.getDataCommunicator().getKeyMapper().key(item)) + .toList()); + } + + public static void updateSelectionFromClient(VirtualList list, + Set addedItems, Set removedItems) { + var addedKeys = getKeysFromItems(list, addedItems); + var removedKeys = getKeysFromItems(list, removedItems); + + try { + var updateSelection = VirtualList.class.getDeclaredMethod( + "updateSelection", JsonArray.class, JsonArray.class); + updateSelection.setAccessible(true); + updateSelection.invoke(list, addedKeys, removedKeys); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} From f2be1b9c9c16709a8ae79e534b7650e9f49c3740 Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Thu, 28 Nov 2024 16:13:26 +0200 Subject: [PATCH 03/10] more tests and cleanup --- .../component/virtuallist/VirtualList.java | 4 +- .../VirtualListMultiSelectionModel.java | 17 ------- .../VirtualListSingleSelectionModel.java | 19 ++----- .../tests/VirtualListAsMultiSelectTest.java | 36 +++++++++++++ .../tests/VirtualListAsSingleSelectTest.java | 38 ++++++++++++++ .../tests/VirtualListMultiSelectionTest.java | 50 ++++++++++++++++++- 6 files changed, 128 insertions(+), 36 deletions(-) diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java index 9a15c68131c..4d8bed3c59c 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java @@ -475,9 +475,9 @@ private void setSelectionModel(SelectionModel, T> model, * {@code virtualList.getSelectionModel().addSelectionListener()}. To get * more detailed selection events, use {@link #getSelectionModel()} and * either - * {@link VirtualListSingleSelectionModel#addSingleSelectionListener(SingleSelectionListener)} + * {@link VirtualListSingleSelectionModel#addSelectionListener(SelectionListener) * or - * {@link VirtualListMultiSelectionModel#addMultiSelectionListener(MultiSelectionListener)} + * {@link VirtualListMultiSelectionModel#addSelectionListener(SelectionListener) * depending on the used selection mode. * * @param listener diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListMultiSelectionModel.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListMultiSelectionModel.java index 066dd3511a7..3da51799f63 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListMultiSelectionModel.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListMultiSelectionModel.java @@ -98,9 +98,6 @@ public Optional getFirstSelectedItem() { @Override public void select(T item) { - if (isSelected(item)) { - return; - } Set selected = new HashSet<>(); if (item != null) { selected.add(item); @@ -111,9 +108,6 @@ public void select(T item) { @Override public void deselect(T item) { - if (!isSelected(item)) { - return; - } Set deselected = new HashSet<>(); if (item != null) { deselected.add(item); @@ -181,17 +175,6 @@ public boolean isSelected(T item) { return selected.containsKey(getItemId(item)); } - public void setSelectedItems(Set items) { - var oldValue = getSelectedItems(); - selected.clear(); - items.forEach(item -> selected.put(getItemId(item), item)); - - // TODO: This should not be here - ComponentUtil.fireEvent(list, new MultiSelectionEvent<>(list, - asMultiSelect(), oldValue, true)); - - } - public MultiSelect, T> asMultiSelect() { return new MultiSelect, T>() { diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java index 29c586b7a73..4667fbbaa53 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java @@ -66,9 +66,7 @@ public void select(T item) { @Override public void deselect(T item) { - if (isSelected(item)) { - select(null); - } + select(null); } @Override @@ -135,22 +133,11 @@ public Registration addSelectionListener( .selectionChange((SelectionEvent) event))); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - public Registration addSingleSelectionListener( - SingleSelectionListener, T> listener) { - Objects.requireNonNull(listener, "listener cannot be null"); - return ComponentUtil.addListener(list, SingleSelectionEvent.class, - (ComponentEventListener) (event -> listener - .selectionChange((SingleSelectionEvent) event))); - } - private void doSelect(T item, boolean userOriginated) { T oldValue = selectedItem; selectedItem = item; - if (oldValue != selectedItem) { - ComponentUtil.fireEvent(list, new SingleSelectionEvent<>(list, - asSingleSelect(), oldValue, true)); - } + ComponentUtil.fireEvent(list, new SingleSelectionEvent<>(list, + asSingleSelect(), oldValue, true)); } private Object getItemId(T item) { diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsMultiSelectTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsMultiSelectTest.java index dd3119615dc..a8d3a944458 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsMultiSelectTest.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsMultiSelectTest.java @@ -25,6 +25,7 @@ import com.vaadin.flow.component.virtuallist.VirtualList; import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; +import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.data.selection.MultiSelect; import com.vaadin.flow.data.selection.MultiSelectionListener; @@ -67,6 +68,11 @@ public void getSelectedItems() { Assert.assertEquals(Set.of("2", "3"), multiSelect.getSelectedItems()); } + @Test + public void getElement() { + Assert.assertEquals("vaadin-virtual-list", multiSelect.getElement().getTag()); + } + @Test public void setValue_updatesSelectionAndTriggersSelectionListener() { multiSelect.setValue(Set.of("2", "3")); @@ -186,4 +192,34 @@ public void emptySelection_deselectAll_noChanges() { Mockito.verify(selectionListenerSpy, Mockito.times(0)) .selectionChange(Mockito.any()); } + + @Test + public void binderTest() { + var binder = new Binder(Person.class); + binder.bind(multiSelect, "values"); + + var person = new Person(Set.of("1")); + binder.setBean(person); + + multiSelect.select("2"); + + Assert.assertEquals(Set.of("1", "2"), multiSelect.getSelectedItems()); + Assert.assertEquals(Set.of("1", "2"), person.getValues()); + } + + public static class Person { + private Set values; + + public Person(Set values) { + this.values = values; + } + + public Set getValues() { + return values; + } + + public void setValues(Set values) { + this.values = values; + } + } } diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsSingleSelectTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsSingleSelectTest.java index 5f8b36bbf86..10b9e7eb079 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsSingleSelectTest.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsSingleSelectTest.java @@ -15,6 +15,8 @@ */ package com.vaadin.flow.component.virtuallist.tests; +import java.util.Set; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -24,6 +26,7 @@ import com.vaadin.flow.component.HasValue.ValueChangeListener; import com.vaadin.flow.component.virtuallist.VirtualList; import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; +import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.data.selection.SingleSelect; /** @@ -52,6 +55,11 @@ public void getValue() { Assert.assertEquals("2", singleSelect.getValue()); } + @Test + public void getElement() { + Assert.assertEquals("vaadin-virtual-list", singleSelect.getElement().getTag()); + } + @Test public void setValue_triggersSelectionListener() { singleSelect.setValue("2"); @@ -102,4 +110,34 @@ public void emptySelection_clear_noChanges() { Mockito.verify(selectionListenerSpy, Mockito.times(0)) .valueChanged(Mockito.any()); } + + @Test + public void binderTest() { + var binder = new Binder(Person.class); + binder.bind(singleSelect, "value"); + + var person = new Person("1"); + binder.setBean(person); + + singleSelect.setValue("2"); + + Assert.assertEquals( "2", singleSelect.getValue()); + Assert.assertEquals( "2", person.getValue()); + } + + public static class Person { + private String value; + + public Person(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } } diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListMultiSelectionTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListMultiSelectionTest.java index 3968b1b282f..17e668a70c9 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListMultiSelectionTest.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListMultiSelectionTest.java @@ -29,6 +29,8 @@ import com.vaadin.flow.data.provider.CompositeDataGenerator; import com.vaadin.flow.data.provider.DataGenerator; import com.vaadin.flow.data.selection.SelectionListener; +import com.vaadin.flow.data.selection.SelectionModel; +import com.vaadin.flow.data.selection.SelectionModel.Multi; /** * Tests multi-selectable VirtualList @@ -39,13 +41,15 @@ public class VirtualListMultiSelectionTest { private SelectionListener, String> selectionListenerSpy; private CompositeDataGenerator dataGenerator; private DataGenerator dataGeneratorSpy; + private SelectionModel.Multi, String> selectionModel; @SuppressWarnings("unchecked") @Before public void setUp() { list = new VirtualList<>(); list.setItems("1", "2", "3", "4", "5"); - list.setSelectionMode(SelectionMode.MULTI); + selectionModel = (Multi, String>) list + .setSelectionMode(SelectionMode.MULTI); selectionListenerSpy = Mockito.mock(SelectionListener.class); list.addSelectionListener(selectionListenerSpy); @@ -129,6 +133,18 @@ public void select_deselect_selectionChanged() { .selectionChange(Mockito.any()); } + @SuppressWarnings("unchecked") + @Test + public void deselectNonSelectedValue_noChanges() { + list.select("1"); + Mockito.reset(selectionListenerSpy); + list.deselect("2"); + + Assert.assertEquals(Set.of("1"), list.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .selectionChange(Mockito.any()); + } + @SuppressWarnings("unchecked") @Test public void selecti_deselectAll_selectionChanged() { @@ -147,6 +163,27 @@ public void emptySelection_deselectAll_noChanges() { .selectionChange(Mockito.any()); } + @Test + public void selectAll_selectionChanged() { + selectionModel.selectAll(); + Assert.assertEquals(Set.of("1", "2", "3", "4", "5"), + list.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(1)) + .selectionChange(Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void selectAll_selectAll_selectionChanged() { + selectionModel.selectAll(); + Mockito.reset(selectionListenerSpy); + selectionModel.selectAll(); + Assert.assertEquals(Set.of("1", "2", "3", "4", "5"), + list.getSelectedItems()); + Mockito.verify(selectionListenerSpy, Mockito.times(0)) + .selectionChange(Mockito.any()); + } + @Test public void select_generateItemSelected() { list.select("1"); @@ -198,4 +235,15 @@ public void updateSelectionFromClient_itemsChanged() { .selectionChange(Mockito.any()); } + @Test + public void getFirstSelectedItem() { + Assert.assertFalse(selectionModel.getFirstSelectedItem().isPresent()); + } + + @Test + public void select_getFirstSelectedItem() { + list.select("1"); + Assert.assertEquals("1", selectionModel.getFirstSelectedItem().get()); + } + } From fcf6d66255e161deeace0ef9b80d29244a480334 Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Mon, 2 Dec 2024 11:57:08 +0200 Subject: [PATCH 04/10] implement deselectAllowed --- .../tests/VirtualListSelectionPage.java | 11 ++++- .../component/virtuallist/VirtualList.java | 3 +- .../VirtualListSingleSelectionModel.java | 5 +-- .../frontend/virtualListConnector.js | 20 +++++++++ .../tests/VirtualListSingleSelectionTest.java | 42 +++++++++++++++++++ 5 files changed, 76 insertions(+), 5 deletions(-) diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java index 959cdfb5780..6954ed284a9 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java @@ -22,6 +22,7 @@ import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.NativeButton; import com.vaadin.flow.component.virtuallist.VirtualList; +import com.vaadin.flow.component.virtuallist.VirtualListSingleSelectionModel; import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; import com.vaadin.flow.data.provider.DataProvider; import com.vaadin.flow.data.renderer.LitRenderer; @@ -45,7 +46,10 @@ public VirtualListSelectionPage() { list.setRenderer(LitRenderer. of("
${item.name}
") .withProperty("name", item -> item.name)); - list.setSelectionMode(SelectionMode.MULTI); + var model = (VirtualListSingleSelectionModel)list.setSelectionMode(SelectionMode.SINGLE); + model.setDeselectAllowed(false); + + list.select(items.get(0)); list.addSelectionListener(e -> { System.out.println("Selected items: " + e.getAllSelectedItems()); @@ -58,6 +62,11 @@ public VirtualListSelectionPage() { }); add(button); + var deselectAll = new NativeButton("Deselect all", e -> { + list.deselectAll(); + }); + add(deselectAll); + var printSelectionButton = new NativeButton("Print selection", e -> { System.out.println("Selected item: " + list.getSelectionModel().getSelectedItems()); diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java index 4d8bed3c59c..48a211c12ae 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java @@ -461,7 +461,8 @@ private void setSelectionModel(SelectionModel, T> model, selectionModel = model; this.selectionMode = selectionMode; - + + getElement().removeProperty("__deselectionDisallowed"); getElement().setProperty("selectionMode", SelectionMode.NONE.equals(selectionMode) ? null : selectionMode.name().toLowerCase()); diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java index 4667fbbaa53..542ab1e2a25 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java @@ -41,7 +41,6 @@ public class VirtualListSingleSelectionModel implements SelectionModel.Single, T> { private T selectedItem; - private boolean deselectAllowed = true; private VirtualList list; /** @@ -81,12 +80,12 @@ public Optional getSelectedItem() { @Override public void setDeselectAllowed(boolean deselectAllowed) { - this.deselectAllowed = deselectAllowed; + list.getElement().setProperty("__deselectionDisallowed", !deselectAllowed); } @Override public boolean isDeselectAllowed() { - return deselectAllowed; + return !list.getElement().getProperty("__deselectionDisallowed", false); } public SingleSelect, T> asSingleSelect() { diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js index 98a99950962..2862a745125 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js @@ -161,6 +161,26 @@ window.Vaadin.Flow.virtualListConnector = { let previousSelectedKeys = []; + // This listener is used to prevent user from de-selecting the selected item when deselection is disallowed + list.addEventListener('selected-items-changed', function (event) { + if (list.$connector.__revertingSelection) { + event.stopImmediatePropagation(); + return; + } + if ( + list.selectionMode === 'single' && + list.__deselectionDisallowed && + previousSelectedKeys.length && + list.selectedItems.length === 0 && + !list.$connector.__updatingSelectedItemsFromServer + ) { + event.stopImmediatePropagation(); + list.$connector.__revertingSelection = true; + list.selectedItems = list.items.filter((item) => item && item.selected); + list.$connector.__revertingSelection = false; + } + }); + list.addEventListener('selected-items-changed', function (event) { const selectedKeys = event.detail.value.map((item) => item.key); const addedKeys = selectedKeys.filter((key) => !previousSelectedKeys.includes(key)); diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java index 68c8c760603..689fc0f4b05 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java @@ -25,6 +25,7 @@ import org.mockito.Mockito; import com.vaadin.flow.component.virtuallist.VirtualList; +import com.vaadin.flow.component.virtuallist.VirtualListSingleSelectionModel; import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; import com.vaadin.flow.data.provider.CompositeDataGenerator; import com.vaadin.flow.data.provider.DataGenerator; @@ -196,4 +197,45 @@ public void updateSelectionFromClient_itemsChanged() { Mockito.verify(selectionListenerSpy, Mockito.times(1)) .selectionChange(Mockito.any()); } + + @Test + public void deselectAllowed_defaultValue() { + var model = (VirtualListSingleSelectionModel) list.getSelectionModel(); + Assert.assertTrue(model.isDeselectAllowed()); + } + + @Test + public void deselectAllowed_setDisallowed() { + var model = (VirtualListSingleSelectionModel) list.getSelectionModel(); + model.setDeselectAllowed(false); + Assert.assertFalse(model.isDeselectAllowed()); + } + + @Test + public void deselectAllowed_changeMode() { + var model = (VirtualListSingleSelectionModel) list.getSelectionModel(); + model.setDeselectAllowed(false); + list.setSelectionMode(SelectionMode.MULTI); + + model = (VirtualListSingleSelectionModel) list.setSelectionMode(SelectionMode.SINGLE); + Assert.assertTrue(model.isDeselectAllowed()); + } + + @Test + public void deselectAllowed_setSameMode() { + var model = (VirtualListSingleSelectionModel) list.getSelectionModel(); + model.setDeselectAllowed(false); + list.setSelectionMode(SelectionMode.SINGLE); + model = (VirtualListSingleSelectionModel) list.setSelectionMode(SelectionMode.SINGLE); + Assert.assertTrue(model.isDeselectAllowed()); + } + + @Test + public void deselectDisallowed_allowProgrammaticDeselection() { + var model = (VirtualListSingleSelectionModel) list.getSelectionModel(); + model.setDeselectAllowed(false); + list.select("1"); + list.deselect("1"); + Assert.assertEquals(Set.of(), list.getSelectedItems()); + } } From ff3d2dcfd88699f79d66db8307f54613863ba29c Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Tue, 3 Dec 2024 12:34:03 +0200 Subject: [PATCH 05/10] feat: add itemAccessibleNameGenerator --- .../tests/VirtualListSelectionPage.java | 2 ++ .../component/virtuallist/VirtualList.java | 31 +++++++++++++++++++ .../frontend/virtualListConnector.js | 2 ++ 3 files changed, 35 insertions(+) diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java index 6954ed284a9..30eef256422 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java @@ -55,6 +55,8 @@ public VirtualListSelectionPage() { System.out.println("Selected items: " + e.getAllSelectedItems()); }); + list.setItemAccessibleNameGenerator(item -> "Accessible " + item.name); + add(list); var button = new NativeButton("Select second item", e -> { diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java index 48a211c12ae..d44859f29d1 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java @@ -55,6 +55,7 @@ import com.vaadin.flow.data.selection.SingleSelectionEvent; import com.vaadin.flow.data.selection.SingleSelectionListener; import com.vaadin.flow.dom.DisabledUpdateMode; +import com.vaadin.flow.function.SerializableFunction; import com.vaadin.flow.function.ValueProvider; import com.vaadin.flow.internal.JsonUtils; import com.vaadin.flow.server.Command; @@ -137,6 +138,7 @@ public void initialize() { private SelectionMode selectionMode; private SelectionModel, T> selectionModel; + private SerializableFunction itemAccessibleNameGenerator = item -> null; private final CompositeDataGenerator dataGenerator = new CompositeDataGenerator<>(); private final List renderingRegistrations = new ArrayList<>(); @@ -175,6 +177,10 @@ private void initSelection() { if (this.getSelectionModel().isSelected(item)) { jsonObject.put("selected", true); } + var accessibleName = this.itemAccessibleNameGenerator.apply(item); + if (accessibleName != null) { + jsonObject.put("accessibleName", accessibleName); + } }); // Set up SingleSelectionEvent and MultiSelectionEvent listeners to @@ -603,6 +609,31 @@ public SelectionModel, T> getSelectionModel() { return selectionModel; } + /** + * A function that generates accessible names for virtual list items. + * + * @param itemAccessibleNameGenerator + * the item accessible name generator to set, not {@code null} + * @throws NullPointerException + * if {@code itemAccessibleNameGenerator} is {@code null} + */ + public void setItemAccessibleNameGenerator( + SerializableFunction itemAccessibleNameGenerator) { + Objects.requireNonNull(itemAccessibleNameGenerator, + "Part name generator can not be null"); + this.itemAccessibleNameGenerator = itemAccessibleNameGenerator; + getDataCommunicator().reset(); + } + + /** + * Gets the function that generates accessible names for virtual list items. + * + * @return the item accessible name generator + */ + public SerializableFunction getItemAccessibleNameGenerator() { + return itemAccessibleNameGenerator; + } + /** * Selection mode representing the built-in selection models in virtual * list. diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js index 2862a745125..e905582ee91 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js @@ -16,6 +16,8 @@ window.Vaadin.Flow.virtualListConnector = { list.$connector = {}; list.$connector.placeholderItem = { __placeholder: true }; + list.itemAccessibleNameGenerator = (item) => item && item.accessibleName; + const updateRequestedItem = function () { /* * TODO virtual list seems to do a small index adjustment after scrolling From f93d85c8ee2fadf912af930af9318908e9c4ed4a Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Wed, 4 Dec 2024 12:33:47 +0200 Subject: [PATCH 06/10] feat: TB element and ITs --- .../tests/VirtualListSelectionPage.java | 64 +++++--- .../tests/VirtualListSelectionIT.java | 139 ++++++++++++++++++ .../frontend/virtualListConnector.js | 3 +- .../testbench/VirtualListElement.java | 53 +++++++ 4 files changed, 239 insertions(+), 20 deletions(-) create mode 100644 vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java index 30eef256422..e384bc80007 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java @@ -20,12 +20,14 @@ import java.util.stream.IntStream; import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.html.NativeButton; import com.vaadin.flow.component.virtuallist.VirtualList; -import com.vaadin.flow.component.virtuallist.VirtualListSingleSelectionModel; import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; import com.vaadin.flow.data.provider.DataProvider; import com.vaadin.flow.data.renderer.LitRenderer; +import com.vaadin.flow.data.selection.SelectionListener; +import com.vaadin.flow.data.selection.SelectionModel; import com.vaadin.flow.router.Route; /** @@ -33,7 +35,7 @@ * * @author Vaadin Ltd. */ -@Route("vaadin-virtual-list/virtual-list-selection") +@Route("vaadin-virtual-list/selection") public class VirtualListSelectionPage extends Div { public VirtualListSelectionPage() { @@ -46,35 +48,59 @@ public VirtualListSelectionPage() { list.setRenderer(LitRenderer. of("
${item.name}
") .withProperty("name", item -> item.name)); - var model = (VirtualListSingleSelectionModel)list.setSelectionMode(SelectionMode.SINGLE); - model.setDeselectAllowed(false); - - list.select(items.get(0)); - - list.addSelectionListener(e -> { - System.out.println("Selected items: " + e.getAllSelectedItems()); - }); list.setItemAccessibleNameGenerator(item -> "Accessible " + item.name); add(list); - var button = new NativeButton("Select second item", e -> { - list.select(items.get(1)); + var selectedIndexes = new Div(); + selectedIndexes.setHeight("30px"); + selectedIndexes.setId("selected-indexes"); + SelectionListener, Item> selectionListener = event -> { + selectedIndexes.setText(event.getAllSelectedItems().stream().map(item -> String.valueOf(items.indexOf(item))).collect(Collectors.joining(", "))); + }; + + add(new Div(new H2("Selected item indexes"), selectedIndexes)); + + var selectFirstButton = new NativeButton("Select first item", e -> { + list.select(items.get(0)); }); - add(button); + selectFirstButton.setId("select-first"); - var deselectAll = new NativeButton("Deselect all", e -> { + var deselectAllButton = new NativeButton("Deselect all", e -> { list.deselectAll(); }); - add(deselectAll); + deselectAllButton.setId("deselect-all"); + + add(new Div(new H2("Actions"), selectFirstButton, deselectAllButton)); - var printSelectionButton = new NativeButton("Print selection", e -> { - System.out.println("Selected item: " - + list.getSelectionModel().getSelectedItems()); + var noneSelectionModeButton = new NativeButton("None", e -> { + list.setSelectionMode(SelectionMode.NONE); }); - add(printSelectionButton); + noneSelectionModeButton.setId("none-selection-mode"); + + var singleSelectionModeButton = new NativeButton("Single", e -> { + list.setSelectionMode(SelectionMode.SINGLE); + list.addSelectionListener(selectionListener); + }); + singleSelectionModeButton.setId("single-selection-mode"); + + var singleSelectionModeDeselectionDisallowedButton = new NativeButton("Single (deselection disallowed)", e -> { + var model = list.setSelectionMode(SelectionMode.SINGLE); + ((SelectionModel.Single, Item>)model).setDeselectAllowed(false); + list.addSelectionListener(selectionListener); + }); + singleSelectionModeDeselectionDisallowedButton.setId("single-selection-mode-deselection-disallowed"); + + var multiSelectionModeButton = new NativeButton("Multi", e -> { + list.setSelectionMode(SelectionMode.MULTI); + list.addSelectionListener(selectionListener); + }); + multiSelectionModeButton.setId("multi-selection-mode"); + + add(new Div(new H2("Selection mode"), noneSelectionModeButton, singleSelectionModeButton, singleSelectionModeDeselectionDisallowedButton, multiSelectionModeButton)); + } private List createItems() { diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java new file mode 100644 index 00000000000..07c855e2d11 --- /dev/null +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java @@ -0,0 +1,139 @@ +/* + * Copyright 2000-2024 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.virtuallist.tests; + +import java.util.Set; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; + +import com.vaadin.flow.component.virtuallist.testbench.VirtualListElement; +import com.vaadin.flow.testutil.TestPath; +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.tests.AbstractComponentIT; + +@TestPath("vaadin-virtual-list/selection") +public class VirtualListSelectionIT extends AbstractComponentIT { + private VirtualListElement virtualList; + private TestBenchElement singleSelectionModeButton; + private TestBenchElement singleSelectionModeDeselectionDisallowedButton; + private TestBenchElement multiSelectionModeButton; + private TestBenchElement selectFirstButton; + private TestBenchElement deselectAllButton; + private TestBenchElement selectedIndexes; + + @Before + public void init() { + open(); + virtualList = $(VirtualListElement.class).waitForFirst(); + singleSelectionModeButton = $("button").id("single-selection-mode"); + singleSelectionModeDeselectionDisallowedButton = $("button").id("single-selection-mode-deselection-disallowed"); + multiSelectionModeButton = $("button").id("multi-selection-mode"); + selectFirstButton = $("button").id("select-first"); + deselectAllButton = $("button").id("deselect-all"); + selectedIndexes = $("div").id("selected-indexes"); + } + + @Test + public void select_shouldNotSelect() { + virtualList.select(0); + + var selectedRows = virtualList.getSelectedIndexes(); + Assert.assertTrue(selectedRows.isEmpty()); + } + + @Test + public void singleSelectionMode_select() { + singleSelectionModeButton.click(); + virtualList.select(0); + Assert.assertEquals(Set.of(0), virtualList.getSelectedIndexes()); + } + + @Test + public void singleSelectionMode_selectAnother() { + singleSelectionModeButton.click(); + virtualList.select(0); + virtualList.select(1); + Assert.assertEquals(Set.of(1), virtualList.getSelectedIndexes()); + } + + @Test + public void singleSelectionMode_deselect() { + singleSelectionModeButton.click(); + virtualList.select(0); + virtualList.deselect(0); + Assert.assertEquals(Set.of(), virtualList.getSelectedIndexes()); + } + + @Test + public void singleSelectionModeDeselectionDisallowed_deselectionNotAllowed() { + singleSelectionModeDeselectionDisallowedButton.click(); + Assert.assertEquals(Set.of(), virtualList.getSelectedIndexes()); + virtualList.select(0); + virtualList.deselect(0); + Assert.assertEquals(Set.of(0), virtualList.getSelectedIndexes()); + } + + @Test + public void multiSelectionMode_selectMultiple() { + multiSelectionModeButton.click(); + virtualList.select(0); + virtualList.select(3); + Assert.assertEquals(Set.of(0, 3), virtualList.getSelectedIndexes()); + } + + @Test + public void multiSelectionMode_deselect() { + multiSelectionModeButton.click(); + virtualList.select(0); + virtualList.select(3); + virtualList.select(1); + virtualList.deselect(0); + Assert.assertEquals(Set.of(1, 3), virtualList.getSelectedIndexes()); + } + + @Test + public void multiSelectionMode_serverSideSelection() { + multiSelectionModeButton.click(); + selectFirstButton.click(); + virtualList.select(3); + virtualList.select(1); + virtualList.deselect(0); + + var selectedIndexesSet = Set.of(selectedIndexes.getText().split(", ")); + Assert.assertEquals(Set.of("1", "3"), selectedIndexesSet); + } + + @Test + public void programmaticSelection() { + singleSelectionModeButton.click(); + selectFirstButton.click(); + Assert.assertEquals(Set.of(0), virtualList.getSelectedIndexes()); + + deselectAllButton.click(); + Assert.assertEquals(Set.of(), virtualList.getSelectedIndexes()); + } + + @Test + public void accessibleName() { + var firstChildElement = virtualList.findElement(By.xpath("child::div[@aria-posinset='1']")); + Assert.assertEquals("Accessible Item 0", firstChildElement.getAttribute("aria-label")); + } + + +} diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js index e905582ee91..6f728471870 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js @@ -8,7 +8,6 @@ window.Vaadin.Flow.virtualListConnector = { return; } - list.itemIdPath = 'key'; const extraItemsBuffer = 20; let lastRequestedRange = [0, 0]; @@ -166,9 +165,11 @@ window.Vaadin.Flow.virtualListConnector = { // This listener is used to prevent user from de-selecting the selected item when deselection is disallowed list.addEventListener('selected-items-changed', function (event) { if (list.$connector.__revertingSelection) { + // Reverting the (de)selection, stop the event and don't do anything event.stopImmediatePropagation(); return; } + if ( list.selectionMode === 'single' && list.__deselectionDisallowed && diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-testbench/src/main/java/com/vaadin/flow/component/virtuallist/testbench/VirtualListElement.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-testbench/src/main/java/com/vaadin/flow/component/virtuallist/testbench/VirtualListElement.java index fc15633da48..5b5a9f19760 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-testbench/src/main/java/com/vaadin/flow/component/virtuallist/testbench/VirtualListElement.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-testbench/src/main/java/com/vaadin/flow/component/virtuallist/testbench/VirtualListElement.java @@ -15,6 +15,13 @@ */ package com.vaadin.flow.component.virtuallist.testbench; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import org.openqa.selenium.By; + import com.vaadin.testbench.TestBenchElement; import com.vaadin.testbench.elementsbase.Element; @@ -77,4 +84,50 @@ public int getRowCount() { return getPropertyInteger("items", "length"); } + /** + * Selects the row with the given index. + * + * @param rowIndex + * the row to select + */ + public void select(int rowIndex) { + if (!isRowInView(rowIndex)) { + scrollToRow(rowIndex); + } + if (getSelectedIndexes().contains(rowIndex)) { + return; + } + + var element = this.findElement(By.xpath("child::div[@aria-posinset='" + (rowIndex + 1) + "']")); + element.click(); + } + + /** + * Deselects the row with the given index. + * + * @param rowIndex + * the row to deselect + */ + public void deselect(int rowIndex) { + if (!isRowInView(rowIndex)) { + scrollToRow(rowIndex); + } + if (!getSelectedIndexes().contains(rowIndex)) { + return; + } + + var element = this.findElement(By.xpath("child::div[@aria-posinset='" + (rowIndex + 1) + "']")); + element.click(); + } + + /** + * Gets the indexes of the selected rows. + * + * @return the indexes of the selected rows + */ + public Set getSelectedIndexes() { + var selectedIndexes = (ArrayList) executeScript("return arguments[0].selectedItems.map(i => arguments[0].items.indexOf(i))", this); + return Set.copyOf(selectedIndexes.stream().map(Long::intValue).toList()); + } + } From 7ae966967405674d8db00e40b4805e3f4b232d9d Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Wed, 4 Dec 2024 17:09:48 +0200 Subject: [PATCH 07/10] chore: format --- .../tests/VirtualListSelectionPage.java | 26 ++++++++++++------- .../tests/VirtualListSelectionIT.java | 16 +++++++----- .../component/virtuallist/VirtualList.java | 4 +-- .../VirtualListSingleSelectionModel.java | 4 +-- .../tests/VirtualListAsMultiSelectTest.java | 3 ++- .../tests/VirtualListAsSingleSelectTest.java | 9 +++---- .../tests/VirtualListSingleSelectionTest.java | 25 +++++++++++------- .../testbench/VirtualListElement.java | 15 ++++++----- 8 files changed, 59 insertions(+), 43 deletions(-) diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java index e384bc80007..acde304e609 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionPage.java @@ -48,7 +48,6 @@ public VirtualListSelectionPage() { list.setRenderer(LitRenderer. of("
${item.name}
") .withProperty("name", item -> item.name)); - list.setItemAccessibleNameGenerator(item -> "Accessible " + item.name); add(list); @@ -57,7 +56,9 @@ public VirtualListSelectionPage() { selectedIndexes.setHeight("30px"); selectedIndexes.setId("selected-indexes"); SelectionListener, Item> selectionListener = event -> { - selectedIndexes.setText(event.getAllSelectedItems().stream().map(item -> String.valueOf(items.indexOf(item))).collect(Collectors.joining(", "))); + selectedIndexes.setText(event.getAllSelectedItems().stream() + .map(item -> String.valueOf(items.indexOf(item))) + .collect(Collectors.joining(", "))); }; add(new Div(new H2("Selected item indexes"), selectedIndexes)); @@ -85,12 +86,15 @@ public VirtualListSelectionPage() { }); singleSelectionModeButton.setId("single-selection-mode"); - var singleSelectionModeDeselectionDisallowedButton = new NativeButton("Single (deselection disallowed)", e -> { - var model = list.setSelectionMode(SelectionMode.SINGLE); - ((SelectionModel.Single, Item>)model).setDeselectAllowed(false); - list.addSelectionListener(selectionListener); - }); - singleSelectionModeDeselectionDisallowedButton.setId("single-selection-mode-deselection-disallowed"); + var singleSelectionModeDeselectionDisallowedButton = new NativeButton( + "Single (deselection disallowed)", e -> { + var model = list.setSelectionMode(SelectionMode.SINGLE); + ((SelectionModel.Single, Item>) model) + .setDeselectAllowed(false); + list.addSelectionListener(selectionListener); + }); + singleSelectionModeDeselectionDisallowedButton + .setId("single-selection-mode-deselection-disallowed"); var multiSelectionModeButton = new NativeButton("Multi", e -> { list.setSelectionMode(SelectionMode.MULTI); @@ -98,9 +102,11 @@ public VirtualListSelectionPage() { }); multiSelectionModeButton.setId("multi-selection-mode"); - add(new Div(new H2("Selection mode"), noneSelectionModeButton, singleSelectionModeButton, singleSelectionModeDeselectionDisallowedButton, multiSelectionModeButton)); + add(new Div(new H2("Selection mode"), noneSelectionModeButton, + singleSelectionModeButton, + singleSelectionModeDeselectionDisallowedButton, + multiSelectionModeButton)); - } private List createItems() { diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java index 07c855e2d11..9bc335fd875 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java @@ -42,7 +42,8 @@ public void init() { open(); virtualList = $(VirtualListElement.class).waitForFirst(); singleSelectionModeButton = $("button").id("single-selection-mode"); - singleSelectionModeDeselectionDisallowedButton = $("button").id("single-selection-mode-deselection-disallowed"); + singleSelectionModeDeselectionDisallowedButton = $("button") + .id("single-selection-mode-deselection-disallowed"); multiSelectionModeButton = $("button").id("multi-selection-mode"); selectFirstButton = $("button").id("select-first"); deselectAllButton = $("button").id("deselect-all"); @@ -52,9 +53,9 @@ public void init() { @Test public void select_shouldNotSelect() { virtualList.select(0); - + var selectedRows = virtualList.getSelectedIndexes(); - Assert.assertTrue(selectedRows.isEmpty()); + Assert.assertTrue(selectedRows.isEmpty()); } @Test @@ -114,7 +115,7 @@ public void multiSelectionMode_serverSideSelection() { virtualList.select(3); virtualList.select(1); virtualList.deselect(0); - + var selectedIndexesSet = Set.of(selectedIndexes.getText().split(", ")); Assert.assertEquals(Set.of("1", "3"), selectedIndexesSet); } @@ -131,9 +132,10 @@ public void programmaticSelection() { @Test public void accessibleName() { - var firstChildElement = virtualList.findElement(By.xpath("child::div[@aria-posinset='1']")); - Assert.assertEquals("Accessible Item 0", firstChildElement.getAttribute("aria-label")); + var firstChildElement = virtualList + .findElement(By.xpath("child::div[@aria-posinset='1']")); + Assert.assertEquals("Accessible Item 0", + firstChildElement.getAttribute("aria-label")); } - } diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java index d44859f29d1..62eca166f0d 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java @@ -47,13 +47,11 @@ import com.vaadin.flow.data.renderer.Renderer; import com.vaadin.flow.data.selection.MultiSelect; import com.vaadin.flow.data.selection.MultiSelectionEvent; -import com.vaadin.flow.data.selection.MultiSelectionListener; import com.vaadin.flow.data.selection.SelectionListener; import com.vaadin.flow.data.selection.SelectionModel; import com.vaadin.flow.data.selection.SelectionModel.Single; import com.vaadin.flow.data.selection.SingleSelect; import com.vaadin.flow.data.selection.SingleSelectionEvent; -import com.vaadin.flow.data.selection.SingleSelectionListener; import com.vaadin.flow.dom.DisabledUpdateMode; import com.vaadin.flow.function.SerializableFunction; import com.vaadin.flow.function.ValueProvider; @@ -467,7 +465,7 @@ private void setSelectionModel(SelectionModel, T> model, selectionModel = model; this.selectionMode = selectionMode; - + getElement().removeProperty("__deselectionDisallowed"); getElement().setProperty("selectionMode", SelectionMode.NONE.equals(selectionMode) ? null diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java index 542ab1e2a25..74576248895 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualListSingleSelectionModel.java @@ -26,7 +26,6 @@ import com.vaadin.flow.data.selection.SelectionModel; import com.vaadin.flow.data.selection.SingleSelect; import com.vaadin.flow.data.selection.SingleSelectionEvent; -import com.vaadin.flow.data.selection.SingleSelectionListener; import com.vaadin.flow.dom.Element; import com.vaadin.flow.shared.Registration; @@ -80,7 +79,8 @@ public Optional getSelectedItem() { @Override public void setDeselectAllowed(boolean deselectAllowed) { - list.getElement().setProperty("__deselectionDisallowed", !deselectAllowed); + list.getElement().setProperty("__deselectionDisallowed", + !deselectAllowed); } @Override diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsMultiSelectTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsMultiSelectTest.java index a8d3a944458..5a0ac38e936 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsMultiSelectTest.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsMultiSelectTest.java @@ -70,7 +70,8 @@ public void getSelectedItems() { @Test public void getElement() { - Assert.assertEquals("vaadin-virtual-list", multiSelect.getElement().getTag()); + Assert.assertEquals("vaadin-virtual-list", + multiSelect.getElement().getTag()); } @Test diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsSingleSelectTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsSingleSelectTest.java index 10b9e7eb079..a0426c10601 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsSingleSelectTest.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListAsSingleSelectTest.java @@ -15,8 +15,6 @@ */ package com.vaadin.flow.component.virtuallist.tests; -import java.util.Set; - import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -57,7 +55,8 @@ public void getValue() { @Test public void getElement() { - Assert.assertEquals("vaadin-virtual-list", singleSelect.getElement().getTag()); + Assert.assertEquals("vaadin-virtual-list", + singleSelect.getElement().getTag()); } @Test @@ -121,8 +120,8 @@ public void binderTest() { singleSelect.setValue("2"); - Assert.assertEquals( "2", singleSelect.getValue()); - Assert.assertEquals( "2", person.getValue()); + Assert.assertEquals("2", singleSelect.getValue()); + Assert.assertEquals("2", person.getValue()); } public static class Person { diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java index 689fc0f4b05..bda49d01343 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSingleSelectionTest.java @@ -25,8 +25,8 @@ import org.mockito.Mockito; import com.vaadin.flow.component.virtuallist.VirtualList; -import com.vaadin.flow.component.virtuallist.VirtualListSingleSelectionModel; import com.vaadin.flow.component.virtuallist.VirtualList.SelectionMode; +import com.vaadin.flow.component.virtuallist.VirtualListSingleSelectionModel; import com.vaadin.flow.data.provider.CompositeDataGenerator; import com.vaadin.flow.data.provider.DataGenerator; import com.vaadin.flow.data.selection.SelectionListener; @@ -200,39 +200,46 @@ public void updateSelectionFromClient_itemsChanged() { @Test public void deselectAllowed_defaultValue() { - var model = (VirtualListSingleSelectionModel) list.getSelectionModel(); + var model = (VirtualListSingleSelectionModel) list + .getSelectionModel(); Assert.assertTrue(model.isDeselectAllowed()); } @Test public void deselectAllowed_setDisallowed() { - var model = (VirtualListSingleSelectionModel) list.getSelectionModel(); + var model = (VirtualListSingleSelectionModel) list + .getSelectionModel(); model.setDeselectAllowed(false); Assert.assertFalse(model.isDeselectAllowed()); } @Test public void deselectAllowed_changeMode() { - var model = (VirtualListSingleSelectionModel) list.getSelectionModel(); + var model = (VirtualListSingleSelectionModel) list + .getSelectionModel(); model.setDeselectAllowed(false); list.setSelectionMode(SelectionMode.MULTI); - model = (VirtualListSingleSelectionModel) list.setSelectionMode(SelectionMode.SINGLE); + model = (VirtualListSingleSelectionModel) list + .setSelectionMode(SelectionMode.SINGLE); Assert.assertTrue(model.isDeselectAllowed()); } @Test public void deselectAllowed_setSameMode() { - var model = (VirtualListSingleSelectionModel) list.getSelectionModel(); + var model = (VirtualListSingleSelectionModel) list + .getSelectionModel(); model.setDeselectAllowed(false); - list.setSelectionMode(SelectionMode.SINGLE); - model = (VirtualListSingleSelectionModel) list.setSelectionMode(SelectionMode.SINGLE); + list.setSelectionMode(SelectionMode.SINGLE); + model = (VirtualListSingleSelectionModel) list + .setSelectionMode(SelectionMode.SINGLE); Assert.assertTrue(model.isDeselectAllowed()); } @Test public void deselectDisallowed_allowProgrammaticDeselection() { - var model = (VirtualListSingleSelectionModel) list.getSelectionModel(); + var model = (VirtualListSingleSelectionModel) list + .getSelectionModel(); model.setDeselectAllowed(false); list.select("1"); list.deselect("1"); diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-testbench/src/main/java/com/vaadin/flow/component/virtuallist/testbench/VirtualListElement.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-testbench/src/main/java/com/vaadin/flow/component/virtuallist/testbench/VirtualListElement.java index 5b5a9f19760..22f5cd300b1 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-testbench/src/main/java/com/vaadin/flow/component/virtuallist/testbench/VirtualListElement.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-testbench/src/main/java/com/vaadin/flow/component/virtuallist/testbench/VirtualListElement.java @@ -16,8 +16,6 @@ package com.vaadin.flow.component.virtuallist.testbench; import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.Set; import org.openqa.selenium.By; @@ -98,7 +96,8 @@ public void select(int rowIndex) { return; } - var element = this.findElement(By.xpath("child::div[@aria-posinset='" + (rowIndex + 1) + "']")); + var element = this.findElement(By + .xpath("child::div[@aria-posinset='" + (rowIndex + 1) + "']")); element.click(); } @@ -116,7 +115,8 @@ public void deselect(int rowIndex) { return; } - var element = this.findElement(By.xpath("child::div[@aria-posinset='" + (rowIndex + 1) + "']")); + var element = this.findElement(By + .xpath("child::div[@aria-posinset='" + (rowIndex + 1) + "']")); element.click(); } @@ -126,8 +126,11 @@ public void deselect(int rowIndex) { * @return the indexes of the selected rows */ public Set getSelectedIndexes() { - var selectedIndexes = (ArrayList) executeScript("return arguments[0].selectedItems.map(i => arguments[0].items.indexOf(i))", this); - return Set.copyOf(selectedIndexes.stream().map(Long::intValue).toList()); + var selectedIndexes = (ArrayList) executeScript( + "return arguments[0].selectedItems.map(i => arguments[0].items.indexOf(i))", + this); + return Set + .copyOf(selectedIndexes.stream().map(Long::intValue).toList()); } } From 9f33e3aadb9404febec633c53aaf998a0bf64443 Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Thu, 5 Dec 2024 11:39:29 +0200 Subject: [PATCH 08/10] refactor: better API for checking selection --- .../tests/VirtualListSelectionIT.java | 38 +++++++---- .../testbench/VirtualListElement.java | 68 +++++++++++-------- 2 files changed, 63 insertions(+), 43 deletions(-) diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java index 9bc335fd875..12b05195b84 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java @@ -54,15 +54,14 @@ public void init() { public void select_shouldNotSelect() { virtualList.select(0); - var selectedRows = virtualList.getSelectedIndexes(); - Assert.assertTrue(selectedRows.isEmpty()); + Assert.assertFalse(virtualList.isRowSelected(0)); } @Test public void singleSelectionMode_select() { singleSelectionModeButton.click(); virtualList.select(0); - Assert.assertEquals(Set.of(0), virtualList.getSelectedIndexes()); + Assert.assertTrue(virtualList.isRowSelected(0)); } @Test @@ -70,7 +69,8 @@ public void singleSelectionMode_selectAnother() { singleSelectionModeButton.click(); virtualList.select(0); virtualList.select(1); - Assert.assertEquals(Set.of(1), virtualList.getSelectedIndexes()); + Assert.assertFalse(virtualList.isRowSelected(0)); + Assert.assertTrue(virtualList.isRowSelected(1)); } @Test @@ -78,34 +78,38 @@ public void singleSelectionMode_deselect() { singleSelectionModeButton.click(); virtualList.select(0); virtualList.deselect(0); - Assert.assertEquals(Set.of(), virtualList.getSelectedIndexes()); + Assert.assertFalse(virtualList.isRowSelected(0)); } @Test public void singleSelectionModeDeselectionDisallowed_deselectionNotAllowed() { singleSelectionModeDeselectionDisallowedButton.click(); - Assert.assertEquals(Set.of(), virtualList.getSelectedIndexes()); + Assert.assertFalse(virtualList.isRowSelected(0)); virtualList.select(0); virtualList.deselect(0); - Assert.assertEquals(Set.of(0), virtualList.getSelectedIndexes()); + Assert.assertTrue(virtualList.isRowSelected(0)); } @Test public void multiSelectionMode_selectMultiple() { multiSelectionModeButton.click(); virtualList.select(0); - virtualList.select(3); - Assert.assertEquals(Set.of(0, 3), virtualList.getSelectedIndexes()); + virtualList.select(2); + Assert.assertTrue(virtualList.isRowSelected(0)); + Assert.assertFalse(virtualList.isRowSelected(1)); + Assert.assertTrue(virtualList.isRowSelected(2)); } @Test public void multiSelectionMode_deselect() { multiSelectionModeButton.click(); virtualList.select(0); - virtualList.select(3); + virtualList.select(2); virtualList.select(1); virtualList.deselect(0); - Assert.assertEquals(Set.of(1, 3), virtualList.getSelectedIndexes()); + Assert.assertFalse(virtualList.isRowSelected(0)); + Assert.assertTrue(virtualList.isRowSelected(1)); + Assert.assertTrue(virtualList.isRowSelected(2)); } @Test @@ -113,21 +117,25 @@ public void multiSelectionMode_serverSideSelection() { multiSelectionModeButton.click(); selectFirstButton.click(); virtualList.select(3); - virtualList.select(1); + virtualList.select(80); virtualList.deselect(0); var selectedIndexesSet = Set.of(selectedIndexes.getText().split(", ")); - Assert.assertEquals(Set.of("1", "3"), selectedIndexesSet); + Assert.assertEquals(Set.of("3", "80"), selectedIndexesSet); } @Test public void programmaticSelection() { singleSelectionModeButton.click(); selectFirstButton.click(); - Assert.assertEquals(Set.of(0), virtualList.getSelectedIndexes()); + Assert.assertTrue(virtualList.isRowSelected(0)); + + var selectedIndexesSet = Set.of(selectedIndexes.getText().split(", ")); + Assert.assertEquals(Set.of("0"), selectedIndexesSet); deselectAllButton.click(); - Assert.assertEquals(Set.of(), virtualList.getSelectedIndexes()); + Assert.assertFalse(virtualList.isRowSelected(0)); + Assert.assertTrue(selectedIndexes.getText().isEmpty()); } @Test diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-testbench/src/main/java/com/vaadin/flow/component/virtuallist/testbench/VirtualListElement.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-testbench/src/main/java/com/vaadin/flow/component/virtuallist/testbench/VirtualListElement.java index 22f5cd300b1..7cb88e5608a 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-testbench/src/main/java/com/vaadin/flow/component/virtuallist/testbench/VirtualListElement.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-testbench/src/main/java/com/vaadin/flow/component/virtuallist/testbench/VirtualListElement.java @@ -15,9 +15,6 @@ */ package com.vaadin.flow.component.virtuallist.testbench; -import java.util.ArrayList; -import java.util.Set; - import org.openqa.selenium.By; import com.vaadin.testbench.TestBenchElement; @@ -89,16 +86,10 @@ public int getRowCount() { * the row to select */ public void select(int rowIndex) { - if (!isRowInView(rowIndex)) { - scrollToRow(rowIndex); - } - if (getSelectedIndexes().contains(rowIndex)) { - return; + var element = getRowElement(rowIndex); + if (!isSelected(element)) { + element.click(); } - - var element = this.findElement(By - .xpath("child::div[@aria-posinset='" + (rowIndex + 1) + "']")); - element.click(); } /** @@ -108,29 +99,50 @@ public void select(int rowIndex) { * the row to deselect */ public void deselect(int rowIndex) { - if (!isRowInView(rowIndex)) { - scrollToRow(rowIndex); - } - if (!getSelectedIndexes().contains(rowIndex)) { - return; + var element = getRowElement(rowIndex); + if (isSelected(element)) { + element.click(); } + } - var element = this.findElement(By - .xpath("child::div[@aria-posinset='" + (rowIndex + 1) + "']")); - element.click(); + /** + * Checks if the row at the specified index is selected. + * + * @param rowIndex + * the index of the row to check + * @return true if the row is selected, false otherwise + */ + public boolean isRowSelected(int rowIndex) { + return isSelected(getRowElement(rowIndex)); } /** - * Gets the indexes of the selected rows. + * Checks if the given TestBenchElement is selected. * - * @return the indexes of the selected rows + * @param element + * the TestBenchElement to check + * @return true if the element has the "selected" attribute, false otherwise */ - public Set getSelectedIndexes() { - var selectedIndexes = (ArrayList) executeScript( - "return arguments[0].selectedItems.map(i => arguments[0].items.indexOf(i))", - this); - return Set - .copyOf(selectedIndexes.stream().map(Long::intValue).toList()); + private boolean isSelected(TestBenchElement element) { + return element.hasAttribute("selected"); + + } + + /** + * Retrieves the row element at the specified index. If the row is not + * currently in view, it will scroll to the row first. + * + * @param rowIndex + * the index of the row to retrieve + * @return the TestBenchElement representing the row at the specified index + */ + private TestBenchElement getRowElement(int rowIndex) { + if (!isRowInView(rowIndex)) { + scrollToRow(rowIndex); + waitUntil(e -> isRowInView(rowIndex)); + } + return this.findElement(By + .xpath("child::div[@aria-posinset='" + (rowIndex + 1) + "']")); } } From 6cc36ab417ca2ef2523716ac9e98c367e49daed3 Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Thu, 5 Dec 2024 11:57:17 +0200 Subject: [PATCH 09/10] fix: deselect all on selection model change --- .../virtuallist/tests/VirtualListSelectionIT.java | 8 ++++++++ .../vaadin/flow/component/virtuallist/VirtualList.java | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java index 12b05195b84..8fb42233d2b 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListSelectionIT.java @@ -146,4 +146,12 @@ public void accessibleName() { firstChildElement.getAttribute("aria-label")); } + @Test + public void changeSelectionMode_resetSelection() { + singleSelectionModeButton.click(); + virtualList.select(0); + multiSelectionModeButton.click(); + Assert.assertFalse(virtualList.isRowSelected(0)); + } + } diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java index 62eca166f0d..94543d54464 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java @@ -463,6 +463,11 @@ private void setSelectionModel(SelectionModel, T> model, Objects.requireNonNull(model, "selection model cannot be null"); Objects.requireNonNull(selectionMode, "selection mode cannot be null"); + if (this.selectionModel != null) { + // Reset existing selections + this.selectionModel.deselectAll(); + } + selectionModel = model; this.selectionMode = selectionMode; @@ -470,7 +475,6 @@ private void setSelectionModel(SelectionModel, T> model, getElement().setProperty("selectionMode", SelectionMode.NONE.equals(selectionMode) ? null : selectionMode.name().toLowerCase()); - } /** From e95612a07a668bd95b1a0f8e911f5f98e7c83a24 Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Thu, 5 Dec 2024 15:10:19 +0200 Subject: [PATCH 10/10] refactor: update selection mode none value --- .../com/vaadin/flow/component/virtuallist/VirtualList.java | 4 +--- .../virtuallist/tests/VirtualListNoneSelectionTest.java | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java index 94543d54464..2c9e945c761 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java @@ -472,9 +472,7 @@ private void setSelectionModel(SelectionModel, T> model, this.selectionMode = selectionMode; getElement().removeProperty("__deselectionDisallowed"); - getElement().setProperty("selectionMode", - SelectionMode.NONE.equals(selectionMode) ? null - : selectionMode.name().toLowerCase()); + getElement().setProperty("selectionMode", selectionMode.name().toLowerCase()); } /** diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListNoneSelectionTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListNoneSelectionTest.java index 929938ab1d6..307180db295 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListNoneSelectionTest.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListNoneSelectionTest.java @@ -55,6 +55,7 @@ public void select_getSelectedItems_empty() { @Test public void clientSelectionMode() { - Assert.assertNull(list.getElement().getProperty("selectionMode")); + Assert.assertEquals("none", + list.getElement().getProperty("selectionMode")); } }