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/services.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_bar.dart';
|
||||
@ -83,6 +81,7 @@ class _DatePickerHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
final TextTheme headerTextTheme = themeData.primaryTextTheme;
|
||||
Color dayColor;
|
||||
@ -130,12 +129,12 @@ class _DatePickerHeader extends StatelessWidget {
|
||||
Widget yearButton = new _DateHeaderButton(
|
||||
color: backgroundColor,
|
||||
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(
|
||||
color: backgroundColor,
|
||||
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.
|
||||
@ -275,12 +274,33 @@ class DayPicker extends StatelessWidget {
|
||||
/// Optional user supplied predicate function to customize selectable days.
|
||||
final SelectableDayPredicate selectableDayPredicate;
|
||||
|
||||
List<Widget> _getDayHeaders(TextStyle headerStyle) {
|
||||
final intl.DateFormat dateFormat = new intl.DateFormat();
|
||||
final intl.DateSymbols symbols = dateFormat.dateSymbols;
|
||||
return symbols.NARROWWEEKDAYS.map((String weekDay) {
|
||||
return new Center(child: new Text(weekDay, style: headerStyle));
|
||||
}).toList(growable: false);
|
||||
/// 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(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.
|
||||
@ -301,18 +321,64 @@ class DayPicker extends StatelessWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
final int year = displayedMonth.year;
|
||||
final int month = displayedMonth.month;
|
||||
final int daysInMonth = getDaysInMonth(year, month);
|
||||
// This assumes a start day of SUNDAY, but could be changed.
|
||||
final int firstWeekday = new DateTime(year, month).weekday % 7;
|
||||
final int firstDayOffset = _computeFirstDayOffset(year, month, localizations);
|
||||
final List<Widget> labels = <Widget>[];
|
||||
labels.addAll(_getDayHeaders(themeData.textTheme.caption));
|
||||
for (int i = 0; true; ++i) {
|
||||
final int day = i - firstWeekday + 1;
|
||||
labels.addAll(_getDayHeaders(themeData.textTheme.caption, localizations));
|
||||
for (int i = 0; true; i += 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)
|
||||
break;
|
||||
if (day < 1) {
|
||||
@ -370,7 +436,7 @@ class DayPicker extends StatelessWidget {
|
||||
child: new Center(
|
||||
child: new GestureDetector(
|
||||
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,
|
||||
),
|
||||
),
|
||||
@ -558,6 +624,7 @@ class _MonthPickerState extends State<MonthPicker> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
return new SizedBox(
|
||||
width: _kMonthPickerPortraitWidth,
|
||||
height: _kMaxDayPickerHeight,
|
||||
@ -576,7 +643,7 @@ class _MonthPickerState extends State<MonthPicker> {
|
||||
start: 8.0,
|
||||
child: new IconButton(
|
||||
icon: _getPreviousMonthIcon(textDirection),
|
||||
tooltip: MaterialLocalizations.of(context).previousMonthTooltip,
|
||||
tooltip: localizations.previousMonthTooltip,
|
||||
onPressed: _isDisplayingFirstMonth ? null : _handlePreviousMonth,
|
||||
),
|
||||
),
|
||||
@ -585,7 +652,7 @@ class _MonthPickerState extends State<MonthPicker> {
|
||||
end: 8.0,
|
||||
child: new IconButton(
|
||||
icon: _getNextMonthIcon(textDirection),
|
||||
tooltip: MaterialLocalizations.of(context).nextMonthTooltip,
|
||||
tooltip: localizations.nextMonthTooltip,
|
||||
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
|
||||
/// 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:
|
||||
///
|
||||
/// * [showTimePicker]
|
||||
@ -893,6 +968,8 @@ Future<DateTime> showDatePicker({
|
||||
@required DateTime lastDate,
|
||||
SelectableDayPredicate selectableDayPredicate,
|
||||
DatePickerMode initialDatePickerMode: DatePickerMode.day,
|
||||
Locale locale,
|
||||
TextDirection textDirection,
|
||||
}) async {
|
||||
assert(!initialDate.isBefore(firstDate), 'initialDate must be on or after firstDate');
|
||||
assert(!initialDate.isAfter(lastDate), 'initialDate must be on or before lastDate');
|
||||
@ -902,14 +979,32 @@ Future<DateTime> showDatePicker({
|
||||
'Provided initialDate must satisfy provided selectableDayPredicate'
|
||||
);
|
||||
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,
|
||||
child: new _DatePickerDialog(
|
||||
initialDate: initialDate,
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
selectableDayPredicate: selectableDayPredicate,
|
||||
initialDatePickerMode: initialDatePickerMode,
|
||||
)
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
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 'time.dart';
|
||||
@ -121,6 +123,52 @@ abstract class MaterialLocalizations {
|
||||
/// Formats [timeOfDay] according to the value of [timeOfDayFormat].
|
||||
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
|
||||
/// that encloses the given context.
|
||||
///
|
||||
@ -146,13 +194,30 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
|
||||
/// [LocalizationsDelegate] implementations typically call the static [load]
|
||||
/// function, rather than constructing this class directly.
|
||||
DefaultMaterialLocalizations(this.locale)
|
||||
: this._localeName = _computeLocaleName(locale) {
|
||||
assert(locale != null);
|
||||
: assert(locale != null),
|
||||
this._localeName = _computeLocaleName(locale) {
|
||||
_loadDateIntlDataIfNotLoaded();
|
||||
|
||||
if (localizations.containsKey(locale.languageCode))
|
||||
_nameToValue.addAll(localizations[locale.languageCode]);
|
||||
if (localizations.containsKey(_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)) {
|
||||
_decimalFormat = new intl.NumberFormat.decimalPattern(_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.
|
||||
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) {
|
||||
final String localeName = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
|
||||
return intl.Intl.canonicalizedLocale(localeName);
|
||||
@ -225,6 +297,29 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
|
||||
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.
|
||||
///
|
||||
/// 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,
|
||||
'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_test/flutter_test.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'feedback_tester.dart';
|
||||
|
||||
@ -115,6 +116,38 @@ void main() {
|
||||
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 {
|
||||
BuildContext buttonContext;
|
||||
await tester.pumpWidget(new MaterialApp(
|
||||
@ -207,7 +240,10 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('2017'));
|
||||
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.pump();
|
||||
await tester.tap(find.text('19'));
|
||||
@ -383,4 +419,250 @@ void main() {
|
||||
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