Unverified Commit 142b526f authored by Darren Austin's avatar Darren Austin Committed by GitHub

Material Date Picker redesign (#50546)

Date Picker UI redesign
parent 25ef78e2
...@@ -41,17 +41,23 @@ ConstructorGenerator generateMaterialConstructor = (LocaleInfo locale) { ...@@ -41,17 +41,23 @@ ConstructorGenerator generateMaterialConstructor = (LocaleInfo locale) {
const MaterialLocalization${locale.camelCase()}({ const MaterialLocalization${locale.camelCase()}({
String localeName = '$localeName', String localeName = '$localeName',
@required intl.DateFormat fullYearFormat, @required intl.DateFormat fullYearFormat,
@required intl.DateFormat compactDateFormat,
@required intl.DateFormat shortDateFormat,
@required intl.DateFormat mediumDateFormat, @required intl.DateFormat mediumDateFormat,
@required intl.DateFormat longDateFormat, @required intl.DateFormat longDateFormat,
@required intl.DateFormat yearMonthFormat, @required intl.DateFormat yearMonthFormat,
@required intl.DateFormat shortMonthDayFormat,
@required intl.NumberFormat decimalFormat, @required intl.NumberFormat decimalFormat,
@required intl.NumberFormat twoDigitZeroPaddedFormat, @required intl.NumberFormat twoDigitZeroPaddedFormat,
}) : super( }) : super(
localeName: localeName, localeName: localeName,
fullYearFormat: fullYearFormat, fullYearFormat: fullYearFormat,
compactDateFormat: compactDateFormat,
shortDateFormat: shortDateFormat,
mediumDateFormat: mediumDateFormat, mediumDateFormat: mediumDateFormat,
longDateFormat: longDateFormat, longDateFormat: longDateFormat,
yearMonthFormat: yearMonthFormat, yearMonthFormat: yearMonthFormat,
shortMonthDayFormat: shortMonthDayFormat,
decimalFormat: decimalFormat, decimalFormat: decimalFormat,
twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat, twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat,
);'''; );''';
...@@ -63,15 +69,18 @@ const String materialFactoryDeclaration = ''' ...@@ -63,15 +69,18 @@ const String materialFactoryDeclaration = '''
GlobalMaterialLocalizations getMaterialTranslation( GlobalMaterialLocalizations getMaterialTranslation(
Locale locale, Locale locale,
intl.DateFormat fullYearFormat, intl.DateFormat fullYearFormat,
intl.DateFormat compactDateFormat,
intl.DateFormat shortDateFormat,
intl.DateFormat mediumDateFormat, intl.DateFormat mediumDateFormat,
intl.DateFormat longDateFormat, intl.DateFormat longDateFormat,
intl.DateFormat yearMonthFormat, intl.DateFormat yearMonthFormat,
intl.DateFormat shortMonthDayFormat,
intl.NumberFormat decimalFormat, intl.NumberFormat decimalFormat,
intl.NumberFormat twoDigitZeroPaddedFormat, intl.NumberFormat twoDigitZeroPaddedFormat,
) {'''; ) {''';
const String materialFactoryArguments = const String materialFactoryArguments =
'fullYearFormat: fullYearFormat, mediumDateFormat: mediumDateFormat, longDateFormat: longDateFormat, yearMonthFormat: yearMonthFormat, decimalFormat: decimalFormat, twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat'; 'fullYearFormat: fullYearFormat, compactDateFormat: compactDateFormat, shortDateFormat: shortDateFormat, mediumDateFormat: mediumDateFormat, longDateFormat: longDateFormat, yearMonthFormat: yearMonthFormat, shortMonthDayFormat: shortMonthDayFormat, decimalFormat: decimalFormat, twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat';
const String materialSupportedLanguagesConstant = 'kMaterialSupportedLanguages'; const String materialSupportedLanguagesConstant = 'kMaterialSupportedLanguages';
......
...@@ -46,7 +46,6 @@ export 'src/material/colors.dart'; ...@@ -46,7 +46,6 @@ export 'src/material/colors.dart';
export 'src/material/constants.dart'; export 'src/material/constants.dart';
export 'src/material/data_table.dart'; export 'src/material/data_table.dart';
export 'src/material/data_table_source.dart'; export 'src/material/data_table_source.dart';
export 'src/material/date_picker.dart';
export 'src/material/debug.dart'; export 'src/material/debug.dart';
export 'src/material/dialog.dart'; export 'src/material/dialog.dart';
export 'src/material/dialog_theme.dart'; export 'src/material/dialog_theme.dart';
...@@ -86,6 +85,7 @@ export 'src/material/outline_button.dart'; ...@@ -86,6 +85,7 @@ export 'src/material/outline_button.dart';
export 'src/material/page.dart'; export 'src/material/page.dart';
export 'src/material/page_transitions_theme.dart'; export 'src/material/page_transitions_theme.dart';
export 'src/material/paginated_data_table.dart'; export 'src/material/paginated_data_table.dart';
export 'src/material/pickers/pickers.dart';
export 'src/material/popup_menu.dart'; export 'src/material/popup_menu.dart';
export 'src/material/popup_menu_theme.dart'; export 'src/material/popup_menu_theme.dart';
export 'src/material/progress_indicator.dart'; export 'src/material/progress_indicator.dart';
......
...@@ -224,6 +224,29 @@ abstract class MaterialLocalizations { ...@@ -224,6 +224,29 @@ abstract class MaterialLocalizations {
/// Full unabbreviated year format, e.g. 2017 rather than 17. /// Full unabbreviated year format, e.g. 2017 rather than 17.
String formatYear(DateTime date); String formatYear(DateTime date);
/// Formats the date in a compact format.
///
/// Usually just the numeric values for the for day, month and year are used.
///
/// Examples:
///
/// - US English: 02/21/2019
/// - Russian: 21.02.2019
///
/// See also:
/// * [parseCompactDate], which will convert a compact date string to a [DateTime].
String formatCompactDate(DateTime date);
/// Formats the date using a short-width format.
///
/// Includes the abbreviation of the month, the day and year.
///
/// Examples:
///
/// - US English: Feb 21, 2019
/// - Russian: 21 февр. 2019 г.
String formatShortDate(DateTime date);
/// Formats the date using a medium-width format. /// Formats the date using a medium-width format.
/// ///
/// Abbreviates month and days of week. This appears in the header of the date /// Abbreviates month and days of week. This appears in the header of the date
...@@ -252,6 +275,24 @@ abstract class MaterialLocalizations { ...@@ -252,6 +275,24 @@ abstract class MaterialLocalizations {
/// in the date picker invoked using [showDatePicker]. /// in the date picker invoked using [showDatePicker].
String formatMonthYear(DateTime date); String formatMonthYear(DateTime date);
/// Formats the month and day of the given [date].
///
/// Examples:
///
/// - US English: Feb 21
/// - Russian: 21 февр.
String formatShortMonthDay(DateTime date);
/// Converts the given compact date formatted string into a [DateTime].
///
/// The format of the string must be a valid compact date format for the
/// given locale. If the text doesn't represent a valid date, `null` will be
/// returned.
///
/// See also:
/// * [formatCompactDate], which will convert a [DateTime] into a string in the compact format.
DateTime parseCompactDate(String inputString);
/// List of week day names in narrow format, usually 1- or 2-letter /// List of week day names in narrow format, usually 1- or 2-letter
/// abbreviations of full names. /// abbreviations of full names.
/// ///
...@@ -437,6 +478,23 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { ...@@ -437,6 +478,23 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
'December', 'December',
]; ];
/// Returns the number of days in a month, according to the proleptic
/// Gregorian calendar.
///
/// This applies the leap year logic introduced by the Gregorian reforms of
/// 1582. It will not give valid results for dates prior to that time.
int _getDaysInMonth(int year, int month) {
if (month == DateTime.february) {
final bool isLeapYear = (year % 4 == 0) && (year % 100 != 0) ||
(year % 400 == 0);
if (isLeapYear)
return 29;
return 28;
}
const List<int> daysInMonth = <int>[31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return daysInMonth[month - 1];
}
@override @override
String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat = false }) { String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat = false }) {
final TimeOfDayFormat format = timeOfDayFormat(alwaysUse24HourFormat: alwaysUse24HourFormat); final TimeOfDayFormat format = timeOfDayFormat(alwaysUse24HourFormat: alwaysUse24HourFormat);
...@@ -470,6 +528,21 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { ...@@ -470,6 +528,21 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
@override @override
String formatYear(DateTime date) => date.year.toString(); String formatYear(DateTime date) => date.year.toString();
@override
String formatCompactDate(DateTime date) {
// Assumes US mm/dd/yyyy format
final String month = _formatTwoDigitZeroPad(date.month);
final String day = _formatTwoDigitZeroPad(date.day);
final String year = date.year.toString().padLeft(4, '0');
return '$month/$day/$year';
}
@override
String formatShortDate(DateTime date) {
final String month = _shortMonths[date.month - DateTime.january];
return '$month ${date.day}, ${date.year}';
}
@override @override
String formatMediumDate(DateTime date) { String formatMediumDate(DateTime date) {
final String day = _shortWeekdays[date.weekday - DateTime.monday]; final String day = _shortWeekdays[date.weekday - DateTime.monday];
...@@ -490,6 +563,37 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { ...@@ -490,6 +563,37 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
return '$month $year'; return '$month $year';
} }
@override
String formatShortMonthDay(DateTime date) {
final String month = _shortMonths[date.month - DateTime.january];
return '$month ${date.day}';
}
@override
DateTime parseCompactDate(String inputString) {
// Assumes US mm/dd/yyyy format
final List<String> inputParts = inputString.split('/');
if (inputParts.length != 3) {
return null;
}
final int year = int.tryParse(inputParts[2], radix: 10);
if (year == null || year < 1) {
return null;
}
final int month = int.tryParse(inputParts[0], radix: 10);
if (month == null || month < 1 || month > 12) {
return null;
}
final int day = int.tryParse(inputParts[1], radix: 10);
if (day == null || day < 1 || day > _getDaysInMonth(year, month)) {
return null;
}
return DateTime(year, month, day);
}
@override @override
List<String> get narrowWeekdays => _narrowWeekdays; List<String> get narrowWeekdays => _narrowWeekdays;
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../color_scheme.dart';
import '../divider.dart';
import '../icon_button.dart';
import '../icons.dart';
import '../ink_well.dart';
import '../material_localizations.dart';
import '../text_theme.dart';
import '../theme.dart';
import 'date_picker_common.dart';
import 'date_utils.dart' as utils;
const Duration _monthScrollDuration = Duration(milliseconds: 200);
const double _dayPickerRowHeight = 42.0;
const int _maxDayPickerRowCount = 6; // A 31 day month that starts on Saturday.
// One extra row for the day-of-week header.
const double _maxDayPickerHeight = _dayPickerRowHeight * (_maxDayPickerRowCount + 1);
const double _monthPickerHorizontalPadding = 8.0;
const int _yearPickerColumnCount = 3;
const double _yearPickerPadding = 16.0;
const double _yearPickerRowHeight = 52.0;
const double _yearPickerRowSpacing = 8.0;
const double _subHeaderHeight = 52.0;
const double _monthNavButtonsWidth = 108.0;
/// Displays a grid of days for a given month and allows the user to select a date.
///
/// Days are arranged in a rectangular grid with one column for each day of the
/// week. Controls are provided to change the year and month that the grid is
/// showing.
///
/// The calendar picker widget is rarely used directly. Instead, consider using
/// [showDatePicker], which will create a dialog that uses this as well as provides
/// a text entry option.
///
/// See also:
///
/// * [showDatePicker], which creates a Dialog that contains a [CalendarDatePicker]
/// and provides an optional compact view where the user can enter a date as
/// a line of text.
/// * [showTimePicker], which shows a dialog that contains a material design
/// time picker.
///
class CalendarDatePicker extends StatefulWidget {
/// Creates a calender date picker
///
/// It will display a grid of days for the [initialDate]'s month. The day
/// indicated by [initialDate] will be selected.
///
/// The optional [onDisplayedMonthChanged] callback can be used to track
/// the currently displayed month.
///
/// The user interface provides a way to change the year of the month being
/// displayed. By default it will show the day grid, but this can be changed
/// to start in the year selection interface with [initialCalendarMode] set
/// to [DatePickerMode.year].
///
/// The [initialDate], [firstDate], [lastDate], [onDateChanged], and
/// [initialCalendarMode] must be non-null.
///
/// [lastDate] must be after or equal to [firstDate].
///
/// [initialDate] must be between [firstDate] and [lastDate] or equal to
/// one of them.
///
/// If [selectableDayPredicate] is non-null, it must return `true` for the
/// [initialDate].
CalendarDatePicker({
Key key,
@required DateTime initialDate,
@required DateTime firstDate,
@required DateTime lastDate,
@required this.onDateChanged,
this.onDisplayedMonthChanged,
this.initialCalendarMode = DatePickerMode.day,
this.selectableDayPredicate,
}) : assert(initialDate != null),
assert(firstDate != null),
assert(lastDate != null),
initialDate = utils.dateOnly(initialDate),
firstDate = utils.dateOnly(firstDate),
lastDate = utils.dateOnly(lastDate),
assert(onDateChanged != null),
assert(initialCalendarMode != null),
super(key: key) {
assert(
!this.lastDate.isBefore(this.firstDate),
'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.'
);
assert(
!this.initialDate.isBefore(this.firstDate),
'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.'
);
assert(
!this.initialDate.isAfter(this.lastDate),
'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.'
);
assert(
selectableDayPredicate == null || selectableDayPredicate(this.initialDate),
'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate.'
);
}
/// The initially selected [DateTime] that the picker should display.
final DateTime initialDate;
/// The earliest allowable [DateTime] that the user can select.
final DateTime firstDate;
/// The latest allowable [DateTime] that the user can select.
final DateTime lastDate;
/// Called when the user selects a date in the picker.
final ValueChanged<DateTime> onDateChanged;
/// Called when the user navigates to a new month/year in the picker.
final ValueChanged<DateTime> onDisplayedMonthChanged;
/// The initial display of the calendar picker.
final DatePickerMode initialCalendarMode;
/// Function to provide full control over which dates in the calendar can be selected.
final SelectableDayPredicate selectableDayPredicate;
@override
_CalendarDatePickerState createState() => _CalendarDatePickerState();
}
class _CalendarDatePickerState extends State<CalendarDatePicker> {
bool _announcedInitialDate = false;
DatePickerMode _mode;
DateTime _currentDisplayedMonthDate;
DateTime _selectedDate;
final GlobalKey _monthPickerKey = GlobalKey();
final GlobalKey _yearPickerKey = GlobalKey();
MaterialLocalizations _localizations;
TextDirection _textDirection;
@override
void initState() {
super.initState();
_mode = widget.initialCalendarMode;
_currentDisplayedMonthDate = DateTime(widget.initialDate.year, widget.initialDate.month);
_selectedDate = widget.initialDate;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_localizations = MaterialLocalizations.of(context);
_textDirection = Directionality.of(context);
if (!_announcedInitialDate) {
_announcedInitialDate = true;
SemanticsService.announce(
_localizations.formatFullDate(_selectedDate),
_textDirection,
);
}
}
void _vibrate() {
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
HapticFeedback.vibrate();
break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
break;
}
}
void _handleModeChanged(DatePickerMode mode) {
_vibrate();
setState(() {
_mode = mode;
if (_mode == DatePickerMode.day) {
SemanticsService.announce(
_localizations.formatMonthYear(_selectedDate),
_textDirection,
);
} else {
SemanticsService.announce(
_localizations.formatYear(_selectedDate),
_textDirection,
);
}
});
}
void _handleMonthChanged(DateTime date) {
setState(() {
if (_currentDisplayedMonthDate.year != date.year || _currentDisplayedMonthDate.month != date.month) {
_currentDisplayedMonthDate = DateTime(date.year, date.month);
widget.onDisplayedMonthChanged?.call(_currentDisplayedMonthDate);
}
});
}
void _handleYearChanged(DateTime value) {
_vibrate();
if (value.isBefore(widget.firstDate)) {
value = widget.firstDate;
} else if (value.isAfter(widget.lastDate)) {
value = widget.lastDate;
}
setState(() {
_mode = DatePickerMode.day;
_handleMonthChanged(value);
});
}
void _handleDayChanged(DateTime value) {
_vibrate();
setState(() {
_selectedDate = value;
widget.onDateChanged?.call(_selectedDate);
});
}
Widget _buildPicker() {
assert(_mode != null);
switch (_mode) {
case DatePickerMode.day:
return _MonthPicker(
key: _monthPickerKey,
initialMonth: _currentDisplayedMonthDate,
currentDate: DateTime.now(),
firstDate: widget.firstDate,
lastDate: widget.lastDate,
selectedDate: _selectedDate,
onChanged: _handleDayChanged,
onDisplayedMonthChanged: _handleMonthChanged,
selectableDayPredicate: widget.selectableDayPredicate,
);
case DatePickerMode.year:
return Padding(
padding: const EdgeInsets.only(top: _subHeaderHeight),
child: _YearPicker(
key: _yearPickerKey,
currentDate: DateTime.now(),
firstDate: widget.firstDate,
lastDate: widget.lastDate,
initialDate: _currentDisplayedMonthDate,
selectedDate: _selectedDate,
onChanged: _handleYearChanged,
),
);
}
return null;
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
SingleChildScrollView(
child: SizedBox(
height: _maxDayPickerHeight,
child: _buildPicker(),
),
),
// Put the mode toggle button on top so that it won't be covered up by the _MonthPicker
_DatePickerModeToggleButton(
mode: _mode,
title: _localizations.formatMonthYear(_currentDisplayedMonthDate),
onTitlePressed: () {
// Toggle the day/year mode.
_handleModeChanged(_mode == DatePickerMode.day ? DatePickerMode.year : DatePickerMode.day);
},
),
],
);
}
}
/// A button that used to toggle the [DatePickerMode] for a date picker.
///
/// This appears above the calendar grid and allows the user to toggle the
/// [DatePickerMode] to display either the calendar view or the year list.
class _DatePickerModeToggleButton extends StatefulWidget {
const _DatePickerModeToggleButton({
@required this.mode,
@required this.title,
@required this.onTitlePressed,
});
/// The current display of the calendar picker.
final DatePickerMode mode;
/// The text that displays the current month/year being viewed.
final String title;
/// The callback when the title is pressed.
final VoidCallback onTitlePressed;
@override
_DatePickerModeToggleButtonState createState() => _DatePickerModeToggleButtonState();
}
class _DatePickerModeToggleButtonState extends State<_DatePickerModeToggleButton> with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
value: widget.mode == DatePickerMode.year ? 0.5 : 0,
upperBound: 0.5,
duration: const Duration(milliseconds: 200),
vsync: this,
);
}
@override
void didUpdateWidget(_DatePickerModeToggleButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.mode == widget.mode) {
return;
}
if (widget.mode == DatePickerMode.year) {
_controller.forward();
} else {
_controller.reverse();
}
}
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final TextTheme textTheme = Theme.of(context).textTheme;
final Color controlColor = colorScheme.onSurface.withOpacity(0.60);
return Container(
padding: const EdgeInsetsDirectional.only(start: 16, end: 4),
height: _subHeaderHeight,
child: Row(
children: <Widget>[
Flexible(
child: Semantics(
// TODO(darrenaustin): localize 'Select year'
label: 'Select year',
excludeSemantics: true,
button: true,
child: Container(
height: _subHeaderHeight,
child: InkWell(
onTap: widget.onTitlePressed,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: <Widget>[
Flexible(
child: Text(
widget.title,
overflow: TextOverflow.ellipsis,
style: textTheme.subtitle2?.copyWith(
color: controlColor,
),
),
),
RotationTransition(
turns: _controller,
child: Icon(
Icons.arrow_drop_down,
color: controlColor,
),
),
],
),
),
),
),
),
),
if (widget.mode == DatePickerMode.day)
// Give space for the prev/next month buttons that are underneath this row
const SizedBox(width: _monthNavButtonsWidth),
],
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
class _MonthPicker extends StatefulWidget {
/// Creates a month picker.
_MonthPicker({
Key key,
@required this.initialMonth,
@required this.currentDate,
@required this.firstDate,
@required this.lastDate,
@required this.selectedDate,
@required this.onChanged,
@required this.onDisplayedMonthChanged,
this.selectableDayPredicate,
}) : assert(selectedDate != null),
assert(currentDate != null),
assert(onChanged != null),
assert(firstDate != null),
assert(lastDate != null),
assert(!firstDate.isAfter(lastDate)),
assert(!selectedDate.isBefore(firstDate)),
assert(!selectedDate.isAfter(lastDate)),
super(key: key);
/// The initial month to display
final DateTime initialMonth;
/// The current date.
///
/// This date is subtly highlighted in the picker.
final DateTime currentDate;
/// The earliest date the user is permitted to pick.
///
/// This date must be on or before the [lastDate].
final DateTime firstDate;
/// The latest date the user is permitted to pick.
///
/// This date must be on or after the [firstDate].
final DateTime lastDate;
/// The currently selected date.
///
/// This date is highlighted in the picker.
final DateTime selectedDate;
/// Called when the user picks a day.
final ValueChanged<DateTime> onChanged;
/// Called when the user navigates to a new month
final ValueChanged<DateTime> onDisplayedMonthChanged;
/// Optional user supplied predicate function to customize selectable days.
final SelectableDayPredicate selectableDayPredicate;
@override
State<StatefulWidget> createState() => _MonthPickerState();
}
class _MonthPickerState extends State<_MonthPicker> {
DateTime _currentMonth;
DateTime _nextMonthDate;
DateTime _previousMonthDate;
PageController _pageController;
MaterialLocalizations _localizations;
TextDirection _textDirection;
@override
void initState() {
super.initState();
_currentMonth = widget.initialMonth;
_previousMonthDate = utils.addMonthsToMonthDate(_currentMonth, -1);
_nextMonthDate = utils.addMonthsToMonthDate(_currentMonth, 1);
_pageController = PageController(initialPage: utils.monthDelta(widget.firstDate, _currentMonth));
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_localizations = MaterialLocalizations.of(context);
_textDirection = Directionality.of(context);
}
@override
void dispose() {
_pageController?.dispose();
super.dispose();
}
void _handleMonthPageChanged(int monthPage) {
final DateTime monthDate = utils.addMonthsToMonthDate(widget.firstDate, monthPage);
if (_currentMonth.year != monthDate.year || _currentMonth.month != monthDate.month) {
_currentMonth = DateTime(monthDate.year, monthDate.month);
_previousMonthDate = utils.addMonthsToMonthDate(_currentMonth, -1);
_nextMonthDate = utils.addMonthsToMonthDate(_currentMonth, 1);
widget.onDisplayedMonthChanged?.call(_currentMonth);
}
}
void _handleNextMonth() {
if (!_isDisplayingLastMonth) {
SemanticsService.announce(
_localizations.formatMonthYear(_nextMonthDate),
_textDirection,
);
_pageController.nextPage(
duration: _monthScrollDuration,
curve: Curves.ease,
);
}
}
void _handlePreviousMonth() {
if (!_isDisplayingFirstMonth) {
SemanticsService.announce(
_localizations.formatMonthYear(_previousMonthDate),
_textDirection,
);
_pageController.previousPage(
duration: _monthScrollDuration,
curve: Curves.ease,
);
}
}
/// True if the earliest allowable month is displayed.
bool get _isDisplayingFirstMonth {
return !_currentMonth.isAfter(
DateTime(widget.firstDate.year, widget.firstDate.month),
);
}
/// True if the latest allowable month is displayed.
bool get _isDisplayingLastMonth {
return !_currentMonth.isBefore(
DateTime(widget.lastDate.year, widget.lastDate.month),
);
}
Widget _buildItems(BuildContext context, int index) {
final DateTime month = utils.addMonthsToMonthDate(widget.firstDate, index);
return _DayPicker(
key: ValueKey<DateTime>(month),
selectedDate: widget.selectedDate,
currentDate: widget.currentDate,
onChanged: widget.onChanged,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
displayedMonth: month,
selectableDayPredicate: widget.selectableDayPredicate,
);
}
@override
Widget build(BuildContext context) {
final String previousTooltipText = '${_localizations.previousMonthTooltip} ${_localizations.formatMonthYear(_previousMonthDate)}';
final String nextTooltipText = '${_localizations.nextMonthTooltip} ${_localizations.formatMonthYear(_nextMonthDate)}';
final Color controlColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.60);
return Semantics(
child: Column(
children: <Widget>[
Container(
padding: const EdgeInsetsDirectional.only(start: 16, end: 4),
height: _subHeaderHeight,
child: Row(
children: <Widget>[
const Spacer(),
IconButton(
icon: const Icon(Icons.chevron_left),
color: controlColor,
tooltip: _isDisplayingFirstMonth ? null : previousTooltipText,
onPressed: _isDisplayingFirstMonth ? null : _handlePreviousMonth,
),
IconButton(
icon: const Icon(Icons.chevron_right),
color: controlColor,
tooltip: _isDisplayingLastMonth ? null : nextTooltipText,
onPressed: _isDisplayingLastMonth ? null : _handleNextMonth,
),
],
),
),
_DayHeaders(),
Expanded(
child: PageView.builder(
key: ValueKey<DateTime>(widget.selectedDate),
controller: _pageController,
itemBuilder: _buildItems,
itemCount: utils.monthDelta(widget.firstDate, widget.lastDate) + 1,
scrollDirection: Axis.horizontal,
onPageChanged: _handleMonthPageChanged,
),
),
],
),
);
}
}
/// Displays the days of a given month and allows choosing a day.
///
/// The days are arranged in a rectangular grid with one column for each day of
/// the week.
class _DayPicker extends StatelessWidget {
/// Creates a day picker.
_DayPicker({
Key key,
@required this.currentDate,
@required this.displayedMonth,
@required this.firstDate,
@required this.lastDate,
@required this.selectedDate,
@required this.onChanged,
this.selectableDayPredicate,
}) : assert(currentDate != null),
assert(displayedMonth != null),
assert(firstDate != null),
assert(lastDate != null),
assert(selectedDate != null),
assert(onChanged != null),
assert(!firstDate.isAfter(lastDate)),
assert(!selectedDate.isBefore(firstDate)),
assert(!selectedDate.isAfter(lastDate)),
super(key: key);
/// The currently selected date.
///
/// This date is highlighted in the picker.
final DateTime selectedDate;
/// The current date at the time the picker is displayed.
final DateTime currentDate;
/// Called when the user picks a day.
final ValueChanged<DateTime> onChanged;
/// The earliest date the user is permitted to pick.
///
/// This date must be on or before the [lastDate].
final DateTime firstDate;
/// The latest date the user is permitted to pick.
///
/// This date must be on or after the [firstDate].
final DateTime lastDate;
/// The month whose days are displayed by this picker.
final DateTime displayedMonth;
/// Optional user supplied predicate function to customize selectable days.
final SelectableDayPredicate selectableDayPredicate;
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final TextTheme textTheme = Theme.of(context).textTheme;
final TextStyle dayStyle = textTheme.caption;
final Color enabledDayColor = colorScheme.onSurface.withOpacity(0.87);
final Color disabledDayColor = colorScheme.onSurface.withOpacity(0.38);
final Color selectedDayColor = colorScheme.onPrimary;
final Color selectedDayBackground = colorScheme.primary;
final Color todayColor = colorScheme.primary;
final int year = displayedMonth.year;
final int month = displayedMonth.month;
final int daysInMonth = utils.getDaysInMonth(year, month);
final int dayOffset = utils.firstDayOffset(year, month, localizations);
final List<Widget> dayItems = <Widget>[];
// 1-based day of month, e.g. 1-31 for January, and 1-29 for February on
// a leap year.
int day = -dayOffset;
while (day < daysInMonth) {
day++;
if (day < 1) {
dayItems.add(Container());
} else {
final DateTime dayToBuild = DateTime(year, month, day);
final bool isDisabled = dayToBuild.isAfter(lastDate) ||
dayToBuild.isBefore(firstDate) ||
(selectableDayPredicate != null && !selectableDayPredicate(dayToBuild));
BoxDecoration decoration;
Color dayColor = enabledDayColor;
final bool isSelectedDay = utils.isSameDay(selectedDate, dayToBuild);
if (isSelectedDay) {
// The selected day gets a circle background highlight, and a
// contrasting text color.
dayColor = selectedDayColor;
decoration = BoxDecoration(
color: selectedDayBackground,
shape: BoxShape.circle,
);
} else if (isDisabled) {
dayColor = disabledDayColor;
} else if (utils.isSameDay(currentDate, dayToBuild)) {
// The current day gets a different text color and a circle stroke
// border.
dayColor = todayColor;
decoration = BoxDecoration(
border: Border.all(color: todayColor, width: 1),
shape: BoxShape.circle,
);
}
Widget dayWidget = Container(
decoration: decoration,
child: Center(
child: Text(localizations.formatDecimal(day), style: dayStyle.apply(color: dayColor)),
),
);
if (isDisabled) {
dayWidget = ExcludeSemantics(
child: dayWidget,
);
} else {
dayWidget = GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onChanged(dayToBuild),
child: Semantics(
// We want the day of month to be spoken first irrespective of the
// locale-specific preferences or TextDirection. This is because
// an accessibility user is more likely to be interested in the
// day of month before the rest of the date, as they are looking
// for the day of month. To do that we prepend day of month to the
// formatted full date.
label: '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}',
selected: isSelectedDay,
excludeSemantics: true,
child: dayWidget,
),
);
}
dayItems.add(dayWidget);
}
}
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: _monthPickerHorizontalPadding,
),
child: GridView.custom(
physics: const ClampingScrollPhysics(),
gridDelegate: _dayPickerGridDelegate,
childrenDelegate: SliverChildListDelegate(
dayItems,
addRepaintBoundaries: false,
),
),
);
}
}
class _DayPickerGridDelegate extends SliverGridDelegate {
const _DayPickerGridDelegate();
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
const int columnCount = DateTime.daysPerWeek;
final double tileWidth = constraints.crossAxisExtent / columnCount;
final double tileHeight = math.min(_dayPickerRowHeight,
constraints.viewportMainAxisExtent / _maxDayPickerRowCount);
return SliverGridRegularTileLayout(
childCrossAxisExtent: tileWidth,
childMainAxisExtent: tileHeight,
crossAxisCount: columnCount,
crossAxisStride: tileWidth,
mainAxisStride: tileHeight,
reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
);
}
@override
bool shouldRelayout(_DayPickerGridDelegate oldDelegate) => false;
}
const _DayPickerGridDelegate _dayPickerGridDelegate = _DayPickerGridDelegate();
class _DayHeaders extends StatelessWidget {
/// Builds widgets showing abbreviated days of week. The first widget in the
/// returned list corresponds to the first day of week for the current locale.
///
/// Examples:
///
/// ```
/// ┌ Sunday is the first day of week in the US (en_US)
/// |
/// S M T W T F S <-- the returned list contains these widgets
/// _ _ _ _ _ 1 2
/// 3 4 5 6 7 8 9
///
/// ┌ But it's Monday in the UK (en_GB)
/// |
/// M T W T F S S <-- the returned list contains these widgets
/// _ _ _ _ 1 2 3
/// 4 5 6 7 8 9 10
/// ```
List<Widget> _getDayHeaders(TextStyle headerStyle, MaterialLocalizations localizations) {
final List<Widget> result = <Widget>[];
for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) {
final String weekday = localizations.narrowWeekdays[i];
result.add(ExcludeSemantics(
child: Center(child: Text(weekday, style: headerStyle)),
));
if (i == (localizations.firstDayOfWeekIndex - 1) % 7)
break;
}
return result;
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final TextStyle dayHeaderStyle = theme.textTheme.caption?.apply(
color: colorScheme.onSurface.withOpacity(0.60),
);
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final List<Widget> labels = _getDayHeaders(dayHeaderStyle, localizations);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: _monthPickerHorizontalPadding,
),
child: GridView.custom(
shrinkWrap: true,
gridDelegate: _dayPickerGridDelegate,
childrenDelegate: SliverChildListDelegate(
labels,
addRepaintBoundaries: false,
),
),
);
}
}
/// A scrollable list of years to allow picking a year.
class _YearPicker extends StatefulWidget {
/// Creates a year picker.
///
/// The [currentDate, [firstDate], [lastDate], [selectedDate], and [onChanged]
/// arguments must be non-null. The [lastDate] must be after the [firstDate].
_YearPicker({
Key key,
@required this.currentDate,
@required this.firstDate,
@required this.lastDate,
@required this.initialDate,
@required this.selectedDate,
@required this.onChanged,
}) : assert(currentDate != null),
assert(firstDate != null),
assert(lastDate != null),
assert(initialDate != null),
assert(selectedDate != null),
assert(onChanged != null),
assert(!firstDate.isAfter(lastDate)),
super(key: key);
/// The current date.
///
/// This date is subtly highlighted in the picker.
final DateTime currentDate;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// 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;
/// Called when the user picks a year.
final ValueChanged<DateTime> onChanged;
@override
_YearPickerState createState() => _YearPickerState();
}
class _YearPickerState extends State<_YearPicker> {
ScrollController scrollController;
// The approximate number of years necessary to fill the available space.
static const int minYears = 18;
@override
void initState() {
super.initState();
// Set the scroll position to approximately center the initial year.
final int initialYearIndex = widget.selectedDate.year - widget.firstDate.year;
final int initialYearRow = initialYearIndex ~/ _yearPickerColumnCount;
// Move the offset down by 2 rows to approximately center it.
final int centeredYearRow = initialYearRow - 2;
final double scrollOffset = _itemCount < minYears ? 0 : centeredYearRow * _yearPickerRowHeight;
scrollController = ScrollController(initialScrollOffset: scrollOffset);
}
Widget _buildYearItem(BuildContext context, int index) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final TextTheme textTheme = Theme.of(context).textTheme;
// 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 isCurrentYear = year == widget.currentDate.year;
final bool isDisabled = year < widget.firstDate.year || year > widget.lastDate.year;
const double decorationHeight = 36.0;
const double decorationWidth = 72.0;
Color textColor;
if (isSelected) {
textColor = colorScheme.onPrimary;
} else if (isDisabled) {
textColor = colorScheme.onSurface.withOpacity(0.38);
} else if (isCurrentYear) {
textColor = colorScheme.primary;
} else {
textColor = colorScheme.onSurface.withOpacity(0.87);
}
final TextStyle itemStyle = textTheme.bodyText1?.apply(color: textColor);
BoxDecoration decoration;
if (isSelected) {
decoration = BoxDecoration(
color: colorScheme.primary,
borderRadius: BorderRadius.circular(decorationHeight / 2),
shape: BoxShape.rectangle,
);
} else if (isCurrentYear && !isDisabled) {
decoration = BoxDecoration(
border: Border.all(
color: colorScheme.primary,
width: 1,
),
borderRadius: BorderRadius.circular(decorationHeight / 2),
shape: BoxShape.rectangle,
);
}
Widget yearItem = Center(
child: Container(
decoration: decoration,
height: decorationHeight,
width: decorationWidth,
child: Center(
child: Semantics(
selected: isSelected,
child: Text(year.toString(), style: itemStyle),
),
),
),
);
if (isDisabled) {
yearItem = ExcludeSemantics(
child: yearItem,
);
} else {
yearItem = InkWell(
key: ValueKey<int>(year),
onTap: () {
widget.onChanged(
DateTime(
year,
widget.initialDate.month,
widget.initialDate.day,
),
);
},
child: yearItem,
);
}
return yearItem;
}
int get _itemCount {
return widget.lastDate.year - widget.firstDate.year + 1;
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
const Divider(),
Expanded(
child: GridView.builder(
controller: scrollController,
gridDelegate: _yearPickerGridDelegate,
itemBuilder: _buildYearItem,
itemCount: math.max(_itemCount, minYears),
padding: const EdgeInsets.symmetric(horizontal: _yearPickerPadding),
),
),
const Divider(),
],
);
}
}
class _YearPickerGridDelegate extends SliverGridDelegate {
const _YearPickerGridDelegate();
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
final double tileWidth =
(constraints.crossAxisExtent - (_yearPickerColumnCount - 1) * _yearPickerRowSpacing) / _yearPickerColumnCount;
return SliverGridRegularTileLayout(
childCrossAxisExtent: tileWidth,
childMainAxisExtent: _yearPickerRowHeight,
crossAxisCount: _yearPickerColumnCount,
crossAxisStride: tileWidth + _yearPickerRowSpacing,
mainAxisStride: _yearPickerRowHeight,
reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
);
}
@override
bool shouldRelayout(_YearPickerGridDelegate oldDelegate) => false;
}
const _YearPickerGridDelegate _yearPickerGridDelegate = _YearPickerGridDelegate();
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// Mode of the date picker dialog.
///
/// Either a calendar or text input. In [calendar] mode, a calendar view is
/// displayed and the user taps the day they wish to select. In [input] mode a
/// [TextField] is displayed and the user types in the date they wish to select.
enum DatePickerEntryMode {
/// Tapping on a calendar.
calendar,
/// Text input.
input,
}
/// Initial display of a calendar date picker.
///
/// Either a grid of available years or a monthly calendar.
///
/// See also:
///
/// * [showDatePicker], which shows a dialog that contains a material design
/// date picker.
/// * [CalendarDatePicker], widget which implements the material design date picker.
enum DatePickerMode {
/// Choosing a month and day.
day,
/// Choosing a year.
year,
}
/// Signature for predicating dates for enabled date selections.
///
/// See [showDatePicker].
typedef SelectableDayPredicate = bool Function(DateTime day);
...@@ -5,44 +5,29 @@ ...@@ -5,44 +5,29 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'button_bar.dart'; import '../debug.dart';
import 'colors.dart'; import '../icon_button.dart';
import 'debug.dart'; import '../icons.dart';
import 'dialog.dart'; import '../ink_well.dart';
import 'feedback.dart'; import '../material.dart';
import 'flat_button.dart'; import '../material_localizations.dart';
import 'icon_button.dart'; import '../theme.dart';
import 'icons.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'text_theme.dart';
import 'theme.dart';
// Examples can assume: import 'date_picker_common.dart';
// BuildContext context;
/// Initial display mode of the date picker dialog. // NOTE: this is the original implementation for the Material Date Picker.
/// // These classes are deprecated and the whole file can be removed after
/// Date picker UI mode for either showing a list of available years or a // this has been on stable for long enough for people to migrate to the new
/// monthly calendar initially in the dialog shown by calling [showDatePicker]. // CalendarDatePicker (if needed, as showDatePicker has already been migrated
/// // and it is what most apps would have used).
/// See also:
///
/// * [showDatePicker], which shows a dialog that contains a material design
/// date picker.
enum DatePickerMode {
/// Show a date picker UI for choosing a month and day.
day,
/// Show a date picker UI for choosing a year.
year, // Examples can assume:
} // BuildContext context;
const Duration _kMonthScrollDuration = Duration(milliseconds: 200); const Duration _kMonthScrollDuration = Duration(milliseconds: 200);
const double _kDayPickerRowHeight = 42.0; const double _kDayPickerRowHeight = 42.0;
...@@ -50,143 +35,6 @@ const int _kMaxDayPickerRowCount = 6; // A 31 day month that starts on Saturday. ...@@ -50,143 +35,6 @@ const int _kMaxDayPickerRowCount = 6; // A 31 day month that starts on Saturday.
// Two extra rows: one for the day-of-week header and one for the month header. // Two extra rows: one for the day-of-week header and one for the month header.
const double _kMaxDayPickerHeight = _kDayPickerRowHeight * (_kMaxDayPickerRowCount + 2); const double _kMaxDayPickerHeight = _kDayPickerRowHeight * (_kMaxDayPickerRowCount + 2);
// Shows the selected date in large font and toggles between year and day mode
class _DatePickerHeader extends StatelessWidget {
const _DatePickerHeader({
Key key,
@required this.selectedDate,
@required this.mode,
@required this.onModeChanged,
@required this.orientation,
}) : assert(selectedDate != null),
assert(mode != null),
assert(orientation != null),
super(key: key);
final DateTime selectedDate;
final DatePickerMode mode;
final ValueChanged<DatePickerMode> onModeChanged;
final Orientation orientation;
void _handleChangeMode(DatePickerMode value) {
if (value != mode)
onModeChanged(value);
}
@override
Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final ThemeData themeData = Theme.of(context);
final TextTheme headerTextTheme = themeData.primaryTextTheme;
Color dayColor;
Color yearColor;
switch (themeData.primaryColorBrightness) {
case Brightness.light:
dayColor = mode == DatePickerMode.day ? Colors.black87 : Colors.black54;
yearColor = mode == DatePickerMode.year ? Colors.black87 : Colors.black54;
break;
case Brightness.dark:
dayColor = mode == DatePickerMode.day ? Colors.white : Colors.white70;
yearColor = mode == DatePickerMode.year ? Colors.white : Colors.white70;
break;
}
final TextStyle dayStyle = headerTextTheme.headline4.copyWith(color: dayColor);
final TextStyle yearStyle = headerTextTheme.subtitle1.copyWith(color: yearColor);
Color backgroundColor;
switch (themeData.brightness) {
case Brightness.light:
backgroundColor = themeData.primaryColor;
break;
case Brightness.dark:
backgroundColor = themeData.backgroundColor;
break;
}
EdgeInsets padding;
MainAxisAlignment mainAxisAlignment;
switch (orientation) {
case Orientation.portrait:
padding = const EdgeInsets.all(16.0);
mainAxisAlignment = MainAxisAlignment.center;
break;
case Orientation.landscape:
padding = const EdgeInsets.all(8.0);
mainAxisAlignment = MainAxisAlignment.start;
break;
}
final Widget yearButton = IgnorePointer(
ignoring: mode != DatePickerMode.day,
ignoringSemantics: false,
child: _DateHeaderButton(
color: backgroundColor,
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.year), context),
child: Semantics(
selected: mode == DatePickerMode.year,
child: Text(localizations.formatYear(selectedDate), style: yearStyle),
),
),
);
final Widget dayButton = IgnorePointer(
ignoring: mode == DatePickerMode.day,
ignoringSemantics: false,
child: _DateHeaderButton(
color: backgroundColor,
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.day), context),
child: Semantics(
selected: mode == DatePickerMode.day,
child: Text(localizations.formatMediumDate(selectedDate), style: dayStyle),
),
),
);
return Container(
padding: padding,
color: backgroundColor,
child: Column(
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[yearButton, dayButton],
),
);
}
}
class _DateHeaderButton extends StatelessWidget {
const _DateHeaderButton({
Key key,
this.onTap,
this.color,
this.child,
}) : super(key: key);
final VoidCallback onTap;
final Color color;
final Widget child;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Material(
type: MaterialType.button,
color: color,
child: InkWell(
borderRadius: kMaterialEdges[MaterialType.button],
highlightColor: theme.highlightColor,
splashColor: theme.splashColor,
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: child,
),
),
);
}
}
class _DayPickerGridDelegate extends SliverGridDelegate { class _DayPickerGridDelegate extends SliverGridDelegate {
const _DayPickerGridDelegate(); const _DayPickerGridDelegate();
...@@ -226,6 +74,11 @@ const _DayPickerGridDelegate _kDayPickerGridDelegate = _DayPickerGridDelegate(); ...@@ -226,6 +74,11 @@ const _DayPickerGridDelegate _kDayPickerGridDelegate = _DayPickerGridDelegate();
/// date picker. /// date picker.
/// * [showTimePicker], which shows a dialog that contains a material design /// * [showTimePicker], which shows a dialog that contains a material design
/// time picker. /// time picker.
///
@Deprecated(
'Use CalendarDatePicker instead. '
'This feature was deprecated after v1.15.3.'
)
class DayPicker extends StatelessWidget { class DayPicker extends StatelessWidget {
/// Creates a day picker. /// Creates a day picker.
/// ///
...@@ -503,6 +356,11 @@ class DayPicker extends StatelessWidget { ...@@ -503,6 +356,11 @@ class DayPicker extends StatelessWidget {
/// date picker. /// date picker.
/// * [showTimePicker], which shows a dialog that contains a material design /// * [showTimePicker], which shows a dialog that contains a material design
/// time picker. /// time picker.
///
@Deprecated(
'Use CalendarDatePicker instead. '
'This feature was deprecated after v1.15.3.'
)
class MonthPicker extends StatefulWidget { class MonthPicker extends StatefulWidget {
/// Creates a month picker. /// Creates a month picker.
/// ///
...@@ -546,6 +404,7 @@ class MonthPicker extends StatefulWidget { ...@@ -546,6 +404,7 @@ class MonthPicker extends StatefulWidget {
_MonthPickerState createState() => _MonthPickerState(); _MonthPickerState createState() => _MonthPickerState();
} }
// ignore: deprecated_member_use_from_same_package
class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStateMixin { class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStateMixin {
static final Animatable<double> _chevronOpacityTween = Tween<double>(begin: 1.0, end: 0.0) static final Animatable<double> _chevronOpacityTween = Tween<double>(begin: 1.0, end: 0.0)
.chain(CurveTween(curve: Curves.easeInOut)); .chain(CurveTween(curve: Curves.easeInOut));
...@@ -567,6 +426,7 @@ class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStat ...@@ -567,6 +426,7 @@ class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStat
} }
@override @override
// ignore: deprecated_member_use_from_same_package
void didUpdateWidget(MonthPicker oldWidget) { void didUpdateWidget(MonthPicker oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.selectedDate != oldWidget.selectedDate) { if (widget.selectedDate != oldWidget.selectedDate) {
...@@ -617,6 +477,7 @@ class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStat ...@@ -617,6 +477,7 @@ class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStat
Widget _buildItems(BuildContext context, int index) { Widget _buildItems(BuildContext context, int index) {
final DateTime month = _addMonthsToMonthDate(widget.firstDate, index); final DateTime month = _addMonthsToMonthDate(widget.firstDate, index);
// ignore: deprecated_member_use_from_same_package
return DayPicker( return DayPicker(
key: ValueKey<DateTime>(month), key: ValueKey<DateTime>(month),
selectedDate: widget.selectedDate, selectedDate: widget.selectedDate,
...@@ -766,6 +627,11 @@ class _MonthPickerSortKey extends OrdinalSortKey { ...@@ -766,6 +627,11 @@ class _MonthPickerSortKey extends OrdinalSortKey {
/// date picker. /// date picker.
/// * [showTimePicker], which shows a dialog that contains a material design /// * [showTimePicker], which shows a dialog that contains a material design
/// time picker. /// time picker.
///
@Deprecated(
'Use CalendarDatePicker instead. '
'This feature was deprecated after v1.15.3.'
)
class YearPicker extends StatefulWidget { class YearPicker extends StatefulWidget {
/// Creates a year picker. /// Creates a year picker.
/// ///
...@@ -807,6 +673,7 @@ class YearPicker extends StatefulWidget { ...@@ -807,6 +673,7 @@ class YearPicker extends StatefulWidget {
_YearPickerState createState() => _YearPickerState(); _YearPickerState createState() => _YearPickerState();
} }
// ignore: deprecated_member_use_from_same_package
class _YearPickerState extends State<YearPicker> { class _YearPickerState extends State<YearPicker> {
static const double _itemExtent = 50.0; static const double _itemExtent = 50.0;
ScrollController scrollController; ScrollController scrollController;
...@@ -852,339 +719,3 @@ class _YearPickerState extends State<YearPicker> { ...@@ -852,339 +719,3 @@ class _YearPickerState extends State<YearPicker> {
); );
} }
} }
class _DatePickerDialog extends StatefulWidget {
const _DatePickerDialog({
Key key,
this.initialDate,
this.firstDate,
this.lastDate,
this.selectableDayPredicate,
this.initialDatePickerMode,
}) : super(key: key);
final DateTime initialDate;
final DateTime firstDate;
final DateTime lastDate;
final SelectableDayPredicate selectableDayPredicate;
final DatePickerMode initialDatePickerMode;
@override
_DatePickerDialogState createState() => _DatePickerDialogState();
}
class _DatePickerDialogState extends State<_DatePickerDialog> {
@override
void initState() {
super.initState();
_selectedDate = widget.initialDate;
_mode = widget.initialDatePickerMode;
}
bool _announcedInitialDate = false;
MaterialLocalizations localizations;
TextDirection textDirection;
@override
void didChangeDependencies() {
super.didChangeDependencies();
localizations = MaterialLocalizations.of(context);
textDirection = Directionality.of(context);
if (!_announcedInitialDate) {
_announcedInitialDate = true;
SemanticsService.announce(
localizations.formatFullDate(_selectedDate),
textDirection,
);
}
}
DateTime _selectedDate;
DatePickerMode _mode;
final GlobalKey _pickerKey = GlobalKey();
void _vibrate() {
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
HapticFeedback.vibrate();
break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
break;
}
}
void _handleModeChanged(DatePickerMode mode) {
_vibrate();
setState(() {
_mode = mode;
if (_mode == DatePickerMode.day) {
SemanticsService.announce(localizations.formatMonthYear(_selectedDate), textDirection);
} else {
SemanticsService.announce(localizations.formatYear(_selectedDate), textDirection);
}
});
}
void _handleYearChanged(DateTime value) {
if (value.isBefore(widget.firstDate))
value = widget.firstDate;
else if (value.isAfter(widget.lastDate))
value = widget.lastDate;
if (value == _selectedDate)
return;
_vibrate();
setState(() {
_mode = DatePickerMode.day;
_selectedDate = value;
});
}
void _handleDayChanged(DateTime value) {
_vibrate();
setState(() {
_selectedDate = value;
});
}
void _handleCancel() {
Navigator.pop(context);
}
void _handleOk() {
Navigator.pop(context, _selectedDate);
}
Widget _buildPicker() {
assert(_mode != null);
switch (_mode) {
case DatePickerMode.day:
return MonthPicker(
key: _pickerKey,
selectedDate: _selectedDate,
onChanged: _handleDayChanged,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
selectableDayPredicate: widget.selectableDayPredicate,
);
case DatePickerMode.year:
return YearPicker(
key: _pickerKey,
selectedDate: _selectedDate,
onChanged: _handleYearChanged,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
);
}
return null;
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final Widget picker = _buildPicker();
final Widget actions = ButtonBar(
children: <Widget>[
FlatButton(
child: Text(localizations.cancelButtonLabel),
onPressed: _handleCancel,
),
FlatButton(
child: Text(localizations.okButtonLabel),
onPressed: _handleOk,
),
],
);
final Dialog dialog = Dialog(
child: OrientationBuilder(
builder: (BuildContext context, Orientation orientation) {
assert(orientation != null);
final Widget header = _DatePickerHeader(
selectedDate: _selectedDate,
mode: _mode,
onModeChanged: _handleModeChanged,
orientation: orientation,
);
switch (orientation) {
case Orientation.portrait:
return Container(
color: theme.dialogBackgroundColor,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
header,
Flexible(child: picker),
actions,
],
),
);
case Orientation.landscape:
return Container(
color: theme.dialogBackgroundColor,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Flexible(child: header),
Flexible(
flex: 2, // have the picker take up 2/3 of the dialog width
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Flexible(child: picker),
actions,
],
),
),
],
),
);
}
return null;
}
),
);
return Theme(
data: theme.copyWith(
dialogBackgroundColor: Colors.transparent,
),
child: dialog,
);
}
}
/// Signature for predicating dates for enabled date selections.
///
/// See [showDatePicker].
typedef SelectableDayPredicate = bool Function(DateTime day);
/// Shows a dialog containing a material design date picker.
///
/// The returned [Future] resolves to the date selected by the user when the
/// user closes the dialog. If the user cancels the dialog, null is returned.
///
/// An optional [selectableDayPredicate] function can be passed in to customize
/// the days to enable for selection. If provided, only the days that
/// [selectableDayPredicate] returned true for will be selectable.
///
/// An optional [initialDatePickerMode] argument can be used to display the
/// date picker initially in the year or month+day picker mode. It defaults
/// to month+day, and must not be null.
///
/// An optional [locale] argument can be used to set the locale for the date
/// picker. It defaults to the ambient locale provided by [Localizations].
///
/// An optional [textDirection] argument can be used to set the text direction
/// (RTL or LTR) for the date picker. It defaults to the ambient text direction
/// provided by [Directionality]. If both [locale] and [textDirection] are not
/// null, [textDirection] overrides the direction chosen for the [locale].
///
/// The [context], [useRootNavigator] and [routeSettings] arguments are passed to
/// [showDialog], the documentation for which discusses how it is used.
///
/// The [builder] parameter can be used to wrap the dialog widget
/// to add inherited widgets like [Theme].
///
/// {@animation 350 622 https://flutter.github.io/assets-for-api-docs/assets/material/show_date_picker.mp4}
///
/// {@tool snippet}
/// Show a date picker with the dark theme.
///
/// ```dart
/// Future<DateTime> selectedDate = showDatePicker(
/// context: context,
/// initialDate: DateTime.now(),
/// firstDate: DateTime(2018),
/// lastDate: DateTime(2030),
/// builder: (BuildContext context, Widget child) {
/// return Theme(
/// data: ThemeData.dark(),
/// child: child,
/// );
/// },
/// );
/// ```
/// {@end-tool}
///
/// The [context], [initialDate], [firstDate], and [lastDate] parameters must
/// not be null.
///
/// See also:
///
/// * [showTimePicker], which shows a dialog that contains a material design
/// time picker.
/// * [DayPicker], which displays the days of a given month and allows
/// choosing a day.
/// * [MonthPicker], which displays a scrollable list of months to allow
/// picking a month.
/// * [YearPicker], which displays a scrollable list of years to allow picking
/// a year.
Future<DateTime> showDatePicker({
@required BuildContext context,
@required DateTime initialDate,
@required DateTime firstDate,
@required DateTime lastDate,
SelectableDayPredicate selectableDayPredicate,
DatePickerMode initialDatePickerMode = DatePickerMode.day,
Locale locale,
TextDirection textDirection,
TransitionBuilder builder,
bool useRootNavigator = true,
RouteSettings routeSettings,
}) async {
assert(initialDate != null);
assert(firstDate != null);
assert(lastDate != null);
assert(useRootNavigator != null);
assert(!initialDate.isBefore(firstDate), 'initialDate must be on or after firstDate');
assert(!initialDate.isAfter(lastDate), 'initialDate must be on or before lastDate');
assert(!firstDate.isAfter(lastDate), 'lastDate must be on or after firstDate');
assert(
selectableDayPredicate == null || selectableDayPredicate(initialDate),
'Provided initialDate must satisfy provided selectableDayPredicate'
);
assert(initialDatePickerMode != null, 'initialDatePickerMode must not be null');
assert(context != null);
assert(debugCheckHasMaterialLocalizations(context));
Widget child = _DatePickerDialog(
initialDate: initialDate,
firstDate: firstDate,
lastDate: lastDate,
selectableDayPredicate: selectableDayPredicate,
initialDatePickerMode: initialDatePickerMode,
);
if (textDirection != null) {
child = Directionality(
textDirection: textDirection,
child: child,
);
}
if (locale != null) {
child = Localizations.override(
context: context,
locale: locale,
child: child,
);
}
return await showDialog<DateTime>(
context: context,
useRootNavigator: useRootNavigator,
builder: (BuildContext context) {
return builder == null ? child : builder(context, child);
},
routeSettings: routeSettings,
);
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import '../button_bar.dart';
import '../button_theme.dart';
import '../color_scheme.dart';
import '../debug.dart';
import '../dialog.dart';
import '../flat_button.dart';
import '../icons.dart';
import '../material_localizations.dart';
import '../text_theme.dart';
import '../theme.dart';
import 'calendar_date_picker.dart';
import 'date_picker_common.dart';
import 'date_picker_header.dart';
import 'date_utils.dart' as utils;
import 'input_date_picker.dart';
const Size _calendarPortraitDialogSize = Size(330.0, 518.0);
const Size _calendarLandscapeDialogSize = Size(496.0, 346.0);
const Size _inputPortraitDialogSize = Size(330.0, 270.0);
const Size _inputLandscapeDialogSize = Size(496, 160.0);
const Duration _dialogSizeAnimationDuration = Duration(milliseconds: 200);
/// Shows a dialog containing a Material Design date picker.
///
/// 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.
///
/// 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.
///
/// An optional [initialEntryMode] argument can be used to display the date
/// picker in the [DatePickerEntryMode.calendar] (a calendar month grid)
/// or [DatePickerEntryMode.input] (a text input field) mode.
/// It defaults to [DatePickerEntryMode.calendar] and must be non-null.
///
/// An optional [selectableDayPredicate] function can be passed in to only allow
/// certain days for selection. If provided, only the days that
/// [selectableDayPredicate] returns true for will be selectable. For example,
/// this can be used to only allow weekdays for selection. If provided, it must
/// return true for [initialDate].
///
/// Optional strings for the [cancelText], [confirmText], [errorFormatText],
/// [errorInvalidText], [fieldHintText], [fieldLabelText], and [helpText] allow
/// you to override the default text used for various parts of the dialog:
///
/// * [cancelText], label on the cancel button.
/// * [confirmText], label on the ok button.
/// * [errorFormatText], message used when the input text isn't in a proper date format.
/// * [errorInvalidText], message used when the input text isn't a selectable date.
/// * [fieldHintText], text used to prompt the user when no text has been entered in the field.
/// * [fieldLabelText], label for the date text input field.
/// * [helpText], label on the top of the dialog.
///
/// An optional [locale] argument can be used to set the locale for the date
/// picker. It defaults to the ambient locale provided by [Localizations].
///
/// An optional [textDirection] argument can be used to set the text direction
/// ([TextDirection.ltr] or [TextDirection.rtl]) for the date picker. It
/// defaults to the ambient text direction provided by [Directionality]. If both
/// [locale] and [textDirection] are non-null, [textDirection] overrides the
/// direction chosen for the [locale].
///
/// The [context], [useRootNavigator] and [routeSettings] arguments are passed to
/// [showDialog], the documentation for which discusses how it is used. [context]
/// and [useRootNavigator] must be non-null.
///
/// The [builder] parameter can be used to wrap the dialog widget
/// to add inherited widgets like [Theme].
///
/// 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.
Future<DateTime> showDatePicker({
@required BuildContext context,
@required DateTime initialDate,
@required DateTime firstDate,
@required DateTime lastDate,
DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar,
SelectableDayPredicate selectableDayPredicate,
String helpText,
String cancelText,
String confirmText,
Locale locale,
bool useRootNavigator = true,
RouteSettings routeSettings,
TextDirection textDirection,
TransitionBuilder builder,
DatePickerMode initialDatePickerMode = DatePickerMode.day,
String errorFormatText,
String errorInvalidText,
String fieldHintText,
String fieldLabelText,
}) async {
assert(context != null);
assert(initialDate != null);
assert(firstDate != null);
assert(lastDate != null);
initialDate = utils.dateOnly(initialDate);
firstDate = utils.dateOnly(firstDate);
lastDate = utils.dateOnly(lastDate);
assert(
!lastDate.isBefore(firstDate),
'lastDate $lastDate must be on or after firstDate $firstDate.'
);
assert(
!initialDate.isBefore(firstDate),
'initialDate $initialDate must be on or after firstDate $firstDate.'
);
assert(
!initialDate.isAfter(lastDate),
'initialDate $initialDate must be on or before lastDate $lastDate.'
);
assert(
selectableDayPredicate == null || selectableDayPredicate(initialDate),
'Provided initialDate $initialDate must satisfy provided selectableDayPredicate.'
);
assert(initialEntryMode != null);
assert(useRootNavigator != null);
assert(initialDatePickerMode != null);
assert(debugCheckHasMaterialLocalizations(context));
Widget dialog = _DatePickerDialog(
initialDate: initialDate,
firstDate: firstDate,
lastDate: lastDate,
initialEntryMode: initialEntryMode,
selectableDayPredicate: selectableDayPredicate,
helpText: helpText,
cancelText: cancelText,
confirmText: confirmText,
initialCalendarMode: initialDatePickerMode,
errorFormatText: errorFormatText,
errorInvalidText: errorInvalidText,
fieldHintText: fieldHintText,
fieldLabelText: fieldLabelText,
);
if (textDirection != null) {
dialog = Directionality(
textDirection: textDirection,
child: dialog,
);
}
if (locale != null) {
dialog = Localizations.override(
context: context,
locale: locale,
child: dialog,
);
}
return showDialog<DateTime>(
context: context,
useRootNavigator: useRootNavigator,
routeSettings: routeSettings,
builder: (BuildContext context) {
return builder == null ? dialog : builder(context, dialog);
},
);
}
class _DatePickerDialog extends StatefulWidget {
_DatePickerDialog({
Key key,
@required DateTime initialDate,
@required DateTime firstDate,
@required DateTime lastDate,
this.initialEntryMode = DatePickerEntryMode.calendar,
this.selectableDayPredicate,
this.cancelText,
this.confirmText,
this.helpText,
this.initialCalendarMode = DatePickerMode.day,
this.errorFormatText,
this.errorInvalidText,
this.fieldHintText,
this.fieldLabelText,
}) : assert(initialDate != null),
assert(firstDate != null),
assert(lastDate != null),
initialDate = utils.dateOnly(initialDate),
firstDate = utils.dateOnly(firstDate),
lastDate = utils.dateOnly(lastDate),
assert(initialEntryMode != null),
assert(initialCalendarMode != null),
super(key: key) {
assert(
!this.lastDate.isBefore(this.firstDate),
'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.'
);
assert(
!this.initialDate.isBefore(this.firstDate),
'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.'
);
assert(
!this.initialDate.isAfter(this.lastDate),
'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.'
);
assert(
selectableDayPredicate == null || selectableDayPredicate(this.initialDate),
'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate'
);
}
/// The initially selected [DateTime] that the picker should display.
final DateTime initialDate;
/// The earliest allowable [DateTime] that the user can select.
final DateTime firstDate;
/// The latest allowable [DateTime] that the user can select.
final DateTime lastDate;
final DatePickerEntryMode initialEntryMode;
/// Function to provide full control over which [DateTime] can be selected.
final SelectableDayPredicate selectableDayPredicate;
/// The text that is displayed on the cancel button.
final String cancelText;
/// The text that is displayed on the confirm button.
final String confirmText;
/// The text that is displayed at the top of the header.
///
/// This is used to indicate to the user what they are selecting a date for.
final String helpText;
/// The initial display of the calendar picker.
final DatePickerMode initialCalendarMode;
final String errorFormatText;
final String errorInvalidText;
final String fieldHintText;
final String fieldLabelText;
@override
_DatePickerDialogState createState() => _DatePickerDialogState();
}
class _DatePickerDialogState extends State<_DatePickerDialog> {
DatePickerEntryMode _entryMode;
DateTime _selectedDate;
bool _autoValidate;
final GlobalKey _calendarPickerKey = GlobalKey();
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
_entryMode = widget.initialEntryMode;
_selectedDate = widget.initialDate;
_autoValidate = false;
}
void _handleOk() {
if (_entryMode == DatePickerEntryMode.input) {
final FormState form = _formKey.currentState;
if (!form.validate()) {
setState(() => _autoValidate = true);
return;
}
form.save();
}
Navigator.pop(context, _selectedDate);
}
void _handleCancel() {
Navigator.pop(context);
}
void _handelEntryModeToggle() {
setState(() {
switch (_entryMode) {
case DatePickerEntryMode.calendar:
_entryMode = DatePickerEntryMode.input;
break;
case DatePickerEntryMode.input:
_formKey.currentState.save();
_entryMode = DatePickerEntryMode.calendar;
break;
}
});
}
void _handleDateChanged(DateTime date) {
setState(() => _selectedDate = date);
}
Size _dialogSize(BuildContext context) {
final Orientation orientation = MediaQuery.of(context).orientation;
switch (_entryMode) {
case DatePickerEntryMode.calendar:
switch (orientation) {
case Orientation.portrait:
return _calendarPortraitDialogSize;
case Orientation.landscape:
return _calendarLandscapeDialogSize;
}
break;
case DatePickerEntryMode.input:
switch (orientation) {
case Orientation.portrait:
return _inputPortraitDialogSize;
case Orientation.landscape:
return _inputLandscapeDialogSize;
}
break;
}
return null;
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final Orientation orientation = MediaQuery.of(context).orientation;
final TextTheme textTheme = theme.textTheme;
// Constrain the textScaleFactor to the largest supported value to prevent
// layout issues.
final double textScaleFactor = math.min(MediaQuery.of(context).textScaleFactor, 1.3);
final String dateText = _selectedDate != null
? localizations.formatMediumDate(_selectedDate)
// TODO(darrenaustin): localize 'Date'
: 'Date';
final Color dateColor = colorScheme.brightness == Brightness.light
? colorScheme.onPrimary
: colorScheme.onSurface;
final TextStyle dateStyle = orientation == Orientation.landscape
? textTheme.headline5?.copyWith(color: dateColor)
: textTheme.headline4?.copyWith(color: dateColor);
final Widget actions = ButtonBar(
buttonTextTheme: ButtonTextTheme.primary,
layoutBehavior: ButtonBarLayoutBehavior.constrained,
children: <Widget>[
FlatButton(
child: Text(widget.cancelText ?? localizations.cancelButtonLabel),
onPressed: _handleCancel,
),
FlatButton(
child: Text(widget.confirmText ?? localizations.okButtonLabel),
onPressed: _handleOk,
),
],
);
Widget picker;
IconData entryModeIcon;
String entryModeTooltip;
switch (_entryMode) {
case DatePickerEntryMode.calendar:
picker = CalendarDatePicker(
key: _calendarPickerKey,
initialDate: _selectedDate,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
onDateChanged: _handleDateChanged,
selectableDayPredicate: widget.selectableDayPredicate,
initialCalendarMode: widget.initialCalendarMode,
);
entryModeIcon = Icons.edit;
// TODO(darrenaustin): localize 'Switch to input'
entryModeTooltip = 'Switch to input';
break;
case DatePickerEntryMode.input:
picker = Form(
key: _formKey,
autovalidate: _autoValidate,
child: InputDatePickerFormField(
initialDate: _selectedDate,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
onDateSubmitted: _handleDateChanged,
onDateSaved: _handleDateChanged,
selectableDayPredicate: widget.selectableDayPredicate,
errorFormatText: widget.errorFormatText,
errorInvalidText: widget.errorInvalidText,
fieldHintText: widget.fieldHintText,
fieldLabelText: widget.fieldLabelText,
autofocus: true,
),
);
entryModeIcon = Icons.calendar_today;
// TODO(darrenaustin): localize 'Switch to calendar'
entryModeTooltip = 'Switch to calendar';
break;
}
final Widget header = DatePickerHeader(
// TODO(darrenaustin): localize 'SELECT DATE'
helpText: widget.helpText ?? 'SELECT DATE',
titleText: dateText,
titleStyle: dateStyle,
orientation: orientation,
isShort: orientation == Orientation.landscape,
icon: entryModeIcon,
iconTooltip: entryModeTooltip,
onIconPressed: _handelEntryModeToggle,
);
final Size dialogSize = _dialogSize(context) * textScaleFactor;
return Dialog(
child: AnimatedContainer(
width: dialogSize.width,
height: dialogSize.height,
duration: _dialogSizeAnimationDuration,
curve: Curves.easeIn,
child: MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: textScaleFactor,
),
child: Builder(builder: (BuildContext context) {
switch (orientation) {
case Orientation.portrait:
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
header,
Expanded(child: picker),
actions,
],
);
case Orientation.landscape:
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
header,
Flexible(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(child: picker),
actions,
],
),
),
],
);
}
return null;
}),
),
),
insetPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0))
),
clipBehavior: Clip.antiAlias,
elevation: 24.0,
);
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import '../color_scheme.dart';
import '../icon_button.dart';
import '../text_theme.dart';
import '../theme.dart';
// NOTE: This is an internal implementation file. Even though there are public
// classes and functions defined here, they are only meant to be used by the
// date picker implementation and are not exported as part of the Material library.
// See pickers.dart for exactly what is considered part of the public API.
const double _datePickerHeaderLandscapeWidth = 152.0;
const double _datePickerHeaderPortraitHeight = 120.0;
const double _headerPaddingLandscape = 16.0;
/// Re-usable widget that displays the selected date (in large font) and the
/// help text above it.
///
/// These types include:
///
/// * Single Date picker with calendar mode.
/// * Single Date picker with manual input mode.
///
/// [helpText], [orientation], [icon], [onIconPressed] are required and must be
/// non-null.
class DatePickerHeader extends StatelessWidget {
/// Creates a header for use in a date picker dialog.
const DatePickerHeader({
Key key,
@required this.helpText,
@required this.titleText,
this.titleSemanticsLabel,
@required this.titleStyle,
@required this.orientation,
this.isShort = false,
@required this.icon,
@required this.iconTooltip,
@required this.onIconPressed,
}) : assert(helpText != null),
assert(orientation != null),
assert(isShort != null),
super(key: key);
/// The text that is displayed at the top of the header.
///
/// This is used to indicate to the user what they are selecting a date for.
final String helpText;
/// The text that is displayed at the center of the header.
final String titleText;
/// The semantic label associated with the [titleText].
final String titleSemanticsLabel;
/// The [TextStyle] that the title text is displayed with.
final TextStyle titleStyle;
/// The orientation is used to decide how to layout its children.
final Orientation orientation;
/// Indicates the header is being displayed in a shorter/narrower context.
///
/// This will be used to tighten up the space between the help text and date
/// text if `true`. Additionally, it will use a smaller typography style if
/// `true`.
///
/// This is necessary for displaying the manual input mode in
/// landscape orientation, in order to account for the keyboard height.
final bool isShort;
/// The mode-switching icon that will be displayed in the lower right
/// in portrait, and lower left in landscape.
///
/// The available icons are described in [Icons].
final IconData icon;
/// The text that is displayed for the tooltip of the icon.
final String iconTooltip;
/// Callback when the user taps the icon in the header.
///
/// The picker will use this to toggle between entry modes.
final VoidCallback onIconPressed;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final TextTheme textTheme = theme.textTheme;
// The header should use the primary color in light themes and surface color in dark
final bool isDark = colorScheme.brightness == Brightness.dark;
final Color primarySurfaceColor = isDark ? colorScheme.surface : colorScheme.primary;
final Color onPrimarySurfaceColor = isDark ? colorScheme.onSurface : colorScheme.onPrimary;
final TextStyle helpStyle = textTheme.overline?.copyWith(
color: onPrimarySurfaceColor,
);
final Text help = Text(
helpText,
style: helpStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
final Text title = Text(
titleText,
semanticsLabel: titleSemanticsLabel ?? titleText,
style: titleStyle,
maxLines: (isShort || orientation == Orientation.portrait) ? 1 : 2,
overflow: TextOverflow.ellipsis,
);
final IconButton icon = IconButton(
icon: Icon(this.icon),
color: onPrimarySurfaceColor,
tooltip: iconTooltip,
onPressed: onIconPressed,
);
switch (orientation) {
case Orientation.portrait:
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
height: _datePickerHeaderPortraitHeight,
color: primarySurfaceColor,
padding: const EdgeInsetsDirectional.only(
start: 24,
end: 12,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(height: 16),
Flexible(child: help),
const SizedBox(height: 38),
Row(
children: <Widget>[
Expanded(child: title),
icon,
],
),
],
),
),
],
);
case Orientation.landscape:
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
width: _datePickerHeaderLandscapeWidth,
color: primarySurfaceColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: _headerPaddingLandscape,
),
child: help,
),
SizedBox(height: isShort ? 16 : 56),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: _headerPaddingLandscape,
),
child: title,
),
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
),
child: icon,
),
],
),
),
],
);
}
return null;
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Common date utility functions used by the date picker implementation
// NOTE: This is an internal implementation file. Even though there are public
// classes and functions defined here, they are only meant to be used by the
// date picker implementation and are not exported as part of the Material library.
// See pickers.dart for exactly what is considered part of the public API.
import '../material_localizations.dart';
/// Returns a [DateTime] with just the date of the original, but no time set.
DateTime dateOnly(DateTime date) {
return DateTime(date.year, date.month, date.day);
}
/// Returns true if the two [DateTime] objects have the same day, month, and
/// year.
bool isSameDay(DateTime dateA, DateTime dateB) {
return
dateA.year == dateB.year &&
dateA.month == dateB.month &&
dateA.day == dateB.day;
}
/// Determines the number of months between two [DateTime] objects.
///
/// For example:
/// ```
/// DateTime date1 = DateTime(year: 2019, month: 6, day: 15);
/// DateTime date2 = DateTime(year: 2020, month: 1, day: 15);
/// int delta = monthDelta(date1, date2);
/// ```
///
/// The value for `delta` would be `7`.
int monthDelta(DateTime startDate, DateTime endDate) {
return (endDate.year - startDate.year) * 12 + endDate.month - startDate.month;
}
/// Returns a [DateTime] with the added number of months and truncates any day
/// and time information.
///
/// For example:
/// ```
/// DateTime date = DateTime(year: 2019, month: 1, day: 15);
/// DateTime futureDate = _addMonthsToMonthDate(date, 3);
/// ```
///
/// `date` would be January 15, 2019.
/// `futureDate` would be April 1, 2019 since it adds 3 months and truncates
/// any additional date information.
DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) {
return DateTime(monthDate.year, monthDate.month + monthsToAdd);
}
/// Computes the offset from the first day of the week that the first day of
/// the [month] falls on.
///
/// For example, September 1, 2017 falls on a Friday, which in the calendar
/// localized for United States English appears as:
///
/// ```
/// S M T W T F S
/// _ _ _ _ _ 1 2
/// ```
///
/// The offset for the first day of the months is the number of leading blanks
/// in the calendar, i.e. 5.
///
/// The same date localized for the Russian calendar has a different offset,
/// because the first day of week is Monday rather than Sunday:
///
/// ```
/// M T W T F S S
/// _ _ _ _ 1 2 3
/// ```
///
/// So the offset is 4, rather than 5.
///
/// This code consolidates the following:
///
/// - [DateTime.weekday] provides a 1-based index into days of week, with 1
/// falling on Monday.
/// - [MaterialLocalizations.firstDayOfWeekIndex] provides a 0-based index
/// into the [MaterialLocalizations.narrowWeekdays] list.
/// - [MaterialLocalizations.narrowWeekdays] list provides localized names of
/// days of week, always starting with Sunday and ending with Saturday.
int firstDayOffset(int year, int month, MaterialLocalizations localizations) {
// 0-based day of week for the month and year, with 0 representing Monday.
final int weekdayFromMonday = DateTime(year, month).weekday - 1;
// 0-based start of week depending on the locale, with 0 representing Sunday.
int firstDayOfWeekIndex = localizations.firstDayOfWeekIndex;
// firstDayOfWeekIndex recomputed to be Monday-based, in order to compare with
// weekdayFromMonday.
firstDayOfWeekIndex = (firstDayOfWeekIndex - 1) % 7;
// Number of days between the first day of week appearing on the calendar,
// and the day corresponding to the first of the month.
return (weekdayFromMonday - firstDayOfWeekIndex) % 7;
}
/// Returns the number of days in a month, according to the proleptic
/// Gregorian calendar.
///
/// This applies the leap year logic introduced by the Gregorian reforms of
/// 1582. It will not give valid results for dates prior to that time.
int getDaysInMonth(int year, int month) {
if (month == DateTime.february) {
final bool isLeapYear = (year % 4 == 0) && (year % 100 != 0) ||
(year % 400 == 0);
if (isLeapYear)
return 29;
return 28;
}
const List<int> daysInMonth = <int>[31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return daysInMonth[month - 1];
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../input_border.dart';
import '../input_decorator.dart';
import '../material_localizations.dart';
import '../text_field.dart';
import '../text_form_field.dart';
import 'date_picker_common.dart';
import 'date_utils.dart' as utils;
const double _inputPortraitHeight = 98.0;
const double _inputLandscapeHeight = 108.0;
/// A [TextFormField] configured to accept and validate a date entered by the user.
///
/// The text entered into this field will be constrained to only allow digits
/// and separators. When saved or submitted, the text will be parsed into a
/// [DateTime] according to the ambient locale. If the input text doesn't parse
/// into a date, the [errorFormatText] message will be displayed under the field.
///
/// [firstDate], [lastDate], and [selectableDayPredicate] provide constraints on
/// what days are valid. If the input date isn't in the date range or doesn't pass
/// the given predicate, then the [errorInvalidText] message will be displayed
/// under the field.
///
/// See also:
///
/// * [showDatePicker], which shows a dialog that contains a material design
/// date picker which includes support for text entry of dates.
/// * [MaterialLocalizations.parseCompactDate], which is used to parse the text
/// input into a [DateTime].
///
class InputDatePickerFormField extends StatefulWidget {
/// Creates a [TextFormField] configured to accept and validate a date.
///
/// If the optional [initialDate] is provided, then it will be used to populate
/// the text field. If the [fieldHintText] is provided, it will be shown.
///
/// If [initialDate] is provided, it must not be before [firstDate] or after
/// [lastDate]. If [selectableDayPredicate] is provided, it must return `true`
/// for [initialDate].
///
/// [firstDate] must be on or before [lastDate].
///
/// [firstDate], [lastDate], and [autofocus] must be non-null.
///
InputDatePickerFormField({
Key key,
DateTime initialDate,
@required DateTime firstDate,
@required DateTime lastDate,
this.onDateSubmitted,
this.onDateSaved,
this.selectableDayPredicate,
this.errorFormatText,
this.errorInvalidText,
this.fieldHintText,
this.fieldLabelText,
this.autofocus = false,
}) : assert(firstDate != null),
assert(lastDate != null),
assert(autofocus != null),
initialDate = initialDate != null ? utils.dateOnly(initialDate) : null,
firstDate = utils.dateOnly(firstDate),
lastDate = utils.dateOnly(lastDate),
super(key: key) {
assert(
!this.lastDate.isBefore(this.firstDate),
'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.'
);
assert(
initialDate == null || !this.initialDate.isBefore(this.firstDate),
'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.'
);
assert(
initialDate == null || !this.initialDate.isAfter(this.lastDate),
'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.'
);
assert(
selectableDayPredicate == null || initialDate == null || selectableDayPredicate(this.initialDate),
'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate.'
);
}
/// If provided, it will be used as the default value of the field.
final DateTime initialDate;
/// The earliest allowable [DateTime] that the user can input.
final DateTime firstDate;
/// The latest allowable [DateTime] that the user can input.
final DateTime lastDate;
/// An optional method to call when the user indicates they are done editing
/// the text in the field. Will only be called if the input represents a valid
/// [DateTime].
final ValueChanged<DateTime> onDateSubmitted;
/// An optional method to call with the final date when the form is
/// saved via [FormState.save]. Will only be called if the input represents
/// a valid [DateTime].
final ValueChanged<DateTime> onDateSaved;
/// Function to provide full control over which [DateTime] can be selected.
final SelectableDayPredicate selectableDayPredicate;
/// The error text displayed if the entered date is not in the correct format.
final String errorFormatText;
/// The error text displayed if the date is not valid.
///
/// A date is not valid if it is earlier than [firstDate], later than
/// [lastDate], or doesn't pass the [selectableDayPredicate].
final String errorInvalidText;
/// The hint text displayed in the [TextField].
///
/// If this is null, it will default to the date format string. For example,
/// 'mm/dd/yyyy' for en_US.
final String fieldHintText;
/// The label text displayed in the [TextField].
///
/// If this is null, it will default to the words representing the date format
/// string. For example, 'Month, Day, Year' for en_US.
final String fieldLabelText;
/// {@macro flutter.widgets.editableText.autofocus}
final bool autofocus;
@override
_InputDatePickerFormFieldState createState() => _InputDatePickerFormFieldState();
}
class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
final TextEditingController _controller = TextEditingController();
DateTime _selectedDate;
String _inputText;
bool _autoSelected = false;
@override
void initState() {
super.initState();
_selectedDate = widget.initialDate;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_selectedDate != null) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
_inputText = localizations.formatCompactDate(_selectedDate);
TextEditingValue textEditingValue = _controller.value.copyWith(text: _inputText);
// Select the new text if we are auto focused and haven't selected the text before.
if (widget.autofocus && !_autoSelected) {
textEditingValue = textEditingValue.copyWith(selection: TextSelection(
baseOffset: 0,
extentOffset: _inputText.length,
));
_autoSelected = true;
}
_controller.value = textEditingValue;
}
}
DateTime _parseDate(String text) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
return localizations.parseCompactDate(text);
}
bool _isValidAcceptableDate(DateTime date) {
return
date != null &&
!date.isBefore(widget.firstDate) &&
!date.isAfter(widget.lastDate) &&
(widget.selectableDayPredicate == null || widget.selectableDayPredicate(date));
}
String _validateDate(String text) {
final DateTime date = _parseDate(text);
if (date == null) {
// TODO(darrenaustin): localize 'Invalid format.'
return widget.errorFormatText ?? 'Invalid format.';
} else if (!_isValidAcceptableDate(date)) {
// TODO(darrenaustin): localize 'Out of range.'
return widget.errorInvalidText ?? 'Out of range.';
}
return null;
}
void _handleSaved(String text) {
if (widget.onDateSaved != null) {
final DateTime date = _parseDate(text);
if (_isValidAcceptableDate(date)) {
_selectedDate = date;
_inputText = text;
widget.onDateSaved(date);
}
}
}
void _handleSubmitted(String text) {
if (widget.onDateSubmitted != null) {
final DateTime date = _parseDate(text);
if (_isValidAcceptableDate(date)) {
_selectedDate = date;
_inputText = text;
widget.onDateSubmitted(date);
}
}
}
@override
Widget build(BuildContext context) {
return OrientationBuilder(builder: (BuildContext context, Orientation orientation) {
assert(orientation != null);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24),
height: orientation == Orientation.portrait ? _inputPortraitHeight : _inputLandscapeHeight,
child: Column(
children: <Widget>[
const Spacer(),
TextFormField(
decoration: InputDecoration(
border: const UnderlineInputBorder(),
filled: true,
// TODO(darrenaustin): localize 'mm/dd/yyyy' and 'Enter Date'
hintText: widget.fieldHintText ?? 'mm/dd/yyyy',
labelText: widget.fieldLabelText ?? 'Enter Date',
),
validator: _validateDate,
inputFormatters: <TextInputFormatter>[
// TODO(darrenaustin): localize date separator '/'
_DateTextInputFormatter('/'),
],
keyboardType: TextInputType.datetime,
onSaved: _handleSaved,
onFieldSubmitted: _handleSubmitted,
autofocus: widget.autofocus,
controller: _controller,
),
const Spacer(),
],
),
);
});
}
}
class _DateTextInputFormatter extends TextInputFormatter {
_DateTextInputFormatter(this.separator);
final String separator;
final WhitelistingTextInputFormatter _filterFormatter =
// Only allow digits and separators (slash, dot, comma, hyphen, space).
WhitelistingTextInputFormatter(RegExp(r'[\d\/\.,-\s]+'));
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
final TextEditingValue filteredValue = _filterFormatter.formatEditUpdate(oldValue, newValue);
return filteredValue.copyWith(
// Replace any separator character with the given separator
text: filteredValue.text.replaceAll(RegExp(r'[\D]'), separator),
);
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Date Picker public API
export 'calendar_date_picker.dart' show CalendarDatePicker;
export 'date_picker_common.dart' show DatePickerEntryMode, DatePickerMode, SelectableDayPredicate;
export 'date_picker_deprecated.dart';
export 'date_picker_dialog.dart' show showDatePicker;
export 'input_date_picker.dart' show InputDatePickerFormField;
...@@ -2,135 +2,55 @@ ...@@ -2,135 +2,55 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart'; import 'feedback_tester.dart';
void main() { void main() {
group('showDatePicker', () {
_tests();
});
}
void _tests() {
DateTime firstDate; DateTime firstDate;
DateTime lastDate; DateTime lastDate;
DateTime initialDate; DateTime initialDate;
SelectableDayPredicate selectableDayPredicate; SelectableDayPredicate selectableDayPredicate;
DatePickerMode initialDatePickerMode; DatePickerEntryMode initialEntryMode;
DatePickerMode initialCalendarMode;
String cancelText;
String confirmText;
String errorFormatText;
String errorInvalidText;
String fieldHintText;
String fieldLabelText;
String helpText;
final Finder nextMonthIcon = find.byWidgetPredicate((Widget w) => w is IconButton && (w.tooltip?.startsWith('Next month') ?? false)); final Finder nextMonthIcon = find.byWidgetPredicate((Widget w) => w is IconButton && (w.tooltip?.startsWith('Next month') ?? false));
final Finder previousMonthIcon = find.byWidgetPredicate((Widget w) => w is IconButton && (w.tooltip?.startsWith('Previous month') ?? false)); final Finder previousMonthIcon = find.byWidgetPredicate((Widget w) => w is IconButton && (w.tooltip?.startsWith('Previous month') ?? false));
final Finder switchToInputIcon = find.byIcon(Icons.edit);
final Finder switchToCalendarIcon = find.byIcon(Icons.calendar_today);
TextField textField(WidgetTester tester) {
return tester.widget<TextField>(find.byType(TextField));
}
setUp(() { setUp(() {
firstDate = DateTime(2001, DateTime.january, 1); firstDate = DateTime(2001, DateTime.january, 1);
lastDate = DateTime(2031, DateTime.december, 31); lastDate = DateTime(2031, DateTime.december, 31);
initialDate = DateTime(2016, DateTime.january, 15); initialDate = DateTime(2016, DateTime.january, 15);
selectableDayPredicate = null; selectableDayPredicate = null;
initialDatePickerMode = null; initialEntryMode = DatePickerEntryMode.calendar;
}); initialCalendarMode = DatePickerMode.day;
testWidgets('tap-select a day', (WidgetTester tester) async {
final Key _datePickerKey = UniqueKey();
DateTime _selectedDate = DateTime(2016, DateTime.july, 26);
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Container(
width: 400.0,
child: SingleChildScrollView(
dragStartBehavior: DragStartBehavior.down,
child: Material(
child: MonthPicker(
dragStartBehavior: DragStartBehavior.down,
firstDate: DateTime(0),
lastDate: DateTime(9999),
key: _datePickerKey,
selectedDate: _selectedDate,
onChanged: (DateTime value) {
setState(() {
_selectedDate = value;
});
},
),
),
),
);
},
),
),
);
expect(_selectedDate, equals(DateTime(2016, DateTime.july, 26)));
await tester.tapAt(const Offset(50.0, 100.0));
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 2));
await tester.tap(find.text('1'));
await tester.pumpAndSettle();
expect(_selectedDate, equals(DateTime(2016, DateTime.july, 1)));
await tester.tap(nextMonthIcon);
await tester.pumpAndSettle();
expect(_selectedDate, equals(DateTime(2016, DateTime.july, 1)));
await tester.tap(find.text('5'));
await tester.pumpAndSettle();
expect(_selectedDate, equals(DateTime(2016, DateTime.august, 5)));
await tester.drag(find.byKey(_datePickerKey), const Offset(-400.0, 0.0));
await tester.pumpAndSettle();
expect(_selectedDate, equals(DateTime(2016, DateTime.august, 5)));
await tester.tap(find.text('25'));
await tester.pumpAndSettle();
expect(_selectedDate, equals(DateTime(2016, DateTime.september, 25)));
await tester.drag(find.byKey(_datePickerKey), const Offset(800.0, 0.0)); cancelText = null;
await tester.pumpAndSettle(); confirmText = null;
expect(_selectedDate, equals(DateTime(2016, DateTime.september, 25))); errorFormatText = null;
errorInvalidText = null;
await tester.tap(find.text('17')); fieldHintText = null;
await tester.pumpAndSettle(); fieldLabelText = null;
expect(_selectedDate, equals(DateTime(2016, DateTime.august, 17))); helpText = null;
});
testWidgets('render picker with intrinsic dimensions', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return IntrinsicWidth(
child: IntrinsicHeight(
child: Material(
child: SingleChildScrollView(
child: MonthPicker(
firstDate: DateTime(0),
lastDate: DateTime(9999),
onChanged: (DateTime value) { },
selectedDate: DateTime(2000, DateTime.january, 1),
),
),
),
),
);
},
),
),
);
await tester.pump(const Duration(seconds: 5));
}); });
Future<void> preparePicker(WidgetTester tester, Future<void> callback(Future<DateTime> date)) async { Future<void> prepareDatePicker(WidgetTester tester, Future<void> callback(Future<DateTime> date)) async {
BuildContext buttonContext; BuildContext buttonContext;
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: Material( home: Material(
...@@ -150,46 +70,164 @@ void _tests() { ...@@ -150,46 +70,164 @@ void _tests() {
await tester.tap(find.text('Go')); await tester.tap(find.text('Go'));
expect(buttonContext, isNotNull); expect(buttonContext, isNotNull);
final Future<DateTime> date = initialDatePickerMode == null final Future<DateTime> date = showDatePicker(
// Exercise the argument default for initialDatePickerMode.
?
showDatePicker(
context: buttonContext,
initialDate: initialDate,
firstDate: firstDate,
lastDate: lastDate,
selectableDayPredicate: selectableDayPredicate,
)
:
showDatePicker(
context: buttonContext, context: buttonContext,
initialDate: initialDate, initialDate: initialDate,
firstDate: firstDate, firstDate: firstDate,
lastDate: lastDate, lastDate: lastDate,
selectableDayPredicate: selectableDayPredicate, selectableDayPredicate: selectableDayPredicate,
initialDatePickerMode: initialDatePickerMode, initialDatePickerMode: initialCalendarMode,
initialEntryMode: initialEntryMode,
cancelText: cancelText,
confirmText: confirmText,
errorFormatText: errorFormatText,
errorInvalidText: errorInvalidText,
fieldHintText: fieldHintText,
fieldLabelText: fieldLabelText,
helpText: helpText,
); );
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
await callback(date); await callback(date);
} }
group('showDatePicker Dialog', () {
testWidgets('Cancel, confirm, and help text is used', (WidgetTester tester) async {
cancelText = 'nope';
confirmText = 'yep';
helpText = 'help';
await prepareDatePicker(tester, (Future<DateTime> date) async {
expect(find.text(cancelText), findsOneWidget);
expect(find.text(confirmText), findsOneWidget);
expect(find.text(helpText), findsOneWidget);
});
});
testWidgets('Initial date is the default', (WidgetTester tester) async { testWidgets('Initial date is the default', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('OK')); await tester.tap(find.text('OK'));
expect(await date, equals(DateTime(2016, DateTime.january, 15))); expect(await date, DateTime(2016, DateTime.january, 15));
}); });
}); });
testWidgets('Can cancel', (WidgetTester tester) async { testWidgets('Can cancel', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('CANCEL')); await tester.tap(find.text('CANCEL'));
expect(await date, isNull); expect(await date, isNull);
}); });
}); });
testWidgets('Can toggle to input entry mode', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
expect(find.byType(TextField), findsNothing);
await tester.tap(find.byIcon(Icons.edit));
await tester.pumpAndSettle();
expect(find.byType(TextField), findsOneWidget);
});
});
testWidgets('Toggle to input mode keeps selected date', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('12'));
await tester.tap(find.byIcon(Icons.edit));
await tester.pumpAndSettle();
await tester.tap(find.text('OK'));
expect(await date, DateTime(2016, DateTime.january, 12));
});
});
testWidgets('builder parameter', (WidgetTester tester) async {
Widget buildFrame(TextDirection textDirection) {
return MaterialApp(
home: Material(
child: Center(
child: Builder(
builder: (BuildContext context) {
return RaisedButton(
child: const Text('X'),
onPressed: () {
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2018),
lastDate: DateTime(2030),
builder: (BuildContext context, Widget child) {
return Directionality(
textDirection: textDirection,
child: child,
);
},
);
},
);
},
),
),
),
);
}
await tester.pumpWidget(buildFrame(TextDirection.ltr));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
final double ltrOkRight = tester.getBottomRight(find.text('OK')).dx;
await tester.tap(find.text('OK')); // Dismiss the dialog.
await tester.pumpAndSettle();
await tester.pumpWidget(buildFrame(TextDirection.rtl));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
// Verify that the time picker is being laid out RTL.
// We expect the left edge of the 'OK' button in the RTL
// layout to match the gap between right edge of the 'OK'
// button and the right edge of the 800 wide window.
expect(tester.getBottomLeft(find.text('OK')).dx, 800 - ltrOkRight);
});
testWidgets('uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
final _DatePickerObserver rootObserver = _DatePickerObserver();
final _DatePickerObserver nestedObserver = _DatePickerObserver();
await tester.pumpWidget(MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Navigator(
observers: <NavigatorObserver>[nestedObserver],
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return RaisedButton(
onPressed: () {
showDatePicker(
context: context,
useRootNavigator: false,
initialDate: DateTime.now(),
firstDate: DateTime(2018),
lastDate: DateTime(2030),
builder: (BuildContext context, Widget child) => const SizedBox(),
);
},
child: const Text('Show Date Picker'),
);
},
);
},
),
));
// Open the dialog.
await tester.tap(find.byType(RaisedButton));
expect(rootObserver.datePickerCount, 0);
expect(nestedObserver.datePickerCount, 1);
});
});
group('Calendar mode', () {
testWidgets('Can select a day', (WidgetTester tester) async { testWidgets('Can select a day', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('12')); await tester.tap(find.text('12'));
await tester.tap(find.text('OK')); await tester.tap(find.text('OK'));
expect(await date, equals(DateTime(2016, DateTime.january, 12))); expect(await date, equals(DateTime(2016, DateTime.january, 12)));
...@@ -197,51 +235,67 @@ void _tests() { ...@@ -197,51 +235,67 @@ void _tests() {
}); });
testWidgets('Can select a month', (WidgetTester tester) async { testWidgets('Can select a month', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(previousMonthIcon); await tester.tap(previousMonthIcon);
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.tap(find.text('25')); await tester.tap(find.text('25'));
await tester.tap(find.text('OK')); await tester.tap(find.text('OK'));
expect(await date, equals(DateTime(2015, DateTime.december, 25))); expect(await date, DateTime(2015, DateTime.december, 25));
}); });
}); });
testWidgets('Can select a year', (WidgetTester tester) async { testWidgets('Can select a year', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('2016')); await tester.tap(find.text('January 2016')); // Switch to year mode.
await tester.pump(); await tester.pump();
await tester.tap(find.text('2018')); await tester.tap(find.text('2018'));
await tester.pump();
expect(find.text('January 2018'), findsOneWidget);
});
});
testWidgets('Changing year does not change selected date', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('January 2016'));
await tester.pump();
await tester.tap(find.text('2018'));
await tester.pump();
await tester.tap(find.text('OK')); await tester.tap(find.text('OK'));
expect(await date, equals(DateTime(2018, DateTime.january, 15))); expect(await date, equals(DateTime(2016, DateTime.january, 15)));
});
});
testWidgets('Changing year does not change the month', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(nextMonthIcon);
await tester.pumpAndSettle();
await tester.tap(nextMonthIcon);
await tester.pumpAndSettle();
await tester.tap(find.text('March 2016'));
await tester.pumpAndSettle();
await tester.tap(find.text('2018'));
await tester.pumpAndSettle();
expect(find.text('March 2018'), findsOneWidget);
}); });
}); });
testWidgets('Can select a year and then a day', (WidgetTester tester) async { testWidgets('Can select a year and then a day', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('2016')); await tester.tap(find.text('January 2016')); // Switch to year mode.
await tester.pump(); await tester.pump();
await tester.tap(find.text('2017')); await tester.tap(find.text('2017'));
await tester.pump(); await tester.pump();
final MaterialLocalizations localizations = MaterialLocalizations.of(
tester.element(find.byType(DayPicker))
);
final String dayLabel = localizations.formatMediumDate(DateTime(2017, DateTime.january, 15));
await tester.tap(find.text(dayLabel));
await tester.pump();
await tester.tap(find.text('19')); await tester.tap(find.text('19'));
await tester.tap(find.text('OK')); await tester.tap(find.text('OK'));
expect(await date, equals(DateTime(2017, DateTime.january, 19))); expect(await date, DateTime(2017, DateTime.january, 19));
}); });
}); });
testWidgets('Current year is initially visible in year picker', (WidgetTester tester) async { testWidgets('Current year is visible in year picker', (WidgetTester tester) async {
initialDate = DateTime(2000); await prepareDatePicker(tester, (Future<DateTime> date) async {
firstDate = DateTime(1900); await tester.tap(find.text('January 2016')); // Switch to year mode.
lastDate = DateTime(2100);
await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('2000'));
await tester.pump(); await tester.pump();
expect(find.text('2000'), findsNWidgets(2)); expect(find.text('2016'), findsOneWidget);
}); });
}); });
...@@ -249,12 +303,14 @@ void _tests() { ...@@ -249,12 +303,14 @@ void _tests() {
initialDate = DateTime(2017, DateTime.january, 15); initialDate = DateTime(2017, DateTime.january, 15);
firstDate = initialDate; firstDate = initialDate;
lastDate = initialDate; lastDate = initialDate;
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('10')); // Earlier than firstDate. Should be ignored. // Earlier than firstDate. Should be ignored.
await tester.tap(find.text('20')); // Later than lastDate. Should be ignored. await tester.tap(find.text('10'));
// Later than lastDate. Should be ignored.
await tester.tap(find.text('20'));
await tester.tap(find.text('OK')); await tester.tap(find.text('OK'));
// We should still be on the initial date. // We should still be on the initial date.
expect(await date, equals(initialDate)); expect(await date, initialDate);
}); });
}); });
...@@ -262,7 +318,7 @@ void _tests() { ...@@ -262,7 +318,7 @@ void _tests() {
initialDate = DateTime(2017, DateTime.january, 15); initialDate = DateTime(2017, DateTime.january, 15);
firstDate = initialDate; firstDate = initialDate;
lastDate = DateTime(2017, DateTime.february, 20); lastDate = DateTime(2017, DateTime.february, 20);
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(nextMonthIcon); await tester.tap(nextMonthIcon);
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
// Shouldn't be possible to keep going into March. // Shouldn't be possible to keep going into March.
...@@ -274,7 +330,7 @@ void _tests() { ...@@ -274,7 +330,7 @@ void _tests() {
initialDate = DateTime(2017, DateTime.january, 15); initialDate = DateTime(2017, DateTime.january, 15);
firstDate = DateTime(2016, DateTime.december, 10); firstDate = DateTime(2016, DateTime.december, 10);
lastDate = initialDate; lastDate = initialDate;
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(previousMonthIcon); await tester.tap(previousMonthIcon);
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
// Shouldn't be possible to keep going into November. // Shouldn't be possible to keep going into November.
...@@ -282,68 +338,198 @@ void _tests() { ...@@ -282,68 +338,198 @@ void _tests() {
}); });
}); });
testWidgets('Cannot select disabled year', (WidgetTester tester) async {
initialDate = DateTime(2018, DateTime.july, 4);
firstDate = DateTime(2018, DateTime.june, 9);
lastDate = DateTime(2018, DateTime.december, 15);
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('July 2018')); // Switch to year mode.
await tester.pumpAndSettle();
await tester.tap(find.text('2016')); // Disabled, doesn't change the year.
await tester.pumpAndSettle();
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
expect(await date, DateTime(2018, DateTime.july, 4));
});
});
testWidgets('Selecting firstDate year respects firstDate', (WidgetTester tester) async { testWidgets('Selecting firstDate year respects firstDate', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/17309
initialDate = DateTime(2018, DateTime.may, 4); initialDate = DateTime(2018, DateTime.may, 4);
firstDate = DateTime(2016, DateTime.june, 9); firstDate = DateTime(2016, DateTime.june, 9);
lastDate = DateTime(2019, DateTime.january, 15); lastDate = DateTime(2019, DateTime.january, 15);
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('2018')); await tester.tap(find.text('May 2018'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('2016')); await tester.tap(find.text('2016'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('OK')); // Month should be clamped to June as the range starts at June 2016
await tester.pumpAndSettle(); expect(find.text('June 2016'), findsOneWidget);
expect(await date, DateTime(2016, DateTime.june, 9));
}); });
}); });
testWidgets('Selecting lastDate year respects lastDate', (WidgetTester tester) async { testWidgets('Selecting lastDate year respects lastDate', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/17309
initialDate = DateTime(2018, DateTime.may, 4); initialDate = DateTime(2018, DateTime.may, 4);
firstDate = DateTime(2016, DateTime.june, 9); firstDate = DateTime(2016, DateTime.june, 9);
lastDate = DateTime(2019, DateTime.january, 15); lastDate = DateTime(2019, DateTime.january, 15);
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('2018')); await tester.tap(find.text('May 2018'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('2019')); await tester.tap(find.text('2019'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('OK')); // Month should be clamped to January as the range ends at January 2019
await tester.pumpAndSettle(); expect(find.text('January 2019'), findsOneWidget);
expect(await date, DateTime(2019, DateTime.january, 15));
}); });
}); });
testWidgets('Only predicate days are selectable', (WidgetTester tester) async { testWidgets('Only predicate days are selectable', (WidgetTester tester) async {
initialDate = DateTime(2017, DateTime.january, 16); initialDate = DateTime(2017, DateTime.january, 16);
firstDate = DateTime(2017, DateTime.january, 10); firstDate = DateTime(2017, DateTime.january, 10);
lastDate = DateTime(2017, DateTime.january, 20); lastDate = DateTime(2017, DateTime.january, 20);
selectableDayPredicate = (DateTime day) => day.day.isEven; selectableDayPredicate = (DateTime day) => day.day.isEven;
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('10')); // Even, works.
await tester.tap(find.text('13')); // Odd, doesn't work. await tester.tap(find.text('13')); // Odd, doesn't work.
await tester.tap(find.text('10')); // Even, works.
await tester.tap(find.text('17')); // Odd, doesn't work. await tester.tap(find.text('17')); // Odd, doesn't work.
await tester.tap(find.text('OK')); await tester.tap(find.text('OK'));
expect(await date, equals(DateTime(2017, DateTime.january, 10))); expect(await date, DateTime(2017, DateTime.january, 10));
}); });
}); });
testWidgets('Can select initial date picker mode', (WidgetTester tester) async { testWidgets('Can select initial date picker mode', (WidgetTester tester) async {
initialDate = DateTime(2014, DateTime.january, 15); initialDate = DateTime(2014, DateTime.january, 15);
initialDatePickerMode = DatePickerMode.year; initialCalendarMode = DatePickerMode.year;
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.pump(); await tester.pump();
// 2018 wouldn't be available if the year picker wasn't showing. // 2018 wouldn't be available if the year picker wasn't showing.
// The initial current year is 2014. // The initial current year is 2014.
await tester.tap(find.text('2018')); await tester.tap(find.text('2018'));
await tester.pump();
expect(find.text('January 2018'), findsOneWidget);
});
});
});
group('Input mode', () {
setUp(() {
firstDate = DateTime(2015, DateTime.january, 1);
lastDate = DateTime(2017, DateTime.december, 31);
initialDate = DateTime(2016, DateTime.january, 15);
initialEntryMode = DatePickerEntryMode.input;
});
testWidgets('Initial entry mode is used', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
expect(find.byType(TextField), findsOneWidget);
});
});
testWidgets('Hint, label, and help text is used', (WidgetTester tester) async {
cancelText = 'nope';
confirmText = 'yep';
fieldHintText = 'hint';
fieldLabelText = 'label';
helpText = 'help';
await prepareDatePicker(tester, (Future<DateTime> date) async {
expect(find.text(cancelText), findsOneWidget);
expect(find.text(confirmText), findsOneWidget);
expect(find.text(fieldHintText), findsOneWidget);
expect(find.text(fieldLabelText), findsOneWidget);
expect(find.text(helpText), findsOneWidget);
});
});
testWidgets('Initial date is the default', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('OK'));
expect(await date, DateTime(2016, DateTime.january, 15));
});
});
testWidgets('Can toggle to calendar entry mode', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
expect(find.byType(TextField), findsOneWidget);
await tester.tap(find.byIcon(Icons.calendar_today));
await tester.pumpAndSettle();
expect(find.byType(TextField), findsNothing);
});
});
testWidgets('Toggle to calendar mode keeps selected date', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
final TextField field = textField(tester);
field.controller.clear();
await tester.enterText(find.byType(TextField), '12/25/2016');
await tester.tap(find.byIcon(Icons.calendar_today));
await tester.pumpAndSettle();
await tester.tap(find.text('OK'));
expect(await date, DateTime(2016, DateTime.december, 25));
});
});
testWidgets('Entered text returns date', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
final TextField field = textField(tester);
field.controller.clear();
await tester.enterText(find.byType(TextField), '12/25/2016');
await tester.tap(find.text('OK'));
expect(await date, DateTime(2016, DateTime.december, 25));
});
});
testWidgets('Too short entered text shows error', (WidgetTester tester) async {
errorFormatText = 'oops';
await prepareDatePicker(tester, (Future<DateTime> date) async {
final TextField field = textField(tester);
field.controller.clear();
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), '1225');
expect(find.text(errorFormatText), findsNothing);
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
expect(find.text(errorFormatText), findsOneWidget);
});
});
testWidgets('Bad format entered text shows error', (WidgetTester tester) async {
errorFormatText = 'oops';
await prepareDatePicker(tester, (Future<DateTime> date) async {
final TextField field = textField(tester);
field.controller.clear();
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), '20202014');
expect(find.text(errorFormatText), findsNothing);
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
expect(find.text(errorFormatText), findsOneWidget);
});
});
testWidgets('Invalid entered text shows error', (WidgetTester tester) async {
errorInvalidText = 'oops';
await prepareDatePicker(tester, (Future<DateTime> date) async {
final TextField field = textField(tester);
field.controller.clear();
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), '08/10/1969');
expect(find.text(errorInvalidText), findsNothing);
await tester.tap(find.text('OK')); await tester.tap(find.text('OK'));
expect(await date, equals(DateTime(2018, DateTime.january, 15))); await tester.pumpAndSettle();
expect(find.text(errorInvalidText), findsOneWidget);
});
}); });
}); });
group('haptic feedback', () { group('Haptic feedback', () {
const Duration kHapticFeedbackInterval = Duration(milliseconds: 10); const Duration hapticFeedbackInterval = Duration(milliseconds: 10);
FeedbackTester feedback; FeedbackTester feedback;
setUp(() { setUp(() {
...@@ -351,6 +537,7 @@ void _tests() { ...@@ -351,6 +537,7 @@ void _tests() {
initialDate = DateTime(2017, DateTime.january, 16); initialDate = DateTime(2017, DateTime.january, 16);
firstDate = DateTime(2017, DateTime.january, 10); firstDate = DateTime(2017, DateTime.january, 10);
lastDate = DateTime(2018, DateTime.january, 20); lastDate = DateTime(2018, DateTime.january, 20);
initialCalendarMode = DatePickerMode.day;
selectableDayPredicate = (DateTime date) => date.day.isEven; selectableDayPredicate = (DateTime date) => date.day.isEven;
}); });
...@@ -358,464 +545,358 @@ void _tests() { ...@@ -358,464 +545,358 @@ void _tests() {
feedback?.dispose(); feedback?.dispose();
}); });
testWidgets('tap-select date vibrates', (WidgetTester tester) async { testWidgets('Selecting date vibrates', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('10')); await tester.tap(find.text('10'));
await tester.pump(kHapticFeedbackInterval); await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 1); expect(feedback.hapticCount, 1);
await tester.tap(find.text('12')); await tester.tap(find.text('12'));
await tester.pump(kHapticFeedbackInterval); await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 2); expect(feedback.hapticCount, 2);
await tester.tap(find.text('14')); await tester.tap(find.text('14'));
await tester.pump(kHapticFeedbackInterval); await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 3); expect(feedback.hapticCount, 3);
}); });
}); });
testWidgets('tap-select unselectable date does not vibrate', (WidgetTester tester) async { testWidgets('Tapping unselectable date does not vibrate', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('11')); await tester.tap(find.text('11'));
await tester.pump(kHapticFeedbackInterval); await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 0); expect(feedback.hapticCount, 0);
await tester.tap(find.text('13')); await tester.tap(find.text('13'));
await tester.pump(kHapticFeedbackInterval); await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 0); expect(feedback.hapticCount, 0);
await tester.tap(find.text('15')); await tester.tap(find.text('15'));
await tester.pump(kHapticFeedbackInterval); await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 0); expect(feedback.hapticCount, 0);
}); });
}); });
testWidgets('mode, year change vibrates', (WidgetTester tester) async { testWidgets('Changing modes and year vibrates', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('2017')); await tester.tap(find.text('January 2017'));
await tester.pump(kHapticFeedbackInterval); await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 1); expect(feedback.hapticCount, 1);
await tester.tap(find.text('2018')); await tester.tap(find.text('2018'));
await tester.pump(kHapticFeedbackInterval); await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 2); expect(feedback.hapticCount, 2);
}); });
}); });
}); });
test('days in month', () { group('Semantics', () {
expect(DayPicker.getDaysInMonth(2017, 10), 31); testWidgets('calendar day mode', (WidgetTester tester) async {
expect(DayPicker.getDaysInMonth(2017, 6), 30); final SemanticsHandle semantics = tester.ensureSemantics();
expect(DayPicker.getDaysInMonth(2017, 2), 28);
expect(DayPicker.getDaysInMonth(2016, 2), 29);
expect(DayPicker.getDaysInMonth(2000, 2), 29);
expect(DayPicker.getDaysInMonth(1900, 2), 28);
});
testWidgets('month header tap', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime> date) async {
selectableDayPredicate = null; // Header
await preparePicker(tester, (Future<DateTime> date) async { expect(
// Switch into the year selector. tester.getSemantics(find.text('SELECT DATE')), matchesSemantics(
await tester.tap(find.text('January 2016')); label: 'SELECT DATE\nFri, Jan 15',
await tester.pump(); ));
expect(find.text('2020'), isNotNull);
await tester.tap(find.text('CANCEL')); // Input mode toggle button
expect(await date, isNull); expect(tester.getSemantics(switchToInputIcon), matchesSemantics(
}); label: 'Switch to input',
}); isButton: true,
hasTapAction: true,
isEnabled: true,
hasEnabledState: true,
isFocusable: true,
));
testWidgets('exports semantics', (WidgetTester tester) async { // Year mode drop down button
final SemanticsTester semantics = SemanticsTester(tester); expect(
await preparePicker(tester, (Future<DateTime> date) async { tester.getSemantics(find.text('January 2016')), matchesSemantics(
final TestSemantics expected = TestSemantics( label: 'Select year',
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], isButton: true,
children: <TestSemantics>[ ));
TestSemantics(
elevation: 24.0, // Prev/Next month buttons
thickness: 0.0, expect(tester.getSemantics(previousMonthIcon), matchesSemantics(
children: <TestSemantics>[ label: 'Previous month December 2015',
TestSemantics( isButton: true,
flags: <SemanticsFlag>[SemanticsFlag.isFocusable], hasTapAction: true,
actions: <SemanticsAction>[SemanticsAction.tap], isEnabled: true,
label: '2016', hasEnabledState: true,
textDirection: TextDirection.ltr, isFocusable: true,
), ));
TestSemantics( expect(tester.getSemantics(nextMonthIcon), matchesSemantics(
flags: <SemanticsFlag>[ label: 'Next month February 2016',
SemanticsFlag.isSelected, isButton: true,
SemanticsFlag.isFocusable, hasTapAction: true,
], isEnabled: true,
actions: <SemanticsAction>[SemanticsAction.tap], hasEnabledState: true,
label: 'Fri, Jan 15', isFocusable: true,
textDirection: TextDirection.ltr, ));
),
TestSemantics( // Day grid
children: <TestSemantics>[ expect(tester.getSemantics(find.text('1')), matchesSemantics(
TestSemantics(
id: 55,
actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight],
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
id: 11,
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
children: <TestSemantics>[
// TODO(dnfield): These shouldn't be here. https://github.com/flutter/flutter/issues/34431
TestSemantics(),
TestSemantics(),
TestSemantics(),
TestSemantics(),
TestSemantics(),
TestSemantics(),
TestSemantics(),
TestSemantics(),
TestSemantics(),
TestSemantics(),
TestSemantics(),
TestSemantics(),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '1, Friday, January 1, 2016', label: '1, Friday, January 1, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('2')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '2, Saturday, January 2, 2016', label: '2, Saturday, January 2, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('3')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '3, Sunday, January 3, 2016', label: '3, Sunday, January 3, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('4')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '4, Monday, January 4, 2016', label: '4, Monday, January 4, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('5')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '5, Tuesday, January 5, 2016', label: '5, Tuesday, January 5, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('6')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '6, Wednesday, January 6, 2016', label: '6, Wednesday, January 6, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('7')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '7, Thursday, January 7, 2016', label: '7, Thursday, January 7, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('8')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '8, Friday, January 8, 2016', label: '8, Friday, January 8, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('9')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '9, Saturday, January 9, 2016', label: '9, Saturday, January 9, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('10')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '10, Sunday, January 10, 2016', label: '10, Sunday, January 10, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('11')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '11, Monday, January 11, 2016', label: '11, Monday, January 11, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('12')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '12, Tuesday, January 12, 2016', label: '12, Tuesday, January 12, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('13')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '13, Wednesday, January 13, 2016', label: '13, Wednesday, January 13, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('14')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '14, Thursday, January 14, 2016', label: '14, Thursday, January 14, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('15')), matchesSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isSelected],
actions: <SemanticsAction>[SemanticsAction.tap],
label: '15, Friday, January 15, 2016', label: '15, Friday, January 15, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), isSelected: true,
TestSemantics( ));
actions: <SemanticsAction>[SemanticsAction.tap], expect(tester.getSemantics(find.text('16')), matchesSemantics(
label: '16, Saturday, January 16, 2016', label: '16, Saturday, January 16, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('17')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '17, Sunday, January 17, 2016', label: '17, Sunday, January 17, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('18')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '18, Monday, January 18, 2016', label: '18, Monday, January 18, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('19')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '19, Tuesday, January 19, 2016', label: '19, Tuesday, January 19, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('20')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '20, Wednesday, January 20, 2016', label: '20, Wednesday, January 20, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('21')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '21, Thursday, January 21, 2016', label: '21, Thursday, January 21, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('22')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '22, Friday, January 22, 2016', label: '22, Friday, January 22, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('23')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '23, Saturday, January 23, 2016', label: '23, Saturday, January 23, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('24')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '24, Sunday, January 24, 2016', label: '24, Sunday, January 24, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('25')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '25, Monday, January 25, 2016', label: '25, Monday, January 25, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('26')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '26, Tuesday, January 26, 2016', label: '26, Tuesday, January 26, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('27')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '27, Wednesday, January 27, 2016', label: '27, Wednesday, January 27, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('28')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '28, Thursday, January 28, 2016', label: '28, Thursday, January 28, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('29')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '29, Friday, January 29, 2016', label: '29, Friday, January 29, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics( expect(tester.getSemantics(find.text('30')), matchesSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '30, Saturday, January 30, 2016', label: '30, Saturday, January 30, 2016',
textDirection: TextDirection.ltr, hasTapAction: true,
), ));
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: '31, Sunday, January 31, 2016',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
),
],
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Previous month December 2015',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Next month February 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'CANCEL',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'OK',
textDirection: TextDirection.ltr,
),
],
),
],
);
expect(semantics, hasSemantics( // Ok/Cancel buttons
TestSemantics.root(children: <TestSemantics>[ expect(tester.getSemantics(find.text('OK')), matchesSemantics(
TestSemantics( label: 'OK',
children: <TestSemantics>[expected], isButton: true,
), hasTapAction: true,
]), isEnabled: true,
ignoreId: true, hasEnabledState: true,
ignoreTransform: true, isFocusable: true,
ignoreRect: true,
)); ));
expect(tester.getSemantics(find.text('CANCEL')), matchesSemantics(
label: 'CANCEL',
isButton: true,
hasTapAction: true,
isEnabled: true,
hasEnabledState: true,
isFocusable: true,
));
}); });
semantics.dispose(); semantics.dispose();
}); });
testWidgets('chervons animate when scrolling month picker', (WidgetTester tester) async { testWidgets('calendar year mode', (WidgetTester tester) async {
final Key _datePickerKey = UniqueKey(); final SemanticsHandle semantics = tester.ensureSemantics();
DateTime _selectedDate = DateTime(2016, DateTime.july, 26);
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Container(
width: 400.0,
child: SingleChildScrollView(
child: Material(
child: MonthPicker(
firstDate: DateTime(0),
lastDate: DateTime(9999),
key: _datePickerKey,
selectedDate: _selectedDate,
onChanged: (DateTime value) {
setState(() {
_selectedDate = value;
});
},
),
),
),
);
},
),
),
);
final Finder chevronFinder = find.byType(IconButton); initialCalendarMode = DatePickerMode.year;
final List<RenderAnimatedOpacity> chevronRenderers = chevronFinder await prepareDatePicker(tester, (Future<DateTime> date) async {
.evaluate() // Header
.map((Element element) => element.findAncestorRenderObjectOfType<RenderAnimatedOpacity>()) expect(tester.getSemantics(find.text('SELECT DATE')), matchesSemantics(
.toList(); label: 'SELECT DATE\nFri, Jan 15',
));
// Initial chevron animation state should be dismissed
// An AlwaysStoppedAnimation is also found and is ignored
for (final RenderAnimatedOpacity renderer in chevronRenderers) {
expect(renderer.opacity.value, equals(1.0));
expect(renderer.opacity.status, equals(AnimationStatus.dismissed));
}
// Drag and hold the picker to test for the opacity change // Input mode toggle button
final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0)); expect(tester.getSemantics(switchToInputIcon), matchesSemantics(
await gesture.moveBy(const Offset(50.0, 100.0)); label: 'Switch to input',
await tester.pumpAndSettle(); isButton: true,
for (final RenderAnimatedOpacity renderer in chevronRenderers) { hasTapAction: true,
expect(renderer.opacity.value, equals(0.0)); isEnabled: true,
expect(renderer.opacity.status, equals(AnimationStatus.completed)); hasEnabledState: true,
} isFocusable: true,
));
// Release the drag and test for the opacity to return to original value // Year mode drop down button
await gesture.up(); expect(tester.getSemantics(find.text('January 2016')), matchesSemantics(
await tester.pumpAndSettle(); label: 'Select year',
for (final RenderAnimatedOpacity renderer in chevronRenderers) { isButton: true,
expect(renderer.opacity.value, equals(1.0)); ));
expect(renderer.opacity.status, equals(AnimationStatus.dismissed));
// Year grid only shows 2010 - 2024
for (int year = 2010; year <= 2024; year++) {
expect(tester.getSemantics(find.text('$year')), matchesSemantics(
label: '$year',
hasTapAction: true,
isSelected: year == 2016,
isFocusable: true,
));
} }
// Ok/Cancel buttons
expect(tester.getSemantics(find.text('OK')), matchesSemantics(
label: 'OK',
isButton: true,
hasTapAction: true,
isEnabled: true,
hasEnabledState: true,
isFocusable: true,
));
expect(tester.getSemantics(find.text('CANCEL')), matchesSemantics(
label: 'CANCEL',
isButton: true,
hasTapAction: true,
isEnabled: true,
hasEnabledState: true,
isFocusable: true,
));
}); });
testWidgets('builder parameter', (WidgetTester tester) async { semantics.dispose();
Widget buildFrame(TextDirection textDirection) { });
return MaterialApp(
home: Material(
child: Center(
child: Builder(
builder: (BuildContext context) {
return RaisedButton(
child: const Text('X'),
onPressed: () {
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2018),
lastDate: DateTime(2030),
builder: (BuildContext context, Widget child) {
return Directionality(
textDirection: textDirection,
child: child,
);
},
);
},
);
},
),
),
),
);
}
await tester.pumpWidget(buildFrame(TextDirection.ltr)); testWidgets('input mode', (WidgetTester tester) async {
await tester.tap(find.text('X')); final SemanticsHandle semantics = tester.ensureSemantics();
await tester.pumpAndSettle();
final double ltrOkRight = tester.getBottomRight(find.text('OK')).dx;
await tester.tap(find.text('OK')); // dismiss the dialog initialEntryMode = DatePickerEntryMode.input;
await tester.pumpAndSettle(); await prepareDatePicker(tester, (Future<DateTime> date) async {
// Header
expect(tester.getSemantics(find.text('SELECT DATE')), matchesSemantics(
label: 'SELECT DATE\nFri, Jan 15',
));
await tester.pumpWidget(buildFrame(TextDirection.rtl)); // Input mode toggle button
await tester.tap(find.text('X')); expect(tester.getSemantics(switchToCalendarIcon), matchesSemantics(
await tester.pumpAndSettle(); label: 'Switch to calendar',
isButton: true,
hasTapAction: true,
isEnabled: true,
hasEnabledState: true,
isFocusable: true,
));
// Text field
expect(tester.getSemantics(find.byType(EditableText)), matchesSemantics(
label: 'Enter Date\nmm/dd/yyyy',
isTextField: true,
isFocused: true,
value: '01/15/2016',
hasTapAction: true,
hasSetSelectionAction: true,
hasCopyAction: true,
hasCutAction: true,
hasPasteAction: true,
hasMoveCursorBackwardByCharacterAction: true,
hasMoveCursorBackwardByWordAction: true,
));
// Ok/Cancel buttons
expect(tester.getSemantics(find.text('OK')), matchesSemantics(
label: 'OK',
isButton: true,
hasTapAction: true,
isEnabled: true,
hasEnabledState: true,
isFocusable: true,
));
expect(tester.getSemantics(find.text('CANCEL')), matchesSemantics(
label: 'CANCEL',
isButton: true,
hasTapAction: true,
isEnabled: true,
hasEnabledState: true,
isFocusable: true,
));
// Verify that the time picker is being laid out RTL.
// We expect the left edge of the 'OK' button in the RTL
// layout to match the gap between right edge of the 'OK'
// button and the right edge of the 800 wide window.
expect(tester.getBottomLeft(find.text('OK')).dx, 800 - ltrOkRight);
}); });
group('screen configurations', () { semantics.dispose();
});
});
group('Screen configurations', () {
// Test various combinations of screen sizes, orientations and text scales // Test various combinations of screen sizes, orientations and text scales
// to ensure the layout doesn't overflow and cause an exception to be thrown. // to ensure the layout doesn't overflow and cause an exception to be thrown.
...@@ -834,26 +915,11 @@ void _tests() { ...@@ -834,26 +915,11 @@ void _tests() {
Future<void> _showPicker(WidgetTester tester, Size size, [double textScaleFactor = 1.0]) async { Future<void> _showPicker(WidgetTester tester, Size size, [double textScaleFactor = 1.0]) async {
tester.binding.window.physicalSizeTestValue = size; tester.binding.window.physicalSizeTestValue = size;
tester.binding.window.devicePixelRatioTestValue = 1.0; tester.binding.window.devicePixelRatioTestValue = 1.0;
await tester.pumpWidget( tester.binding.window.clearPhysicalSizeTestValue();
MaterialApp( tester.binding.window.clearDevicePixelRatioTestValue();
home: Builder( await prepareDatePicker(tester, (Future<DateTime> date) async {
builder: (BuildContext context) { await tester.tap(find.text('OK'));
return RaisedButton( });
child: const Text('X'),
onPressed: () {
showDatePicker(
context: context,
initialDate: initialDate,
firstDate: firstDate,
lastDate: lastDate,
);
},
);
},
),
),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
} }
...@@ -897,84 +963,9 @@ void _tests() { ...@@ -897,84 +963,9 @@ void _tests() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
}); });
testWidgets('uses root navigator by default', (WidgetTester tester) async {
final DatePickerObserver rootObserver = DatePickerObserver();
final DatePickerObserver nestedObserver = DatePickerObserver();
await tester.pumpWidget(MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Navigator(
observers: <NavigatorObserver>[nestedObserver],
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return RaisedButton(
onPressed: () {
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2018),
lastDate: DateTime(2030),
builder: (BuildContext context, Widget child) {
return const SizedBox();
},
);
},
child: const Text('Show Date Picker'),
);
},
);
},
),
));
// Open the dialog.
await tester.tap(find.byType(RaisedButton));
expect(rootObserver.datePickerCount, 1);
expect(nestedObserver.datePickerCount, 0);
});
testWidgets('uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
final DatePickerObserver rootObserver = DatePickerObserver();
final DatePickerObserver nestedObserver = DatePickerObserver();
await tester.pumpWidget(MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Navigator(
observers: <NavigatorObserver>[nestedObserver],
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return RaisedButton(
onPressed: () {
showDatePicker(
context: context,
useRootNavigator: false,
initialDate: DateTime.now(),
firstDate: DateTime(2018),
lastDate: DateTime(2030),
builder: (BuildContext context, Widget child) => const SizedBox(),
);
},
child: const Text('Show Date Picker'),
);
},
);
},
),
));
// Open the dialog.
await tester.tap(find.byType(RaisedButton));
expect(rootObserver.datePickerCount, 0);
expect(nestedObserver.datePickerCount, 1);
});
} }
class DatePickerObserver extends NavigatorObserver { class _DatePickerObserver extends NavigatorObserver {
int datePickerCount = 0; int datePickerCount = 0;
@override @override
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -76,12 +76,14 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations { ...@@ -76,12 +76,14 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
/// 1. The string that would be returned by [Intl.canonicalizedLocale] for /// 1. The string that would be returned by [Intl.canonicalizedLocale] for
/// the locale. /// the locale.
/// 2. The [intl.DateFormat] for [formatYear]. /// 2. The [intl.DateFormat] for [formatYear].
/// 3. The [intl.DateFormat] for [formatMediumDate]. /// 3. The [int.DateFormat] for [formatShortDate].
/// 4. The [intl.DateFormat] for [formatFullDate]. /// 4. The [intl.DateFormat] for [formatMediumDate].
/// 5. The [intl.DateFormat] for [formatMonthYear]. /// 5. The [intl.DateFormat] for [formatFullDate].
/// 6. The [NumberFormat] for [formatDecimal] (also used by [formatHour] and /// 6. The [intl.DateFormat] for [formatMonthYear].
/// 7. The [int.DateFormat] for [formatShortMonthDay].
/// 8. The [NumberFormat] for [formatDecimal] (also used by [formatHour] and
/// [formatTimeOfDay] when [timeOfDayFormat] doesn't use [HourFormat.HH]). /// [formatTimeOfDay] when [timeOfDayFormat] doesn't use [HourFormat.HH]).
/// 7. The [NumberFormat] for [formatHour] and the hour part of /// 9. The [NumberFormat] for [formatHour] and the hour part of
/// [formatTimeOfDay] when [timeOfDayFormat] uses [HourFormat.HH], and for /// [formatTimeOfDay] when [timeOfDayFormat] uses [HourFormat.HH], and for
/// [formatMinute] and the minute part of [formatTimeOfDay]. /// [formatMinute] and the minute part of [formatTimeOfDay].
/// ///
...@@ -90,21 +92,30 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations { ...@@ -90,21 +92,30 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
const GlobalMaterialLocalizations({ const GlobalMaterialLocalizations({
@required String localeName, @required String localeName,
@required intl.DateFormat fullYearFormat, @required intl.DateFormat fullYearFormat,
@required intl.DateFormat compactDateFormat,
@required intl.DateFormat shortDateFormat,
@required intl.DateFormat mediumDateFormat, @required intl.DateFormat mediumDateFormat,
@required intl.DateFormat longDateFormat, @required intl.DateFormat longDateFormat,
@required intl.DateFormat yearMonthFormat, @required intl.DateFormat yearMonthFormat,
@required intl.DateFormat shortMonthDayFormat,
@required intl.NumberFormat decimalFormat, @required intl.NumberFormat decimalFormat,
@required intl.NumberFormat twoDigitZeroPaddedFormat, @required intl.NumberFormat twoDigitZeroPaddedFormat,
}) : assert(localeName != null), }) : assert(localeName != null),
_localeName = localeName, _localeName = localeName,
assert(fullYearFormat != null), assert(fullYearFormat != null),
_fullYearFormat = fullYearFormat, _fullYearFormat = fullYearFormat,
assert(compactDateFormat != null),
_compactDateFormat = compactDateFormat,
assert(shortDateFormat != null),
_shortDateFormat = shortDateFormat,
assert(mediumDateFormat != null), assert(mediumDateFormat != null),
_mediumDateFormat = mediumDateFormat, _mediumDateFormat = mediumDateFormat,
assert(longDateFormat != null), assert(longDateFormat != null),
_longDateFormat = longDateFormat, _longDateFormat = longDateFormat,
assert(yearMonthFormat != null), assert(yearMonthFormat != null),
_yearMonthFormat = yearMonthFormat, _yearMonthFormat = yearMonthFormat,
assert(shortMonthDayFormat != null),
_shortMonthDayFormat = shortMonthDayFormat,
assert(decimalFormat != null), assert(decimalFormat != null),
_decimalFormat = decimalFormat, _decimalFormat = decimalFormat,
assert(twoDigitZeroPaddedFormat != null), assert(twoDigitZeroPaddedFormat != null),
...@@ -112,9 +123,12 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations { ...@@ -112,9 +123,12 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
final String _localeName; final String _localeName;
final intl.DateFormat _fullYearFormat; final intl.DateFormat _fullYearFormat;
final intl.DateFormat _compactDateFormat;
final intl.DateFormat _shortDateFormat;
final intl.DateFormat _mediumDateFormat; final intl.DateFormat _mediumDateFormat;
final intl.DateFormat _longDateFormat; final intl.DateFormat _longDateFormat;
final intl.DateFormat _yearMonthFormat; final intl.DateFormat _yearMonthFormat;
final intl.DateFormat _shortMonthDayFormat;
final intl.NumberFormat _decimalFormat; final intl.NumberFormat _decimalFormat;
final intl.NumberFormat _twoDigitZeroPaddedFormat; final intl.NumberFormat _twoDigitZeroPaddedFormat;
...@@ -142,6 +156,16 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations { ...@@ -142,6 +156,16 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
return _fullYearFormat.format(date); return _fullYearFormat.format(date);
} }
@override
String formatCompactDate(DateTime date) {
return _compactDateFormat.format(date);
}
@override
String formatShortDate(DateTime date) {
return _shortDateFormat.format(date);
}
@override @override
String formatMediumDate(DateTime date) { String formatMediumDate(DateTime date) {
return _mediumDateFormat.format(date); return _mediumDateFormat.format(date);
...@@ -157,6 +181,20 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations { ...@@ -157,6 +181,20 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
return _yearMonthFormat.format(date); return _yearMonthFormat.format(date);
} }
@override
String formatShortMonthDay(DateTime date) {
return _shortMonthDayFormat.format(date);
}
@override
DateTime parseCompactDate(String inputString) {
try {
return _compactDateFormat.parseStrict(inputString);
} on FormatException {
return null;
}
}
@override @override
List<String> get narrowWeekdays { List<String> get narrowWeekdays {
return _longDateFormat.dateSymbols.NARROWWEEKDAYS; return _longDateFormat.dateSymbols.NARROWWEEKDAYS;
...@@ -576,24 +614,36 @@ class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocal ...@@ -576,24 +614,36 @@ class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocal
); );
intl.DateFormat fullYearFormat; intl.DateFormat fullYearFormat;
intl.DateFormat compactDateFormat;
intl.DateFormat shortDateFormat;
intl.DateFormat mediumDateFormat; intl.DateFormat mediumDateFormat;
intl.DateFormat longDateFormat; intl.DateFormat longDateFormat;
intl.DateFormat yearMonthFormat; intl.DateFormat yearMonthFormat;
intl.DateFormat shortMonthDayFormat;
if (intl.DateFormat.localeExists(localeName)) { if (intl.DateFormat.localeExists(localeName)) {
fullYearFormat = intl.DateFormat.y(localeName); fullYearFormat = intl.DateFormat.y(localeName);
compactDateFormat = intl.DateFormat.yMd(localeName);
shortDateFormat = intl.DateFormat.yMMMd(localeName);
mediumDateFormat = intl.DateFormat.MMMEd(localeName); mediumDateFormat = intl.DateFormat.MMMEd(localeName);
longDateFormat = intl.DateFormat.yMMMMEEEEd(localeName); longDateFormat = intl.DateFormat.yMMMMEEEEd(localeName);
yearMonthFormat = intl.DateFormat.yMMMM(localeName); yearMonthFormat = intl.DateFormat.yMMMM(localeName);
shortMonthDayFormat = intl.DateFormat.MMMd(localeName);
} else if (intl.DateFormat.localeExists(locale.languageCode)) { } else if (intl.DateFormat.localeExists(locale.languageCode)) {
fullYearFormat = intl.DateFormat.y(locale.languageCode); fullYearFormat = intl.DateFormat.y(locale.languageCode);
compactDateFormat = intl.DateFormat.yMd(locale.languageCode);
shortDateFormat = intl.DateFormat.yMMMd(locale.languageCode);
mediumDateFormat = intl.DateFormat.MMMEd(locale.languageCode); mediumDateFormat = intl.DateFormat.MMMEd(locale.languageCode);
longDateFormat = intl.DateFormat.yMMMMEEEEd(locale.languageCode); longDateFormat = intl.DateFormat.yMMMMEEEEd(locale.languageCode);
yearMonthFormat = intl.DateFormat.yMMMM(locale.languageCode); yearMonthFormat = intl.DateFormat.yMMMM(locale.languageCode);
shortMonthDayFormat = intl.DateFormat.MMMd(locale.languageCode);
} else { } else {
fullYearFormat = intl.DateFormat.y(); fullYearFormat = intl.DateFormat.y();
compactDateFormat = intl.DateFormat.yMd();
shortDateFormat = intl.DateFormat.yMMMd();
mediumDateFormat = intl.DateFormat.MMMEd(); mediumDateFormat = intl.DateFormat.MMMEd();
longDateFormat = intl.DateFormat.yMMMMEEEEd(); longDateFormat = intl.DateFormat.yMMMMEEEEd();
yearMonthFormat = intl.DateFormat.yMMMM(); yearMonthFormat = intl.DateFormat.yMMMM();
shortMonthDayFormat = intl.DateFormat.MMMd();
} }
intl.NumberFormat decimalFormat; intl.NumberFormat decimalFormat;
...@@ -612,9 +662,12 @@ class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocal ...@@ -612,9 +662,12 @@ class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocal
return SynchronousFuture<MaterialLocalizations>(getMaterialTranslation( return SynchronousFuture<MaterialLocalizations>(getMaterialTranslation(
locale, locale,
fullYearFormat, fullYearFormat,
compactDateFormat,
shortDateFormat,
mediumDateFormat, mediumDateFormat,
longDateFormat, longDateFormat,
yearMonthFormat, yearMonthFormat,
shortMonthDayFormat,
decimalFormat, decimalFormat,
twoDigitZeroPaddedFormat, twoDigitZeroPaddedFormat,
)); ));
......
...@@ -19,7 +19,7 @@ void main() { ...@@ -19,7 +19,7 @@ void main() {
initialDate = DateTime(2016, DateTime.january, 15); initialDate = DateTime(2016, DateTime.january, 15);
}); });
group(DayPicker, () { group(CalendarDatePicker, () {
final intl.NumberFormat arabicNumbers = intl.NumberFormat('0', 'ar'); final intl.NumberFormat arabicNumbers = intl.NumberFormat('0', 'ar');
final Map<Locale, Map<String, dynamic>> testLocales = <Locale, Map<String, dynamic>>{ final Map<Locale, Map<String, dynamic>> testLocales = <Locale, Map<String, dynamic>>{
// Tests the default. // Tests the default.
...@@ -59,13 +59,11 @@ void main() { ...@@ -59,13 +59,11 @@ void main() {
final TextDirection textDirection = testLocales[locale]['textDirection'] as TextDirection; final TextDirection textDirection = testLocales[locale]['textDirection'] as TextDirection;
final DateTime baseDate = DateTime(2017, 9, 27); final DateTime baseDate = DateTime(2017, 9, 27);
await _pumpBoilerplate(tester, DayPicker( await _pumpBoilerplate(tester, CalendarDatePicker(
selectedDate: baseDate, initialDate: baseDate,
currentDate: baseDate,
onChanged: (DateTime newValue) { },
firstDate: baseDate.subtract(const Duration(days: 90)), firstDate: baseDate.subtract(const Duration(days: 90)),
lastDate: baseDate.add(const Duration(days: 90)), lastDate: baseDate.add(const Duration(days: 90)),
displayedMonth: baseDate, onDateChanged: (DateTime newValue) {},
), locale: locale, textDirection: textDirection); ), locale: locale, textDirection: textDirection);
expect(find.text(expectedMonthYearHeader), findsOneWidget); expect(find.text(expectedMonthYearHeader), findsOneWidget);
...@@ -126,14 +124,14 @@ void main() { ...@@ -126,14 +124,14 @@ void main() {
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
final Element dayPicker = tester.element(find.byType(DayPicker)); final Element picker = tester.element(find.byType(CalendarDatePicker));
expect( expect(
Localizations.localeOf(dayPicker), Localizations.localeOf(picker),
const Locale('fr', 'CA'), const Locale('fr', 'CA'),
); );
expect( expect(
Directionality.of(dayPicker), Directionality.of(picker),
TextDirection.ltr, TextDirection.ltr,
); );
...@@ -169,9 +167,9 @@ void main() { ...@@ -169,9 +167,9 @@ void main() {
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
final Element dayPicker = tester.element(find.byType(DayPicker)); final Element picker = tester.element(find.byType(CalendarDatePicker));
expect( expect(
Directionality.of(dayPicker), Directionality.of(picker),
TextDirection.rtl, TextDirection.rtl,
); );
...@@ -210,14 +208,14 @@ void main() { ...@@ -210,14 +208,14 @@ void main() {
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
final Element dayPicker = tester.element(find.byType(DayPicker)); final Element picker = tester.element(find.byType(CalendarDatePicker));
expect( expect(
Localizations.localeOf(dayPicker), Localizations.localeOf(picker),
const Locale('fr', 'CA'), const Locale('fr', 'CA'),
); );
expect( expect(
Directionality.of(dayPicker), Directionality.of(picker),
TextDirection.rtl, TextDirection.rtl,
); );
...@@ -292,12 +290,16 @@ Future<void> _pumpBoilerplate( ...@@ -292,12 +290,16 @@ Future<void> _pumpBoilerplate(
Locale locale = const Locale('en', 'US'), Locale locale = const Locale('en', 'US'),
TextDirection textDirection = TextDirection.ltr, TextDirection textDirection = TextDirection.ltr,
}) async { }) async {
await tester.pumpWidget(Directionality( await tester.pumpWidget(MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Localizations( child: Localizations(
locale: locale, locale: locale,
delegates: GlobalMaterialLocalizations.delegates, delegates: GlobalMaterialLocalizations.delegates,
child: Material(
child: child, child: child,
), ),
),
),
)); ));
} }
...@@ -15,9 +15,12 @@ class FooMaterialLocalizations extends MaterialLocalizationEn { ...@@ -15,9 +15,12 @@ class FooMaterialLocalizations extends MaterialLocalizationEn {
) : super( ) : super(
localeName: localeName.toString(), localeName: localeName.toString(),
fullYearFormat: intl.DateFormat.y(), fullYearFormat: intl.DateFormat.y(),
compactDateFormat: intl.DateFormat.yMd(),
shortDateFormat: intl.DateFormat.yMMMd(),
mediumDateFormat: intl.DateFormat('E, MMM\u00a0d'), mediumDateFormat: intl.DateFormat('E, MMM\u00a0d'),
longDateFormat: intl.DateFormat.yMMMMEEEEd(), longDateFormat: intl.DateFormat.yMMMMEEEEd(),
yearMonthFormat: intl.DateFormat.yMMMM(), yearMonthFormat: intl.DateFormat.yMMMM(),
shortMonthDayFormat: intl.DateFormat.MMMd(),
decimalFormat: intl.NumberFormat.decimalPattern(), decimalFormat: intl.NumberFormat.decimalPattern(),
twoDigitZeroPaddedFormat: intl.NumberFormat('00'), twoDigitZeroPaddedFormat: intl.NumberFormat('00'),
); );
......
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