Date picker i18n (#12324)
* formatYear * localize date picker * tests * clean-ups * address comments
This commit is contained in:
parent
b6185b6668
commit
150c58303e
@ -9,8 +9,6 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:intl/date_symbols.dart' as intl show DateSymbols;
|
|
||||||
import 'package:intl/intl.dart' as intl show DateFormat;
|
|
||||||
|
|
||||||
import 'button.dart';
|
import 'button.dart';
|
||||||
import 'button_bar.dart';
|
import 'button_bar.dart';
|
||||||
@ -83,6 +81,7 @@ class _DatePickerHeader extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||||
final ThemeData themeData = Theme.of(context);
|
final ThemeData themeData = Theme.of(context);
|
||||||
final TextTheme headerTextTheme = themeData.primaryTextTheme;
|
final TextTheme headerTextTheme = themeData.primaryTextTheme;
|
||||||
Color dayColor;
|
Color dayColor;
|
||||||
@ -130,12 +129,12 @@ class _DatePickerHeader extends StatelessWidget {
|
|||||||
Widget yearButton = new _DateHeaderButton(
|
Widget yearButton = new _DateHeaderButton(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.year), context),
|
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.year), context),
|
||||||
child: new Text(new intl.DateFormat('yyyy').format(selectedDate), style: yearStyle),
|
child: new Text(localizations.formatYear(selectedDate), style: yearStyle),
|
||||||
);
|
);
|
||||||
Widget dayButton = new _DateHeaderButton(
|
Widget dayButton = new _DateHeaderButton(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.day), context),
|
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.day), context),
|
||||||
child: new Text(new intl.DateFormat('E, MMM\u00a0d').format(selectedDate), style: dayStyle),
|
child: new Text(localizations.formatMediumDate(selectedDate), style: dayStyle),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Disable the button for the current mode.
|
// Disable the button for the current mode.
|
||||||
@ -275,12 +274,33 @@ class DayPicker extends StatelessWidget {
|
|||||||
/// Optional user supplied predicate function to customize selectable days.
|
/// Optional user supplied predicate function to customize selectable days.
|
||||||
final SelectableDayPredicate selectableDayPredicate;
|
final SelectableDayPredicate selectableDayPredicate;
|
||||||
|
|
||||||
List<Widget> _getDayHeaders(TextStyle headerStyle) {
|
/// Builds widgets showing abbreviated days of week. The first widget in the
|
||||||
final intl.DateFormat dateFormat = new intl.DateFormat();
|
/// returned list corresponds to the first day of week for the current locale.
|
||||||
final intl.DateSymbols symbols = dateFormat.dateSymbols;
|
///
|
||||||
return symbols.NARROWWEEKDAYS.map((String weekDay) {
|
/// Examples:
|
||||||
return new Center(child: new Text(weekDay, style: headerStyle));
|
///
|
||||||
}).toList(growable: false);
|
/// ```
|
||||||
|
/// ┌ 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(new Center(child: new Text(weekDay, style: headerStyle)));
|
||||||
|
if (i == (localizations.firstDayOfWeekIndex - 1) % 7)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not use this directly - call getDaysInMonth instead.
|
// Do not use this directly - call getDaysInMonth instead.
|
||||||
@ -301,18 +321,64 @@ class DayPicker extends StatelessWidget {
|
|||||||
return _kDaysInMonth[month - 1];
|
return _kDaysInMonth[month - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Computes the offset from the first day of 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 _computeFirstDayOffset(int year, int month, MaterialLocalizations localizations) {
|
||||||
|
// 0-based day of week, with 0 representing Monday.
|
||||||
|
final int weekDayFromMonday = new DateTime(year, month).weekday - 1;
|
||||||
|
// 0-based day of week, with 0 representing Sunday.
|
||||||
|
final int firstDayOfWeekFromSunday = localizations.firstDayOfWeekIndex;
|
||||||
|
// firstDayOfWeekFromSunday recomputed to be Monday-based
|
||||||
|
final int firstDayOfWeekFromMonday = (firstDayOfWeekFromSunday - 1) % 7;
|
||||||
|
// Number of days between the first day of week appearing on the calendar,
|
||||||
|
// and the day corresponding to the 1-st of the month.
|
||||||
|
return (weekDayFromMonday - firstDayOfWeekFromMonday) % 7;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ThemeData themeData = Theme.of(context);
|
final ThemeData themeData = Theme.of(context);
|
||||||
|
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||||
final int year = displayedMonth.year;
|
final int year = displayedMonth.year;
|
||||||
final int month = displayedMonth.month;
|
final int month = displayedMonth.month;
|
||||||
final int daysInMonth = getDaysInMonth(year, month);
|
final int daysInMonth = getDaysInMonth(year, month);
|
||||||
// This assumes a start day of SUNDAY, but could be changed.
|
final int firstDayOffset = _computeFirstDayOffset(year, month, localizations);
|
||||||
final int firstWeekday = new DateTime(year, month).weekday % 7;
|
|
||||||
final List<Widget> labels = <Widget>[];
|
final List<Widget> labels = <Widget>[];
|
||||||
labels.addAll(_getDayHeaders(themeData.textTheme.caption));
|
labels.addAll(_getDayHeaders(themeData.textTheme.caption, localizations));
|
||||||
for (int i = 0; true; ++i) {
|
for (int i = 0; true; i += 1) {
|
||||||
final int day = i - firstWeekday + 1;
|
// 1-based day of month, e.g. 1-31 for January, and 1-29 for February on
|
||||||
|
// a leap year.
|
||||||
|
final int day = i - firstDayOffset + 1;
|
||||||
if (day > daysInMonth)
|
if (day > daysInMonth)
|
||||||
break;
|
break;
|
||||||
if (day < 1) {
|
if (day < 1) {
|
||||||
@ -370,7 +436,7 @@ class DayPicker extends StatelessWidget {
|
|||||||
child: new Center(
|
child: new Center(
|
||||||
child: new GestureDetector(
|
child: new GestureDetector(
|
||||||
onTap: onMonthHeaderTap != null ? Feedback.wrapForTap(onMonthHeaderTap, context) : null,
|
onTap: onMonthHeaderTap != null ? Feedback.wrapForTap(onMonthHeaderTap, context) : null,
|
||||||
child: new Text(new intl.DateFormat('yMMMM').format(displayedMonth),
|
child: new Text(localizations.formatMonthYear(displayedMonth),
|
||||||
style: themeData.textTheme.subhead,
|
style: themeData.textTheme.subhead,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -558,6 +624,7 @@ class _MonthPickerState extends State<MonthPicker> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final TextDirection textDirection = Directionality.of(context);
|
final TextDirection textDirection = Directionality.of(context);
|
||||||
|
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||||
return new SizedBox(
|
return new SizedBox(
|
||||||
width: _kMonthPickerPortraitWidth,
|
width: _kMonthPickerPortraitWidth,
|
||||||
height: _kMaxDayPickerHeight,
|
height: _kMaxDayPickerHeight,
|
||||||
@ -576,7 +643,7 @@ class _MonthPickerState extends State<MonthPicker> {
|
|||||||
start: 8.0,
|
start: 8.0,
|
||||||
child: new IconButton(
|
child: new IconButton(
|
||||||
icon: _getPreviousMonthIcon(textDirection),
|
icon: _getPreviousMonthIcon(textDirection),
|
||||||
tooltip: MaterialLocalizations.of(context).previousMonthTooltip,
|
tooltip: localizations.previousMonthTooltip,
|
||||||
onPressed: _isDisplayingFirstMonth ? null : _handlePreviousMonth,
|
onPressed: _isDisplayingFirstMonth ? null : _handlePreviousMonth,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -585,7 +652,7 @@ class _MonthPickerState extends State<MonthPicker> {
|
|||||||
end: 8.0,
|
end: 8.0,
|
||||||
child: new IconButton(
|
child: new IconButton(
|
||||||
icon: _getNextMonthIcon(textDirection),
|
icon: _getNextMonthIcon(textDirection),
|
||||||
tooltip: MaterialLocalizations.of(context).nextMonthTooltip,
|
tooltip: localizations.nextMonthTooltip,
|
||||||
onPressed: _isDisplayingLastMonth ? null : _handleNextMonth,
|
onPressed: _isDisplayingLastMonth ? null : _handleNextMonth,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -882,6 +949,14 @@ typedef bool SelectableDayPredicate(DateTime day);
|
|||||||
/// date picker initially in the year or month+day picker mode. It defaults
|
/// date picker initially in the year or month+day picker mode. It defaults
|
||||||
/// to month+day, and must not be null.
|
/// 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].
|
||||||
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [showTimePicker]
|
/// * [showTimePicker]
|
||||||
@ -893,6 +968,8 @@ Future<DateTime> showDatePicker({
|
|||||||
@required DateTime lastDate,
|
@required DateTime lastDate,
|
||||||
SelectableDayPredicate selectableDayPredicate,
|
SelectableDayPredicate selectableDayPredicate,
|
||||||
DatePickerMode initialDatePickerMode: DatePickerMode.day,
|
DatePickerMode initialDatePickerMode: DatePickerMode.day,
|
||||||
|
Locale locale,
|
||||||
|
TextDirection textDirection,
|
||||||
}) async {
|
}) async {
|
||||||
assert(!initialDate.isBefore(firstDate), 'initialDate must be on or after firstDate');
|
assert(!initialDate.isBefore(firstDate), 'initialDate must be on or after firstDate');
|
||||||
assert(!initialDate.isAfter(lastDate), 'initialDate must be on or before lastDate');
|
assert(!initialDate.isAfter(lastDate), 'initialDate must be on or before lastDate');
|
||||||
@ -902,14 +979,32 @@ Future<DateTime> showDatePicker({
|
|||||||
'Provided initialDate must satisfy provided selectableDayPredicate'
|
'Provided initialDate must satisfy provided selectableDayPredicate'
|
||||||
);
|
);
|
||||||
assert(initialDatePickerMode != null, 'initialDatePickerMode must not be null');
|
assert(initialDatePickerMode != null, 'initialDatePickerMode must not be null');
|
||||||
return await showDialog(
|
|
||||||
|
Widget child = new _DatePickerDialog(
|
||||||
|
initialDate: initialDate,
|
||||||
|
firstDate: firstDate,
|
||||||
|
lastDate: lastDate,
|
||||||
|
selectableDayPredicate: selectableDayPredicate,
|
||||||
|
initialDatePickerMode: initialDatePickerMode,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (textDirection != null) {
|
||||||
|
child = new Directionality(
|
||||||
|
textDirection: textDirection,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locale != null) {
|
||||||
|
child = new Localizations.override(
|
||||||
|
context: context,
|
||||||
|
locale: locale,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await showDialog<DateTime>(
|
||||||
context: context,
|
context: context,
|
||||||
child: new _DatePickerDialog(
|
child: child,
|
||||||
initialDate: initialDate,
|
|
||||||
firstDate: firstDate,
|
|
||||||
lastDate: lastDate,
|
|
||||||
selectableDayPredicate: selectableDayPredicate,
|
|
||||||
initialDatePickerMode: initialDatePickerMode,
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ import 'dart:async';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:intl/intl.dart' as intl;
|
import 'package:intl/intl.dart' as intl;
|
||||||
|
import 'package:intl/date_symbols.dart' as intl;
|
||||||
|
import 'package:intl/date_symbol_data_local.dart' as intl_local_date_data;
|
||||||
|
|
||||||
import 'i18n/localizations.dart';
|
import 'i18n/localizations.dart';
|
||||||
import 'time.dart';
|
import 'time.dart';
|
||||||
@ -121,6 +123,52 @@ abstract class MaterialLocalizations {
|
|||||||
/// Formats [timeOfDay] according to the value of [timeOfDayFormat].
|
/// Formats [timeOfDay] according to the value of [timeOfDayFormat].
|
||||||
String formatTimeOfDay(TimeOfDay timeOfDay);
|
String formatTimeOfDay(TimeOfDay timeOfDay);
|
||||||
|
|
||||||
|
/// Full unabbreviated year format, e.g. 2017 rather than 17.
|
||||||
|
String formatYear(DateTime date);
|
||||||
|
|
||||||
|
/// Formats the date using a medium-width format.
|
||||||
|
///
|
||||||
|
/// Abbreviates month and days of week. This appears in the header of the date
|
||||||
|
/// picker invoked using [showDatePicker].
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
///
|
||||||
|
/// - US English: Wed, Sep 27
|
||||||
|
/// - Russian: ср, сент. 27
|
||||||
|
String formatMediumDate(DateTime date);
|
||||||
|
|
||||||
|
/// Formats the month and the year of the given [date].
|
||||||
|
///
|
||||||
|
/// The returned string does not contain the day of the month. This appears
|
||||||
|
/// in the date picker invoked using [showDatePicker].
|
||||||
|
String formatMonthYear(DateTime date);
|
||||||
|
|
||||||
|
/// List of week day names in narrow format, usually 1- or 2-letter
|
||||||
|
/// abbreviations of full names.
|
||||||
|
///
|
||||||
|
/// The list begins with the value corresponding to Sunday and ends with
|
||||||
|
/// Saturday. Use [firstDayOfWeekIndex] to find the first day of week in this
|
||||||
|
/// list.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
///
|
||||||
|
/// - US English: S, M, T, W, T, F, S
|
||||||
|
/// - Russian: вс, пн, вт, ср, чт, пт, сб - notice that the list begins with
|
||||||
|
/// вс (Sunday) even though the first day of week for Russian is Monday.
|
||||||
|
List<String> get narrowWeekDays;
|
||||||
|
|
||||||
|
/// Index of the first day of week, where 0 points to Sunday, and 6 points to
|
||||||
|
/// Saturday.
|
||||||
|
///
|
||||||
|
/// This getter is compatible with [narrowWeekDays]. For example:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// var localizations = MaterialLocalizations.of(context);
|
||||||
|
/// // The name of the first day of week for the current locale.
|
||||||
|
/// var firstDayOfWeek = localizations.narrowWeekDays[localizations.firstDayOfWeekIndex];
|
||||||
|
/// ```
|
||||||
|
int get firstDayOfWeekIndex;
|
||||||
|
|
||||||
/// The `MaterialLocalizations` from the closest [Localizations] instance
|
/// The `MaterialLocalizations` from the closest [Localizations] instance
|
||||||
/// that encloses the given context.
|
/// that encloses the given context.
|
||||||
///
|
///
|
||||||
@ -146,13 +194,30 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
|
|||||||
/// [LocalizationsDelegate] implementations typically call the static [load]
|
/// [LocalizationsDelegate] implementations typically call the static [load]
|
||||||
/// function, rather than constructing this class directly.
|
/// function, rather than constructing this class directly.
|
||||||
DefaultMaterialLocalizations(this.locale)
|
DefaultMaterialLocalizations(this.locale)
|
||||||
: this._localeName = _computeLocaleName(locale) {
|
: assert(locale != null),
|
||||||
assert(locale != null);
|
this._localeName = _computeLocaleName(locale) {
|
||||||
|
_loadDateIntlDataIfNotLoaded();
|
||||||
|
|
||||||
if (localizations.containsKey(locale.languageCode))
|
if (localizations.containsKey(locale.languageCode))
|
||||||
_nameToValue.addAll(localizations[locale.languageCode]);
|
_nameToValue.addAll(localizations[locale.languageCode]);
|
||||||
if (localizations.containsKey(_localeName))
|
if (localizations.containsKey(_localeName))
|
||||||
_nameToValue.addAll(localizations[_localeName]);
|
_nameToValue.addAll(localizations[_localeName]);
|
||||||
|
|
||||||
|
const String kMediumDatePattern = 'E, MMM\u00a0d';
|
||||||
|
if (intl.DateFormat.localeExists(_localeName)) {
|
||||||
|
_fullYearFormat = new intl.DateFormat.y(_localeName);
|
||||||
|
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern, _localeName);
|
||||||
|
_yearMonthFormat = new intl.DateFormat('yMMMM', _localeName);
|
||||||
|
} else if (intl.DateFormat.localeExists(locale.languageCode)) {
|
||||||
|
_fullYearFormat = new intl.DateFormat.y(locale.languageCode);
|
||||||
|
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern, locale.languageCode);
|
||||||
|
_yearMonthFormat = new intl.DateFormat('yMMMM', locale.languageCode);
|
||||||
|
} else {
|
||||||
|
_fullYearFormat = new intl.DateFormat.y();
|
||||||
|
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern);
|
||||||
|
_yearMonthFormat = new intl.DateFormat('yMMMM');
|
||||||
|
}
|
||||||
|
|
||||||
if (intl.NumberFormat.localeExists(_localeName)) {
|
if (intl.NumberFormat.localeExists(_localeName)) {
|
||||||
_decimalFormat = new intl.NumberFormat.decimalPattern(_localeName);
|
_decimalFormat = new intl.NumberFormat.decimalPattern(_localeName);
|
||||||
_twoDigitZeroPaddedFormat = new intl.NumberFormat('00', _localeName);
|
_twoDigitZeroPaddedFormat = new intl.NumberFormat('00', _localeName);
|
||||||
@ -183,6 +248,13 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
|
|||||||
/// If the number is less than 10, zero-pads it.
|
/// If the number is less than 10, zero-pads it.
|
||||||
intl.NumberFormat _twoDigitZeroPaddedFormat;
|
intl.NumberFormat _twoDigitZeroPaddedFormat;
|
||||||
|
|
||||||
|
/// Full unabbreviated year format, e.g. 2017 rather than 17.
|
||||||
|
intl.DateFormat _fullYearFormat;
|
||||||
|
|
||||||
|
intl.DateFormat _mediumDateFormat;
|
||||||
|
|
||||||
|
intl.DateFormat _yearMonthFormat;
|
||||||
|
|
||||||
static String _computeLocaleName(Locale locale) {
|
static String _computeLocaleName(Locale locale) {
|
||||||
final String localeName = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
|
final String localeName = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
|
||||||
return intl.Intl.canonicalizedLocale(localeName);
|
return intl.Intl.canonicalizedLocale(localeName);
|
||||||
@ -225,6 +297,29 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
|
|||||||
return _twoDigitZeroPaddedFormat.format(timeOfDay.minute);
|
return _twoDigitZeroPaddedFormat.format(timeOfDay.minute);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String formatYear(DateTime date) {
|
||||||
|
return _fullYearFormat.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String formatMediumDate(DateTime date) {
|
||||||
|
return _mediumDateFormat.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String formatMonthYear(DateTime date) {
|
||||||
|
return _yearMonthFormat.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get narrowWeekDays {
|
||||||
|
return _fullYearFormat.dateSymbols.NARROWWEEKDAYS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get firstDayOfWeekIndex => (_fullYearFormat.dateSymbols.FIRSTDAYOFWEEK + 1) % 7;
|
||||||
|
|
||||||
/// Formats a [number] using local decimal number format.
|
/// Formats a [number] using local decimal number format.
|
||||||
///
|
///
|
||||||
/// Inserts locale-appropriate thousands separator, if necessary.
|
/// Inserts locale-appropriate thousands separator, if necessary.
|
||||||
@ -415,3 +510,20 @@ const Map<String, TimeOfDayFormat> _icuTimeOfDayToEnum = const <String, TimeOfDa
|
|||||||
'a h:mm': TimeOfDayFormat.a_space_h_colon_mm,
|
'a h:mm': TimeOfDayFormat.a_space_h_colon_mm,
|
||||||
'ah:mm': TimeOfDayFormat.a_space_h_colon_mm,
|
'ah:mm': TimeOfDayFormat.a_space_h_colon_mm,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Tracks if date i18n data has been loaded.
|
||||||
|
bool _dateIntlDataInitialized = false;
|
||||||
|
|
||||||
|
/// Loads i18n data for dates if it hasn't be loaded yet.
|
||||||
|
///
|
||||||
|
/// Only the first invocation of this function has the effect of loading the
|
||||||
|
/// data. Subsequent invocations have no effect.
|
||||||
|
void _loadDateIntlDataIfNotLoaded() {
|
||||||
|
if (!_dateIntlDataInitialized) {
|
||||||
|
// The returned Future is intentionally dropped on the floor. The
|
||||||
|
// function only returns it to be compatible with the async counterparts.
|
||||||
|
// The Future has no value otherwise.
|
||||||
|
intl_local_date_data.initializeDateFormatting();
|
||||||
|
_dateIntlDataInitialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'feedback_tester.dart';
|
import 'feedback_tester.dart';
|
||||||
|
|
||||||
@ -115,6 +116,38 @@ void main() {
|
|||||||
await tester.pump(const Duration(seconds: 5));
|
await tester.pump(const Duration(seconds: 5));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('MonthPicker receives header taps', (WidgetTester tester) async {
|
||||||
|
DateTime currentValue;
|
||||||
|
bool headerTapped = false;
|
||||||
|
|
||||||
|
final Widget widget = new MaterialApp(
|
||||||
|
home: new Material(
|
||||||
|
child: new ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
new MonthPicker(
|
||||||
|
selectedDate: new DateTime.utc(2015, 6, 9, 7, 12),
|
||||||
|
firstDate: new DateTime.utc(2013),
|
||||||
|
lastDate: new DateTime.utc(2018),
|
||||||
|
onChanged: (DateTime dateTime) {
|
||||||
|
currentValue = dateTime;
|
||||||
|
},
|
||||||
|
onMonthHeaderTap: () {
|
||||||
|
headerTapped = true;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(widget);
|
||||||
|
|
||||||
|
expect(currentValue, isNull);
|
||||||
|
expect(headerTapped, false);
|
||||||
|
await tester.tap(find.text('June 2015'));
|
||||||
|
expect(headerTapped, true);
|
||||||
|
});
|
||||||
|
|
||||||
Future<Null> preparePicker(WidgetTester tester, Future<Null> callback(Future<DateTime> date)) async {
|
Future<Null> preparePicker(WidgetTester tester, Future<Null> callback(Future<DateTime> date)) async {
|
||||||
BuildContext buttonContext;
|
BuildContext buttonContext;
|
||||||
await tester.pumpWidget(new MaterialApp(
|
await tester.pumpWidget(new MaterialApp(
|
||||||
@ -207,7 +240,10 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.tap(find.text('2017'));
|
await tester.tap(find.text('2017'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
final String dayLabel = new DateFormat('E, MMM\u00a0d').format(new DateTime(2017, DateTime.JANUARY, 15));
|
final MaterialLocalizations localizations = MaterialLocalizations.of(
|
||||||
|
tester.element(find.byType(DayPicker))
|
||||||
|
);
|
||||||
|
final String dayLabel = localizations.formatMediumDate(new DateTime(2017, DateTime.JANUARY, 15));
|
||||||
await tester.tap(find.text(dayLabel));
|
await tester.tap(find.text(dayLabel));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.tap(find.text('19'));
|
await tester.tap(find.text('19'));
|
||||||
@ -383,4 +419,250 @@ void main() {
|
|||||||
expect(await date, isNull);
|
expect(await date, isNull);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group(DayPicker, () {
|
||||||
|
final Map<Locale, Map<String, dynamic>> testLocales = <Locale, Map<String, dynamic>>{
|
||||||
|
// Tests the default.
|
||||||
|
const Locale('en', 'US'): <String, dynamic>{
|
||||||
|
'textDirection': TextDirection.ltr,
|
||||||
|
'expectedDaysOfWeek': <String>['S', 'M', 'T', 'W', 'T', 'F', 'S'],
|
||||||
|
'expectedDaysOfMonth': new List<String>.generate(30, (int i) => '${i + 1}'),
|
||||||
|
'expectedMonthYearHeader': 'September 2017',
|
||||||
|
},
|
||||||
|
// Tests a different first day of week.
|
||||||
|
const Locale('ru', 'RU'): <String, dynamic>{
|
||||||
|
'textDirection': TextDirection.ltr,
|
||||||
|
'expectedDaysOfWeek': <String>['пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'вс'],
|
||||||
|
'expectedDaysOfMonth': new List<String>.generate(30, (int i) => '${i + 1}'),
|
||||||
|
'expectedMonthYearHeader': 'сентябрь 2017 г.',
|
||||||
|
},
|
||||||
|
// Tests RTL.
|
||||||
|
// TODO: change to Arabic numerals when these are fixed:
|
||||||
|
// TODO: https://github.com/dart-lang/intl/issues/143
|
||||||
|
// TODO: https://github.com/flutter/flutter/issues/12289
|
||||||
|
const Locale('ar', 'AR'): <String, dynamic>{
|
||||||
|
'textDirection': TextDirection.rtl,
|
||||||
|
'expectedDaysOfWeek': <String>['ح', 'ن', 'ث', 'ر', 'خ', 'ج', 'س'],
|
||||||
|
'expectedDaysOfMonth': new List<String>.generate(30, (int i) => '${i + 1}'),
|
||||||
|
'expectedMonthYearHeader': 'سبتمبر 2017',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (Locale locale in testLocales.keys) {
|
||||||
|
testWidgets('shows dates for $locale', (WidgetTester tester) async {
|
||||||
|
final List<String> expectedDaysOfWeek = testLocales[locale]['expectedDaysOfWeek'];
|
||||||
|
final List<String> expectedDaysOfMonth = testLocales[locale]['expectedDaysOfMonth'];
|
||||||
|
final String expectedMonthYearHeader = testLocales[locale]['expectedMonthYearHeader'];
|
||||||
|
final TextDirection textDirection = testLocales[locale]['textDirection'];
|
||||||
|
final DateTime baseDate = new DateTime(2017, 9, 27);
|
||||||
|
|
||||||
|
await _pumpBoilerplate(tester, new DayPicker(
|
||||||
|
selectedDate: baseDate,
|
||||||
|
currentDate: baseDate,
|
||||||
|
onChanged: (DateTime newValue) {},
|
||||||
|
firstDate: baseDate.subtract(const Duration(days: 90)),
|
||||||
|
lastDate: baseDate.add(const Duration(days: 90)),
|
||||||
|
displayedMonth: baseDate,
|
||||||
|
), locale: locale, textDirection: textDirection);
|
||||||
|
|
||||||
|
expect(find.text(expectedMonthYearHeader), findsOneWidget);
|
||||||
|
|
||||||
|
expectedDaysOfWeek.forEach((String dayOfWeek) {
|
||||||
|
expect(find.text(dayOfWeek), findsWidgets);
|
||||||
|
});
|
||||||
|
|
||||||
|
Offset previousCellOffset;
|
||||||
|
expectedDaysOfMonth.forEach((String dayOfMonth) {
|
||||||
|
final Finder dayCell = find.descendant(of: find.byType(GridView), matching: find.text(dayOfMonth));
|
||||||
|
expect(dayCell, findsOneWidget);
|
||||||
|
|
||||||
|
// Check that cells are correctly positioned relative to each other,
|
||||||
|
// taking text direction into account.
|
||||||
|
final Offset offset = tester.getCenter(dayCell);
|
||||||
|
if (previousCellOffset != null) {
|
||||||
|
if (textDirection == TextDirection.ltr) {
|
||||||
|
expect(offset.dx > previousCellOffset.dx && offset.dy == previousCellOffset.dy || offset.dy > previousCellOffset.dy, true);
|
||||||
|
} else {
|
||||||
|
expect(offset.dx < previousCellOffset.dx && offset.dy == previousCellOffset.dy || offset.dy > previousCellOffset.dy, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousCellOffset = offset;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('locale parameter overrides ambient locale', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(new MaterialApp(
|
||||||
|
locale: const Locale('en', 'US'),
|
||||||
|
supportedLocales: const <Locale>[
|
||||||
|
const Locale('en', 'US'),
|
||||||
|
const Locale('fr', 'CA'),
|
||||||
|
],
|
||||||
|
home: new Material(
|
||||||
|
child: new Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return new FlatButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: initialDate,
|
||||||
|
firstDate: firstDate,
|
||||||
|
lastDate: lastDate,
|
||||||
|
locale: const Locale('fr', 'CA'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('X'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.tap(find.text('X'));
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
final Element dayPicker = tester.element(find.byType(DayPicker));
|
||||||
|
expect(
|
||||||
|
Localizations.localeOf(dayPicker),
|
||||||
|
const Locale('fr', 'CA'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Directionality.of(dayPicker),
|
||||||
|
TextDirection.ltr,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('ANNULER'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('textDirection parameter overrides ambient textDirection', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(new MaterialApp(
|
||||||
|
locale: const Locale('en', 'US'),
|
||||||
|
supportedLocales: const <Locale>[
|
||||||
|
const Locale('en', 'US'),
|
||||||
|
],
|
||||||
|
home: new Material(
|
||||||
|
child: new Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return new FlatButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: initialDate,
|
||||||
|
firstDate: firstDate,
|
||||||
|
lastDate: lastDate,
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('X'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.tap(find.text('X'));
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
final Element dayPicker = tester.element(find.byType(DayPicker));
|
||||||
|
expect(
|
||||||
|
Directionality.of(dayPicker),
|
||||||
|
TextDirection.rtl,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('CANCEL'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('textDirection parameter takes precendence over locale parameter', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(new MaterialApp(
|
||||||
|
locale: const Locale('en', 'US'),
|
||||||
|
supportedLocales: const <Locale>[
|
||||||
|
const Locale('en', 'US'),
|
||||||
|
const Locale('fr', 'CA'),
|
||||||
|
],
|
||||||
|
home: new Material(
|
||||||
|
child: new Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return new FlatButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: initialDate,
|
||||||
|
firstDate: firstDate,
|
||||||
|
lastDate: lastDate,
|
||||||
|
locale: const Locale('fr', 'CA'),
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('X'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.tap(find.text('X'));
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
final Element dayPicker = tester.element(find.byType(DayPicker));
|
||||||
|
expect(
|
||||||
|
Localizations.localeOf(dayPicker),
|
||||||
|
const Locale('fr', 'CA'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Directionality.of(dayPicker),
|
||||||
|
TextDirection.rtl,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('ANNULER'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Null> _pumpBoilerplate(
|
||||||
|
WidgetTester tester,
|
||||||
|
Widget child, {
|
||||||
|
Locale locale = const Locale('en', 'US'),
|
||||||
|
TextDirection textDirection: TextDirection.ltr
|
||||||
|
}) async {
|
||||||
|
await tester.pumpWidget(new Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: new Localizations(
|
||||||
|
locale: locale,
|
||||||
|
delegates: <LocalizationsDelegate<dynamic>>[
|
||||||
|
new _MaterialLocalizationsDelegate(
|
||||||
|
new DefaultMaterialLocalizations(locale),
|
||||||
|
),
|
||||||
|
const DefaultWidgetsLocalizationsDelegate(),
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
|
||||||
|
const _MaterialLocalizationsDelegate(this.localizations);
|
||||||
|
|
||||||
|
final MaterialLocalizations localizations;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<MaterialLocalizations> load(Locale locale) {
|
||||||
|
return new SynchronousFuture<MaterialLocalizations>(localizations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldReload(_MaterialLocalizationsDelegate old) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultWidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
|
||||||
|
const DefaultWidgetsLocalizationsDelegate();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<WidgetsLocalizations> load(Locale locale) {
|
||||||
|
return new SynchronousFuture<WidgetsLocalizations>(new DefaultWidgetsLocalizations(locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldReload(DefaultWidgetsLocalizationsDelegate old) => false;
|
||||||
}
|
}
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
// Copyright 2015 The Chromium 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_test/flutter_test.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('Can select a day', (WidgetTester tester) async {
|
|
||||||
DateTime currentValue;
|
|
||||||
|
|
||||||
final Widget widget = new Directionality(
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
child: new Material(
|
|
||||||
child: new ListView(
|
|
||||||
children: <Widget>[
|
|
||||||
new MonthPicker(
|
|
||||||
selectedDate: new DateTime.utc(2015, 6, 9, 7, 12),
|
|
||||||
firstDate: new DateTime.utc(2013),
|
|
||||||
lastDate: new DateTime.utc(2018),
|
|
||||||
onChanged: (DateTime dateTime) {
|
|
||||||
currentValue = dateTime;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pumpWidget(widget);
|
|
||||||
|
|
||||||
expect(currentValue, isNull);
|
|
||||||
await tester.tap(find.text('2015'));
|
|
||||||
await tester.pumpWidget(widget);
|
|
||||||
await tester.tap(find.text('2014'));
|
|
||||||
await tester.pumpWidget(widget);
|
|
||||||
expect(currentValue, equals(new DateTime(2014, 6, 9)));
|
|
||||||
await tester.tap(find.text('30'));
|
|
||||||
expect(currentValue, equals(new DateTime(2013, 1, 30)));
|
|
||||||
}, skip: true);
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user