]> source.dussan.org Git - vaadin-framework.git/commitdiff
Add multi selection support to CheckBoxGroup
authorLeif Åstrand <leif@vaadin.com>
Tue, 13 Sep 2016 09:31:08 +0000 (12:31 +0300)
committerVaadin Code Review <review@vaadin.com>
Wed, 14 Sep 2016 07:55:54 +0000 (07:55 +0000)
This patch adds multi selection support only for CheckBoxGroup without
even trying to generalize anything. Adopting the concepts to work with
other components will be done separately.

Change-Id: Id4ccd2c743b74cb022dc9dfd8cd8dae3bf8f0c54

client/src/main/java/com/vaadin/client/ui/VCheckBoxGroup.java
client/src/main/java/com/vaadin/client/ui/optiongroup/CheckBoxGroupConnector.java
server/src/main/java/com/vaadin/event/selection/MultiSelectionEvent.java [new file with mode: 0644]
server/src/main/java/com/vaadin/event/selection/MultiSelectionListener.java [new file with mode: 0644]
server/src/main/java/com/vaadin/ui/CheckBoxGroup.java
server/src/test/java/com/vaadin/ui/CheckBoxGroupTest.java [new file with mode: 0644]
shared/src/main/java/com/vaadin/shared/ui/optiongroup/CheckBoxGroupConstants.java
uitest-common/src/main/java/com/vaadin/testbench/customelements/CheckBoxGroupElement.java
uitest/src/main/java/com/vaadin/tests/components/checkbox/CheckBoxGroupTestUI.java
uitest/src/test/java/com/vaadin/tests/components/checkboxgroup/CheckBoxGroupTest.java

index 3b6c73950cc4c660027dbbd3a291bcb622564c16..3fde2f69983fc026b8b3b9b2076833ac82795ac3 100644 (file)
 
 package com.vaadin.client.ui;
 
+import static com.vaadin.shared.ui.optiongroup.CheckBoxGroupConstants.JSONKEY_ITEM_DISABLED;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
 import com.google.gwt.aria.client.Roles;
-import com.google.gwt.event.dom.client.ChangeEvent;
-import com.google.gwt.event.dom.client.ChangeHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.user.client.ui.Composite;
@@ -32,16 +39,8 @@ import com.vaadin.client.ApplicationConnection;
 import com.vaadin.client.WidgetUtil;
 import com.vaadin.shared.Registration;
 import com.vaadin.shared.ui.optiongroup.CheckBoxGroupConstants;
-import elemental.json.JsonObject;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Consumer;
-
-import static com.vaadin.shared.ui.optiongroup.CheckBoxGroupConstants.JSONKEY_ITEM_DISABLED;
+import elemental.json.JsonObject;
 
 /**
  * The client-side widget for the {@code CheckBoxGroup} component.
@@ -49,8 +48,7 @@ import static com.vaadin.shared.ui.optiongroup.CheckBoxGroupConstants.JSONKEY_IT
  * @author Vaadin Ltd.
  * @since 8.0
  */
-public class VCheckBoxGroup extends Composite
-        implements Field, ClickHandler, ChangeHandler,
+public class VCheckBoxGroup extends Composite implements Field, ClickHandler,
         com.vaadin.client.Focusable, HasEnabled {
 
     public static final String CLASSNAME = "v-select-optiongroup";
@@ -62,10 +60,7 @@ public class VCheckBoxGroup extends Composite
      * For internal use only. May be removed or replaced in the future.
      */
     public ApplicationConnection client;
-    /**
-     * For internal use only. May be removed or replaced in the future.
-     */
-    public JsonObject selected; //TODO replace with SelectionModel
+
     /**
      * Widget holding the different options (e.g. ListBox or Panel for radio
      * buttons) (optional, fallbacks to container Panel)
@@ -78,7 +73,7 @@ public class VCheckBoxGroup extends Composite
 
     private boolean enabled;
     private boolean readonly;
-    private List<Consumer<JsonObject>> selectionChangeListeners;
+    private List<BiConsumer<JsonObject, Boolean>> selectionChangeListeners;
 
     public VCheckBoxGroup() {
         optionsContainer = new FlowPanel();
@@ -102,15 +97,15 @@ public class VCheckBoxGroup extends Composite
         Roles.getRadiogroupRole().set(getElement());
         optionsContainer.clear();
         for (JsonObject item : items) {
-            String itemHtml =
-                    item.getString(CheckBoxGroupConstants.JSONKEY_ITEM_VALUE);
+            String itemHtml = item
+                    .getString(CheckBoxGroupConstants.JSONKEY_ITEM_VALUE);
             if (!isHtmlContentAllowed()) {
                 itemHtml = WidgetUtil.escapeHTML(itemHtml);
             }
             VCheckBox checkBox = new VCheckBox();
 
-            String iconUrl =
-                    item.getString(CheckBoxGroupConstants.JSONKEY_ITEM_ICON);
+            String iconUrl = item
+                    .getString(CheckBoxGroupConstants.JSONKEY_ITEM_ICON);
             if (iconUrl != null && iconUrl.length() != 0) {
                 checkBox.icon = client.getIcon(iconUrl);
             }
@@ -118,7 +113,8 @@ public class VCheckBoxGroup extends Composite
             checkBox.addStyleName(CLASSNAME_OPTION);
             checkBox.addClickHandler(this);
             checkBox.setHTML(itemHtml);
-            checkBox.setValue(true);//TODO selection model here
+            checkBox.setValue(item
+                    .getBoolean(CheckBoxGroupConstants.JSONKEY_ITEM_SELECTED));
             boolean optionEnabled = !item.getBoolean(JSONKEY_ITEM_DISABLED);
             boolean enabled = optionEnabled && !isReadonly() && isEnabled();
             checkBox.setEnabled(enabled);
@@ -138,13 +134,13 @@ public class VCheckBoxGroup extends Composite
                 return;
             }
 
-            final boolean selected = source.getValue();
-            JsonObject item = optionsToItems.get(source);  //TODO SelectionModel
-            if (selected) {
-                this.selected = item;
-            } else {
-                this.selected = null;
-            }
+            Boolean selected = source.getValue();
+
+            JsonObject item = optionsToItems.get(source);
+            assert item != null;
+
+            new ArrayList<>(selectionChangeListeners)
+                    .forEach(listener -> listener.accept(item, selected));
         }
     }
 
@@ -163,8 +159,8 @@ public class VCheckBoxGroup extends Composite
                 .entrySet()) {
             VCheckBox checkBox = entry.getKey();
             JsonObject value = entry.getValue();
-            Boolean isOptionEnabled = !value.getBoolean(
-                    CheckBoxGroupConstants.JSONKEY_ITEM_DISABLED);
+            Boolean isOptionEnabled = !value
+                    .getBoolean(CheckBoxGroupConstants.JSONKEY_ITEM_DISABLED);
             checkBox.setEnabled(optionGroupEnabled && isOptionEnabled);
         }
     }
@@ -194,11 +190,6 @@ public class VCheckBoxGroup extends Composite
         return readonly;
     }
 
-    @Override
-    public void onChange(ChangeEvent event) {
-        //TODO selectionModel
-    }
-
     public void setReadonly(boolean readonly) {
         if (this.readonly != readonly) {
             this.readonly = readonly;
@@ -214,8 +205,8 @@ public class VCheckBoxGroup extends Composite
         }
     }
 
-    public Registration addNotifyHandler(
-            Consumer<JsonObject> selectionChanged) {
+    public Registration addSelectionChangeHandler(
+            BiConsumer<JsonObject, Boolean> selectionChanged) {
         selectionChangeListeners.add(selectionChanged);
         return (Registration) () -> selectionChangeListeners
                 .remove(selectionChanged);
index dbd531382d4f5f7729808a55fc928bd72d94d296..a60a1b111f836f56b63c4395c3292d19af2e3ea4 100644 (file)
@@ -23,8 +23,8 @@ import com.vaadin.client.communication.StateChangeEvent;
 import com.vaadin.client.connectors.AbstractListingConnector;
 import com.vaadin.client.data.DataSource;
 import com.vaadin.client.ui.VCheckBoxGroup;
-import com.vaadin.shared.Registration;
 import com.vaadin.shared.data.selection.SelectionModel;
+import com.vaadin.shared.data.selection.SelectionServerRpc;
 import com.vaadin.shared.ui.Connect;
 import com.vaadin.shared.ui.optiongroup.CheckBoxGroupState;
 import com.vaadin.ui.CheckBoxGroup;
@@ -32,20 +32,25 @@ import com.vaadin.ui.CheckBoxGroup;
 import elemental.json.JsonObject;
 
 @Connect(CheckBoxGroup.class)
+// We don't care about the framework-provided selection model at this point
 public class CheckBoxGroupConnector
-        extends AbstractListingConnector<SelectionModel.Multi<JsonObject>> {
-
-    private Registration selectionChangeRegistration;
+        extends AbstractListingConnector<SelectionModel<?>> {
 
     @Override
     protected void init() {
         super.init();
-        selectionChangeRegistration =
-                getWidget().addNotifyHandler(this::selectionChanged);
+        getWidget().addSelectionChangeHandler(this::selectionChanged);
     }
 
-    private void selectionChanged(JsonObject newSelection) {
-        getSelectionModel().select(newSelection);
+    private void selectionChanged(JsonObject changedItem, Boolean selected) {
+        SelectionServerRpc rpc = getRpcProxy(SelectionServerRpc.class);
+        String key = getRowKey(changedItem);
+
+        if (Boolean.TRUE.equals(selected)) {
+            rpc.select(key);
+        } else {
+            rpc.deselect(key);
+        }
     }
 
     @Override
diff --git a/server/src/main/java/com/vaadin/event/selection/MultiSelectionEvent.java b/server/src/main/java/com/vaadin/event/selection/MultiSelectionEvent.java
new file mode 100644 (file)
index 0000000..81c8478
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2000-2016 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.event.selection;
+
+import java.util.Collections;
+import java.util.Set;
+
+import com.vaadin.event.ConnectorEvent;
+import com.vaadin.shared.data.selection.SelectionModel;
+import com.vaadin.ui.AbstractListing;
+
+/**
+ * Event fired when the the selection changes in a
+ * {@link com.vaadin.shared.data.selection.SelectionModel.Multi}.
+ *
+ * @author Vaadin Ltd
+ *
+ * @since 8.0
+ *
+ * @param <T>
+ *            the data type of the selection model
+ */
+public class MultiSelectionEvent<T> extends ConnectorEvent {
+
+    private Set<T> oldSelection;
+    private Set<T> newSelection;
+
+    /**
+     * Creates a new event.
+     *
+     * @param source
+     *            the listing component in which the selection changed
+     * @param oldSelection
+     *            the old set of selected items
+     * @param newSelection
+     *            the new set of selected items
+     */
+    public MultiSelectionEvent(
+            AbstractListing<T, SelectionModel.Multi<T>> source,
+            Set<T> oldSelection, Set<T> newSelection) {
+        super(source);
+        this.oldSelection = oldSelection;
+        this.newSelection = newSelection;
+    }
+
+    /**
+     * Gets the new selection.
+     *
+     * @return a set of items selected after the selection was changed
+     */
+    public Set<T> getNewSelection() {
+        return Collections.unmodifiableSet(newSelection);
+    }
+
+    /**
+     * Gets the old selection.
+     *
+     * @return a set of items selected before the selection was changed
+     */
+    public Set<T> getOldSelection() {
+        return Collections.unmodifiableSet(oldSelection);
+    }
+
+}
diff --git a/server/src/main/java/com/vaadin/event/selection/MultiSelectionListener.java b/server/src/main/java/com/vaadin/event/selection/MultiSelectionListener.java
new file mode 100644 (file)
index 0000000..44c3175
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2000-2016 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.event.selection;
+
+import com.vaadin.event.EventListener;
+
+/**
+ * Listens to changes from a
+ * {@link com.vaadin.shared.data.selection.SelectionModel.Multi}.
+ *
+ * @author Vaadin Ltd
+ *
+ * @since 8.0
+ *
+ * @param <T>
+ *            the data type of the selection model
+ */
+public interface MultiSelectionListener<T>
+        extends EventListener<MultiSelectionEvent<T>> {
+    @Override
+    // Explicitly defined to make reflection logic happy
+    void accept(MultiSelectionEvent<T> event);
+}
index 2ad72c5e2c4393fd2a21ad3902919734fee27595..8687aa2b92b2b9832cbfc2aecba71bfa3f55da5a 100644 (file)
 
 package com.vaadin.ui;
 
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
 import com.vaadin.data.Listing;
+import com.vaadin.event.selection.MultiSelectionEvent;
+import com.vaadin.event.selection.MultiSelectionListener;
 import com.vaadin.server.Resource;
 import com.vaadin.server.ResourceReference;
 import com.vaadin.server.data.DataGenerator;
 import com.vaadin.server.data.DataSource;
+import com.vaadin.shared.Registration;
 import com.vaadin.shared.data.selection.SelectionModel;
+import com.vaadin.shared.data.selection.SelectionServerRpc;
 import com.vaadin.shared.ui.optiongroup.CheckBoxGroupConstants;
 import com.vaadin.shared.ui.optiongroup.CheckBoxGroupState;
-import elemental.json.JsonObject;
+import com.vaadin.util.ReflectTools;
 
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Objects;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.function.Predicate;
+import elemental.json.JsonObject;
 
 /**
- * A group of Checkboxes. Individual checkboxes are made from items given to
- * supplied by {@code Datasource}. Checkboxes my have captions and icons.
+ * A group of Checkboxes. Individual checkboxes are made from items supplied by
+ * a {@link DataSource}. Checkboxes may have captions and icons.
  *
  * @param <T>
- *         item type
+ *            item type
  * @author Vaadin Ltd
  * @since 8.0
  */
-public class CheckBoxGroup<T> extends AbstractListing<T, SelectionModel<T>> {
+public class CheckBoxGroup<T>
+        extends AbstractListing<T, SelectionModel.Multi<T>> {
+
+    private final class SimpleMultiSelectModel
+            implements SelectionModel.Multi<T> {
+
+        private Set<T> selection = new LinkedHashSet<>();
+
+        @Override
+        public void select(T item) {
+            if (selection.contains(item)) {
+                return;
+            }
+
+            updateSelection(set -> set.add(item));
+        }
+
+        @Override
+        public Set<T> getSelectedItems() {
+            return Collections.unmodifiableSet(selection);
+        }
+
+        @Override
+        public void deselect(T item) {
+            if (!selection.contains(item)) {
+                return;
+            }
+
+            updateSelection(set -> set.remove(item));
+        }
+
+        @Override
+        public void deselectAll() {
+            if (selection.isEmpty()) {
+                return;
+            }
+
+            updateSelection(Set::clear);
+        }
+
+        private void updateSelection(Consumer<Set<T>> handler) {
+            LinkedHashSet<T> oldSelection = new LinkedHashSet<>(selection);
+            handler.accept(selection);
+            LinkedHashSet<T> newSelection = new LinkedHashSet<>(selection);
+
+            fireEvent(new MultiSelectionEvent<>(CheckBoxGroup.this,
+                    oldSelection, newSelection));
+
+            getDataCommunicator().reset();
+        }
+
+        @Override
+        public boolean isSelected(T item) {
+            return selection.contains(item);
+        }
+    }
+
+    @Deprecated
+    private static final Method SELECTION_CHANGE_METHOD = ReflectTools
+            .findMethod(MultiSelectionListener.class, "accept",
+                    MultiSelectionEvent.class);
 
     private Function<T, Resource> itemIconProvider = item -> null;
 
@@ -54,7 +124,7 @@ public class CheckBoxGroup<T> extends AbstractListing<T, SelectionModel<T>> {
      * Constructs a new CheckBoxGroup with caption.
      *
      * @param caption
-     *         caption text
+     *            caption text
      * @see Listing#setDataSource(DataSource)
      */
     public CheckBoxGroup(String caption) {
@@ -66,9 +136,9 @@ public class CheckBoxGroup<T> extends AbstractListing<T, SelectionModel<T>> {
      * Constructs a new CheckBoxGroup with caption and DataSource.
      *
      * @param caption
-     *         the caption text
+     *            the caption text
      * @param dataSource
-     *         the data source, not null
+     *            the data source, not null
      * @see Listing#setDataSource(DataSource)
      */
     public CheckBoxGroup(String caption, DataSource<T> dataSource) {
@@ -81,9 +151,9 @@ public class CheckBoxGroup<T> extends AbstractListing<T, SelectionModel<T>> {
      * given items.
      *
      * @param caption
-     *         the caption text
+     *            the caption text
      * @param items
-     *         the data items to use, not null
+     *            the data items to use, not null
      * @see Listing#setDataSource(DataSource)
      */
     public CheckBoxGroup(String caption, Collection<T> items) {
@@ -96,51 +166,52 @@ public class CheckBoxGroup<T> extends AbstractListing<T, SelectionModel<T>> {
      * @see Listing#setDataSource(DataSource)
      */
     public CheckBoxGroup() {
-        //TODO Fix when MultiSelection is ready
-        //            SingleSelection<T> model = new SingleSelection<>(this);
-        //            setSelectionModel(model);
-        //            model.addSelectionListener(event -> beforeClientResponse(false));
-        setSelectionModel(new SelectionModel.Multi<T>() {
-            @Override
-            public void select(T item) {
+        setSelectionModel(new SimpleMultiSelectModel());
 
-            }
+        registerRpc(new SelectionServerRpc() {
 
             @Override
-            public Set<T> getSelectedItems() {
-                return Collections.emptySet();
+            public void select(String key) {
+                getItemForSelectionChange(key)
+                        .ifPresent(getSelectionModel()::select);
             }
 
             @Override
-            public void deselect(T item) {
-
+            public void deselect(String key) {
+                getItemForSelectionChange(key)
+                        .ifPresent(getSelectionModel()::deselect);
             }
 
-            @Override
-            public void deselectAll() {
-
-            }
+            private Optional<T> getItemForSelectionChange(String key) {
+                T item = getDataCommunicator().getKeyMapper().get(key);
+                if (item == null || !itemEnabledProvider.test(item)) {
+                    return Optional.empty();
+                }
 
-            @Override
-            public boolean isSelected(T item) {
-                return false;
+                return Optional.of(item);
             }
         });
+
         addDataGenerator(new DataGenerator<T>() {
             @Override
             public void generateData(T data, JsonObject jsonObject) {
                 jsonObject.put(CheckBoxGroupConstants.JSONKEY_ITEM_VALUE,
-                               itemCaptionProvider.apply(data));
+                        itemCaptionProvider.apply(data));
                 Resource icon = itemIconProvider.apply(data);
                 if (icon != null) {
                     String iconUrl = ResourceReference
                             .create(icon, CheckBoxGroup.this, null).getURL();
                     jsonObject.put(CheckBoxGroupConstants.JSONKEY_ITEM_ICON,
-                                   iconUrl);
+                            iconUrl);
                 }
                 if (!itemEnabledProvider.test(data)) {
                     jsonObject.put(CheckBoxGroupConstants.JSONKEY_ITEM_DISABLED,
-                                   true);
+                            true);
+                }
+
+                if (getSelectionModel().isSelected(data)) {
+                    jsonObject.put(CheckBoxGroupConstants.JSONKEY_ITEM_SELECTED,
+                            true);
                 }
             }
 
@@ -158,8 +229,8 @@ public class CheckBoxGroup<T> extends AbstractListing<T, SelectionModel<T>> {
      * content is passed to the browser as plain text.
      *
      * @param htmlContentAllowed
-     *         true if the captions are used as html, false if used as plain
-     *         text
+     *            true if the captions are used as html, false if used as plain
+     *            text
      */
     public void setHtmlContentAllowed(boolean htmlContentAllowed) {
         getState().htmlContentAllowed = htmlContentAllowed;
@@ -169,7 +240,7 @@ public class CheckBoxGroup<T> extends AbstractListing<T, SelectionModel<T>> {
      * Checks whether captions are interpreted as html or plain text.
      *
      * @return true if the captions are used as html, false if used as plain
-     * text
+     *         text
      * @see #setHtmlContentAllowed(boolean)
      */
     public boolean isHtmlContentAllowed() {
@@ -197,13 +268,13 @@ public class CheckBoxGroup<T> extends AbstractListing<T, SelectionModel<T>> {
     }
 
     /**
-     * Sets the item icon provider for this checkbox group. The icon provider
-     * is queried for each item to optionally display an icon next to
-     * the item caption. If the provider returns null for an item, no icon is
-     * displayed. The default provider always returns null (no icons).
+     * Sets the item icon provider for this checkbox group. The icon provider is
+     * queried for each item to optionally display an icon next to the item
+     * caption. If the provider returns null for an item, no icon is displayed.
+     * The default provider always returns null (no icons).
      *
      * @param itemIconProvider
-     *         icons provider, not null
+     *            icons provider, not null
      */
     public void setItemIconProvider(Function<T, Resource> itemIconProvider) {
         Objects.nonNull(itemIconProvider);
@@ -222,12 +293,12 @@ public class CheckBoxGroup<T> extends AbstractListing<T, SelectionModel<T>> {
 
     /**
      * Sets the item caption provider for this checkbox group. The caption
-     * provider is queried for each item to optionally display an item
-     * textual representation. The default provider returns
+     * provider is queried for each item to optionally display an item textual
+     * representation. The default provider returns
      * {@code String.valueOf(item)}.
      *
      * @param itemCaptionProvider
-     *         the item caption provider, not null
+     *            the item caption provider, not null
      */
     public void setItemCaptionProvider(
             Function<T, String> itemCaptionProvider) {
@@ -246,17 +317,33 @@ public class CheckBoxGroup<T> extends AbstractListing<T, SelectionModel<T>> {
     }
 
     /**
-     * Sets the item enabled predicate for this checkbox group. The predicate
-     * is applied to each item to determine whether the item should be
-     * enabled  (true) or disabled (false). Disabled items are displayed as
-     * grayed out and the user cannot select them. The default predicate
-     * always returns true (all the items are enabled).
+     * Sets the item enabled predicate for this checkbox group. The predicate is
+     * applied to each item to determine whether the item should be enabled
+     * (true) or disabled (false). Disabled items are displayed as grayed out
+     * and the user cannot select them. The default predicate always returns
+     * true (all the items are enabled).
      *
      * @param itemEnabledProvider
-     *         the item enable predicate, not null
+     *            the item enable predicate, not null
      */
     public void setItemEnabledProvider(Predicate<T> itemEnabledProvider) {
         Objects.nonNull(itemEnabledProvider);
         this.itemEnabledProvider = itemEnabledProvider;
     }
+
+    /**
+     * Adds a selection listener that will be called when the selection is
+     * changed either by the user or programmatically.
+     *
+     * @param listener
+     *            the value change listener, not <code>null</code>
+     * @return a registration for the listener
+     */
+    public Registration addSelectionListener(
+            MultiSelectionListener<T> listener) {
+        Objects.requireNonNull(listener, "listener cannot be null");
+        addListener(MultiSelectionEvent.class, listener,
+                SELECTION_CHANGE_METHOD);
+        return () -> removeListener(MultiSelectionEvent.class, listener);
+    }
 }
diff --git a/server/src/test/java/com/vaadin/ui/CheckBoxGroupTest.java b/server/src/test/java/com/vaadin/ui/CheckBoxGroupTest.java
new file mode 100644 (file)
index 0000000..0ad1801
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2000-2016 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.ui;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.server.data.DataSource;
+import com.vaadin.shared.data.selection.SelectionModel.Multi;
+
+public class CheckBoxGroupTest {
+    @Test
+    public void stableSelectionOrder() {
+        CheckBoxGroup<String> checkBoxGroup = new CheckBoxGroup<>();
+        // Intentional deviation from upcoming selection order
+        checkBoxGroup
+                .setDataSource(DataSource.create("Third", "Second", "First"));
+        Multi<String> selectionModel = checkBoxGroup.getSelectionModel();
+
+        selectionModel.select("First");
+        selectionModel.select("Second");
+        selectionModel.select("Third");
+
+        assertSelectionOrder(selectionModel, "First", "Second", "Third");
+
+        selectionModel.deselect("First");
+        assertSelectionOrder(selectionModel, "Second", "Third");
+
+        selectionModel.select("First");
+        assertSelectionOrder(selectionModel, "Second", "Third", "First");
+    }
+
+    private static void assertSelectionOrder(Multi<String> selectionModel,
+            String... selectionOrder) {
+        Assert.assertEquals(Arrays.asList(selectionOrder),
+                new ArrayList<>(selectionModel.getSelectedItems()));
+    }
+}
index 2747dd4c9928d5888aecf9a8be40101f0a4f61b0..6bca43852a24a0850c8ce7ba0b57f6a57c9c58a8 100644 (file)
@@ -25,4 +25,6 @@ public class CheckBoxGroupConstants implements Serializable {
     public static final String JSONKEY_ITEM_VALUE = "v";
 
     public static final String JSONKEY_ITEM_KEY = "k";
+
+    public static final String JSONKEY_ITEM_SELECTED = "s";
 }
index b070ece852956c93a77edacb1971573c9e0c0edc..b2dd93a7b00f9203dfe3e9d963088956dadd1f6f 100644 (file)
@@ -1,12 +1,12 @@
 /*
  * Copyright 2000-2014 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
@@ -32,13 +32,13 @@ import com.vaadin.testbench.elementsbase.ServerClass;
 
 @ServerClass("com.vaadin.ui.CheckBoxGroup")
 public class CheckBoxGroupElement extends AbstractSelectElement {
-    private static org.openqa.selenium.By byButtonSpan =
-            By.className("v-select-option");
+    private static org.openqa.selenium.By byButtonSpan = By
+            .className("v-select-option");
     private static org.openqa.selenium.By byLabel = By.tagName("label");
     private static org.openqa.selenium.By byInput = By.tagName("input");
 
     public List<String> getOptions() {
-        List<String> optionTexts = new ArrayList<String>();
+        List<String> optionTexts = new ArrayList<>();
         List<WebElement> options = findElements(byButtonSpan);
         for (WebElement option : options) {
             optionTexts.add(option.findElement(byLabel).getText());
@@ -51,9 +51,17 @@ public class CheckBoxGroupElement extends AbstractSelectElement {
             throw new ReadOnlyException();
         }
         List<WebElement> options = findElements(byButtonSpan);
-        for (WebElement option : options) {
+        for (int i = 0; i < options.size(); i++) {
+            WebElement option = options.get(i);
             if (text.equals(option.findElement(byLabel).getText())) {
                 option.findElement(byInput).click();
+
+                // Seems like this is needed because of #19753
+                waitForVaadin();
+
+                // Toggling selection causes the DOM to be rebuilt, so fetch new
+                // items and continue iterating from the same index
+                options = findElements(byButtonSpan);
             }
         }
     }
@@ -70,8 +78,8 @@ public class CheckBoxGroupElement extends AbstractSelectElement {
             WebElement checkedItem;
             checkedItem = option.findElement(By.tagName("input"));
             String checked = checkedItem.getAttribute("checked");
-            if (checked != null &&
-                    checkedItem.getAttribute("checked").equals("true")) {
+            if (checked != null
+                    && checkedItem.getAttribute("checked").equals("true")) {
                 values.add(option.findElement(By.tagName("label")).getText());
             }
         }
@@ -82,7 +90,8 @@ public class CheckBoxGroupElement extends AbstractSelectElement {
      * Select option in the checkbox group with the specified value
      *
      * @param chars
-     *         value of the option in the checkbox group which will be selected
+     *            value of the option in the checkbox group which will be
+     *            selected
      */
     public void selectOption(CharSequence chars) throws ReadOnlyException {
         selectByText((String) chars);
@@ -91,8 +100,7 @@ public class CheckBoxGroupElement extends AbstractSelectElement {
     @Override
     public void clear() {
         throw new UnsupportedOperationException(
-                "Clear operation is not supported for CheckBoxGroup." +
-                        " This operation has no effect on the element.");
+                "Clear operation is not supported for CheckBoxGroup."
+                        " This operation has no effect on the element.");
     }
 }
-
index 343faf393068d760371ffaa7b579165767177cf9..fef23f56d3de0cd392a7c65bf7e767071502ba32 100644 (file)
@@ -1,12 +1,12 @@
 /*
  * Copyright 2000-2014 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
@@ -15,6 +15,9 @@
  */
 package com.vaadin.tests.components.checkbox;
 
+import java.util.stream.IntStream;
+
+import com.vaadin.shared.data.selection.SelectionModel.Multi;
 import com.vaadin.tests.components.abstractlisting.AbstractListingTestUI;
 import com.vaadin.ui.CheckBoxGroup;
 
@@ -25,8 +28,50 @@ import com.vaadin.ui.CheckBoxGroup;
  */
 public class CheckBoxGroupTestUI
         extends AbstractListingTestUI<CheckBoxGroup<Object>> {
+
+    private final String selectionCategory = "Selection";
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
     @Override
     protected Class<CheckBoxGroup<Object>> getTestClass() {
         return (Class) CheckBoxGroup.class;
     }
+
+    @Override
+    protected void createActions() {
+        super.createActions();
+        createListenerMenu();
+        createSelectionMenu();
+    }
+
+    protected void createSelectionMenu() {
+        createClickAction(
+                "Clear selection", selectionCategory, (component, item,
+                        data) -> component.getSelectionModel().deselectAll(),
+                "");
+
+        Command<CheckBoxGroup<Object>, String> toggleSelection = (component,
+                item, data) -> toggleSelection(item);
+
+        IntStream.of(0, 1, 5, 10, 25).mapToObj(i -> "Item " + i)
+                .forEach(item -> {
+                    createClickAction("Toggle " + item, selectionCategory,
+                            toggleSelection, item);
+                });
+    }
+
+    private void toggleSelection(String item) {
+        Multi<Object> selectionModel = getComponent().getSelectionModel();
+        if (selectionModel.isSelected(item)) {
+            selectionModel.deselect(item);
+        } else {
+            selectionModel.select(item);
+        }
+    }
+
+    protected void createListenerMenu() {
+        createListenerAction("Selection listener", "Listeners",
+                c -> c.addSelectionListener(
+                        e -> log("Selected: " + e.getNewSelection())));
+    }
 }
index 9a3c2a656c521c4557727d7aae693cf732da04c8..9a8f6315943a67fa9d6c5a6cae1aeef4d11f3539 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * Copyright 2000-2014 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
  */
 package com.vaadin.tests.components.checkboxgroup;
 
-import com.vaadin.testbench.customelements.CheckBoxGroupElement;
-import com.vaadin.tests.components.checkbox.CheckBoxGroupTestUI;
-import com.vaadin.tests.tb3.MultiBrowserTest;
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
-import static org.junit.Assert.assertEquals;
+import com.vaadin.testbench.customelements.CheckBoxGroupElement;
+import com.vaadin.tests.components.checkbox.CheckBoxGroupTestUI;
+import com.vaadin.tests.tb3.MultiBrowserTest;
 
 /**
  * Test for CheckBoxGroup
@@ -53,6 +57,45 @@ public class CheckBoxGroupTest extends MultiBrowserTest {
         assertItems(100);
     }
 
+    @Test
+    public void clickToSelect() {
+        selectMenuPath("Component", "Listeners", "Selection listener");
+
+        getSelect().selectByText("Item 4");
+        Assert.assertEquals("1. Selected: [Item 4]", getLogRow(0));
+
+        getSelect().selectByText("Item 2");
+        // Selection order (most recently selected is last)
+        Assert.assertEquals("2. Selected: [Item 4, Item 2]", getLogRow(0));
+
+        getSelect().selectByText("Item 4");
+        Assert.assertEquals("3. Selected: [Item 2]", getLogRow(0));
+    }
+
+    @Test
+    public void selectProgramatically() {
+        selectMenuPath("Component", "Listeners", "Selection listener");
+
+        selectMenuPath("Component", "Selection", "Toggle Item 5");
+        Assert.assertEquals("2. Selected: [Item 5]", getLogRow(0));
+        assertSelected("Item 5");
+
+        selectMenuPath("Component", "Selection", "Toggle Item 1");
+        // Selection order (most recently selected is last)
+        Assert.assertEquals("4. Selected: [Item 5, Item 1]", getLogRow(0));
+        // DOM order
+        assertSelected("Item 1", "Item 5");
+
+        selectMenuPath("Component", "Selection", "Toggle Item 5");
+        Assert.assertEquals("6. Selected: [Item 1]", getLogRow(0));
+        assertSelected("Item 1");
+    }
+
+    private void assertSelected(String... expectedSelection) {
+        Assert.assertEquals(Arrays.asList(expectedSelection),
+                getSelect().getSelection());
+    }
+
     @Override
     protected Class<?> getUIClass() {
         return CheckBoxGroupTestUI.class;