From f4d0f5653a99cd723bd2b05ed05dfec816c8e1c3 Mon Sep 17 00:00:00 2001 From: pfthomas Date: Mon, 24 Apr 2023 11:56:05 -0400 Subject: [PATCH] [MaterialDatePicker] a11y/i18n alignment Resolves https://github.com/material-components/material-components-android/issues/3343 PiperOrigin-RevId: 526657661 --- .../datepicker/DateFormatTextWatcher.java | 34 ++- .../datepicker/MaterialDatePicker.java | 13 -- .../datepicker/RangeDateSelector.java | 3 - .../datepicker/SingleDateSelector.java | 2 - .../android/material/datepicker/UtcDates.java | 44 +++- .../datepicker/RangeDateSelectorTest.java | 218 +++++++++++++++++- .../datepicker/SingleDateSelectorTest.java | 145 +++++++++++- .../material/datepicker/UtcDatesTest.java | 70 +++++- 8 files changed, 486 insertions(+), 43 deletions(-) diff --git a/lib/java/com/google/android/material/datepicker/DateFormatTextWatcher.java b/lib/java/com/google/android/material/datepicker/DateFormatTextWatcher.java index dbb586562a2..19cbdf96edc 100644 --- a/lib/java/com/google/android/material/datepicker/DateFormatTextWatcher.java +++ b/lib/java/com/google/android/material/datepicker/DateFormatTextWatcher.java @@ -18,6 +18,7 @@ import com.google.android.material.R; import android.content.Context; +import android.text.Editable; import android.text.TextUtils; import android.view.View; import androidx.annotation.NonNull; @@ -27,19 +28,20 @@ import java.text.DateFormat; import java.text.ParseException; import java.util.Date; +import java.util.Locale; abstract class DateFormatTextWatcher extends TextWatcherAdapter { - private static final int VALIDATION_DELAY = 1000; - @NonNull private final TextInputLayout textInputLayout; + private final String formatHint; private final DateFormat dateFormat; private final CalendarConstraints constraints; private final String outOfRange; private final Runnable setErrorCallback; private Runnable setRangeErrorCallback; + private int lastLength = 0; DateFormatTextWatcher( final String formatHint, @@ -47,6 +49,7 @@ abstract class DateFormatTextWatcher extends TextWatcherAdapter { @NonNull TextInputLayout textInputLayout, CalendarConstraints constraints) { + this.formatHint = formatHint; this.dateFormat = dateFormat; this.textInputLayout = textInputLayout; this.constraints = constraints; @@ -81,7 +84,8 @@ public void onTextChanged(@NonNull CharSequence s, int start, int before, int co textInputLayout.removeCallbacks(setRangeErrorCallback); textInputLayout.setError(null); onValidDate(null); - if (TextUtils.isEmpty(s)) { + + if (TextUtils.isEmpty(s) || s.length() < formatHint.length()) { return; } @@ -102,6 +106,28 @@ public void onTextChanged(@NonNull CharSequence s, int start, int before, int co } } + @Override + public void beforeTextChanged(@NonNull CharSequence s, int start, int count, int after) { + lastLength = s.length(); + } + + @Override + public void afterTextChanged(@NonNull Editable s) { + // Exclude some languages from automatically adding delimiters. + if (Locale.getDefault().getLanguage().equals(Locale.KOREAN.getLanguage())) { + return; + } + + if (s.length() == 0 || s.length() >= formatHint.length() || s.length() < lastLength) { + return; + } + + char nextCharHint = formatHint.charAt(s.length()); + if (!Character.isDigit(nextCharHint)) { + s.append(nextCharHint); + } + } + private Runnable createRangeErrorCallback(final long milliseconds) { return () -> { String dateString = DateStrings.getDateString(milliseconds); @@ -116,6 +142,6 @@ private String sanitizeDateString(String dateString) { } public void runValidation(View view, Runnable validation) { - view.postDelayed(validation, VALIDATION_DELAY); + view.post(validation); } } diff --git a/lib/java/com/google/android/material/datepicker/MaterialDatePicker.java b/lib/java/com/google/android/material/datepicker/MaterialDatePicker.java index a8ce76e969a..9dc319d35d6 100644 --- a/lib/java/com/google/android/material/datepicker/MaterialDatePicker.java +++ b/lib/java/com/google/android/material/datepicker/MaterialDatePicker.java @@ -52,11 +52,9 @@ import androidx.annotation.StyleRes; import androidx.annotation.VisibleForTesting; import androidx.core.util.Pair; -import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.google.android.material.dialog.InsetDialogOnTouchListener; import com.google.android.material.internal.CheckableImageButton; import com.google.android.material.internal.EdgeToEdgeUtils; @@ -308,17 +306,6 @@ public void onClick(View v) { dismiss(); } }); - ViewCompat.setAccessibilityDelegate( - confirmButton, - new AccessibilityDelegateCompat() { - @Override - public void onInitializeAccessibilityNodeInfo( - @NonNull View host, @NonNull AccessibilityNodeInfoCompat info) { - super.onInitializeAccessibilityNodeInfo(host, info); - String contentDescription = getDateSelector().getError() + ", " + info.getText(); - info.setContentDescription(contentDescription); - } - }); Button cancelButton = root.findViewById(R.id.cancel_button); cancelButton.setTag(CANCEL_BUTTON_TAG); diff --git a/lib/java/com/google/android/material/datepicker/RangeDateSelector.java b/lib/java/com/google/android/material/datepicker/RangeDateSelector.java index 5f13379fafa..7a7e57ca653 100644 --- a/lib/java/com/google/android/material/datepicker/RangeDateSelector.java +++ b/lib/java/com/google/android/material/datepicker/RangeDateSelector.java @@ -35,7 +35,6 @@ import androidx.annotation.RestrictTo.Scope; import androidx.core.util.Pair; import androidx.core.util.Preconditions; -import androidx.core.view.ViewCompat; import com.google.android.material.internal.ManufacturerUtils; import com.google.android.material.resources.MaterialAttributes; import com.google.android.material.textfield.TextInputLayout; @@ -212,8 +211,6 @@ public View onCreateTextInputView( final TextInputLayout startTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_start); final TextInputLayout endTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_end); - startTextInput.setErrorAccessibilityLiveRegion(ViewCompat.ACCESSIBILITY_LIVE_REGION_NONE); - endTextInput.setErrorAccessibilityLiveRegion(ViewCompat.ACCESSIBILITY_LIVE_REGION_NONE); EditText startEditText = startTextInput.getEditText(); EditText endEditText = endTextInput.getEditText(); if (ManufacturerUtils.isDateInputKeyboardMissingSeparatorCharacters()) { diff --git a/lib/java/com/google/android/material/datepicker/SingleDateSelector.java b/lib/java/com/google/android/material/datepicker/SingleDateSelector.java index 7ea3cbf2ac6..79d50c11edb 100644 --- a/lib/java/com/google/android/material/datepicker/SingleDateSelector.java +++ b/lib/java/com/google/android/material/datepicker/SingleDateSelector.java @@ -33,7 +33,6 @@ import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; import androidx.core.util.Pair; -import androidx.core.view.ViewCompat; import com.google.android.material.internal.ManufacturerUtils; import com.google.android.material.resources.MaterialAttributes; import com.google.android.material.textfield.TextInputLayout; @@ -113,7 +112,6 @@ public View onCreateTextInputView( View root = layoutInflater.inflate(R.layout.mtrl_picker_text_input_date, viewGroup, false); TextInputLayout dateTextInput = root.findViewById(R.id.mtrl_picker_text_input_date); - dateTextInput.setErrorAccessibilityLiveRegion(ViewCompat.ACCESSIBILITY_LIVE_REGION_NONE); EditText dateEditText = dateTextInput.getEditText(); if (ManufacturerUtils.isDateInputKeyboardMissingSeparatorCharacters()) { // Using the URI variation places the '/' and '.' in more prominent positions diff --git a/lib/java/com/google/android/material/datepicker/UtcDates.java b/lib/java/com/google/android/material/datepicker/UtcDates.java index e118441ce83..e32c5a50bb0 100644 --- a/lib/java/com/google/android/material/datepicker/UtcDates.java +++ b/lib/java/com/google/android/material/datepicker/UtcDates.java @@ -158,8 +158,8 @@ static DateFormat getNormalizedFormat(@NonNull DateFormat dateFormat) { static SimpleDateFormat getDefaultTextInputFormat() { String defaultFormatPattern = ((SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())) - .toPattern() - .replaceAll("\\s+", ""); + .toPattern(); + defaultFormatPattern = getDatePatternAsInputFormat(defaultFormatPattern); SimpleDateFormat format = new SimpleDateFormat(defaultFormatPattern, Locale.getDefault()); format.setTimeZone(getTimeZone()); format.setLenient(false); @@ -172,20 +172,44 @@ static String getDefaultTextInputHint(Resources res, SimpleDateFormat format) { String monthChar = res.getString(R.string.mtrl_picker_text_input_month_abbr); String dayChar = res.getString(R.string.mtrl_picker_text_input_day_abbr); - // Format year to always be displayed as 4 chars when only 1 char is used in localized pattern. - // Example: (fr-FR) dd/MM/y -> dd/MM/yyyy - if (formatHint.replaceAll("[^y]", "").length() == 1) { - formatHint = formatHint.replace("y", "yyyy"); - } - - // Remove duplicate year characters for Korean. + // Remove duplicate characters for Korean. if (Locale.getDefault().getLanguage().equals(Locale.KOREAN.getLanguage())) { - formatHint = formatHint.replaceAll("y+", "y"); + formatHint = formatHint.replaceAll("d+", "d").replaceAll("M+", "M").replaceAll("y+", "y"); } return formatHint.replace("d", dayChar).replace("M", monthChar).replace("y", yearChar); } + /** + * Receives a given local date format string and returns a string that can be displayed to the + * user and parsed by the date parser. + * + *

This function: + * - Removes all characters that don't match `d`, `M` and `y`, or any of the date format + * delimiters `.`, `/` and `-`. + * - Ensures that the format is for two digits day and month, and four digits year. + * + *

The output of this cleanup is always a 10 characters string in one of the following + * variations: + * - yyyy/MM/dd + * - yyyy-MM-dd + * - yyyy.MM.dd + * - dd/MM/yyyy + * - dd-MM-yyyy + * - dd.MM.yyyy + * - MM/dd/yyyy + */ + @NonNull + static String getDatePatternAsInputFormat(@NonNull String localeFormat) { + return localeFormat + .replaceAll("[^dMy/\\-.]", "") + .replaceAll("d{1,2}", "dd") + .replaceAll("M{1,2}", "MM") + .replaceAll("y{1,4}", "yyyy") + .replaceAll("\\.$", "") // Removes a dot suffix that appears in some formats + .replaceAll("My", "M/y"); // Edge case for the Kako locale + } + static SimpleDateFormat getSimpleFormat(String pattern) { return getSimpleFormat(pattern, Locale.getDefault()); } diff --git a/lib/javatests/com/google/android/material/datepicker/RangeDateSelectorTest.java b/lib/javatests/com/google/android/material/datepicker/RangeDateSelectorTest.java index a93f73490fe..9e11dab54ac 100644 --- a/lib/javatests/com/google/android/material/datepicker/RangeDateSelectorTest.java +++ b/lib/javatests/com/google/android/material/datepicker/RangeDateSelectorTest.java @@ -27,6 +27,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; import android.widget.GridView; import android.widget.TextView.BufferType; import androidx.core.util.Pair; @@ -111,7 +112,7 @@ public void textFieldFormatPlaceholder() { TextInputLayout startTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_start); - assertThat(startTextInput.getPlaceholderText().toString()).isEqualTo("m/d/yy"); + assertThat(startTextInput.getPlaceholderText().toString()).isEqualTo("mm/dd/yyyy"); } @Test @@ -132,8 +133,8 @@ public void textInputRangeError() { TextInputLayout startTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_start); TextInputLayout endTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_end); - startTextInput.getEditText().setText("2/2/2010", BufferType.EDITABLE); - endTextInput.getEditText().setText("2/2/2008", BufferType.EDITABLE); + startTextInput.getEditText().setText("02/02/2010", BufferType.EDITABLE); + endTextInput.getEditText().setText("02/02/2008", BufferType.EDITABLE); activity.setContentView(root); ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); @@ -228,8 +229,8 @@ public void textInputHintValidWithUSLocale() { activity.setContentView(root); ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); - assertThat(startTextInput.getPlaceholderText().toString()).isEqualTo("m/d/yy"); - assertThat(endTextInput.getPlaceholderText().toString()).isEqualTo("m/d/yy"); + assertThat(startTextInput.getPlaceholderText().toString()).isEqualTo("mm/dd/yyyy"); + assertThat(endTextInput.getPlaceholderText().toString()).isEqualTo("mm/dd/yyyy"); } @Test @@ -341,7 +342,7 @@ public void getError_invalidStartDate_isNotEmpty() { View root = getRootView(); TextInputLayout startTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_start); activity.setContentView(root); - startTextInput.getEditText().setText("1/1/"); + startTextInput.getEditText().setText("11/1111111"); ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); assertThat(rangeDateSelector.getError()).isNotEmpty(); @@ -352,12 +353,108 @@ public void getError_invalidEndDate_isNotEmpty() { View root = getRootView(); TextInputLayout endTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_end); activity.setContentView(root); - endTextInput.getEditText().setText("1/1/"); + endTextInput.getEditText().setText("11/1111111"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(rangeDateSelector.getError()).isNotEmpty(); + } + + @Test + public void getError_isNotEmptyWhenInvalidStartDateIsPatternLength() { + rangeDateSelector.setTextInputFormat(new SimpleDateFormat("MM/dd/yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_range_start); + textInputLayout.getEditText().setText("11/1111111"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(rangeDateSelector.getError()).isNotEmpty(); + } + + @Test + public void getError_isNotEmptyWhenInvalidEndDateIsPatternLength() { + rangeDateSelector.setTextInputFormat(new SimpleDateFormat("MM/dd/yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_range_end); + textInputLayout.getEditText().setText("11/1111111"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(rangeDateSelector.getError()).isNotEmpty(); + } + + @Test + public void getError_isNotEmptyWhenInvalidStartDateIsMoreThanPatternLength() { + rangeDateSelector.setTextInputFormat(new SimpleDateFormat("MM/dd/yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_range_start); + textInputLayout.getEditText().setText("12/12/20233"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(rangeDateSelector.getError()).isNotEmpty(); + } + + @Test + public void getError_isNotEmptyWhenInvalidEndDateIsMoreThanPatternLength() { + rangeDateSelector.setTextInputFormat(new SimpleDateFormat("MM/dd/yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_range_end); + textInputLayout.getEditText().setText("12/12/20233"); ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); assertThat(rangeDateSelector.getError()).isNotEmpty(); } + @Test + public void getError_isNullWhenInvalidStartDateIsLessThanPatternLength() { + rangeDateSelector.setTextInputFormat(new SimpleDateFormat("MM/dd/yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_range_start); + textInputLayout.getEditText().setText("11/11/111"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(rangeDateSelector.getError()).isNull(); + } + + @Test + public void getError_isNullWhenInvalidEndDateIsLessThanPatternLength() { + rangeDateSelector.setTextInputFormat(new SimpleDateFormat("MM/dd/yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_range_end); + textInputLayout.getEditText().setText("11/11/111"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(rangeDateSelector.getError()).isNull(); + } + + @Test + public void getError_isNullWhenValidStartDateIsPatternLength() { + rangeDateSelector.setTextInputFormat(new SimpleDateFormat("MM/dd/yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_range_start); + textInputLayout.getEditText().setText("12/12/2023"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(rangeDateSelector.getError()).isNull(); + } + + @Test + public void getError_isNullWhenValidEndDateIsPatternLength() { + rangeDateSelector.setTextInputFormat(new SimpleDateFormat("MM/dd/yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_range_end); + textInputLayout.getEditText().setText("12/12/2023"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(rangeDateSelector.getError()).isNull(); + } + @Test public void getSelectedRanges_fullRange() { Calendar setToStart = UtcDates.getUtcCalendar(); @@ -381,6 +478,113 @@ public void getSelectedRanges_partialRange() { assertThat(rangeDateSelector.getSelectedRanges()).containsExactly(selection); } + @Test + public void textField_addsDelimitersAutomatically() { + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout startTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_start); + TextInputLayout endTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_end); + EditText startEditText = startTextInput.getEditText(); + EditText endEditText = endTextInput.getEditText(); + + startEditText.append("1"); + endEditText.append("1"); + + assertThat(startEditText.getText().toString()).isEqualTo("1"); + assertThat(endEditText.getText().toString()).isEqualTo("1"); + + startEditText.append("2"); + endEditText.append("2"); + + assertThat(startEditText.getText().toString()).isEqualTo("12/"); + assertThat(endEditText.getText().toString()).isEqualTo("12/"); + + startEditText.append("1"); + endEditText.append("1"); + + assertThat(startEditText.getText().toString()).isEqualTo("12/1"); + assertThat(endEditText.getText().toString()).isEqualTo("12/1"); + + startEditText.append("2"); + endEditText.append("2"); + + assertThat(startEditText.getText().toString()).isEqualTo("12/12/"); + assertThat(endEditText.getText().toString()).isEqualTo("12/12/"); + + startEditText.append("2023"); + endEditText.append("2023"); + + assertThat(startEditText.getText().toString()).isEqualTo("12/12/2023"); + assertThat(endEditText.getText().toString()).isEqualTo("12/12/2023"); + } + + @Test + public void textField_addsMultipleDelimitersAutomatically() { + rangeDateSelector.setTextInputFormat(new SimpleDateFormat("mm/.-dd/.-yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout startTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_start); + TextInputLayout endTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_end); + EditText startEditText = startTextInput.getEditText(); + EditText endEditText = endTextInput.getEditText(); + + startEditText.append("1"); + startEditText.append("2"); + startEditText.append("1"); + endEditText.append("1"); + endEditText.append("2"); + endEditText.append("1"); + + assertThat(startEditText.getText().toString()).isEqualTo("12/.-1"); + assertThat(endEditText.getText().toString()).isEqualTo("12/.-1"); + } + + @Test + public void textField_shouldAllowAddingDelimitersManually() { + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout startTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_start); + TextInputLayout endTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_end); + EditText startEditText = startTextInput.getEditText(); + EditText endEditText = endTextInput.getEditText(); + + startEditText.append("1"); + startEditText.append("2"); + startEditText.getText().delete(startEditText.length() - 1, startEditText.length()); + startEditText.append("-"); + endEditText.append("1"); + endEditText.append("2"); + endEditText.getText().delete(endEditText.length() - 1, endEditText.length()); + endEditText.append("-"); + + assertThat(startEditText.getText().toString()).isEqualTo("12-"); + assertThat(endEditText.getText().toString()).isEqualTo("12-"); + } + + @Test + public void textField_shouldNotRemoveDelimitersAutomatically() { + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout startTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_start); + TextInputLayout endTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_end); + EditText startEditText = startTextInput.getEditText(); + EditText endEditText = endTextInput.getEditText(); + startEditText.setText("12/12/2023"); + endEditText.setText("12/12/2023"); + + startEditText.getText().delete(startEditText.length() - 4, startEditText.length()); + endEditText.getText().delete(endEditText.length() - 4, endEditText.length()); + + assertThat(startEditText.getText().toString()).isEqualTo("12/12/"); + assertThat(endEditText.getText().toString()).isEqualTo("12/12/"); + + startEditText.getText().delete(startEditText.length() - 4, startEditText.length()); + endEditText.getText().delete(endEditText.length() - 4, endEditText.length()); + + assertThat(startEditText.getText().toString()).isEqualTo("12"); + assertThat(endEditText.getText().toString()).isEqualTo("12"); + } + @Test public void focusAndShowKeyboardAtStartup() { InputMethodManager inputMethodManager = getSystemService(activity, InputMethodManager.class); diff --git a/lib/javatests/com/google/android/material/datepicker/SingleDateSelectorTest.java b/lib/javatests/com/google/android/material/datepicker/SingleDateSelectorTest.java index a720b94357c..8740380fffd 100644 --- a/lib/javatests/com/google/android/material/datepicker/SingleDateSelectorTest.java +++ b/lib/javatests/com/google/android/material/datepicker/SingleDateSelectorTest.java @@ -27,6 +27,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; import android.widget.GridView; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; @@ -40,6 +41,7 @@ import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.Shadows; +import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowInputMethodManager; import org.robolectric.shadows.ShadowLooper; @@ -154,12 +156,60 @@ public void getError_invalidDate_isNotEmpty() { View root = getRootView(); ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_date); - textInputLayout.getEditText().setText("1/1/"); + textInputLayout.getEditText().setText("11/1111111"); ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); assertThat(singleDateSelector.getError()).isNotEmpty(); } + @Test + public void getError_isNotEmptyWhenInvalidDateIsPatternLength() { + singleDateSelector.setTextInputFormat(new SimpleDateFormat("MM/dd/yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_date); + textInputLayout.getEditText().setText("11/1111111"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(singleDateSelector.getError()).isNotEmpty(); + } + + @Test + public void getError_isNotEmptyWhenInvalidDateIsMoreThanPatternLength() { + singleDateSelector.setTextInputFormat(new SimpleDateFormat("MM/dd/yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_date); + textInputLayout.getEditText().setText("12/12/20233"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(singleDateSelector.getError()).isNotEmpty(); + } + + @Test + public void getError_isNullWhenInvalidDateIsLessThanPatternLength() { + singleDateSelector.setTextInputFormat(new SimpleDateFormat("MM/dd/yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_date); + textInputLayout.getEditText().setText("11/11/111"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(singleDateSelector.getError()).isNull(); + } + + @Test + public void getError_isNullWhenValidDateIsPatternLength() { + singleDateSelector.setTextInputFormat(new SimpleDateFormat("MM/dd/yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_date); + textInputLayout.getEditText().setText("12/12/2023"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(singleDateSelector.getError()).isNull(); + } + @Test public void getSelectedRanges_isEmpty() { Calendar calendar = UtcDates.getUtcCalendar(); @@ -176,7 +226,7 @@ public void textFieldPlaceholder_usesDefaultFormat() { TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_date); - assertThat(textInputLayout.getPlaceholderText().toString()).isEqualTo("m/d/yy"); + assertThat(textInputLayout.getPlaceholderText().toString()).isEqualTo("mm/dd/yyyy"); } @Test @@ -190,6 +240,97 @@ public void textFieldPlaceholder_usesCustomFormat() { assertThat(textInputLayout.getPlaceholderText().toString()).isEqualTo("kk:mm:ss mm/dd/yyyy"); } + @Test + public void textField_addsDelimitersAutomatically() { + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_date); + EditText editText = textInputLayout.getEditText(); + + editText.append("1"); + + assertThat(editText.getText().toString()).isEqualTo("1"); + + editText.append("2"); + + assertThat(editText.getText().toString()).isEqualTo("12/"); + + editText.append("1"); + + assertThat(editText.getText().toString()).isEqualTo("12/1"); + + editText.append("2"); + + assertThat(editText.getText().toString()).isEqualTo("12/12/"); + + editText.append("2023"); + + assertThat(editText.getText().toString()).isEqualTo("12/12/2023"); + } + + @Test + public void textField_addsMultipleDelimitersAutomatically() { + singleDateSelector.setTextInputFormat(new SimpleDateFormat("mm/.-dd/.-yyyy")); + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_date); + EditText editText = textInputLayout.getEditText(); + + editText.append("1"); + editText.append("2"); + editText.append("1"); + + assertThat(editText.getText().toString()).isEqualTo("12/.-1"); + } + + @Test + public void textField_shouldAllowAddingDelimitersManually() { + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_date); + EditText editText = textInputLayout.getEditText(); + + editText.append("1"); + editText.append("2"); + editText.getText().delete(editText.length() - 1, editText.length()); + editText.append("-"); + + assertThat(editText.getText().toString()).isEqualTo("12-"); + } + + @Test + public void textField_shouldNotRemoveDelimitersAutomatically() { + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_date); + EditText editText = textInputLayout.getEditText(); + editText.setText("12/12/2023"); + + editText.getText().delete(editText.length() - 4, editText.length()); + + assertThat(editText.getText().toString()).isEqualTo("12/12/"); + + editText.getText().delete(editText.length() - 4, editText.length()); + + assertThat(editText.getText().toString()).isEqualTo("12"); + } + + @Test + @Config(qualifiers = "ko") + public void textField_shouldNotAddDelimitersAutomaticallyForKorean() { + View root = getRootView(); + ((ViewGroup) activity.findViewById(android.R.id.content)).addView(root); + TextInputLayout textInputLayout = root.findViewById(R.id.mtrl_picker_text_input_date); + EditText editText = textInputLayout.getEditText(); + + editText.append("2"); + editText.append("0"); + editText.append("2"); + editText.append("3"); + + assertThat(editText.getText().toString()).isEqualTo("2023"); + } + @Test public void focusAndShowKeyboardAtStartup() { InputMethodManager inputMethodManager = getSystemService(activity, InputMethodManager.class); diff --git a/lib/javatests/com/google/android/material/datepicker/UtcDatesTest.java b/lib/javatests/com/google/android/material/datepicker/UtcDatesTest.java index 66e14604d12..a24cb6d2ca4 100644 --- a/lib/javatests/com/google/android/material/datepicker/UtcDatesTest.java +++ b/lib/javatests/com/google/android/material/datepicker/UtcDatesTest.java @@ -22,7 +22,9 @@ import android.content.Context; import androidx.appcompat.app.AppCompatActivity; import androidx.test.core.app.ApplicationProvider; +import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.Locale; import java.util.TimeZone; import org.junit.Before; import org.junit.Test; @@ -48,7 +50,7 @@ public void textInputHintWith1CharYear() { SimpleDateFormat sdf = new SimpleDateFormat("M/d/y"); String hint = UtcDates.getDefaultTextInputHint(context.getResources(), sdf); - assertEquals("m/d/yyyy", hint); + assertEquals("m/d/y", hint); } @Test @@ -73,7 +75,7 @@ public void textInputHintWith1CharYearLocalized() { SimpleDateFormat sdf = new SimpleDateFormat("M/d/y"); String hint = UtcDates.getDefaultTextInputHint(context.getResources(), sdf); - assertEquals("m/j/aaaa", hint); + assertEquals("m/j/a", hint); } @Test @@ -95,4 +97,68 @@ public void normalizeTextInputFormat() { assertEquals(TimeZone.getTimeZone("US/Pacific"), sdf.getTimeZone()); assertEquals(TimeZone.getTimeZone("UTC"), normalized.getTimeZone()); } + + @Test + public void getDefaultTextInputFormat() { + SimpleDateFormat sdf = UtcDates.getDefaultTextInputFormat(); + + assertEquals("MM/dd/yyyy", sdf.toPattern()); + } + + @Test + @Config(qualifiers = "fr-rFR") + public void getDefaultTextInputFormatForFrench() { + SimpleDateFormat sdf = UtcDates.getDefaultTextInputFormat(); + + assertEquals("dd/MM/yyyy", sdf.toPattern()); + } + + @Test + @Config(qualifiers = "ru") + public void getDefaultTextInputFormatForRussian() { + SimpleDateFormat sdf = UtcDates.getDefaultTextInputFormat(); + + assertEquals("dd.MM.yyyy", sdf.toPattern()); + } + + @Test + @Config(qualifiers = "se") + public void getDefaultTextInputFormatForSwedish() { + SimpleDateFormat sdf = UtcDates.getDefaultTextInputFormat(); + + assertEquals("yyyy-MM-dd", sdf.toPattern()); + } + + @Test + public void getDatePatternAsInputFormat_filterDateCharactersAndDelimiters() { + String pattern = UtcDates.getDatePatternAsInputFormat("ddDe/FMM-mNo._!$% xYyyyyz"); + + assertEquals("dd/MM-.yyyy", pattern); + } + + @Test + public void getDatePatternAsInputFormat_enforce2CharsForDayMonth4CharsForYear() { + String pattern = UtcDates.getDatePatternAsInputFormat("d/M/y"); + + assertEquals("dd/MM/yyyy", pattern); + } + + @Test + public void getDatePatternAsInputFormat_removeDotSuffix() { + String pattern = UtcDates.getDatePatternAsInputFormat("yyyy.MM.dd."); + + assertEquals("yyyy.MM.dd", pattern); + } + + @Test + @Config(qualifiers = "kkj") + public void getDatePatternAsInputFormat_fixForKakoLanguage() { + String defaultPattern = + ((SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())) + .toPattern(); + String pattern = UtcDates.getDatePatternAsInputFormat(defaultPattern); + + assertEquals("dd/MM y", defaultPattern); + assertEquals("dd/MM/yyyy", pattern); + } }