diff --git a/dev/integration_tests/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart index fa7554e27a8..c22519d647c 100644 --- a/dev/integration_tests/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart +++ b/dev/integration_tests/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart @@ -62,7 +62,7 @@ class _DateTimePicker extends StatelessWidget { Future _selectDate(BuildContext context) async { final DateTime? picked = await showDatePicker( context: context, - initialDate: selectedDate!, + initialDate: selectedDate, firstDate: DateTime(2015, 8), lastDate: DateTime(2101), ); @@ -120,9 +120,9 @@ class DateAndTimePickerDemo extends StatefulWidget { } class _DateAndTimePickerDemoState extends State { - DateTime _fromDate = DateTime.now(); + DateTime? _fromDate = DateTime.now(); TimeOfDay _fromTime = const TimeOfDay(hour: 7, minute: 28); - DateTime _toDate = DateTime.now(); + DateTime? _toDate = DateTime.now(); TimeOfDay _toTime = const TimeOfDay(hour: 8, minute: 28); final List _allActivities = ['hiking', 'swimming', 'boating', 'fishing']; String? _activity = 'fishing'; diff --git a/packages/flutter/lib/src/material/calendar_date_picker.dart b/packages/flutter/lib/src/material/calendar_date_picker.dart index 46bccac26f5..878439b61b0 100644 --- a/packages/flutter/lib/src/material/calendar_date_picker.dart +++ b/packages/flutter/lib/src/material/calendar_date_picker.dart @@ -60,8 +60,9 @@ const double _monthNavButtonsWidth = 108.0; class CalendarDatePicker extends StatefulWidget { /// Creates a calendar date picker. /// - /// It will display a grid of days for the [initialDate]'s month. The day - /// indicated by [initialDate] will be selected. + /// It will display a grid of days for the [initialDate]'s month, or, if that + /// is null, the [currentDate]'s month. The day indicated by [initialDate] will + /// be selected if it is not null. /// /// The optional [onDisplayedMonthChanged] callback can be used to track /// the currently displayed month. @@ -71,23 +72,20 @@ class CalendarDatePicker extends StatefulWidget { /// to start in the year selection interface with [initialCalendarMode] set /// to [DatePickerMode.year]. /// - /// The [initialDate], [firstDate], [lastDate], [onDateChanged], and - /// [initialCalendarMode] must be non-null. + /// The [lastDate] must be after or equal to [firstDate]. /// - /// [lastDate] must be after or equal to [firstDate]. + /// The [initialDate], if provided, must be between [firstDate] and [lastDate] + /// or equal to one of them. /// - /// [initialDate] must be between [firstDate] and [lastDate] or equal to - /// one of them. - /// - /// [currentDate] represents the current day (i.e. today). This + /// The [currentDate] represents the current day (i.e. today). This /// date will be highlighted in the day grid. If null, the date of /// `DateTime.now()` will be used. /// - /// If [selectableDayPredicate] is non-null, it must return `true` for the - /// [initialDate]. + /// If [selectableDayPredicate] and [initialDate] are both non-null, + /// [selectableDayPredicate] must return `true` for the [initialDate]. CalendarDatePicker({ super.key, - required DateTime initialDate, + required DateTime? initialDate, required DateTime firstDate, required DateTime lastDate, DateTime? currentDate, @@ -95,7 +93,7 @@ class CalendarDatePicker extends StatefulWidget { this.onDisplayedMonthChanged, this.initialCalendarMode = DatePickerMode.day, this.selectableDayPredicate, - }) : initialDate = DateUtils.dateOnly(initialDate), + }) : initialDate = initialDate == null ? null : DateUtils.dateOnly(initialDate), firstDate = DateUtils.dateOnly(firstDate), lastDate = DateUtils.dateOnly(lastDate), currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) { @@ -104,21 +102,26 @@ class CalendarDatePicker extends StatefulWidget { 'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.', ); assert( - !this.initialDate.isBefore(this.firstDate), + this.initialDate == null || !this.initialDate!.isBefore(this.firstDate), 'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.', ); assert( - !this.initialDate.isAfter(this.lastDate), + this.initialDate == null || !this.initialDate!.isAfter(this.lastDate), 'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.', ); assert( - selectableDayPredicate == null || selectableDayPredicate!(this.initialDate), + selectableDayPredicate == null || this.initialDate == null || selectableDayPredicate!(this.initialDate!), 'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate.', ); } /// The initially selected [DateTime] that the picker should display. - final DateTime initialDate; + /// + /// Subsequently changing this has no effect. To change the selected date, + /// change the [key] to create a new instance of the [CalendarDatePicker], and + /// provide that widget the new [initialDate]. This will reset the widget's + /// interactive state. + final DateTime? initialDate; /// The earliest allowable [DateTime] that the user can select. final DateTime firstDate; @@ -136,6 +139,11 @@ class CalendarDatePicker extends StatefulWidget { final ValueChanged? onDisplayedMonthChanged; /// The initial display of the calendar picker. + /// + /// Subsequently changing this has no effect. To change the calendar mode, + /// change the [key] to create a new instance of the [CalendarDatePicker], and + /// provide that widget a new [initialCalendarMode]. This will reset the + /// widget's interactive state. final DatePickerMode initialCalendarMode; /// Function to provide full control over which dates in the calendar can be selected. @@ -149,7 +157,7 @@ class _CalendarDatePickerState extends State { bool _announcedInitialDate = false; late DatePickerMode _mode; late DateTime _currentDisplayedMonthDate; - late DateTime _selectedDate; + DateTime? _selectedDate; final GlobalKey _monthPickerKey = GlobalKey(); final GlobalKey _yearPickerKey = GlobalKey(); late MaterialLocalizations _localizations; @@ -159,18 +167,9 @@ class _CalendarDatePickerState extends State { void initState() { super.initState(); _mode = widget.initialCalendarMode; - _currentDisplayedMonthDate = DateTime(widget.initialDate.year, widget.initialDate.month); - _selectedDate = widget.initialDate; - } - - @override - void didUpdateWidget(CalendarDatePicker oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.initialCalendarMode != oldWidget.initialCalendarMode) { - _mode = widget.initialCalendarMode; - } - if (!DateUtils.isSameDay(widget.initialDate, oldWidget.initialDate)) { - _currentDisplayedMonthDate = DateTime(widget.initialDate.year, widget.initialDate.month); + final DateTime currentDisplayedDate = widget.initialDate ?? widget.currentDate; + _currentDisplayedMonthDate = DateTime(currentDisplayedDate.year, currentDisplayedDate.month); + if (widget.initialDate != null) { _selectedDate = widget.initialDate; } } @@ -183,12 +182,13 @@ class _CalendarDatePickerState extends State { assert(debugCheckHasDirectionality(context)); _localizations = MaterialLocalizations.of(context); _textDirection = Directionality.of(context); - if (!_announcedInitialDate) { + if (!_announcedInitialDate && widget.initialDate != null) { + assert(_selectedDate != null); _announcedInitialDate = true; final bool isToday = DateUtils.isSameDay(widget.currentDate, _selectedDate); final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : ''; SemanticsService.announce( - '${_localizations.formatFullDate(_selectedDate)}$semanticLabelSuffix', + '${_localizations.formatFullDate(_selectedDate!)}$semanticLabelSuffix', _textDirection, ); } @@ -211,16 +211,18 @@ class _CalendarDatePickerState extends State { _vibrate(); setState(() { _mode = mode; - if (_mode == DatePickerMode.day) { - SemanticsService.announce( - _localizations.formatMonthYear(_selectedDate), - _textDirection, - ); - } else { - SemanticsService.announce( - _localizations.formatYear(_selectedDate), - _textDirection, - ); + if (_selectedDate != null) { + if (_mode == DatePickerMode.day) { + SemanticsService.announce( + _localizations.formatMonthYear(_selectedDate!), + _textDirection, + ); + } else { + SemanticsService.announce( + _localizations.formatYear(_selectedDate!), + _textDirection, + ); + } } }); } @@ -238,7 +240,7 @@ class _CalendarDatePickerState extends State { _vibrate(); final int daysInMonth = DateUtils.getDaysInMonth(value.year, value.month); - final int preferredDay = math.min(_selectedDate.day, daysInMonth); + final int preferredDay = math.min(_selectedDate?.day ?? 1, daysInMonth); value = value.copyWith(day: preferredDay); if (value.isBefore(widget.firstDate)) { @@ -253,7 +255,7 @@ class _CalendarDatePickerState extends State { if (_isSelectable(value)) { _selectedDate = value; - widget.onDateChanged(_selectedDate); + widget.onDateChanged(_selectedDate!); } }); } @@ -262,7 +264,7 @@ class _CalendarDatePickerState extends State { _vibrate(); setState(() { _selectedDate = value; - widget.onDateChanged(_selectedDate); + widget.onDateChanged(_selectedDate!); }); } @@ -292,7 +294,6 @@ class _CalendarDatePickerState extends State { currentDate: widget.currentDate, firstDate: widget.firstDate, lastDate: widget.lastDate, - initialDate: _currentDisplayedMonthDate, selectedDate: _currentDisplayedMonthDate, onChanged: _handleYearChanged, ), @@ -452,10 +453,15 @@ class _MonthPicker extends StatefulWidget { required this.onDisplayedMonthChanged, this.selectableDayPredicate, }) : assert(!firstDate.isAfter(lastDate)), - assert(!selectedDate.isBefore(firstDate)), - assert(!selectedDate.isAfter(lastDate)); + assert(selectedDate == null || !selectedDate.isBefore(firstDate)), + assert(selectedDate == null || !selectedDate.isAfter(lastDate)); /// The initial month to display. + /// + /// Subsequently changing this has no effect. To change the selected month, + /// change the [key] to create a new instance of the [_MonthPicker], and + /// provide that widget the new [initialMonth]. This will reset the widget's + /// interactive state. final DateTime initialMonth; /// The current date. @@ -476,7 +482,7 @@ class _MonthPicker extends StatefulWidget { /// The currently selected date. /// /// This date is highlighted in the picker. - final DateTime selectedDate; + final DateTime? selectedDate; /// Called when the user picks a day. final ValueChanged onChanged; @@ -528,17 +534,6 @@ class _MonthPickerState extends State<_MonthPicker> { _textDirection = Directionality.of(context); } - @override - void didUpdateWidget(_MonthPicker oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.initialMonth != oldWidget.initialMonth && widget.initialMonth != _currentMonth) { - // We can't interrupt this widget build with a scroll, so do it next frame - WidgetsBinding.instance.addPostFrameCallback( - (Duration timeStamp) => _showMonth(widget.initialMonth, jump: true), - ); - } - } - @override void dispose() { _pageController.dispose(); @@ -834,13 +829,13 @@ class _DayPicker extends StatefulWidget { required this.onChanged, this.selectableDayPredicate, }) : assert(!firstDate.isAfter(lastDate)), - assert(!selectedDate.isBefore(firstDate)), - assert(!selectedDate.isAfter(lastDate)); + assert(selectedDate == null || !selectedDate.isBefore(firstDate)), + assert(selectedDate == null || !selectedDate.isAfter(lastDate)); /// The currently selected date. /// /// This date is highlighted in the picker. - final DateTime selectedDate; + final DateTime? selectedDate; /// The current date at the time the picker is displayed. final DateTime currentDate; @@ -1105,13 +1100,18 @@ class YearPicker extends StatefulWidget { DateTime? currentDate, required this.firstDate, required this.lastDate, + @Deprecated( + 'This parameter has no effect and can be removed. Previously it controlled ' + 'the month that was used in "onChanged" when a new year was selected, but ' + 'now that role is filled by "selectedDate" instead. ' + 'This feature was deprecated after v3.13.0-0.3.pre.' + ) DateTime? initialDate, required this.selectedDate, required this.onChanged, this.dragStartBehavior = DragStartBehavior.start, }) : assert(!firstDate.isAfter(lastDate)), - currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()), - initialDate = DateUtils.dateOnly(initialDate ?? selectedDate); + currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()); /// The current date. /// @@ -1124,13 +1124,10 @@ class YearPicker extends StatefulWidget { /// The latest date the user is permitted to pick. final DateTime lastDate; - /// The initial date to center the year display around. - final DateTime initialDate; - /// The currently selected date. /// /// This date is highlighted in the picker. - final DateTime selectedDate; + final DateTime? selectedDate; /// Called when the user picks a year. final ValueChanged onChanged; @@ -1151,14 +1148,14 @@ class _YearPickerState extends State { @override void initState() { super.initState(); - _scrollController = ScrollController(initialScrollOffset: _scrollOffsetForYear(widget.selectedDate)); + _scrollController = ScrollController(initialScrollOffset: _scrollOffsetForYear(widget.selectedDate ?? widget.firstDate)); } @override void didUpdateWidget(YearPicker oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.selectedDate != oldWidget.selectedDate) { - _scrollController.jumpTo(_scrollOffsetForYear(widget.selectedDate)); + if (widget.selectedDate != oldWidget.selectedDate && widget.selectedDate != null) { + _scrollController.jumpTo(_scrollOffsetForYear(widget.selectedDate!)); } } @@ -1189,7 +1186,7 @@ class _YearPickerState extends State { // Backfill the _YearPicker with disabled years if necessary. final int offset = _itemCount < minYears ? (minYears - _itemCount) ~/ 2 : 0; final int year = widget.firstDate.year + index - offset; - final bool isSelected = year == widget.selectedDate.year; + final bool isSelected = year == widget.selectedDate?.year; final bool isCurrentYear = year == widget.currentDate.year; final bool isDisabled = year < widget.firstDate.year || year > widget.lastDate.year; const double decorationHeight = 36.0; @@ -1241,9 +1238,19 @@ class _YearPickerState extends State { child: yearItem, ); } else { + DateTime date = DateTime(year, widget.selectedDate?.month ?? DateTime.january); + if (date.isBefore(DateTime(widget.firstDate.year, widget.firstDate.month))) { + // Ignore firstDate.day because we're just working in years and months here. + assert(date.year == widget.firstDate.year); + date = DateTime(year, widget.firstDate.month); + } else if (date.isAfter(widget.lastDate)) { + // No need to ignore the day here because it can only be bigger than what we care about. + assert(date.year == widget.lastDate.year); + date = DateTime(year, widget.lastDate.month); + } yearItem = InkWell( key: ValueKey(year), - onTap: () => widget.onChanged(DateTime(year, widget.initialDate.month)), + onTap: () => widget.onChanged(date), statesController: MaterialStatesController(states), overlayColor: overlayColor, child: yearItem, diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index 93c699cc9d7..d5d2e1d0d0a 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -53,18 +53,19 @@ const double _kMaxTextScaleFactor = 1.3; /// The returned [Future] resolves to the date selected by the user when the /// user confirms the dialog. If the user cancels the dialog, null is returned. /// -/// When the date picker is first displayed, it will show the month of -/// [initialDate], with [initialDate] selected. +/// When the date picker is first displayed, if [initialDate] is not null, it +/// will show the month of [initialDate], with [initialDate] selected. Otherwise +/// it will show the [currentDate]'s month. /// /// The [firstDate] is the earliest allowable date. The [lastDate] is the latest -/// allowable date. [initialDate] must either fall between these dates, -/// or be equal to one of them. For each of these [DateTime] parameters, only -/// their dates are considered. Their time fields are ignored. They must all -/// be non-null. +/// allowable date. If [initialDate] is not null, it must either fall between +/// these dates, or be equal to one of them. For each of these [DateTime] +/// parameters, only their dates are considered. Their time fields are ignored. +/// They must all be non-null. /// /// The [currentDate] represents the current day (i.e. today). This /// date will be highlighted in the day grid. If null, the date of -/// `DateTime.now()` will be used. +/// [DateTime.now] will be used. /// /// An optional [initialEntryMode] argument can be used to display the date /// picker in the [DatePickerEntryMode.calendar] (a calendar month grid) @@ -123,8 +124,7 @@ const double _kMaxTextScaleFactor = 1.3; /// /// An optional [initialDatePickerMode] argument can be used to have the /// calendar date picker initially appear in the [DatePickerMode.year] or -/// [DatePickerMode.day] mode. It defaults to [DatePickerMode.day], and -/// must be non-null. +/// [DatePickerMode.day] mode. It defaults to [DatePickerMode.day]. /// /// {@macro flutter.widgets.RawDialogRoute} /// @@ -157,10 +157,9 @@ const double _kMaxTextScaleFactor = 1.3; /// * [DisplayFeatureSubScreen], which documents the specifics of how /// [DisplayFeature]s can split the screen into sub-screens. /// * [showTimePicker], which shows a dialog that contains a Material Design time picker. -/// Future showDatePicker({ required BuildContext context, - required DateTime initialDate, + DateTime? initialDate, required DateTime firstDate, required DateTime lastDate, DateTime? currentDate, @@ -188,7 +187,7 @@ Future showDatePicker({ final Icon? switchToInputEntryModeIcon, final Icon? switchToCalendarEntryModeIcon, }) async { - initialDate = DateUtils.dateOnly(initialDate); + initialDate = initialDate == null ? null : DateUtils.dateOnly(initialDate); firstDate = DateUtils.dateOnly(firstDate); lastDate = DateUtils.dateOnly(lastDate); assert( @@ -196,15 +195,15 @@ Future showDatePicker({ 'lastDate $lastDate must be on or after firstDate $firstDate.', ); assert( - !initialDate.isBefore(firstDate), + initialDate == null || !initialDate.isBefore(firstDate), 'initialDate $initialDate must be on or after firstDate $firstDate.', ); assert( - !initialDate.isAfter(lastDate), + initialDate == null || !initialDate.isAfter(lastDate), 'initialDate $initialDate must be on or before lastDate $lastDate.', ); assert( - selectableDayPredicate == null || selectableDayPredicate(initialDate), + selectableDayPredicate == null || initialDate == null || selectableDayPredicate(initialDate), 'Provided initialDate $initialDate must satisfy provided selectableDayPredicate.', ); assert(debugCheckHasMaterialLocalizations(context)); @@ -272,7 +271,7 @@ class DatePickerDialog extends StatefulWidget { /// A Material-style date picker dialog. DatePickerDialog({ super.key, - required DateTime initialDate, + DateTime? initialDate, required DateTime firstDate, required DateTime lastDate, DateTime? currentDate, @@ -291,7 +290,7 @@ class DatePickerDialog extends StatefulWidget { this.onDatePickerModeChange, this.switchToInputEntryModeIcon, this.switchToCalendarEntryModeIcon, - }) : initialDate = DateUtils.dateOnly(initialDate), + }) : initialDate = initialDate == null ? null : DateUtils.dateOnly(initialDate), firstDate = DateUtils.dateOnly(firstDate), lastDate = DateUtils.dateOnly(lastDate), currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) { @@ -300,21 +299,24 @@ class DatePickerDialog extends StatefulWidget { 'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.', ); assert( - !this.initialDate.isBefore(this.firstDate), + initialDate == null || !this.initialDate!.isBefore(this.firstDate), 'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.', ); assert( - !this.initialDate.isAfter(this.lastDate), + initialDate == null || !this.initialDate!.isAfter(this.lastDate), 'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.', ); assert( - selectableDayPredicate == null || selectableDayPredicate!(this.initialDate), + selectableDayPredicate == null || initialDate == null || selectableDayPredicate!(this.initialDate!), 'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate', ); } /// The initially selected [DateTime] that the picker should display. - final DateTime initialDate; + /// + /// If this is null, there is no selected date. A date must be selected to + /// submit the dialog. + final DateTime? initialDate; /// The earliest allowable [DateTime] that the user can select. final DateTime firstDate; @@ -410,7 +412,7 @@ class DatePickerDialog extends StatefulWidget { } class _DatePickerDialogState extends State with RestorationMixin { - late final RestorableDateTime _selectedDate = RestorableDateTime(widget.initialDate); + late final RestorableDateTimeN _selectedDate = RestorableDateTimeN(widget.initialDate); late final _RestorableDatePickerEntryMode _entryMode = _RestorableDatePickerEntryMode(widget.initialEntryMode); final _RestorableAutovalidateMode _autovalidateMode = _RestorableAutovalidateMode(AutovalidateMode.disabled); @@ -639,7 +641,7 @@ class _DatePickerDialogState extends State with RestorationMix ? localizations.datePickerHelpText : localizations.datePickerHelpText.toUpperCase() ), - titleText: localizations.formatMediumDate(_selectedDate.value), + titleText: _selectedDate.value == null ? '' : localizations.formatMediumDate(_selectedDate.value!), titleStyle: headlineStyle, orientation: orientation, isShort: orientation == Orientation.landscape, @@ -1365,7 +1367,7 @@ class _DateRangePickerDialogState extends State with Rest _entryMode.value = DatePickerEntryMode.input; case DatePickerEntryMode.input: - // Validate the range dates + // Validate the range dates if (_selectedStart.value != null && (_selectedStart.value!.isBefore(widget.firstDate) || _selectedStart.value!.isAfter(widget.lastDate))) { _selectedStart.value = null; diff --git a/packages/flutter/test/material/calendar_date_picker_test.dart b/packages/flutter/test/material/calendar_date_picker_test.dart index d952a67860f..c3b3c80845c 100644 --- a/packages/flutter/test/material/calendar_date_picker_test.dart +++ b/packages/flutter/test/material/calendar_date_picker_test.dart @@ -34,7 +34,7 @@ void main() { textDirection: textDirection, child: CalendarDatePicker( key: key, - initialDate: initialDate ?? DateTime(2016, DateTime.january, 15), + initialDate: initialDate, firstDate: firstDate ?? DateTime(2001), lastDate: lastDate ?? DateTime(2031, DateTime.december, 31), currentDate: currentDate ?? DateTime(2016, DateTime.january, 3), @@ -65,7 +65,6 @@ void main() { child: YearPicker( key: key, selectedDate: selectedDate ?? DateTime(2016, DateTime.january, 15), - initialDate: initialDate ?? DateTime(2016, DateTime.january, 15), firstDate: firstDate ?? DateTime(2001), lastDate: lastDate ?? DateTime(2031, DateTime.december, 31), currentDate: currentDate ?? DateTime(2016, DateTime.january, 3), @@ -78,6 +77,16 @@ void main() { group('CalendarDatePicker', () { testWidgets('Can select a day', (WidgetTester tester) async { + DateTime? selectedDate; + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + onDateChanged: (DateTime date) => selectedDate = date, + )); + await tester.tap(find.text('12')); + expect(selectedDate, equals(DateTime(2016, DateTime.january, 12))); + }); + + testWidgets('Can select a day with nothing first selected', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( onDateChanged: (DateTime date) => selectedDate = date, @@ -87,6 +96,31 @@ void main() { }); testWidgets('Can select a month', (WidgetTester tester) async { + DateTime? displayedMonth; + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, + )); + expect(find.text('January 2016'), findsOneWidget); + + // Go back two months + await tester.tap(previousMonthIcon); + await tester.pumpAndSettle(); + expect(find.text('December 2015'), findsOneWidget); + expect(displayedMonth, equals(DateTime(2015, DateTime.december))); + await tester.tap(previousMonthIcon); + await tester.pumpAndSettle(); + expect(find.text('November 2015'), findsOneWidget); + expect(displayedMonth, equals(DateTime(2015, DateTime.november))); + + // Go forward a month + await tester.tap(nextMonthIcon); + await tester.pumpAndSettle(); + expect(find.text('December 2015'), findsOneWidget); + expect(displayedMonth, equals(DateTime(2015, DateTime.december))); + }); + + testWidgets('Can select a month with nothing first selected', (WidgetTester tester) async { DateTime? displayedMonth; await tester.pumpWidget(calendarDatePicker( onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, @@ -111,6 +145,21 @@ void main() { }); testWidgets('Can select a year', (WidgetTester tester) async { + DateTime? displayedMonth; + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, + )); + + await tester.tap(find.text('January 2016')); // Switch to year mode. + await tester.pumpAndSettle(); + await tester.tap(find.text('2018')); + await tester.pumpAndSettle(); + expect(find.text('January 2018'), findsOneWidget); + expect(displayedMonth, equals(DateTime(2018))); + }); + + testWidgets('Can select a year with nothing first selected', (WidgetTester tester) async { DateTime? displayedMonth; await tester.pumpWidget(calendarDatePicker( onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, @@ -150,6 +199,7 @@ void main() { testWidgets('Changing year does change selected date', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDateChanged: (DateTime date) => selectedDate = date, )); await tester.tap(find.text('4')); @@ -183,6 +233,7 @@ void main() { testWidgets('Changing year does not change the month', (WidgetTester tester) async { DateTime? displayedMonth; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, )); await tester.tap(nextMonthIcon); @@ -200,6 +251,7 @@ void main() { testWidgets('Can select a year and then a day', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDateChanged: (DateTime date) => selectedDate = date, )); await tester.tap(find.text('January 2016')); // Switch to year mode. @@ -308,14 +360,39 @@ void main() { onDateChanged: (DateTime date) => selectedDate = date, onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, )); + // Selected date is now 2018-05-04 (initialDate). await tester.tap(find.text('May 2018')); + // Selected date is still 2018-05-04. await tester.pumpAndSettle(); await tester.tap(find.text('2019')); + // Selected date would become 2019-05-04 but gets clamped to the month of lastDate, so 2019-01-04. await tester.pumpAndSettle(); - // 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)); + expect(selectedDate, DateTime(2019, DateTime.january, 4)); + }); + + 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, 15), + lastDate: DateTime(2019, DateTime.january, 4), + onDateChanged: (DateTime date) => selectedDate = date, + onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, + )); + // Selected date is now 2018-05-15 (initialDate). + await tester.tap(find.text('May 2018')); + // Selected date is still 2018-05-15. + await tester.pumpAndSettle(); + await tester.tap(find.text('2019')); + // Selected date would become 2019-05-15 but gets clamped to the month of lastDate, so 2019-01-15. + // Day is now beyond the lastDate so that also gets clamped, to 2019-01-04. + await tester.pumpAndSettle(); + expect(find.text('January 2019'), findsOneWidget); + expect(displayedMonth, DateTime(2019)); + expect(selectedDate, DateTime(2019, DateTime.january, 4)); }); testWidgets('Only predicate days are selectable', (WidgetTester tester) async { @@ -350,6 +427,7 @@ void main() { testWidgets('Material2 - currentDate is highlighted', (WidgetTester tester) async { await tester.pumpWidget(calendarDatePicker( useMaterial3: false, + initialDate: DateTime(2016, DateTime.january, 15), currentDate: DateTime(2016, 1, 2), )); const Color todayColor = Color(0xff2196f3); // default primary color @@ -367,6 +445,7 @@ void main() { testWidgets('Material3 - currentDate is highlighted', (WidgetTester tester) async { await tester.pumpWidget(calendarDatePicker( useMaterial3: true, + initialDate: DateTime(2016, DateTime.january, 15), currentDate: DateTime(2016, 1, 2), )); const Color todayColor = Color(0xff6750a4); // default primary color @@ -437,107 +516,63 @@ void main() { expect(find.text('2017'), findsNothing); }); - testWidgets('Material2 - Updates to initialDate parameter is reflected in the state', (WidgetTester tester) async { - final Key pickerKey = UniqueKey(); - final DateTime initialDate = DateTime(2020, 1, 21); - final DateTime updatedDate = DateTime(1976, 2, 23); - final DateTime firstDate = DateTime(1970); - final DateTime lastDate = DateTime(2099, 31, 12); - const Color selectedColor = Color(0xff2196f3); // default primary color + for (final bool useMaterial3 in [false, true]) { + testWidgets('Updates to initialDate parameter are not reflected in the state (useMaterial3=$useMaterial3)', (WidgetTester tester) async { + final Key pickerKey = UniqueKey(); + final DateTime initialDate = DateTime(2020, 1, 21); + final DateTime updatedDate = DateTime(1976, 2, 23); + final DateTime firstDate = DateTime(1970); + final DateTime lastDate = DateTime(2099, 31, 12); + final Color selectedColor = useMaterial3 ? const Color(0xff6750a4) : const Color(0xff2196f3); // default primary color - await tester.pumpWidget(calendarDatePicker( - key: pickerKey, - useMaterial3: false, - initialDate: initialDate, - firstDate: firstDate, - lastDate: lastDate, - onDateChanged: (DateTime value) {}, - )); - await tester.pumpAndSettle(); + await tester.pumpWidget(calendarDatePicker( + key: pickerKey, + useMaterial3: useMaterial3, + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, + onDateChanged: (DateTime value) {}, + )); + await tester.pumpAndSettle(); - // Month should show as January 2020 - expect(find.text('January 2020'), findsOneWidget); - // Selected date should be painted with a colored circle. - expect( - Material.of(tester.element(find.text('21'))), - paints..circle(color: selectedColor, style: PaintingStyle.fill), - ); + // Month should show as January 2020. + expect(find.text('January 2020'), findsOneWidget); + // Selected date should be painted with a colored circle. + expect( + Material.of(tester.element(find.text('21'))), + paints..circle(color: selectedColor, style: PaintingStyle.fill), + ); - // Change to the updated initialDate - await tester.pumpWidget(calendarDatePicker( - key: pickerKey, - useMaterial3: false, - initialDate: updatedDate, - firstDate: firstDate, - lastDate: lastDate, - onDateChanged: (DateTime value) {}, - )); - // Wait for the page scroll animation to finish. - await tester.pumpAndSettle(const Duration(milliseconds: 200)); + // Change to the updated initialDate. + // This should have no effect, the initialDate is only the _initial_ date. + await tester.pumpWidget(calendarDatePicker( + key: pickerKey, + useMaterial3: useMaterial3, + initialDate: updatedDate, + firstDate: firstDate, + lastDate: lastDate, + onDateChanged: (DateTime value) {}, + )); + // Wait for the page scroll animation to finish. + await tester.pumpAndSettle(const Duration(milliseconds: 200)); - // Month should show as February 1976 - expect(find.text('January 2020'), findsNothing); - expect(find.text('February 1976'), findsOneWidget); - // Selected date should be painted with a colored circle. - expect( - Material.of(tester.element(find.text('23'))), - paints..circle(color: selectedColor, style: PaintingStyle.fill), - ); - }); + // Month should show as January 2020 still. + expect(find.text('January 2020'), findsOneWidget); + expect(find.text('February 1976'), findsNothing); + // Selected date should be painted with a colored circle. + expect( + Material.of(tester.element(find.text('21'))), + paints..circle(color: selectedColor, style: PaintingStyle.fill), + ); + }); + } - testWidgets('Material3 - Updates to initialDate parameter is reflected in the state', (WidgetTester tester) async { - final Key pickerKey = UniqueKey(); - final DateTime initialDate = DateTime(2020, 1, 21); - final DateTime updatedDate = DateTime(1976, 2, 23); - final DateTime firstDate = DateTime(1970); - final DateTime lastDate = DateTime(2099, 31, 12); - const Color selectedColor = Color(0xff6750a4); // default primary color - - await tester.pumpWidget(calendarDatePicker( - key: pickerKey, - useMaterial3: true, - initialDate: initialDate, - firstDate: firstDate, - lastDate: lastDate, - onDateChanged: (DateTime value) {}, - )); - await tester.pumpAndSettle(); - - // Month should show as January 2020 - expect(find.text('January 2020'), findsOneWidget); - // Selected date should be painted with a colored circle. - expect( - Material.of(tester.element(find.text('21'))), - paints..circle(color: selectedColor, style: PaintingStyle.fill), - ); - - // Change to the updated initialDate - await tester.pumpWidget(calendarDatePicker( - key: pickerKey, - useMaterial3: true, - initialDate: updatedDate, - firstDate: firstDate, - lastDate: lastDate, - onDateChanged: (DateTime value) {}, - )); - // Wait for the page scroll animation to finish. - await tester.pumpAndSettle(const Duration(milliseconds: 200)); - - // Month should show as February 1976 - expect(find.text('January 2020'), findsNothing); - expect(find.text('February 1976'), findsOneWidget); - // Selected date should be painted with a colored circle. - expect( - Material.of(tester.element(find.text('23'))), - paints..circle(color: selectedColor, style: PaintingStyle.fill), - ); - }); - - testWidgets('Updates to initialCalendarMode parameter is reflected in the state', (WidgetTester tester) async { + testWidgets('Updates to initialCalendarMode parameter is not reflected in the state', (WidgetTester tester) async { final Key pickerKey = UniqueKey(); await tester.pumpWidget(calendarDatePicker( key: pickerKey, + initialDate: DateTime(2016, DateTime.january, 15), initialCalendarMode: DatePickerMode.year, )); await tester.pumpAndSettle(); @@ -549,17 +584,20 @@ void main() { await tester.pumpWidget(calendarDatePicker( key: pickerKey, + initialDate: DateTime(2016, DateTime.january, 15), )); await tester.pumpAndSettle(); - // Should be in day mode. + // Should be in year mode still; updating an _initial_ parameter has no effect. expect(find.text('January 2016'), findsOneWidget); // Day/year selector - expect(find.text('15'), findsOneWidget); // day 15 in grid - expect(find.text('2016'), findsNothing); // 2016 in year grid + expect(find.text('15'), findsNothing); // day 15 in grid + expect(find.text('2016'), findsOneWidget); // 2016 in year grid }); testWidgets('Dragging more than half the width should not cause a jump', (WidgetTester tester) async { - await tester.pumpWidget(calendarDatePicker()); + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + )); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(PageView))); // This initial drag is required for the PageView to recognize the gesture, as it uses DragStartBehavior.start. @@ -579,7 +617,9 @@ void main() { group('Keyboard navigation', () { testWidgets('Can toggle to year mode', (WidgetTester tester) async { - await tester.pumpWidget(calendarDatePicker()); + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + )); expect(find.text('2016'), findsNothing); expect(find.text('January 2016'), findsOneWidget); // Navigate to the year selector and activate it. @@ -592,7 +632,9 @@ void main() { }); testWidgets('Can navigate next/previous months', (WidgetTester tester) async { - await tester.pumpWidget(calendarDatePicker()); + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + )); expect(find.text('January 2016'), findsOneWidget); // Navigate to the previous month button and activate it twice. await tester.sendKeyEvent(LogicalKeyboardKey.tab); @@ -621,6 +663,7 @@ void main() { testWidgets('Can navigate date grid with arrow keys', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDateChanged: (DateTime date) => selectedDate = date, )); // Navigate to the grid. @@ -650,6 +693,7 @@ void main() { testWidgets('Navigating with arrow keys scrolls months', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDateChanged: (DateTime date) => selectedDate = date, )); // Navigate to the grid. @@ -690,6 +734,7 @@ void main() { testWidgets('RTL text direction reverses the horizontal arrow key navigation', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDateChanged: (DateTime date) => selectedDate = date, textDirection: TextDirection.rtl, )); @@ -731,7 +776,9 @@ void main() { }); testWidgets('Selecting date vibrates', (WidgetTester tester) async { - await tester.pumpWidget(calendarDatePicker()); + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + )); await tester.tap(find.text('10')); await tester.pump(hapticFeedbackInterval); expect(feedback.hapticCount, 1); @@ -760,7 +807,9 @@ void main() { }); testWidgets('Changing modes and year vibrates', (WidgetTester tester) async { - await tester.pumpWidget(calendarDatePicker()); + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + )); await tester.tap(find.text('January 2016')); await tester.pump(hapticFeedbackInterval); expect(feedback.hapticCount, 1); @@ -774,7 +823,9 @@ void main() { testWidgets('day mode', (WidgetTester tester) async { final SemanticsHandle semantics = tester.ensureSemantics(); - await tester.pumpWidget(calendarDatePicker()); + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + )); // Year mode drop down button. expect(tester.getSemantics(find.text('January 2016')), matchesSemantics( @@ -989,6 +1040,7 @@ void main() { final SemanticsHandle semantics = tester.ensureSemantics(); await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), initialCalendarMode: DatePickerMode.year, )); @@ -1034,7 +1086,7 @@ void main() { DateTime? selectedYear; await tester.pumpWidget(yearPicker( firstDate: DateTime(2018, DateTime.june, 9), - initialDate: DateTime(2018, DateTime.july, 4), + selectedDate: DateTime(2018, DateTime.july, 4), lastDate: DateTime(2018, DateTime.december, 15), onChanged: (DateTime date) => selectedYear = date, )); @@ -1048,5 +1100,43 @@ void main() { await tester.pumpAndSettle(); expect(selectedYear, equals(DateTime(2018, DateTime.july))); }); + + testWidgets('Selecting year with no selected month uses earliest month', (WidgetTester tester) async { + DateTime? selectedYear; + await tester.pumpWidget(yearPicker( + firstDate: DateTime(2018, DateTime.june, 9), + lastDate: DateTime(2019, DateTime.december, 15), + onChanged: (DateTime date) => selectedYear = date, + )); + await tester.tap(find.text('2018')); + expect(selectedYear, equals(DateTime(2018, DateTime.june))); + await tester.pumpWidget(yearPicker( + firstDate: DateTime(2018, DateTime.june, 9), + lastDate: DateTime(2019, DateTime.december, 15), + selectedDate: DateTime(2018, DateTime.june), + onChanged: (DateTime date) => selectedYear = date, + )); + await tester.tap(find.text('2019')); + expect(selectedYear, equals(DateTime(2019, DateTime.june))); + }); + + testWidgets('Selecting year with no selected month uses January', (WidgetTester tester) async { + DateTime? selectedYear; + await tester.pumpWidget(yearPicker( + firstDate: DateTime(2018, DateTime.june, 9), + lastDate: DateTime(2019, DateTime.december, 15), + onChanged: (DateTime date) => selectedYear = date, + )); + await tester.tap(find.text('2019')); + expect(selectedYear, equals(DateTime(2019))); // january implied + await tester.pumpWidget(yearPicker( + firstDate: DateTime(2018, DateTime.june, 9), + lastDate: DateTime(2019, DateTime.december, 15), + selectedDate: DateTime(2018), + onChanged: (DateTime date) => selectedYear = date, + )); + await tester.tap(find.text('2018')); + expect(selectedYear, equals(DateTime(2018, DateTime.june))); + }); }); } diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart index de670d71315..4c47543d147 100644 --- a/packages/flutter/test/material/date_picker_test.dart +++ b/packages/flutter/test/material/date_picker_test.dart @@ -14,7 +14,7 @@ void main() { late DateTime firstDate; late DateTime lastDate; - late DateTime initialDate; + late DateTime? initialDate; late DateTime today; late SelectableDayPredicate? selectableDayPredicate; late DatePickerEntryMode initialEntryMode; @@ -1044,6 +1044,37 @@ void main() { }); }); + testWidgets('Can select a day with no initial date', (WidgetTester tester) async { + initialDate = null; + await prepareDatePicker(tester, (Future date) async { + await tester.tap(find.text('12')); + await tester.tap(find.text('OK')); + expect(await date, equals(DateTime(2016, DateTime.january, 12))); + }); + }); + + testWidgets('Can select a month with no initial date', (WidgetTester tester) async { + initialDate = null; + await prepareDatePicker(tester, (Future date) async { + await tester.tap(previousMonthIcon); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.tap(find.text('25')); + await tester.tap(find.text('OK')); + expect(await date, DateTime(2015, DateTime.december, 25)); + }); + }); + + testWidgets('Can select a year with no initial date', (WidgetTester tester) async { + initialDate = null; + await prepareDatePicker(tester, (Future date) async { + await tester.tap(find.text('January 2016')); // Switch to year mode. + await tester.pump(); + await tester.tap(find.text('2018')); + await tester.pump(); + expect(find.text('January 2018'), findsOneWidget); + }); + }); + testWidgets('Selecting date does not change displayed month', (WidgetTester tester) async { initialDate = DateTime(2020, DateTime.march, 15); await prepareDatePicker(tester, (Future date) async { @@ -1105,8 +1136,8 @@ void main() { testWidgets('Cannot select a day outside bounds', (WidgetTester tester) async { initialDate = DateTime(2017, DateTime.january, 15); - firstDate = initialDate; - lastDate = initialDate; + firstDate = initialDate!; + lastDate = initialDate!; await prepareDatePicker(tester, (Future date) async { // Earlier than firstDate. Should be ignored. await tester.tap(find.text('10')); @@ -1120,7 +1151,7 @@ void main() { testWidgets('Cannot select a month past last date', (WidgetTester tester) async { initialDate = DateTime(2017, DateTime.january, 15); - firstDate = initialDate; + firstDate = initialDate!; lastDate = DateTime(2017, DateTime.february, 20); await prepareDatePicker(tester, (Future date) async { await tester.tap(nextMonthIcon); @@ -1133,7 +1164,7 @@ void main() { testWidgets('Cannot select a month before first date', (WidgetTester tester) async { initialDate = DateTime(2017, DateTime.january, 15); firstDate = DateTime(2016, DateTime.december, 10); - lastDate = initialDate; + lastDate = initialDate!; await prepareDatePicker(tester, (Future date) async { await tester.tap(previousMonthIcon); await tester.pumpAndSettle(const Duration(seconds: 1));