diff options
10 files changed, 1088 insertions, 0 deletions
diff --git a/client/src/main/java/com/vaadin/client/ui/VRadioButtonGroup.java b/client/src/main/java/com/vaadin/client/ui/VRadioButtonGroup.java new file mode 100644 index 0000000000..893935d562 --- /dev/null +++ b/client/src/main/java/com/vaadin/client/ui/VRadioButtonGroup.java @@ -0,0 +1,235 @@ +/* + * 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.client.ui; + +import com.google.gwt.aria.client.Roles; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.FocusWidget; +import com.google.gwt.user.client.ui.Focusable; +import com.google.gwt.user.client.ui.HasEnabled; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.RadioButton; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.BrowserInfo; +import com.vaadin.client.WidgetUtil; +import com.vaadin.shared.Registration; +import com.vaadin.shared.data.DataCommunicatorConstants; +import com.vaadin.shared.ui.optiongroup.RadioButtonGroupConstants; +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.RadioButtonGroupConstants.JSONKEY_ITEM_DISABLED; + +/** + * The client-side widget for the {@code RadioButtonGroup} component. + * + * @author Vaadin Ltd. + * @since 8.0 + */ +public class VRadioButtonGroup extends Composite implements Field, ClickHandler, + com.vaadin.client.Focusable, HasEnabled { + + public static final String CLASSNAME = "v-select-optiongroup"; + public static final String CLASSNAME_OPTION = "v-select-option"; + + private final Map<RadioButton, JsonObject> optionsToItems; + private final Map<String, RadioButton> keyToOptions; + + /** + * For internal use only. May be removed or replaced in the future. + */ + public ApplicationConnection client; + + /** + * Widget holding the different options (e.g. ListBox or Panel for radio + * buttons) (optional, fallbacks to container Panel) + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public Panel optionsContainer; + + private boolean htmlContentAllowed = false; + + private boolean enabled; + private boolean readonly; + private final String groupId; + private List<Consumer<JsonObject>> selectionChangeListeners; + + public VRadioButtonGroup() { + groupId = DOM.createUniqueId(); + optionsContainer = new FlowPanel(); + initWidget(this.optionsContainer); + optionsContainer.setStyleName(CLASSNAME); + optionsToItems = new HashMap<>(); + keyToOptions = new HashMap<>(); + selectionChangeListeners = new ArrayList<>(); + } + + /* + * Build all the options + */ + public void buildOptions(List<JsonObject> items) { + /* + * In order to retain focus, we need to update values rather than + * recreate panel from scratch (#10451). However, the panel will be + * rebuilt (losing focus) if number of elements or their order is + * changed. + */ + + Roles.getRadiogroupRole().set(getElement()); + optionsContainer.clear(); + optionsToItems.clear(); + keyToOptions.clear(); + for (JsonObject item : items) { + String itemHtml = item + .getString(RadioButtonGroupConstants.JSONKEY_ITEM_VALUE); + if (!isHtmlContentAllowed()) { + itemHtml = WidgetUtil.escapeHTML(itemHtml); + } + RadioButton radioButton = new RadioButton(groupId); + + String iconUrl = item + .getString(RadioButtonGroupConstants.JSONKEY_ITEM_ICON); + if (iconUrl != null && iconUrl.length() != 0) { + Icon icon = client.getIcon(iconUrl); + itemHtml = icon.getElement().getString() + itemHtml; + } + radioButton.setStyleName("v-radiobutton"); + radioButton.addStyleName(CLASSNAME_OPTION); + radioButton.addClickHandler(this); + radioButton.setHTML(itemHtml); + radioButton.setValue(item + .getBoolean(RadioButtonGroupConstants.JSONKEY_ITEM_SELECTED)); + boolean optionEnabled = !item.getBoolean(JSONKEY_ITEM_DISABLED); + boolean enabled = optionEnabled && !isReadonly() && isEnabled(); + radioButton.setEnabled(enabled); + String key = item.getString(DataCommunicatorConstants.KEY); + + optionsContainer.add(radioButton); + optionsToItems.put(radioButton, item); + keyToOptions.put(key, radioButton); + } + } + + @Override + public void onClick(ClickEvent event) { + if (event.getSource() instanceof RadioButton) { + RadioButton source = (RadioButton) event.getSource(); + if (!source.isEnabled()) { + // Click events on the text are received even though the + // radiobutton is disabled + return; + } + if (BrowserInfo.get().isWebkit()) { + // Webkit does not focus non-text input elements on click + // (#11854) + source.setFocus(true); + } + + JsonObject item = optionsToItems.get(source); + assert item != null; + + new ArrayList<>(selectionChangeListeners) + .forEach(listener -> listener.accept(item)); + } + } + + public void setTabIndex(int tabIndex) { + for (Widget anOptionsContainer : optionsContainer) { + FocusWidget widget = (FocusWidget) anOptionsContainer; + widget.setTabIndex(tabIndex); + } + } + + protected void updateEnabledState() { + boolean radioButtonEnabled = isEnabled() && !isReadonly(); + // sets options enabled according to the widget's enabled, + // readonly and each options own enabled + for (Map.Entry<RadioButton, JsonObject> entry : optionsToItems + .entrySet()) { + RadioButton radioButton = entry.getKey(); + JsonObject value = entry.getValue(); + Boolean isOptionEnabled = !value + .getBoolean(RadioButtonGroupConstants.JSONKEY_ITEM_DISABLED); + radioButton.setEnabled(radioButtonEnabled && isOptionEnabled); + } + } + + @Override + public void focus() { + Iterator<Widget> iterator = optionsContainer.iterator(); + if (iterator.hasNext()) { + ((Focusable) iterator.next()).setFocus(true); + } + } + + public boolean isHtmlContentAllowed() { + return htmlContentAllowed; + } + + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + this.htmlContentAllowed = htmlContentAllowed; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + public boolean isReadonly() { + return readonly; + } + + public void setReadonly(boolean readonly) { + if (this.readonly != readonly) { + this.readonly = readonly; + updateEnabledState(); + } + } + + @Override + public void setEnabled(boolean enabled) { + if (this.enabled != enabled) { + this.enabled = enabled; + updateEnabledState(); + } + } + + public Registration addSelectionChangeHandler( + Consumer<JsonObject> selectionChanged) { + selectionChangeListeners.add(selectionChanged); + return (Registration) () -> selectionChangeListeners + .remove(selectionChanged); + } + + public void selectItemKey(String selectedItemKey) { + RadioButton radioButton = keyToOptions.get(selectedItemKey); + assert radioButton!=null; + radioButton.setValue(true); + } +} diff --git a/client/src/main/java/com/vaadin/client/ui/optiongroup/RadioButtonGroupConnector.java b/client/src/main/java/com/vaadin/client/ui/optiongroup/RadioButtonGroupConnector.java new file mode 100644 index 0000000000..135126a099 --- /dev/null +++ b/client/src/main/java/com/vaadin/client/ui/optiongroup/RadioButtonGroupConnector.java @@ -0,0 +1,122 @@ +/* + * 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.client.ui.optiongroup; + +import com.vaadin.client.annotations.OnStateChange; +import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.connectors.AbstractListingConnector; +import com.vaadin.client.data.DataSource; +import com.vaadin.client.ui.VRadioButtonGroup; +import com.vaadin.shared.Range; +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.RadioButtonGroupState; +import com.vaadin.ui.RadioButtonGroup; +import elemental.json.JsonObject; + +import java.util.ArrayList; +import java.util.List; + +@Connect(RadioButtonGroup.class) +public class RadioButtonGroupConnector + extends AbstractListingConnector<SelectionModel.Single<?>> { + + private Registration selectionChangeRegistration; + private Registration dataChangeRegistration; + + private final SelectionServerRpc selectionRpc = getRpcProxy( + SelectionServerRpc.class); + + @Override + protected void init() { + super.init(); + + selectionChangeRegistration = getWidget().addSelectionChangeHandler( + e -> selectionRpc.select(getRowKey(e))); + } + + @Override + public void onUnregister() { + super.onUnregister(); + selectionChangeRegistration.remove(); + selectionChangeRegistration = null; + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + getWidget().client = getConnection(); + } + + @Override + public void setDataSource(DataSource<JsonObject> dataSource) { + if (dataChangeRegistration != null) { + dataChangeRegistration.remove(); + } + dataChangeRegistration = dataSource + .addDataChangeHandler(this::onDataChange); + super.setDataSource(dataSource); + } + + @OnStateChange("readOnly") + @SuppressWarnings("deprecation") + void updateWidgetReadOnly() { + getWidget().setEnabled(isEnabled() && !isReadOnly()); + } + + @OnStateChange("selectedItemKey") + void updateSelectedItem() { + getWidget().selectItemKey(getState().selectedItemKey); + } + + @Override + public VRadioButtonGroup getWidget() { + return (VRadioButtonGroup) super.getWidget(); + } + + @Override + public RadioButtonGroupState getState() { + return (RadioButtonGroupState) super.getState(); + } + + /** + * A data change handler registered to the data source. Updates the data + * items and selection status when the data source notifies of new changes + * from the server side. + * + * @param range + * the new range of data items + */ + private void onDataChange(Range range) { + assert range.getStart() == 0 && range.getEnd() == getDataSource() + .size() : "RadioButtonGroup only supports full updates, but " + + "got range " + + range; + + final VRadioButtonGroup select = getWidget(); + DataSource<JsonObject> dataSource = getDataSource(); + int size = dataSource.size(); + List<JsonObject> options = new ArrayList<>(); + for (int i = 0; i < size; i++) { + options.add(dataSource.getRow(i)); + } + select.buildOptions(options); + updateSelectedItem(); + } +} diff --git a/server/src/main/java/com/vaadin/ui/RadioButtonGroup.java b/server/src/main/java/com/vaadin/ui/RadioButtonGroup.java new file mode 100644 index 0000000000..e3cc1892b0 --- /dev/null +++ b/server/src/main/java/com/vaadin/ui/RadioButtonGroup.java @@ -0,0 +1,236 @@ +/* + * 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 com.vaadin.data.Listing; +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.ui.optiongroup.RadioButtonGroupConstants; +import com.vaadin.shared.ui.optiongroup.RadioButtonGroupState; +import elemental.json.JsonObject; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * A group of RadioButtons. Individual radiobuttons are made from items supplied by + * a {@link DataSource}. RadioButtons may have captions and icons. + * + * @param <T> + * item type + * @author Vaadin Ltd + * @since 8.0 + */ +public class RadioButtonGroup<T> extends AbstractSingleSelect<T> { + + private Function<T, Resource> itemIconProvider = item -> null; + + private Function<T, String> itemCaptionProvider = String::valueOf; + + private Predicate<T> itemEnabledProvider = item -> true; + + /** + * Constructs a new RadioButtonGroup with caption. + * + * @param caption + * caption text + * @see Listing#setDataSource(DataSource) + */ + public RadioButtonGroup(String caption) { + this(); + setCaption(caption); + } + + /** + * Constructs a new RadioButtonGroup with caption and DataSource. + * + * @param caption + * the caption text + * @param dataSource + * the data source, not null + * @see Listing#setDataSource(DataSource) + */ + public RadioButtonGroup(String caption, DataSource<T> dataSource) { + this(caption); + setDataSource(dataSource); + } + + /** + * Constructs a new RadioButtonGroup with caption and DataSource containing + * given items. + * + * @param caption + * the caption text + * @param items + * the data items to use, not null + * @see Listing#setDataSource(DataSource) + */ + public RadioButtonGroup(String caption, Collection<T> items) { + this(caption, DataSource.create(items)); + } + + /** + * Constructs a new RadioButtonGroup. + * + * @see Listing#setDataSource(DataSource) + */ + public RadioButtonGroup() { + setSelectionModel(new SimpleSingleSelection()); + + addDataGenerator(new DataGenerator<T>() { + @Override + public void generateData(T data, JsonObject jsonObject) { + jsonObject.put(RadioButtonGroupConstants.JSONKEY_ITEM_VALUE, + itemCaptionProvider.apply(data)); + Resource icon = itemIconProvider.apply(data); + if (icon != null) { + String iconUrl = ResourceReference + .create(icon, RadioButtonGroup.this, null).getURL(); + jsonObject.put(RadioButtonGroupConstants.JSONKEY_ITEM_ICON, + iconUrl); + } + if (!itemEnabledProvider.test(data)) { + jsonObject.put(RadioButtonGroupConstants.JSONKEY_ITEM_DISABLED, + true); + } + + if (getSelectionModel().isSelected(data)) { + jsonObject.put(RadioButtonGroupConstants.JSONKEY_ITEM_SELECTED, + true); + } + } + + @Override + public void destroyData(T data) { + } + }); + + } + + /** + * Sets whether html is allowed in the item captions. If set to true, the + * captions are passed to the browser as html and the developer is + * responsible for ensuring no harmful html is used. If set to false, the + * 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 + */ + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + getState().htmlContentAllowed = htmlContentAllowed; + } + + /** + * 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 + * @see #setHtmlContentAllowed(boolean) + */ + public boolean isHtmlContentAllowed() { + return getState(false).htmlContentAllowed; + } + + @Override + protected RadioButtonGroupState getState() { + return (RadioButtonGroupState) super.getState(); + } + + @Override + protected RadioButtonGroupState getState(boolean markAsDirty) { + return (RadioButtonGroupState) super.getState(markAsDirty); + } + + /** + * Returns the item icons provider. + * + * @return the icons provider for items + * @see #setItemIconProvider + */ + public Function<T, Resource> getItemIconProvider() { + return itemIconProvider; + } + + /** + * Sets the item icon provider for this radiobutton 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 + */ + public void setItemIconProvider(Function<T, Resource> itemIconProvider) { + Objects.requireNonNull(itemIconProvider); + this.itemIconProvider = itemIconProvider; + } + + /** + * Returns the item caption provider. + * + * @return the captions provider + * @see #setItemCaptionProvider + */ + public Function<T, String> getItemCaptionProvider() { + return itemCaptionProvider; + } + + /** + * Sets the item caption provider for this radiobutton group. The caption + * 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 + */ + public void setItemCaptionProvider( + Function<T, String> itemCaptionProvider) { + Objects.requireNonNull(itemCaptionProvider); + this.itemCaptionProvider = itemCaptionProvider; + } + + /** + * Returns the item enabled predicate. + * + * @return the item enabled predicate + * @see #setItemEnabledProvider + */ + public Predicate<T> getItemEnabledProvider() { + return itemEnabledProvider; + } + + /** + * Sets the item enabled predicate for this radiobutton 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 + */ + public void setItemEnabledProvider(Predicate<T> itemEnabledProvider) { + Objects.requireNonNull(itemEnabledProvider); + this.itemEnabledProvider = itemEnabledProvider; + } +} diff --git a/server/src/test/java/com/vaadin/ui/RadioButtonGroupBoVTest.java b/server/src/test/java/com/vaadin/ui/RadioButtonGroupBoVTest.java new file mode 100644 index 0000000000..2ec2404515 --- /dev/null +++ b/server/src/test/java/com/vaadin/ui/RadioButtonGroupBoVTest.java @@ -0,0 +1,46 @@ +/* + * 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 + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui; + +import java.util.EnumSet; + +/** + * Option group test from Book of Vaadin + * + * @author Vaadin Ltd + * @since 8.0 + */ +public class RadioButtonGroupBoVTest +{ + public enum Status { + STATE_A, + STATE_B, + STATE_C, + STATE_D; + + public String getCaption() { + return "** " + toString(); + } + } + + + public void createOptionGroup() { + RadioButtonGroup<Status> s = new RadioButtonGroup<>(); + s.setItems(EnumSet.allOf(Status.class)); + s.setItemCaptionProvider(Status::getCaption); + } + +} diff --git a/server/src/test/java/com/vaadin/ui/RadioButtonGroupTest.java b/server/src/test/java/com/vaadin/ui/RadioButtonGroupTest.java new file mode 100644 index 0000000000..a03da685d8 --- /dev/null +++ b/server/src/test/java/com/vaadin/ui/RadioButtonGroupTest.java @@ -0,0 +1,91 @@ +/* + * 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 com.vaadin.server.data.DataSource; +import com.vaadin.shared.data.selection.SelectionModel; +import com.vaadin.shared.data.selection.SelectionModel.Multi; +import com.vaadin.shared.data.selection.SelectionServerRpc; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +public class RadioButtonGroupTest { + private RadioButtonGroup<String> radioButtonGroup; + private SelectionModel.Single<String> selectionModel; + + @Before + public void setUp() { + radioButtonGroup = new RadioButtonGroup<>(); + // Intentional deviation from upcoming selection order + radioButtonGroup + .setDataSource(DataSource.create("Third", "Second", "First")); + selectionModel = radioButtonGroup.getSelectionModel(); + } + + + @Test + public void apiSelectionChange_notUserOriginated() { + AtomicInteger listenerCount = new AtomicInteger(0); + + radioButtonGroup.addSelectionListener(event -> { + listenerCount.incrementAndGet(); + Assert.assertFalse(event.isUserOriginated()); + }); + + radioButtonGroup.select("First"); + radioButtonGroup.select("Second"); + + radioButtonGroup.deselect("Second"); + radioButtonGroup.getSelectionModel().deselectAll(); + + Assert.assertEquals(3, listenerCount.get()); + } + + @Test + public void rpcSelectionChange_userOriginated() { + AtomicInteger listenerCount = new AtomicInteger(0); + + radioButtonGroup.addSelectionListener(event -> { + listenerCount.incrementAndGet(); + Assert.assertTrue(event.isUserOriginated()); + }); + + SelectionServerRpc rpc = ComponentTest.getRpcProxy(radioButtonGroup, + SelectionServerRpc.class); + + rpc.select(getItemKey("First")); + rpc.select(getItemKey("Second")); + rpc.deselect(getItemKey("Second")); + + Assert.assertEquals(3, listenerCount.get()); + } + + private String getItemKey(String dataObject) { + return radioButtonGroup.getDataCommunicator().getKeyMapper() + .key(dataObject); + } + + private static void assertSelectionOrder(Multi<String> selectionModel, + String... selectionOrder) { + Assert.assertEquals(Arrays.asList(selectionOrder), + new ArrayList<>(selectionModel.getSelectedItems())); + } +} diff --git a/shared/src/main/java/com/vaadin/shared/ui/optiongroup/RadioButtonGroupConstants.java b/shared/src/main/java/com/vaadin/shared/ui/optiongroup/RadioButtonGroupConstants.java new file mode 100644 index 0000000000..5278d211de --- /dev/null +++ b/shared/src/main/java/com/vaadin/shared/ui/optiongroup/RadioButtonGroupConstants.java @@ -0,0 +1,30 @@ +/* + * 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.shared.ui.optiongroup; + +import java.io.Serializable; + +public class RadioButtonGroupConstants implements Serializable { + public static final String JSONKEY_ITEM_DISABLED = "d"; + + public static final String JSONKEY_ITEM_ICON = "i"; + + public static final String JSONKEY_ITEM_VALUE = "v"; + + public static final String JSONKEY_ITEM_KEY = "k"; + + public static final String JSONKEY_ITEM_SELECTED = "s"; +} diff --git a/shared/src/main/java/com/vaadin/shared/ui/optiongroup/RadioButtonGroupState.java b/shared/src/main/java/com/vaadin/shared/ui/optiongroup/RadioButtonGroupState.java new file mode 100644 index 0000000000..bdd007613c --- /dev/null +++ b/shared/src/main/java/com/vaadin/shared/ui/optiongroup/RadioButtonGroupState.java @@ -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.shared.ui.optiongroup; + +import com.vaadin.shared.AbstractFieldState; +import com.vaadin.shared.annotations.DelegateToWidget; +import com.vaadin.shared.ui.AbstractSingleSelectState; + +/** + * Shared state for the RadioButtonGroup component. + * + * @author Vaadin Ltd. + * @since 8.0 + */ +public class RadioButtonGroupState extends AbstractSingleSelectState { + + { + primaryStyleName = "v-select-optiongroup"; + } + + @DelegateToWidget + public boolean htmlContentAllowed = false; +} diff --git a/uitest-common/src/main/java/com/vaadin/testbench/customelements/RadioButtonGroupElement.java b/uitest-common/src/main/java/com/vaadin/testbench/customelements/RadioButtonGroupElement.java new file mode 100644 index 0000000000..f6bb4d93f7 --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/testbench/customelements/RadioButtonGroupElement.java @@ -0,0 +1,105 @@ +/* + * 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 + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.testbench.customelements; + +import com.vaadin.testbench.By; +import com.vaadin.testbench.elements.AbstractSelectElement; +import com.vaadin.testbench.elementsbase.ServerClass; +import org.openqa.selenium.WebElement; + +import java.util.ArrayList; +import java.util.List; + +/** + * TestBench element supporting RadioButtonGroup + * + * @author Vaadin Ltd + */ + +@ServerClass("com.vaadin.ui.RadioButtonGroup") +public class RadioButtonGroupElement extends AbstractSelectElement { + 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<>(); + List<WebElement> options = findElements(byButtonSpan); + for (WebElement option : options) { + optionTexts.add(option.findElement(byLabel).getText()); + } + return optionTexts; + } + + public void selectByText(String text) throws ReadOnlyException { + if (isReadOnly()) { + throw new ReadOnlyException(); + } + List<WebElement> options = findElements(byButtonSpan); + 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); + } + } + } + + /** + * Return list of the selected options in the radiobutton group + * + * @return list of the selected options in the radiobutton group + */ + public List<String> getSelection() { + List<String> values = new ArrayList<>(); + List<WebElement> options = findElements(byButtonSpan); + for (WebElement option : options) { + WebElement checkedItem; + checkedItem = option.findElement(By.tagName("input")); + String checked = checkedItem.getAttribute("checked"); + if (checked != null + && checkedItem.getAttribute("checked").equals("true")) { + values.add(option.findElement(By.tagName("label")).getText()); + } + } + return values; + } + + /** + * Select option in the radiobutton group with the specified value + * + * @param chars + * value of the option in the radiobutton group which will be + * selected + */ + public void selectOption(CharSequence chars) throws ReadOnlyException { + selectByText((String) chars); + } + + @Override + public void clear() { + throw new UnsupportedOperationException( + "Clear operation is not supported for RadioButtonGroup." + + " This operation has no effect on the element."); + } +} diff --git a/uitest/src/main/java/com/vaadin/tests/components/radiobutton/RadioButtonGroupTestUI.java b/uitest/src/main/java/com/vaadin/tests/components/radiobutton/RadioButtonGroupTestUI.java new file mode 100644 index 0000000000..1b4e9ac0df --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/radiobutton/RadioButtonGroupTestUI.java @@ -0,0 +1,75 @@ +/* + * 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 + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.radiobutton; + +import com.vaadin.shared.data.selection.SelectionModel; +import com.vaadin.tests.components.abstractlisting.AbstractListingTestUI; +import com.vaadin.ui.RadioButtonGroup; + +import java.util.stream.IntStream; + +/** + * Test UI for RadioButtonGroup component + * + * @author Vaadin Ltd + */ +public class RadioButtonGroupTestUI + extends AbstractListingTestUI<RadioButtonGroup<Object>> { + + private final String selectionCategory = "Selection"; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + protected Class<RadioButtonGroup<Object>> getTestClass() { + return (Class) RadioButtonGroup.class; + } + + @Override + protected void createActions() { + super.createActions(); + createListenerMenu(); + createSelectionMenu(); + } + + protected void createSelectionMenu() { + createClickAction( + "Clear selection", selectionCategory, (component, item, + data) -> component.getSelectionModel().deselectAll(), + ""); + + Command<RadioButtonGroup<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) { + SelectionModel.Single<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.getSelectedItem()))); + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/radiobutton/RadioButtonGroupTest.java b/uitest/src/test/java/com/vaadin/tests/components/radiobutton/RadioButtonGroupTest.java new file mode 100644 index 0000000000..a591924641 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/radiobutton/RadioButtonGroupTest.java @@ -0,0 +1,112 @@ +/* + * 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 + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.radiobutton; + +import com.vaadin.testbench.customelements.RadioButtonGroupElement; +import com.vaadin.tests.tb3.MultiBrowserTest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; + +/** + * Test for RadioButtonGroup + * + * @author Vaadin Ltd + * @since 8.0 + */ +public class RadioButtonGroupTest extends MultiBrowserTest { + + @Before + public void setUp() throws Exception { + openTestURL(); + } + + @Test + public void initialLoad_containsCorrectItems() { + assertItems(20); + } + + @Test + public void initialItems_reduceItemCount_containsCorrectItems() { + selectMenuPath("Component", "Data source", "Items", "5"); + assertItems(5); + } + + @Test + public void initialItems_increaseItemCount_containsCorrectItems() { + selectMenuPath("Component", "Data source", "Items", "100"); + assertItems(100); + } + + @Test + public void clickToSelect() { + selectMenuPath("Component", "Listeners", "Selection listener"); + + getSelect().selectByText("Item 4"); + Assert.assertEquals("1. Selected: Optional[Item 4]", getLogRow(0)); + + getSelect().selectByText("Item 2"); + Assert.assertEquals("2. Selected: Optional[Item 2]", getLogRow(0)); + + getSelect().selectByText("Item 4"); + Assert.assertEquals("3. Selected: Optional[Item 4]", getLogRow(0)); + } + + @Test + public void selectProgramatically() { + selectMenuPath("Component", "Listeners", "Selection listener"); + + selectMenuPath("Component", "Selection", "Toggle Item 5"); + Assert.assertEquals("2. Selected: Optional[Item 5]", getLogRow(0)); + assertSelected("Item 5"); + + selectMenuPath("Component", "Selection", "Toggle Item 1"); + Assert.assertEquals("4. Selected: Optional[Item 1]", getLogRow(0)); + // DOM order + assertSelected("Item 1"); + + selectMenuPath("Component", "Selection", "Toggle Item 5"); + Assert.assertEquals("6. Selected: Optional[Item 5]", getLogRow(0)); + assertSelected("Item 5"); + } + + private void assertSelected(String... expectedSelection) { + Assert.assertEquals(Arrays.asList(expectedSelection), + getSelect().getSelection()); + } + + @Override + protected Class<?> getUIClass() { + return RadioButtonGroupTestUI.class; + } + + protected RadioButtonGroupElement getSelect() { + return $(RadioButtonGroupElement.class).first(); + } + + protected void assertItems(int count) { + int i = 0; + for (String text : getSelect().getOptions()) { + assertEquals("Item " + i, text); + i++; + } + assertEquals("Number of items", count, i); + } +} |