Browse Source

Checkbox allow customizing of input and label classNames. (#11372)

* add client side integration for custom styles for checkbox.label and checkbox.input

* add server side integration for checkbox element styling

* add server side tests

* add client side test
tags/8.7.0.beta1
Knoobie 5 years ago
parent
commit
353ba29cfd

+ 16
- 3
client/src/main/java/com/vaadin/client/ui/VCheckBox.java View File

@@ -84,19 +84,32 @@ public class VCheckBox extends com.google.gwt.user.client.ui.CheckBox
*
* @return Element of the CheckBox itself
*/
private Element getCheckBoxElement() {
public Element getInputElement() {
// public to allow CheckBoxState to access it.
// FIXME: Would love to use a better way to access the checkbox element
return getElement().getFirstChildElement();
}

/**
* Gives access to the label element.
*
* @return Element of the Label itself
* @since
*/
public Element getLabelElement() {
// public to allow CheckBoxState to access it.
// FIXME: Would love to use a better way to access the label element
return getInputElement().getNextSiblingElement();
}

@Override
public void setAriaRequired(boolean required) {
AriaHelper.handleInputRequired(getCheckBoxElement(), required);
AriaHelper.handleInputRequired(getInputElement(), required);
}

@Override
public void setAriaInvalid(boolean invalid) {
AriaHelper.handleInputInvalid(getCheckBoxElement(), invalid);
AriaHelper.handleInputInvalid(getInputElement(), invalid);
}

@Override

+ 42
- 0
client/src/main/java/com/vaadin/client/ui/checkbox/CheckBoxConnector.java View File

@@ -15,6 +15,8 @@
*/
package com.vaadin.client.ui.checkbox;

import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.DOM;
@@ -30,11 +32,14 @@ import com.vaadin.client.ui.Icon;
import com.vaadin.client.ui.VCheckBox;
import com.vaadin.shared.EventId;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.ui.ComponentStateUtil;
import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.checkbox.CheckBoxServerRpc;
import com.vaadin.shared.ui.checkbox.CheckBoxState;
import com.vaadin.ui.CheckBox;

import java.util.List;

/**
* The client-side connector for the {@code CheckBoxGroup} component.
*
@@ -45,6 +50,22 @@ import com.vaadin.ui.CheckBox;
public class CheckBoxConnector extends AbstractFieldConnector
implements ClickHandler {

/**
* The style names from getState().inputStyles which are currently applied
* to the checkbox.
*
* @since
*/
private JsArrayString inputStyleNames = JsArrayString.createArray().cast();

/**
* The style names from getState().labelStyles which are currently applied
* to the checkbox.
*
* @since
*/
private JsArrayString labelStyleNames = JsArrayString.createArray().cast();

@Override
public boolean delegateCaptionHandling() {
return false;
@@ -88,6 +109,10 @@ public class CheckBoxConnector extends AbstractFieldConnector
VCaption.setCaptionText(getWidget(), getState());

getWidget().setValue(getState().checked);

// Set styles for input and label
updateStyles(getWidget().getInputElement(), inputStyleNames, getState().inputStyles);
updateStyles(getWidget().getLabelElement(), labelStyleNames, getState().labelStyles);
}

@Override
@@ -134,4 +159,21 @@ public class CheckBoxConnector extends AbstractFieldConnector
contextEventSunk = true;
}
}

private void updateStyles(Element clientElement, JsArrayString clientSideStyles, List<String> serverSideStyes) {
// Remove all old stylenames
for (int i = 0; i < clientSideStyles.length(); i++) {
clientElement.removeClassName(clientSideStyles.get(i));
}
clientSideStyles.setLength(0);

if (ComponentStateUtil.hasStyles(serverSideStyes)) {
// add new style names
for (String newStyle : serverSideStyes) {
clientElement.addClassName(newStyle);
clientSideStyles.push(newStyle);
}

}
}
}

+ 201
- 0
server/src/main/java/com/vaadin/ui/CheckBox.java View File

@@ -16,8 +16,12 @@

package com.vaadin.ui;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.StringTokenizer;

import org.jsoup.nodes.Attributes;
import org.jsoup.nodes.Element;
@@ -30,6 +34,7 @@ import com.vaadin.event.FieldEvents.FocusEvent;
import com.vaadin.event.FieldEvents.FocusListener;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.Registration;
import com.vaadin.shared.ui.ComponentStateUtil;
import com.vaadin.shared.ui.checkbox.CheckBoxServerRpc;
import com.vaadin.shared.ui.checkbox.CheckBoxState;
import com.vaadin.ui.declarative.DesignAttributeHandler;
@@ -64,6 +69,175 @@ public class CheckBox extends AbstractField<Boolean>
}
};

private CheckBoxInputElement checkBoxInputElement = null;
private CheckBoxLabelElement checkBoxLabelElement = null;

/**
* The inner input element of the CheckBox.
*/
public static class CheckBoxInputElement implements HasStyleNames {

private final CheckBox checkBox;

private CheckBoxInputElement(CheckBox checkBox){
this.checkBox = checkBox;
}

@Override
// Implementation copied from AbstractComponent
public String getStyleName() {
// replaced String with StringBuilder
StringBuilder s = new StringBuilder();
if (ComponentStateUtil.hasStyles(checkBox.getState(false).inputStyles)) {
for (final Iterator<String> it = checkBox.getState(false).inputStyles
.iterator(); it.hasNext();) {
s.append(it.next());
if (it.hasNext()) {
s.append(" ");
}
}
}
return s.toString();
}

@Override
// Implementation copied from AbstractComponent
public void setStyleName(String style) {
if (style == null || style.isEmpty()) {
checkBox.getState().inputStyles = null;
return;
}
if (checkBox.getState().inputStyles == null) {
checkBox.getState().inputStyles = new ArrayList<>();
}
List<String> styles = checkBox.getState().inputStyles;
styles.clear();
StringTokenizer tokenizer = new StringTokenizer(style, " ");
while (tokenizer.hasMoreTokens()) {
styles.add(tokenizer.nextToken());
}
}

@Override
// Implementation copied from AbstractComponent
public void addStyleName(String style) {
if (style == null || style.isEmpty()) {
return;
}
if (checkBox.getState().inputStyles != null && checkBox.getState().inputStyles.contains(style)) {
return;
}
if (style.contains(" ")) {
// Split space separated style names and add them one by one.
StringTokenizer tokenizer = new StringTokenizer(style, " ");
while (tokenizer.hasMoreTokens()) {
addStyleName(tokenizer.nextToken());
}
return;
}

if (checkBox.getState().inputStyles == null) {
checkBox.getState().inputStyles = new ArrayList<>();
}
List<String> styles = checkBox.getState().inputStyles;
styles.add(style);
}

@Override
// Implementation copied from AbstractComponent
public void removeStyleName(String style) {
if (ComponentStateUtil.hasStyles(checkBox.getState().inputStyles)) {
StringTokenizer tokenizer = new StringTokenizer(style, " ");
while (tokenizer.hasMoreTokens()) {
checkBox.getState().inputStyles.remove(tokenizer.nextToken());
}
}
}
}

/**
* The inner label element of the CheckBox.
*/
public static class CheckBoxLabelElement implements HasStyleNames {

private final CheckBox checkBox;

private CheckBoxLabelElement(CheckBox checkBox){
this.checkBox = checkBox;
}

@Override
// Implementation copied from AbstractComponent
public String getStyleName() {
// replaced String with StringBuilder
StringBuilder s = new StringBuilder();
if (ComponentStateUtil.hasStyles(checkBox.getState(false).labelStyles)) {
for (final Iterator<String> it = checkBox.getState(false).labelStyles
.iterator(); it.hasNext();) {
s.append(it.next());
if (it.hasNext()) {
s.append(" ");
}
}
}
return s.toString();
}

@Override
// Implementation copied from AbstractComponent
public void setStyleName(String style) {
if (style == null || style.isEmpty()) {
checkBox.getState().labelStyles = null;
return;
}
if (checkBox.getState().labelStyles == null) {
checkBox.getState().labelStyles = new ArrayList<>();
}
List<String> styles = checkBox.getState().labelStyles;
styles.clear();
StringTokenizer tokenizer = new StringTokenizer(style, " ");
while (tokenizer.hasMoreTokens()) {
styles.add(tokenizer.nextToken());
}
}

@Override
// Implementation copied from AbstractComponent
public void addStyleName(String style) {
if (style == null || style.isEmpty()) {
return;
}
if (checkBox.getState().labelStyles != null && checkBox.getState().labelStyles.contains(style)) {
return;
}
if (style.contains(" ")) {
// Split space separated style names and add them one by one.
StringTokenizer tokenizer = new StringTokenizer(style, " ");
while (tokenizer.hasMoreTokens()) {
addStyleName(tokenizer.nextToken());
}
return;
}

if (checkBox.getState().labelStyles == null) {
checkBox.getState().labelStyles = new ArrayList<>();
}
List<String> styles = checkBox.getState().labelStyles;
styles.add(style);
}

@Override
// Implementation copied from AbstractComponent
public void removeStyleName(String style) {
if (ComponentStateUtil.hasStyles(checkBox.getState().labelStyles)) {
StringTokenizer tokenizer = new StringTokenizer(style, " ");
while (tokenizer.hasMoreTokens()) {
checkBox.getState().labelStyles.remove(tokenizer.nextToken());
}
}
}
}

/**
* Creates a new checkbox.
*/
@@ -211,4 +385,31 @@ public class CheckBox extends AbstractField<Boolean>
def.getValue(), Boolean.class, designContext);
}

/**
* Returns the {@link CheckBoxInputElement} element to manipulate
* the style name of the {@code input} element of the {@link CheckBox}.
*
* @since
* @return the current {@link CheckBoxInputElement}, not {@code null}.
*/
public CheckBoxInputElement getInputElement() {
if(checkBoxInputElement == null) {
checkBoxInputElement = new CheckBoxInputElement(this);
}
return checkBoxInputElement;
}

/**
* Returns the {@link CheckBoxLabelElement} element to manipulate
* the style name of the {@code label} element of the {@link CheckBox}.
*
* @since
* @return the current {@link CheckBoxLabelElement}, not {@code null}.
*/
public CheckBoxLabelElement getLabelElement() {
if(checkBoxLabelElement == null) {
checkBoxLabelElement = new CheckBoxLabelElement(this);
}
return checkBoxLabelElement;
}
}

+ 179
- 0
server/src/main/java/com/vaadin/ui/HasStyleNames.java View File

@@ -0,0 +1,179 @@
/*
* Copyright 2000-2018 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.io.Serializable;

/**
* Implemented by components which support style names.
*
* <p>
* Each style name will occur only once as specified and it is not
* prefixed with the style name of the component.
* </p>
*
* @since
*/
public interface HasStyleNames extends Serializable {

/**
* Gets all user-defined CSS style names of a component. If the component
* has multiple style names defined, the return string is a space-separated
* list of style names. Built-in style names defined in Vaadin or GWT are
* not returned.
*
* <p>
* The style names are returned only in the basic form in which they were
* added.
* </p>
*
* @since
* @return the style name or a space-separated list of user-defined style
* names of the component
* @see #setStyleName(String)
* @see #addStyleName(String)
* @see #removeStyleName(String)
*/
String getStyleName();

/**
* Sets one or more user-defined style names of the component, replacing any
* previous user-defined styles. Multiple styles can be specified as a
* space-separated list of style names. The style names must be valid CSS
* class names.
*
* <p>
* It is normally a good practice to use {@link #addStyleName(String)
* addStyleName()} rather than this setter, as different software
* abstraction layers can then add their own styles without accidentally
* removing those defined in other layers.
* </p>
*
* @since
* @param style
* the new style or styles of the component as a space-separated
* list
* @see #getStyleName()
* @see #addStyleName(String)
* @see #removeStyleName(String)
*/
void setStyleName(String style);

/**
* Adds or removes a style name. Multiple styles can be specified as a
* space-separated list of style names.
*
* If the {@code add} parameter is true, the style name is added to the
* component. If the {@code add} parameter is false, the style name is
* removed from the component.
* <p>
* Functionally this is equivalent to using {@link #addStyleName(String)} or
* {@link #removeStyleName(String)}
*
* @since
* @param style
* the style name to be added or removed
* @param add
* <code>true</code> to add the given style, <code>false</code>
* to remove it
* @see #addStyleName(String)
* @see #removeStyleName(String)
*/
default void setStyleName(String style, boolean add) {
if (add) {
addStyleName(style);
} else {
removeStyleName(style);
}
}

/**
* Adds one or more style names to this component. Multiple styles can be
* specified as a space-separated list of style names. The style name will
* be rendered as a HTML class name, which can be used in a CSS definition.
*
*
* @since
* @param style
* the new style to be added to the component
* @see #getStyleName()
* @see #setStyleName(String)
* @see #removeStyleName(String)
*/
void addStyleName(String style);

/**
* Adds one or more style names to this component by using one or multiple
* parameters.
*
* @since
* @param styles
* the style name or style names to be added to the component
* @see #addStyleName(String)
* @see #setStyleName(String)
* @see #removeStyleName(String)
*/
default void addStyleNames(String... styles) {
for (String style : styles) {
addStyleName(style);
}
}

/**
* Removes one or more style names from component. Multiple styles can be
* specified as a space-separated list of style names.
*
* <p>
* The parameter must be a valid CSS style name. Only user-defined style
* names added with {@link #addStyleName(String) addStyleName()} or
* {@link #setStyleName(String) setStyleName()} can be removed; built-in
* style names defined in Vaadin or GWT can not be removed.
* </p>
*
* @since
* @param style
* the style name or style names to be removed
* @see #getStyleName()
* @see #setStyleName(String)
* @see #addStyleName(String)
*/
void removeStyleName(String style);

/**
* Removes one or more style names from component. Multiple styles can be
* specified by using multiple parameters.
*
* <p>
* The parameter must be a valid CSS style name. Only user-defined style
* names added with {@link #addStyleName(String) addStyleName()} or
* {@link #setStyleName(String) setStyleName()} can be removed; built-in
* style names defined in Vaadin or GWT can not be removed.
* </p>
*
* @since
* @param styles
* the style name or style names to be removed
* @see #removeStyleName(String)
* @see #setStyleName(String)
* @see #addStyleName(String)
*/
default void removeStyleNames(String... styles) {
for (String style : styles) {
removeStyleName(style);
}
}

}

+ 107
- 0
server/src/test/java/com/vaadin/ui/CheckBoxTest.java View File

@@ -1,10 +1,13 @@
package com.vaadin.ui;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.util.concurrent.atomic.AtomicBoolean;

import org.junit.Ignore;
import org.junit.Test;

import com.vaadin.server.ServerRpcManager;
@@ -68,4 +71,108 @@ public class CheckBoxTest {
cb.setValue(null);
}

@Test
public void getComboBoxInput() {
CheckBox cb = new CheckBox();
assertNotNull("getInputElement should always return a element", cb.getInputElement());
assertHasStyleNames(cb.getInputElement());
}

@Test
public void getCheckBoxLabel() {
CheckBox cb = new CheckBox();
assertNotNull("getLabelElement should always return a element", cb.getLabelElement());
assertHasStyleNames(cb.getLabelElement());
}

@Test
@Ignore("Component#setStyleName(null, false) should not throw a NPE")
public void setStyleName_null_false_throws_NPE() {
// FIXME? - Currently it throws a NPE like the implementation in Component.java
// waiting for other ticket that fixes the behaviour in Component.java before
CheckBox cb = new CheckBox();
cb.getLabelElement().addStyleName("first");
cb.getLabelElement().setStyleName(null, false);
assertEquals("Removing a null style should be ignored",
"first", cb.getLabelElement().getStyleName());
}

private void assertHasStyleNames(HasStyleNames hasStyleNames) {
assertEquals("Given element should not have a default style name",
"", hasStyleNames.getStyleName());

hasStyleNames.addStyleName("first");
assertEquals("first", hasStyleNames.getStyleName());

hasStyleNames.addStyleName("first");
assertEquals("Adding two times the same style should be ignored",
"first", hasStyleNames.getStyleName());

hasStyleNames.addStyleName(null);
assertEquals("Adding null as style should be ignored",
"first", hasStyleNames.getStyleName());

hasStyleNames.addStyleName("");
assertEquals("Adding an empty string as style should be ignored",
"first", hasStyleNames.getStyleName());

hasStyleNames.addStyleName("second");
assertEquals("first second", hasStyleNames.getStyleName());

hasStyleNames.removeStyleName("second");
assertEquals("first", hasStyleNames.getStyleName());

hasStyleNames.addStyleName("second third fourth");
assertEquals("first second third fourth", hasStyleNames.getStyleName());

hasStyleNames.removeStyleName("third fourth");
assertEquals("first second", hasStyleNames.getStyleName());

hasStyleNames.addStyleNames("third", "fourth");
assertEquals("first second third fourth", hasStyleNames.getStyleName());

hasStyleNames.removeStyleNames("second", "fourth");
assertEquals("first third", hasStyleNames.getStyleName());

hasStyleNames.setStyleName(null);
assertEquals("Setting null as style should reset them",
"", hasStyleNames.getStyleName());

hasStyleNames.setStyleName("set-style");
assertEquals("set-style", hasStyleNames.getStyleName());

hasStyleNames.setStyleName("");
assertEquals("Setting an empty string as style should reset them",
"", hasStyleNames.getStyleName());

hasStyleNames.setStyleName("set-style multiple values");
assertEquals("set-style multiple values", hasStyleNames.getStyleName());

hasStyleNames.setStyleName("set-style", false);
assertEquals("multiple values", hasStyleNames.getStyleName());

hasStyleNames.setStyleName("", false);
assertEquals("Removing an empty style should be ignored",
"multiple values", hasStyleNames.getStyleName());

hasStyleNames.setStyleName("", true);
assertEquals("Adding an empty style should be ignored",
"multiple values", hasStyleNames.getStyleName());

hasStyleNames.setStyleName(null, true);
assertEquals("Adding a null style should be ignored",
"multiple values", hasStyleNames.getStyleName());

hasStyleNames.setStyleName("multiple values", false);
assertEquals("Removing all set style names should result in an empty style name",
"", hasStyleNames.getStyleName());

hasStyleNames.setStyleName("set-style", true);
assertEquals("set-style", hasStyleNames.getStyleName());

hasStyleNames.setStyleName("multiple values", true);
assertEquals("set-style multiple values", hasStyleNames.getStyleName());

}

}

+ 6
- 1
shared/src/main/java/com/vaadin/shared/ui/ComponentStateUtil.java View File

@@ -17,6 +17,7 @@ package com.vaadin.shared.ui;

import java.io.Serializable;
import java.util.HashSet;
import java.util.List;

import com.vaadin.shared.AbstractComponentState;
import com.vaadin.shared.Registration;
@@ -42,7 +43,11 @@ public final class ComponentStateUtil implements Serializable {
}

public static final boolean hasStyles(AbstractComponentState state) {
return state.styles != null && !state.styles.isEmpty();
return hasStyles(state.styles);
}

public static final boolean hasStyles(List<String> styles) {
return styles != null && !styles.isEmpty();
}

public static final boolean isRelativeWidth(AbstractComponentState state) {

+ 4
- 0
shared/src/main/java/com/vaadin/shared/ui/checkbox/CheckBoxState.java View File

@@ -17,10 +17,14 @@ package com.vaadin.shared.ui.checkbox;

import com.vaadin.shared.AbstractFieldState;

import java.util.List;

public class CheckBoxState extends AbstractFieldState {
{
primaryStyleName = "v-checkbox";
}

public boolean checked = false;
public List<String> inputStyles = null;
public List<String> labelStyles = null;
}

+ 33
- 0
uitest/src/main/java/com/vaadin/tests/components/checkbox/CheckboxLabelInputElement.java View File

@@ -0,0 +1,33 @@
package com.vaadin.tests.components.checkbox;

import com.vaadin.server.VaadinRequest;
import com.vaadin.tests.components.AbstractTestUIWithLog;
import com.vaadin.ui.CheckBox;

public class CheckboxLabelInputElement extends AbstractTestUIWithLog {

@Override
protected void setup(VaadinRequest request) {
final CheckBox cb = new CheckBox("Test custom style names for inner elements", true);
cb.getInputElement().addStyleName("my-input-class");
cb.getLabelElement().addStyleName("my-label-class");

addComponent(cb);

addButton("add-style", e -> {
cb.getInputElement().addStyleName("later-applied-input-class");
cb.getLabelElement().addStyleName("later-applied-label-class");
});

addButton("remove-style", e -> {
cb.getInputElement().removeStyleName("my-input-class");
cb.getLabelElement().removeStyleName("my-label-class");
});

addButton("remove-style-2", e -> {
cb.getInputElement().removeStyleName("later-applied-input-class");
cb.getLabelElement().removeStyleName("later-applied-label-class");
});
}

}

+ 48
- 0
uitest/src/test/java/com/vaadin/tests/components/checkbox/CheckboxLabelInputElementTest.java View File

@@ -0,0 +1,48 @@
package com.vaadin.tests.components.checkbox;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import com.vaadin.testbench.elements.ButtonElement;
import com.vaadin.testbench.elements.CheckBoxElement;
import com.vaadin.tests.tb3.MultiBrowserTest;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;

public class CheckboxLabelInputElementTest extends MultiBrowserTest {

@Test
public void contextClickCheckboxAndText() {
openTestURL();
CheckBoxElement checkBoxElement = $(CheckBoxElement.class).first();
WebElement labelElem = checkBoxElement.findElement(By.tagName("label"));
WebElement inputElem = checkBoxElement.findElement(By.tagName("input"));

assertEquals("my-label-class", labelElem.getAttribute("class"));
assertEquals("my-input-class", inputElem.getAttribute("class"));
assertTrue("The Checkbox Widget should not contain the classes that are " +
"defined as style names for the input or label.",
!checkBoxElement.getAttribute("class").contains("my-label-class") &&
!checkBoxElement.getAttribute("class").contains("my-input-class"));

$(ButtonElement.class).caption("add-style").first().click();

assertEquals("my-label-class later-applied-label-class", labelElem.getAttribute("class"));
assertEquals("my-input-class later-applied-input-class", inputElem.getAttribute("class"));
assertTrue("The Checkbox Widget should not contain the classes that are " +
"defined as style names for the input or label.",
!checkBoxElement.getAttribute("class").contains("later-applied-label-class") &&
!checkBoxElement.getAttribute("class").contains("later-applied-input-class"));

$(ButtonElement.class).caption("remove-style").first().click();

assertEquals("later-applied-label-class", labelElem.getAttribute("class"));
assertEquals("later-applied-input-class", inputElem.getAttribute("class"));

$(ButtonElement.class).caption("remove-style-2").first().click();

assertTrue(labelElem.getAttribute("class").isEmpty());
assertTrue(inputElem.getAttribute("class").isEmpty());
}
}

Loading…
Cancel
Save