diff --git a/docs/components/TextField.md b/docs/components/TextField.md index 44f0bce806c..396b32140fd 100644 --- a/docs/components/TextField.md +++ b/docs/components/TextField.md @@ -526,20 +526,21 @@ Element | Attribute | Related method(s) #### Helper/error/counter text attributes -Element | Attribute | Related method(s) | Default value ---------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------- -**Helper text enabled** | `app:helperTextEnabled` | `setHelperTextEnabled`
`isHelperTextEnabled` | `false` -**Helper text** | `app:helperText` | `setHelperText`
`getHelperText` | `null` -**Helper text color** | `app:helperTextColor` | `setHelperTextColor`
`getHelperTextColor` | `?attr/colorOnSurfaceVariant` (see all [states](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/textfield/res/color/m3_textfield_indicator_text_color.xml)) -**Helper text typography** | `app:helperTextAppearance` | `setHelperTextAppearance` | `?attr/textAppearanceBodySmall` -**Error text enabled** | `app:errorEnabled` | `setErrorEnabled`
`isErrorEnabled` | `false` -**Error text** | N/A | `setError`
`getError` | `null` -**Error text color** | `app:errorTextColor` | `setErrorTextColor`
`getErrorCurrentTextColors` | `?attr/colorError` -**Error text typography** | `app:errorTextAppearance` | `setErrorTextAppearance` | `?attr/textAppearanceBodySmall` -**Counter text enabled** | `app:counterEnabled` | `setCounterEnabled`
`isCounterEnabled` | `false` -**Counter text length** | `app:counterMaxLength` | `setCounterMaxLength`
`getCounterMaxLength` | `-1` -**Counter text typography** | `app:counterTextAppearance`
`app:counterOverflowTextAppearance` | `setCounterTextAppearance`
`setCounterOverflowTextAppearance` | `?attr/textAppearanceBodySmall` -**Counter text color** | `app:counterTextColor`
`app:counterOverflowTextColor` | `setCounterTextColor`
`setCounterOverflowTextColor`
`getCounterTextColor`
`getCounterOverflowTextColor` | `?attr/colorOnSurfaceVariant` (`app:counterTextColor`) (see all [states](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/textfield/res/color/m3_textfield_indicator_text_color.xml))
`?attr/colorError` (`app:counterOverflowTextColor`) +| Element | Attribute | Related method(s) | Default value | +|------------------------------------------|---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Helper text enabled** | `app:helperTextEnabled` | `setHelperTextEnabled`
`isHelperTextEnabled` | `false` | +| **Helper text** | `app:helperText` | `setHelperText`
`getHelperText` | `null` | +| **Helper text color** | `app:helperTextColor` | `setHelperTextColor`
`getHelperTextColor` | `?attr/colorOnSurfaceVariant` (see all [states](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/textfield/res/color/m3_textfield_indicator_text_color.xml)) | +| **Helper text typography** | `app:helperTextAppearance` | `setHelperTextAppearance` | `?attr/textAppearanceBodySmall` | +| **Error text enabled** | `app:errorEnabled` | `setErrorEnabled`
`isErrorEnabled` | `false` | +| **Error text** | N/A | `setError`
`getError` | `null` | +| **Error text accessibility live region** | `app:errorAccessibilityLiveRegion` | `setErrorAccessibilityLiveRegion`
`getErrorAccessibilityLiveRegion` | `ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE` | +| **Error text color** | `app:errorTextColor` | `setErrorTextColor`
`getErrorCurrentTextColors` | `?attr/colorError` | +| **Error text typography** | `app:errorTextAppearance` | `setErrorTextAppearance` | `?attr/textAppearanceBodySmall` | +| **Counter text enabled** | `app:counterEnabled` | `setCounterEnabled`
`isCounterEnabled` | `false` | +| **Counter text length** | `app:counterMaxLength` | `setCounterMaxLength`
`getCounterMaxLength` | `-1` | +| **Counter text typography** | `app:counterTextAppearance`
`app:counterOverflowTextAppearance` | `setCounterTextAppearance`
`setCounterOverflowTextAppearance` | `?attr/textAppearanceBodySmall` | +| **Counter text color** | `app:counterTextColor`
`app:counterOverflowTextColor` | `setCounterTextColor`
`setCounterOverflowTextColor`
`getCounterTextColor`
`getCounterOverflowTextColor` | `?attr/colorOnSurfaceVariant` (`app:counterTextColor`) (see all [states](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/textfield/res/color/m3_textfield_indicator_text_color.xml))
`?attr/colorError` (`app:counterOverflowTextColor`) | #### Prefix/suffix attributes @@ -697,20 +698,21 @@ Element | Attribute | Related #### Helper/error/counter text attributes -Element | Attribute | Related method(s) | Default value ---------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------- -**Helper text enabled** | `app:helperTextEnabled` | `setHelperTextEnabled`
`isHelperTextEnabled` | `false` -**Helper text** | `app:helperText` | `setHelperText`
`getHelperText` | `null` -**Helper text color** | `app:helperTextColor` | `setHelperTextColor`
`getHelperTextColor` | `?attr/colorOnSurfaceVariant` (see all [states](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/textfield/res/color/m3_textfield_indicator_text_color.xml)) -**Helper text typography** | `app:helperTextAppearance` | `setHelperTextAppearance` | `?attr/textAppearanceBodySmall` -**Error text enabled** | `app:errorEnabled` | `setErrorEnabled`
`isErrorEnabled` | `false` -**Error text** | N/A | `setError`
`getError` | `null` -**Error text color** | `app:errorTextColor` | `setErrorTextColor`
`getErrorCurrentTextColors` | `?attr/colorError` -**Error text typography** | `app:errorTextAppearance` | `setErrorTextAppearance` | `?attr/textAppearanceBodySmall` -**Counter text enabled** | `app:counterEnabled` | `setCounterEnabled`
`isCounterEnabled` | `false` -**Counter text length** | `app:counterMaxLength` | `setCounterMaxLength`
`getCounterMaxLength` | `-1` -**Counter text typography** | `app:counterTextAppearance`
`app:counterOverflowTextAppearance` | `setCounterTextAppearance`
`setCounterOverflowTextAppearance` | `?attr/textAppearanceBodySmall` -**Counter text color** | `app:counterTextColor`
`app:counterOverflowTextColor` | `setCounterTextColor`
`setCounterOverflowTextColor`
`getCounterTextColor`
`getCounterOverflowTextColor` | `?attr/colorOnSurfaceVariant` (`app:counterTextColor`) (see all [states](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/textfield/res/color/m3_textfield_indicator_text_color.xml))
`?attr/colorError` (`app:counterOverflowTextColor`) +| Element | Attribute | Related method(s) | Default value | +|------------------------------------------|---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Helper text enabled** | `app:helperTextEnabled` | `setHelperTextEnabled`
`isHelperTextEnabled` | `false` | +| **Helper text** | `app:helperText` | `setHelperText`
`getHelperText` | `null` | +| **Helper text color** | `app:helperTextColor` | `setHelperTextColor`
`getHelperTextColor` | `?attr/colorOnSurfaceVariant` (see all [states](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/textfield/res/color/m3_textfield_indicator_text_color.xml)) | +| **Helper text typography** | `app:helperTextAppearance` | `setHelperTextAppearance` | `?attr/textAppearanceBodySmall` | +| **Error text enabled** | `app:errorEnabled` | `setErrorEnabled`
`isErrorEnabled` | `false` | +| **Error text** | N/A | `setError`
`getError` | `null` | +| **Error text accessibility live region** | `app:errorAccessibilityLiveRegion` | `setErrorAccessibilityLiveRegion`
`getErrorAccessibilityLiveRegion` | `ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE` | +| **Error text color** | `app:errorTextColor` | `setErrorTextColor`
`getErrorCurrentTextColors` | `?attr/colorError` | +| **Error text typography** | `app:errorTextAppearance` | `setErrorTextAppearance` | `?attr/textAppearanceBodySmall` | +| **Counter text enabled** | `app:counterEnabled` | `setCounterEnabled`
`isCounterEnabled` | `false` | +| **Counter text length** | `app:counterMaxLength` | `setCounterMaxLength`
`getCounterMaxLength` | `-1` | +| **Counter text typography** | `app:counterTextAppearance`
`app:counterOverflowTextAppearance` | `setCounterTextAppearance`
`setCounterOverflowTextAppearance` | `?attr/textAppearanceBodySmall` | +| **Counter text color** | `app:counterTextColor`
`app:counterOverflowTextColor` | `setCounterTextColor`
`setCounterOverflowTextColor`
`getCounterTextColor`
`getCounterOverflowTextColor` | `?attr/colorOnSurfaceVariant` (`app:counterTextColor`) (see all [states](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/textfield/res/color/m3_textfield_indicator_text_color.xml))
`?attr/colorError` (`app:counterOverflowTextColor`) | #### Prefix/suffix attributes diff --git a/lib/java/com/google/android/material/datepicker/DateSelector.java b/lib/java/com/google/android/material/datepicker/DateSelector.java index 6c7a6ef7699..5f473b1fae4 100644 --- a/lib/java/com/google/android/material/datepicker/DateSelector.java +++ b/lib/java/com/google/android/material/datepicker/DateSelector.java @@ -100,6 +100,9 @@ public interface DateSelector extends Parcelable { @NonNull String getSelectionContentDescription(@NonNull Context context); + @Nullable + String getError(); + @StringRes int getDefaultTitleResId(); diff --git a/lib/java/com/google/android/material/datepicker/MaterialDatePicker.java b/lib/java/com/google/android/material/datepicker/MaterialDatePicker.java index cf5085deaee..6281f4b9a3d 100644 --- a/lib/java/com/google/android/material/datepicker/MaterialDatePicker.java +++ b/lib/java/com/google/android/material/datepicker/MaterialDatePicker.java @@ -52,9 +52,11 @@ 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; @@ -296,6 +298,16 @@ 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); + info.setContentDescription(getDateSelector().getError()); + } + }); 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 637488cce1d..69db6c4512f 100644 --- a/lib/java/com/google/android/material/datepicker/RangeDateSelector.java +++ b/lib/java/com/google/android/material/datepicker/RangeDateSelector.java @@ -23,6 +23,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.InputType; +import android.text.TextUtils; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; @@ -34,6 +35,7 @@ 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; @@ -50,6 +52,7 @@ @RestrictTo(Scope.LIBRARY_GROUP) public class RangeDateSelector implements DateSelector> { + @Nullable private CharSequence error; private String invalidRangeStartError; // "" is not considered an error private final String invalidRangeEndError = " "; @@ -176,6 +179,12 @@ public String getSelectionContentDescription(@NonNull Context context) { R.string.mtrl_picker_announce_current_range_selection, startPlaceholder, endPlaceholder); } + @Nullable + @Override + public String getError() { + return TextUtils.isEmpty(error) ? null : error.toString(); + } + @Override public int getDefaultTitleResId() { return R.string.mtrl_picker_range_header_title; @@ -199,6 +208,8 @@ 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()) { @@ -278,9 +289,7 @@ private void updateIfValidTextProposal( if (proposedTextStart == null || proposedTextEnd == null) { clearInvalidRange(startTextInput, endTextInput); listener.onIncompleteSelectionChanged(); - return; - } - if (isValidRange(proposedTextStart, proposedTextEnd)) { + } else if (isValidRange(proposedTextStart, proposedTextEnd)) { selectedStartItem = proposedTextStart; selectedEndItem = proposedTextEnd; listener.onSelectionChanged(getSelection()); @@ -288,6 +297,17 @@ private void updateIfValidTextProposal( setInvalidRange(startTextInput, endTextInput); listener.onIncompleteSelectionChanged(); } + updateError(startTextInput, endTextInput); + } + + private void updateError(@NonNull TextInputLayout start, @NonNull TextInputLayout end) { + if (!TextUtils.isEmpty(start.getError())) { + error = start.getError(); + } else if (!TextUtils.isEmpty(end.getError())) { + error = end.getError(); + } else { + error = null; + } } private void clearInvalidRange(@NonNull TextInputLayout start, @NonNull TextInputLayout end) { diff --git a/lib/java/com/google/android/material/datepicker/SingleDateSelector.java b/lib/java/com/google/android/material/datepicker/SingleDateSelector.java index 618928e3076..145207d8024 100644 --- a/lib/java/com/google/android/material/datepicker/SingleDateSelector.java +++ b/lib/java/com/google/android/material/datepicker/SingleDateSelector.java @@ -23,6 +23,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.InputType; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -32,6 +33,7 @@ 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; @@ -47,6 +49,7 @@ @RestrictTo(Scope.LIBRARY_GROUP) public class SingleDateSelector implements DateSelector { + @Nullable private CharSequence error; @Nullable private Long selectedItem; @Nullable private SimpleDateFormat textInputFormat; @@ -106,6 +109,7 @@ 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 @@ -135,11 +139,13 @@ void onValidDate(@Nullable Long day) { } else { select(day); } + error = null; listener.onSelectionChanged(getSelection()); } @Override void onInvalidDate() { + error = dateTextInput.getError(); listener.onIncompleteSelectionChanged(); } }); @@ -177,6 +183,12 @@ public String getSelectionContentDescription(@NonNull Context context) { return res.getString(R.string.mtrl_picker_announce_current_selection, placeholder); } + @Nullable + @Override + public String getError() { + return TextUtils.isEmpty(error) ? null : error.toString(); + } + @Override public int getDefaultTitleResId() { return R.string.mtrl_picker_date_header_title; diff --git a/lib/java/com/google/android/material/textfield/IndicatorViewController.java b/lib/java/com/google/android/material/textfield/IndicatorViewController.java index 856a24c440d..616fea8b178 100644 --- a/lib/java/com/google/android/material/textfield/IndicatorViewController.java +++ b/lib/java/com/google/android/material/textfield/IndicatorViewController.java @@ -119,6 +119,7 @@ final class IndicatorViewController { private boolean errorEnabled; @Nullable private TextView errorView; @Nullable private CharSequence errorViewContentDescription; + private int errorViewAccessibilityLiveRegion; private int errorTextAppearance; @Nullable private ColorStateList errorViewTextColor; @@ -501,8 +502,8 @@ void setErrorEnabled(boolean enabled) { setErrorTextAppearance(errorTextAppearance); setErrorViewTextColor(errorViewTextColor); setErrorContentDescription(errorViewContentDescription); + setErrorAccessibilityLiveRegion(errorViewAccessibilityLiveRegion); errorView.setVisibility(View.INVISIBLE); - ViewCompat.setAccessibilityLiveRegion(errorView, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); addIndicator(errorView, ERROR_INDEX); } else { hideError(); @@ -658,11 +659,22 @@ void setErrorContentDescription(@Nullable final CharSequence errorContentDescrip } } + void setErrorAccessibilityLiveRegion(final int accessibilityLiveRegion) { + this.errorViewAccessibilityLiveRegion = accessibilityLiveRegion; + if (errorView != null) { + ViewCompat.setAccessibilityLiveRegion(errorView, accessibilityLiveRegion); + } + } + @Nullable CharSequence getErrorContentDescription() { return errorViewContentDescription; } + int getErrorAccessibilityLiveRegion() { + return errorViewAccessibilityLiveRegion; + } + @ColorInt int getHelperTextViewCurrentTextColor() { return helperTextView != null ? helperTextView.getCurrentTextColor() : -1; diff --git a/lib/java/com/google/android/material/textfield/TextInputLayout.java b/lib/java/com/google/android/material/textfield/TextInputLayout.java index cdd62fdaa1f..5ef2d0cee7a 100644 --- a/lib/java/com/google/android/material/textfield/TextInputLayout.java +++ b/lib/java/com/google/android/material/textfield/TextInputLayout.java @@ -614,6 +614,10 @@ public TextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, i a.getResourceId(R.styleable.TextInputLayout_errorTextAppearance, 0); final CharSequence errorContentDescription = a.getText(R.styleable.TextInputLayout_errorContentDescription); + final int errorAccessibilityLiveRegion = + a.getInt( + R.styleable.TextInputLayout_errorAccessibilityLiveRegion, + ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); final boolean errorEnabled = a.getBoolean(R.styleable.TextInputLayout_errorEnabled, false); final int helperTextTextAppearance = @@ -636,6 +640,7 @@ public TextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, i a.getInt(R.styleable.TextInputLayout_boxBackgroundMode, BOX_BACKGROUND_NONE)); setErrorContentDescription(errorContentDescription); + setErrorAccessibilityLiveRegion(errorAccessibilityLiveRegion); setCounterOverflowTextAppearance(counterOverflowTextAppearance); setHelperTextTextAppearance(helperTextTextAppearance); @@ -2071,6 +2076,25 @@ public CharSequence getErrorContentDescription() { return indicatorViewController.getErrorContentDescription(); } + /** + * Sets an accessibility live region for the error message. + * + * @param errorAccessibilityLiveRegion Accessibility live region to set + * @attr ref com.google.android.material.R.styleable#TextInputLayout_errorAccessibilityLiveRegion + */ + public void setErrorAccessibilityLiveRegion(final int errorAccessibilityLiveRegion) { + indicatorViewController.setErrorAccessibilityLiveRegion(errorAccessibilityLiveRegion); + } + + /** + * Returns the accessibility live region of the error message. + * + * @see #setErrorAccessibilityLiveRegion(int) + */ + public int getErrorAccessibilityLiveRegion() { + return indicatorViewController.getErrorAccessibilityLiveRegion(); + } + /** * Sets an error message that will be displayed below our {@link EditText}. If the {@code error} * is {@code null}, the error message will be cleared. diff --git a/lib/java/com/google/android/material/textfield/res-public/values/public.xml b/lib/java/com/google/android/material/textfield/res-public/values/public.xml index 91b7579c299..9c707a8867e 100644 --- a/lib/java/com/google/android/material/textfield/res-public/values/public.xml +++ b/lib/java/com/google/android/material/textfield/res-public/values/public.xml @@ -53,6 +53,7 @@ + diff --git a/lib/java/com/google/android/material/textfield/res/values/attrs.xml b/lib/java/com/google/android/material/textfield/res/values/attrs.xml index be6ff8c5907..9c59726cc79 100644 --- a/lib/java/com/google/android/material/textfield/res/values/attrs.xml +++ b/lib/java/com/google/android/material/textfield/res/values/attrs.xml @@ -92,6 +92,8 @@ Should be set when the error message has special characters that a screen reader is not able to announce properly. --> + + diff --git a/lib/javatests/com/google/android/material/datepicker/RangeDateSelectorTest.java b/lib/javatests/com/google/android/material/datepicker/RangeDateSelectorTest.java index da9f0e8791b..dc3d7fa1fec 100644 --- a/lib/javatests/com/google/android/material/datepicker/RangeDateSelectorTest.java +++ b/lib/javatests/com/google/android/material/datepicker/RangeDateSelectorTest.java @@ -305,6 +305,55 @@ public void getSelectionContentDescription_startNotEmpty_endNotEmpty_returnsStar assertThat(contentDescription).isEqualTo(expected); } + @Test + public void getError_emptyDates_isNull() { + assertThat(rangeDateSelector.getError()).isNull(); + } + + @Test + public void getError_validStartDate_isNull() { + View root = getRootView(); + TextInputLayout startTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_start); + activity.setContentView(root); + startTextInput.getEditText().setText("1/1/11"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(rangeDateSelector.getError()).isNull(); + } + + @Test + public void getError_validEndDate_isNull() { + View root = getRootView(); + TextInputLayout endTextInput = root.findViewById(R.id.mtrl_picker_text_input_range_end); + activity.setContentView(root); + endTextInput.getEditText().setText("1/1/11"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(rangeDateSelector.getError()).isNull(); + } + + @Test + 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/"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(rangeDateSelector.getError()).isNotEmpty(); + } + + @Test + 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/"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(rangeDateSelector.getError()).isNotEmpty(); + } + @Test public void getSelectedRanges_fullRange() { Calendar setToStart = UtcDates.getUtcCalendar(); diff --git a/lib/javatests/com/google/android/material/datepicker/SingleDateSelectorTest.java b/lib/javatests/com/google/android/material/datepicker/SingleDateSelectorTest.java index 464488c8fa5..935cd7b0092 100644 --- a/lib/javatests/com/google/android/material/datepicker/SingleDateSelectorTest.java +++ b/lib/javatests/com/google/android/material/datepicker/SingleDateSelectorTest.java @@ -37,6 +37,7 @@ import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowLooper; @RunWith(RobolectricTestRunner.class) public class SingleDateSelectorTest { @@ -128,6 +129,33 @@ public void getSelectionContentDescription_notEmpty_returnsDate() { assertThat(contentDescription).isEqualTo(expected); } + @Test + public void getError_emptyDate_isNull() { + assertThat(singleDateSelector.getError()).isNull(); + } + + @Test + public void getError_validDate_isNull() { + 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/11"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(singleDateSelector.getError()).isNull(); + } + + @Test + 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/"); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + assertThat(singleDateSelector.getError()).isNotEmpty(); + } + @Test public void getSelectedRanges_isEmpty() { Calendar calendar = UtcDates.getUtcCalendar(); diff --git a/tests/javatests/com/google/android/material/testutils/TextInputLayoutActions.java b/tests/javatests/com/google/android/material/testutils/TextInputLayoutActions.java index e98a3a4521c..17eed4cd56e 100644 --- a/tests/javatests/com/google/android/material/testutils/TextInputLayoutActions.java +++ b/tests/javatests/com/google/android/material/testutils/TextInputLayoutActions.java @@ -124,6 +124,26 @@ public void perform(UiController uiController, View view) { }; } + public static ViewAction setErrorAccessibilityLiveRegion(final int accessibilityLiveRegion) { + return new ViewAction() { + @Override + public Matcher getConstraints() { + return isAssignableFrom(TextInputLayout.class); + } + + @Override + public String getDescription() { + return "Sets the error message's accessibility live region"; + } + + @Override + public void perform(UiController uiController, View view) { + TextInputLayout layout = (TextInputLayout) view; + layout.setErrorAccessibilityLiveRegion(accessibilityLiveRegion); + } + }; + } + public static ViewAction setHelperTextEnabled(final boolean enabled) { return new ViewAction() { @Override diff --git a/tests/javatests/com/google/android/material/textfield/TextInputLayoutTest.java b/tests/javatests/com/google/android/material/textfield/TextInputLayoutTest.java index 970892032bf..a5161d8b363 100644 --- a/tests/javatests/com/google/android/material/textfield/TextInputLayoutTest.java +++ b/tests/javatests/com/google/android/material/textfield/TextInputLayoutTest.java @@ -40,6 +40,7 @@ import static com.google.android.material.testutils.TextInputLayoutActions.setCounterEnabled; import static com.google.android.material.testutils.TextInputLayoutActions.setCounterMaxLength; import static com.google.android.material.testutils.TextInputLayoutActions.setError; +import static com.google.android.material.testutils.TextInputLayoutActions.setErrorAccessibilityLiveRegion; import static com.google.android.material.testutils.TextInputLayoutActions.setErrorContentDescription; import static com.google.android.material.testutils.TextInputLayoutActions.setErrorEnabled; import static com.google.android.material.testutils.TextInputLayoutActions.setErrorTextAppearance; @@ -68,7 +69,6 @@ import android.os.Build; import android.os.Parcelable; import android.text.TextPaint; -import android.util.AttributeSet; import android.util.SparseArray; import android.view.inputmethod.EditorInfo; import android.widget.EditText; @@ -120,14 +120,6 @@ public TestTextInputLayout(Context context) { super(context); } - public TestTextInputLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public TestTextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - @Override protected void animateToExpansionFraction(float target) { super.animateToExpansionFraction(target); @@ -499,6 +491,38 @@ public void testSetErrorContentDescription() { assertEquals(errorContentDesc, textInputLayout.getErrorContentDescription().toString()); } + @Test + public void testSetErrorAccessibilityLiveRegion() { + int errorAccessibilityLiveRegion = ViewCompat.ACCESSIBILITY_LIVE_REGION_NONE; + // Set error and error accessibility live region. + onView(withId(R.id.textinput)) + .perform(setErrorEnabled(true)) + .perform(setError(ERROR_MESSAGE_1)) + .perform(setErrorAccessibilityLiveRegion(errorAccessibilityLiveRegion)); + + final Activity activity = activityTestRule.getActivity(); + final TextInputLayout textInputLayout = activity.findViewById(R.id.textinput); + + // Assert the error accessibility live region is as expected. + assertEquals(errorAccessibilityLiveRegion, textInputLayout.getErrorAccessibilityLiveRegion()); + } + + @Test + public void testDefaultErrorAccessibilityLiveRegionIsPolite() { + // Set error. + onView(withId(R.id.textinput)) + .perform(setErrorEnabled(true)) + .perform(setError(ERROR_MESSAGE_1)); + + final Activity activity = activityTestRule.getActivity(); + final TextInputLayout textInputLayout = activity.findViewById(R.id.textinput); + + // Assert the error accessibility live region is as expected. + assertEquals( + ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE, + textInputLayout.getErrorAccessibilityLiveRegion()); + } + @Test public void testSetTypefaceUpdatesErrorView() { onView(withId(R.id.textinput))