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<*>
This commit is contained in:
parent
dc9c95375f
commit
235b64ed2f
@ -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 {
|
|||||||
@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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
@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 {
|
|||||||
/// 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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
|
|
||||||
@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> {
|
|||||||
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> {
|
|||||||
_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> {
|
|||||||
_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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
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 {
|
|||||||
/// - 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 {
|
|||||||
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 {
|
|||||||
'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 {
|
|||||||
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 @@
|
|||||||
// 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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
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 {
|
||||||
|
@ -0,0 +1,139 @@
|
|||||||
|
// 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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user