diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index fd2a1bd0dc1ba..215cb74d7b842 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -400,26 +400,33 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta Widget _buildError() { assert(widget.error != null || widget.errorText != null); - return Semantics( - container: true, - child: FadeTransition( - opacity: _controller, - child: FractionalTranslation( - translation: Tween( - begin: const Offset(0.0, -0.25), - end: Offset.zero, - ).evaluate(_controller.view), - child: - widget.error ?? - Text( - widget.errorText!, - style: widget.errorStyle, - textAlign: widget.textAlign, - overflow: TextOverflow.ellipsis, - maxLines: widget.errorMaxLines, - ), - ), - ), + final Widget? capturedError = widget.error; + final String? capturedErrorText = widget.errorText; + return Builder( + builder: (BuildContext context) { + return Semantics( + container: true, + liveRegion: !MediaQuery.supportsAnnounceOf(context), + child: FadeTransition( + opacity: _controller, + child: FractionalTranslation( + translation: Tween( + begin: const Offset(0.0, -0.25), + end: Offset.zero, + ).evaluate(_controller.view), + child: + capturedError ?? + Text( + capturedErrorText!, + style: widget.errorStyle, + textAlign: widget.textAlign, + overflow: TextOverflow.ellipsis, + maxLines: widget.errorMaxLines, + ), + ), + ), + ); + }, ); } @@ -3898,6 +3905,7 @@ class InputDecoration { bool? alignLabelWithHint, BoxConstraints? constraints, VisualDensity? visualDensity, + SemanticsService? semanticsService, }) { return InputDecoration( icon: icon ?? this.icon, diff --git a/packages/flutter/lib/src/semantics/semantics_service.dart b/packages/flutter/lib/src/semantics/semantics_service.dart index 7086ee5716592..750e887e39758 100644 --- a/packages/flutter/lib/src/semantics/semantics_service.dart +++ b/packages/flutter/lib/src/semantics/semantics_service.dart @@ -7,7 +7,6 @@ library; import 'dart:ui' show TextDirection; -import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show SystemChannels; import 'semantics_event.dart' show AnnounceSemanticsEvent, Assertiveness, TooltipSemanticsEvent; @@ -34,8 +33,8 @@ abstract final class SemanticsService { /// Currently, this is only supported by the web engine and has no effect on /// other platforms. The default mode is [Assertiveness.polite]. /// - /// Not all platforms support announcements. Check to see if - /// [isAnnounceSupported] before calling this method. + /// Not all platforms support announcements. Check to see if it is supported using + /// [MediaQuery.supportsAnnounceOf] before calling this method. /// /// ### Android /// Android has [deprecated announcement events][1] due to its disruptive @@ -66,12 +65,4 @@ abstract final class SemanticsService { final TooltipSemanticsEvent event = TooltipSemanticsEvent(message); await SystemChannels.accessibility.send(event.toMap()); } - - /// Checks if announce is supported on the given platform. - /// - /// On Android the announce method is deprecated, therefore will return false. - /// On other platforms, this will return true. - static bool isAnnounceSupported() { - return defaultTargetPlatform != TargetPlatform.android; - } } diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index 7a8a29be5e1d8..55cac4d592f8c 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -16,6 +16,7 @@ import 'binding.dart'; import 'focus_manager.dart'; import 'focus_scope.dart'; import 'framework.dart'; +import 'media_query.dart'; import 'navigator.dart'; import 'pop_scope.dart'; import 'restoration.dart'; @@ -369,7 +370,7 @@ class FormState extends State
{ } } - if (errorMessage.isNotEmpty) { + if (errorMessage.isNotEmpty && MediaQuery.supportsAnnounceOf(context)) { final TextDirection directionality = Directionality.of(context); if (defaultTargetPlatform == TargetPlatform.iOS) { unawaited( diff --git a/packages/flutter/lib/src/widgets/media_query.dart b/packages/flutter/lib/src/widgets/media_query.dart index 042b6af458d21..a4a2c667fc658 100644 --- a/packages/flutter/lib/src/widgets/media_query.dart +++ b/packages/flutter/lib/src/widgets/media_query.dart @@ -103,6 +103,9 @@ enum _MediaQueryAspect { /// Specifies the aspect corresponding to [MediaQueryData.boldText]. boldText, + /// Specifies the aspect corresponding to [MediaQueryData.supportsAnnounce]. + supportsAnnounce, + /// Specifies the aspect corresponding to [MediaQueryData.navigationMode]. navigationMode, @@ -210,6 +213,7 @@ class MediaQueryData { this.onOffSwitchLabels = false, this.disableAnimations = false, this.boldText = false, + this.supportsAnnounce = false, this.navigationMode = NavigationMode.traditional, this.gestureSettings = const DeviceGestureSettings(touchSlop: kTouchSlop), this.displayFeatures = const [], @@ -295,6 +299,9 @@ class MediaQueryData { platformData?.disableAnimations ?? view.platformDispatcher.accessibilityFeatures.disableAnimations, boldText = platformData?.boldText ?? view.platformDispatcher.accessibilityFeatures.boldText, + supportsAnnounce = + platformData?.supportsAnnounce ?? + view.platformDispatcher.accessibilityFeatures.supportsAnnounce, highContrast = platformData?.highContrast ?? view.platformDispatcher.accessibilityFeatures.highContrast, onOffSwitchLabels = @@ -584,6 +591,21 @@ class MediaQueryData { /// originates. final bool boldText; + /// Whether accessibility announcements (like [SemanticsService.announce]) + /// are supported on the current platform. + /// + /// Returns `false` on platforms where announcements are deprecated or + /// unsupported by the underlying platform. + /// + /// Returns `true` on platforms where such announcements are + /// generally supported without discouragement. (iOS, web etc) + /// + /// See also: + /// + /// * [dart:ui.PlatformDispatcher.accessibilityFeatures], where the setting + /// originates. + final bool supportsAnnounce; + /// Describes the navigation mode requested by the platform. /// /// Some user interfaces are better navigated using a directional pad (DPAD) @@ -665,6 +687,7 @@ class MediaQueryData { bool? invertColors, bool? accessibleNavigation, bool? boldText, + bool? supportsAnnounce, NavigationMode? navigationMode, DeviceGestureSettings? gestureSettings, List? displayFeatures, @@ -690,6 +713,7 @@ class MediaQueryData { disableAnimations: disableAnimations ?? this.disableAnimations, accessibleNavigation: accessibleNavigation ?? this.accessibleNavigation, boldText: boldText ?? this.boldText, + supportsAnnounce: supportsAnnounce ?? this.supportsAnnounce, navigationMode: navigationMode ?? this.navigationMode, gestureSettings: gestureSettings ?? this.gestureSettings, displayFeatures: displayFeatures ?? this.displayFeatures, @@ -892,6 +916,7 @@ class MediaQueryData { other.invertColors == invertColors && other.accessibleNavigation == accessibleNavigation && other.boldText == boldText && + other.supportsAnnounce == supportsAnnounce && other.navigationMode == navigationMode && other.gestureSettings == gestureSettings && listEquals(other.displayFeatures, displayFeatures) && @@ -1713,6 +1738,30 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { static bool? maybeBoldTextOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.boldText)?.boldText; + /// Returns the [MediaQueryData.supportsAnnounce] accessibility setting for the + /// nearest [MediaQuery] ancestor or false, if no such ancestor exists. + /// + /// Use of this method will cause the given [context] to rebuild any time that + /// the [MediaQueryData.supportsAnnounce] property of the ancestor [MediaQuery] + /// changes. This is especially important for supportsAnnounce because supportsAnnounce has a + /// low frequency change rate. The performance difference between rebuilding + /// for all media query data changes and only rebuilding for supportsAnnounce is a + /// dramatic difference. + /// + /// {@macro flutter.widgets.media_query.MediaQuery.dontUseOf} + static bool supportsAnnounceOf(BuildContext context) => maybeSupportsAnnounceOf(context) ?? false; + + /// Returns the [MediaQueryData.supportsAnnounce] accessibility setting for the + /// nearest [MediaQuery] ancestor or null, if no such ancestor exists. + /// + /// Use of this method will cause the given [context] to rebuild any time that + /// the [MediaQueryData.supportsAnnounce] property of the ancestor [MediaQuery] + /// changes. + /// + /// {@macro flutter.widgets.media_query.MediaQuery.dontUseMaybeOf} + static bool? maybeSupportsAnnounceOf(BuildContext context) => + _maybeOf(context, _MediaQueryAspect.supportsAnnounce)?.supportsAnnounce; + /// Returns [MediaQueryData.navigationMode] for the nearest [MediaQuery] /// ancestor or throws an exception, if no such ancestor exists. /// @@ -1843,6 +1892,8 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { _MediaQueryAspect.disableAnimations => data.disableAnimations != oldWidget.data.disableAnimations, _MediaQueryAspect.boldText => data.boldText != oldWidget.data.boldText, + _MediaQueryAspect.supportsAnnounce => + data.supportsAnnounce != oldWidget.data.supportsAnnounce, _MediaQueryAspect.navigationMode => data.navigationMode != oldWidget.data.navigationMode, _MediaQueryAspect.gestureSettings => diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 7f1a225b66301..0eef4590d0faa 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -14899,4 +14899,41 @@ void main() { final RenderBox box = tester.renderObject(find.byType(InputDecorator)); expect(box, isNot(paints..drrect())); }); + + testWidgets( + 'InputDecorator _buildError with errorText correctly updates on BuildContext updates', + (WidgetTester tester) async { + final ValueNotifier errorTextNotifier = ValueNotifier('initial error'); + const String helperTextValue = 'helper text'; + + addTearDown(errorTextNotifier.dispose); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ValueListenableBuilder( + key: const Key('value_listenable_builder_parent'), + valueListenable: errorTextNotifier, + builder: (BuildContext context, String? value, Widget? child) { + return buildInputDecorator( + decoration: InputDecoration(errorText: value, helperText: helperTextValue), + ); + }, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('initial error'), findsOneWidget); + expect(find.text(helperTextValue), findsNothing); + + errorTextNotifier.value = null; + + await tester.pumpAndSettle(); + + expect(find.text('initial error'), findsNothing); + expect(find.text(helperTextValue), findsOneWidget); + }, + ); } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index aa553dfe6d053..0386113e2787b 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -9247,55 +9247,67 @@ void main() { semantics.dispose(); }); - testWidgets('InputDecoration errorText semantics', (WidgetTester tester) async { - final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = _textEditingController(); - final Key key = UniqueKey(); + for (final bool supportsAnnounce in [true, false]) { + testWidgets('InputDecoration errorText semantics (supportsAnnounce=$supportsAnnounce)', ( + WidgetTester tester, + ) async { + final SemanticsTester semantics = SemanticsTester(tester); + final TextEditingController controller = _textEditingController(); + final Key key = UniqueKey(); - await tester.pumpWidget( - overlay( - child: TextField( - key: key, - controller: controller, - decoration: const InputDecoration( - labelText: 'label', - hintText: 'hint', - errorText: 'oh no!', + await tester.pumpWidget( + overlay( + child: MediaQuery( + data: MediaQueryData(supportsAnnounce: supportsAnnounce), + child: TextField( + key: key, + controller: controller, + decoration: const InputDecoration( + labelText: 'label', + hintText: 'hint', + errorText: 'oh no!', + ), + ), ), ), - ), - ); + ); - expect( - semantics, - hasSemantics( - TestSemantics.root( - children: [ - TestSemantics.rootChild( - label: 'label', - textDirection: TextDirection.ltr, - actions: [SemanticsAction.tap, SemanticsAction.focus], - flags: [ - SemanticsFlag.isTextField, - SemanticsFlag.hasEnabledState, - SemanticsFlag.isEnabled, - ], - inputType: ui.SemanticsInputType.text, - currentValueLength: 0, - children: [ - TestSemantics(label: 'oh no!', textDirection: TextDirection.ltr), - ], - ), - ], + expect( + semantics, + hasSemantics( + TestSemantics.root( + children: [ + TestSemantics.rootChild( + label: 'label', + textDirection: TextDirection.ltr, + actions: [SemanticsAction.tap, SemanticsAction.focus], + flags: [ + SemanticsFlag.isTextField, + SemanticsFlag.hasEnabledState, + SemanticsFlag.isEnabled, + ], + inputType: ui.SemanticsInputType.text, + currentValueLength: 0, + children: [ + TestSemantics( + label: 'oh no!', + textDirection: TextDirection.ltr, + flags: [if (!supportsAnnounce) SemanticsFlag.isLiveRegion], + ), + ], + ), + ], + ), + ignoreTransform: true, + ignoreRect: true, + ignoreId: true, ), - ignoreTransform: true, - ignoreRect: true, - ignoreId: true, - ), - ); + ); - semantics.dispose(); - }); + semantics.dispose(); + debugDefaultTargetPlatformOverride = null; + }); + } testWidgets('floating label does not overlap with value at large textScaleFactors', ( WidgetTester tester, diff --git a/packages/flutter/test/semantics/semantics_service_test.dart b/packages/flutter/test/semantics/semantics_service_test.dart index 59c8fab6ad896..f741f316e5f1c 100644 --- a/packages/flutter/test/semantics/semantics_service_test.dart +++ b/packages/flutter/test/semantics/semantics_service_test.dart @@ -2,9 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:flutter/semantics.dart'; -import 'package:flutter/services.dart' show SystemChannels, TargetPlatform; +import 'package:flutter/services.dart' show SystemChannels; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -45,12 +44,4 @@ void main() { ]), ); }); - - for (final TargetPlatform platform in TargetPlatform.values) { - test('Announce not supported on Android. (platform=$platform)', () { - debugDefaultTargetPlatformOverride = platform; - expect(SemanticsService.isAnnounceSupported(), platform != TargetPlatform.android); - debugDefaultTargetPlatformOverride = null; - }); - } } diff --git a/packages/flutter/test/widgets/form_test.dart b/packages/flutter/test/widgets/form_test.dart index cedc183c4b7bb..cf5e6e9c80d29 100644 --- a/packages/flutter/test/widgets/form_test.dart +++ b/packages/flutter/test/widgets/form_test.dart @@ -169,50 +169,67 @@ void main() { await checkErrorText(''); }); - testWidgets('Should announce only the first error message when validate returns errors', ( - WidgetTester tester, - ) async { - final GlobalKey formKey = GlobalKey(); - await tester.pumpWidget( - MaterialApp( - home: MediaQuery( - data: const MediaQueryData(), - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: Material( - child: Form( - key: formKey, - child: Column( - children: [ - TextFormField(validator: (_) => 'First error message'), - TextFormField(validator: (_) => 'Second error message'), - ], + for (final _PlatformAnnounceScenario test in <_PlatformAnnounceScenario>[ + _PlatformAnnounceScenario( + supportsAnnounce: false, + testName: + 'Should announce only the first error message when validate returns errors and announce = false', + ), + _PlatformAnnounceScenario( + supportsAnnounce: true, + testName: + 'Should not announce error message when validate returns errors and announce = true', + ), + ]) { + testWidgets(test.testName, (WidgetTester tester) async { + final GlobalKey formKey = GlobalKey(); + await tester.pumpWidget( + MaterialApp( + home: MediaQuery( + data: MediaQueryData(supportsAnnounce: test.supportsAnnounce), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: Form( + key: formKey, + child: Column( + children: [ + TextFormField(validator: (_) => 'First error message'), + TextFormField(validator: (_) => 'Second error message'), + ], + ), ), ), ), ), ), ), - ), - ); - formKey.currentState!.reset(); - await tester.enterText(find.byType(TextFormField).first, ''); - await tester.pump(); - - // Manually validate. - expect(find.text('First error message'), findsNothing); - expect(find.text('Second error message'), findsNothing); - formKey.currentState!.validate(); - await tester.pump(); - expect(find.text('First error message'), findsOneWidget); - expect(find.text('Second error message'), findsOneWidget); + ); + formKey.currentState!.reset(); + await tester.enterText(find.byType(TextFormField).first, ''); + await tester.pump(); - final CapturedAccessibilityAnnouncement announcement = tester.takeAnnouncements().single; - expect(announcement.message, 'First error message'); - expect(announcement.textDirection, TextDirection.ltr); - expect(announcement.assertiveness, Assertiveness.assertive); - }); + // Manually validate. + expect(find.text('First error message'), findsNothing); + expect(find.text('Second error message'), findsNothing); + formKey.currentState!.validate(); + await tester.pump(); + expect(find.text('First error message'), findsOneWidget); + expect(find.text('Second error message'), findsOneWidget); + + if (test.supportsAnnounce) { + final CapturedAccessibilityAnnouncement announcement = tester.takeAnnouncements().single; + expect(announcement.message, 'First error message'); + expect(announcement.textDirection, TextDirection.ltr); + expect(announcement.assertiveness, Assertiveness.assertive); + } else { + final CapturedAccessibilityAnnouncement? announcement = + tester.takeAnnouncements().firstOrNull; + expect(announcement, null); + } + }); + } testWidgets('isValid returns true when a field is valid', (WidgetTester tester) async { final GlobalKey> fieldKey1 = GlobalKey>(); @@ -388,7 +405,7 @@ void main() { Widget builder() { return MaterialApp( home: MediaQuery( - data: const MediaQueryData(), + data: const MediaQueryData(supportsAnnounce: true), child: Directionality( textDirection: TextDirection.ltr, child: Center( @@ -1615,3 +1632,9 @@ void main() { ); }); } + +class _PlatformAnnounceScenario { + _PlatformAnnounceScenario({required this.supportsAnnounce, required this.testName}); + final bool supportsAnnounce; + final String testName; +} diff --git a/packages/flutter/test/widgets/media_query_test.dart b/packages/flutter/test/widgets/media_query_test.dart index 6f671c3841bb2..1d98a4bfc64e7 100644 --- a/packages/flutter/test/widgets/media_query_test.dart +++ b/packages/flutter/test/widgets/media_query_test.dart @@ -144,6 +144,7 @@ void main() { }); testWidgets('MediaQueryData.fromView is sane', (WidgetTester tester) async { + tester.platformDispatcher.accessibilityFeaturesTestValue = const FakeAccessibilityFeatures(); final MediaQueryData data = MediaQueryData.fromView(tester.view); expect(data, hasOneLineDescription); expect(data.hashCode, equals(data.copyWith().hashCode)); @@ -154,6 +155,7 @@ void main() { expect(data.boldText, false); expect(data.highContrast, false); expect(data.onOffSwitchLabels, false); + expect(data.supportsAnnounce, false); expect(data.platformBrightness, Brightness.light); expect(data.gestureSettings.touchSlop, null); expect(data.displayFeatures, isEmpty); @@ -169,6 +171,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + supportsAnnounce: true, alwaysUse24HourFormat: true, navigationMode: NavigationMode.directional, ); @@ -202,6 +205,7 @@ void main() { expect(data.boldText, platformData.boldText); expect(data.highContrast, platformData.highContrast); expect(data.onOffSwitchLabels, platformData.onOffSwitchLabels); + expect(data.supportsAnnounce, platformData.supportsAnnounce); expect(data.alwaysUse24HourFormat, platformData.alwaysUse24HourFormat); expect(data.navigationMode, platformData.navigationMode); expect(data.gestureSettings, DeviceGestureSettings.fromView(tester.view)); @@ -255,6 +259,10 @@ void main() { data.onOffSwitchLabels, tester.platformDispatcher.accessibilityFeatures.onOffSwitchLabels, ); + expect( + data.supportsAnnounce, + tester.platformDispatcher.accessibilityFeatures.supportsAnnounce, + ); expect(data.alwaysUse24HourFormat, tester.platformDispatcher.alwaysUse24HourFormat); expect(data.navigationMode, NavigationMode.traditional); expect(data.gestureSettings, DeviceGestureSettings.fromView(tester.view)); @@ -274,6 +282,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + supportsAnnounce: true, alwaysUse24HourFormat: true, navigationMode: NavigationMode.directional, ); @@ -321,6 +330,7 @@ void main() { expect(data.boldText, platformData.boldText); expect(data.highContrast, platformData.highContrast); expect(data.onOffSwitchLabels, platformData.onOffSwitchLabels); + expect(data.supportsAnnounce, platformData.supportsAnnounce); expect(data.alwaysUse24HourFormat, platformData.alwaysUse24HourFormat); expect(data.navigationMode, platformData.navigationMode); expect(data.gestureSettings, DeviceGestureSettings.fromView(tester.view)); @@ -393,6 +403,10 @@ void main() { data.onOffSwitchLabels, tester.platformDispatcher.accessibilityFeatures.onOffSwitchLabels, ); + expect( + data.supportsAnnounce, + tester.platformDispatcher.accessibilityFeatures.supportsAnnounce, + ); expect(data.alwaysUse24HourFormat, tester.platformDispatcher.alwaysUse24HourFormat); expect(data.navigationMode, NavigationMode.traditional); expect(data.gestureSettings, DeviceGestureSettings.fromView(tester.view)); @@ -577,6 +591,7 @@ void main() { expect(copied.boldText, data.boldText); expect(copied.highContrast, data.highContrast); expect(copied.onOffSwitchLabels, data.onOffSwitchLabels); + expect(copied.supportsAnnounce, data.supportsAnnounce); expect(copied.platformBrightness, data.platformBrightness); expect(copied.gestureSettings, data.gestureSettings); expect(copied.displayFeatures, data.displayFeatures); @@ -617,6 +632,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + supportsAnnounce: true, platformBrightness: Brightness.dark, navigationMode: NavigationMode.directional, gestureSettings: gestureSettings, @@ -636,6 +652,7 @@ void main() { expect(copied.boldText, true); expect(copied.highContrast, true); expect(copied.onOffSwitchLabels, true); + expect(copied.supportsAnnounce, true); expect(copied.platformBrightness, Brightness.dark); expect(copied.navigationMode, NavigationMode.directional); expect(copied.gestureSettings, gestureSettings); @@ -674,6 +691,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + supportsAnnounce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -710,6 +728,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); + expect(unpadded.supportsAnnounce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -748,6 +767,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + supportsAnnounce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -781,6 +801,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); + expect(unpadded.supportsAnnounce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -819,6 +840,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + supportsAnnounce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -855,6 +877,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); + expect(unpadded.supportsAnnounce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -893,6 +916,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + supportsAnnounce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -926,6 +950,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); + expect(unpadded.supportsAnnounce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -964,6 +989,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + supportsAnnounce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -1000,6 +1026,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); + expect(unpadded.supportsAnnounce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -1038,6 +1065,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + supportsAnnounce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -1071,6 +1099,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); + expect(unpadded.supportsAnnounce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -1175,6 +1204,32 @@ void main() { expect(insideOnOffSwitchLabels, true); }); + testWidgets('MediaQuery.supportsAnnounce', (WidgetTester tester) async { + late bool outsideSupportsAnnounce; + late bool insideSupportsAnnounce; + + tester.platformDispatcher.accessibilityFeaturesTestValue = const FakeAccessibilityFeatures(); + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + outsideSupportsAnnounce = MediaQuery.supportsAnnounceOf(context); + return MediaQuery( + data: const MediaQueryData(supportsAnnounce: true), + child: Builder( + builder: (BuildContext context) { + insideSupportsAnnounce = MediaQuery.supportsAnnounceOf(context); + return Container(); + }, + ), + ); + }, + ), + ); + + expect(outsideSupportsAnnounce, false); + expect(insideSupportsAnnounce, true); + }); + testWidgets('MediaQuery.boldTextOf', (WidgetTester tester) async { late bool outsideBoldTextOverride; late bool insideBoldTextOverride; @@ -1314,6 +1369,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + supportsAnnounce: true, displayFeatures: displayFeatures, ), child: Builder( @@ -1345,6 +1401,7 @@ void main() { expect(subScreenMediaQuery.boldText, true); expect(subScreenMediaQuery.highContrast, true); expect(subScreenMediaQuery.onOffSwitchLabels, true); + expect(subScreenMediaQuery.supportsAnnounce, true); expect(subScreenMediaQuery.displayFeatures, isEmpty); }); @@ -1391,6 +1448,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + supportsAnnounce: true, displayFeatures: displayFeatures, ), child: Builder( @@ -1428,6 +1486,7 @@ void main() { expect(subScreenMediaQuery.boldText, true); expect(subScreenMediaQuery.highContrast, true); expect(subScreenMediaQuery.onOffSwitchLabels, true); + expect(subScreenMediaQuery.supportsAnnounce, true); expect(subScreenMediaQuery.displayFeatures, [cutoutDisplayFeature]); }); @@ -1692,6 +1751,14 @@ void main() { MediaQuery.maybeOnOffSwitchLabelsOf, MediaQueryData(onOffSwitchLabels: true), ), + const _MediaQueryAspectCase( + MediaQuery.supportsAnnounceOf, + MediaQueryData(supportsAnnounce: true), + ), + const _MediaQueryAspectCase( + MediaQuery.maybeSupportsAnnounceOf, + MediaQueryData(supportsAnnounce: true), + ), const _MediaQueryAspectCase( MediaQuery.disableAnimationsOf, MediaQueryData(disableAnimations: true),