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) {
const MaterialLocalization${locale.camelCase()}({
String localeName = '$localeName',
@required intl.DateFormat fullYearFormat,
@required intl.DateFormat compactDateFormat,
@required intl.DateFormat shortDateFormat,
@required intl.DateFormat mediumDateFormat,
@required intl.DateFormat longDateFormat,
@required intl.DateFormat yearMonthFormat,
@required intl.DateFormat shortMonthDayFormat,
@required intl.NumberFormat decimalFormat,
@required intl.NumberFormat twoDigitZeroPaddedFormat,
}) : super(
localeName: localeName,
fullYearFormat: fullYearFormat,
compactDateFormat: compactDateFormat,
shortDateFormat: shortDateFormat,
mediumDateFormat: mediumDateFormat,
longDateFormat: longDateFormat,
yearMonthFormat: yearMonthFormat,
shortMonthDayFormat: shortMonthDayFormat,
decimalFormat: decimalFormat,
twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat,
);''';
......@@ -63,15 +69,18 @@ const String materialFactoryDeclaration = '''
GlobalMaterialLocalizations getMaterialTranslation(
Locale locale,
intl.DateFormat fullYearFormat,
intl.DateFormat compactDateFormat,
intl.DateFormat shortDateFormat,
intl.DateFormat mediumDateFormat,
intl.DateFormat longDateFormat,
intl.DateFormat yearMonthFormat,
intl.DateFormat shortMonthDayFormat,
intl.NumberFormat decimalFormat,
intl.NumberFormat twoDigitZeroPaddedFormat,
) {''';
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';
......
......@@ -46,7 +46,6 @@ export 'src/material/colors.dart';
export 'src/material/constants.dart';
export 'src/material/data_table.dart';
export 'src/material/data_table_source.dart';
export 'src/material/date_picker.dart';
export 'src/material/debug.dart';
export 'src/material/dialog.dart';
export 'src/material/dialog_theme.dart';
......@@ -86,6 +85,7 @@ export 'src/material/outline_button.dart';
export 'src/material/page.dart';
export 'src/material/page_transitions_theme.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_theme.dart';
export 'src/material/progress_indicator.dart';
......
......@@ -224,6 +224,29 @@ abstract class MaterialLocalizations {
/// Full unabbreviated year format, e.g. 2017 rather than 17.
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.
///
/// Abbreviates month and days of week. This appears in the header of the date
......@@ -252,6 +275,24 @@ abstract class MaterialLocalizations {
/// in the date picker invoked using [showDatePicker].
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
/// abbreviations of full names.
///
......@@ -437,6 +478,23 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
'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
String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat = false }) {
final TimeOfDayFormat format = timeOfDayFormat(alwaysUse24HourFormat: alwaysUse24HourFormat);
......@@ -470,6 +528,21 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
@override
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
String formatMediumDate(DateTime date) {
final String day = _shortWeekdays[date.weekday - DateTime.monday];
......@@ -490,6 +563,37 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
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
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 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'button_bar.dart';
import 'colors.dart';
import 'debug.dart';
import 'dialog.dart';
import 'feedback.dart';
import 'flat_button.dart';
import 'icon_button.dart';
import 'icons.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'text_theme.dart';
import 'theme.dart';
import '../debug.dart';
import '../icon_button.dart';
import '../icons.dart';
import '../ink_well.dart';
import '../material.dart';
import '../material_localizations.dart';
import '../theme.dart';
// Examples can assume:
// BuildContext context;
import 'date_picker_common.dart';
/// Initial display mode of the date picker dialog.
///
/// Date picker UI mode for either showing a list of available years or a
/// monthly calendar initially in the dialog shown by calling [showDatePicker].
///
/// 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,
// NOTE: this is the original implementation for the Material Date Picker.
// These classes are deprecated and the whole file can be removed after
// this has been on stable for long enough for people to migrate to the new
// CalendarDatePicker (if needed, as showDatePicker has already been migrated
// and it is what most apps would have used).
/// Show a date picker UI for choosing a year.
year,
}
// Examples can assume:
// BuildContext context;
const Duration _kMonthScrollDuration = Duration(milliseconds: 200);
const double _kDayPickerRowHeight = 42.0;
......@@ -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.
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 {
const _DayPickerGridDelegate();
......@@ -226,6 +74,11 @@ const _DayPickerGridDelegate _kDayPickerGridDelegate = _DayPickerGridDelegate();
/// date picker.
/// * [showTimePicker], which shows a dialog that contains a material design
/// time picker.
///
@Deprecated(
'Use CalendarDatePicker instead. '
'This feature was deprecated after v1.15.3.'
)
class DayPicker extends StatelessWidget {
/// Creates a day picker.
///
......@@ -503,6 +356,11 @@ class DayPicker extends StatelessWidget {
/// date picker.
/// * [showTimePicker], which shows a dialog that contains a material design
/// time picker.
///
@Deprecated(
'Use CalendarDatePicker instead. '
'This feature was deprecated after v1.15.3.'
)
class MonthPicker extends StatefulWidget {
/// Creates a month picker.
///
......@@ -546,6 +404,7 @@ class MonthPicker extends StatefulWidget {
_MonthPickerState createState() => _MonthPickerState();
}
// ignore: deprecated_member_use_from_same_package
class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStateMixin {
static final Animatable<double> _chevronOpacityTween = Tween<double>(begin: 1.0, end: 0.0)
.chain(CurveTween(curve: Curves.easeInOut));
......@@ -567,6 +426,7 @@ class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStat
}
@override
// ignore: deprecated_member_use_from_same_package
void didUpdateWidget(MonthPicker oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedDate != oldWidget.selectedDate) {
......@@ -617,6 +477,7 @@ class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStat
Widget _buildItems(BuildContext context, int index) {
final DateTime month = _addMonthsToMonthDate(widget.firstDate, index);
// ignore: deprecated_member_use_from_same_package
return DayPicker(
key: ValueKey<DateTime>(month),
selectedDate: widget.selectedDate,
......@@ -766,6 +627,11 @@ class _MonthPickerSortKey extends OrdinalSortKey {
/// date picker.
/// * [showTimePicker], which shows a dialog that contains a material design
/// time picker.
///
@Deprecated(
'Use CalendarDatePicker instead. '
'This feature was deprecated after v1.15.3.'
)
class YearPicker extends StatefulWidget {
/// Creates a year picker.
///
......@@ -807,6 +673,7 @@ class YearPicker extends StatefulWidget {
_YearPickerState createState() => _YearPickerState();
}
// ignore: deprecated_member_use_from_same_package
class _YearPickerState extends State<YearPicker> {
static const double _itemExtent = 50.0;
ScrollController scrollController;
......@@ -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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';
void main() {
group('showDatePicker', () {
_tests();
});
}
void _tests() {
DateTime firstDate;
DateTime lastDate;
DateTime initialDate;
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 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(() {
firstDate = DateTime(2001, DateTime.january, 1);
lastDate = DateTime(2031, DateTime.december, 31);
initialDate = DateTime(2016, DateTime.january, 15);
selectableDayPredicate = null;
initialDatePickerMode = null;
});
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)));
initialEntryMode = DatePickerEntryMode.calendar;
initialCalendarMode = DatePickerMode.day;
await tester.drag(find.byKey(_datePickerKey), const Offset(800.0, 0.0));
await tester.pumpAndSettle();
expect(_selectedDate, equals(DateTime(2016, DateTime.september, 25)));
await tester.tap(find.text('17'));
await tester.pumpAndSettle();
expect(_selectedDate, equals(DateTime(2016, DateTime.august, 17)));
});
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));
cancelText = null;
confirmText = null;
errorFormatText = null;
errorInvalidText = null;
fieldHintText = null;
fieldLabelText = null;
helpText = null;
});
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;
await tester.pumpWidget(MaterialApp(
home: Material(
......@@ -150,46 +70,164 @@ void _tests() {
await tester.tap(find.text('Go'));
expect(buttonContext, isNotNull);
final Future<DateTime> date = initialDatePickerMode == null
// Exercise the argument default for initialDatePickerMode.
?
showDatePicker(
context: buttonContext,
initialDate: initialDate,
firstDate: firstDate,
lastDate: lastDate,
selectableDayPredicate: selectableDayPredicate,
)
:
showDatePicker(
final Future<DateTime> date = showDatePicker(
context: buttonContext,
initialDate: initialDate,
firstDate: firstDate,
lastDate: lastDate,
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 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 {
await preparePicker(tester, (Future<DateTime> date) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
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 {
await preparePicker(tester, (Future<DateTime> date) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('CANCEL'));
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 {
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('OK'));
expect(await date, equals(DateTime(2016, DateTime.january, 12)));
......@@ -197,51 +235,67 @@ void _tests() {
});
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.pumpAndSettle(const Duration(seconds: 1));
await tester.tap(find.text('25'));
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 {
await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('2016'));
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('January 2016')); // Switch to year mode.
await tester.pump();
await tester.tap(find.text('2018'));
await tester.pump();
expect(find.text('January 2018'), findsOneWidget);
});
});
testWidgets('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'));
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 {
await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('2016'));
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('January 2016')); // Switch to year mode.
await tester.pump();
await tester.tap(find.text('2017'));
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('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 {
initialDate = DateTime(2000);
firstDate = DateTime(1900);
lastDate = DateTime(2100);
await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('2000'));
testWidgets('Current year is visible in year picker', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('January 2016')); // Switch to year mode.
await tester.pump();
expect(find.text('2000'), findsNWidgets(2));
expect(find.text('2016'), findsOneWidget);
});
});
......@@ -249,12 +303,14 @@ void _tests() {
initialDate = DateTime(2017, DateTime.january, 15);
firstDate = initialDate;
lastDate = initialDate;
await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('10')); // Earlier than firstDate. Should be ignored.
await tester.tap(find.text('20')); // Later than lastDate. Should be ignored.
await prepareDatePicker(tester, (Future<DateTime> date) async {
// Earlier than firstDate. 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'));
// We should still be on the initial date.
expect(await date, equals(initialDate));
expect(await date, initialDate);
});
});
......@@ -262,7 +318,7 @@ void _tests() {
initialDate = DateTime(2017, DateTime.january, 15);
firstDate = initialDate;
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.pumpAndSettle(const Duration(seconds: 1));
// Shouldn't be possible to keep going into March.
......@@ -274,7 +330,7 @@ void _tests() {
initialDate = DateTime(2017, DateTime.january, 15);
firstDate = DateTime(2016, DateTime.december, 10);
lastDate = initialDate;
await preparePicker(tester, (Future<DateTime> date) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(previousMonthIcon);
await tester.pumpAndSettle(const Duration(seconds: 1));
// Shouldn't be possible to keep going into November.
......@@ -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 {
// Regression test for https://github.com/flutter/flutter/issues/17309
initialDate = DateTime(2018, DateTime.may, 4);
firstDate = DateTime(2016, DateTime.june, 9);
lastDate = DateTime(2019, DateTime.january, 15);
await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('2018'));
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('May 2018'));
await tester.pumpAndSettle();
await tester.tap(find.text('2016'));
await tester.pumpAndSettle();
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
expect(await date, DateTime(2016, DateTime.june, 9));
// Month should be clamped to June as the range starts at June 2016
expect(find.text('June 2016'), findsOneWidget);
});
});
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);
firstDate = DateTime(2016, DateTime.june, 9);
lastDate = DateTime(2019, DateTime.january, 15);
await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('2018'));
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('May 2018'));
await tester.pumpAndSettle();
await tester.tap(find.text('2019'));
await tester.pumpAndSettle();
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
expect(await date, DateTime(2019, DateTime.january, 15));
// Month should be clamped to January as the range ends at January 2019
expect(find.text('January 2019'), findsOneWidget);
});
});
testWidgets('Only predicate days are selectable', (WidgetTester tester) async {
initialDate = DateTime(2017, DateTime.january, 16);
firstDate = DateTime(2017, DateTime.january, 10);
lastDate = DateTime(2017, DateTime.january, 20);
selectableDayPredicate = (DateTime day) => day.day.isEven;
await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('10')); // Even, works.
await prepareDatePicker(tester, (Future<DateTime> date) async {
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('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 {
initialDate = DateTime(2014, DateTime.january, 15);
initialDatePickerMode = DatePickerMode.year;
await preparePicker(tester, (Future<DateTime> date) async {
initialCalendarMode = DatePickerMode.year;
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.pump();
// 2018 wouldn't be available if the year picker wasn't showing.
// The initial current year is 2014.
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'));
expect(await date, equals(DateTime(2018, DateTime.january, 15)));
await tester.pumpAndSettle();
expect(find.text(errorInvalidText), findsOneWidget);
});
});
});
group('haptic feedback', () {
const Duration kHapticFeedbackInterval = Duration(milliseconds: 10);
group('Haptic feedback', () {
const Duration hapticFeedbackInterval = Duration(milliseconds: 10);
FeedbackTester feedback;
setUp(() {
......@@ -351,6 +537,7 @@ void _tests() {
initialDate = DateTime(2017, DateTime.january, 16);
firstDate = DateTime(2017, DateTime.january, 10);
lastDate = DateTime(2018, DateTime.january, 20);
initialCalendarMode = DatePickerMode.day;
selectableDayPredicate = (DateTime date) => date.day.isEven;
});
......@@ -358,464 +545,358 @@ void _tests() {
feedback?.dispose();
});
testWidgets('tap-select date vibrates', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async {
testWidgets('Selecting date vibrates', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('10'));
await tester.pump(kHapticFeedbackInterval);
await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 1);
await tester.tap(find.text('12'));
await tester.pump(kHapticFeedbackInterval);
await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 2);
await tester.tap(find.text('14'));
await tester.pump(kHapticFeedbackInterval);
await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 3);
});
});
testWidgets('tap-select unselectable date does not vibrate', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async {
testWidgets('Tapping unselectable date does not vibrate', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('11'));
await tester.pump(kHapticFeedbackInterval);
await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 0);
await tester.tap(find.text('13'));
await tester.pump(kHapticFeedbackInterval);
await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 0);
await tester.tap(find.text('15'));
await tester.pump(kHapticFeedbackInterval);
await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 0);
});
});
testWidgets('mode, year change vibrates', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('2017'));
await tester.pump(kHapticFeedbackInterval);
testWidgets('Changing modes and year vibrates', (WidgetTester tester) async {
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('January 2017'));
await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 1);
await tester.tap(find.text('2018'));
await tester.pump(kHapticFeedbackInterval);
await tester.pump(hapticFeedbackInterval);
expect(feedback.hapticCount, 2);
});
});
});
test('days in month', () {
expect(DayPicker.getDaysInMonth(2017, 10), 31);
expect(DayPicker.getDaysInMonth(2017, 6), 30);
expect(DayPicker.getDaysInMonth(2017, 2), 28);
expect(DayPicker.getDaysInMonth(2016, 2), 29);
expect(DayPicker.getDaysInMonth(2000, 2), 29);
expect(DayPicker.getDaysInMonth(1900, 2), 28);
});
group('Semantics', () {
testWidgets('calendar day mode', (WidgetTester tester) async {
final SemanticsHandle semantics = tester.ensureSemantics();
testWidgets('month header tap', (WidgetTester tester) async {
selectableDayPredicate = null;
await preparePicker(tester, (Future<DateTime> date) async {
// Switch into the year selector.
await tester.tap(find.text('January 2016'));
await tester.pump();
expect(find.text('2020'), isNotNull);
await prepareDatePicker(tester, (Future<DateTime> date) async {
// Header
expect(
tester.getSemantics(find.text('SELECT DATE')), matchesSemantics(
label: 'SELECT DATE\nFri, Jan 15',
));
await tester.tap(find.text('CANCEL'));
expect(await date, isNull);
});
});
// Input mode toggle button
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 {
final SemanticsTester semantics = SemanticsTester(tester);
await preparePicker(tester, (Future<DateTime> date) async {
final TestSemantics expected = TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
elevation: 24.0,
thickness: 0.0,
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
label: '2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isSelected,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Fri, Jan 15',
textDirection: TextDirection.ltr,
),
TestSemantics(
children: <TestSemantics>[
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],
// Year mode drop down button
expect(
tester.getSemantics(find.text('January 2016')), matchesSemantics(
label: 'Select year',
isButton: true,
));
// Prev/Next month buttons
expect(tester.getSemantics(previousMonthIcon), matchesSemantics(
label: 'Previous month December 2015',
isButton: true,
hasTapAction: true,
isEnabled: true,
hasEnabledState: true,
isFocusable: true,
));
expect(tester.getSemantics(nextMonthIcon), matchesSemantics(
label: 'Next month February 2016',
isButton: true,
hasTapAction: true,
isEnabled: true,
hasEnabledState: true,
isFocusable: true,
));
// Day grid
expect(tester.getSemantics(find.text('1')), matchesSemantics(
label: '1, Friday, January 1, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('2')), matchesSemantics(
label: '2, Saturday, January 2, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('3')), matchesSemantics(
label: '3, Sunday, January 3, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('4')), matchesSemantics(
label: '4, Monday, January 4, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('5')), matchesSemantics(
label: '5, Tuesday, January 5, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('6')), matchesSemantics(
label: '6, Wednesday, January 6, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('7')), matchesSemantics(
label: '7, Thursday, January 7, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('8')), matchesSemantics(
label: '8, Friday, January 8, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('9')), matchesSemantics(
label: '9, Saturday, January 9, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('10')), matchesSemantics(
label: '10, Sunday, January 10, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('11')), matchesSemantics(
label: '11, Monday, January 11, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('12')), matchesSemantics(
label: '12, Tuesday, January 12, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('13')), matchesSemantics(
label: '13, Wednesday, January 13, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('14')), matchesSemantics(
label: '14, Thursday, January 14, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isSelected],
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('15')), matchesSemantics(
label: '15, Friday, January 15, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
isSelected: true,
));
expect(tester.getSemantics(find.text('16')), matchesSemantics(
label: '16, Saturday, January 16, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('17')), matchesSemantics(
label: '17, Sunday, January 17, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('18')), matchesSemantics(
label: '18, Monday, January 18, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('19')), matchesSemantics(
label: '19, Tuesday, January 19, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('20')), matchesSemantics(
label: '20, Wednesday, January 20, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('21')), matchesSemantics(
label: '21, Thursday, January 21, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('22')), matchesSemantics(
label: '22, Friday, January 22, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('23')), matchesSemantics(
label: '23, Saturday, January 23, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('24')), matchesSemantics(
label: '24, Sunday, January 24, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('25')), matchesSemantics(
label: '25, Monday, January 25, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('26')), matchesSemantics(
label: '26, Tuesday, January 26, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('27')), matchesSemantics(
label: '27, Wednesday, January 27, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('28')), matchesSemantics(
label: '28, Thursday, January 28, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('29')), matchesSemantics(
label: '29, Friday, January 29, 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
hasTapAction: true,
));
expect(tester.getSemantics(find.text('30')), matchesSemantics(
label: '30, Saturday, January 30, 2016',
textDirection: TextDirection.ltr,
),
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,
),
],
),
],
);
hasTapAction: true,
));
expect(semantics, hasSemantics(
TestSemantics.root(children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[expected],
),
]),
ignoreId: true,
ignoreTransform: true,
ignoreRect: 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,
));
});
semantics.dispose();
});
testWidgets('chervons animate when scrolling month picker', (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(
child: Material(
child: MonthPicker(
firstDate: DateTime(0),
lastDate: DateTime(9999),
key: _datePickerKey,
selectedDate: _selectedDate,
onChanged: (DateTime value) {
setState(() {
_selectedDate = value;
});
},
),
),
),
);
},
),
),
);
testWidgets('calendar year mode', (WidgetTester tester) async {
final SemanticsHandle semantics = tester.ensureSemantics();
final Finder chevronFinder = find.byType(IconButton);
final List<RenderAnimatedOpacity> chevronRenderers = chevronFinder
.evaluate()
.map((Element element) => element.findAncestorRenderObjectOfType<RenderAnimatedOpacity>())
.toList();
// 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));
}
initialCalendarMode = DatePickerMode.year;
await prepareDatePicker(tester, (Future<DateTime> date) async {
// Header
expect(tester.getSemantics(find.text('SELECT DATE')), matchesSemantics(
label: 'SELECT DATE\nFri, Jan 15',
));
// Drag and hold the picker to test for the opacity change
final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
await gesture.moveBy(const Offset(50.0, 100.0));
await tester.pumpAndSettle();
for (final RenderAnimatedOpacity renderer in chevronRenderers) {
expect(renderer.opacity.value, equals(0.0));
expect(renderer.opacity.status, equals(AnimationStatus.completed));
}
// Input mode toggle button
expect(tester.getSemantics(switchToInputIcon), matchesSemantics(
label: 'Switch to input',
isButton: true,
hasTapAction: true,
isEnabled: true,
hasEnabledState: true,
isFocusable: true,
));
// Release the drag and test for the opacity to return to original value
await gesture.up();
await tester.pumpAndSettle();
for (final RenderAnimatedOpacity renderer in chevronRenderers) {
expect(renderer.opacity.value, equals(1.0));
expect(renderer.opacity.status, equals(AnimationStatus.dismissed));
// Year mode drop down button
expect(tester.getSemantics(find.text('January 2016')), matchesSemantics(
label: 'Select year',
isButton: true,
));
// 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 {
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,
);
},
);
},
);
},
),
),
),
);
}
semantics.dispose();
});
await tester.pumpWidget(buildFrame(TextDirection.ltr));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
final double ltrOkRight = tester.getBottomRight(find.text('OK')).dx;
testWidgets('input mode', (WidgetTester tester) async {
final SemanticsHandle semantics = tester.ensureSemantics();
await tester.tap(find.text('OK')); // dismiss the dialog
await tester.pumpAndSettle();
initialEntryMode = DatePickerEntryMode.input;
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));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
// Input mode toggle button
expect(tester.getSemantics(switchToCalendarIcon), matchesSemantics(
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
// to ensure the layout doesn't overflow and cause an exception to be thrown.
......@@ -834,26 +915,11 @@ void _tests() {
Future<void> _showPicker(WidgetTester tester, Size size, [double textScaleFactor = 1.0]) async {
tester.binding.window.physicalSizeTestValue = size;
tester.binding.window.devicePixelRatioTestValue = 1.0;
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (BuildContext context) {
return RaisedButton(
child: const Text('X'),
onPressed: () {
showDatePicker(
context: context,
initialDate: initialDate,
firstDate: firstDate,
lastDate: lastDate,
);
},
);
},
),
),
);
await tester.tap(find.text('X'));
tester.binding.window.clearPhysicalSizeTestValue();
tester.binding.window.clearDevicePixelRatioTestValue();
await prepareDatePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('OK'));
});
await tester.pumpAndSettle();
}
......@@ -897,84 +963,9 @@ void _tests() {
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;
@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 {
/// 1. The string that would be returned by [Intl.canonicalizedLocale] for
/// the locale.
/// 2. The [intl.DateFormat] for [formatYear].
/// 3. The [intl.DateFormat] for [formatMediumDate].
/// 4. The [intl.DateFormat] for [formatFullDate].
/// 5. The [intl.DateFormat] for [formatMonthYear].
/// 6. The [NumberFormat] for [formatDecimal] (also used by [formatHour] and
/// 3. The [int.DateFormat] for [formatShortDate].
/// 4. The [intl.DateFormat] for [formatMediumDate].
/// 5. The [intl.DateFormat] for [formatFullDate].
/// 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]).
/// 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
/// [formatMinute] and the minute part of [formatTimeOfDay].
///
......@@ -90,21 +92,30 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
const GlobalMaterialLocalizations({
@required String localeName,
@required intl.DateFormat fullYearFormat,
@required intl.DateFormat compactDateFormat,
@required intl.DateFormat shortDateFormat,
@required intl.DateFormat mediumDateFormat,
@required intl.DateFormat longDateFormat,
@required intl.DateFormat yearMonthFormat,
@required intl.DateFormat shortMonthDayFormat,
@required intl.NumberFormat decimalFormat,
@required intl.NumberFormat twoDigitZeroPaddedFormat,
}) : assert(localeName != null),
_localeName = localeName,
assert(fullYearFormat != null),
_fullYearFormat = fullYearFormat,
assert(compactDateFormat != null),
_compactDateFormat = compactDateFormat,
assert(shortDateFormat != null),
_shortDateFormat = shortDateFormat,
assert(mediumDateFormat != null),
_mediumDateFormat = mediumDateFormat,
assert(longDateFormat != null),
_longDateFormat = longDateFormat,
assert(yearMonthFormat != null),
_yearMonthFormat = yearMonthFormat,
assert(shortMonthDayFormat != null),
_shortMonthDayFormat = shortMonthDayFormat,
assert(decimalFormat != null),
_decimalFormat = decimalFormat,
assert(twoDigitZeroPaddedFormat != null),
......@@ -112,9 +123,12 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
final String _localeName;
final intl.DateFormat _fullYearFormat;
final intl.DateFormat _compactDateFormat;
final intl.DateFormat _shortDateFormat;
final intl.DateFormat _mediumDateFormat;
final intl.DateFormat _longDateFormat;
final intl.DateFormat _yearMonthFormat;
final intl.DateFormat _shortMonthDayFormat;
final intl.NumberFormat _decimalFormat;
final intl.NumberFormat _twoDigitZeroPaddedFormat;
......@@ -142,6 +156,16 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
return _fullYearFormat.format(date);
}
@override
String formatCompactDate(DateTime date) {
return _compactDateFormat.format(date);
}
@override
String formatShortDate(DateTime date) {
return _shortDateFormat.format(date);
}
@override
String formatMediumDate(DateTime date) {
return _mediumDateFormat.format(date);
......@@ -157,6 +181,20 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
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
List<String> get narrowWeekdays {
return _longDateFormat.dateSymbols.NARROWWEEKDAYS;
......@@ -576,24 +614,36 @@ class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocal
);
intl.DateFormat fullYearFormat;
intl.DateFormat compactDateFormat;
intl.DateFormat shortDateFormat;
intl.DateFormat mediumDateFormat;
intl.DateFormat longDateFormat;
intl.DateFormat yearMonthFormat;
intl.DateFormat shortMonthDayFormat;
if (intl.DateFormat.localeExists(localeName)) {
fullYearFormat = intl.DateFormat.y(localeName);
compactDateFormat = intl.DateFormat.yMd(localeName);
shortDateFormat = intl.DateFormat.yMMMd(localeName);
mediumDateFormat = intl.DateFormat.MMMEd(localeName);
longDateFormat = intl.DateFormat.yMMMMEEEEd(localeName);
yearMonthFormat = intl.DateFormat.yMMMM(localeName);
shortMonthDayFormat = intl.DateFormat.MMMd(localeName);
} else if (intl.DateFormat.localeExists(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);
longDateFormat = intl.DateFormat.yMMMMEEEEd(locale.languageCode);
yearMonthFormat = intl.DateFormat.yMMMM(locale.languageCode);
shortMonthDayFormat = intl.DateFormat.MMMd(locale.languageCode);
} else {
fullYearFormat = intl.DateFormat.y();
compactDateFormat = intl.DateFormat.yMd();
shortDateFormat = intl.DateFormat.yMMMd();
mediumDateFormat = intl.DateFormat.MMMEd();
longDateFormat = intl.DateFormat.yMMMMEEEEd();
yearMonthFormat = intl.DateFormat.yMMMM();
shortMonthDayFormat = intl.DateFormat.MMMd();
}
intl.NumberFormat decimalFormat;
......@@ -612,9 +662,12 @@ class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocal
return SynchronousFuture<MaterialLocalizations>(getMaterialTranslation(
locale,
fullYearFormat,
compactDateFormat,
shortDateFormat,
mediumDateFormat,
longDateFormat,
yearMonthFormat,
shortMonthDayFormat,
decimalFormat,
twoDigitZeroPaddedFormat,
));
......
......@@ -19,7 +19,7 @@ void main() {
initialDate = DateTime(2016, DateTime.january, 15);
});
group(DayPicker, () {
group(CalendarDatePicker, () {
final intl.NumberFormat arabicNumbers = intl.NumberFormat('0', 'ar');
final Map<Locale, Map<String, dynamic>> testLocales = <Locale, Map<String, dynamic>>{
// Tests the default.
......@@ -59,13 +59,11 @@ void main() {
final TextDirection textDirection = testLocales[locale]['textDirection'] as TextDirection;
final DateTime baseDate = DateTime(2017, 9, 27);
await _pumpBoilerplate(tester, DayPicker(
selectedDate: baseDate,
currentDate: baseDate,
onChanged: (DateTime newValue) { },
await _pumpBoilerplate(tester, CalendarDatePicker(
initialDate: baseDate,
firstDate: baseDate.subtract(const Duration(days: 90)),
lastDate: baseDate.add(const Duration(days: 90)),
displayedMonth: baseDate,
onDateChanged: (DateTime newValue) {},
), locale: locale, textDirection: textDirection);
expect(find.text(expectedMonthYearHeader), findsOneWidget);
......@@ -126,14 +124,14 @@ void main() {
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
final Element dayPicker = tester.element(find.byType(DayPicker));
final Element picker = tester.element(find.byType(CalendarDatePicker));
expect(
Localizations.localeOf(dayPicker),
Localizations.localeOf(picker),
const Locale('fr', 'CA'),
);
expect(
Directionality.of(dayPicker),
Directionality.of(picker),
TextDirection.ltr,
);
......@@ -169,9 +167,9 @@ void main() {
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
final Element dayPicker = tester.element(find.byType(DayPicker));
final Element picker = tester.element(find.byType(CalendarDatePicker));
expect(
Directionality.of(dayPicker),
Directionality.of(picker),
TextDirection.rtl,
);
......@@ -210,14 +208,14 @@ void main() {
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
final Element dayPicker = tester.element(find.byType(DayPicker));
final Element picker = tester.element(find.byType(CalendarDatePicker));
expect(
Localizations.localeOf(dayPicker),
Localizations.localeOf(picker),
const Locale('fr', 'CA'),
);
expect(
Directionality.of(dayPicker),
Directionality.of(picker),
TextDirection.rtl,
);
......@@ -292,12 +290,16 @@ Future<void> _pumpBoilerplate(
Locale locale = const Locale('en', 'US'),
TextDirection textDirection = TextDirection.ltr,
}) async {
await tester.pumpWidget(Directionality(
await tester.pumpWidget(MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Localizations(
locale: locale,
delegates: GlobalMaterialLocalizations.delegates,
child: Material(
child: child,
),
),
),
));
}
......@@ -15,9 +15,12 @@ class FooMaterialLocalizations extends MaterialLocalizationEn {
) : super(
localeName: localeName.toString(),
fullYearFormat: intl.DateFormat.y(),
compactDateFormat: intl.DateFormat.yMd(),
shortDateFormat: intl.DateFormat.yMMMd(),
mediumDateFormat: intl.DateFormat('E, MMM\u00a0d'),
longDateFormat: intl.DateFormat.yMMMMEEEEd(),
yearMonthFormat: intl.DateFormat.yMMMM(),
shortMonthDayFormat: intl.DateFormat.MMMd(),
decimalFormat: intl.NumberFormat.decimalPattern(),
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