Skip to content

Commit 2f9844b

Browse files
committed
[MaterialDatePicker][a11y] Announce start/end dates
PiperOrigin-RevId: 481152229
1 parent c8108b1 commit 2f9844b

File tree

6 files changed

+200
-20
lines changed

6 files changed

+200
-20
lines changed

catalog/java/io/material/catalog/datepicker/CircleIndicatorDecorator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public CharSequence getContentDescription(
7272
boolean valid,
7373
boolean selected,
7474
@Nullable CharSequence originalContentDescription) {
75-
if (!valid || selected || !shouldShowIndicator(year, month, day)) {
75+
if (!valid || !shouldShowIndicator(year, month, day)) {
7676
return originalContentDescription;
7777
}
7878
return String.format(

lib/java/com/google/android/material/datepicker/DateStrings.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,28 @@ static Pair<String, String> getDateRangeString(
208208
* @param context the {@link Context}
209209
* @param dayInMillis UTC milliseconds representing the first moment of the day in local timezone
210210
* @param isToday boolean representing if the day is today
211+
* @param isStartOfRange boolean representing if the day is the start of a range
212+
* @param isEndOfRange boolean representing if the day is the end of a range
211213
* @return Day content description string
212214
*/
213-
static String getDayContentDescription(Context context, long dayInMillis, boolean isToday) {
215+
static String getDayContentDescription(
216+
Context context,
217+
long dayInMillis,
218+
boolean isToday,
219+
boolean isStartOfRange,
220+
boolean isEndOfRange) {
214221
String dayContentDescription = getOptionalYearMonthDayOfWeekDay(dayInMillis);
215222
if (isToday) {
223+
dayContentDescription =
224+
String.format(
225+
context.getString(R.string.mtrl_picker_today_description), dayContentDescription);
226+
}
227+
if (isStartOfRange) {
228+
return String.format(
229+
context.getString(R.string.mtrl_picker_start_date_description), dayContentDescription);
230+
} else if (isEndOfRange) {
216231
return String.format(
217-
context.getString(R.string.mtrl_picker_today_description), dayContentDescription);
232+
context.getString(R.string.mtrl_picker_end_date_description), dayContentDescription);
218233
}
219234
return dayContentDescription;
220235
}

lib/java/com/google/android/material/datepicker/MonthAdapter.java

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import android.widget.TextView;
2828
import androidx.annotation.NonNull;
2929
import androidx.annotation.Nullable;
30+
import androidx.annotation.VisibleForTesting;
31+
import androidx.core.util.Pair;
3032
import java.util.Calendar;
3133
import java.util.Collection;
3234
import java.util.Locale;
@@ -137,10 +139,6 @@ public TextView getView(int position, @Nullable View convertView, @NonNull ViewG
137139
dayTextView.setTag(month);
138140
Locale locale = dayTextView.getResources().getConfiguration().locale;
139141
dayTextView.setText(String.format(locale, "%d", dayNumber));
140-
long dayInMillis = month.getDay(dayNumber);
141-
dayTextView.setContentDescription(
142-
DateStrings.getDayContentDescription(
143-
dayTextView.getContext(), dayInMillis, isToday(dayInMillis)));
144142
dayTextView.setVisibility(View.VISIBLE);
145143
dayTextView.setEnabled(true);
146144
}
@@ -186,17 +184,21 @@ private void updateSelectedState(@Nullable TextView dayTextView, long date, int
186184
if (dayTextView == null) {
187185
return;
188186
}
187+
188+
Context context = dayTextView.getContext();
189+
String contentDescription = getDayContentDescription(context, date);
190+
dayTextView.setContentDescription(contentDescription);
191+
189192
final CalendarItemStyle style;
190193
boolean valid = calendarConstraints.getDateValidator().isValid(date);
191194
boolean selected = false;
192-
boolean isToday = isToday(date);
193195
if (valid) {
194196
dayTextView.setEnabled(true);
195197
selected = isSelected(date);
196198
dayTextView.setSelected(selected);
197199
if (selected) {
198200
style = calendarStyle.selectedDay;
199-
} else if (isToday) {
201+
} else if (isToday(date)) {
200202
style = calendarStyle.todayDay;
201203
} else {
202204
style = calendarStyle.day;
@@ -207,8 +209,6 @@ private void updateSelectedState(@Nullable TextView dayTextView, long date, int
207209
}
208210

209211
if (dayViewDecorator != null && dayNumber != NO_DAY_NUMBER) {
210-
Context context = dayTextView.getContext();
211-
long dayInMillis = month.getDay(dayNumber);
212212
int year = month.year;
213213
int month = this.month.month;
214214

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

232232
CharSequence decoratorContentDescription =
233233
dayViewDecorator.getContentDescription(
234-
context,
235-
year,
236-
month,
237-
dayNumber,
238-
valid,
239-
selected,
240-
DateStrings.getDayContentDescription(context, dayInMillis, isToday));
234+
context, year, month, dayNumber, valid, selected, contentDescription);
241235
dayTextView.setContentDescription(decoratorContentDescription);
242236
} else {
243237
style.styleItem(dayTextView);
244238
}
245239
}
246240

241+
private String getDayContentDescription(Context context, long date) {
242+
return DateStrings.getDayContentDescription(
243+
context, date, isToday(date), isStartOfRange(date), isEndOfRange(date));
244+
}
245+
247246
private boolean isToday(long date) {
248247
return UtcDates.getTodayCalendar().getTimeInMillis() == date;
249248
}
250249

250+
@VisibleForTesting
251+
boolean isStartOfRange(long date) {
252+
for (Pair<Long, Long> range : dateSelector.getSelectedRanges()) {
253+
if (range.first == date) {
254+
return true;
255+
}
256+
}
257+
return false;
258+
}
259+
260+
@VisibleForTesting
261+
boolean isEndOfRange(long date) {
262+
for (Pair<Long, Long> range : dateSelector.getSelectedRanges()) {
263+
if (range.second == date) {
264+
return true;
265+
}
266+
}
267+
return false;
268+
}
269+
251270
private boolean isSelected(long date) {
252271
for (long selectedDay : dateSelector.getSelectedDays()) {
253272
if (UtcDates.canonicalYearMonthDay(date) == UtcDates.canonicalYearMonthDay(selectedDay)) {

lib/java/com/google/android/material/datepicker/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,7 @@
5151
<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>
5252
<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>
5353
<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>
54+
<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>
55+
<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>
5456

5557
</resources>

lib/javatests/com/google/android/material/datepicker/DateStringsTest.java

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,23 +304,118 @@ public void getDayContentDescription_notToday() {
304304
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
305305
String contentDescription =
306306
DateStrings.getDayContentDescription(
307-
ApplicationProvider.getApplicationContext(), startDate.getTimeInMillis(), false);
307+
ApplicationProvider.getApplicationContext(),
308+
startDate.getTimeInMillis(),
309+
/* isToday= */ false,
310+
/* isStartOfRange= */ false,
311+
/* isEndOfRange= */ false);
312+
308313
assertThat(contentDescription, is("Mon, Nov 30, 2020"));
309314
}
310315

316+
@Test
317+
public void getDayContentDescription_notToday_startOfRange() {
318+
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
319+
String contentDescription =
320+
DateStrings.getDayContentDescription(
321+
ApplicationProvider.getApplicationContext(),
322+
startDate.getTimeInMillis(),
323+
/* isToday= */ false,
324+
/* isStartOfRange= */ true,
325+
/* isEndOfRange= */ false);
326+
327+
assertThat(contentDescription, is("Start date Mon, Nov 30, 2020"));
328+
}
329+
330+
@Test
331+
public void getDayContentDescription_notToday_endOfRange() {
332+
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
333+
String contentDescription =
334+
DateStrings.getDayContentDescription(
335+
ApplicationProvider.getApplicationContext(),
336+
startDate.getTimeInMillis(),
337+
/* isToday= */ false,
338+
/* isStartOfRange= */ false,
339+
/* isEndOfRange= */ true);
340+
341+
assertThat(contentDescription, is("End date Mon, Nov 30, 2020"));
342+
}
343+
344+
@Test
345+
public void getDayContentDescription_notToday_startAndEndOfRange() {
346+
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
347+
String contentDescription =
348+
DateStrings.getDayContentDescription(
349+
ApplicationProvider.getApplicationContext(),
350+
startDate.getTimeInMillis(),
351+
/* isToday= */ false,
352+
/* isStartOfRange= */ true,
353+
/* isEndOfRange= */ true);
354+
355+
assertThat(contentDescription, is("Start date Mon, Nov 30, 2020"));
356+
}
357+
311358
@Test
312359
public void getDayContentDescription_today() {
313360
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
314361
String contentDescription =
315362
DateStrings.getDayContentDescription(
316-
ApplicationProvider.getApplicationContext(), startDate.getTimeInMillis(), true);
363+
ApplicationProvider.getApplicationContext(),
364+
startDate.getTimeInMillis(),
365+
/* isToday= */ true,
366+
/* isStartOfRange= */ false,
367+
/* isEndOfRange= */ false);
368+
317369
assertThat(contentDescription, is("Today Mon, Nov 30, 2020"));
318370
}
319371

372+
@Test
373+
public void getDayContentDescription_today_startOfRange() {
374+
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
375+
String contentDescription =
376+
DateStrings.getDayContentDescription(
377+
ApplicationProvider.getApplicationContext(),
378+
startDate.getTimeInMillis(),
379+
/* isToday= */ true,
380+
/* isStartOfRange= */ true,
381+
/* isEndOfRange= */ false);
382+
383+
assertThat(contentDescription, is("Start date Today Mon, Nov 30, 2020"));
384+
}
385+
386+
@Test
387+
public void getDayContentDescription_today_endOfRange() {
388+
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
389+
String contentDescription =
390+
DateStrings.getDayContentDescription(
391+
ApplicationProvider.getApplicationContext(),
392+
startDate.getTimeInMillis(),
393+
/* isToday= */ true,
394+
/* isStartOfRange= */ false,
395+
/* isEndOfRange= */ true);
396+
397+
assertThat(contentDescription, is("End date Today Mon, Nov 30, 2020"));
398+
}
399+
400+
@Test
401+
public void getDayContentDescription_today_startAndEndOfRange() {
402+
startDate = setupLocalizedCalendar(Locale.US, 2020, 10, 30);
403+
String contentDescription =
404+
DateStrings.getDayContentDescription(
405+
ApplicationProvider.getApplicationContext(),
406+
startDate.getTimeInMillis(),
407+
/* isToday= */ true,
408+
/* isStartOfRange= */ true,
409+
/* isEndOfRange= */ true);
410+
411+
assertThat(contentDescription, is("Start date Today Mon, Nov 30, 2020"));
412+
}
413+
320414
@Test
321415
public void getYearContentDescription_notCurrent() {
322416
String contentDescription =
323417
DateStrings.getYearContentDescription(ApplicationProvider.getApplicationContext(), 2020);
418+
324419
assertThat(contentDescription, is("Navigate to year 2020"));
325420
}
326421

@@ -329,6 +424,7 @@ public void getYearContentDescription_current() {
329424
String contentDescription =
330425
DateStrings.getYearContentDescription(
331426
ApplicationProvider.getApplicationContext(), CURRENT_YEAR);
427+
332428
assertThat(contentDescription, is("Navigate to current year " + CURRENT_YEAR));
333429
}
334430
}

lib/javatests/com/google/android/material/datepicker/MonthAdapterTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import static com.google.common.truth.Truth.assertThat;
2121
import static org.junit.Assert.assertEquals;
22+
import static org.junit.Assert.assertFalse;
2223
import static org.junit.Assert.assertNotNull;
2324
import static org.junit.Assert.assertNull;
2425
import static org.junit.Assert.assertTrue;
@@ -27,6 +28,7 @@
2728
import android.os.Parcel;
2829
import androidx.annotation.NonNull;
2930
import androidx.annotation.Nullable;
31+
import androidx.core.util.Pair;
3032
import androidx.test.core.app.ApplicationProvider;
3133
import java.util.Arrays;
3234
import java.util.Calendar;
@@ -349,4 +351,50 @@ public void rowIds() {
349351
assertEquals(3, monthFeb2019.getItemId(26));
350352
assertEquals(5, monthMarch2019.getItemId(35));
351353
}
354+
355+
@Test
356+
public void rangeDateSelector_isStartOfRange() {
357+
Month month = Month.create(2016, Calendar.FEBRUARY);
358+
MonthAdapter monthAdapter =
359+
createRangeMonthAdapter(month, new Pair<>(month.getDay(1), month.getDay(10)));
360+
361+
assertTrue(monthAdapter.isStartOfRange(month.getDay(1)));
362+
}
363+
364+
@Test
365+
public void rangeDateSelector_isNotStartOfRange() {
366+
Month month = Month.create(2016, Calendar.FEBRUARY);
367+
MonthAdapter monthAdapter =
368+
createRangeMonthAdapter(month, new Pair<>(month.getDay(1), month.getDay(10)));
369+
370+
assertFalse(monthAdapter.isStartOfRange(month.getDay(2)));
371+
}
372+
373+
@Test
374+
public void rangeDateSelector_isEndOfRange() {
375+
Month month = Month.create(2016, Calendar.FEBRUARY);
376+
MonthAdapter monthAdapter =
377+
createRangeMonthAdapter(month, new Pair<>(month.getDay(1), month.getDay(10)));
378+
379+
assertTrue(monthAdapter.isEndOfRange(month.getDay(10)));
380+
}
381+
382+
@Test
383+
public void rangeDateSelector_isNotEndOfRange() {
384+
Month month = Month.create(2016, Calendar.FEBRUARY);
385+
MonthAdapter monthAdapter =
386+
createRangeMonthAdapter(month, new Pair<>(month.getDay(1), month.getDay(10)));
387+
388+
assertFalse(monthAdapter.isEndOfRange(month.getDay(9)));
389+
}
390+
391+
private MonthAdapter createRangeMonthAdapter(Month month, Pair<Long, Long> selection) {
392+
DateSelector<Pair<Long, Long>> dateSelector = new RangeDateSelector();
393+
dateSelector.setSelection(selection);
394+
return new MonthAdapter(
395+
month,
396+
dateSelector,
397+
new CalendarConstraints.Builder().build(),
398+
/* dayViewDecorator= */ null);
399+
}
352400
}

0 commit comments

Comments
 (0)