Reverts "Add ability to provide selectableDayPredicate for showDateRangePicker (#150355)" (#154089)

Reverts: flutter/flutter#150355
Initiated by: chingjun
Reason for reverting: this is breaking an internal test.
Original PR Author: Chuckame

Reviewed By: {victorsanni, MitchellGoodwin}

This change reverts the following previous change:
- Closes #63973
- Now able to provide a `selectableDayPredicate`
- No breaking change (same behavior as before if not set)
- Reuse the same feature as the DatePicker: non-selectable days are greyed and not clickable
- Reuse the same error message as if the user set a wrong date range
- Made public `CalendarDateRangePicker`, actually the same for `CalendarDatePicker`, to allow using the range picker outside the `showDateRangePicker` bottom sheet modal

## Examples
- Disable days after the next non selectable day when start day has been selected

https://github.com/flutter/flutter/assets/16419143/2a2be325-1e2f-470c-8b17-b4ed5b2ad43e

- Select a range including non-selectable days

<img width="363" alt="image" src="https://github.com/flutter/flutter/assets/16419143/21e32def-46f0-41d6-974f-281a0405e28e">
This commit is contained in:
auto-submit[bot] 2024-08-26 04:33:30 +00:00 committed by GitHub
parent 9308a799c6
commit a7eaca934d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 19 additions and 179 deletions

View File

@ -1012,18 +1012,6 @@ class _DatePickerHeader extends StatelessWidget {
}
}
/// Signature for predicating enabled dates in date range pickers.
///
/// The [selectedStartDay] and [selectedEndDay] are the currently selected start
/// and end dates of a date range, which conditionally enables or disables each
/// date in the picker based on the user selection. (Example: in a hostel's room
/// selection, you are not able to select the end date after the next
/// non-selectable day).
///
/// See [showDateRangePicker], which has a [SelectableDayForRangePredicate]
/// parameter used to specify allowable days in the date range picker.
typedef SelectableDayForRangePredicate = bool Function(DateTime day, DateTime? selectedStartDay, DateTime? selectedEndDay);
/// Shows a full screen modal dialog containing a Material Design date range
/// picker.
///
@ -1152,8 +1140,11 @@ Future<DateTimeRange?> showDateRangePicker({
TextInputType keyboardType = TextInputType.datetime,
final Icon? switchToInputEntryModeIcon,
final Icon? switchToCalendarEntryModeIcon,
SelectableDayForRangePredicate? selectableDayPredicate,
}) async {
assert(
initialDateRange == null || !initialDateRange.start.isAfter(initialDateRange.end),
"initialDateRange's start date must not be after it's end date.",
);
initialDateRange = initialDateRange == null ? null : DateUtils.datesOnly(initialDateRange);
firstDate = DateUtils.dateOnly(firstDate);
lastDate = DateUtils.dateOnly(lastDate);
@ -1177,16 +1168,6 @@ Future<DateTimeRange?> showDateRangePicker({
initialDateRange == null || !initialDateRange.end.isAfter(lastDate),
"initialDateRange's end date must be on or before lastDate $lastDate.",
);
assert(
initialDateRange == null || selectableDayPredicate == null ||
selectableDayPredicate(initialDateRange.start, initialDateRange.start, initialDateRange.end),
"initialDateRange's start date must be selectable.",
);
assert(
initialDateRange == null || selectableDayPredicate == null ||
selectableDayPredicate(initialDateRange.end, initialDateRange.start, initialDateRange.end),
"initialDateRange's end date must be selectable.",
);
currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now());
assert(debugCheckHasMaterialLocalizations(context));
@ -1195,7 +1176,6 @@ Future<DateTimeRange?> showDateRangePicker({
firstDate: firstDate,
lastDate: lastDate,
currentDate: currentDate,
selectableDayPredicate: selectableDayPredicate,
initialEntryMode: initialEntryMode,
helpText: helpText,
cancelText: cancelText,
@ -1304,7 +1284,6 @@ class DateRangePickerDialog extends StatefulWidget {
this.restorationId,
this.switchToInputEntryModeIcon,
this.switchToCalendarEntryModeIcon,
this.selectableDayPredicate,
});
/// The date range that the date range picker starts with when it opens.
@ -1433,9 +1412,6 @@ class DateRangePickerDialog extends StatefulWidget {
/// {@macro flutter.material.date_picker.switchToCalendarEntryModeIcon}
final Icon? switchToCalendarEntryModeIcon;
/// Function to provide full control over which [DateTime] can be selected.
final SelectableDayForRangePredicate? selectableDayPredicate;
@override
State<DateRangePickerDialog> createState() => _DateRangePickerDialogState();
}
@ -1498,14 +1474,18 @@ class _DateRangePickerDialogState extends State<DateRangePickerDialog> with Rest
case DatePickerEntryMode.input:
// Validate the range dates
if (_selectedStart.value != null && _selectedEnd.value != null && _selectedStart.value!.isAfter(_selectedEnd.value!)) {
_selectedEnd.value = null;
}
if (_selectedStart.value != null && !_isDaySelectable(_selectedStart.value!)) {
if (_selectedStart.value != null &&
(_selectedStart.value!.isBefore(widget.firstDate) || _selectedStart.value!.isAfter(widget.lastDate))) {
_selectedStart.value = null;
// With no valid start date, having an end date makes no sense for the UI.
_selectedEnd.value = null;
} else if (_selectedEnd.value != null && !_isDaySelectable(_selectedEnd.value!)) {
}
if (_selectedEnd.value != null &&
(_selectedEnd.value!.isBefore(widget.firstDate) || _selectedEnd.value!.isAfter(widget.lastDate))) {
_selectedEnd.value = null;
}
// If invalid range (start after end), then just use the start date
if (_selectedStart.value != null && _selectedEnd.value != null && _selectedStart.value!.isAfter(_selectedEnd.value!)) {
_selectedEnd.value = null;
}
_entryMode.value = DatePickerEntryMode.calendar;
@ -1517,16 +1497,6 @@ class _DateRangePickerDialogState extends State<DateRangePickerDialog> with Rest
});
}
bool _isDaySelectable(DateTime day) {
if (day.isBefore(widget.firstDate) || day.isAfter(widget.lastDate)) {
return false;
}
if (widget.selectableDayPredicate == null) {
return true;
}
return widget.selectableDayPredicate!(day, _selectedStart.value, _selectedEnd.value);
}
void _handleStartDateChanged(DateTime? date) {
setState(() => _selectedStart.value = date);
}
@ -1565,7 +1535,6 @@ class _DateRangePickerDialogState extends State<DateRangePickerDialog> with Rest
selectedEndDate: _selectedEnd.value,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
selectableDayPredicate: widget.selectableDayPredicate,
currentDate: widget.currentDate,
onStartDateChanged: _handleStartDateChanged,
onEndDateChanged: _handleEndDateChanged,
@ -1618,7 +1587,6 @@ class _DateRangePickerDialogState extends State<DateRangePickerDialog> with Rest
initialEndDate: _selectedEnd.value,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
selectableDayPredicate: widget.selectableDayPredicate,
onStartDateChanged: _handleStartDateChanged,
onEndDateChanged: _handleEndDateChanged,
autofocus: true,
@ -1714,7 +1682,6 @@ class _CalendarRangePickerDialog extends StatelessWidget {
required this.onCancel,
required this.confirmText,
required this.helpText,
required this.selectableDayPredicate,
this.entryModeButton,
});
@ -1722,7 +1689,6 @@ class _CalendarRangePickerDialog extends StatelessWidget {
final DateTime? selectedEndDate;
final DateTime firstDate;
final DateTime lastDate;
final SelectableDayForRangePredicate? selectableDayPredicate;
final DateTime? currentDate;
final ValueChanged<DateTime> onStartDateChanged;
final ValueChanged<DateTime?> onEndDateChanged;
@ -1847,7 +1813,6 @@ class _CalendarRangePickerDialog extends StatelessWidget {
currentDate: currentDate,
onStartDateChanged: onStartDateChanged,
onEndDateChanged: onEndDateChanged,
selectableDayPredicate: selectableDayPredicate,
),
),
);
@ -1873,7 +1838,6 @@ class _CalendarDateRangePicker extends StatefulWidget {
DateTime? initialEndDate,
required DateTime firstDate,
required DateTime lastDate,
required this.selectableDayPredicate,
DateTime? currentDate,
required this.onStartDateChanged,
required this.onEndDateChanged,
@ -1904,9 +1868,6 @@ class _CalendarDateRangePicker extends StatefulWidget {
/// The latest allowable [DateTime] that the user can select.
final DateTime lastDate;
/// Function to provide full control over which [DateTime] can be selected.
final SelectableDayForRangePredicate? selectableDayPredicate;
/// The [DateTime] representing today. It will be highlighted in the day grid.
final DateTime currentDate;
@ -1917,7 +1878,7 @@ class _CalendarDateRangePicker extends StatefulWidget {
final ValueChanged<DateTime?>? onEndDateChanged;
@override
State<_CalendarDateRangePicker> createState() => _CalendarDateRangePickerState();
_CalendarDateRangePickerState createState() => _CalendarDateRangePickerState();
}
class _CalendarDateRangePickerState extends State<_CalendarDateRangePicker> {
@ -2020,7 +1981,6 @@ class _CalendarDateRangePickerState extends State<_CalendarDateRangePicker> {
lastDate: widget.lastDate,
displayedMonth: month,
onChanged: _updateSelection,
selectableDayPredicate: widget.selectableDayPredicate,
);
}
@ -2409,7 +2369,6 @@ class _MonthItem extends StatefulWidget {
required this.firstDate,
required this.lastDate,
required this.displayedMonth,
required this.selectableDayPredicate,
}) : assert(!firstDate.isAfter(lastDate)),
assert(selectedDateStart == null || !selectedDateStart.isBefore(firstDate)),
assert(selectedDateEnd == null || !selectedDateEnd.isBefore(firstDate)),
@ -2442,8 +2401,6 @@ class _MonthItem extends StatefulWidget {
/// The month whose days are displayed by this picker.
final DateTime displayedMonth;
final SelectableDayForRangePredicate? selectableDayPredicate;
@override
_MonthItemState createState() => _MonthItemState();
}
@ -2509,10 +2466,7 @@ class _MonthItemState extends State<_MonthItem> {
Widget _buildDayItem(BuildContext context, DateTime dayToBuild, int firstDayOffset, int daysInMonth) {
final int day = dayToBuild.day;
final bool isDisabled = dayToBuild.isAfter(widget.lastDate) ||
dayToBuild.isBefore(widget.firstDate) ||
widget.selectableDayPredicate != null &&
!widget.selectableDayPredicate!(dayToBuild, widget.selectedDateStart, widget.selectedDateEnd);
final bool isDisabled = dayToBuild.isAfter(widget.lastDate) || dayToBuild.isBefore(widget.firstDate);
final bool isRangeSelected = widget.selectedDateStart != null && widget.selectedDateEnd != null;
final bool isSelectedDayStart = widget.selectedDateStart != null && dayToBuild.isAtSameMomentAs(widget.selectedDateStart!);
final bool isSelectedDayEnd = widget.selectedDateEnd != null && dayToBuild.isAtSameMomentAs(widget.selectedDateEnd!);
@ -2754,7 +2708,7 @@ class _DayItemState extends State<_DayItem> {
if (widget.isSelectedDayStart || widget.isSelectedDayEnd) {
// The selected start and end dates gets a circle background
// highlight, and a contrasting text color.
itemStyle = itemStyle?.apply(color: dayForegroundColor);
itemStyle = textTheme.bodyMedium?.apply(color: dayForegroundColor);
decoration = BoxDecoration(
color: dayBackgroundColor,
shape: BoxShape.circle,
@ -2777,15 +2731,12 @@ class _DayItemState extends State<_DayItem> {
style: _HighlightPainterStyle.highlightAll,
textDirection: textDirection,
);
if (widget.isDisabled) {
itemStyle = itemStyle?.apply(color: colorScheme.onSurface.withOpacity(0.38));
}
} else if (widget.isDisabled) {
itemStyle = itemStyle?.apply(color: colorScheme.onSurface.withOpacity(0.38));
itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.onSurface.withOpacity(0.38));
} else if (widget.isToday) {
// The current day gets a different text color and a circle stroke
// border.
itemStyle = itemStyle?.apply(color: colorScheme.primary);
itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.primary);
decoration = BoxDecoration(
border: Border.all(color: colorScheme.primary),
shape: BoxShape.circle,
@ -3071,7 +3022,6 @@ class _InputDateRangePicker extends StatefulWidget {
required DateTime lastDate,
required this.onStartDateChanged,
required this.onEndDateChanged,
required this.selectableDayPredicate,
this.helpText,
this.errorFormatText,
this.errorInvalidText,
@ -3145,8 +3095,6 @@ class _InputDateRangePicker extends StatefulWidget {
/// {@macro flutter.material.datePickerDialog}
final TextInputType keyboardType;
final SelectableDayForRangePredicate? selectableDayPredicate;
@override
_InputDateRangePickerState createState() => _InputDateRangePickerState();
}
@ -3226,22 +3174,12 @@ class _InputDateRangePickerState extends State<_InputDateRangePicker> {
String? _validateDate(DateTime? date) {
if (date == null) {
return widget.errorFormatText ?? MaterialLocalizations.of(context).invalidDateFormatLabel;
} else if (!_isDaySelectable(date)) {
} else if (date.isBefore(widget.firstDate) || date.isAfter(widget.lastDate)) {
return widget.errorInvalidText ?? MaterialLocalizations.of(context).dateOutOfRangeLabel;
}
return null;
}
bool _isDaySelectable(DateTime day) {
if (day.isBefore(widget.firstDate) || day.isAfter(widget.lastDate)) {
return false;
}
if (widget.selectableDayPredicate == null) {
return true;
}
return widget.selectableDayPredicate!(day, _startDate, _endDate);
}
void _updateController(TextEditingController controller, String text, bool selectText) {
TextEditingValue textEditingValue = controller.value.copyWith(text: text);
if (selectText) {

View File

@ -61,7 +61,6 @@ void main() {
Future<void> Function(Future<DateTimeRange?> date) callback, {
TextDirection textDirection = TextDirection.ltr,
bool useMaterial3 = false,
SelectableDayForRangePredicate? selectableDayPredicate,
}) async {
late BuildContext buttonContext;
await tester.pumpWidget(MaterialApp(
@ -101,7 +100,6 @@ void main() {
fieldEndLabelText: fieldEndLabelText,
helpText: helpText,
saveText: saveText,
selectableDayPredicate: selectableDayPredicate,
builder: (BuildContext context, Widget? child) {
return Directionality(
textDirection: textDirection,
@ -406,71 +404,6 @@ void main() {
});
});
testWidgets('Can select a range even if the range includes non selectable days', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.tap(find.text('12').first);
await tester.tap(find.text('14').first);
await tester.tap(find.text('SAVE'));
// The day 13 is not selectable, but the range is still valid.
expect(await range, DateTimeRange(
start: DateTime(2016, DateTime.january, 12),
end: DateTime(2016, DateTime.january, 14),
));
}, selectableDayPredicate: (DateTime day, _, __) => day.day != 13);
});
testWidgets('Cannot select a day inside bounds but not selectable', (WidgetTester tester) async {
initialDateRange = DateTimeRange(
start: DateTime(2017, DateTime.january, 13),
end: DateTime(2017, DateTime.january, 14),
);
firstDate = DateTime(2017, DateTime.january, 12);
lastDate = DateTime(2017, DateTime.january, 16);
await preparePicker(tester, (Future<DateTimeRange?> range) async {
// Non-selectable date. Should be ignored.
await tester.tap(find.text('15'));
await tester.tap(find.text('SAVE'));
// We should still be on the initial date.
expect(await range, initialDateRange);
}, selectableDayPredicate: (DateTime day, _, __) => day.day != 15);
});
testWidgets('Selectable date becoming non selectable when selected start day', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.tap(find.text('12').first);
await tester.pumpAndSettle();
await tester.tap(find.text('11').first);
await tester.pumpAndSettle();
await tester.tap(find.text('14').first);
await tester.pumpAndSettle();
await tester.tap(find.text('SAVE'));
expect(await range, DateTimeRange(
start: DateTime(2016, DateTime.january, 12),
end: DateTime(2016, DateTime.january, 14),
));
}, selectableDayPredicate: (DateTime day, DateTime? selectedStart, DateTime? selectedEnd) {
if (selectedEnd == null && selectedStart != null) {
return day == selectedStart || day.isAfter(selectedStart);
}
return true;
});
});
testWidgets('selectableDayPredicate should be called with the selected start and end dates', (WidgetTester tester) async {
initialDateRange = DateTimeRange(
start: DateTime(2017, DateTime.january, 13),
end: DateTime(2017, DateTime.january, 15),
);
firstDate = DateTime(2017, DateTime.january, 12);
lastDate = DateTime(2017, DateTime.january, 16);
await preparePicker(tester, (Future<DateTimeRange?> range) async {
}, selectableDayPredicate: (DateTime day, DateTime? selectedStartDate, DateTime? selectedEndDate) {
expect(selectedStartDate, DateTime(2017, DateTime.january, 13));
expect(selectedEndDate, DateTime(2017, DateTime.january, 15));
return true;
});
});
testWidgets('Can switch from calendar to input entry mode', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
expect(find.byType(TextField), findsNothing);
@ -553,22 +486,6 @@ void main() {
});
});
testWidgets('Non-selectable start date', (WidgetTester tester) async {
// Even if start and end dates are selected, the start date is not selectable
// ending up to no date selected at all in calendar mode.
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.enterText(find.byType(TextField).at(0), '12/24/2016');
await tester.enterText(find.byType(TextField).at(1), '12/25/2016');
await tester.tap(find.byIcon(Icons.calendar_today));
await tester.pumpAndSettle();
expect(find.text('Start Date'), findsOneWidget);
expect(find.text('End Date'), findsOneWidget);
}, selectableDayPredicate: (DateTime day, DateTime? selectedStart, DateTime? selectedEnd) {
return day != DateTime(2016, DateTime.december, 24);
});
});
testWidgets('Invalid end date', (WidgetTester tester) async {
// Invalid end date should only have a start date selected
await preparePicker(tester, (Future<DateTimeRange?> range) async {
@ -582,21 +499,6 @@ void main() {
});
});
testWidgets('Non-selectable end date', (WidgetTester tester) async {
// The end date is not selectable, so only the start date should be selected.
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.enterText(find.byType(TextField).at(0), '12/24/2016');
await tester.enterText(find.byType(TextField).at(1), '12/25/2016');
await tester.tap(find.byIcon(Icons.calendar_today));
await tester.pumpAndSettle();
expect(find.text('Dec 24'), findsOneWidget);
expect(find.text('End Date'), findsOneWidget);
}, selectableDayPredicate: (DateTime day, DateTime? selectedStart, DateTime? selectedEnd) {
return day != DateTime(2016, DateTime.december, 25);
});
});
testWidgets('Invalid range', (WidgetTester tester) async {
// Start date after end date should just use the start date
await preparePicker(tester, (Future<DateTimeRange?> range) async {