Skip to content

Commit

Permalink
[MaterialDatePicker][a11y] Improve date selection announcements
Browse files Browse the repository at this point in the history
- improve selection announcement for both single and range date selectors
- properly announce start date when partially selected range

PiperOrigin-RevId: 482581175
  • Loading branch information
paulfthomas committed Oct 21, 2022
1 parent f0e5bda commit 5c5b1e8
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 24 deletions.
Expand Up @@ -90,6 +90,15 @@ public interface DateSelector<S> extends Parcelable {
@NonNull
String getSelectionDisplayString(Context context);

/**
* Returns the selection content description.
*
* @param context the {@link Context}
* @return The selection content description
*/
@NonNull
String getSelectionContentDescription(@NonNull Context context);

@StringRes
int getDefaultTitleResId();

Expand Down
Expand Up @@ -412,11 +412,14 @@ private void updateTitle(boolean textInputMode) {

@VisibleForTesting
void updateHeader(String headerText) {
headerSelectionText.setContentDescription(
String.format(getString(R.string.mtrl_picker_announce_current_selection), headerText));
headerSelectionText.setContentDescription(getHeaderContentDescription());
headerSelectionText.setText(headerText);
}

private String getHeaderContentDescription() {
return getDateSelector().getSelectionContentDescription(requireContext());
}

private void startPickerFragment() {
int themeResId = getThemeResId(requireContext());
calendar =
Expand Down
Expand Up @@ -250,7 +250,7 @@ private boolean isToday(long date) {
@VisibleForTesting
boolean isStartOfRange(long date) {
for (Pair<Long, Long> range : dateSelector.getSelectedRanges()) {
if (range.first == date) {
if (range.first != null && range.first == date) {
return true;
}
}
Expand All @@ -260,7 +260,7 @@ boolean isStartOfRange(long date) {
@VisibleForTesting
boolean isEndOfRange(long date) {
for (Pair<Long, Long> range : dateSelector.getSelectedRanges()) {
if (range.second == date) {
if (range.second != null && range.second == date) {
return true;
}
}
Expand Down
Expand Up @@ -97,9 +97,6 @@ public Pair<Long, Long> getSelection() {
@NonNull
@Override
public Collection<Pair<Long, Long>> getSelectedRanges() {
if (selectedStartItem == null || selectedEndItem == null) {
return new ArrayList<>();
}
ArrayList<Pair<Long, Long>> ranges = new ArrayList<>();
Pair<Long, Long> range = new Pair<>(selectedStartItem, selectedEndItem);
ranges.add(range);
Expand Down Expand Up @@ -159,6 +156,24 @@ public String getSelectionDisplayString(@NonNull Context context) {
dateRangeStrings.second);
}

@NonNull
@Override
public String getSelectionContentDescription(@NonNull Context context) {
Resources res = context.getResources();
Pair<String, String> dateRangeStrings =
DateStrings.getDateRangeString(selectedStartItem, selectedEndItem);
String startPlaceholder =
dateRangeStrings.first == null
? res.getString(R.string.mtrl_picker_announce_current_selection_none)
: dateRangeStrings.first;
String endPlaceholder =
dateRangeStrings.second == null
? res.getString(R.string.mtrl_picker_announce_current_selection_none)
: dateRangeStrings.second;
return res.getString(
R.string.mtrl_picker_announce_current_range_selection, startPlaceholder, endPlaceholder);
}

@Override
public int getDefaultTitleResId() {
return R.string.mtrl_picker_range_header_title;
Expand Down Expand Up @@ -220,11 +235,14 @@ void onInvalidDate() {

endEditText.addTextChangedListener(
new DateFormatTextWatcher(formatHint, format, endTextInput, constraints) {

@Override
void onValidDate(@Nullable Long day) {
proposedTextEnd = day;
updateIfValidTextProposal(startTextInput, endTextInput, listener);
}

@Override
void onInvalidDate() {
proposedTextEnd = null;
updateIfValidTextProposal(startTextInput, endTextInput, listener);
Expand Down
Expand Up @@ -154,6 +154,17 @@ public String getSelectionDisplayString(@NonNull Context context) {
return res.getString(R.string.mtrl_picker_date_header_selected, startString);
}

@NonNull
@Override
public String getSelectionContentDescription(@NonNull Context context) {
Resources res = context.getResources();
String placeholder =
selectedItem == null
? res.getString(R.string.mtrl_picker_announce_current_selection_none)
: DateStrings.getYearMonthDay(selectedItem);
return res.getString(R.string.mtrl_picker_announce_current_selection, placeholder);
}

@Override
public int getDefaultTitleResId() {
return R.string.mtrl_picker_date_header_title;
Expand Down
Expand Up @@ -48,6 +48,8 @@
<string name="mtrl_picker_toggle_to_day_selection" description="a11y string to indicate this button switches the user to choosing a day [CHAR_LIMIT=NONE]">Tap to switch to Calendar view</string>
<string name="mtrl_picker_day_of_week_column_header" description="a11y string to indicate this is a header for a column of days for one day of the week (e.g., Monday) [CHAR_LIMIT=NONE]">Column of days: %1$s</string>
<string name="mtrl_picker_announce_current_selection" description="a11y string read on selection change to indicate the new selection [CHAR_LIMIT=NONE]">Current selection: %1$s</string>
<string name="mtrl_picker_announce_current_range_selection" description="a11y string read on range selection change to indicate the new selection [CHAR_LIMIT=NONE]">Start date selection: %1$s – End date selection: %2$s</string>
<string name="mtrl_picker_announce_current_selection_none" description="a11y string read on selection change to indicate an empty selection [CHAR_LIMIT=NONE]">none</string>
<string name="mtrl_picker_navigate_to_year_description" description="a11y string that informs the user that tapping this button will switch the year [CHAR_LIMIT=NONE]">Navigate to year %1$d</string>
<string name="mtrl_picker_navigate_to_current_year_description" description="a11y string that informs the user that tapping this button will switch the current year [CHAR_LIMIT=NONE]">Navigate to current year %1$d</string>
<string name="mtrl_picker_today_description" description="a11y string that informs the user that the focused day is today [CHAR_LIMIT=NONE]">Today %1$s</string>
Expand Down
Expand Up @@ -20,6 +20,7 @@
import static com.google.common.truth.Truth.assertThat;

import android.content.Context;
import android.content.res.Resources;
import androidx.appcompat.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
Expand Down Expand Up @@ -47,13 +48,15 @@ public class RangeDateSelectorTest {
private RangeDateSelector rangeDateSelector;
private MonthAdapter adapter;
private Context context;
private Resources res;
private AppCompatActivity activity;

@Before
public void setupMonthAdapters() {
ApplicationProvider.getApplicationContext().setTheme(R.style.Theme_MaterialComponents_Light);
activity = Robolectric.buildActivity(AppCompatActivity.class).setup().get();
context = activity.getApplicationContext();
res = context.getResources();
GridView gridView = new GridView(context);
rangeDateSelector = new RangeDateSelector();
adapter =
Expand Down Expand Up @@ -222,6 +225,88 @@ public void textInputHintValidWithPTLocale() {
assertThat(endTextInput.getPlaceholderText().toString()).isEqualTo(expectedDateFormat);
}

@Test
public void getSelectionContentDescription_startEmpty_endEmpty_returnsStartAndEndNone() {
rangeDateSelector.setSelection(new Pair<>(null, null));
String contentDescription = rangeDateSelector.getSelectionContentDescription(context);

String expected =
res.getString(
R.string.mtrl_picker_announce_current_range_selection,
res.getString(R.string.mtrl_picker_announce_current_selection_none),
res.getString(R.string.mtrl_picker_announce_current_selection_none));
assertThat(contentDescription).isEqualTo(expected);
}

@Test
public void getSelectionContentDescription_startNotEmpty_endEmpty_returnsEndNone() {
Calendar setToStart = UtcDates.getUtcCalendar();
setToStart.set(2004, Calendar.MARCH, 5);
rangeDateSelector.setSelection(new Pair<>(setToStart.getTimeInMillis(), null));
String contentDescription = rangeDateSelector.getSelectionContentDescription(context);

String expected =
res.getString(
R.string.mtrl_picker_announce_current_range_selection,
"Mar 5, 2004",
res.getString(R.string.mtrl_picker_announce_current_selection_none));
assertThat(contentDescription).isEqualTo(expected);
}

@Test
public void getSelectionContentDescription_startEmpty_endNotEmpty_returnsStartNone() {
Calendar setToEnd = UtcDates.getUtcCalendar();
setToEnd.set(2005, Calendar.FEBRUARY, 1);
rangeDateSelector.setSelection(new Pair<>(null, setToEnd.getTimeInMillis()));
String contentDescription = rangeDateSelector.getSelectionContentDescription(context);

String expected =
res.getString(
R.string.mtrl_picker_announce_current_range_selection,
res.getString(R.string.mtrl_picker_announce_current_selection_none),
"Feb 1, 2005");
assertThat(contentDescription).isEqualTo(expected);
}

@Test
public void getSelectionContentDescription_startNotEmpty_endNotEmpty_returnsStartAndEndDates() {
Calendar setToStart = UtcDates.getUtcCalendar();
setToStart.set(2004, Calendar.MARCH, 5);
Calendar setToEnd = UtcDates.getUtcCalendar();
setToEnd.set(2005, Calendar.FEBRUARY, 1);
rangeDateSelector.setSelection(
new Pair<>(setToStart.getTimeInMillis(), setToEnd.getTimeInMillis()));
String contentDescription = rangeDateSelector.getSelectionContentDescription(context);

String expected =
res.getString(
R.string.mtrl_picker_announce_current_range_selection, "Mar 5, 2004", "Feb 1, 2005");
assertThat(contentDescription).isEqualTo(expected);
}

@Test
public void getSelectedRanges_fullRange() {
Calendar setToStart = UtcDates.getUtcCalendar();
setToStart.set(2004, Calendar.MARCH, 5);
Calendar setToEnd = UtcDates.getUtcCalendar();
setToEnd.set(2005, Calendar.FEBRUARY, 1);
Pair<Long, Long> selection =
new Pair<>(setToStart.getTimeInMillis(), setToEnd.getTimeInMillis());
rangeDateSelector.setSelection(selection);

assertThat(rangeDateSelector.getSelectedRanges()).containsExactly(selection);
}

@Test
public void getSelectedRanges_partialRange() {
Calendar setToStart = UtcDates.getUtcCalendar();
setToStart.set(2004, Calendar.MARCH, 5);
Pair<Long, Long> selection = new Pair<>(setToStart.getTimeInMillis(), null);
rangeDateSelector.setSelection(selection);

assertThat(rangeDateSelector.getSelectedRanges()).containsExactly(selection);
}

private View getRootView() {
return rangeDateSelector.onCreateTextInputView(
LayoutInflater.from(context),
Expand Down
Expand Up @@ -17,11 +17,10 @@

import com.google.android.material.test.R;

import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assert.assertThat;
import static com.google.common.truth.Truth.assertThat;

import android.content.Context;
import android.content.res.Resources;
import androidx.appcompat.app.AppCompatActivity;
import android.widget.GridView;
import androidx.test.core.app.ApplicationProvider;
Expand All @@ -40,12 +39,15 @@ public class SingleDateSelectorTest {

private SingleDateSelector singleDateSelector;
private MonthAdapter adapter;
private Context context;
private Resources res;

@Before
public void setupMonthAdapters() {
ApplicationProvider.getApplicationContext().setTheme(R.style.Theme_MaterialComponents_Light);
AppCompatActivity activity = Robolectric.buildActivity(AppCompatActivity.class).setup().get();
Context context = activity.getApplicationContext();
context = activity.getApplicationContext();
res = context.getResources();
GridView gridView = new GridView(context);
singleDateSelector = new SingleDateSelector();
adapter =
Expand All @@ -60,19 +62,19 @@ public void setupMonthAdapters() {
@Test
public void dateSelectorMaintainsSelectionAfterParceling() {
int position = 8;
assertThat(adapter.withinMonth(position), is(true));
assertThat(adapter.withinMonth(position)).isTrue();
singleDateSelector.select(adapter.getItem(position));
long expected = adapter.getItem(position);
SingleDateSelector singleDateSelectorFromParcel =
ParcelableTestUtils.parcelAndCreate(singleDateSelector, SingleDateSelector.CREATOR);
assertThat(singleDateSelectorFromParcel.getSelection(), is(expected));
assertThat(singleDateSelectorFromParcel.getSelection()).isEqualTo(expected);
}

@Test
public void nullDateSelectionFromParcel() {
SingleDateSelector singleDateSelector =
ParcelableTestUtils.parcelAndCreate(this.singleDateSelector, SingleDateSelector.CREATOR);
assertThat(singleDateSelector.getSelection(), nullValue());
assertThat(singleDateSelector.getSelection()).isNull();
}

@Test
Expand All @@ -84,17 +86,49 @@ public void setSelectionDirectly() {

resultCalendar.setTimeInMillis(singleDateSelector.getSelection());

assertThat(resultCalendar.get(Calendar.DAY_OF_MONTH), is(5));
assertThat(resultCalendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(5);
assertThat(
singleDateSelector
.getSelectedDays()
.contains(UtcDates.canonicalYearMonthDay(setTo.getTimeInMillis())),
is(true));
singleDateSelector
.getSelectedDays()
.contains(UtcDates.canonicalYearMonthDay(setTo.getTimeInMillis())))
.isTrue();
assertThat(
singleDateSelector
.getSelectedDays()
.contains(
UtcDates.canonicalYearMonthDay(UtcDates.getTodayCalendar().getTimeInMillis())),
is(false));
singleDateSelector
.getSelectedDays()
.contains(
UtcDates.canonicalYearMonthDay(UtcDates.getTodayCalendar().getTimeInMillis())))
.isFalse();
}

@Test
public void getSelectionContentDescription_empty_returnsNone() {
singleDateSelector.setSelection(null);
String contentDescription = singleDateSelector.getSelectionContentDescription(context);

String expected =
res.getString(
R.string.mtrl_picker_announce_current_selection,
res.getString(R.string.mtrl_picker_announce_current_selection_none));
assertThat(contentDescription).isEqualTo(expected);
}

@Test
public void getSelectionContentDescription_notEmpty_returnsDate() {
Calendar calendar = UtcDates.getUtcCalendar();
calendar.set(2016, Calendar.FEBRUARY, 1);
singleDateSelector.setSelection(calendar.getTimeInMillis());
String contentDescription = singleDateSelector.getSelectionContentDescription(context);

String expected = res.getString(R.string.mtrl_picker_announce_current_selection, "Feb 1, 2016");
assertThat(contentDescription).isEqualTo(expected);
}

@Test
public void getSelectedRanges_isEmpty() {
Calendar calendar = UtcDates.getUtcCalendar();
calendar.set(2016, Calendar.FEBRUARY, 1);
singleDateSelector.setSelection(calendar.getTimeInMillis());

assertThat(singleDateSelector.getSelectedRanges().isEmpty()).isTrue();
}
}

0 comments on commit 5c5b1e8

Please sign in to comment.