mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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:
parent
9308a799c6
commit
a7eaca934d
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user