Skip to content

Commit

Permalink
[MaterialDatePicker][a11y] Announce start/end dates
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 481152229
  • Loading branch information
paulfthomas committed Oct 17, 2022
1 parent c8108b1 commit 2f9844b
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 20 deletions.
Expand Up @@ -72,7 +72,7 @@ public CharSequence getContentDescription(
boolean valid,
boolean selected,
@Nullable CharSequence originalContentDescription) {
if (!valid || selected || !shouldShowIndicator(year, month, day)) {
if (!valid || !shouldShowIndicator(year, month, day)) {
return originalContentDescription;
}
return String.format(
Expand Down
19 changes: 17 additions & 2 deletions lib/java/com/google/android/material/datepicker/DateStrings.java
Expand Up @@ -208,13 +208,28 @@ static Pair<String, String> getDateRangeString(
* @param context the {@link Context}
* @param dayInMillis UTC milliseconds representing the first moment of the day in local timezone
* @param isToday boolean representing if the day is today
* @param isStartOfRange boolean representing if the day is the start of a range
* @param isEndOfRange boolean representing if the day is the end of a range
* @return Day content description string
*/
static String getDayContentDescription(Context context, long dayInMillis, boolean isToday) {
static String getDayContentDescription(
Context context,
long dayInMillis,
boolean isToday,
boolean isStartOfRange,
boolean isEndOfRange) {
String dayContentDescription = getOptionalYearMonthDayOfWeekDay(dayInMillis);
if (isToday) {
dayContentDescription =
String.format(
context.getString(R.string.mtrl_picker_today_description), dayContentDescription);
}
if (isStartOfRange) {
return String.format(
context.getString(R.string.mtrl_picker_start_date_description), dayContentDescription);
} else if (isEndOfRange) {
return String.format(
context.getString(R.string.mtrl_picker_today_description), dayContentDescription);
context.getString(R.string.mtrl_picker_end_date_description), dayContentDescription);
}
return dayContentDescription;
}
Expand Down
49 changes: 34 additions & 15 deletions lib/java/com/google/android/material/datepicker/MonthAdapter.java
Expand Up @@ -27,6 +27,8 @@
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.Pair;
import java.util.Calendar;
import java.util.Collection;
import java.util.Locale;
Expand Down Expand Up @@ -137,10 +139,6 @@ public TextView getView(int position, @Nullable View convertView, @NonNull ViewG
dayTextView.setTag(month);
Locale locale = dayTextView.getResources().getConfiguration().locale;
dayTextView.setText(String.format(locale, "%d", dayNumber));
long dayInMillis = month.getDay(dayNumber);
dayTextView.setContentDescription(
DateStrings.getDayContentDescription(
dayTextView.getContext(), dayInMillis, isToday(dayInMillis)));
dayTextView.setVisibility(View.VISIBLE);
dayTextView.setEnabled(true);
}
Expand Down Expand Up @@ -186,17 +184,21 @@ private void updateSelectedState(@Nullable TextView dayTextView, long date, int
if (dayTextView == null) {
return;
}

Context context = dayTextView.getContext();
String contentDescription = getDayContentDescription(context, date);
dayTextView.setContentDescription(contentDescription);

final CalendarItemStyle style;
boolean valid = calendarConstraints.getDateValidator().isValid(date);
boolean selected = false;
boolean isToday = isToday(date);
if (valid) {
dayTextView.setEnabled(true);
selected = isSelected(date);
dayTextView.setSelected(selected);
if (selected) {
style = calendarStyle.selectedDay;
} else if (isToday) {
} else if (isToday(date)) {
style = calendarStyle.todayDay;
} else {
style = calendarStyle.day;
Expand All @@ -207,8 +209,6 @@ private void updateSelectedState(@Nullable TextView dayTextView, long date, int
}

if (dayViewDecorator != null && dayNumber != NO_DAY_NUMBER) {
Context context = dayTextView.getContext();
long dayInMillis = month.getDay(dayNumber);
int year = month.year;
int month = this.month.month;

Expand All @@ -231,23 +231,42 @@ private void updateSelectedState(@Nullable TextView dayTextView, long date, int

CharSequence decoratorContentDescription =
dayViewDecorator.getContentDescription(
context,
year,
month,
dayNumber,
valid,
selected,
DateStrings.getDayContentDescription(context, dayInMillis, isToday));
context, year, month, dayNumber, valid, selected, contentDescription);
dayTextView.setContentDescription(decoratorContentDescription);
} else {
style.styleItem(dayTextView);
}
}

private String getDayContentDescription(Context context, long date) {
return DateStrings.getDayContentDescription(
context, date, isToday(date), isStartOfRange(date), isEndOfRange(date));
}

private boolean isToday(long date) {
return UtcDates.getTodayCalendar().getTimeInMillis() == date;
}

@VisibleForTesting
boolean isStartOfRange(long date) {
for (Pair<Long, Long> range : dateSelector.getSelectedRanges()) {
if (range.first == date) {
return true;
}
}
return false;
}

@VisibleForTesting
boolean isEndOfRange(long date) {
for (Pair<Long, Long> range : dateSelector.getSelectedRanges()) {
if (range.second == date) {
return true;
}
}
return false;
}

private boolean isSelected(long date) {
for (long selectedDay : dateSelector.getSelectedDays()) {
if (UtcDates.canonicalYearMonthDay(date) == UtcDates.canonicalYearMonthDay(selectedDay)) {
Expand Down
Expand Up @@ -51,5 +51,7 @@
<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>
<string name="mtrl_picker_start_date_description" description="a11y string that informs the user that the focused day is the start of a range [CHAR_LIMIT=NONE]">Start date %1$s</string>
<string name="mtrl_picker_end_date_description" description="a11y string that informs the user that the focused day is the end of a range [CHAR_LIMIT=NONE]">End date %1$s</string>

</resources>
Expand Up @@ -304,23 +304,118 @@ public void getDayContentDescription_notToday() {
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
String contentDescription =
DateStrings.getDayContentDescription(
ApplicationProvider.getApplicationContext(), startDate.getTimeInMillis(), false);
ApplicationProvider.getApplicationContext(),
startDate.getTimeInMillis(),
/* isToday= */ false,
/* isStartOfRange= */ false,
/* isEndOfRange= */ false);

assertThat(contentDescription, is("Mon, Nov 30, 2020"));
}

@Test
public void getDayContentDescription_notToday_startOfRange() {
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
String contentDescription =
DateStrings.getDayContentDescription(
ApplicationProvider.getApplicationContext(),
startDate.getTimeInMillis(),
/* isToday= */ false,
/* isStartOfRange= */ true,
/* isEndOfRange= */ false);

assertThat(contentDescription, is("Start date Mon, Nov 30, 2020"));
}

@Test
public void getDayContentDescription_notToday_endOfRange() {
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
String contentDescription =
DateStrings.getDayContentDescription(
ApplicationProvider.getApplicationContext(),
startDate.getTimeInMillis(),
/* isToday= */ false,
/* isStartOfRange= */ false,
/* isEndOfRange= */ true);

assertThat(contentDescription, is("End date Mon, Nov 30, 2020"));
}

@Test
public void getDayContentDescription_notToday_startAndEndOfRange() {
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
String contentDescription =
DateStrings.getDayContentDescription(
ApplicationProvider.getApplicationContext(),
startDate.getTimeInMillis(),
/* isToday= */ false,
/* isStartOfRange= */ true,
/* isEndOfRange= */ true);

assertThat(contentDescription, is("Start date Mon, Nov 30, 2020"));
}

@Test
public void getDayContentDescription_today() {
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
String contentDescription =
DateStrings.getDayContentDescription(
ApplicationProvider.getApplicationContext(), startDate.getTimeInMillis(), true);
ApplicationProvider.getApplicationContext(),
startDate.getTimeInMillis(),
/* isToday= */ true,
/* isStartOfRange= */ false,
/* isEndOfRange= */ false);

assertThat(contentDescription, is("Today Mon, Nov 30, 2020"));
}

@Test
public void getDayContentDescription_today_startOfRange() {
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
String contentDescription =
DateStrings.getDayContentDescription(
ApplicationProvider.getApplicationContext(),
startDate.getTimeInMillis(),
/* isToday= */ true,
/* isStartOfRange= */ true,
/* isEndOfRange= */ false);

assertThat(contentDescription, is("Start date Today Mon, Nov 30, 2020"));
}

@Test
public void getDayContentDescription_today_endOfRange() {
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
String contentDescription =
DateStrings.getDayContentDescription(
ApplicationProvider.getApplicationContext(),
startDate.getTimeInMillis(),
/* isToday= */ true,
/* isStartOfRange= */ false,
/* isEndOfRange= */ true);

assertThat(contentDescription, is("End date Today Mon, Nov 30, 2020"));
}

@Test
public void getDayContentDescription_today_startAndEndOfRange() {
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
String contentDescription =
DateStrings.getDayContentDescription(
ApplicationProvider.getApplicationContext(),
startDate.getTimeInMillis(),
/* isToday= */ true,
/* isStartOfRange= */ true,
/* isEndOfRange= */ true);

assertThat(contentDescription, is("Start date Today Mon, Nov 30, 2020"));
}

@Test
public void getYearContentDescription_notCurrent() {
String contentDescription =
DateStrings.getYearContentDescription(ApplicationProvider.getApplicationContext(), 2020);

assertThat(contentDescription, is("Navigate to year 2020"));
}

Expand All @@ -329,6 +424,7 @@ public void getYearContentDescription_current() {
String contentDescription =
DateStrings.getYearContentDescription(
ApplicationProvider.getApplicationContext(), CURRENT_YEAR);

assertThat(contentDescription, is("Navigate to current year " + CURRENT_YEAR));
}
}
Expand Up @@ -19,6 +19,7 @@

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
Expand All @@ -27,6 +28,7 @@
import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import java.util.Arrays;
import java.util.Calendar;
Expand Down Expand Up @@ -349,4 +351,50 @@ public void rowIds() {
assertEquals(3, monthFeb2019.getItemId(26));
assertEquals(5, monthMarch2019.getItemId(35));
}

@Test
public void rangeDateSelector_isStartOfRange() {
Month month = Month.create(2016, Calendar.FEBRUARY);
MonthAdapter monthAdapter =
createRangeMonthAdapter(month, new Pair<>(month.getDay(1), month.getDay(10)));

assertTrue(monthAdapter.isStartOfRange(month.getDay(1)));
}

@Test
public void rangeDateSelector_isNotStartOfRange() {
Month month = Month.create(2016, Calendar.FEBRUARY);
MonthAdapter monthAdapter =
createRangeMonthAdapter(month, new Pair<>(month.getDay(1), month.getDay(10)));

assertFalse(monthAdapter.isStartOfRange(month.getDay(2)));
}

@Test
public void rangeDateSelector_isEndOfRange() {
Month month = Month.create(2016, Calendar.FEBRUARY);
MonthAdapter monthAdapter =
createRangeMonthAdapter(month, new Pair<>(month.getDay(1), month.getDay(10)));

assertTrue(monthAdapter.isEndOfRange(month.getDay(10)));
}

@Test
public void rangeDateSelector_isNotEndOfRange() {
Month month = Month.create(2016, Calendar.FEBRUARY);
MonthAdapter monthAdapter =
createRangeMonthAdapter(month, new Pair<>(month.getDay(1), month.getDay(10)));

assertFalse(monthAdapter.isEndOfRange(month.getDay(9)));
}

private MonthAdapter createRangeMonthAdapter(Month month, Pair<Long, Long> selection) {
DateSelector<Pair<Long, Long>> dateSelector = new RangeDateSelector();
dateSelector.setSelection(selection);
return new MonthAdapter(
month,
dateSelector,
new CalendarConstraints.Builder().build(),
/* dayViewDecorator= */ null);
}
}

0 comments on commit 2f9844b

Please sign in to comment.