Unverified Commit 2a1f26c4 authored by Lexycon's avatar Lexycon Committed by GitHub

Fix material date picker behavior when changing year (#130486)

This PR changes the material date picker behavior when changing the year so that it matches the native picker and the material component guideline. (#81547)

See material component guideline for the date picker: [Material component date-picker behavior](https://m3.material.io/components/date-pickers/guidelines#1531a81f-4052-4a75-a20d-228c7e110156)
See also: [Material components android discussion](https://github.com/material-components/material-components-android/issues/1723)

When selecting another year in the native picker, the same day will be selected (by respecting the boundaries of the date picker). The current material date picker does not select any day when changing the year. This will lead to confusion if the user presses OK and the year does not get updated.

So here is my suggestion:
It will try to preselect the day like the native picker:
 - respecting the boundaries of the date picker (firstDate, lastDate)
 - changing from leapyear 29th february will set 28th february if not a leapyear is selected
 - only set the day if it is selectable (selectableDayPredicate)

The calendar shown in the recording was setup with this parameters:
```
firstDate: DateTime(2016, DateTime.june, 9),
initialDate: DateTime(2018, DateTime.may, 4),
lastDate: DateTime(2021, DateTime.january, 15),
```
 
https://github.com/flutter/flutter/assets/13588771/3041c296-b9d0-4078-88cd-d1135fc343b3

Fixes #81547
parent 9690ef58
......@@ -237,6 +237,10 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
void _handleYearChanged(DateTime value) {
_vibrate();
final int daysInMonth = DateUtils.getDaysInMonth(value.year, value.month);
final int preferredDay = math.min(_selectedDate.day, daysInMonth);
value = value.copyWith(day: preferredDay);
if (value.isBefore(widget.firstDate)) {
value = widget.firstDate;
} else if (value.isAfter(widget.lastDate)) {
......@@ -246,6 +250,11 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
setState(() {
_mode = DatePickerMode.day;
_handleMonthChanged(value);
if (_isSelectable(value)) {
_selectedDate = value;
widget.onDateChanged(_selectedDate);
}
});
}
......@@ -257,6 +266,10 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
});
}
bool _isSelectable(DateTime date) {
return widget.selectableDayPredicate == null || widget.selectableDayPredicate!.call(date);
}
Widget _buildPicker() {
switch (_mode) {
case DatePickerMode.day:
......
......@@ -147,7 +147,7 @@ void main() {
expect(find.text('31'), findsNothing);
});
testWidgets('Changing year does not change selected date', (WidgetTester tester) async {
testWidgets('Changing year does change selected date', (WidgetTester tester) async {
DateTime? selectedDate;
await tester.pumpWidget(calendarDatePicker(
onDateChanged: (DateTime date) => selectedDate = date,
......@@ -158,7 +158,26 @@ void main() {
await tester.pumpAndSettle();
await tester.tap(find.text('2018'));
await tester.pumpAndSettle();
expect(selectedDate, equals(DateTime(2016, DateTime.january, 4)));
expect(selectedDate, equals(DateTime(2018, DateTime.january, 4)));
});
testWidgets('Changing year for february 29th', (WidgetTester tester) async {
DateTime? selectedDate;
await tester.pumpWidget(calendarDatePicker(
initialDate: DateTime(2020, DateTime.february, 29),
onDateChanged: (DateTime date) => selectedDate = date,
));
await tester.tap(find.text('February 2020'));
await tester.pumpAndSettle();
await tester.tap(find.text('2018'));
await tester.pumpAndSettle();
expect(selectedDate, equals(DateTime(2018, DateTime.february, 28)));
await tester.tap(find.text('February 2018'));
await tester.pumpAndSettle();
await tester.tap(find.text('2020'));
await tester.pumpAndSettle();
// Changing back to 2020 the 29th is not selected anymore.
expect(selectedDate, equals(DateTime(2020, DateTime.february, 28)));
});
testWidgets('Changing year does not change the month', (WidgetTester tester) async {
......@@ -260,11 +279,13 @@ void main() {
});
testWidgets('Selecting firstDate year respects firstDate', (WidgetTester tester) async {
DateTime? selectedDate;
DateTime? displayedMonth;
await tester.pumpWidget(calendarDatePicker(
firstDate: DateTime(2016, DateTime.june, 9),
initialDate: DateTime(2018, DateTime.may, 4),
lastDate: DateTime(2019, DateTime.january, 15),
onDateChanged: (DateTime date) => selectedDate = date,
onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
));
await tester.tap(find.text('May 2018'));
......@@ -274,14 +295,17 @@ void main() {
// Month should be clamped to June as the range starts at June 2016.
expect(find.text('June 2016'), findsOneWidget);
expect(displayedMonth, DateTime(2016, DateTime.june));
expect(selectedDate, DateTime(2016, DateTime.june, 9));
});
testWidgets('Selecting lastDate year respects lastDate', (WidgetTester tester) async {
DateTime? selectedDate;
DateTime? displayedMonth;
await tester.pumpWidget(calendarDatePicker(
firstDate: DateTime(2016, DateTime.june, 9),
initialDate: DateTime(2018, DateTime.may, 4),
lastDate: DateTime(2019, DateTime.january, 15),
onDateChanged: (DateTime date) => selectedDate = date,
onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
));
await tester.tap(find.text('May 2018'));
......@@ -291,6 +315,7 @@ void main() {
// Month should be clamped to January as the range ends at January 2019.
expect(find.text('January 2019'), findsOneWidget);
expect(displayedMonth, DateTime(2019));
expect(selectedDate, DateTime(2019, DateTime.january, 15));
});
testWidgets('Only predicate days are selectable', (WidgetTester tester) async {
......
......@@ -874,14 +874,14 @@ void main() {
});
});
testWidgets('Changing year does not change selected date', (WidgetTester tester) async {
testWidgets('Changing year does change selected date', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime?> date) async {
await tester.tap(find.text('January 2016'));
await tester.pump();
await tester.tap(find.text('2018'));
await tester.pump();
await tester.tap(find.text('OK'));
expect(await date, equals(DateTime(2016, DateTime.january, 15)));
expect(await date, equals(DateTime(2018, DateTime.january, 15)));
});
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment