Unverified Commit b80751cd authored by Yegor's avatar Yegor Committed by GitHub

Make time picker accessible (#13152)

* make time picker accessible

* use new CustomPaint a11y API

* flutter_localizations tests; use bigger distance delta

* fix am/pm control; selected values

* fix translations; remove @mustCallSuper in describeSemanticsConfiguration

* exclude AM/PM announcement from iOS as on iOS the label is read back automatically
parent 927a143d
......@@ -119,6 +119,14 @@ abstract class MaterialLocalizations {
/// The abbreviation for post meridiem (after noon) shown in the time picker.
String get postMeridiemAbbreviation;
/// The text-to-speech announcement made when a time picker invoked using
/// [showTimePicker] is set to the hour picker mode.
String get timePickerHourModeAnnouncement;
/// The text-to-speech announcement made when a time picker invoked using
/// [showTimePicker] is set to the minute picker mode.
String get timePickerMinuteModeAnnouncement;
/// The format used to lay out the time picker.
///
/// The documentation for [TimeOfDayFormat] enum values provides details on
......@@ -505,6 +513,12 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
@override
String get postMeridiemAbbreviation => 'PM';
@override
String get timePickerHourModeAnnouncement => 'Select hours';
@override
String get timePickerMinuteModeAnnouncement => 'Select minutes';
@override
TimeOfDayFormat timeOfDayFormat({ bool alwaysUse24HourFormat: false }) {
return alwaysUse24HourFormat
......
......@@ -147,7 +147,7 @@ class TextField extends StatefulWidget {
///
/// This text style is also used as the base style for the [decoration].
///
/// If null, defaults to a text style from the current [Theme].
/// If null, defaults to the `subhead` text style from the current [Theme].
final TextStyle style;
/// How the text being edited should be aligned horizontally.
......
......@@ -432,7 +432,8 @@ class RenderCustomPaint extends RenderProxyBox {
// Check if we need to rebuild semantics.
if (newPainter == null) {
assert(oldPainter != null); // We should be called only for changes.
markNeedsSemanticsUpdate();
if (attached)
markNeedsSemanticsUpdate();
} else if (oldPainter == null ||
newPainter.runtimeType != oldPainter.runtimeType ||
newPainter.shouldRebuildSemantics(oldPainter)) {
......
......@@ -822,7 +822,8 @@ class PipelineOwner {
/// objects for a given [PipelineOwner] are closed, the [PipelineOwner] stops
/// maintaining the semantics tree.
SemanticsHandle ensureSemantics({ VoidCallback listener }) {
if (_outstandingSemanticsHandle++ == 0) {
_outstandingSemanticsHandle += 1;
if (_outstandingSemanticsHandle == 1) {
assert(_semanticsOwner == null);
_semanticsOwner = new SemanticsOwner();
if (onSemanticsOwnerCreated != null)
......@@ -833,7 +834,8 @@ class PipelineOwner {
void _didDisposeSemanticsHandle() {
assert(_semanticsOwner != null);
if (--_outstandingSemanticsHandle == 0) {
_outstandingSemanticsHandle -= 1;
if (_outstandingSemanticsHandle == 0) {
_semanticsOwner.dispose();
_semanticsOwner = null;
if (onSemanticsOwnerDisposed != null)
......
......@@ -2583,8 +2583,8 @@ class RenderSemanticsGestureHandler extends RenderProxyBox {
/// purposes.
///
/// If this tag is used, the first "outer" semantics node is the regular node
/// of this object. The second "inner" node is introduces as a child to that
/// node. All scrollable children are now a child of the inner node, which has
/// of this object. The second "inner" node is introduced as a child to that
/// node. All scrollable children become children of the inner node, which has
/// the semantic scrolling logic enabled. All children that have been
/// excluded from scrolling with [excludeFromScrolling] are turned into
/// children of the outer node.
......@@ -3204,6 +3204,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = container;
config.explicitChildNodes = explicitChildNodes;
......
......@@ -325,7 +325,7 @@ class SemanticsProperties extends DiagnosticableTree {
/// Provides a brief textual description of the result of an action performed
/// on the widget.
///
/// If a hint is provided, there must either by an ambient [Directionality]
/// If a hint is provided, there must either be an ambient [Directionality]
/// or an explicit [textDirection] should be provided.
///
/// See also:
......@@ -889,7 +889,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
static final SemanticsConfiguration _kEmptyConfig = new SemanticsConfiguration();
/// Reconfigures the properties of this object to describe the configuration
/// provided in the `config` argument and the children listen in the
/// provided in the `config` argument and the children listed in the
/// `childrenInInversePaintOrder` argument.
///
/// The arguments may be null; this represents an empty configuration (all
......@@ -899,7 +899,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// list is used as-is and should therefore not be changed after this call.
void updateWith({
@required SemanticsConfiguration config,
@required List<SemanticsNode> childrenInInversePaintOrder,
List<SemanticsNode> childrenInInversePaintOrder,
}) {
config ??= _kEmptyConfig;
if (_isDifferentFromCurrentSemanticAnnotation(config))
......@@ -1338,7 +1338,7 @@ class SemanticsConfiguration {
/// create semantic boundaries that are either writable or not for children.
bool explicitChildNodes = false;
/// Whether the owning [RenderObject] makes other [RenderObjects] previously
/// Whether the owning [RenderObject] makes other [RenderObject]s previously
/// painted within the same semantic boundary unreachable for accessibility
/// purposes.
///
......
......@@ -5,8 +5,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
/// An event that can be send by the application to notify interested listeners
/// that something happened to the user interface (e.g. a view scrolled).
/// An event sent by the application to notify interested listeners that
/// something happened to the user interface (e.g. a view scrolled).
///
/// These events are usually interpreted by assistive technologies to give the
/// user additional clues about the current state of the UI.
......
......@@ -4922,7 +4922,7 @@ class BlockSemantics extends SingleChildRenderObjectWidget {
/// When [excluding] is true, this widget (and its subtree) is excluded from
/// the semantics tree.
///
/// This can be used to hide subwidgets that would otherwise be
/// This can be used to hide descendant widgets that would otherwise be
/// reported but that would only be confusing. For example, the
/// material library's [Chip] widget hides the avatar since it is
/// redundant with the chip label.
......
......@@ -282,8 +282,15 @@ abstract class PaintPattern {
/// arguments that are passed to this method are compared to the actual
/// [Canvas.drawParagraph] call's argument, and any mismatches result in failure.
///
/// The `offset` argument can be either an [Offset] or a [Matcher]. If it is
/// an [Offset] then the actual value must match the expected offset
/// precisely. If it is a [Matcher] then the comparison is made according to
/// the semantics of the [Matcher]. For example, [within] can be used to
/// assert that the actual offset is within a given distance from the expected
/// offset.
///
/// If no call to [Canvas.drawParagraph] was made, then this results in failure.
void paragraph({ ui.Paragraph paragraph, Offset offset });
void paragraph({ ui.Paragraph paragraph, dynamic offset });
/// Indicates that an image is expected next.
///
......@@ -626,7 +633,7 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp
}
@override
void paragraph({ ui.Paragraph paragraph, Offset offset }) {
void paragraph({ ui.Paragraph paragraph, dynamic offset }) {
_predicates.add(new _FunctionPaintPredicate(#drawParagraph, <dynamic>[paragraph, offset]));
}
......@@ -1140,8 +1147,12 @@ class _FunctionPaintPredicate extends _PaintPredicate {
for (int index = 0; index < arguments.length; index += 1) {
final dynamic actualArgument = call.current.invocation.positionalArguments[index];
final dynamic desiredArgument = arguments[index];
if (desiredArgument != null && desiredArgument != actualArgument)
if (desiredArgument is Matcher) {
expect(actualArgument, desiredArgument);
} else if (desiredArgument != null && desiredArgument != actualArgument) {
throw 'It called ${_symbolName(symbol)} with argument $index having value ${_valueName(actualArgument)} when ${_valueName(desiredArgument)} was expected.';
}
}
call.moveNext();
}
......
......@@ -299,6 +299,47 @@ class SemanticsTester {
@override
String toString() => 'SemanticsTester for ${tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode}';
Iterable<SemanticsNode> nodesWith({
String label,
String value,
TextDirection textDirection,
List<SemanticsAction> actions,
List<SemanticsFlags> flags,
}) {
bool checkNode(SemanticsNode node) {
if (label != null && node.label != label)
return false;
if (value != null && node.value != value)
return false;
if (textDirection != null && node.textDirection != textDirection)
return false;
if (actions != null) {
final int expectedActions = actions.fold(0, (int value, SemanticsAction action) => value | action.index);
final int actualActions = node.getSemanticsData().actions;
if (expectedActions != actualActions)
return false;
}
if (flags != null) {
final int expectedFlags = flags.fold(0, (int value, SemanticsFlags flag) => value | flag.index);
final int actualFlags = node.getSemanticsData().flags;
if (expectedFlags != actualFlags)
return false;
}
return true;
}
final List<SemanticsNode> result = <SemanticsNode>[];
bool visit(SemanticsNode node) {
if (checkNode(node)) {
result.add(node);
}
node.visitChildren(visit);
return true;
}
visit(tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode);
return result;
}
}
class _HasSemantics extends Matcher {
......@@ -354,41 +395,13 @@ class _IncludesNodeWith extends Matcher {
@override
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
bool result = false;
SemanticsNodeVisitor visitor;
visitor = (SemanticsNode node) {
if (checkNode(node)) {
result = true;
} else {
node.visitChildren(visitor);
}
return !result;
};
final SemanticsNode root = item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
visitor(root);
return result;
}
bool checkNode(SemanticsNode node) {
if (label != null && node.label != label)
return false;
if (value != null && node.value != value)
return false;
if (textDirection != null && node.textDirection != textDirection)
return false;
if (actions != null) {
final int expectedActions = actions.fold(0, (int value, SemanticsAction action) => value | action.index);
final int actualActions = node.getSemanticsData().actions;
if (expectedActions != actualActions)
return false;
}
if (flags != null) {
final int expectedFlags = flags.fold(0, (int value, SemanticsFlags flag) => value | flag.index);
final int actualFlags = node.getSemanticsData().flags;
if (expectedFlags != actualFlags)
return false;
}
return true;
return item.nodesWith(
label: label,
value: value,
textDirection: textDirection,
actions: actions,
flags: flags,
).isNotEmpty;
}
@override
......
......@@ -45,6 +45,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'viewLicensesButtonLabel': r'الاطّلاع على التراخيص',
'anteMeridiemAbbreviation': r'ص',
'postMeridiemAbbreviation': r'م',
'timePickerHourModeAnnouncement': r'حدد ساعات',
'timePickerMinuteModeAnnouncement': r'حدد دقائق',
},
'de': const <String, String>{
'scriptCategory': r'English-like',
......@@ -77,6 +79,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'viewLicensesButtonLabel': r'LIZENZEN ANZEIGEN',
'anteMeridiemAbbreviation': r'VORM.',
'postMeridiemAbbreviation': r'NACHM.',
'timePickerHourModeAnnouncement': r'Stunde auswählen',
'timePickerMinuteModeAnnouncement': r'Minute auswählen',
},
'de_CH': const <String, String>{
'scriptCategory': r'English-like',
......@@ -140,6 +144,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'viewLicensesButtonLabel': r'VIEW LICENSES',
'anteMeridiemAbbreviation': r'AM',
'postMeridiemAbbreviation': r'PM',
'timePickerHourModeAnnouncement': r'Select hours',
'timePickerMinuteModeAnnouncement': r'Select minutes',
},
'en_AU': const <String, String>{
'scriptCategory': r'English-like',
......@@ -389,6 +395,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'viewLicensesButtonLabel': r'VER LICENCIAS',
'anteMeridiemAbbreviation': r'A.M.',
'postMeridiemAbbreviation': r'P.M.',
'timePickerHourModeAnnouncement': r'Seleccione Horas',
'timePickerMinuteModeAnnouncement': r'Seleccione Minutos',
},
'es_US': const <String, String>{
'scriptCategory': r'English-like',
......@@ -426,6 +434,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'viewLicensesButtonLabel': r'مشاهده مجوزها',
'anteMeridiemAbbreviation': r'ق.ظ.',
'postMeridiemAbbreviation': r'ب.ظ.',
'timePickerHourModeAnnouncement': r'ساعت ها را انتخاب کنید',
'timePickerMinuteModeAnnouncement': r'دقیقه را انتخاب کنید',
},
'fr': const <String, String>{
'scriptCategory': r'English-like',
......@@ -458,6 +468,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'viewLicensesButtonLabel': r'AFFICHER LES LICENCES',
'anteMeridiemAbbreviation': r'AM',
'postMeridiemAbbreviation': r'PM',
'timePickerHourModeAnnouncement': r'Sélectionnez les heures',
'timePickerMinuteModeAnnouncement': r'Sélectionnez les minutes',
},
'fr_CA': const <String, String>{
'scriptCategory': r'English-like',
......@@ -526,6 +538,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'viewLicensesButtonLabel': r'הצגת הרישיונות',
'anteMeridiemAbbreviation': r'AM',
'postMeridiemAbbreviation': r'PM',
'timePickerHourModeAnnouncement': r'בחר שעות',
'timePickerMinuteModeAnnouncement': r'בחר דקות',
},
'it': const <String, String>{
'scriptCategory': r'English-like',
......@@ -557,6 +571,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'viewLicensesButtonLabel': r'VISUALIZZA LICENZE',
'anteMeridiemAbbreviation': r'AM',
'postMeridiemAbbreviation': r'PM',
'timePickerHourModeAnnouncement': r'Seleziona ore',
'timePickerMinuteModeAnnouncement': r'Seleziona minuti',
},
'ja': const <String, String>{
'scriptCategory': r'dense',
......@@ -588,6 +604,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'viewLicensesButtonLabel': r'ライセンスを表示',
'anteMeridiemAbbreviation': r'AM',
'postMeridiemAbbreviation': r'PM',
'timePickerHourModeAnnouncement': r'時を選択',
'timePickerMinuteModeAnnouncement': r'分を選択',
},
'ps': const <String, String>{
'scriptCategory': r'tall',
......@@ -616,6 +634,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'pasteButtonLabel': r'پیټ کړئ',
'selectAllButtonLabel': r'غوره کړئ',
'viewLicensesButtonLabel': r'لیدلس وګورئ',
'timePickerHourModeAnnouncement': r'وختونه وټاکئ',
'timePickerMinuteModeAnnouncement': r'منې غوره کړئ',
},
'pt': const <String, String>{
'scriptCategory': r'English-like',
......@@ -644,6 +664,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'pasteButtonLabel': r'COLAR',
'selectAllButtonLabel': r'SELECIONAR TUDO',
'viewLicensesButtonLabel': r'VER LICENÇAS',
'timePickerHourModeAnnouncement': r'Selecione horários',
'timePickerMinuteModeAnnouncement': r'Selecione Minutos',
},
'pt_PT': const <String, String>{
'scriptCategory': r'English-like',
......@@ -709,6 +731,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'viewLicensesButtonLabel': r'ЛИЦЕНЗИИ',
'anteMeridiemAbbreviation': r'АМ',
'postMeridiemAbbreviation': r'PM',
'timePickerHourModeAnnouncement': r'ВЫБРАТЬ ЧАСЫ',
'timePickerMinuteModeAnnouncement': r'ВЫБРАТЬ МИНУТЫ',
},
'ur': const <String, String>{
'scriptCategory': r'tall',
......@@ -740,6 +764,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'viewLicensesButtonLabel': r'لائسنسز دیکھیں',
'anteMeridiemAbbreviation': r'AM',
'postMeridiemAbbreviation': r'PM',
'timePickerHourModeAnnouncement': r'گھنٹے منتخب کریں',
'timePickerMinuteModeAnnouncement': r'منٹ منتخب کریں',
},
'zh': const <String, String>{
'scriptCategory': r'dense',
......@@ -771,6 +797,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
'previousMonthTooltip': r'上个月',
'anteMeridiemAbbreviation': r'上午',
'postMeridiemAbbreviation': r'下午',
'timePickerHourModeAnnouncement': r'选择小时',
'timePickerMinuteModeAnnouncement': r'选择分钟',
},
};
......@@ -31,5 +31,7 @@
"selectAllButtonLabel": "اختيار الكل",
"viewLicensesButtonLabel": "الاطّلاع على التراخيص",
"anteMeridiemAbbreviation": "ص",
"postMeridiemAbbreviation": "م"
"postMeridiemAbbreviation": "م",
"timePickerHourModeAnnouncement": "حدد ساعات",
"timePickerMinuteModeAnnouncement": "حدد دقائق"
}
......@@ -28,5 +28,7 @@
"selectAllButtonLabel": "ALLE AUSWÄHLEN",
"viewLicensesButtonLabel": "LIZENZEN ANZEIGEN",
"anteMeridiemAbbreviation": "VORM.",
"postMeridiemAbbreviation": "NACHM."
"postMeridiemAbbreviation": "NACHM.",
"timePickerHourModeAnnouncement": "Stunde auswählen",
"timePickerMinuteModeAnnouncement": "Minute auswählen"
}
......@@ -143,5 +143,15 @@
"postMeridiemAbbreviation": "PM",
"@postMeridiemAbbreviation": {
"description": "The abbreviation for post meridiem (after noon) shown in the time picker. Translations for this abbreviation will only be provided for locales that support it."
},
"timePickerHourModeAnnouncement": "Select hours",
"@timePickerHourModeAnnouncement": {
"description": "The audio announcement made when the time picker dialog is set to hour mode."
},
"timePickerMinuteModeAnnouncement": "Select minutes",
"@timePickerMinuteModeAnnouncement": {
"description": "The audio announcement made when the time picker dialog is set to minute mode."
}
}
......@@ -28,5 +28,7 @@
"selectAllButtonLabel": "SELECCIONAR TODO",
"viewLicensesButtonLabel": "VER LICENCIAS",
"anteMeridiemAbbreviation": "A.M.",
"postMeridiemAbbreviation": "P.M."
"postMeridiemAbbreviation": "P.M.",
"timePickerHourModeAnnouncement": "Seleccione Horas",
"timePickerMinuteModeAnnouncement": "Seleccione Minutos"
}
......@@ -27,5 +27,7 @@
"selectAllButtonLabel": "انتخاب همه",
"viewLicensesButtonLabel": "مشاهده مجوزها",
"anteMeridiemAbbreviation": "ق.ظ.",
"postMeridiemAbbreviation": "ب.ظ."
"postMeridiemAbbreviation": "ب.ظ.",
"timePickerHourModeAnnouncement": "ساعت ها را انتخاب کنید",
"timePickerMinuteModeAnnouncement": "دقیقه را انتخاب کنید"
}
......@@ -28,5 +28,7 @@
"selectAllButtonLabel": "TOUT SÉLECTIONNER",
"viewLicensesButtonLabel": "AFFICHER LES LICENCES",
"anteMeridiemAbbreviation": "AM",
"postMeridiemAbbreviation": "PM"
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "Sélectionnez les heures",
"timePickerMinuteModeAnnouncement": "Sélectionnez les minutes"
}
......@@ -29,5 +29,7 @@
"selectAllButtonLabel": "בחירת הכול",
"viewLicensesButtonLabel": "הצגת הרישיונות",
"anteMeridiemAbbreviation": "AM",
"postMeridiemAbbreviation": "PM"
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "בחר שעות",
"timePickerMinuteModeAnnouncement": "בחר דקות"
}
......@@ -27,5 +27,7 @@
"selectAllButtonLabel": "SELEZIONA TUTTO",
"viewLicensesButtonLabel": "VISUALIZZA LICENZE",
"anteMeridiemAbbreviation": "AM",
"postMeridiemAbbreviation": "PM"
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "Seleziona ore",
"timePickerMinuteModeAnnouncement": "Seleziona minuti"
}
......@@ -27,5 +27,7 @@
"selectAllButtonLabel": "すべて選択",
"viewLicensesButtonLabel": "ライセンスを表示",
"anteMeridiemAbbreviation": "AM",
"postMeridiemAbbreviation": "PM"
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "時を選択",
"timePickerMinuteModeAnnouncement": "分を選択"
}
......@@ -26,5 +26,7 @@
"okButtonLabel": "سمه ده",
"pasteButtonLabel": "پیټ کړئ",
"selectAllButtonLabel": "غوره کړئ",
"viewLicensesButtonLabel": "لیدلس وګورئ"
"viewLicensesButtonLabel": "لیدلس وګورئ",
"timePickerHourModeAnnouncement": "وختونه وټاکئ",
"timePickerMinuteModeAnnouncement": "منې غوره کړئ"
}
......@@ -26,5 +26,7 @@
"okButtonLabel": "OK",
"pasteButtonLabel": "COLAR",
"selectAllButtonLabel": "SELECIONAR TUDO",
"viewLicensesButtonLabel": "VER LICENÇAS"
"viewLicensesButtonLabel": "VER LICENÇAS",
"timePickerHourModeAnnouncement": "Selecione horários",
"timePickerMinuteModeAnnouncement": "Selecione Minutos"
}
......@@ -30,5 +30,7 @@
"selectAllButtonLabel": "ВЫБРАТЬ ВСЕ",
"viewLicensesButtonLabel": "ЛИЦЕНЗИИ",
"anteMeridiemAbbreviation": "АМ",
"postMeridiemAbbreviation": "PM"
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "ВЫБРАТЬ ЧАСЫ",
"timePickerMinuteModeAnnouncement": "ВЫБРАТЬ МИНУТЫ"
}
......@@ -27,5 +27,7 @@
"selectAllButtonLabel": "سبھی منتخب کریں",
"viewLicensesButtonLabel": "لائسنسز دیکھیں",
"anteMeridiemAbbreviation": "AM",
"postMeridiemAbbreviation": "PM"
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "گھنٹے منتخب کریں",
"timePickerMinuteModeAnnouncement": "منٹ منتخب کریں"
}
......@@ -27,5 +27,7 @@
"nextMonthTooltip": "下个月",
"previousMonthTooltip": "上个月",
"anteMeridiemAbbreviation": "上午",
"postMeridiemAbbreviation": "下午"
"postMeridiemAbbreviation": "下午",
"timePickerHourModeAnnouncement": "选择小时",
"timePickerMinuteModeAnnouncement": "选择分钟"
}
......@@ -316,6 +316,12 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
@override
String get postMeridiemAbbreviation => _nameToValue['postMeridiemAbbreviation'];
@override
String get timePickerHourModeAnnouncement => _nameToValue['timePickerHourModeAnnouncement'];
@override
String get timePickerMinuteModeAnnouncement => _nameToValue['timePickerMinuteModeAnnouncement'];
/// The [TimeOfDayFormat] corresponding to one of the following supported
/// patterns:
///
......
......@@ -140,57 +140,58 @@ void main() {
],
child: new MediaQuery(
data: new MediaQueryData(alwaysUse24HourFormat: alwaysUse24HourFormat),
child: new Directionality(
textDirection: TextDirection.ltr,
child: new Navigator(
onGenerateRoute: (RouteSettings settings) {
return new MaterialPageRoute<dynamic>(builder: (BuildContext context) {
showTimePicker(context: context, initialTime: const TimeOfDay(hour: 7, minute: 0));
return new Container();
});
},
child: new Material(
child: new Directionality(
textDirection: TextDirection.ltr,
child: new Navigator(
onGenerateRoute: (RouteSettings settings) {
return new MaterialPageRoute<dynamic>(builder: (BuildContext context) {
return new FlatButton(
onPressed: () {
showTimePicker(context: context, initialTime: const TimeOfDay(hour: 7, minute: 0));
},
child: const Text('X'),
);
});
},
),
),
),
),
),
);
// Pump once, because the dialog shows up asynchronously.
await tester.pump();
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
}
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, false);
final CustomPaint dialPaint = tester.widget(find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'),
matching: find.byType(CustomPaint),
));
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
final dynamic dialPainter = dialPaint.painter;
final List<TextPainter> primaryOuterLabels = dialPainter.primaryOuterLabels;
expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels12To11);
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels;
expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text), labels12To11);
expect(dialPainter.primaryInnerLabels, null);
final List<TextPainter> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
expect(secondaryOuterLabels.map((TextPainter tp) => tp.text.text), labels12To11);
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text), labels12To11);
expect(dialPainter.secondaryInnerLabels, null);
});
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, true);
final CustomPaint dialPaint = tester.widget(find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'),
matching: find.byType(CustomPaint),
));
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
final dynamic dialPainter = dialPaint.painter;
final List<TextPainter> primaryOuterLabels = dialPainter.primaryOuterLabels;
expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23);
final List<TextPainter> primaryInnerLabels = dialPainter.primaryInnerLabels;
expect(primaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit);
final List<TextPainter> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
expect(secondaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23);
final List<TextPainter> secondaryInnerLabels = dialPainter.secondaryInnerLabels;
expect(secondaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit);
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels;
expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text), labels00To23);
final List<dynamic> primaryInnerLabels = dialPainter.primaryInnerLabels;
expect(primaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text), labels12To11TwoDigit);
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text), labels00To23);
final List<dynamic> secondaryInnerLabels = dialPainter.secondaryInnerLabels;
expect(secondaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text), labels12To11TwoDigit);
});
}
......@@ -597,6 +597,7 @@ const Map<Type, DistanceFunction<dynamic>> _kStandardDistanceFunctions = const <
Offset: _offsetDistance,
int: _intDistance,
double: _doubleDistance,
Rect: _rectDistance,
};
int _intDistance(int a, int b) => (b - a).abs();
......@@ -610,6 +611,13 @@ double _maxComponentColorDistance(Color a, Color b) {
return delta.toDouble();
}
double _rectDistance(Rect a, Rect b) {
double delta = math.max<double>((a.left - b.left).abs(), (a.top - b.top).abs());
delta = math.max<double>(delta, (a.right - b.right).abs());
delta = math.max<double>(delta, (a.bottom - b.bottom).abs());
return delta;
}
/// Asserts that two values are within a certain distance from each other.
///
/// The distance is computed by a [DistanceFunction].
......@@ -669,11 +677,23 @@ class _IsWithinDistance<T> extends Matcher {
'double value, but it returned $distance.'
);
}
matchState['distance'] = distance;
return distance <= epsilon;
}
@override
Description describe(Description description) => description.add('$value$epsilon)');
@override
Description describeMismatch(
Object object,
Description mismatchDescription,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
mismatchDescription.add('was ${matchState['distance']} away from the desired value.');
return mismatchDescription;
}
}
class _MoreOrLessEquals extends Matcher {
......
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