diff --git a/vaadin-date-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datepicker/InvalidDateStringView.java b/vaadin-date-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datepicker/InvalidDateStringView.java new file mode 100644 index 0000000..2ba2784 --- /dev/null +++ b/vaadin-date-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datepicker/InvalidDateStringView.java @@ -0,0 +1,26 @@ +package com.vaadin.flow.component.datepicker; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.router.Route; + +@Route("invalid-date-string") +public class InvalidDateStringView extends Div { + + public InvalidDateStringView() { + DatePicker datePicker = new DatePicker(); + + Div value = new Div(); + datePicker.addValueChangeListener(e -> value.setText( + e.getValue() == null ? "null" : e.getValue().toString())); + value.setId("value"); + + NativeButton checkValidity = new NativeButton("check validity", + e -> e.getSource() + .setText(datePicker.isInvalid() ? "invalid" : "valid")); + checkValidity.setId("check-validity"); + + add(datePicker, value, checkValidity); + } + +} diff --git a/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/DatePickerBinderIT.java b/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/DatePickerBinderIT.java index 32d9531..999d64b 100644 --- a/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/DatePickerBinderIT.java +++ b/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/DatePickerBinderIT.java @@ -15,16 +15,13 @@ */ package com.vaadin.flow.component.datepicker; -import com.vaadin.flow.component.datepicker.testbench.DatePickerElement; -import com.vaadin.flow.testutil.AbstractComponentIT; -import com.vaadin.flow.testutil.TestPath; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import java.time.LocalDate; -import java.util.Collections; - +import com.vaadin.flow.component.datepicker.testbench.DatePickerElement; +import com.vaadin.flow.testutil.AbstractComponentIT; +import com.vaadin.flow.testutil.TestPath; /** * Integration tests for the {@link BinderValidationView}. @@ -32,42 +29,38 @@ @TestPath("binder-validation") public class DatePickerBinderIT extends AbstractComponentIT { + private DatePickerElement field; + @Before public void init() { open(); + field = $(DatePickerElement.class).waitForFirst(); } @Test - public void selectDateOnSimpleDatePicker() { - final DatePickerElement element = $(DatePickerElement.class).waitForFirst(); - Assert.assertFalse(element.getPropertyBoolean("invalid")); + public void initiallyValid() { + assertValid(field); } private void setInternalValidBinderInvalidValue(DatePickerElement field) { - field.setDate(LocalDate.of(2019, 1, 2)); - field.dispatchEvent("change", - Collections.singletonMap("bubbles", true)); - field.dispatchEvent("blur"); + field.setInputValue("1/2/2019"); } @Test public void dateField_internalValidationPass_binderValidationFail_fieldInvalid() { - DatePickerElement field = $(DatePickerElement.class).first(); setInternalValidBinderInvalidValue(field); - assertInvalid(field); + assertInvalidatedByBinder(field); } @Test public void dateField_internalValidationPass_binderValidationFail_validateClient_fieldInvalid() { - DatePickerElement field = $(DatePickerElement.class).first(); - setInternalValidBinderInvalidValue(field); field.getCommandExecutor().executeScript( "arguments[0].validate(); arguments[0].immediateInvalid = arguments[0].invalid;", field); - assertInvalid(field); + assertInvalidatedByBinder(field); // State before server roundtrip (avoid flash of valid // state) Assert.assertTrue("Unexpected immediateInvalid state", @@ -76,22 +69,29 @@ public void dateField_internalValidationPass_binderValidationFail_validateClient @Test public void dateField_internalValidationPass_binderValidationFail_setClientValid_serverFieldInvalid() { - DatePickerElement field = $(DatePickerElement.class).first(); - setInternalValidBinderInvalidValue(field); - field.getCommandExecutor() - .executeScript("arguments[0].invalid = false", field); + field.getCommandExecutor().executeScript("arguments[0].invalid = false", + field); Assert.assertEquals(field.getPropertyString("label"), "invalid"); } - private void assertInvalid(DatePickerElement field) { - Assert.assertTrue("Unexpected invalid state", - field.getPropertyBoolean("invalid")); + private void assertInvalidatedByBinder(DatePickerElement field) { + assertInvalid(field); Assert.assertEquals( "Expected to have error message configured in the Binder Validator", BinderValidationView.BINDER_ERROR_MSG, field.getPropertyString("errorMessage")); } + + private void assertInvalid(DatePickerElement field) { + Assert.assertTrue("Unexpected invalid state", + field.getPropertyBoolean("invalid")); + } + + private void assertValid(DatePickerElement field) { + Assert.assertFalse("Unexpected invalid state", + field.getPropertyBoolean("invalid")); + } } diff --git a/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/InvalidDateStringIT.java b/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/InvalidDateStringIT.java new file mode 100644 index 0000000..951a95b --- /dev/null +++ b/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/InvalidDateStringIT.java @@ -0,0 +1,90 @@ +/* + * Copyright 2000-2017 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.datepicker; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.flow.component.datepicker.testbench.DatePickerElement; +import com.vaadin.flow.testutil.AbstractComponentIT; +import com.vaadin.flow.testutil.TestPath; +import com.vaadin.testbench.TestBenchElement; + +/** + * Integration tests for the {@link InvalidDateStringView}. + */ +@TestPath("invalid-date-string") +public class InvalidDateStringIT extends AbstractComponentIT { + + private DatePickerElement datePicker; + private TestBenchElement checkValidity; + private TestBenchElement value; + + @Before + public void init() { + open(); + datePicker = $(DatePickerElement.class).first(); + checkValidity = $(TestBenchElement.class).id("check-validity"); + value = $(TestBenchElement.class).id("value"); + } + + @Test + public void setInvalidDateString_fieldInvalid() { + datePicker.setInputValue("asdf"); + assertValid(false); + assertValue(""); + } + + @Test + public void setValidValue_setInvalidDateString_fieldInvalid() { + datePicker.setInputValue("1/1/2020"); + assertValid(true); + assertValue("2020-01-01"); + datePicker.setInputValue("asdf"); + assertValid(false); + assertValue(null); + } + + @Test + public void setInvalidDateString_clearField_fieldValid() { + datePicker.setInputValue("asdf"); + datePicker.setInputValue(""); + assertValid(true); + assertValue(""); + } + + @Test + public void setInvalidDateString_setValidValue_fieldValid() { + datePicker.setInputValue("asdf"); + datePicker.setInputValue("1/1/2020"); + assertValid(true); + assertValue("2020-01-01"); + } + + private void assertValid(boolean expectedValid) { + String expectedText = expectedValid ? "valid" : "invalid"; + checkValidity.click(); + Assert.assertEquals("Expected DatePicker to be " + expectedText, + expectedText, checkValidity.getText()); + } + + private void assertValue(String expectedValue) { + Assert.assertEquals("Unexpected DatePicker value", + expectedValue == null ? "null" : expectedValue, + value.getText()); + } +} diff --git a/vaadin-date-picker-flow-testbench/src/main/java/com/vaadin/flow/component/datepicker/testbench/DatePickerElement.java b/vaadin-date-picker-flow-testbench/src/main/java/com/vaadin/flow/component/datepicker/testbench/DatePickerElement.java index 36bb8ce..ce6a4cb 100644 --- a/vaadin-date-picker-flow-testbench/src/main/java/com/vaadin/flow/component/datepicker/testbench/DatePickerElement.java +++ b/vaadin-date-picker-flow-testbench/src/main/java/com/vaadin/flow/component/datepicker/testbench/DatePickerElement.java @@ -95,6 +95,7 @@ protected String getValue() { public void setInputValue(String value) { executeScript("arguments[0].open();", this); setProperty("_inputValue", value); + executeScript("arguments[0].dispatchEvent(new Event('blur'));", this); executeScript("arguments[0].close();", this); } diff --git a/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java b/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java index 9c855d7..81009e9 100644 --- a/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java +++ b/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java @@ -26,6 +26,7 @@ import com.vaadin.flow.component.HasSize; import com.vaadin.flow.component.HasValidation; import com.vaadin.flow.component.HasValue; +import com.vaadin.flow.component.Synchronize; import com.vaadin.flow.component.UI; import com.vaadin.flow.component.dependency.JsModule; import com.vaadin.flow.function.SerializableConsumer; @@ -88,6 +89,7 @@ public DatePicker(LocalDate initialDate) { setInvalid(false); addValueChangeListener(e -> validate()); + addBlurListener(e -> validate()); FieldValidationUtil.disableClientValidation(this); } @@ -400,14 +402,17 @@ public boolean isInvalid() { * because it is possible to circumvent the client side validation * constraints using browser development tools. */ - private boolean isInvalid(LocalDate value) { + private boolean isInvalid(LocalDate value, String inputValue) { final boolean isRequiredButEmpty = required && Objects.equals(getEmptyValue(), value); final boolean isGreaterThanMax = value != null && max != null && value.isAfter(max); final boolean isSmallerThenMin = value != null && min != null && value.isBefore(min); - return isRequiredButEmpty || isGreaterThanMax || isSmallerThenMin; + final boolean hasNonParseableInputString = value == null + && !inputValue.isEmpty(); + return isRequiredButEmpty || isGreaterThanMax || isSmallerThenMin + || hasNonParseableInputString; } /** @@ -605,7 +610,7 @@ public String getName() { * constraints using browser development tools. */ protected void validate() { - setInvalid(isInvalid(getValue())); + setInvalid(isInvalid(getValue(), getInputValue())); } @Override @@ -620,6 +625,11 @@ public Registration addInvalidChangeListener( return super.addInvalidChangeListener(listener); } + @Synchronize(property = "_inputValue", value = "sync-input-value") + private String getInputValue() { + return getElement().getProperty("_inputValue", ""); + } + /** * The internationalization properties for {@link DatePicker}. */ diff --git a/vaadin-date-picker-flow/src/main/resources/META-INF/resources/frontend/datepickerConnector.js b/vaadin-date-picker-flow/src/main/resources/META-INF/resources/frontend/datepickerConnector.js index 2810602..522ff0e 100644 --- a/vaadin-date-picker-flow/src/main/resources/META-INF/resources/frontend/datepickerConnector.js +++ b/vaadin-date-picker-flow/src/main/resources/META-INF/resources/frontend/datepickerConnector.js @@ -1,4 +1,102 @@ (function () { + window.Vaadin = window.Vaadin || {}; + + Vaadin.DatePickerHelper = Vaadin.DatePickerHelper || class VaadinDatePickerHelper { + /** + * Get ISO 8601 week number for the given date. + * + * @param {Date} Date object + * @return {Number} Week number + */ + static _getISOWeekNumber(date) { + // Ported from Vaadin Framework method com.vaadin.client.DateTimeService.getISOWeekNumber(date) + var dayOfWeek = date.getDay(); // 0 == sunday + + // ISO 8601 use weeks that start on monday so we use + // mon=1,tue=2,...sun=7; + if (dayOfWeek === 0) { + dayOfWeek = 7; + } + // Find nearest thursday (defines the week in ISO 8601). The week number + // for the nearest thursday is the same as for the target date. + var nearestThursdayDiff = 4 - dayOfWeek; // 4 is thursday + var nearestThursday = new Date(date.getTime() + nearestThursdayDiff * 24 * 3600 * 1000); + + var firstOfJanuary = new Date(0, 0); + firstOfJanuary.setFullYear(nearestThursday.getFullYear()); + + var timeDiff = nearestThursday.getTime() - firstOfJanuary.getTime(); + + // Rounding the result, as the division doesn't result in an integer + // when the given date is inside daylight saving time period. + var daysSinceFirstOfJanuary = Math.round(timeDiff / (24 * 3600 * 1000)); + + return Math.floor((daysSinceFirstOfJanuary) / 7 + 1); + } + + /** + * Check if two dates are equal. + * + * @param {Date} date1 + * @param {Date} date2 + * @return {Boolean} True if the given date objects refer to the same date + */ + static _dateEquals(date1, date2) { + return date1 instanceof Date && date2 instanceof Date && + date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate(); + } + + /** + * Check if the given date is in the range of allowed dates. + * + * @param {Date} date The date to check + * @param {Date} min Range start + * @param {Date} max Range end + * @return {Boolean} True if the date is in the range + */ + static _dateAllowed(date, min, max) { + return (!min || date >= min) && (!max || date <= max); + } + + /** + * Get closest date from array of dates. + * + * @param {Date} date The date to compare dates with + * @param {Array} dates Array of date objects + * @return {Date} Closest date + */ + static _getClosestDate(date, dates) { + return dates.filter(date => date !== undefined) + .reduce((closestDate, candidate) => { + if (!candidate) { + return closestDate; + } + + if (!closestDate) { + return candidate; + } + + var candidateDiff = Math.abs(date.getTime() - candidate.getTime()); + var closestDateDiff = Math.abs(closestDate.getTime() - date.getTime()); + return candidateDiff < closestDateDiff ? candidate : closestDate; + }); + } + + /** + * Extracts the basic component parts of a date (day, month and year) + * to the expected format. + */ + static _extractDateParts(date) { + return { + day: date.getDate(), + month: date.getMonth(), + year: date.getFullYear() + }; + } + }; + const tryCatchWrapper = function (callback) { return window.Vaadin.Flow.tryCatchWrapper(callback, 'Vaadin Date Picker', 'vaadin-date-picker-flow'); }; @@ -29,6 +127,81 @@ return; } + datepicker._selectedDateChanged = (function(selectedDate, formatDate) { + if (selectedDate === undefined || formatDate === undefined) { + return; + } + if (this.__userInputOccurred) { + this.__dispatchChange = true; + } + const inputValue = selectedDate && formatDate(window.Vaadin.DatePickerHelper._extractDateParts(selectedDate)); + const value = this._formatISO(selectedDate); + if (!this.__keepInputValue) { + this._inputValue = selectedDate ? inputValue : ''; + } + if (value !== this.value) { + this.validate(); + this.value = value; + } + this.__userInputOccurred = false; + this.__dispatchChange = false; + this._ignoreFocusedDateChange = true; + this._focusedDate = selectedDate; + this._ignoreFocusedDateChange = false; + }).bind(datepicker); + + datepicker._onOverlayClosed = (function() { + this._ignoreAnnounce = true; + + window.removeEventListener('scroll', this._boundOnScroll, true); + this.removeEventListener('iron-resize', this._boundUpdateAlignmentAndPosition); + + if (this._touchPrevented) { + this._touchPrevented.forEach(prevented => + prevented.element.style.webkitOverflowScrolling = prevented.oldInlineValue); + this._touchPrevented = []; + } + + this.updateStyles(); + + // Select the parsed input or focused date + this._ignoreFocusedDateChange = true; + if (this.i18n.parseDate) { + var inputValue = this._inputValue || ''; + const dateObject = this.i18n.parseDate(inputValue); + const parsedDate = dateObject && + this._parseDate(`${dateObject.year}-${dateObject.month + 1}-${dateObject.day}`); + + if (this._isValidDate(parsedDate)) { + this._selectedDate = parsedDate; + } else { + this.__keepInputValue = true; + this._selectedDate = null; + this.__keepInputValue = false; + } + } else if (this._focusedDate) { + this._selectedDate = this._focusedDate; + } + this._ignoreFocusedDateChange = false; + + if (this._nativeInput && this._nativeInput.selectionStart) { + this._nativeInput.selectionStart = this._nativeInput.selectionEnd; + } + // No need to revalidate the value after `_selectedDateChanged` + // Needed in case the value was not changed: open and close dropdown. + !this.value && this.validate(); + }).bind(datepicker); + + const notifyInputValueChange = () => { + // setTimeout(() => + console.log('asdf ' + datepicker._inputValue); + datepicker.dispatchEvent(new CustomEvent('sync-input-value')) + } + // ); + + datepicker.addEventListener('value-changed', notifyInputValueChange); + datepicker.addEventListener('blur', notifyInputValueChange); + datepicker.$connector = {}; /* init helper parts for reverse-engineering date-regex */