Repository: vaadin/flow-components
Pull request #7346: feat!: date time picker validation improvements
Author: login: ugur-vaadin
Labels: name: target/24.8
name: cherry-picked-24.8
name: Released with Vaadin 25.0.0-alpha1
Created: 2025-04-24T20:19:30Z | Merged: 2025-05-21T09:27:02Z

## Description

This PR implements DateTimePicker validation improvements for the Flow counterpart. The changes include:
- Use the new `unparsable-change` event from the web component
- Replace the `validated` event
- Prevent multiple validations during a single round-trip
- Improve validation consistency similar to the web component
- Postpone validation during navigation between DP and TP unless we are certain the value will be invalid (unless it is invalid or out of the range set using mix/max)
- Introduce a dedicated error message for incomplete values
- Handle the input element value changes caused by programmatically setting values 

Based on the [prototype](https://github.com/vaadin/flow-components/compare/proto/date-time-picker/unparsable-incomplete-change).

Depends on https://github.com/vaadin/web-components/pull/8986.

Part of #6697 

## Type of change

- [ ] Bugfix
- [x] Feature

## Checklist

- [x] I have read the contribution guide: https://vaadin.com/docs/latest/contributing/overview
- [x] I have added a description following the guideline.
- [x] The issue is created in the corresponding repository and I have referenced it.
- [x] I have added tests to ensure my change is effective and works as intended.
- [x] New and existing tests are passing locally with my change.
- [x] I have performed self-review and corrected misspellings.
- [x] Enhancement / new feature was discussed in a corresponding GitHub issue and Acceptance Criteria were created.


Diff:
diff --git a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java
index b8c6fa50192..d2a14dcf29e 100644
--- a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java
+++ b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java
@@ -167,9 +167,9 @@ public class DatePicker
     private Validator<LocalDate> defaultValidator = (value, context) -> {
         boolean fromComponent = context == null;
 
-        if (unparsableValue != null && fallbackParserErrorMessage != null) {
+        if (isInputUnparsable() && fallbackParserErrorMessage != null) {
             return ValidationResult.error(fallbackParserErrorMessage);
-        } else if (unparsableValue != null) {
+        } else if (isInputUnparsable()) {
             return ValidationResult.error(getI18nErrorMessage(
                     DatePickerI18n::getBadInputErrorMessage));
         }
@@ -665,15 +665,31 @@ private void fireValidationStatusChangeEvent() {
     }
 
     /**
+     * For internal use only.
+     * <p>
      * Returns whether the input element has a value or not.
      *
      * @return <code>true</code> if the input element's value is populated,
      *         <code>false</code> otherwise
+     * @deprecated Since v24.8
      */
+    @Deprecated(since = "24.8")
     protected boolean isInputValuePresent() {
         return !getInputElementValue().isEmpty();
     }
 
+    /**
+     * For internal use only.
+     * <p>
+     * Returns whether the input value is unparsable.
+     *
+     * @return <code>true</code> if the input element's value is populated and
+     *         unparsable, <code>false</code> otherwise
+     */
+    protected final boolean isInputUnparsable() {
+        return unparsableValue != null;
+    }
+
     /**
      * Gets the value of the input element. This value is updated on the server
      * when the web component dispatches a `change` or `unparsable-change`
@@ -760,7 +776,7 @@ private Result<LocalDate> runFallbackParser(String s) {
     @Override
     public void setValue(LocalDate value) {
         LocalDate oldValue = getValue();
-        if (oldValue == null && value == null && unparsableValue != null) {
+        if (oldValue == null && value == null && isInputUnparsable()) {
             // When the value is programmatically cleared while the field
             // contains an unparsable input, ValueChangeEvent isn't fired,
             // so we need to call setModelValue manually to clear the bad
@@ -795,7 +811,7 @@ protected void setModelValue(LocalDate newModelValue, boolean fromClient) {
         try {
             isFallbackParserRunning = true;
 
-            if (fallbackParser != null && unparsableValue != null) {
+            if (fallbackParser != null && isInputUnparsable()) {
                 Result<LocalDate> result = runFallbackParser(unparsableValue);
                 if (result.isError()) {
                     fallbackParserErrorMessage = result.getMessage()
diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java
index a2fcc85c9c9..192b62c0d46 100644
--- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java
+++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationPage.java
@@ -30,15 +30,17 @@ public class BasicValidationPage
     public static final String CLEAR_VALUE_BUTTON = "clear-value-button";
 
     public static final String REQUIRED_ERROR_MESSAGE = "Field is required";
-    public static final String BAD_INPUT_ERROR_MESSAGE = "Value has incorrect format";
-    public static final String MIN_ERROR_MESSAGE = "Value is too small";
-    public static final String MAX_ERROR_MESSAGE = "Value is too big";
+    public static final String BAD_INPUT_ERROR_MESSAGE = "Invalid date format";
+    public static final String INCOMPLETE_INPUT_ERROR_MESSAGE = "Must fill in both date and time";
+    public static final String MIN_ERROR_MESSAGE = "Date is too early";
+    public static final String MAX_ERROR_MESSAGE = "Date is too late";
 
     public BasicValidationPage() {
         super();
 
         testField.setI18n(new DateTimePicker.DateTimePickerI18n()
                 .setRequiredErrorMessage(REQUIRED_ERROR_MESSAGE)
+                .setIncompleteInputErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE)
                 .setBadInputErrorMessage(BAD_INPUT_ERROR_MESSAGE)
                 .setMinErrorMessage(MIN_ERROR_MESSAGE)
                 .setMaxErrorMessage(MAX_ERROR_MESSAGE));
@@ -63,6 +65,12 @@ public BasicValidationPage() {
     }
 
     protected DateTimePicker createTestField() {
-        return new DateTimePicker();
+        return new DateTimePicker() {
+            @Override
+            protected void validate() {
+                super.validate();
+                incrementServerValidationCounter();
+            }
+        };
     }
 }
diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationPage.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationPage.java
index b0bac82ca5d..2637bc9f908 100644
--- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationPage.java
+++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationPage.java
@@ -32,6 +32,7 @@ public class BinderValidationPage
 
     public static final String REQUIRED_ERROR_MESSAGE = "Field is required";
     public static final String BAD_INPUT_ERROR_MESSAGE = "Value has incorrect format";
+    public static final String INCOMPLETE_INPUT_ERROR_MESSAGE = "Value is incomplete";
     public static final String MIN_ERROR_MESSAGE = "Value is too small";
     public static final String MAX_ERROR_MESSAGE = "Value is too big";
     public static final String UNEXPECTED_VALUE_ERROR_MESSAGE = "Value does not match the expected value";
@@ -63,6 +64,7 @@ public BinderValidationPage() {
 
         testField.setI18n(new DateTimePicker.DateTimePickerI18n()
                 .setBadInputErrorMessage(BAD_INPUT_ERROR_MESSAGE)
+                .setIncompleteInputErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE)
                 .setMinErrorMessage(MIN_ERROR_MESSAGE)
                 .setMaxErrorMessage(MAX_ERROR_MESSAGE));
 
@@ -88,6 +90,12 @@ public BinderValidationPage() {
     }
 
     protected DateTimePicker createTestField() {
-        return new DateTimePicker();
+        return new DateTimePicker() {
+            @Override
+            protected void validate() {
+                super.validate();
+                incrementServerValidationCounter();
+            }
+        };
     }
 }
diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
index a917fc027c2..61fa984058a 100644
--- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
+++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
@@ -17,6 +17,7 @@
 
 import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.BAD_INPUT_ERROR_MESSAGE;
 import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.CLEAR_VALUE_BUTTON;
+import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.INCOMPLETE_INPUT_ERROR_MESSAGE;
 import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.MAX_ERROR_MESSAGE;
 import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.MAX_INPUT;
 import static com.vaadin.flow.component.datetimepicker.validation.BasicValidationPage.MIN_ERROR_MESSAGE;
@@ -59,52 +60,87 @@ public void triggerBlur_assertValidity() {
         timeInput.sendKeys(Keys.TAB);
         assertServerValid();
         assertClientValid();
-        assertErrorMessage("");
+        assertErrorMessage(null);
+        assertValidationCount(0);
     }
 
     @Test
     public void required_triggerBlur_assertValidity() {
         $("button").id(REQUIRED_BUTTON).click();
 
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys(Keys.TAB);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
+        assertValidationCount(0);
+    }
+
+    @Test
+    public void required_changeInputTemporarily_triggerBlur_assertValidity() {
+        $("button").id(REQUIRED_BUTTON).click();
+        dateInput.sendKeys("1", Keys.BACK_SPACE, Keys.ENTER);
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys(Keys.TAB);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
+        assertValidationCount(0);
+    }
+
+    @Test
+    public void required_changeAndClearValueWithoutBlur_triggerBlur_assertValidity() {
+        $("button").id(REQUIRED_BUTTON).click();
+        dateInput.sendKeys("1/1/2000", Keys.ENTER);
+        dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE);
         dateInput.sendKeys(Keys.TAB);
         timeInput.sendKeys(Keys.TAB);
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     @Test
     public void required_changeValue_assertValidity() {
         $("button").id(REQUIRED_BUTTON).click();
 
-        setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "12:00");
+        setValue("1/1/2000", "12:00");
         assertServerValid();
         assertClientValid();
         assertErrorMessage("");
+        assertValidationCount(1);
+
+        setInputValue(timeInput, "");
+        assertServerInvalid();
+        assertServerInvalid();
+        assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(dateInput, "");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(timeInput, "");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(0);
 
-        setInputValue(dateInput, "INVALID");
         setInputValue(timeInput, "INVALID");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "");
         setInputValue(timeInput, "");
         timeInput.sendKeys(Keys.TAB);
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     @Test
@@ -112,34 +148,46 @@ public void min_changeValue_assertValidity() {
         $("input").id(MIN_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER);
 
         setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "11:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "11:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(dateInput, "2/2/2000");
+        assertClientInvalid();
+        assertServerInvalid();
+        assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(1);
+
         setInputValue(timeInput, "12:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "13:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
         setInputValue(dateInput, "3/3/2000");
+        assertClientValid();
+        assertServerValid();
+        assertErrorMessage("");
+        assertValidationCount(1);
+
         setInputValue(timeInput, "11:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
     }
 
     @Test
@@ -147,69 +195,88 @@ public void max_changeDateInputValue_assertValidity() {
         $("input").id(MAX_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER);
 
         setInputValue(dateInput, "3/3/2000");
-        setInputValue(timeInput, "13:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(1);
+
+        setInputValue(timeInput, "12:00");
+        assertClientInvalid();
+        assertServerInvalid();
+        assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(dateInput, "2/2/2000");
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage("");
+        assertValidationCount(1);
+
         setInputValue(timeInput, "13:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "12:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "11:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
         setInputValue(dateInput, "1/1/2000");
+        assertClientValid();
+        assertServerValid();
+        assertErrorMessage("");
+        assertValidationCount(1);
+
         setInputValue(timeInput, "13:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
     }
 
     @Test
     public void setValue_clearValue_assertValidity() {
-        setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "10:00");
+        setValue("1/1/2000", "10:00");
         assertServerValid();
         assertClientValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
         $("button").id(CLEAR_VALUE_BUTTON).click();
         assertServerValid();
         assertClientValid();
         assertErrorMessage("");
+        assertValidationCount(1);
     }
 
     @Test
     public void badInput_changeValue_assertValidity() {
-        setInputValue(dateInput, "INVALID");
-        setInputValue(timeInput, "INVALID");
+        setValue("1/1/2000", "INVALID");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "1/1/2000");
         setInputValue(timeInput, "10:00");
         assertServerValid();
         assertClientValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
         setInputValue(dateInput, "INVALID");
-        setInputValue(timeInput, "INVALID");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     @Test
@@ -220,6 +287,7 @@ public void badInput_setDateInputValue_blur_assertValidity() {
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     @Test
@@ -229,20 +297,22 @@ public void badInput_setTimeInputValue_blur_assertValidity() {
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     @Test
     public void badInput_setValue_clearValue_assertValidity() {
         setInputValue(dateInput, "INVALID");
-        setInputValue(timeInput, "INVALID");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         $("button").id(CLEAR_VALUE_BUTTON).click();
         assertServerValid();
         assertClientValid();
         assertErrorMessage("");
+        assertValidationCount(1);
     }
 
     @Test
@@ -253,11 +323,13 @@ public void badInput_setDateInputValue_blur_clearValue_assertValidity() {
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         $("button").id(CLEAR_VALUE_BUTTON).click();
         assertServerValid();
         assertClientValid();
         assertErrorMessage("");
+        assertValidationCount(1);
     }
 
     @Test
@@ -267,19 +339,117 @@ public void badInput_setTimeInputValue_blur_clearValue_assertValidity() {
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         $("button").id(CLEAR_VALUE_BUTTON).click();
         assertServerValid();
         assertClientValid();
         assertErrorMessage("");
+        assertValidationCount(1);
     }
 
     @Test
-    public void detach_attach_preservesInvalidState() {
-        // Make field invalid
-        $("button").id(REQUIRED_BUTTON).click();
+    public void incompleteInput_assertValidity() {
+        setInputValue(dateInput, "1/1/2000");
+        assertServerInvalid();
+        assertClientInvalid();
+        assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void incompleteInput_changeToValidValue_assertValidity() {
+        setInputValue(dateInput, "1/1/2000");
+        resetValidationCount();
+
+        setInputValue(timeInput, "10:00");
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage("");
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void validInput_changeToIncompleteInput_assertValidity() {
+        setValue("1/1/2001", "10:00");
+        resetValidationCount();
+
+        setInputValue(timeInput, "");
+        assertServerInvalid();
+        assertClientInvalid();
+        assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void incompleteInput_setDateInputValue_blur_assertValidity() {
+        setInputValue(dateInput, "1/1/2000");
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys(Keys.TAB);
+        assertServerInvalid();
+        assertClientInvalid();
+        assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void incompleteInput_setTimeInputValue_blur_assertValidity() {
+        setInputValue(timeInput, "10:00");
+        timeInput.sendKeys(Keys.TAB);
+        assertServerInvalid();
+        assertClientInvalid();
+        assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void incompleteInput_setValue_clearValue_assertValidity() {
+        setInputValue(dateInput, "1/1/2000");
+        timeInput.sendKeys(Keys.ENTER);
+        resetValidationCount();
+
+        $("button").id(CLEAR_VALUE_BUTTON).click();
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage("");
+        assertValidationCount(1);
+    }
+
+    @Override
+    protected void assertValidationCount(int expected) {
+        super.assertValidationCount(expected);
+    }
+
+    @Test
+    public void incompleteInput_setDateInputValue_blur_clearValue_assertValidity() {
+        setInputValue(dateInput, "1/1/2000");
         dateInput.sendKeys(Keys.TAB);
         timeInput.sendKeys(Keys.TAB);
+        resetValidationCount();
+
+        $("button").id(CLEAR_VALUE_BUTTON).click();
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage("");
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void incompleteInput_setTimeInputValue_blur_clearValue_assertValidity() {
+        setInputValue(timeInput, "10:00");
+        timeInput.sendKeys(Keys.TAB);
+        resetValidationCount();
+
+        $("button").id(CLEAR_VALUE_BUTTON).click();
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage("");
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void detach_attach_preservesInvalidState() {
+        setInputValue(dateInput, "INVALID");
 
         detachAndReattachField();
 
@@ -309,10 +479,7 @@ public void detach_hide_attach_showAndInvalidate_preservesInvalidState() {
 
     @Test
     public void clientSideInvalidStateIsNotPropagatedToServer() {
-        // Make the field invalid
-        $("button").id(REQUIRED_BUTTON).click();
-        dateInput.sendKeys(Keys.TAB);
-        timeInput.sendKeys(Keys.TAB);
+        setInputValue(dateInput, "INVALID");
 
         executeScript("arguments[0].invalid = false", testField);
 
@@ -323,6 +490,12 @@ protected DateTimePickerElement getTestField() {
         return $(DateTimePickerElement.class).first();
     }
 
+    private void setValue(String dateValue, String timeValue) {
+        setInputValue(dateInput, dateValue);
+        dateInput.sendKeys(Keys.TAB);
+        setInputValue(timeInput, timeValue);
+    }
+
     private void setInputValue(TestBenchElement input, String value) {
         input.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE);
         input.sendKeys(value, Keys.ENTER);
diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java
index 7ed8a57d1f9..a5e1c52cf13 100644
--- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java
+++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java
@@ -18,6 +18,7 @@
 import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.BAD_INPUT_ERROR_MESSAGE;
 import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.CLEAR_VALUE_BUTTON;
 import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.EXPECTED_VALUE_INPUT;
+import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.INCOMPLETE_INPUT_ERROR_MESSAGE;
 import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.MAX_ERROR_MESSAGE;
 import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.MAX_INPUT;
 import static com.vaadin.flow.component.datetimepicker.validation.BinderValidationPage.MIN_ERROR_MESSAGE;
@@ -58,17 +59,31 @@ public void fieldIsInitiallyValid() {
     public void required_triggerDateInputBlur_assertValidity() {
         dateInput.sendKeys(Keys.TAB);
         timeInput.sendKeys(Keys.TAB);
-        assertServerInvalid();
-        assertClientInvalid();
-        assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
+        assertValidationCount(0);
     }
 
     @Test
     public void required_triggerTimeInputBlur_assertValidity() {
+        timeInput.sendKeys(Keys.TAB);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
+        assertValidationCount(0);
+    }
+
+    @Test
+    public void required_changeAndClearValueWithoutBlur_triggerBlur_assertValidity() {
+        dateInput.sendKeys("1/1/2000", Keys.ENTER);
+        dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE);
+        dateInput.sendKeys(Keys.TAB);
         timeInput.sendKeys(Keys.TAB);
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     @Test
@@ -76,33 +91,35 @@ public void required_changeValue_assertValidity() {
         $("input").id(EXPECTED_VALUE_INPUT).sendKeys("2000-01-01T12:00",
                 Keys.ENTER);
 
-        setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "12:00");
+        setValue("1/1/2000", "12:00");
         assertServerValid();
         assertClientValid();
+        assertValidationCount(1);
 
         setInputValue(dateInput, "");
         assertServerInvalid();
         assertClientInvalid();
-        assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(timeInput, "");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(dateInput, "INVALID");
-        setInputValue(timeInput, "INVALID");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(dateInput, "");
-        setInputValue(timeInput, "");
         timeInput.sendKeys(Keys.TAB);
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     @Test
@@ -112,33 +129,45 @@ public void min_changeValue_assertValidity() {
                 Keys.ENTER);
 
         setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "11:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
-        setInputValue(timeInput, "11:00");
+        setInputValue(timeInput, "10:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(dateInput, "2/2/2000");
+        assertClientInvalid();
+        assertServerInvalid();
+        assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(1);
+
         setInputValue(timeInput, "12:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(UNEXPECTED_VALUE_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "13:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(UNEXPECTED_VALUE_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(dateInput, "3/3/2000");
+        assertClientInvalid();
+        assertServerInvalid();
+        assertErrorMessage(UNEXPECTED_VALUE_ERROR_MESSAGE);
+        assertValidationCount(1);
+
         setInputValue(timeInput, "11:00");
         assertClientValid();
         assertServerValid();
+        assertValidationCount(1);
     }
 
     @Test
@@ -148,33 +177,45 @@ public void max_changeValue_assertValidity() {
                 Keys.ENTER);
 
         setInputValue(dateInput, "3/3/2000");
-        setInputValue(timeInput, "13:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "13:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(dateInput, "2/2/2000");
+        assertClientInvalid();
+        assertServerInvalid();
+        assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(1);
+
         setInputValue(timeInput, "12:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(UNEXPECTED_VALUE_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "11:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(UNEXPECTED_VALUE_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(dateInput, "1/1/2000");
+        assertClientInvalid();
+        assertServerInvalid();
+        assertErrorMessage(UNEXPECTED_VALUE_ERROR_MESSAGE);
+        assertValidationCount(1);
+
         setInputValue(timeInput, "13:00");
         assertClientValid();
         assertServerValid();
+        assertValidationCount(1);
     }
 
     @Test
@@ -182,15 +223,16 @@ public void setValue_clearValue_assertValidity() {
         $("input").id(EXPECTED_VALUE_INPUT).sendKeys("2000-01-01T10:00",
                 Keys.ENTER);
 
-        setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "10:00");
+        setValue("1/1/2000", "10:00");
         assertServerValid();
         assertClientValid();
+        assertValidationCount(1);
 
         $("button").id(CLEAR_VALUE_BUTTON).click();
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     @Test
@@ -198,42 +240,55 @@ public void badInput_changeValue_assertValidity() {
         $("input").id(EXPECTED_VALUE_INPUT).sendKeys("2000-01-01T10:00",
                 Keys.ENTER);
 
-        setInputValue(dateInput, "INVALID");
-        setInputValue(timeInput, "INVALID");
+        setValue("1/1/2000", "INVALID");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "1/1/2000");
         setInputValue(timeInput, "10:00");
         assertServerValid();
         assertClientValid();
+        assertValidationCount(1);
 
         setInputValue(dateInput, "INVALID");
-        setInputValue(timeInput, "INVALID");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     @Test
     public void badInput_setValue_clearValue_assertValidity() {
         setInputValue(dateInput, "INVALID");
+        assertServerInvalid();
+        assertClientInvalid();
+        assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
+
         setInputValue(timeInput, "INVALID");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         $("button").id(CLEAR_VALUE_BUTTON).click();
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     protected DateTimePickerElement getTestField() {
         return $(DateTimePickerElement.class).first();
     }
 
+    private void setValue(String dateValue, String timeValue) {
+        setInputValue(dateInput, dateValue);
+        dateInput.sendKeys(Keys.TAB);
+        setInputValue(timeInput, timeValue);
+    }
+
     private void setInputValue(TestBenchElement input, String value) {
         input.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE);
         input.sendKeys(value, Keys.ENTER);
diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
index 5f0068331f2..b314c701d6e 100644
--- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
+++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
@@ -22,6 +22,7 @@
 import java.util.Locale;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.function.Function;
 
 import com.vaadin.flow.component.AbstractField;
@@ -61,9 +62,8 @@ protected void validate() {
         // Should not change invalid state
     }
 
-    @Override
-    protected boolean isInputValuePresent() {
-        return super.isInputValuePresent();
+    boolean isPickerInputUnparsable() {
+        return super.isInputUnparsable();
     }
 }
 
@@ -75,9 +75,8 @@ protected void validate() {
         // Should not change invalid state
     }
 
-    @Override
-    protected boolean isInputValuePresent() {
-        return super.isInputValuePresent();
+    boolean isPickerInputUnparsable() {
+        return super.isInputUnparsable();
     }
 }
 
@@ -121,18 +120,45 @@ public class DateTimePicker
     private LocalDateTime max;
     private LocalDateTime min;
 
-    private Validator<LocalDateTime> defaultValidator = (value, context) -> {
-        boolean fromComponent = context == null;
+    private final CopyOnWriteArrayList<ValidationStatusChangeListener<LocalDateTime>> validationStatusChangeListeners = new CopyOnWriteArrayList<>();
 
-        boolean hasBadDatePickerInput = Objects.equals(datePicker.getValue(),
-                datePicker.getEmptyValue()) && datePicker.isInputValuePresent();
-        boolean hasBadTimePickerInput = Objects.equals(timePicker.getValue(),
-                timePicker.getEmptyValue()) && timePicker.isInputValuePresent();
-        if (hasBadDatePickerInput || hasBadTimePickerInput) {
+    private final Validator<LocalDateTime> defaultValidator = (value,
+            context) -> {
+        var fromComponent = context == null;
+
+        // Report error if any of the pickers has bad input
+        if (isInputUnparsable()) {
             return ValidationResult.error(getI18nErrorMessage(
                     DateTimePickerI18n::getBadInputErrorMessage));
         }
 
+        // Report error if only date picker has a value, and it's outside the
+        // range.
+        if (Objects.equals(value, getEmptyValue()) && !datePicker.isEmpty()) {
+            var maxDate = max != null ? max.toLocalDate() : null;
+            var minDate = min != null ? min.toLocalDate() : null;
+
+            var maxResult = ValidationUtil.validateMaxConstraint(
+                    getI18nErrorMessage(DateTimePickerI18n::getMaxErrorMessage),
+                    datePicker.getValue(), maxDate);
+            if (maxResult.isError()) {
+                return maxResult;
+            }
+
+            var minResult = ValidationUtil.validateMinConstraint(
+                    getI18nErrorMessage(DateTimePickerI18n::getMinErrorMessage),
+                    datePicker.getValue(), minDate);
+            if (minResult.isError()) {
+                return minResult;
+            }
+        }
+
+        // Report error if only one of the pickers has a value
+        if (isInputIncomplete()) {
+            return ValidationResult.error(getI18nErrorMessage(
+                    DateTimePickerI18n::getIncompleteInputErrorMessage));
+        }
+
         // Do the required check only if the validator is called from the
         // component, and not from Binder. Binder has its own implementation
         // of required validation.
@@ -147,14 +173,14 @@ public class DateTimePicker
             }
         }
 
-        ValidationResult maxResult = ValidationUtil.validateMaxConstraint(
+        var maxResult = ValidationUtil.validateMaxConstraint(
                 getI18nErrorMessage(DateTimePickerI18n::getMaxErrorMessage),
                 value, max);
         if (maxResult.isError()) {
             return maxResult;
         }
 
-        ValidationResult minResult = ValidationUtil.validateMinConstraint(
+        var minResult = ValidationUtil.validateMinConstraint(
                 getI18nErrorMessage(DateTimePickerI18n::getMinErrorMessage),
                 value, min);
         if (minResult.isError()) {
@@ -235,9 +261,7 @@ public DateTimePicker(LocalDateTime initialDateTime) {
         // workaround for https://github.com/vaadin/flow/issues/3496
         setInvalid(false);
 
-        addValueChangeListener(e -> validate());
-
-        addClientValidatedEventListener(e -> validate());
+        addValidationListeners();
     }
 
     /**
@@ -325,6 +349,36 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) {
         setLocale(locale);
     }
 
+    private void addValidationListeners() {
+        addValueChangeListener(e -> validate());
+        getElement().addEventListener("unparsable-change", e -> validate(true));
+        // Add listeners to invalidate a required DateTimePicker when:
+        // 1. It's initially empty
+        // 2. The user selects a value in one picker (date or time)
+        // 3. The user then removes that value
+        // 4. The user leaves the field without proceeding to the other picker
+        datePicker.addValueChangeListener(event -> handlePickerValueChange());
+        timePicker.addValueChangeListener(event -> handlePickerValueChange());
+    }
+
+    private void handlePickerValueChange() {
+        // Only handle picker value change if required.
+        if (!isRequiredIndicatorVisible()) {
+            return;
+        }
+        // If the component is already invalid, any picker value change will
+        // trigger the listener for either unparsable-change event or value
+        // change event. Therefore, the component will be validated and there is
+        // no need to validate here.
+        if (isInvalid()) {
+            return;
+        }
+        if (isEmpty() && timePicker.isEmpty() && datePicker.isEmpty()
+                && !isInputUnparsable()) {
+            validate(true);
+        }
+    }
+
     /**
      * Sets the selected date and time value of the component. The value can be
      * cleared by setting null.
@@ -341,23 +395,16 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) {
      */
     @Override
     public void setValue(LocalDateTime value) {
-        LocalDateTime oldValue = getValue();
-
+        var oldValue = getValue();
         value = sanitizeValue(value);
+        var shouldFireValidationStatusChangeEvent = oldValue == null
+                && value == null
+                && (isInputUnparsable() || isInputIncomplete());
         super.setValue(value);
-
-        boolean isInputValuePresent = timePicker.isInputValuePresent()
-                || datePicker.isInputValuePresent();
-        boolean isValueRemainedEmpty = valueEquals(oldValue, getEmptyValue())
-                && valueEquals(value, getEmptyValue());
-        if (isValueRemainedEmpty && isInputValuePresent) {
-            // Clear the input elements from possible bad input.
-            synchronizeChildComponentValues(value);
-            fireEvent(new ClientValidatedEvent(this, false));
-        } else {
-            synchronizeChildComponentValues(value);
+        synchronizeChildComponentValues(value);
+        if (shouldFireValidationStatusChangeEvent) {
+            validate(true);
         }
-
     }
 
     /**
@@ -747,6 +794,15 @@ public void removeThemeNames(String... themeNames) {
         synchronizeTheme();
     }
 
+    private boolean isInputUnparsable() {
+        return datePicker.isPickerInputUnparsable()
+                || timePicker.isPickerInputUnparsable();
+    }
+
+    private boolean isInputIncomplete() {
+        return datePicker.isEmpty() != timePicker.isEmpty();
+    }
+
     @Override
     public Validator<LocalDateTime> getDefaultValidator() {
         return defaultValidator;
@@ -755,9 +811,8 @@ public Validator<LocalDateTime> getDefaultValidator() {
     @Override
     public Registration addValidationStatusChangeListener(
             ValidationStatusChangeListener<LocalDateTime> listener) {
-        return addClientValidatedEventListener(event -> listener
-                .validationStatusChanged(new ValidationStatusChangeEvent<>(this,
-                        event.isValid())));
+        return Registration.addAndRemove(validationStatusChangeListeners,
+                listener);
     }
 
     @Override
@@ -778,6 +833,26 @@ protected void validate() {
         validationController.validate(getValue());
     }
 
+    /**
+     * Delegates the call to {@link #validate()} and additionally fires
+     * {@link ValidationStatusChangeEvent} to notify Binder that it needs to
+     * revalidate since the component's own validity state may have changed.
+     * <p>
+     * NOTE: There is no need to notify Binder separately when running
+     * validation on {@link ValueChangeEvent}, as Binder already listens to this
+     * event and revalidates automatically.
+     */
+    private void validate(boolean shouldFireValidationStatusChangeEvent) {
+        validate();
+
+        if (shouldFireValidationStatusChangeEvent) {
+            ValidationStatusChangeEvent<LocalDateTime> event = new ValidationStatusChangeEvent<>(
+                    this, !isInvalid());
+            validationStatusChangeListeners.forEach(
+                    listener -> listener.validationStatusChanged(event));
+        }
+    }
+
     /**
      * Sets the minimum date and time in the date time picker. Dates and times
      * before that will be disabled in the popups.
@@ -927,6 +1002,7 @@ public static class DateTimePickerI18n implements Serializable {
         private String dateLabel;
         private String timeLabel;
         private String badInputErrorMessage;
+        private String incompleteInputErrorMessage;
         private String requiredErrorMessage;
         private String minErrorMessage;
         private String maxErrorMessage;
@@ -934,7 +1010,7 @@ public static class DateTimePickerI18n implements Serializable {
         /**
          * Gets the aria-label suffix for the date picker.
          * <p>
-         * The date picker's final aria-label is a concatanation of the
+         * The date picker's final aria-label is a concatenation of the
          * DateTimePicker's {@link #getAriaLabel()} or {@link #getLabel()}
          * methods and this suffix.
          *
@@ -947,7 +1023,7 @@ public String getDateLabel() {
         /**
          * Sets the aria-label suffix for the date picker.
          * <p>
-         * The date picker's final aria-label is a concatanation of the
+         * The date picker's final aria-label is a concatenation of the
          * DateTimePicker's {@link #getAriaLabel()} or {@link #getLabel()}
          * methods and this suffix.
          *
@@ -964,7 +1040,7 @@ public DateTimePickerI18n setDateLabel(String dateLabel) {
         /**
          * Gets the aria-label suffix for the time picker.
          * <p>
-         * The time picker's aria-label is a concatanation of the
+         * The time picker's aria-label is a concatenation of the
          * DateTimePicker's {@link #getAriaLabel()} or {@link #getLabel()}
          * methods and this suffix.
          *
@@ -977,7 +1053,7 @@ public String getTimeLabel() {
         /**
          * Sets the aria-label suffix for the time picker.
          * <p>
-         * The time picker's aria-label is a concatanation of the
+         * The time picker's aria-label is a concatenation of the
          * DateTimePicker's {@link #getAriaLabel()} or {@link #getLabel()}
          * methods and this suffix.
          *
@@ -1018,6 +1094,34 @@ public DateTimePickerI18n setBadInputErrorMessage(String errorMessage) {
             return this;
         }
 
+        /**
+         * Gets the error message displayed when either the date or time is
+         * empty.
+         *
+         * @return the error message or {@code null} if not set
+         */
+        public String getIncompleteInputErrorMessage() {
+            return incompleteInputErrorMessage;
+        }
+
+        /**
+         * Sets the error message to display when either the date or time is
+         * empty.
+         * <p>
+         * Note, custom error messages set with
+         * {@link DateTimePicker#setErrorMessage(String)} take priority over
+         * i18n error messages.
+         *
+         * @param errorMessage
+         *            the error message to set, or {@code null} to clear
+         * @return this instance for method chaining
+         */
+        public DateTimePickerI18n setIncompleteInputErrorMessage(
+                String errorMessage) {
+            incompleteInputErrorMessage = errorMessage;
+            return this;
+        }
+
         /**
          * Gets the error message displayed when the field is required but
          * empty.
diff --git a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java
index 6a7a88ea2b2..9b71d6ba7c5 100644
--- a/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java
+++ b/vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java
@@ -15,7 +15,11 @@
  */
 package com.vaadin.flow.component.datetimepicker.validation;
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.junit.Assert;
 import org.junit.Test;
@@ -25,6 +29,7 @@
 import com.vaadin.flow.component.shared.SlotUtils;
 import com.vaadin.flow.component.timepicker.TimePicker;
 import com.vaadin.flow.dom.DomEvent;
+import com.vaadin.flow.dom.Element;
 import com.vaadin.flow.internal.nodefeature.ElementListenerMap;
 import com.vaadin.tests.validation.AbstractBasicValidationTest;
 
@@ -32,28 +37,99 @@
 
 public class BasicValidationTest
         extends AbstractBasicValidationTest<DateTimePicker, LocalDateTime> {
+
     @Test
-    public void badInput_validate_emptyErrorMessageDisplayed() {
+    public void badInputOnDatePicker_validate_emptyErrorMessageDisplayed() {
         getDatePicker().getElement().setProperty("_inputElementValue", "foo");
-        fireValidatedDomEvent();
+        fireDomEvent("unparsable-change", getDatePicker().getElement());
+        fireUnparsableChangeDomEvent();
         Assert.assertEquals("", testField.getErrorMessage());
     }
 
     @Test
-    public void badInput_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() {
+    public void badInputOnTimePicker_validate_emptyErrorMessageDisplayed() {
+        getTimePicker().getElement().setProperty("_inputElementValue", "foo");
+        fireDomEvent("unparsable-change", getTimePicker().getElement());
+        fireUnparsableChangeDomEvent();
+        Assert.assertEquals("", testField.getErrorMessage());
+    }
+
+    @Test
+    public void badInputOnDatePicker_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() {
+        var errorMessage = "Value has invalid format";
         testField.setI18n(new DateTimePicker.DateTimePickerI18n()
-                .setBadInputErrorMessage("Value has invalid format"));
+                .setBadInputErrorMessage(errorMessage));
         getDatePicker().getElement().setProperty("_inputElementValue", "foo");
-        fireValidatedDomEvent();
-        Assert.assertEquals("Value has invalid format",
-                testField.getErrorMessage());
+        fireDomEvent("unparsable-change", getDatePicker().getElement());
+        fireUnparsableChangeDomEvent();
+        Assert.assertEquals(errorMessage, testField.getErrorMessage());
+    }
+
+    @Test
+    public void badInputOnTimePicker_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() {
+        var errorMessage = "Value has invalid format";
+        testField.setI18n(new DateTimePicker.DateTimePickerI18n()
+                .setBadInputErrorMessage(errorMessage));
+        getTimePicker().getElement().setProperty("_inputElementValue", "foo");
+        fireDomEvent("unparsable-change", getTimePicker().getElement());
+        fireUnparsableChangeDomEvent();
+        Assert.assertEquals(errorMessage, testField.getErrorMessage());
+    }
+
+    @Test
+    public void incompleteInputOnDatePicker_validate_emptyErrorMessageDisplayed() {
+        var picker = getDatePicker();
+        picker.setValue(LocalDate.now());
+        fireUnparsableChangeDomEvent();
+        Assert.assertEquals("", testField.getErrorMessage());
+    }
+
+    @Test
+    public void incompleteInputOnTimePicker_validate_emptyErrorMessageDisplayed() {
+        var picker = getTimePicker();
+        picker.setValue(LocalTime.now());
+        fireUnparsableChangeDomEvent();
+        Assert.assertEquals("", testField.getErrorMessage());
+    }
+
+    @Test
+    public void incompleteInputOnDatePicker_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() {
+        var errorMessage = "Value is incomplete";
+        testField.setI18n(new DateTimePicker.DateTimePickerI18n()
+                .setIncompleteInputErrorMessage(errorMessage));
+        var picker = getDatePicker();
+        picker.setValue(LocalDate.now());
+        fireUnparsableChangeDomEvent();
+        Assert.assertEquals(errorMessage, testField.getErrorMessage());
+    }
+
+    @Test
+    public void incompleteInputOnTimePicker_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() {
+        var errorMessage = "Value is incomplete";
+        testField.setI18n(new DateTimePicker.DateTimePickerI18n()
+                .setIncompleteInputErrorMessage(errorMessage));
+        var picker = getTimePicker();
+        picker.setValue(LocalTime.now());
+        fireUnparsableChangeDomEvent();
+        Assert.assertEquals(errorMessage, testField.getErrorMessage());
+    }
+
+    @Test
+    public void setIncompleteInputErrorMessage_errorMessageIsSet() {
+        var errorMessage = "Value is incomplete";
+        testField.setI18n(new DateTimePicker.DateTimePickerI18n()
+                .setIncompleteInputErrorMessage(errorMessage));
+        Assert.assertEquals(errorMessage,
+                testField.getI18n().getIncompleteInputErrorMessage());
     }
 
     @Test
     public void required_validate_emptyErrorMessageDisplayed() {
         testField.setRequiredIndicatorVisible(true);
         testField.setValue(LocalDateTime.now());
+        fireChangeDomEvent();
         testField.setValue(null);
+        fireChangeDomEvent();
         Assert.assertEquals("", testField.getErrorMessage());
     }
 
@@ -63,7 +139,9 @@ public void required_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() {
         testField.setI18n(new DateTimePicker.DateTimePickerI18n()
                 .setRequiredErrorMessage("Field is required"));
         testField.setValue(LocalDateTime.now());
+        fireChangeDomEvent();
         testField.setValue(null);
+        fireChangeDomEvent();
         Assert.assertEquals("Field is required", testField.getErrorMessage());
     }
 
@@ -71,6 +149,7 @@ public void required_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() {
     public void min_validate_emptyErrorMessageDisplayed() {
         testField.setMin(LocalDateTime.now());
         testField.setValue(LocalDateTime.now().minusDays(1));
+        fireChangeDomEvent();
         Assert.assertEquals("", testField.getErrorMessage());
     }
 
@@ -80,6 +159,7 @@ public void min_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() {
         testField.setI18n(new DateTimePicker.DateTimePickerI18n()
                 .setMinErrorMessage("Value is too small"));
         testField.setValue(LocalDateTime.now().minusDays(1));
+        fireChangeDomEvent();
         Assert.assertEquals("Value is too small", testField.getErrorMessage());
     }
 
@@ -87,6 +167,7 @@ public void min_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() {
     public void max_validate_emptyErrorMessageDisplayed() {
         testField.setMax(LocalDateTime.now());
         testField.setValue(LocalDateTime.now().plusDays(1));
+        fireChangeDomEvent();
         Assert.assertEquals("", testField.getErrorMessage());
     }
 
@@ -96,6 +177,7 @@ public void max_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() {
         testField.setI18n(new DateTimePicker.DateTimePickerI18n()
                 .setMaxErrorMessage("Value is too big"));
         testField.setValue(LocalDateTime.now().plusDays(1));
+        fireChangeDomEvent();
         Assert.assertEquals("Value is too big", testField.getErrorMessage());
     }
 
@@ -106,7 +188,9 @@ public void setI18nAndCustomErrorMessage_validate_customErrorMessageDisplayed()
                 .setRequiredErrorMessage("Field is required"));
         testField.setErrorMessage("Custom error message");
         testField.setValue(LocalDateTime.now());
+        fireChangeDomEvent();
         testField.setValue(null);
+        fireChangeDomEvent();
         Assert.assertEquals("Custom error message",
                 testField.getErrorMessage());
     }
@@ -118,10 +202,14 @@ public void setI18nAndCustomErrorMessage_validate_removeCustomErrorMessage_valid
                 .setRequiredErrorMessage("Field is required"));
         testField.setErrorMessage("Custom error message");
         testField.setValue(LocalDateTime.now());
+        fireChangeDomEvent();
         testField.setValue(null);
+        fireChangeDomEvent();
         testField.setErrorMessage("");
         testField.setValue(LocalDateTime.now());
+        fireChangeDomEvent();
         testField.setValue(null);
+        fireChangeDomEvent();
         Assert.assertEquals("Field is required", testField.getErrorMessage());
     }
 
@@ -132,6 +220,33 @@ public void setInvalid_nestedPickersAreInvalid() {
         Assert.assertTrue(getTimePicker().isInvalid());
     }
 
+    @Test
+    public void setValueProgrammatically_fieldValidatedOnce() {
+        var dateTimePicker = new TestDateTimePicker();
+        dateTimePicker.setValue(LocalDateTime.now());
+        Assert.assertEquals(1, dateTimePicker.getValidationCount());
+    }
+
+    @Test
+    public void clearValueProgrammatically_fieldValidatedOnce() {
+        var dateTimePicker = new TestDateTimePicker();
+        dateTimePicker.setValue(LocalDateTime.now());
+        var validationCount = dateTimePicker.getValidationCount();
+        dateTimePicker.setValue(null);
+        Assert.assertEquals(validationCount + 1,
+                dateTimePicker.getValidationCount());
+    }
+
+    @Test
+    public void setValueProgrammatically_invalidStateIsUpdatedInValueChangeListener() {
+        var isInvalid = new AtomicBoolean();
+        testField.addValueChangeListener(
+                e -> isInvalid.set(e.getSource().isInvalid()));
+        testField.setMax(LocalDateTime.now());
+        testField.setValue(LocalDateTime.now().plusDays(1));
+        Assert.assertTrue(isInvalid.get());
+    }
+
     @Override
     protected DateTimePicker createTestField() {
         return new DateTimePicker();
@@ -145,10 +260,31 @@ private TimePicker getTimePicker() {
         return (TimePicker) SlotUtils.getChildInSlot(testField, "time-picker");
     }
 
-    private void fireValidatedDomEvent() {
-        DomEvent validatedDomEvent = new DomEvent(testField.getElement(),
-                "validated", Json.createObject());
-        testField.getElement().getNode().getFeature(ElementListenerMap.class)
-                .fireEvent(validatedDomEvent);
+    private void fireChangeDomEvent() {
+        fireDomEvent("change", testField.getElement());
+    }
+
+    private void fireUnparsableChangeDomEvent() {
+        fireDomEvent("unparsable-change", testField.getElement());
+    }
+
+    private void fireDomEvent(String eventType, Element element) {
+        var domEvent = new DomEvent(element, eventType, Json.createObject());
+        element.getNode().getFeature(ElementListenerMap.class)
+                .fireEvent(domEvent);
+    }
+
+    private class TestDateTimePicker extends DateTimePicker {
+        private final AtomicInteger validationCount = new AtomicInteger(0);
+
+        @Override
+        protected void validate() {
+            super.validate();
+            validationCount.incrementAndGet();
+        }
+
+        public int getValidationCount() {
+            return validationCount.get();
+        }
     }
 }
diff --git a/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java b/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java
index cb117876162..5a52a53efbd 100644
--- a/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java
+++ b/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java
@@ -146,7 +146,7 @@ public class TimePicker
     private Validator<LocalTime> defaultValidator = (value, context) -> {
         boolean fromComponent = context == null;
 
-        if (unparsableValue != null) {
+        if (isInputUnparsable()) {
             return ValidationResult.error(getI18nErrorMessage(
                     TimePickerI18n::getBadInputErrorMessage));
         }
@@ -362,7 +362,7 @@ public void setLabel(String label) {
     @Override
     public void setValue(LocalTime value) {
         LocalTime oldValue = getValue();
-        if (oldValue == null && value == null && unparsableValue != null) {
+        if (oldValue == null && value == null && isInputUnparsable()) {
             // When the value is programmatically cleared while the field
             // contains an unparsable input, ValueChangeEvent isn't fired,
             // so we need to call setModelValue manually to clear the bad
@@ -473,15 +473,31 @@ private void fireValidationStatusChangeEvent() {
     }
 
     /**
+     * For internal use only.
+     * <p>
      * Returns whether the input element has a value or not.
      *
      * @return <code>true</code> if the input element's value is populated,
      *         <code>false</code> otherwise
+     * @deprecated Since v24.8
      */
+    @Deprecated(since = "24.8")
     protected boolean isInputValuePresent() {
         return !getInputElementValue().isEmpty();
     }
 
+    /**
+     * For internal use only.
+     * <p>
+     * Returns whether the input value is unparsable.
+     *
+     * @return <code>true</code> if the input element's value is populated and
+     *         unparsable, <code>false</code> otherwise
+     */
+    protected final boolean isInputUnparsable() {
+        return unparsableValue != null;
+    }
+
     /**
      * Gets the value of the input element. This value is updated on the server
      * when the web component dispatches a `change` or `unparsable-change`


Reviews:
user: login: vursen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T15:01:38Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T20:10:21Z
user: login: vursen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T04:07:06Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:09:43Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:10:41Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:10:47Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:51:39Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:52:56Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:18:53Z
user: login: vursen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T03:59:22Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T05:24:07Z
user: login: vursen
body: What about [this case](https://github.com/vaadin/flow-components/compare/proto/date-time-picker/unparsable-incomplete-change#diff-ab78f9fecfbab9d2eba396796f0defc447d88eb48e752cafd9bc231000a0b4b1R395-R399)? Was there a specific decision to exclude it?
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T08:04:28Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T08:42:29Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T08:44:23Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T08:46:47Z
user: login: vursen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T15:24:40Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T18:36:44Z
user: login: vursen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T06:00:51Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T08:03:13Z
user: login: vursen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T08:44:56Z
user: login: ugur-vaadin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T08:58:07Z
user: login: vursen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T09:02:24Z
user: login: vursen
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T09:02:35Z

Review comments:
diff_hunk: @@ -343,23 +443,13 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) {
      */
     @Override
     public void setValue(LocalDateTime value) {
-        LocalDateTime oldValue = getValue();
-
         value = sanitizeValue(value);
+        synchronizeChildComponentValues(value);
         super.setValue(value);
-
-        boolean isInputValuePresent = timePicker.isInputValuePresent()
-                || datePicker.isInputValuePresent();
-        boolean isValueRemainedEmpty = valueEquals(oldValue, getEmptyValue())
-                && valueEquals(value, getEmptyValue());
-        if (isValueRemainedEmpty && isInputValuePresent) {
-            // Clear the input elements from possible bad input.
-            synchronizeChildComponentValues(value);
-            fireEvent(new ClientValidatedEvent(this, false));
-        } else {
-            synchronizeChildComponentValues(value);
-        }
-
+        // Notify the server in order to use the formatted values in validation
+        pendingInputElementValueSyncs++;
+        getElement().executeJs(
+                "this.dispatchEvent(new CustomEvent('value-programmatically-set'));");
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
user: login: vursen
body: Could you please explain why this additional event is necessary? What scenario is it intended to cover?
created_at: 2025-04-29T14:55:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -343,23 +443,13 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) {
      */
     @Override
     public void setValue(LocalDateTime value) {
-        LocalDateTime oldValue = getValue();
-
         value = sanitizeValue(value);
+        synchronizeChildComponentValues(value);
         super.setValue(value);
-
-        boolean isInputValuePresent = timePicker.isInputValuePresent()
-                || datePicker.isInputValuePresent();
-        boolean isValueRemainedEmpty = valueEquals(oldValue, getEmptyValue())
-                && valueEquals(value, getEmptyValue());
-        if (isValueRemainedEmpty && isInputValuePresent) {
-            // Clear the input elements from possible bad input.
-            synchronizeChildComponentValues(value);
-            fireEvent(new ClientValidatedEvent(this, false));
-        } else {
-            synchronizeChildComponentValues(value);
-        }
-
+        // Notify the server in order to use the formatted values in validation
+        pendingInputElementValueSyncs++;
+        getElement().executeJs(
+                "this.dispatchEvent(new CustomEvent('value-programmatically-set'));");
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
user: login: vursen
body: This approach might not be correct since `isInvalid()` will still report `true` on the server side until the next round-trip occurs. I explored this a bit on my own and may have found a solution that avoids a round-trip: https://github.com/vaadin/flow-components/commit/cbd6285522e313b857b2ff52dbde7df0e4d486b2. Please let me know if you see any issues with it.
created_at: 2025-04-29T15:01:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -343,23 +443,13 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) {
      */
     @Override
     public void setValue(LocalDateTime value) {
-        LocalDateTime oldValue = getValue();
-
         value = sanitizeValue(value);
+        synchronizeChildComponentValues(value);
         super.setValue(value);
-
-        boolean isInputValuePresent = timePicker.isInputValuePresent()
-                || datePicker.isInputValuePresent();
-        boolean isValueRemainedEmpty = valueEquals(oldValue, getEmptyValue())
-                && valueEquals(value, getEmptyValue());
-        if (isValueRemainedEmpty && isInputValuePresent) {
-            // Clear the input elements from possible bad input.
-            synchronizeChildComponentValues(value);
-            fireEvent(new ClientValidatedEvent(this, false));
-        } else {
-            synchronizeChildComponentValues(value);
-        }
-
+        // Notify the server in order to use the formatted values in validation
+        pendingInputElementValueSyncs++;
+        getElement().executeJs(
+                "this.dispatchEvent(new CustomEvent('value-programmatically-set'));");
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
user: login: ugur-vaadin
body: This solution is quite good overall. Updated the PR based in this approach and applied a few fixes.
created_at: 2025-05-05T20:10:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -309,16 +420,120 @@ public void detach_hide_attach_showAndInvalidate_preservesInvalidState() {
 
     @Test
     public void clientSideInvalidStateIsNotPropagatedToServer() {
-        // Make the field invalid
-        $("button").id(REQUIRED_BUTTON).click();
-        dateInput.sendKeys(Keys.TAB);
-        timeInput.sendKeys(Keys.TAB);
+        setFieldInvalid();
 
         executeScript("arguments[0].invalid = false", testField);
 
         assertServerInvalid();
     }
 
+    @Test
+    public void triggerBlurWithNoChange_fieldNotValidated() {
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys(Keys.TAB);
+        assertValidationCount(0);
+    }
+
+    @Test
+    public void initiallyEmpty_setValidValue_fieldValidatedOnce() {
+        dateInput.sendKeys("1/1/2000");
+        dateInput.sendKeys(Keys.ENTER);
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys("10:00");
+        timeInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void initiallyEmpty_setInvalidValue_fieldNotValidatedOnce() {
+        dateInput.sendKeys("Invalid");
+        dateInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void initiallyValidValue_clearValue_fieldValidatedOnce() {
+        dateInput.sendKeys("1/1/2000");
+        dateInput.sendKeys(Keys.ENTER);
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys("10:00");
+        timeInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+        clearInputValue();
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void initiallyValidValue_changeValue_fieldValidatedOnce() {
+        dateInput.sendKeys("1/1/2000");
+        dateInput.sendKeys(Keys.ENTER);
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys("10:00");
+        timeInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+        dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE);
+        dateInput.sendKeys("1/1/2001");
+        dateInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void initiallyInvalidValue_changeInvalidValue_fieldValidatedOnce() {
+        dateInput.sendKeys("Invalid");
+        dateInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+        dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE);
+        dateInput.sendKeys("Not a date");
+        dateInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void initiallyInvalidValue_setValidValue_fieldValidatedOnce() {
+        dateInput.sendKeys("Invalid");
+        dateInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+        dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE);
+        dateInput.sendKeys("1/1/2000");
+        timeInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE);
+        timeInput.sendKeys("10:00");
+        timeInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void initiallyInvalidValue_clearValue_fieldValidatedOnce() {
+        dateInput.sendKeys("Invalid");
+        dateInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+        clearInputValue();
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void max_setDateOutOfRange_fieldValidatedOnce() {
+        $("input").id(MAX_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER);
+        setInputValue(dateInput, "3/3/2000");
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void setValueProgrammatically_fieldValidatedOnce() {
+        clickElementWithJs(SET_VALUE_PROGRAMMATICALLY);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void clearAndSetValueProgrammatically_fieldValidatedTwice() {
+        clickElementWithJs(CLEAR_AND_SET_VALUE_PROGRAMMATICALLY);
+        assertValidationCount(2);
+    }
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
user: login: vursen
body: This could be tested using unit tests in `datetimepicker/validation/BasicValidationTest.java`
created_at: 2025-05-06T03:48:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -309,16 +420,120 @@ public void detach_hide_attach_showAndInvalidate_preservesInvalidState() {
 
     @Test
     public void clientSideInvalidStateIsNotPropagatedToServer() {
-        // Make the field invalid
-        $("button").id(REQUIRED_BUTTON).click();
-        dateInput.sendKeys(Keys.TAB);
-        timeInput.sendKeys(Keys.TAB);
+        setFieldInvalid();
 
         executeScript("arguments[0].invalid = false", testField);
 
         assertServerInvalid();
     }
 
+    @Test
+    public void triggerBlurWithNoChange_fieldNotValidated() {
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys(Keys.TAB);
+        assertValidationCount(0);
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
user: login: vursen
body: I suggest combining the tests that assert validity with those that assert the validation count. The validation count should be asserted in every test that verifies validity
created_at: 2025-05-06T03:52:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -343,23 +374,14 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) {
      */
     @Override
     public void setValue(LocalDateTime value) {
-        LocalDateTime oldValue = getValue();
-
+        var oldValue = getValue();
         value = sanitizeValue(value);
         super.setValue(value);
-
-        boolean isInputValuePresent = timePicker.isInputValuePresent()
-                || datePicker.isInputValuePresent();
-        boolean isValueRemainedEmpty = valueEquals(oldValue, getEmptyValue())
-                && valueEquals(value, getEmptyValue());
-        if (isValueRemainedEmpty && isInputValuePresent) {
-            // Clear the input elements from possible bad input.
-            synchronizeChildComponentValues(value);
-            fireEvent(new ClientValidatedEvent(this, false));
-        } else {
-            synchronizeChildComponentValues(value);
-        }
-
+        var shouldFireValidationStatusChangeEvent = oldValue == null
+                && value == null
+                && (isInputUnparsable() || isInputIncomplete());
+        synchronizeChildComponentValues(value);
+        validate(shouldFireValidationStatusChangeEvent);
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
user: login: vursen
body: Components must be validated before user-defined `ValueChangeEvent` listeners are triggered to ensure that those listeners have access to the updated invalid state. That's why `validate()` was originally called inside `addValueChangeListener`.
created_at: 2025-05-06T03:55:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -327,6 +353,11 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) {
         setLocale(locale);
     }
 
+    private void addValidationListeners() {
+        getElement().addEventListener("change", e -> validate(true));
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
user: login: vursen
body: Calling `validate(true)` on change will result in duplicate validation when using Binder, since Binder already listens to `ValueChangeEvent` and triggers validation automatically. To prevent similar regressions in the future, each test in `BinderValidationIT` should assert the validation count.
created_at: 2025-05-06T03:59:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -193,7 +211,7 @@ public void setValue_clearValue_assertValidity() {
 
     @Test
     public void badInput_changeValue_assertValidity() {
-        setInputValue(dateInput, "INVALID");
+        setFieldInvalid();
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
user: login: vursen
body: Let's use explicit `setInputValue` calls throughout to avoid potential side-effects and make the test flow more obvious.
created_at: 2025-05-06T04:03:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,17 +59,17 @@ public void fieldIsInitiallyValid() {
     public void required_triggerDateInputBlur_assertValidity() {
         dateInput.sendKeys(Keys.TAB);
         timeInput.sendKeys(Keys.TAB);
-        assertServerInvalid();
-        assertClientInvalid();
-        assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java
user: login: vursen
body: Each test case should also assert the validation count.
created_at: 2025-05-06T04:04:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -193,7 +211,7 @@ public void setValue_clearValue_assertValidity() {
 
     @Test
     public void badInput_changeValue_assertValidity() {
-        setInputValue(dateInput, "INVALID");
+        setFieldInvalid();
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
user: login: ugur-vaadin
body: Updated to set the values explicitly.
created_at: 2025-05-06T07:09:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -309,16 +420,120 @@ public void detach_hide_attach_showAndInvalidate_preservesInvalidState() {
 
     @Test
     public void clientSideInvalidStateIsNotPropagatedToServer() {
-        // Make the field invalid
-        $("button").id(REQUIRED_BUTTON).click();
-        dateInput.sendKeys(Keys.TAB);
-        timeInput.sendKeys(Keys.TAB);
+        setFieldInvalid();
 
         executeScript("arguments[0].invalid = false", testField);
 
         assertServerInvalid();
     }
 
+    @Test
+    public void triggerBlurWithNoChange_fieldNotValidated() {
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys(Keys.TAB);
+        assertValidationCount(0);
+    }
+
+    @Test
+    public void initiallyEmpty_setValidValue_fieldValidatedOnce() {
+        dateInput.sendKeys("1/1/2000");
+        dateInput.sendKeys(Keys.ENTER);
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys("10:00");
+        timeInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void initiallyEmpty_setInvalidValue_fieldNotValidatedOnce() {
+        dateInput.sendKeys("Invalid");
+        dateInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void initiallyValidValue_clearValue_fieldValidatedOnce() {
+        dateInput.sendKeys("1/1/2000");
+        dateInput.sendKeys(Keys.ENTER);
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys("10:00");
+        timeInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+        clearInputValue();
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void initiallyValidValue_changeValue_fieldValidatedOnce() {
+        dateInput.sendKeys("1/1/2000");
+        dateInput.sendKeys(Keys.ENTER);
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys("10:00");
+        timeInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+        dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE);
+        dateInput.sendKeys("1/1/2001");
+        dateInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void initiallyInvalidValue_changeInvalidValue_fieldValidatedOnce() {
+        dateInput.sendKeys("Invalid");
+        dateInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+        dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE);
+        dateInput.sendKeys("Not a date");
+        dateInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void initiallyInvalidValue_setValidValue_fieldValidatedOnce() {
+        dateInput.sendKeys("Invalid");
+        dateInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+        dateInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE);
+        dateInput.sendKeys("1/1/2000");
+        timeInput.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME), Keys.BACK_SPACE);
+        timeInput.sendKeys("10:00");
+        timeInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void initiallyInvalidValue_clearValue_fieldValidatedOnce() {
+        dateInput.sendKeys("Invalid");
+        dateInput.sendKeys(Keys.ENTER);
+        assertValidationCount(1);
+        clearInputValue();
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void max_setDateOutOfRange_fieldValidatedOnce() {
+        $("input").id(MAX_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER);
+        setInputValue(dateInput, "3/3/2000");
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void setValueProgrammatically_fieldValidatedOnce() {
+        clickElementWithJs(SET_VALUE_PROGRAMMATICALLY);
+        assertValidationCount(1);
+    }
+
+    @Test
+    public void clearAndSetValueProgrammatically_fieldValidatedTwice() {
+        clickElementWithJs(CLEAR_AND_SET_VALUE_PROGRAMMATICALLY);
+        assertValidationCount(2);
+    }
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
user: login: ugur-vaadin
body: Removed from ITs and converted to unit tests.
created_at: 2025-05-06T07:10:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -309,16 +420,120 @@ public void detach_hide_attach_showAndInvalidate_preservesInvalidState() {
 
     @Test
     public void clientSideInvalidStateIsNotPropagatedToServer() {
-        // Make the field invalid
-        $("button").id(REQUIRED_BUTTON).click();
-        dateInput.sendKeys(Keys.TAB);
-        timeInput.sendKeys(Keys.TAB);
+        setFieldInvalid();
 
         executeScript("arguments[0].invalid = false", testField);
 
         assertServerInvalid();
     }
 
+    @Test
+    public void triggerBlurWithNoChange_fieldNotValidated() {
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys(Keys.TAB);
+        assertValidationCount(0);
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
user: login: ugur-vaadin
body: Added validation count assertions.
created_at: 2025-05-06T07:10:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,17 +59,17 @@ public void fieldIsInitiallyValid() {
     public void required_triggerDateInputBlur_assertValidity() {
         dateInput.sendKeys(Keys.TAB);
         timeInput.sendKeys(Keys.TAB);
-        assertServerInvalid();
-        assertClientInvalid();
-        assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BinderValidationIT.java
user: login: ugur-vaadin
body: Added count assertions.
created_at: 2025-05-06T07:51:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -327,6 +353,11 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) {
         setLocale(locale);
     }
 
+    private void addValidationListeners() {
+        getElement().addEventListener("change", e -> validate(true));
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
user: login: ugur-vaadin
body: The validation count seems to be in line with regular validation in my testing. Can you provide some example cases where the binder validation count is doubled?
created_at: 2025-05-06T07:52:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -343,23 +374,14 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) {
      */
     @Override
     public void setValue(LocalDateTime value) {
-        LocalDateTime oldValue = getValue();
-
+        var oldValue = getValue();
         value = sanitizeValue(value);
         super.setValue(value);
-
-        boolean isInputValuePresent = timePicker.isInputValuePresent()
-                || datePicker.isInputValuePresent();
-        boolean isValueRemainedEmpty = valueEquals(oldValue, getEmptyValue())
-                && valueEquals(value, getEmptyValue());
-        if (isValueRemainedEmpty && isInputValuePresent) {
-            // Clear the input elements from possible bad input.
-            synchronizeChildComponentValues(value);
-            fireEvent(new ClientValidatedEvent(this, false));
-        } else {
-            synchronizeChildComponentValues(value);
-        }
-
+        var shouldFireValidationStatusChangeEvent = oldValue == null
+                && value == null
+                && (isInputUnparsable() || isInputIncomplete());
+        synchronizeChildComponentValues(value);
+        validate(shouldFireValidationStatusChangeEvent);
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
user: login: ugur-vaadin
body: Changing the order seems to handle this issue. Added a unit test that should cover this case.
created_at: 2025-05-06T08:18:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -343,23 +374,14 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) {
      */
     @Override
     public void setValue(LocalDateTime value) {
-        LocalDateTime oldValue = getValue();
-
+        var oldValue = getValue();
         value = sanitizeValue(value);
         super.setValue(value);
-
-        boolean isInputValuePresent = timePicker.isInputValuePresent()
-                || datePicker.isInputValuePresent();
-        boolean isValueRemainedEmpty = valueEquals(oldValue, getEmptyValue())
-                && valueEquals(value, getEmptyValue());
-        if (isValueRemainedEmpty && isInputValuePresent) {
-            // Clear the input elements from possible bad input.
-            synchronizeChildComponentValues(value);
-            fireEvent(new ClientValidatedEvent(this, false));
-        } else {
-            synchronizeChildComponentValues(value);
-        }
-
+        var shouldFireValidationStatusChangeEvent = oldValue == null
+                && value == null
+                && (isInputUnparsable() || isInputIncomplete());
+        synchronizeChildComponentValues(value);
+        validate(shouldFireValidationStatusChangeEvent);
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
user: login: vursen
body: I'm not sure... it looks like validation is now running with the previous value.
created_at: 2025-05-07T03:59:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -343,23 +374,14 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) {
      */
     @Override
     public void setValue(LocalDateTime value) {
-        LocalDateTime oldValue = getValue();
-
+        var oldValue = getValue();
         value = sanitizeValue(value);
         super.setValue(value);
-
-        boolean isInputValuePresent = timePicker.isInputValuePresent()
-                || datePicker.isInputValuePresent();
-        boolean isValueRemainedEmpty = valueEquals(oldValue, getEmptyValue())
-                && valueEquals(value, getEmptyValue());
-        if (isValueRemainedEmpty && isInputValuePresent) {
-            // Clear the input elements from possible bad input.
-            synchronizeChildComponentValues(value);
-            fireEvent(new ClientValidatedEvent(this, false));
-        } else {
-            synchronizeChildComponentValues(value);
-        }
-
+        var shouldFireValidationStatusChangeEvent = oldValue == null
+                && value == null
+                && (isInputUnparsable() || isInputIncomplete());
+        synchronizeChildComponentValues(value);
+        validate(shouldFireValidationStatusChangeEvent);
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
user: login: ugur-vaadin
body: You are absolutely right. The test passing was a coincidence. I reverted the `change` and replaced change event listener with value change listener.
created_at: 2025-05-07T05:24:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -668,14 +668,28 @@ private void fireValidationStatusChangeEvent() {
 
     /**
      * Returns whether the input element has a value or not.
+     * <p>
+     * For internal use only.
      *
      * @return <code>true</code> if the input element's value is populated,
      *         <code>false</code> otherwise
+     * @deprecated Since v24.8
      */
+    @Deprecated(since = "24.8")
     protected boolean isInputValuePresent() {
         return !getInputElementValue().isEmpty();
     }
 
+    /**
+     * Returns whether the input value is unparsable.
+     *
+     * @return <code>true</code> if the input element's value is populated and
+     *         unparsable, <code>false</code> otherwise
+     */
+    protected boolean isInputUnparsable() {
path: vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java
user: login: vursen
body: Let's mention that this method is internal in the JavaDoc and add `final` to prevent overrides.

```suggestion
    protected final boolean isInputUnparsable() {
```

And do the same for TimePicker.
created_at: 2025-05-14T07:43:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,35 +29,105 @@
 import com.vaadin.flow.component.shared.SlotUtils;
 import com.vaadin.flow.component.timepicker.TimePicker;
 import com.vaadin.flow.dom.DomEvent;
+import com.vaadin.flow.dom.Element;
 import com.vaadin.flow.internal.nodefeature.ElementListenerMap;
 import com.vaadin.tests.validation.AbstractBasicValidationTest;
 
 import elemental.json.Json;
 
 public class BasicValidationTest
         extends AbstractBasicValidationTest<DateTimePicker, LocalDateTime> {
+
     @Test
-    public void badInput_validate_emptyErrorMessageDisplayed() {
+    public void badInputOnDatePicker_validate_emptyErrorMessageDisplayed() {
         getDatePicker().getElement().setProperty("_inputElementValue", "foo");
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java
user: login: vursen
body: It should fire the unparsable-change event on date-picker too.
created_at: 2025-05-14T07:49:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,148 +70,170 @@ public void required_triggerBlur_assertValidity() {
 
         dateInput.sendKeys(Keys.TAB);
         timeInput.sendKeys(Keys.TAB);
-        assertServerInvalid();
-        assertClientInvalid();
-        assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
+        assertValidationCount(0);
+    }
+
+    @Test
+    public void required_changeInputTemporarily_triggerBlur_assertValidity() {
+        $("button").id(REQUIRED_BUTTON).click();
+        dateInput.sendKeys("1", Keys.BACK_SPACE, Keys.ENTER);
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys(Keys.TAB);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
+        assertValidationCount(0);
     }
 
     @Test
     public void required_changeValue_assertValidity() {
         $("button").id(REQUIRED_BUTTON).click();
 
-        setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "12:00");
+        setValue("1/1/2000", "12:00");
         assertServerValid();
         assertClientValid();
         assertErrorMessage("");
+        assertValidationCount(1);
+
+        setInputValue(timeInput, "");
+        assertServerInvalid();
+        assertServerInvalid();
+        assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(dateInput, "");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(timeInput, "");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(0);
 
-        setInputValue(dateInput, "INVALID");
         setInputValue(timeInput, "INVALID");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "");
         setInputValue(timeInput, "");
         timeInput.sendKeys(Keys.TAB);
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     @Test
     public void min_changeValue_assertValidity() {
         $("input").id(MIN_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER);
 
-        setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "11:00");
+        setValue("1/1/2000", "11:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(2);
 
         setInputValue(dateInput, "2/2/2000");
-        setInputValue(timeInput, "11:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "12:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "13:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "3/3/2000");
-        setInputValue(timeInput, "11:00");
+        setValue("3/3/2000", "11:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(2);
     }
 
     @Test
     public void max_changeDateInputValue_assertValidity() {
         $("input").id(MAX_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER);
 
         setInputValue(dateInput, "3/3/2000");
-        setInputValue(timeInput, "13:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
-        setInputValue(timeInput, "13:00");
+        setValue("2/2/2000", "13:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(2);
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
user: login: vursen
body: Could you explain why this results in 2 validation passes?
created_at: 2025-05-14T07:54:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,148 +70,170 @@ public void required_triggerBlur_assertValidity() {
 
         dateInput.sendKeys(Keys.TAB);
         timeInput.sendKeys(Keys.TAB);
-        assertServerInvalid();
-        assertClientInvalid();
-        assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
+        assertValidationCount(0);
+    }
+
+    @Test
+    public void required_changeInputTemporarily_triggerBlur_assertValidity() {
+        $("button").id(REQUIRED_BUTTON).click();
+        dateInput.sendKeys("1", Keys.BACK_SPACE, Keys.ENTER);
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys(Keys.TAB);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
+        assertValidationCount(0);
     }
 
     @Test
     public void required_changeValue_assertValidity() {
         $("button").id(REQUIRED_BUTTON).click();
 
-        setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "12:00");
+        setValue("1/1/2000", "12:00");
         assertServerValid();
         assertClientValid();
         assertErrorMessage("");
+        assertValidationCount(1);
+
+        setInputValue(timeInput, "");
+        assertServerInvalid();
+        assertServerInvalid();
+        assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(dateInput, "");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(timeInput, "");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(0);
 
-        setInputValue(dateInput, "INVALID");
         setInputValue(timeInput, "INVALID");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "");
         setInputValue(timeInput, "");
         timeInput.sendKeys(Keys.TAB);
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     @Test
     public void min_changeValue_assertValidity() {
         $("input").id(MIN_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER);
 
-        setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "11:00");
+        setValue("1/1/2000", "11:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(2);
 
         setInputValue(dateInput, "2/2/2000");
-        setInputValue(timeInput, "11:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "12:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "13:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "3/3/2000");
-        setInputValue(timeInput, "11:00");
+        setValue("3/3/2000", "11:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(2);
     }
 
     @Test
     public void max_changeDateInputValue_assertValidity() {
         $("input").id(MAX_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER);
 
         setInputValue(dateInput, "3/3/2000");
-        setInputValue(timeInput, "13:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
-        setInputValue(timeInput, "13:00");
+        setValue("2/2/2000", "13:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(2);
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
user: login: ugur-vaadin
body: It sets the value for the date picker first, validates, and validates once more when the time picker value changes.
created_at: 2025-05-14T08:42:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,35 +29,105 @@
 import com.vaadin.flow.component.shared.SlotUtils;
 import com.vaadin.flow.component.timepicker.TimePicker;
 import com.vaadin.flow.dom.DomEvent;
+import com.vaadin.flow.dom.Element;
 import com.vaadin.flow.internal.nodefeature.ElementListenerMap;
 import com.vaadin.tests.validation.AbstractBasicValidationTest;
 
 import elemental.json.Json;
 
 public class BasicValidationTest
         extends AbstractBasicValidationTest<DateTimePicker, LocalDateTime> {
+
     @Test
-    public void badInput_validate_emptyErrorMessageDisplayed() {
+    public void badInputOnDatePicker_validate_emptyErrorMessageDisplayed() {
         getDatePicker().getElement().setProperty("_inputElementValue", "foo");
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationTest.java
user: login: ugur-vaadin
body: Done.
created_at: 2025-05-14T08:44:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -668,14 +668,28 @@ private void fireValidationStatusChangeEvent() {
 
     /**
      * Returns whether the input element has a value or not.
+     * <p>
+     * For internal use only.
      *
      * @return <code>true</code> if the input element's value is populated,
      *         <code>false</code> otherwise
+     * @deprecated Since v24.8
      */
+    @Deprecated(since = "24.8")
     protected boolean isInputValuePresent() {
         return !getInputElementValue().isEmpty();
     }
 
+    /**
+     * Returns whether the input value is unparsable.
+     *
+     * @return <code>true</code> if the input element's value is populated and
+     *         unparsable, <code>false</code> otherwise
+     */
+    protected boolean isInputUnparsable() {
path: vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java
user: login: ugur-vaadin
body: Done.
created_at: 2025-05-14T08:46:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,148 +70,170 @@ public void required_triggerBlur_assertValidity() {
 
         dateInput.sendKeys(Keys.TAB);
         timeInput.sendKeys(Keys.TAB);
-        assertServerInvalid();
-        assertClientInvalid();
-        assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
+        assertValidationCount(0);
+    }
+
+    @Test
+    public void required_changeInputTemporarily_triggerBlur_assertValidity() {
+        $("button").id(REQUIRED_BUTTON).click();
+        dateInput.sendKeys("1", Keys.BACK_SPACE, Keys.ENTER);
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys(Keys.TAB);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
+        assertValidationCount(0);
     }
 
     @Test
     public void required_changeValue_assertValidity() {
         $("button").id(REQUIRED_BUTTON).click();
 
-        setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "12:00");
+        setValue("1/1/2000", "12:00");
         assertServerValid();
         assertClientValid();
         assertErrorMessage("");
+        assertValidationCount(1);
+
+        setInputValue(timeInput, "");
+        assertServerInvalid();
+        assertServerInvalid();
+        assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(dateInput, "");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(timeInput, "");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(0);
 
-        setInputValue(dateInput, "INVALID");
         setInputValue(timeInput, "INVALID");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "");
         setInputValue(timeInput, "");
         timeInput.sendKeys(Keys.TAB);
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     @Test
     public void min_changeValue_assertValidity() {
         $("input").id(MIN_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER);
 
-        setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "11:00");
+        setValue("1/1/2000", "11:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(2);
 
         setInputValue(dateInput, "2/2/2000");
-        setInputValue(timeInput, "11:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "12:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "13:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "3/3/2000");
-        setInputValue(timeInput, "11:00");
+        setValue("3/3/2000", "11:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(2);
     }
 
     @Test
     public void max_changeDateInputValue_assertValidity() {
         $("input").id(MAX_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER);
 
         setInputValue(dateInput, "3/3/2000");
-        setInputValue(timeInput, "13:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
-        setInputValue(timeInput, "13:00");
+        setValue("2/2/2000", "13:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(2);
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
user: login: vursen
body: It's because we validate the date eagerly, right? In that case, I'd suggest splitting this into two separate assertions:

1. Enter date, assert that the field is already marked as invalid
2. Enter time, assert that the field remains invalid
created_at: 2025-05-14T15:24:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,148 +70,170 @@ public void required_triggerBlur_assertValidity() {
 
         dateInput.sendKeys(Keys.TAB);
         timeInput.sendKeys(Keys.TAB);
-        assertServerInvalid();
-        assertClientInvalid();
-        assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
+        assertValidationCount(0);
+    }
+
+    @Test
+    public void required_changeInputTemporarily_triggerBlur_assertValidity() {
+        $("button").id(REQUIRED_BUTTON).click();
+        dateInput.sendKeys("1", Keys.BACK_SPACE, Keys.ENTER);
+        dateInput.sendKeys(Keys.TAB);
+        timeInput.sendKeys(Keys.TAB);
+        assertServerValid();
+        assertClientValid();
+        assertErrorMessage(null);
+        assertValidationCount(0);
     }
 
     @Test
     public void required_changeValue_assertValidity() {
         $("button").id(REQUIRED_BUTTON).click();
 
-        setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "12:00");
+        setValue("1/1/2000", "12:00");
         assertServerValid();
         assertClientValid();
         assertErrorMessage("");
+        assertValidationCount(1);
+
+        setInputValue(timeInput, "");
+        assertServerInvalid();
+        assertServerInvalid();
+        assertErrorMessage(INCOMPLETE_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(dateInput, "");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
 
         setInputValue(timeInput, "");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(0);
 
-        setInputValue(dateInput, "INVALID");
         setInputValue(timeInput, "INVALID");
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(BAD_INPUT_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "");
         setInputValue(timeInput, "");
         timeInput.sendKeys(Keys.TAB);
         assertServerInvalid();
         assertClientInvalid();
         assertErrorMessage(REQUIRED_ERROR_MESSAGE);
+        assertValidationCount(1);
     }
 
     @Test
     public void min_changeValue_assertValidity() {
         $("input").id(MIN_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER);
 
-        setInputValue(dateInput, "1/1/2000");
-        setInputValue(timeInput, "11:00");
+        setValue("1/1/2000", "11:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(2);
 
         setInputValue(dateInput, "2/2/2000");
-        setInputValue(timeInput, "11:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MIN_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "12:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
         setInputValue(timeInput, "13:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "3/3/2000");
-        setInputValue(timeInput, "11:00");
+        setValue("3/3/2000", "11:00");
         assertClientValid();
         assertServerValid();
         assertErrorMessage("");
+        assertValidationCount(2);
     }
 
     @Test
     public void max_changeDateInputValue_assertValidity() {
         $("input").id(MAX_INPUT).sendKeys("2000-02-02T12:00", Keys.ENTER);
 
         setInputValue(dateInput, "3/3/2000");
-        setInputValue(timeInput, "13:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(1);
 
-        setInputValue(dateInput, "2/2/2000");
-        setInputValue(timeInput, "13:00");
+        setValue("2/2/2000", "13:00");
         assertClientInvalid();
         assertServerInvalid();
         assertErrorMessage(MAX_ERROR_MESSAGE);
+        assertValidationCount(2);
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
user: login: ugur-vaadin
body: Done.
created_at: 2025-05-14T18:36:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -319,11 +349,17 @@ public void incompleteInput_changeToValidValue_assertValidity() {
         setInputValue(dateInput, "1/1/2000");
         resetValidationCount();
 
-        setValue("1/1/2001", "10:00");
+        setInputValue(dateInput, "1/1/2001");
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
user: login: vursen
body: Is this needed given that the date input is already set by the previous lines?
created_at: 2025-05-15T05:59:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -319,11 +349,17 @@ public void incompleteInput_changeToValidValue_assertValidity() {
         setInputValue(dateInput, "1/1/2000");
         resetValidationCount();
 
-        setValue("1/1/2001", "10:00");
+        setInputValue(dateInput, "1/1/2001");
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datetimepicker/validation/BasicValidationIT.java
user: login: ugur-vaadin
body: Removed.
created_at: 2025-05-15T08:03:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -327,6 +359,36 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) {
         setLocale(locale);
     }
 
+    private void addValidationListeners() {
+        addValueChangeListener(e -> validate());
+        getElement().addEventListener("unparsable-change", e -> validate(true));
+        // Add listeners to invalidate a required DateTimePicker when:
+        // 1. It's initially empty
+        // 2. The user selects a value in one picker (date or time)
+        // 3. The user then removes that value
+        // 4. The user leaves the field without proceeding to the other picker
+        datePicker.addValueChangeListener(event -> handlePickerValueChange());
+        timePicker.addValueChangeListener(event -> handlePickerValueChange());
+    }
+
+    private void handlePickerValueChange() {
+        // Only handle picker value change if required.
+        if (!isRequiredIndicatorVisible()) {
+            return;
+        }
+        // If the component is already invalid, any picker value change will
+        // trigger the listener for either unparsable-change event or value
+        // change event. Therefore, the component will be validated and there is
+        // no need to validate here.
+        if (isInvalid()) {
+            return;
+        }
+        if (isEmpty() && timePicker.isEmpty() && datePicker.isEmpty()
+                && !isInputValuePresent()) {
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
user: login: vursen
body: It seems `isInputUnparsable()` would also work:

```suggestion
                && !isInputUnparsable()) {
```

This would allow us to deprecate `isInputValuePresent()` as you did before.
created_at: 2025-05-21T08:44:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -327,6 +359,36 @@ public DateTimePicker(LocalDateTime initialDateTime, Locale locale) {
         setLocale(locale);
     }
 
+    private void addValidationListeners() {
+        addValueChangeListener(e -> validate());
+        getElement().addEventListener("unparsable-change", e -> validate(true));
+        // Add listeners to invalidate a required DateTimePicker when:
+        // 1. It's initially empty
+        // 2. The user selects a value in one picker (date or time)
+        // 3. The user then removes that value
+        // 4. The user leaves the field without proceeding to the other picker
+        datePicker.addValueChangeListener(event -> handlePickerValueChange());
+        timePicker.addValueChangeListener(event -> handlePickerValueChange());
+    }
+
+    private void handlePickerValueChange() {
+        // Only handle picker value change if required.
+        if (!isRequiredIndicatorVisible()) {
+            return;
+        }
+        // If the component is already invalid, any picker value change will
+        // trigger the listener for either unparsable-change event or value
+        // change event. Therefore, the component will be validated and there is
+        // no need to validate here.
+        if (isInvalid()) {
+            return;
+        }
+        if (isEmpty() && timePicker.isEmpty() && datePicker.isEmpty()
+                && !isInputValuePresent()) {
path: vaadin-date-time-picker-flow-parent/vaadin-date-time-picker-flow/src/main/java/com/vaadin/flow/component/datetimepicker/DateTimePicker.java
user: login: ugur-vaadin
body: Deprecated `isInputValuePresent` in the individual pickers and replaced it with `isInputUnparsable` in `DateTimePicker`.
created_at: 2025-05-21T08:58:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -667,15 +667,31 @@ private void fireValidationStatusChangeEvent() {
     }
 
     /**
+     * For internal use only.
+     * <p>
      * Returns whether the input element has a value or not.
      *
      * @return <code>true</code> if the input element's value is populated,
      *         <code>false</code> otherwise
+     * @deprecated Since v24.8
      */
-    protected boolean isInputValuePresent() {
+    @Deprecated(since = "24.8")
+    protected final boolean isInputValuePresent() {
path: vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java
user: login: vursen
body: ```suggestion
    protected boolean isInputValuePresent() {
```
created_at: 2025-05-21T09:02:24Z
author_association: CONTRIBUTOR

PR comments:
user: login: ugur-vaadin
created_at: 2025-05-14T10:08:15Z
body: > What about [this case](https://github.com/vaadin/flow-components/compare/proto/date-time-picker/unparsable-incomplete-change#diff-ab78f9fecfbab9d2eba396796f0defc447d88eb48e752cafd9bc231000a0b4b1R395-R399)? Was there a specific decision to exclude it?

I didn't get this part. Is the description missing a case?
author_association: CONTRIBUTOR
user: login: vursen
created_at: 2025-05-15T06:11:52Z
body: > I didn't get this part. Is the description missing a case?

I meant the case where

1. DateTimePicker is required and initially empty
2. User selects a value in one picker (date or time)
3. User removes that value
4. User leaves the field
5. Result: should the field be valid or invalid?

When building the prototype, we considered that these steps could qualify as an interaction with the field, so they should probably cause it to become invalid if it's marked as required – since they involve selecting a value, not just touching the field and leaving.
author_association: CONTRIBUTOR
user: login: ugur-vaadin
created_at: 2025-05-15T13:54:23Z
body: > When building the prototype, we considered that these steps could qualify as an interaction with the field, so they should probably cause it to become invalid if it's marked as required – since they involve selecting a value, not just touching the field and leaving.

Added the case and the tests for it.
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-21T09:23:37Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=vaadin_flow-components&pullRequest=7346) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4 New issues](https://sonarcloud.io/project/issues?id=vaadin_flow-components&pullRequest=7346&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=vaadin_flow-components&pullRequest=7346&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=vaadin_flow-components&pullRequest=7346&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=vaadin_flow-components&pullRequest=7346&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [6.7% Duplication on New Code](https://sonarcloud.io/component_measures?id=vaadin_flow-components&pullRequest=7346&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=vaadin_flow-components&pullRequest=7346)


author_association: NONE
user: login: vaadin-bot
created_at: 2025-07-01T12:12:55Z
body: This ticket/PR has been released with Vaadin 25.0.0-alpha1.
author_association: COLLABORATOR