Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: check for input value populated state (WIP) #3550

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.vaadin.flow.component.datepicker;

import com.vaadin.flow.component.ClickEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.NativeButton;
import com.vaadin.flow.router.Route;

import java.time.LocalDate;
import java.util.Locale;

/**
* Page for testing DatePicker validation constraints
*/
@Route("vaadin-date-picker/constraint-validation")
public class DatePickerConstraintValidationPage extends Div {

public static final String MIN_DATE_BUTTON = "min-date-button";
public static final String MAX_DATE_BUTTON = "max-date-button";
public static final String REQUIRED_BUTTON = "required-button";

public static final LocalDate CONSTRAINT_DATE_VALUE = LocalDate.of(2022, 1,
1);
public static final String SERVER_VALIDITY_STATE_BUTTON = "server-validity-state-button";
public static final String VALIDITY_STATE = "validity-state";

public DatePickerConstraintValidationPage() {
var field = new DatePicker("Select date");
field.setLocale(Locale.US);

var required = addButton("required", REQUIRED_BUTTON,
e -> field.setRequired(true));
var minDate = addButton("min date", MIN_DATE_BUTTON,
e -> field.setMin(CONSTRAINT_DATE_VALUE));
var maxDate = addButton("max date", MAX_DATE_BUTTON,
e -> field.setMax(CONSTRAINT_DATE_VALUE));

var validityState = new Div();
validityState.setId(VALIDITY_STATE);

var retrieveValidityState = addButton("server validity state",
SERVER_VALIDITY_STATE_BUTTON,
e -> validityState.setText(String.valueOf(field.isInvalid())));

add(field, required, minDate, maxDate,
new Div(retrieveValidityState, validityState));
}

private NativeButton addButton(String label, String id,
ComponentEventListener<ClickEvent<NativeButton>> clickListener) {
var button = new NativeButton(label, clickListener);
button.setId(id);
return button;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ public void dateField_internalValidationPass_binderValidationFail_setClientValid
Assert.assertEquals(field.getPropertyString("label"), "invalid");
}

@Test
public void dateField_internalValidationFail_fieldInvalid() {
DatePickerElement field = $(DatePickerElement.class).first();

field.setInputValue("invalid_value");
Assert.assertTrue("Field should be invalid",
field.getPropertyBoolean("invalid"));
}

private void assertInvalid(DatePickerElement field) {
Assert.assertTrue("Unexpected invalid state",
field.getPropertyBoolean("invalid"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.vaadin.flow.component.datepicker;

import com.vaadin.flow.component.datepicker.testbench.DatePickerElement;
import com.vaadin.flow.testutil.TestPath;
import com.vaadin.tests.AbstractComponentIT;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

@TestPath("vaadin-date-picker/constraint-validation")
public class DatePickerConstraintValidationIT extends AbstractComponentIT {

DatePickerElement datePickerElement;

@Before
public void init() {
open();
datePickerElement = $(DatePickerElement.class).first();
}

@Test
public void setRequired_focusAndBlurField_fieldIsInvalid() {
clickButton(DatePickerConstraintValidationPage.REQUIRED_BUTTON);
datePickerElement.focus();
datePickerElement.sendKeys(Keys.TAB);

assertClientInvalid();
assertServerInvalid();
}

@Test
public void setRequired_fillWithValidValue_fieldIsValid() {
clickButton(DatePickerConstraintValidationPage.REQUIRED_BUTTON);
datePickerElement.setInputValue("01/01/2022");

assertClientValid();
assertServerValid();
}

@Test
public void setRequired_fillAndEmptyValue_fieldIsInvalid() {
clickButton(DatePickerConstraintValidationPage.REQUIRED_BUTTON);
datePickerElement.setInputValue("01/01/2022");

datePickerElement.clear();
assertClientInvalid();
assertServerInvalid();
}

@Test
public void setMinDate_enterDateLessThanMin_fieldIsInvalid() {
clickButton(DatePickerConstraintValidationPage.MIN_DATE_BUTTON);
String value = dateToString(
DatePickerConstraintValidationPage.CONSTRAINT_DATE_VALUE
.minusDays(1));
datePickerElement.setInputValue(value);

assertClientInvalid();
assertServerInvalid();
}

@Test
public void setMaxDate_enterDateGreaterThanMax_fieldIsInvalid() {
clickButton(DatePickerConstraintValidationPage.MAX_DATE_BUTTON);
String value = dateToString(
DatePickerConstraintValidationPage.CONSTRAINT_DATE_VALUE
.plusDays(1));
datePickerElement.setInputValue(value);

assertClientInvalid();
assertServerInvalid();
}

@Test
public void emptyField_invalidDateFormat_fieldIsInvalid() {
datePickerElement.setInputValue("invalid_format");

assertClientInvalid();
assertServerInvalid();
}

@Test
public void fieldInvalid_validDateAdded_fieldIsValid() {
datePickerElement.setInputValue("invalid_format");
datePickerElement.setInputValue("01/01/2022");

assertClientValid();
assertServerValid();
}

private void assertServerInvalid() {
Assert.assertEquals("Server should be invalid", "true",
getValidityState());
}

private void assertServerValid() {
Assert.assertEquals("Server should be valid", "false",
getValidityState());
}

private String getValidityState() {
clickButton(
DatePickerConstraintValidationPage.SERVER_VALIDITY_STATE_BUTTON);
return findElement(
By.id(DatePickerConstraintValidationPage.VALIDITY_STATE))
.getText();
}

private void assertClientInvalid() {
Assert.assertTrue("Client should be invalid", isClientInvalid());
}

private void assertClientValid() {
Assert.assertFalse("Client should be valid", isClientInvalid());
}

private boolean isClientInvalid() {
return datePickerElement.getPropertyBoolean("invalid");
}

private void setValue(String value) {
datePickerElement.sendKeys(value);
datePickerElement.sendKeys(Keys.ENTER);
}

private String dateToString(LocalDate date) {
return date.format(DateTimeFormatter.ofPattern("MM/dd/yyyy"));
}

private void clickButton(String id) {
findElement(By.id(id)).click();

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.io.Serializable;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
Expand All @@ -26,6 +27,8 @@

import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.Synchronize;
import com.vaadin.flow.component.shared.ClientValidationUtil;
import com.vaadin.flow.component.shared.HasAllowedCharPattern;
import com.vaadin.flow.component.shared.HasClearButton;
import com.vaadin.flow.component.HasHelper;
Expand All @@ -37,9 +40,12 @@
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.component.shared.HasClientValidation;
import com.vaadin.flow.component.shared.ValidationUtil;
import com.vaadin.flow.data.binder.HasValidator;
import com.vaadin.flow.data.binder.ValidationResult;
import com.vaadin.flow.data.binder.ValidationStatusChangeEvent;
import com.vaadin.flow.data.binder.ValidationStatusChangeListener;
import com.vaadin.flow.data.binder.Validator;
import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.function.SerializableFunction;
Expand Down Expand Up @@ -69,7 +75,8 @@
@NpmPackage(value = "date-fns", version = "2.28.0")
public class DatePicker extends GeneratedVaadinDatePicker<DatePicker, LocalDate>
implements HasSize, HasValidation, HasHelper, HasTheme, HasLabel,
HasClearButton, HasAllowedCharPattern, HasValidator<LocalDate> {
HasClearButton, HasAllowedCharPattern, HasValidator<LocalDate>,
HasClientValidation {

private static final String PROP_AUTO_OPEN_DISABLED = "autoOpenDisabled";

Expand All @@ -88,6 +95,7 @@ public class DatePicker extends GeneratedVaadinDatePicker<DatePicker, LocalDate>
private LocalDate max;
private LocalDate min;
private boolean required;
private final Collection<ValidationStatusChangeListener<LocalDate>> validationStatusChangeListeners = new ArrayList<>();

private StateTree.ExecutionRegistration pendingI18nUpdate;

Expand Down Expand Up @@ -134,6 +142,11 @@ private DatePicker(LocalDate initialDate, boolean isInitialValueOptional) {
// workaround for https://github.com/vaadin/flow/issues/3496
setInvalid(false);

addClientValidatedEventListener(e -> {
validate();
fireValidationStatusChangeEvent();
});

addValueChangeListener(e -> validate());
}

Expand Down Expand Up @@ -339,7 +352,7 @@ protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
initConnector();
requestI18nUpdate();
FieldValidationUtil.disableClientValidation(this);
ClientValidationUtil.preventWebComponentFromSettingItselfToValid(this);
}

private void initConnector() {
Expand Down Expand Up @@ -459,6 +472,19 @@ public String getErrorMessage() {
return getErrorMessageString();
}

@Override
public Registration addValidationStatusChangeListener(
ValidationStatusChangeListener<LocalDate> listener) {
validationStatusChangeListeners.add(listener);
return () -> validationStatusChangeListeners.remove(listener);
}

private void fireValidationStatusChangeEvent() {
var event = new ValidationStatusChangeEvent<>(this, !isInvalid());
validationStatusChangeListeners
.forEach(listener -> listener.validationStatusChanged(event));
}

@Override
public Validator<LocalDate> getDefaultValidator() {
return (value, context) -> checkValidity(value);
Expand All @@ -482,6 +508,12 @@ public boolean isInvalid() {
}

private ValidationResult checkValidity(LocalDate value) {
var hasNonParsableValue = value == getEmptyValue()
&& getInputValuePopulated();
if (hasNonParsableValue) {
return ValidationResult.error("");
}

var greaterThanMax = ValidationUtil.checkGreaterThanMax(value, max);
if (greaterThanMax.isError()) {
return greaterThanMax;
Expand All @@ -507,6 +539,18 @@ private boolean isInvalid(LocalDate value) {
return requiredValidation.isError() || checkValidity(value).isError();
}

/**
* Gets the populated state of the input's value, which is {@code false} by
* default.
*
* @return <code>true</code> if the input's value is populated,
* <code>false</code> otherwise
*/
@Synchronize(property = "_hasInputValue", value = "has-input-value-changed")
private boolean getInputValuePopulated() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we name it hasInputValue?

return getElement().getProperty("_hasInputValue", false);
}

/**
* Sets the label for the datepicker.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ protected String getValue() {
public void setInputValue(String value) {
this.open();
setProperty("_inputValue", value);
$("input").first().dispatchEvent("change");
this.close();
}

Expand Down