Skip to content

Commit

Permalink
[MaterialDatePicker] a11y/i18n alignment
Browse files Browse the repository at this point in the history
Resolves #3343

PiperOrigin-RevId: 526657661
  • Loading branch information
paulfthomas authored and leticiarossi committed Apr 24, 2023
1 parent fdaff78 commit f4d0f56
Show file tree
Hide file tree
Showing 8 changed files with 486 additions and 43 deletions.
Expand Up @@ -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;
Expand All @@ -27,26 +28,28 @@
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,
DateFormat dateFormat,
@NonNull TextInputLayout textInputLayout,
CalendarConstraints constraints) {

this.formatHint = formatHint;
this.dateFormat = dateFormat;
this.textInputLayout = textInputLayout;
this.constraints = constraints;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
Expand All @@ -116,6 +142,6 @@ private String sanitizeDateString(String dateString) {
}

public void runValidation(View view, Runnable validation) {
view.postDelayed(validation, VALIDATION_DELAY);
view.post(validation);
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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()) {
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
44 changes: 34 additions & 10 deletions lib/java/com/google/android/material/datepicker/UtcDates.java
Expand Up @@ -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);
Expand All @@ -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.
*
* <p>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.
*
* <p>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());
}
Expand Down

0 comments on commit f4d0f56

Please sign in to comment.