Unverified Commit 235b64ed authored by Yegor's avatar Yegor Committed by GitHub

make date picker accessible (#13502)

* make date picker accessible

* make test file lookup location-independent

* address some comments

* always wrap in IgnorePointer

* no bitmasks for flags and actions

* recommend List<*>
parent dc9c9537
...@@ -126,22 +126,31 @@ class _DatePickerHeader extends StatelessWidget { ...@@ -126,22 +126,31 @@ class _DatePickerHeader extends StatelessWidget {
break; break;
} }
Widget yearButton = new _DateHeaderButton( final Widget yearButton = new IgnorePointer(
color: backgroundColor, ignoring: mode != DatePickerMode.day,
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.year), context), ignoringSemantics: false,
child: new Text(localizations.formatYear(selectedDate), style: yearStyle), child: new _DateHeaderButton(
); color: backgroundColor,
Widget dayButton = new _DateHeaderButton( onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.year), context),
color: backgroundColor, child: new Semantics(
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.day), context), selected: mode == DatePickerMode.year,
child: new Text(localizations.formatMediumDate(selectedDate), style: dayStyle), child: new Text(localizations.formatYear(selectedDate), style: yearStyle),
),
),
); );
// Disable the button for the current mode. final Widget dayButton = new IgnorePointer(
if (mode == DatePickerMode.day) ignoring: mode == DatePickerMode.day,
dayButton = new IgnorePointer(child: dayButton); ignoringSemantics: false,
else child: new _DateHeaderButton(
yearButton = new IgnorePointer(child: yearButton); color: backgroundColor,
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.day), context),
child: new Semantics(
selected: mode == DatePickerMode.day,
child: new Text(localizations.formatMediumDate(selectedDate), style: dayStyle),
),
),
);
return new Container( return new Container(
width: width, width: width,
...@@ -238,7 +247,6 @@ class DayPicker extends StatelessWidget { ...@@ -238,7 +247,6 @@ class DayPicker extends StatelessWidget {
@required this.firstDate, @required this.firstDate,
@required this.lastDate, @required this.lastDate,
@required this.displayedMonth, @required this.displayedMonth,
this.onMonthHeaderTap,
this.selectableDayPredicate, this.selectableDayPredicate,
}) : assert(selectedDate != null), }) : assert(selectedDate != null),
assert(currentDate != null), assert(currentDate != null),
...@@ -259,9 +267,6 @@ class DayPicker extends StatelessWidget { ...@@ -259,9 +267,6 @@ class DayPicker extends StatelessWidget {
/// Called when the user picks a day. /// Called when the user picks a day.
final ValueChanged<DateTime> onChanged; final ValueChanged<DateTime> onChanged;
/// Called when the user taps on the header that displays the current month.
final VoidCallback onMonthHeaderTap;
/// The earliest date the user is permitted to pick. /// The earliest date the user is permitted to pick.
final DateTime firstDate; final DateTime firstDate;
...@@ -296,7 +301,9 @@ class DayPicker extends StatelessWidget { ...@@ -296,7 +301,9 @@ class DayPicker extends StatelessWidget {
final List<Widget> result = <Widget>[]; final List<Widget> result = <Widget>[];
for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) { for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) {
final String weekday = localizations.narrowWeekdays[i]; final String weekday = localizations.narrowWeekdays[i];
result.add(new Center(child: new Text(weekday, style: headerStyle))); result.add(new ExcludeSemantics(
child: new Center(child: new Text(weekday, style: headerStyle)),
));
if (i == (localizations.firstDayOfWeekIndex - 1) % 7) if (i == (localizations.firstDayOfWeekIndex - 1) % 7)
break; break;
} }
...@@ -392,7 +399,8 @@ class DayPicker extends StatelessWidget { ...@@ -392,7 +399,8 @@ class DayPicker extends StatelessWidget {
BoxDecoration decoration; BoxDecoration decoration;
TextStyle itemStyle = themeData.textTheme.body1; TextStyle itemStyle = themeData.textTheme.body1;
if (selectedDate.year == year && selectedDate.month == month && selectedDate.day == day) { final bool isSelectedDay = selectedDate.year == year && selectedDate.month == month && selectedDate.day == day;
if (isSelectedDay) {
// The selected day gets a circle background highlight, and a contrasting text color. // The selected day gets a circle background highlight, and a contrasting text color.
itemStyle = themeData.accentTextTheme.body2; itemStyle = themeData.accentTextTheme.body2;
decoration = new BoxDecoration( decoration = new BoxDecoration(
...@@ -409,7 +417,19 @@ class DayPicker extends StatelessWidget { ...@@ -409,7 +417,19 @@ class DayPicker extends StatelessWidget {
Widget dayWidget = new Container( Widget dayWidget = new Container(
decoration: decoration, decoration: decoration,
child: new Center( child: new Center(
child: new Text(localizations.formatDecimal(day), style: itemStyle), child: new 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,
child: new ExcludeSemantics(
child: new Text(localizations.formatDecimal(day), style: itemStyle),
),
),
), ),
); );
...@@ -434,9 +454,9 @@ class DayPicker extends StatelessWidget { ...@@ -434,9 +454,9 @@ class DayPicker extends StatelessWidget {
new Container( new Container(
height: _kDayPickerRowHeight, height: _kDayPickerRowHeight,
child: new Center( child: new Center(
child: new GestureDetector( child: new ExcludeSemantics(
onTap: onMonthHeaderTap != null ? Feedback.wrapForTap(onMonthHeaderTap, context) : null, child: new Text(
child: new Text(localizations.formatMonthYear(displayedMonth), localizations.formatMonthYear(displayedMonth),
style: themeData.textTheme.subhead, style: themeData.textTheme.subhead,
), ),
), ),
...@@ -478,7 +498,6 @@ class MonthPicker extends StatefulWidget { ...@@ -478,7 +498,6 @@ class MonthPicker extends StatefulWidget {
@required this.firstDate, @required this.firstDate,
@required this.lastDate, @required this.lastDate,
this.selectableDayPredicate, this.selectableDayPredicate,
this.onMonthHeaderTap,
}) : assert(selectedDate != null), }) : assert(selectedDate != null),
assert(onChanged != null), assert(onChanged != null),
assert(!firstDate.isAfter(lastDate)), assert(!firstDate.isAfter(lastDate)),
...@@ -493,9 +512,6 @@ class MonthPicker extends StatefulWidget { ...@@ -493,9 +512,6 @@ class MonthPicker extends StatefulWidget {
/// Called when the user picks a month. /// Called when the user picks a month.
final ValueChanged<DateTime> onChanged; final ValueChanged<DateTime> onChanged;
/// Called when the user taps on the header that displays the current month.
final VoidCallback onMonthHeaderTap;
/// The earliest date the user is permitted to pick. /// The earliest date the user is permitted to pick.
final DateTime firstDate; final DateTime firstDate;
...@@ -514,8 +530,9 @@ class _MonthPickerState extends State<MonthPicker> { ...@@ -514,8 +530,9 @@ class _MonthPickerState extends State<MonthPicker> {
void initState() { void initState() {
super.initState(); super.initState();
// Initially display the pre-selected date. // Initially display the pre-selected date.
_dayPickerController = new PageController(initialPage: _monthDelta(widget.firstDate, widget.selectedDate)); final int monthPage = _monthDelta(widget.firstDate, widget.selectedDate);
_currentDisplayedMonthDate = new DateTime(widget.selectedDate.year, widget.selectedDate.month); _dayPickerController = new PageController(initialPage: monthPage);
_handleMonthPageChanged(monthPage);
_updateCurrentDate(); _updateCurrentDate();
} }
...@@ -523,12 +540,22 @@ class _MonthPickerState extends State<MonthPicker> { ...@@ -523,12 +540,22 @@ class _MonthPickerState extends State<MonthPicker> {
void didUpdateWidget(MonthPicker oldWidget) { void didUpdateWidget(MonthPicker oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.selectedDate != oldWidget.selectedDate) { if (widget.selectedDate != oldWidget.selectedDate) {
_dayPickerController = new PageController(initialPage: _monthDelta(widget.firstDate, widget.selectedDate)); final int monthPage = _monthDelta(widget.firstDate, widget.selectedDate);
_currentDisplayedMonthDate = _dayPickerController = new PageController(initialPage: monthPage);
new DateTime(widget.selectedDate.year, widget.selectedDate.month); _handleMonthPageChanged(monthPage);
} }
} }
MaterialLocalizations localizations;
TextDirection textDirection;
@override
void didChangeDependencies() {
super.didChangeDependencies();
localizations = MaterialLocalizations.of(context);
textDirection = Directionality.of(context);
}
DateTime _todayDate; DateTime _todayDate;
DateTime _currentDisplayedMonthDate; DateTime _currentDisplayedMonthDate;
Timer _timer; Timer _timer;
...@@ -567,18 +594,21 @@ class _MonthPickerState extends State<MonthPicker> { ...@@ -567,18 +594,21 @@ class _MonthPickerState extends State<MonthPicker> {
lastDate: widget.lastDate, lastDate: widget.lastDate,
displayedMonth: month, displayedMonth: month,
selectableDayPredicate: widget.selectableDayPredicate, selectableDayPredicate: widget.selectableDayPredicate,
onMonthHeaderTap: widget.onMonthHeaderTap,
); );
} }
void _handleNextMonth() { void _handleNextMonth() {
if (!_isDisplayingLastMonth) if (!_isDisplayingLastMonth) {
SemanticsService.announce(localizations.formatMonthYear(_nextMonthDate), textDirection);
_dayPickerController.nextPage(duration: _kMonthScrollDuration, curve: Curves.ease); _dayPickerController.nextPage(duration: _kMonthScrollDuration, curve: Curves.ease);
}
} }
void _handlePreviousMonth() { void _handlePreviousMonth() {
if (!_isDisplayingFirstMonth) if (!_isDisplayingFirstMonth) {
SemanticsService.announce(localizations.formatMonthYear(_previousMonthDate), textDirection);
_dayPickerController.previousPage(duration: _kMonthScrollDuration, curve: Curves.ease); _dayPickerController.previousPage(duration: _kMonthScrollDuration, curve: Curves.ease);
}
} }
/// True if the earliest allowable month is displayed. /// True if the earliest allowable month is displayed.
...@@ -593,15 +623,19 @@ class _MonthPickerState extends State<MonthPicker> { ...@@ -593,15 +623,19 @@ class _MonthPickerState extends State<MonthPicker> {
new DateTime(widget.lastDate.year, widget.lastDate.month)); new DateTime(widget.lastDate.year, widget.lastDate.month));
} }
DateTime _previousMonthDate;
DateTime _nextMonthDate;
void _handleMonthPageChanged(int monthPage) { void _handleMonthPageChanged(int monthPage) {
setState(() { setState(() {
_previousMonthDate = _addMonthsToMonthDate(widget.firstDate, monthPage - 1);
_currentDisplayedMonthDate = _addMonthsToMonthDate(widget.firstDate, monthPage); _currentDisplayedMonthDate = _addMonthsToMonthDate(widget.firstDate, monthPage);
_nextMonthDate = _addMonthsToMonthDate(widget.firstDate, monthPage + 1);
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
return new SizedBox( return new SizedBox(
width: _kMonthPickerPortraitWidth, width: _kMonthPickerPortraitWidth,
height: _kMaxDayPickerHeight, height: _kMaxDayPickerHeight,
...@@ -620,7 +654,7 @@ class _MonthPickerState extends State<MonthPicker> { ...@@ -620,7 +654,7 @@ class _MonthPickerState extends State<MonthPicker> {
start: 8.0, start: 8.0,
child: new IconButton( child: new IconButton(
icon: const Icon(Icons.chevron_left), icon: const Icon(Icons.chevron_left),
tooltip: localizations.previousMonthTooltip, tooltip: _isDisplayingFirstMonth ? null : '${localizations.previousMonthTooltip} ${localizations.formatMonthYear(_previousMonthDate)}',
onPressed: _isDisplayingFirstMonth ? null : _handlePreviousMonth, onPressed: _isDisplayingFirstMonth ? null : _handlePreviousMonth,
), ),
), ),
...@@ -629,7 +663,7 @@ class _MonthPickerState extends State<MonthPicker> { ...@@ -629,7 +663,7 @@ class _MonthPickerState extends State<MonthPicker> {
end: 8.0, end: 8.0,
child: new IconButton( child: new IconButton(
icon: const Icon(Icons.chevron_right), icon: const Icon(Icons.chevron_right),
tooltip: localizations.nextMonthTooltip, tooltip: _isDisplayingLastMonth ? null : '${localizations.nextMonthTooltip} ${localizations.formatMonthYear(_nextMonthDate)}',
onPressed: _isDisplayingLastMonth ? null : _handleNextMonth, onPressed: _isDisplayingLastMonth ? null : _handleNextMonth,
), ),
), ),
...@@ -640,8 +674,8 @@ class _MonthPickerState extends State<MonthPicker> { ...@@ -640,8 +674,8 @@ class _MonthPickerState extends State<MonthPicker> {
@override @override
void dispose() { void dispose() {
if (_timer != null) _timer?.cancel();
_timer.cancel(); _dayPickerController?.dispose();
super.dispose(); super.dispose();
} }
} }
...@@ -718,15 +752,20 @@ class _YearPickerState extends State<YearPicker> { ...@@ -718,15 +752,20 @@ class _YearPickerState extends State<YearPicker> {
itemCount: widget.lastDate.year - widget.firstDate.year + 1, itemCount: widget.lastDate.year - widget.firstDate.year + 1,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final int year = widget.firstDate.year + index; final int year = widget.firstDate.year + index;
final TextStyle itemStyle = year == widget.selectedDate.year ? final bool isSelected = year == widget.selectedDate.year;
themeData.textTheme.headline.copyWith(color: themeData.accentColor) : style; final TextStyle itemStyle = isSelected
? themeData.textTheme.headline.copyWith(color: themeData.accentColor)
: style;
return new InkWell( return new InkWell(
key: new ValueKey<int>(year), key: new ValueKey<int>(year),
onTap: () { onTap: () {
widget.onChanged(new DateTime(year, widget.selectedDate.month, widget.selectedDate.day)); widget.onChanged(new DateTime(year, widget.selectedDate.month, widget.selectedDate.day));
}, },
child: new Center( child: new Center(
child: new Text(year.toString(), style: itemStyle), child: new Semantics(
selected: isSelected,
child: new Text(year.toString(), style: itemStyle),
),
), ),
); );
}, },
...@@ -762,6 +801,25 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { ...@@ -762,6 +801,25 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
_mode = widget.initialDatePickerMode; _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; DateTime _selectedDate;
DatePickerMode _mode; DatePickerMode _mode;
final GlobalKey _pickerKey = new GlobalKey(); final GlobalKey _pickerKey = new GlobalKey();
...@@ -781,6 +839,11 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { ...@@ -781,6 +839,11 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
_vibrate(); _vibrate();
setState(() { setState(() {
_mode = mode; _mode = mode;
if (_mode == DatePickerMode.day) {
SemanticsService.announce(localizations.formatMonthYear(_selectedDate), textDirection);
} else {
SemanticsService.announce(localizations.formatYear(_selectedDate), textDirection);
}
}); });
} }
...@@ -807,10 +870,6 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { ...@@ -807,10 +870,6 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
Navigator.pop(context, _selectedDate); Navigator.pop(context, _selectedDate);
} }
void _handleMonthHeaderTap() {
_handleModeChanged(DatePickerMode.year);
}
Widget _buildPicker() { Widget _buildPicker() {
assert(_mode != null); assert(_mode != null);
switch (_mode) { switch (_mode) {
...@@ -822,7 +881,6 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { ...@@ -822,7 +881,6 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
firstDate: widget.firstDate, firstDate: widget.firstDate,
lastDate: widget.lastDate, lastDate: widget.lastDate,
selectableDayPredicate: widget.selectableDayPredicate, selectableDayPredicate: widget.selectableDayPredicate,
onMonthHeaderTap: _handleMonthHeaderTap,
); );
case DatePickerMode.year: case DatePickerMode.year:
return new YearPicker( return new YearPicker(
...@@ -844,7 +902,6 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { ...@@ -844,7 +902,6 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
child: _buildPicker(), child: _buildPicker(),
), ),
); );
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final Widget actions = new ButtonTheme.bar( final Widget actions = new ButtonTheme.bar(
child: new ButtonBar( child: new ButtonBar(
children: <Widget>[ children: <Widget>[
...@@ -862,13 +919,13 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { ...@@ -862,13 +919,13 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
return new Dialog( return new Dialog(
child: new OrientationBuilder( child: new OrientationBuilder(
builder: (BuildContext context, Orientation orientation) { builder: (BuildContext context, Orientation orientation) {
assert(orientation != null);
final Widget header = new _DatePickerHeader( final Widget header = new _DatePickerHeader(
selectedDate: _selectedDate, selectedDate: _selectedDate,
mode: _mode, mode: _mode,
onModeChanged: _handleModeChanged, onModeChanged: _handleModeChanged,
orientation: orientation, orientation: orientation,
); );
assert(orientation != null);
switch (orientation) { switch (orientation) {
case Orientation.portrait: case Orientation.portrait:
return new SizedBox( return new SizedBox(
......
...@@ -196,6 +196,17 @@ abstract class MaterialLocalizations { ...@@ -196,6 +196,17 @@ abstract class MaterialLocalizations {
/// - Russian: ср, сент. 27 /// - Russian: ср, сент. 27
String formatMediumDate(DateTime date); String formatMediumDate(DateTime date);
/// Formats day of week, month, day of month and year in a long-width format.
///
/// Does not abbreviate names. Appears in spoken announcements of the date
/// picker invoked using [showDatePicker], when accessibility mode is on.
///
/// Examples:
///
/// - US English: Wednesday, September 27, 2017
/// - Russian: Среда, Сентябрь 27, 2017
String formatFullDate(DateTime date);
/// Formats the month and the year of the given [date]. /// Formats the month and the year of the given [date].
/// ///
/// The returned string does not contain the day of the month. This appears /// The returned string does not contain the day of the month. This appears
...@@ -275,7 +286,7 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { ...@@ -275,7 +286,7 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
const DefaultMaterialLocalizations(); const DefaultMaterialLocalizations();
// Ordered to match DateTime.MONDAY=1, DateTime.SUNDAY=6 // Ordered to match DateTime.MONDAY=1, DateTime.SUNDAY=6
static const List<String>_shortWeekdays = const <String>[ static const List<String> _shortWeekdays = const <String>[
'Mon', 'Mon',
'Tue', 'Tue',
'Wed', 'Wed',
...@@ -285,6 +296,17 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { ...@@ -285,6 +296,17 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
'Sun', 'Sun',
]; ];
// Ordered to match DateTime.MONDAY=1, DateTime.SUNDAY=6
static const List<String> _weekdays = const <String>[
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
];
static const List<String> _narrowWeekdays = const <String>[ static const List<String> _narrowWeekdays = const <String>[
'S', 'S',
'M', 'M',
...@@ -365,6 +387,12 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { ...@@ -365,6 +387,12 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
return '$day, $month ${date.day}'; return '$day, $month ${date.day}';
} }
@override
String formatFullDate(DateTime date) {
final String month = _months[date.month - DateTime.JANUARY];
return '${_weekdays[date.weekday - DateTime.MONDAY]}, $month ${date.day}, ${date.year}';
}
@override @override
String formatMonthYear(DateTime date) { String formatMonthYear(DateTime date) {
final String year = formatYear(date); final String year = formatYear(date);
......
...@@ -2,17 +2,28 @@ ...@@ -2,17 +2,28 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart'; import 'feedback_tester.dart';
void main() { void main() {
group('showDatePicker', () {
_tests();
});
}
void _tests() {
DateTime firstDate; DateTime firstDate;
DateTime lastDate; DateTime lastDate;
DateTime initialDate; DateTime initialDate;
SelectableDayPredicate selectableDayPredicate; SelectableDayPredicate selectableDayPredicate;
DatePickerMode initialDatePickerMode; DatePickerMode initialDatePickerMode;
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));
setUp(() { setUp(() {
firstDate = new DateTime(2001, DateTime.JANUARY, 1); firstDate = new DateTime(2001, DateTime.JANUARY, 1);
...@@ -63,7 +74,7 @@ void main() { ...@@ -63,7 +74,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(_selectedDate, equals(new DateTime(2016, DateTime.JULY, 1))); expect(_selectedDate, equals(new DateTime(2016, DateTime.JULY, 1)));
await tester.tap(find.byTooltip('Next month')); await tester.tap(nextMonthIcon);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(_selectedDate, equals(new DateTime(2016, DateTime.JULY, 1))); expect(_selectedDate, equals(new DateTime(2016, DateTime.JULY, 1)));
...@@ -114,38 +125,6 @@ void main() { ...@@ -114,38 +125,6 @@ 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(
...@@ -214,7 +193,7 @@ void main() { ...@@ -214,7 +193,7 @@ void main() {
testWidgets('Can select a month', (WidgetTester tester) async { testWidgets('Can select a month', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async { await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.byTooltip('Previous month')); await tester.tap(previousMonthIcon);
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.tap(find.text('25')); await tester.tap(find.text('25'));
await tester.tap(find.text('OK')); await tester.tap(find.text('OK'));
...@@ -279,17 +258,10 @@ void main() { ...@@ -279,17 +258,10 @@ void main() {
firstDate = initialDate; firstDate = initialDate;
lastDate = new DateTime(2017, DateTime.FEBRUARY, 20); lastDate = new DateTime(2017, DateTime.FEBRUARY, 20);
await preparePicker(tester, (Future<DateTime> date) async { await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.byTooltip('Next month')); await tester.tap(nextMonthIcon);
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
// Shouldn't be possible to keep going into March. // Shouldn't be possible to keep going into March.
await tester.tap(find.byTooltip('Next month')); expect(nextMonthIcon, findsNothing);
await tester.pumpAndSettle(const Duration(seconds: 1));
// We're still in February
await tester.tap(find.text('20'));
// Days outside bound for new month pages also disabled.
await tester.tap(find.text('25'));
await tester.tap(find.text('OK'));
expect(await date, equals(new DateTime(2017, DateTime.FEBRUARY, 20)));
}); });
}); });
...@@ -298,17 +270,10 @@ void main() { ...@@ -298,17 +270,10 @@ void main() {
firstDate = new DateTime(2016, DateTime.DECEMBER, 10); firstDate = new DateTime(2016, DateTime.DECEMBER, 10);
lastDate = initialDate; lastDate = initialDate;
await preparePicker(tester, (Future<DateTime> date) async { await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.byTooltip('Previous month')); await tester.tap(previousMonthIcon);
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
// Shouldn't be possible to keep going into November. // Shouldn't be possible to keep going into November.
await tester.tap(find.byTooltip('Previous month')); expect(previousMonthIcon, findsNothing);
await tester.pumpAndSettle(const Duration(seconds: 1));
// We're still in December
await tester.tap(find.text('10'));
// Days outside bound for new month pages also disabled.
await tester.tap(find.text('5'));
await tester.tap(find.text('OK'));
expect(await date, equals(new DateTime(2016, DateTime.DECEMBER, 10)));
}); });
}); });
...@@ -417,4 +382,227 @@ void main() { ...@@ -417,4 +382,227 @@ void main() {
expect(await date, isNull); expect(await date, isNull);
}); });
}); });
testWidgets('exports semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await preparePicker(tester, (Future<DateTime> date) async {
final TestSemantics expected = new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
flags: <SemanticsFlags>[SemanticsFlags.isSelected],
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'Fri, Jan 15',
textDirection: TextDirection.ltr,
),
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight],
children: <TestSemantics>[
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'1, Friday, January 1, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'2, Saturday, January 2, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'3, Sunday, January 3, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'4, Monday, January 4, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'5, Tuesday, January 5, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'6, Wednesday, January 6, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'7, Thursday, January 7, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'8, Friday, January 8, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'9, Saturday, January 9, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'10, Sunday, January 10, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'11, Monday, January 11, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'12, Tuesday, January 12, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'13, Wednesday, January 13, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'14, Thursday, January 14, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
flags: <SemanticsFlags>[SemanticsFlags.isSelected],
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'15, Friday, January 15, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'16, Saturday, January 16, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'17, Sunday, January 17, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'18, Monday, January 18, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'19, Tuesday, January 19, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'20, Wednesday, January 20, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'21, Thursday, January 21, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'22, Friday, January 22, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'23, Saturday, January 23, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'24, Sunday, January 24, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'25, Monday, January 25, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'26, Tuesday, January 26, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'27, Wednesday, January 27, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'28, Thursday, January 28, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'29, Friday, January 29, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'30, Saturday, January 30, 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'31, Sunday, January 31, 2016',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'Previous month December 2015',
textDirection: TextDirection.ltr,
),
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'Next month February 2016',
textDirection: TextDirection.ltr,
),
new TestSemantics(
flags: <SemanticsFlags>[SemanticsFlags.isButton],
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'CANCEL',
textDirection: TextDirection.ltr,
),
new TestSemantics(
flags: <SemanticsFlags>[SemanticsFlags.isButton],
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'OK',
textDirection: TextDirection.ltr,
),
],
);
expect(semantics, hasSemantics(
expected,
ignoreId: true,
ignoreTransform: true,
ignoreRect: true,
));
});
});
} }
...@@ -46,7 +46,8 @@ class TestSemantics { ...@@ -46,7 +46,8 @@ class TestSemantics {
this.transform, this.transform,
this.children: const <TestSemantics>[], this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags, Iterable<SemanticsTag> tags,
}) : assert(flags != null), }) : assert(flags is int || flags is List<SemanticsFlags>),
assert(actions is int || actions is List<SemanticsAction>),
assert(label != null), assert(label != null),
assert(value != null), assert(value != null),
assert(increasedValue != null), assert(increasedValue != null),
...@@ -70,7 +71,8 @@ class TestSemantics { ...@@ -70,7 +71,8 @@ class TestSemantics {
this.children: const <TestSemantics>[], this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags, Iterable<SemanticsTag> tags,
}) : id = 0, }) : id = 0,
assert(flags != null), assert(flags is int || flags is List<SemanticsFlags>),
assert(actions is int || actions is List<SemanticsAction>),
assert(label != null), assert(label != null),
assert(increasedValue != null), assert(increasedValue != null),
assert(decreasedValue != null), assert(decreasedValue != null),
...@@ -103,7 +105,8 @@ class TestSemantics { ...@@ -103,7 +105,8 @@ class TestSemantics {
Matrix4 transform, Matrix4 transform,
this.children: const <TestSemantics>[], this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags, Iterable<SemanticsTag> tags,
}) : assert(flags != null), }) : assert(flags is int || flags is List<SemanticsFlags>),
assert(actions is int || actions is List<SemanticsAction>),
assert(label != null), assert(label != null),
assert(value != null), assert(value != null),
assert(increasedValue != null), assert(increasedValue != null),
...@@ -119,11 +122,24 @@ class TestSemantics { ...@@ -119,11 +122,24 @@ class TestSemantics {
/// they are created. /// they are created.
final int id; final int id;
/// A bit field of [SemanticsFlags] that apply to this node. /// The [SemanticsFlags] set on this node.
final int flags; ///
/// There are two ways to specify this property: as an `int` that encodes the
/// flags as a bit field, or as a `List<SemanticsFlags>` that are _on_.
///
/// Using `List<SemanticsFlags>` is recommended due to better readability.
final dynamic flags;
/// A bit field of [SemanticsActions] that apply to this node. /// The [SemanticsAction]s set on this node.
final int actions; ///
/// There are two ways to specify this property: as an `int` that encodes the
/// actions as a bit field, or as a `List<SemanticsAction>`.
///
/// Using `List<SemanticsAction>` is recommended due to better readability.
///
/// The tester does not check the function corresponding to the action, but
/// only its existence.
final dynamic actions;
/// A textual description of this node. /// A textual description of this node.
final String label; final String label;
...@@ -204,10 +220,19 @@ class TestSemantics { ...@@ -204,10 +220,19 @@ class TestSemantics {
return fail('could not find node with id $id.'); return fail('could not find node with id $id.');
if (!ignoreId && id != node.id) if (!ignoreId && id != node.id)
return fail('expected node id $id but found id ${node.id}.'); return fail('expected node id $id but found id ${node.id}.');
if (flags != nodeData.flags)
final int flagsBitmask = flags is int
? flags
: flags.fold<int>(0, (int bitmask, SemanticsFlags flag) => bitmask | flag.index);
if (flagsBitmask != nodeData.flags)
return fail('expected node id $id to have flags $flags but found flags ${nodeData.flags}.'); return fail('expected node id $id to have flags $flags but found flags ${nodeData.flags}.');
if (actions != nodeData.actions)
final int actionsBitmask = actions is int
? actions
: actions.fold<int>(0, (int bitmask, SemanticsAction action) => bitmask | action.index);
if (actionsBitmask != nodeData.actions)
return fail('expected node id $id to have actions $actions but found actions ${nodeData.actions}.'); return fail('expected node id $id to have actions $actions but found actions ${nodeData.actions}.');
if (label != nodeData.label) if (label != nodeData.label)
return fail('expected node id $id to have label "$label" but found label "${nodeData.label}".'); return fail('expected node id $id to have label "$label" but found label "${nodeData.label}".');
if (value != nodeData.value) if (value != nodeData.value)
...@@ -340,6 +365,109 @@ class SemanticsTester { ...@@ -340,6 +365,109 @@ class SemanticsTester {
visit(tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode); visit(tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode);
return result; return result;
} }
/// Generates an expression that creates a [TestSemantics] reflecting the
/// current tree of [SemanticsNode]s.
///
/// Use this method to generate code for unit tests. It works similar to
/// screenshot testing. The very first time you add semantics to a widget you
/// verify manually that the widget behaves correctly. You then use ths method
/// to generate test code for this widget.
///
/// Example:
///
/// ```dart
/// testWidgets('generate code for MyWidget', (WidgetTester tester) async {
/// var semantics = new SemanticsTester(tester);
/// await tester.pumpWidget(new MyWidget());
/// print(semantics.generateTestSemanticsExpressionForCurrentSemanticsTree());
/// semantics.dispose();
/// });
/// ```
///
/// You can now copy the code printed to the console into a unit test:
///
/// ```dart
/// testWidgets('generate code for MyWidget', (WidgetTester tester) async {
/// var semantics = new SemanticsTester(tester);
/// await tester.pumpWidget(new MyWidget());
/// expect(semantics, hasSemantics(
/// // Generated code:
/// new TestSemantics(
/// ... properties and child nodes ...
/// ),
/// ignoreRect: true,
/// ignoreTransform: true,
/// ignoreId: true,
/// ));
/// semantics.dispose();
/// });
///
/// At this point the unit test should automatically pass because it was
/// generated from the actual [SemanticsNode]s. Next time the semantics tree
/// changes, the test code may either be updated manually, or regenerated and
/// replaced using this method again.
///
/// Avoid submitting huge piles of generated test code. This will make test
/// code hard to review and it will make it tempting to regenerate test code
/// every time and ignore potential regressions. Make sure you do not
/// over-test. Prefer breaking your widgets into smaller widgets and test them
/// individually.
String generateTestSemanticsExpressionForCurrentSemanticsTree() {
final SemanticsNode node = tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
return _generateSemanticsTestForNode(node, 0);
}
String _flagsToSemanticsFlagsExpression(int bitmap) {
return SemanticsFlags.values.values
.where((SemanticsFlags flag) => (flag.index & bitmap) != 0)
.join(', ');
}
String _actionsToSemanticsActionExpression(int bitmap) {
return SemanticsAction.values.values
.where((SemanticsAction action) => (action.index & bitmap) != 0)
.join(', ');
}
/// Recursively generates [TestSemantics] code for [node] and its children,
/// indenting the expression by `indentAmount`.
String _generateSemanticsTestForNode(SemanticsNode node, int indentAmount) {
final String indent = ' ' * indentAmount;
final StringBuffer buf = new StringBuffer();
final SemanticsData nodeData = node.getSemanticsData();
buf.writeln('new TestSemantics(');
if (nodeData.flags != 0)
buf.writeln(' flags: <SemanticsFlags>[${_flagsToSemanticsFlagsExpression(nodeData.flags)}],');
if (nodeData.actions != 0)
buf.writeln(' actions: <SemanticsAction>[${_actionsToSemanticsActionExpression(nodeData.actions)}],');
if (node.label != null && node.label.isNotEmpty)
buf.writeln(' label: r\'${node.label}\',');
if (node.value != null && node.value.isNotEmpty)
buf.writeln(' value: r\'${node.value}\',');
if (node.increasedValue != null && node.increasedValue.isNotEmpty)
buf.writeln(' increasedValue: r\'${node.increasedValue}\',');
if (node.decreasedValue != null && node.decreasedValue.isNotEmpty)
buf.writeln(' decreasedValue: r\'${node.decreasedValue}\',');
if (node.hint != null && node.hint.isNotEmpty)
buf.writeln(' hint: r\'${node.hint}\',');
if (node.textDirection != null)
buf.writeln(' textDirection: ${node.textDirection},');
if (node.hasChildren) {
buf.writeln(' children: <TestSemantics>[');
node.visitChildren((SemanticsNode child) {
buf
..write(_generateSemanticsTestForNode(child, 2))
..writeln(',');
return true;
});
buf.writeln(' ],');
}
buf.write(')');
return buf.toString().split('\n').map((String l) => '$indent$l').join('\n');
}
} }
class _HasSemantics extends Matcher { class _HasSemantics extends Matcher {
......
// Copyright 2017 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 'dart:async';
import 'dart:io';
import 'dart:ui' show SemanticsFlags;
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
void main() {
group('generateTestSemanticsExpressionForCurrentSemanticsTree', () {
_tests();
});
}
void _tests() {
setUp(() {
debugResetSemanticsIdCounter();
});
Future<Null> pumpTestWidget(WidgetTester tester) async {
await tester.pumpWidget(new MaterialApp(
home: new ListView(
children: <Widget>[
const Text('Plain text'),
new Semantics(
selected: true,
checked: true,
onTap: () {},
onDecrease: () {},
value: 'test-value',
increasedValue: 'test-increasedValue',
decreasedValue: 'test-decreasedValue',
hint: 'test-hint',
textDirection: TextDirection.rtl,
child: const Text('Interactive text'),
),
],
),
));
}
// This test generates code using generateTestSemanticsExpressionForCurrentSemanticsTree
// then compares it to the code used in the 'generated code is correct' test
// below. When you update the implementation of generateTestSemanticsExpressionForCurrentSemanticsTree
// also update this code to reflect the new output.
//
// This test is flexible w.r.t. leading and trailing whitespace.
testWidgets('generates code', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await pumpTestWidget(tester);
final String code = semantics
.generateTestSemanticsExpressionForCurrentSemanticsTree()
.split('\n')
.map((String line) => line.trim())
.join('\n')
.trim() + ',';
File findThisTestFile(Directory directory) {
for (FileSystemEntity entity in directory.listSync()) {
if (entity is Directory) {
final File childSearch = findThisTestFile(entity);
if (childSearch != null) {
return childSearch;
}
} else if (entity is File && entity.path.endsWith('semantics_tester_generateTestSemanticsExpressionForCurrentSemanticsTree_test.dart')) {
return entity;
}
}
return null;
}
final File thisTestFile = findThisTestFile(Directory.current);
expect(thisTestFile, isNotNull);
String expectedCode = thisTestFile.readAsStringSync();
expectedCode = expectedCode.substring(
expectedCode.indexOf('>' * 12) + 12,
expectedCode.indexOf('<' * 12) - 3,
)
.split('\n')
.map((String line) => line.trim())
.join('\n')
.trim();
semantics.dispose();
expect(code, expectedCode);
});
testWidgets('generated code is correct', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await pumpTestWidget(tester);
expect(
semantics,
hasSemantics(
// The code below delimited by > and < characters is generated by
// generateTestSemanticsExpressionForCurrentSemanticsTree function.
// You must update it when changing the output generated by
// generateTestSemanticsExpressionForCurrentSemanticsTree. Otherwise,
// the test 'generates code', defined above, will fail.
// >>>>>>>>>>>>
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
label: r'Plain text',
textDirection: TextDirection.ltr,
),
new TestSemantics(
flags: <SemanticsFlags>[SemanticsFlags.hasCheckedState, SemanticsFlags.isChecked, SemanticsFlags.isSelected],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.decrease],
label: r'‪Interactive text‬',
value: r'test-value',
increasedValue: r'test-increasedValue',
decreasedValue: r'test-decreasedValue',
hint: r'test-hint',
textDirection: TextDirection.rtl,
),
],
),
],
),
],
),
// <<<<<<<<<<<<
ignoreRect: true,
ignoreTransform: true,
ignoreId: true,
)
);
semantics.dispose();
});
}
...@@ -76,14 +76,18 @@ class GlobalMaterialLocalizations implements MaterialLocalizations { ...@@ -76,14 +76,18 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
if (intl.DateFormat.localeExists(_localeName)) { if (intl.DateFormat.localeExists(_localeName)) {
_fullYearFormat = new intl.DateFormat.y(_localeName); _fullYearFormat = new intl.DateFormat.y(_localeName);
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern, _localeName); _mediumDateFormat = new intl.DateFormat(kMediumDatePattern, _localeName);
_longDateFormat = new intl.DateFormat.yMMMMEEEEd(_localeName);
_yearMonthFormat = new intl.DateFormat('yMMMM', _localeName); _yearMonthFormat = new intl.DateFormat('yMMMM', _localeName);
} else if (intl.DateFormat.localeExists(locale.languageCode)) { } else if (intl.DateFormat.localeExists(locale.languageCode)) {
_fullYearFormat = new intl.DateFormat.y(locale.languageCode); _fullYearFormat = new intl.DateFormat.y(locale.languageCode);
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern, locale.languageCode); _mediumDateFormat = new intl.DateFormat(kMediumDatePattern, locale.languageCode);
_longDateFormat = new intl.DateFormat.yMMMMEEEEd(locale.languageCode);
_yearMonthFormat = new intl.DateFormat('yMMMM', locale.languageCode); _yearMonthFormat = new intl.DateFormat('yMMMM', locale.languageCode);
} else { } else {
_fullYearFormat = new intl.DateFormat.y(); _fullYearFormat = new intl.DateFormat.y();
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern); _mediumDateFormat = new intl.DateFormat(kMediumDatePattern);
_longDateFormat = new intl.DateFormat.yMMMMEEEEd();
_yearMonthFormat = new intl.DateFormat('yMMMM'); _yearMonthFormat = new intl.DateFormat('yMMMM');
} }
...@@ -115,6 +119,8 @@ class GlobalMaterialLocalizations implements MaterialLocalizations { ...@@ -115,6 +119,8 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
intl.DateFormat _mediumDateFormat; intl.DateFormat _mediumDateFormat;
intl.DateFormat _longDateFormat;
intl.DateFormat _yearMonthFormat; intl.DateFormat _yearMonthFormat;
static String _computeLocaleName(Locale locale) { static String _computeLocaleName(Locale locale) {
...@@ -169,6 +175,11 @@ class GlobalMaterialLocalizations implements MaterialLocalizations { ...@@ -169,6 +175,11 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
return _mediumDateFormat.format(date); return _mediumDateFormat.format(date);
} }
@override
String formatFullDate(DateTime date) {
return _longDateFormat.format(date);
}
@override @override
String formatMonthYear(DateTime date) { String formatMonthYear(DateTime date) {
return _yearMonthFormat.format(date); return _yearMonthFormat.format(date);
......
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