Unverified Commit d19fb632 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Allow date pickers to not have selected date (#132343)

This enables our various date picker classes to have a null `initialDate`.

It also fixes the logic of some of the widgets which used to do something when you _changed_ the `initial*` parameters, which is wrong for `initial*` properties (they by definition should only impact the initial state) and wrong for properties in general (behaviour should not change based on whether the widget was built with a new value or not, that violates the reactive design principles).

Fixes https://github.com/flutter/flutter/issues/638.
parent 3f34b480
......@@ -62,7 +62,7 @@ class _DateTimePicker extends StatelessWidget {
Future<void> _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<DateAndTimePickerDemo> {
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<String> _allActivities = <String>['hiking', 'swimming', 'boating', 'fishing'];
String? _activity = 'fishing';
......
......@@ -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<DateTime?> showDatePicker({
required BuildContext context,
required DateTime initialDate,
DateTime? initialDate,
required DateTime firstDate,
required DateTime lastDate,
DateTime? currentDate,
......@@ -188,7 +187,7 @@ Future<DateTime?> 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<DateTime?> 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<DatePickerDialog> 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<DatePickerDialog> 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<DateRangePickerDialog> 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;
......
......@@ -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<DateTime?> 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<DateTime?> 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<DateTime?> 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<DateTime?> 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<DateTime?> 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<DateTime?> 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<DateTime?> date) async {
await tester.tap(previousMonthIcon);
await tester.pumpAndSettle(const Duration(seconds: 1));
......
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