From 9bd8d5689bd5a8259962b1c6ea7a9a1faa71ecd6 Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Wed, 14 May 2025 15:10:13 -0700 Subject: [PATCH 01/16] Add back "Use live region in error text input decorator for Android #165531" --- engine/src/flutter/lib/ui/window.dart | 18 ++++ .../lib/ui/window/platform_configuration.h | 1 + .../lib/src/engine/semantics/semantics.dart | 19 ++++ engine/src/flutter/lib/web_ui/lib/window.dart | 1 + .../test/engine/semantics/semantics_test.dart | 28 +++++- .../io/flutter/view/AccessibilityBridge.java | 4 +- .../flutter/view/AccessibilityBridgeTest.java | 49 +++++++-- .../shell/platform/embedder/embedder.h | 2 + .../lib/src/material/input_decorator.dart | 47 +++++---- .../lib/src/semantics/semantics_service.dart | 13 +-- packages/flutter/lib/src/widgets/form.dart | 3 +- .../flutter/lib/src/widgets/media_query.dart | 47 +++++++++ .../test/material/text_field_test.dart | 98 ++++++++++-------- .../semantics/semantics_service_test.dart | 11 +-- packages/flutter/test/widgets/form_test.dart | 99 ++++++++++++------- .../test/widgets/media_query_test.dart | 55 +++++++++++ packages/flutter_test/lib/src/window.dart | 9 +- 17 files changed, 370 insertions(+), 134 deletions(-) diff --git a/engine/src/flutter/lib/ui/window.dart b/engine/src/flutter/lib/ui/window.dart index 48a18692cf85f..a49364c593762 100644 --- a/engine/src/flutter/lib/ui/window.dart +++ b/engine/src/flutter/lib/ui/window.dart @@ -931,6 +931,7 @@ class AccessibilityFeatures { static const int _kReduceMotionIndex = 1 << 4; static const int _kHighContrastIndex = 1 << 5; static const int _kOnOffSwitchLabelsIndex = 1 << 6; + static const int _kNoAnnounceIndex = 1 << 7; // A bitfield which represents each enabled feature. final int _index; @@ -968,6 +969,20 @@ class AccessibilityFeatures { /// Only supported on iOS. bool get onOffSwitchLabels => _kOnOffSwitchLabelsIndex & _index != 0; + /// Whether accessibility announcements (like [SemanticsService.announce]) + /// are allowed on the current platform. + /// + /// Returns `false` on Android, where platform announcements are deprecated + /// by the underlying platform. + /// + /// Returns `true` on all other platforms (iOS, web, desktop) where such + /// announcements are generally supported without discouragement. + /// + /// Use this flag to conditionally avoid making announcements on Android. + // This is an inverted check on _index since there are many more platforms + // that support announce whereas don't. + bool get announce => _kNoAnnounceIndex & _index == 0; + @override String toString() { final List features = []; @@ -992,6 +1007,9 @@ class AccessibilityFeatures { if (onOffSwitchLabels) { features.add('onOffSwitchLabels'); } + if (announce) { + features.add('announce'); + } return 'AccessibilityFeatures$features'; } diff --git a/engine/src/flutter/lib/ui/window/platform_configuration.h b/engine/src/flutter/lib/ui/window/platform_configuration.h index 29ee8ca4fffaa..36e5ddac1ffca 100644 --- a/engine/src/flutter/lib/ui/window/platform_configuration.h +++ b/engine/src/flutter/lib/ui/window/platform_configuration.h @@ -48,6 +48,7 @@ enum class AccessibilityFeatureFlag : int32_t { kReduceMotion = 1 << 4, kHighContrast = 1 << 5, kOnOffSwitchLabels = 1 << 6, + kNoAnnounce = 1 << 7, }; //-------------------------------------------------------------------------- diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart index 825c96f8aa942..21bf446e1c75c 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -54,6 +54,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures { static const int _kReduceMotionIndex = 1 << 4; static const int _kHighContrastIndex = 1 << 5; static const int _kOnOffSwitchLabelsIndex = 1 << 6; + static const int _kNoAnnounceIndex = 1 << 7; // A bitfield which represents each enabled feature. final int _index; @@ -72,6 +73,10 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures { bool get highContrast => _kHighContrastIndex & _index != 0; @override bool get onOffSwitchLabels => _kOnOffSwitchLabelsIndex & _index != 0; + // This is an inverted check on _index since there are many more platforms + // that support announce whereas don't. + @override + bool get announce => _kNoAnnounceIndex & _index == 0; @override String toString() { @@ -97,6 +102,9 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures { if (onOffSwitchLabels) { features.add('onOffSwitchLabels'); } + if (announce) { + features.add('announce'); + } return 'AccessibilityFeatures$features'; } @@ -119,6 +127,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures { bool? reduceMotion, bool? highContrast, bool? onOffSwitchLabels, + bool? announce, }) { final EngineAccessibilityFeaturesBuilder builder = EngineAccessibilityFeaturesBuilder(0); @@ -129,6 +138,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures { builder.reduceMotion = reduceMotion ?? this.reduceMotion; builder.highContrast = highContrast ?? this.highContrast; builder.onOffSwitchLabels = onOffSwitchLabels ?? this.onOffSwitchLabels; + builder.announce = announce ?? this.announce; return builder.build(); } @@ -146,6 +156,9 @@ class EngineAccessibilityFeaturesBuilder { bool get reduceMotion => EngineAccessibilityFeatures._kReduceMotionIndex & _index != 0; bool get highContrast => EngineAccessibilityFeatures._kHighContrastIndex & _index != 0; bool get onOffSwitchLabels => EngineAccessibilityFeatures._kOnOffSwitchLabelsIndex & _index != 0; + // This is an inverted check on _index since there are many more platforms + // that support announce whereas don't. + bool get announce => EngineAccessibilityFeatures._kNoAnnounceIndex & _index == 0; set accessibleNavigation(bool value) { const int accessibleNavigation = EngineAccessibilityFeatures._kAccessibleNavigation; @@ -182,6 +195,12 @@ class EngineAccessibilityFeaturesBuilder { _index = value ? _index | onOffSwitchLabels : _index & ~onOffSwitchLabels; } + set announce(bool value) { + const int noAnnounce = EngineAccessibilityFeatures._kNoAnnounceIndex; + // Since we are using noAnnounce for the embedder, we need to flip the value. + _index = !value ? _index | noAnnounce : _index & ~noAnnounce; + } + /// Creates and returns an instance of EngineAccessibilityFeatures based on the value of _index EngineAccessibilityFeatures build() { return EngineAccessibilityFeatures(_index); diff --git a/engine/src/flutter/lib/web_ui/lib/window.dart b/engine/src/flutter/lib/web_ui/lib/window.dart index 6abd944be64ff..251b719678ea6 100644 --- a/engine/src/flutter/lib/web_ui/lib/window.dart +++ b/engine/src/flutter/lib/web_ui/lib/window.dart @@ -115,6 +115,7 @@ abstract class AccessibilityFeatures { bool get reduceMotion; bool get highContrast; bool get onOffSwitchLabels; + bool get announce; } enum Brightness { dark, light } diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart index 7d1c646624a13..c5877ef8714cf 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -299,6 +299,14 @@ void _testEngineAccessibilityBuilder() { expect(features.onOffSwitchLabels, isTrue); }); + test('announce', () { + // By default this starts off true, see EngineAccessibilityFeatures.announce + expect(features.announce, isTrue); + builder.announce = false; + features = builder.build(); + expect(features.announce, isFalse); + }); + test('reduce motion', () { expect(features.reduceMotion, isFalse); builder.reduceMotion = true; @@ -391,7 +399,10 @@ void _testEngineSemanticsOwner() { }); test('accessibilityFeatures copyWith function works', () { - const EngineAccessibilityFeatures original = EngineAccessibilityFeatures(0); + // Announce is an inverted check, see EngineAccessibilityFeatures.announce. + // Therefore, we need to ensure that the original copy starts with false (1 << 7). + const EngineAccessibilityFeatures original = EngineAccessibilityFeatures(0 | 1 << 7); + EngineAccessibilityFeatures copy = original.copyWith(accessibleNavigation: true); expect(copy.accessibleNavigation, true); expect(copy.boldText, false); @@ -399,6 +410,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, false); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); + expect(copy.announce, false); expect(copy.reduceMotion, false); copy = original.copyWith(boldText: true); @@ -417,6 +429,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, false); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); + expect(copy.announce, false); expect(copy.reduceMotion, false); copy = original.copyWith(highContrast: true); @@ -426,6 +439,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, true); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); + expect(copy.announce, false); expect(copy.reduceMotion, false); copy = original.copyWith(invertColors: true); @@ -435,6 +449,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, false); expect(copy.invertColors, true); expect(copy.onOffSwitchLabels, false); + expect(copy.announce, false); expect(copy.reduceMotion, false); copy = original.copyWith(onOffSwitchLabels: true); @@ -446,6 +461,16 @@ void _testEngineSemanticsOwner() { expect(copy.onOffSwitchLabels, true); expect(copy.reduceMotion, false); + copy = original.copyWith(announce: true); + expect(copy.accessibleNavigation, false); + expect(copy.boldText, false); + expect(copy.disableAnimations, false); + expect(copy.highContrast, false); + expect(copy.invertColors, false); + expect(copy.onOffSwitchLabels, false); + expect(copy.announce, true); + expect(copy.reduceMotion, false); + copy = original.copyWith(reduceMotion: true); expect(copy.accessibleNavigation, false); expect(copy.boldText, false); @@ -453,6 +478,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, false); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); + expect(copy.announce, false); expect(copy.reduceMotion, true); }); diff --git a/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 914c6e2a9ba61..fa7f7fdc792bf 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -490,6 +490,7 @@ public void onTouchExplorationStateChanged(boolean isTouchExplorationEnabled) { this.accessibilityManager.addTouchExplorationStateChangeListener( touchExplorationStateChangeListener); + accessibilityFeatureFlags |= AccessibilityFeature.NO_ANNOUNCE.value; // Tell Flutter whether animations should initially be enabled or disabled. Then register a // listener to be notified of changes in the future. animationScaleObserver.onChange(false); @@ -2170,7 +2171,8 @@ private enum AccessibilityFeature { BOLD_TEXT(1 << 3), // NOT SUPPORTED REDUCE_MOTION(1 << 4), // NOT SUPPORTED HIGH_CONTRAST(1 << 5), // NOT SUPPORTED - ON_OFF_SWITCH_LABELS(1 << 6); // NOT SUPPORTED + ON_OFF_SWITCH_LABELS(1 << 6), // NOT SUPPORTED + NO_ANNOUNCE(1 << 7); final int value; diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index c2cf6e624dd5f..2823c0e454d52 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -66,6 +66,11 @@ @RunWith(AndroidJUnit4.class) public class AccessibilityBridgeTest { + private static final int ACCESSIBILITY_FEATURE_NAVIGATION = 1 << 0; + private static final int ACCESSIBILITY_FEATURE_DISABLE_ANIMATIONS = 1 << 2; + private static final int ACCESSIBILITY_FEATURE_BOLD_TEXT = 1 << 3; + private static final int ACCESSIBILITY_FEATURE_NO_ANNOUNCE = 1 << 7; + @Test public void itDescribesNonTextFieldsWithAContentDescription() { AccessibilityBridge accessibilityBridge = setUpBridge(); @@ -135,6 +140,26 @@ public void itTakesGlobalCoordinatesOfFlutterViewIntoAccount() { assertEquals(position, outBoundsInScreen.top); } + @Test + public void itSetsNoAnnounceAccessibleFlagByDefault() { + AccessibilityChannel mockChannel = mock(AccessibilityChannel.class); + AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class); + AccessibilityManager mockManager = mock(AccessibilityManager.class); + View mockRootView = mock(View.class); + Context context = mock(Context.class); + when(mockRootView.getContext()).thenReturn(context); + when(context.getPackageName()).thenReturn("test"); + when(mockManager.isTouchExplorationEnabled()).thenReturn(false); + setUpBridge( + /*rootAccessibilityView=*/ mockRootView, + /*accessibilityChannel=*/ mockChannel, + /*accessibilityManager=*/ mockManager, + /*contentResolver=*/ null, + /*accessibilityViewEmbedder=*/ mockViewEmbedder, + /*platformViewsAccessibilityDelegate=*/ null); + verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE); + } + @Test public void itSetsAccessibleNavigation() { AccessibilityChannel mockChannel = mock(AccessibilityChannel.class); @@ -158,18 +183,20 @@ public void itSetsAccessibleNavigation() { verify(mockManager).addTouchExplorationStateChangeListener(listenerCaptor.capture()); assertEquals(accessibilityBridge.getAccessibleNavigation(), false); - verify(mockChannel).setAccessibilityFeatures(0); + verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE); reset(mockChannel); // Simulate assistive technology accessing accessibility tree. accessibilityBridge.createAccessibilityNodeInfo(0); - verify(mockChannel).setAccessibilityFeatures(1); + verify(mockChannel) + .setAccessibilityFeatures( + ACCESSIBILITY_FEATURE_NAVIGATION | ACCESSIBILITY_FEATURE_NO_ANNOUNCE); assertEquals(accessibilityBridge.getAccessibleNavigation(), true); // Simulate turning off TalkBack. reset(mockChannel); listenerCaptor.getValue().onTouchExplorationStateChanged(false); - verify(mockChannel).setAccessibilityFeatures(0); + verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE); assertEquals(accessibilityBridge.getAccessibleNavigation(), false); } @@ -1157,7 +1184,9 @@ public void itSetsBoldTextFlagCorrectly() { /*accessibilityViewEmbedder=*/ mockViewEmbedder, /*platformViewsAccessibilityDelegate=*/ null); - verify(mockChannel).setAccessibilityFeatures(1 << 3); + verify(mockChannel) + .setAccessibilityFeatures( + ACCESSIBILITY_FEATURE_BOLD_TEXT | ACCESSIBILITY_FEATURE_NO_ANNOUNCE); reset(mockChannel); // Now verify that clearing the BOLD_TEXT flag doesn't touch any of the other flags. @@ -1179,7 +1208,9 @@ public void itSetsBoldTextFlagCorrectly() { // constructor, verify that the latest argument is correct ArgumentCaptor captor = ArgumentCaptor.forClass(Integer.class); verify(mockChannel, atLeastOnce()).setAccessibilityFeatures(captor.capture()); - assertEquals(1 << 2 /* DISABLE_ANIMATION */, captor.getValue().intValue()); + assertEquals( + ACCESSIBILITY_FEATURE_DISABLE_ANIMATIONS | ACCESSIBILITY_FEATURE_NO_ANNOUNCE, + captor.getValue().intValue()); // Set back to default Settings.Global.putFloat(null, "transition_animation_scale", 1.0f); @@ -1873,19 +1904,21 @@ public void testItSetsDisableAnimationsFlagBasedOnTransitionAnimationScale() { ContentObserver observer = observerCaptor.getValue(); // Initial state - verify(mockChannel).setAccessibilityFeatures(0); + verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE); reset(mockChannel); // Animations are disabled Settings.Global.putFloat(mockContentResolver, "transition_animation_scale", 0.0f); observer.onChange(false); - verify(mockChannel).setAccessibilityFeatures(1 << 2); + verify(mockChannel) + .setAccessibilityFeatures( + ACCESSIBILITY_FEATURE_DISABLE_ANIMATIONS | ACCESSIBILITY_FEATURE_NO_ANNOUNCE); reset(mockChannel); // Animations are enabled Settings.Global.putFloat(mockContentResolver, "transition_animation_scale", 1.0f); observer.onChange(false); - verify(mockChannel).setAccessibilityFeatures(0); + verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE); } @Test diff --git a/engine/src/flutter/shell/platform/embedder/embedder.h b/engine/src/flutter/shell/platform/embedder/embedder.h index 07f48914d223e..a097891756133 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder.h +++ b/engine/src/flutter/shell/platform/embedder/embedder.h @@ -105,6 +105,8 @@ typedef enum { kFlutterAccessibilityFeatureHighContrast = 1 << 5, /// Request to show on/off labels inside switches. kFlutterAccessibilityFeatureOnOffSwitchLabels = 1 << 6, + /// Indicate the platform does not support announcements. + kFlutterAccessibilityFeatureNoAnnounce = 1 << 7, } FlutterAccessibilityFeature; /// The set of possible actions that can be conveyed to a semantics node. diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index b65ed44d04eed..0afbfa05d79d9 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -322,6 +322,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta void initState() { super.initState(); _controller = AnimationController(duration: _kTransitionDuration, vsync: this); + // TODO(ash2moon): https://github.com/flutter/flutter/issues/168022 if (_hasError) { _error = _buildError(); _controller.value = 1.0; @@ -399,26 +400,31 @@ 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, - ), - ), - ), + return Builder( + builder: (BuildContext context) { + return Semantics( + container: true, + liveRegion: !MediaQuery.announceOf(context), + 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, + ), + ), + ), + ); + }, ); } @@ -3927,6 +3933,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..4944821804026 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.announceOf] 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..0733bd7c6df4b 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.announceOf(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..6286df35114eb 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.announce]. + announce, + /// Specifies the aspect corresponding to [MediaQueryData.navigationMode]. navigationMode, @@ -210,6 +213,7 @@ class MediaQueryData { this.onOffSwitchLabels = false, this.disableAnimations = false, this.boldText = false, + this.announce = false, this.navigationMode = NavigationMode.traditional, this.gestureSettings = const DeviceGestureSettings(touchSlop: kTouchSlop), this.displayFeatures = const [], @@ -295,6 +299,7 @@ class MediaQueryData { platformData?.disableAnimations ?? view.platformDispatcher.accessibilityFeatures.disableAnimations, boldText = platformData?.boldText ?? view.platformDispatcher.accessibilityFeatures.boldText, + announce = platformData?.announce ?? view.platformDispatcher.accessibilityFeatures.announce, highContrast = platformData?.highContrast ?? view.platformDispatcher.accessibilityFeatures.highContrast, onOffSwitchLabels = @@ -584,6 +589,23 @@ class MediaQueryData { /// originates. final bool boldText; + /// Whether accessibility announcements (like [SemanticsService.announce]) + /// are allowed on the current platform. + /// + /// Returns `false` on Android, where platform announcements are deprecated + /// by the underlying platform. + /// + /// Returns `true` on all other platforms (iOS, web, desktop) where such + /// announcements are generally supported without discouragement. + /// + /// Use this flag to conditionally avoid making announcements on Android. + /// + /// See also: + /// + /// * [dart:ui.PlatformDispatcher.accessibilityFeatures], where the setting + /// originates. + final bool announce; + /// 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? announce, NavigationMode? navigationMode, DeviceGestureSettings? gestureSettings, List? displayFeatures, @@ -690,6 +713,7 @@ class MediaQueryData { disableAnimations: disableAnimations ?? this.disableAnimations, accessibleNavigation: accessibleNavigation ?? this.accessibleNavigation, boldText: boldText ?? this.boldText, + announce: announce ?? this.announce, 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.announce == announce && other.navigationMode == navigationMode && other.gestureSettings == gestureSettings && listEquals(other.displayFeatures, displayFeatures) && @@ -1713,6 +1738,27 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { static bool? maybeBoldTextOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.boldText)?.boldText; + /// Returns the [MediaQueryData.announce] 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.announce] property of the ancestor [MediaQuery] + /// changes. + /// + /// {@macro flutter.widgets.media_query.MediaQuery.dontUseOf} + static bool announceOf(BuildContext context) => maybeAnnounceOf(context) ?? false; + + /// Returns the [MediaQueryData.announce] 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.announce] property of the ancestor [MediaQuery] + /// changes. + /// + /// {@macro flutter.widgets.media_query.MediaQuery.dontUseMaybeOf} + static bool? maybeAnnounceOf(BuildContext context) => + _maybeOf(context, _MediaQueryAspect.announce)?.announce; + /// Returns [MediaQueryData.navigationMode] for the nearest [MediaQuery] /// ancestor or throws an exception, if no such ancestor exists. /// @@ -1843,6 +1889,7 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { _MediaQueryAspect.disableAnimations => data.disableAnimations != oldWidget.data.disableAnimations, _MediaQueryAspect.boldText => data.boldText != oldWidget.data.boldText, + _MediaQueryAspect.announce => data.announce != oldWidget.data.announce, _MediaQueryAspect.navigationMode => data.navigationMode != oldWidget.data.navigationMode, _MediaQueryAspect.gestureSettings => diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 8c360947c595b..9df58ec9941d5 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -8945,55 +8945,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 announce in [true, false]) { + testWidgets('InputDecoration errorText semantics (announce=$announce)', ( + 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(announce: announce), + 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 (!announce) 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..79b88bb31469c 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( + announce: false, + testName: + 'Should announce only the first error message when validate returns errors and announce = false', + ), + _PlatformAnnounceScenario( + announce: 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(announce: test.announce), + 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.announce) { + 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(announce: true), child: Directionality( textDirection: TextDirection.ltr, child: Center( @@ -1615,3 +1632,9 @@ void main() { ); }); } + +class _PlatformAnnounceScenario { + _PlatformAnnounceScenario({required this.announce, required this.testName}); + final bool announce; + final String testName; +} diff --git a/packages/flutter/test/widgets/media_query_test.dart b/packages/flutter/test/widgets/media_query_test.dart index 8ca9fddcf8bc2..d530da3860f0e 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.announce, 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, + announce: 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.announce, platformData.announce); expect(data.alwaysUse24HourFormat, platformData.alwaysUse24HourFormat); expect(data.navigationMode, platformData.navigationMode); expect(data.gestureSettings, DeviceGestureSettings.fromView(tester.view)); @@ -255,6 +259,7 @@ void main() { data.onOffSwitchLabels, tester.platformDispatcher.accessibilityFeatures.onOffSwitchLabels, ); + expect(data.announce, tester.platformDispatcher.accessibilityFeatures.announce); expect(data.alwaysUse24HourFormat, tester.platformDispatcher.alwaysUse24HourFormat); expect(data.navigationMode, NavigationMode.traditional); expect(data.gestureSettings, DeviceGestureSettings.fromView(tester.view)); @@ -274,6 +279,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + announce: true, alwaysUse24HourFormat: true, navigationMode: NavigationMode.directional, ); @@ -321,6 +327,7 @@ void main() { expect(data.boldText, platformData.boldText); expect(data.highContrast, platformData.highContrast); expect(data.onOffSwitchLabels, platformData.onOffSwitchLabels); + expect(data.announce, platformData.announce); expect(data.alwaysUse24HourFormat, platformData.alwaysUse24HourFormat); expect(data.navigationMode, platformData.navigationMode); expect(data.gestureSettings, DeviceGestureSettings.fromView(tester.view)); @@ -393,6 +400,7 @@ void main() { data.onOffSwitchLabels, tester.platformDispatcher.accessibilityFeatures.onOffSwitchLabels, ); + expect(data.announce, tester.platformDispatcher.accessibilityFeatures.announce); expect(data.alwaysUse24HourFormat, tester.platformDispatcher.alwaysUse24HourFormat); expect(data.navigationMode, NavigationMode.traditional); expect(data.gestureSettings, DeviceGestureSettings.fromView(tester.view)); @@ -577,6 +585,7 @@ void main() { expect(copied.boldText, data.boldText); expect(copied.highContrast, data.highContrast); expect(copied.onOffSwitchLabels, data.onOffSwitchLabels); + expect(copied.announce, data.announce); expect(copied.platformBrightness, data.platformBrightness); expect(copied.gestureSettings, data.gestureSettings); expect(copied.displayFeatures, data.displayFeatures); @@ -617,6 +626,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + announce: true, platformBrightness: Brightness.dark, navigationMode: NavigationMode.directional, gestureSettings: gestureSettings, @@ -636,6 +646,7 @@ void main() { expect(copied.boldText, true); expect(copied.highContrast, true); expect(copied.onOffSwitchLabels, true); + expect(copied.announce, true); expect(copied.platformBrightness, Brightness.dark); expect(copied.navigationMode, NavigationMode.directional); expect(copied.gestureSettings, gestureSettings); @@ -674,6 +685,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + announce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -710,6 +722,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); + expect(unpadded.announce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -748,6 +761,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + announce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -781,6 +795,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); + expect(unpadded.announce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -819,6 +834,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + announce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -855,6 +871,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); + expect(unpadded.announce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -893,6 +910,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + announce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -926,6 +944,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); + expect(unpadded.announce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -964,6 +983,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + announce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -1000,6 +1020,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); + expect(unpadded.announce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -1038,6 +1059,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + announce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -1071,6 +1093,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); + expect(unpadded.announce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -1175,6 +1198,32 @@ void main() { expect(insideOnOffSwitchLabels, true); }); + testWidgets('MediaQuery.announce', (WidgetTester tester) async { + late bool outsideAnnounce; + late bool insideAnnounce; + + tester.platformDispatcher.accessibilityFeaturesTestValue = const FakeAccessibilityFeatures(); + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + outsideAnnounce = MediaQuery.announceOf(context); + return MediaQuery( + data: const MediaQueryData(announce: true), + child: Builder( + builder: (BuildContext context) { + insideAnnounce = MediaQuery.announceOf(context); + return Container(); + }, + ), + ); + }, + ), + ); + + expect(outsideAnnounce, false); + expect(insideAnnounce, true); + }); + testWidgets('MediaQuery.boldTextOf', (WidgetTester tester) async { late bool outsideBoldTextOverride; late bool insideBoldTextOverride; @@ -1314,6 +1363,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + announce: true, displayFeatures: displayFeatures, ), child: Builder( @@ -1345,6 +1395,7 @@ void main() { expect(subScreenMediaQuery.boldText, true); expect(subScreenMediaQuery.highContrast, true); expect(subScreenMediaQuery.onOffSwitchLabels, true); + expect(subScreenMediaQuery.announce, true); expect(subScreenMediaQuery.displayFeatures, isEmpty); }); @@ -1391,6 +1442,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, + announce: true, displayFeatures: displayFeatures, ), child: Builder( @@ -1428,6 +1480,7 @@ void main() { expect(subScreenMediaQuery.boldText, true); expect(subScreenMediaQuery.highContrast, true); expect(subScreenMediaQuery.onOffSwitchLabels, true); + expect(subScreenMediaQuery.announce, true); expect(subScreenMediaQuery.displayFeatures, [cutoutDisplayFeature]); }); @@ -1692,6 +1745,8 @@ void main() { MediaQuery.maybeOnOffSwitchLabelsOf, MediaQueryData(onOffSwitchLabels: true), ), + const _MediaQueryAspectCase(MediaQuery.announceOf, MediaQueryData(announce: true)), + const _MediaQueryAspectCase(MediaQuery.maybeAnnounceOf, MediaQueryData(announce: true)), const _MediaQueryAspectCase( MediaQuery.disableAnimationsOf, MediaQueryData(disableAnimations: true), diff --git a/packages/flutter_test/lib/src/window.dart b/packages/flutter_test/lib/src/window.dart index 562cd1bcbe04f..f2eb6f8a0468e 100644 --- a/packages/flutter_test/lib/src/window.dart +++ b/packages/flutter_test/lib/src/window.dart @@ -32,6 +32,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { this.reduceMotion = false, this.highContrast = false, this.onOffSwitchLabels = false, + this.announce = false, }); /// An instance of [AccessibilityFeatures] where all the features are enabled. @@ -43,6 +44,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { reduceMotion: true, highContrast: true, onOffSwitchLabels: true, + announce: true, ); @override @@ -66,6 +68,9 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { @override final bool onOffSwitchLabels; + @override + final bool announce; + @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) { @@ -78,7 +83,8 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { other.boldText == boldText && other.reduceMotion == reduceMotion && other.highContrast == highContrast && - other.onOffSwitchLabels == onOffSwitchLabels; + other.onOffSwitchLabels == onOffSwitchLabels && + other.announce == announce; } @override @@ -91,6 +97,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { reduceMotion, highContrast, onOffSwitchLabels, + announce, ); } From cb5647c393ea5ef6e4087128bcca125ca4740d7c Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Sat, 17 May 2025 20:48:40 -0700 Subject: [PATCH 02/16] fix _buildError state transition issue --- .../lib/src/material/input_decorator.dart | 6 ++- .../test/material/input_decorator_test.dart | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 0afbfa05d79d9..bbe4bbd58e87b 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -400,6 +400,8 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta Widget _buildError() { assert(widget.error != null || widget.errorText != null); + final Widget? capturedError = widget.error; + final String? capturedErrorText = widget.errorText; return Builder( builder: (BuildContext context) { return Semantics( @@ -413,9 +415,9 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta end: Offset.zero, ).evaluate(_controller.view), child: - widget.error ?? + capturedError ?? Text( - widget.errorText!, + capturedErrorText!, style: widget.errorStyle, textAlign: widget.textAlign, overflow: TextOverflow.ellipsis, diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 49da3d42029e3..f877813c6a6ce 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -14421,4 +14421,42 @@ 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'; + + 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); + }, + ); } From 6392cf1d262779a420f56cbc21f65c1599cb5f23 Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Mon, 19 May 2025 10:11:35 -0700 Subject: [PATCH 03/16] fix _buildError state transition issue --- packages/flutter/test/material/input_decorator_test.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index f877813c6a6ce..9969d1a2c3585 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -14424,7 +14424,7 @@ void main() { testWidgets( 'InputDecorator _buildError with errorText correctly updates on BuildContext updates', - (WidgetTester tester) async { + (WidgetTester tester) async { final ValueNotifier errorTextNotifier = ValueNotifier('initial error'); const String helperTextValue = 'helper text'; @@ -14436,10 +14436,7 @@ void main() { valueListenable: errorTextNotifier, builder: (BuildContext context, String? value, Widget? child) { return buildInputDecorator( - decoration: InputDecoration( - errorText: value, - helperText: helperTextValue, - ), + decoration: InputDecoration(errorText: value, helperText: helperTextValue), ); }, ), From 7242050818d685fb41440f7fba21d13acf05b1f7 Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Wed, 28 May 2025 13:40:22 -0700 Subject: [PATCH 04/16] add teardown for valuenotifier in test case --- packages/flutter/test/material/input_decorator_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 9969d1a2c3585..0cf9b1a22098e 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -14428,6 +14428,8 @@ void main() { final ValueNotifier errorTextNotifier = ValueNotifier('initial error'); const String helperTextValue = 'helper text'; + addTearDown(errorTextNotifier.dispose); + await tester.pumpWidget( MaterialApp( home: Scaffold( From aa8b30de3e14db5433c2476ef2dea08a90c04ae6 Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Mon, 9 Jun 2025 15:29:42 -0700 Subject: [PATCH 05/16] update docs --- .../flutter/lib/src/widgets/media_query.dart | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/flutter/lib/src/widgets/media_query.dart b/packages/flutter/lib/src/widgets/media_query.dart index 6286df35114eb..b871897d64b21 100644 --- a/packages/flutter/lib/src/widgets/media_query.dart +++ b/packages/flutter/lib/src/widgets/media_query.dart @@ -590,15 +590,13 @@ class MediaQueryData { final bool boldText; /// Whether accessibility announcements (like [SemanticsService.announce]) - /// are allowed on the current platform. + /// are supported on the current platform. /// - /// Returns `false` on Android, where platform announcements are deprecated - /// by the underlying platform. + /// Returns `false` on platforms where announcements are deprecated or + /// unsupported by the underlying platform. /// - /// Returns `true` on all other platforms (iOS, web, desktop) where such - /// announcements are generally supported without discouragement. - /// - /// Use this flag to conditionally avoid making announcements on Android. + /// Returns `true` on platforms where such announcements are + /// generally supported without discouragement. (iOS, web etc) /// /// See also: /// @@ -1743,7 +1741,10 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { /// /// Use of this method will cause the given [context] to rebuild any time that /// the [MediaQueryData.announce] property of the ancestor [MediaQuery] - /// changes. + /// changes. This is especially important for announce because announce has a + /// low frequency change rate. The performance difference between rebuilding + /// for all media query data changes and only rebuilding for announce is a + /// dramatic difference. /// /// {@macro flutter.widgets.media_query.MediaQuery.dontUseOf} static bool announceOf(BuildContext context) => maybeAnnounceOf(context) ?? false; From c10da58fb7d7b0884c9938f84b5e418d81940172 Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Fri, 13 Jun 2025 11:52:30 -0700 Subject: [PATCH 06/16] add comments to the announce setter --- .../lib/web_ui/lib/src/engine/semantics/semantics.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart index 9d8731488922d..4fa85d93ec25b 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -196,9 +196,11 @@ class EngineAccessibilityFeaturesBuilder { _index = value ? _index | onOffSwitchLabels : _index & ~onOffSwitchLabels; } + // This setter uses an inverted check (!value instead of value) to set the noAnnounce + // field in EngineAccessibilityFeatures since far more platforms support announce + // than not. set announce(bool value) { const int noAnnounce = EngineAccessibilityFeatures._kNoAnnounceIndex; - // Since we are using noAnnounce for the embedder, we need to flip the value. _index = !value ? _index | noAnnounce : _index & ~noAnnounce; } From 2e50f29559bf2f90ba8702b50311565bbaeb2d51 Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Fri, 13 Jun 2025 14:21:30 -0700 Subject: [PATCH 07/16] rename from announce to supportsAnnounce --- engine/src/flutter/lib/ui/window.dart | 6 +- .../lib/src/engine/semantics/semantics.dart | 18 ++-- .../lib/src/material/input_decorator.dart | 2 +- .../lib/src/semantics/semantics_service.dart | 2 +- packages/flutter/lib/src/widgets/form.dart | 2 +- .../flutter/lib/src/widgets/media_query.dart | 41 ++++----- .../test/material/text_field_test.dart | 8 +- packages/flutter/test/widgets/form_test.dart | 14 ++-- .../test/widgets/media_query_test.dart | 84 +++++++++++-------- 9 files changed, 96 insertions(+), 81 deletions(-) diff --git a/engine/src/flutter/lib/ui/window.dart b/engine/src/flutter/lib/ui/window.dart index 3db2091b15d3d..f85460a0789e0 100644 --- a/engine/src/flutter/lib/ui/window.dart +++ b/engine/src/flutter/lib/ui/window.dart @@ -981,7 +981,7 @@ class AccessibilityFeatures { /// Use this flag to conditionally avoid making announcements on Android. // This index check is inverted (== 0 vs != 0); far more platforms support // "announce" than discourage it. - bool get announce => _kNoAnnounceIndex & _index == 0; + bool get supportsAnnounce => _kNoAnnounceIndex & _index == 0; @override String toString() { @@ -1007,8 +1007,8 @@ class AccessibilityFeatures { if (onOffSwitchLabels) { features.add('onOffSwitchLabels'); } - if (announce) { - features.add('announce'); + if (supportsAnnounce) { + features.add('supportsAnnounce'); } return 'AccessibilityFeatures$features'; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart index 4fa85d93ec25b..c1c554f017c72 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -75,9 +75,9 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures { @override bool get onOffSwitchLabels => _kOnOffSwitchLabelsIndex & _index != 0; // This index check is inverted (== 0 vs != 0); far more platforms support - // "announce" than discourage it. + // announce than discourage it. @override - bool get announce => _kNoAnnounceIndex & _index == 0; + bool get supportsAnnounce => _kNoAnnounceIndex & _index == 0; @override String toString() { @@ -103,8 +103,8 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures { if (onOffSwitchLabels) { features.add('onOffSwitchLabels'); } - if (announce) { - features.add('announce'); + if (supportsAnnounce) { + features.add('supportsAnnounce'); } return 'AccessibilityFeatures$features'; } @@ -128,7 +128,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures { bool? reduceMotion, bool? highContrast, bool? onOffSwitchLabels, - bool? announce, + bool? supportsAnnounce, }) { final EngineAccessibilityFeaturesBuilder builder = EngineAccessibilityFeaturesBuilder(0); @@ -139,7 +139,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures { builder.reduceMotion = reduceMotion ?? this.reduceMotion; builder.highContrast = highContrast ?? this.highContrast; builder.onOffSwitchLabels = onOffSwitchLabels ?? this.onOffSwitchLabels; - builder.announce = announce ?? this.announce; + builder.supportsAnnounce = supportsAnnounce ?? this.supportsAnnounce; return builder.build(); } @@ -158,8 +158,8 @@ class EngineAccessibilityFeaturesBuilder { bool get highContrast => EngineAccessibilityFeatures._kHighContrastIndex & _index != 0; bool get onOffSwitchLabels => EngineAccessibilityFeatures._kOnOffSwitchLabelsIndex & _index != 0; // This index check is inverted (== 0 vs != 0); far more platforms support - // "announce" than discourage it. - bool get announce => EngineAccessibilityFeatures._kNoAnnounceIndex & _index == 0; + // announce than discourage it. + bool get supportsAnnounce => EngineAccessibilityFeatures._kNoAnnounceIndex & _index == 0; set accessibleNavigation(bool value) { const int accessibleNavigation = EngineAccessibilityFeatures._kAccessibleNavigation; @@ -199,7 +199,7 @@ class EngineAccessibilityFeaturesBuilder { // This setter uses an inverted check (!value instead of value) to set the noAnnounce // field in EngineAccessibilityFeatures since far more platforms support announce // than not. - set announce(bool value) { + set supportsAnnounce(bool value) { const int noAnnounce = EngineAccessibilityFeatures._kNoAnnounceIndex; _index = !value ? _index | noAnnounce : _index & ~noAnnounce; } diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index c4d399bc05f89..abb4047ef8e5e 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -406,7 +406,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta builder: (BuildContext context) { return Semantics( container: true, - liveRegion: !MediaQuery.announceOf(context), + liveRegion: !MediaQuery.supportsAnnounceOf(context), child: FadeTransition( opacity: _controller, child: FractionalTranslation( diff --git a/packages/flutter/lib/src/semantics/semantics_service.dart b/packages/flutter/lib/src/semantics/semantics_service.dart index 4944821804026..750e887e39758 100644 --- a/packages/flutter/lib/src/semantics/semantics_service.dart +++ b/packages/flutter/lib/src/semantics/semantics_service.dart @@ -34,7 +34,7 @@ abstract final class SemanticsService { /// other platforms. The default mode is [Assertiveness.polite]. /// /// Not all platforms support announcements. Check to see if it is supported using - /// [MediaQuery.announceOf] before calling this method. + /// [MediaQuery.supportsAnnounceOf] before calling this method. /// /// ### Android /// Android has [deprecated announcement events][1] due to its disruptive diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index 0733bd7c6df4b..55cac4d592f8c 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -370,7 +370,7 @@ class FormState extends State { } } - if (errorMessage.isNotEmpty && MediaQuery.announceOf(context)) { + 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 b871897d64b21..c1f55b18a7df2 100644 --- a/packages/flutter/lib/src/widgets/media_query.dart +++ b/packages/flutter/lib/src/widgets/media_query.dart @@ -103,8 +103,8 @@ enum _MediaQueryAspect { /// Specifies the aspect corresponding to [MediaQueryData.boldText]. boldText, - /// Specifies the aspect corresponding to [MediaQueryData.announce]. - announce, + /// Specifies the aspect corresponding to [MediaQueryData.supportsAnnounce]. + supportsAnnounce, /// Specifies the aspect corresponding to [MediaQueryData.navigationMode]. navigationMode, @@ -213,7 +213,7 @@ class MediaQueryData { this.onOffSwitchLabels = false, this.disableAnimations = false, this.boldText = false, - this.announce = false, + this.supportsAnnounce = false, this.navigationMode = NavigationMode.traditional, this.gestureSettings = const DeviceGestureSettings(touchSlop: kTouchSlop), this.displayFeatures = const [], @@ -299,7 +299,9 @@ class MediaQueryData { platformData?.disableAnimations ?? view.platformDispatcher.accessibilityFeatures.disableAnimations, boldText = platformData?.boldText ?? view.platformDispatcher.accessibilityFeatures.boldText, - announce = platformData?.announce ?? view.platformDispatcher.accessibilityFeatures.announce, + supportsAnnounce = + platformData?.supportsAnnounce ?? + view.platformDispatcher.accessibilityFeatures.supportsAnnounce, highContrast = platformData?.highContrast ?? view.platformDispatcher.accessibilityFeatures.highContrast, onOffSwitchLabels = @@ -589,7 +591,7 @@ class MediaQueryData { /// originates. final bool boldText; - /// Whether accessibility announcements (like [SemanticsService.announce]) + /// Whether accessibility announcements (like [SemanticsService.supportsAnnounce]) /// are supported on the current platform. /// /// Returns `false` on platforms where announcements are deprecated or @@ -602,7 +604,7 @@ class MediaQueryData { /// /// * [dart:ui.PlatformDispatcher.accessibilityFeatures], where the setting /// originates. - final bool announce; + final bool supportsAnnounce; /// Describes the navigation mode requested by the platform. /// @@ -685,7 +687,7 @@ class MediaQueryData { bool? invertColors, bool? accessibleNavigation, bool? boldText, - bool? announce, + bool? supportsAnnounce, NavigationMode? navigationMode, DeviceGestureSettings? gestureSettings, List? displayFeatures, @@ -711,7 +713,7 @@ class MediaQueryData { disableAnimations: disableAnimations ?? this.disableAnimations, accessibleNavigation: accessibleNavigation ?? this.accessibleNavigation, boldText: boldText ?? this.boldText, - announce: announce ?? this.announce, + supportsAnnounce: supportsAnnounce ?? this.supportsAnnounce, navigationMode: navigationMode ?? this.navigationMode, gestureSettings: gestureSettings ?? this.gestureSettings, displayFeatures: displayFeatures ?? this.displayFeatures, @@ -914,7 +916,7 @@ class MediaQueryData { other.invertColors == invertColors && other.accessibleNavigation == accessibleNavigation && other.boldText == boldText && - other.announce == announce && + other.supportsAnnounce == supportsAnnounce && other.navigationMode == navigationMode && other.gestureSettings == gestureSettings && listEquals(other.displayFeatures, displayFeatures) && @@ -1736,29 +1738,29 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { static bool? maybeBoldTextOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.boldText)?.boldText; - /// Returns the [MediaQueryData.announce] accessibility setting for the + /// 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.announce] property of the ancestor [MediaQuery] - /// changes. This is especially important for announce because announce has a + /// 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 announce is a + /// for all media query data changes and only rebuilding for supportsAnnounce is a /// dramatic difference. /// /// {@macro flutter.widgets.media_query.MediaQuery.dontUseOf} - static bool announceOf(BuildContext context) => maybeAnnounceOf(context) ?? false; + static bool supportsAnnounceOf(BuildContext context) => maybeSupportsAnnounceOf(context) ?? false; - /// Returns the [MediaQueryData.announce] accessibility setting for the + /// 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.announce] property of the ancestor [MediaQuery] + /// the [MediaQueryData.supportsAnnounce] property of the ancestor [MediaQuery] /// changes. /// /// {@macro flutter.widgets.media_query.MediaQuery.dontUseMaybeOf} - static bool? maybeAnnounceOf(BuildContext context) => - _maybeOf(context, _MediaQueryAspect.announce)?.announce; + 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. @@ -1890,7 +1892,8 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { _MediaQueryAspect.disableAnimations => data.disableAnimations != oldWidget.data.disableAnimations, _MediaQueryAspect.boldText => data.boldText != oldWidget.data.boldText, - _MediaQueryAspect.announce => data.announce != oldWidget.data.announce, + _MediaQueryAspect.supportsAnnounce => + data.supportsAnnounce != oldWidget.data.supportsAnnounce, _MediaQueryAspect.navigationMode => data.navigationMode != oldWidget.data.navigationMode, _MediaQueryAspect.gestureSettings => diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 146b56c75b223..f009d44079715 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -9247,8 +9247,8 @@ void main() { semantics.dispose(); }); - for (final bool announce in [true, false]) { - testWidgets('InputDecoration errorText semantics (announce=$announce)', ( + for (final bool supportsAnnounce in [true, false]) { + testWidgets('InputDecoration errorText semantics (supportsAnnounce=$supportsAnnounce)', ( WidgetTester tester, ) async { final SemanticsTester semantics = SemanticsTester(tester); @@ -9258,7 +9258,7 @@ void main() { await tester.pumpWidget( overlay( child: MediaQuery( - data: MediaQueryData(announce: announce), + data: MediaQueryData(supportsAnnounce: supportsAnnounce), child: TextField( key: key, controller: controller, @@ -9292,7 +9292,7 @@ void main() { TestSemantics( label: 'oh no!', textDirection: TextDirection.ltr, - flags: [if (!announce) SemanticsFlag.isLiveRegion], + flags: [if (!supportsAnnounce) SemanticsFlag.isLiveRegion], ), ], ), diff --git a/packages/flutter/test/widgets/form_test.dart b/packages/flutter/test/widgets/form_test.dart index 79b88bb31469c..cf5e6e9c80d29 100644 --- a/packages/flutter/test/widgets/form_test.dart +++ b/packages/flutter/test/widgets/form_test.dart @@ -171,12 +171,12 @@ void main() { for (final _PlatformAnnounceScenario test in <_PlatformAnnounceScenario>[ _PlatformAnnounceScenario( - announce: false, + supportsAnnounce: false, testName: 'Should announce only the first error message when validate returns errors and announce = false', ), _PlatformAnnounceScenario( - announce: true, + supportsAnnounce: true, testName: 'Should not announce error message when validate returns errors and announce = true', ), @@ -186,7 +186,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: MediaQuery( - data: MediaQueryData(announce: test.announce), + data: MediaQueryData(supportsAnnounce: test.supportsAnnounce), child: Directionality( textDirection: TextDirection.ltr, child: Center( @@ -218,7 +218,7 @@ void main() { expect(find.text('First error message'), findsOneWidget); expect(find.text('Second error message'), findsOneWidget); - if (test.announce) { + if (test.supportsAnnounce) { final CapturedAccessibilityAnnouncement announcement = tester.takeAnnouncements().single; expect(announcement.message, 'First error message'); expect(announcement.textDirection, TextDirection.ltr); @@ -405,7 +405,7 @@ void main() { Widget builder() { return MaterialApp( home: MediaQuery( - data: const MediaQueryData(announce: true), + data: const MediaQueryData(supportsAnnounce: true), child: Directionality( textDirection: TextDirection.ltr, child: Center( @@ -1634,7 +1634,7 @@ void main() { } class _PlatformAnnounceScenario { - _PlatformAnnounceScenario({required this.announce, required this.testName}); - final bool announce; + _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 7cba1c408982d..1d98a4bfc64e7 100644 --- a/packages/flutter/test/widgets/media_query_test.dart +++ b/packages/flutter/test/widgets/media_query_test.dart @@ -155,7 +155,7 @@ void main() { expect(data.boldText, false); expect(data.highContrast, false); expect(data.onOffSwitchLabels, false); - expect(data.announce, false); + expect(data.supportsAnnounce, false); expect(data.platformBrightness, Brightness.light); expect(data.gestureSettings.touchSlop, null); expect(data.displayFeatures, isEmpty); @@ -171,7 +171,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, - announce: true, + supportsAnnounce: true, alwaysUse24HourFormat: true, navigationMode: NavigationMode.directional, ); @@ -205,7 +205,7 @@ void main() { expect(data.boldText, platformData.boldText); expect(data.highContrast, platformData.highContrast); expect(data.onOffSwitchLabels, platformData.onOffSwitchLabels); - expect(data.announce, platformData.announce); + expect(data.supportsAnnounce, platformData.supportsAnnounce); expect(data.alwaysUse24HourFormat, platformData.alwaysUse24HourFormat); expect(data.navigationMode, platformData.navigationMode); expect(data.gestureSettings, DeviceGestureSettings.fromView(tester.view)); @@ -259,7 +259,10 @@ void main() { data.onOffSwitchLabels, tester.platformDispatcher.accessibilityFeatures.onOffSwitchLabels, ); - expect(data.announce, tester.platformDispatcher.accessibilityFeatures.announce); + 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)); @@ -279,7 +282,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, - announce: true, + supportsAnnounce: true, alwaysUse24HourFormat: true, navigationMode: NavigationMode.directional, ); @@ -327,7 +330,7 @@ void main() { expect(data.boldText, platformData.boldText); expect(data.highContrast, platformData.highContrast); expect(data.onOffSwitchLabels, platformData.onOffSwitchLabels); - expect(data.announce, platformData.announce); + expect(data.supportsAnnounce, platformData.supportsAnnounce); expect(data.alwaysUse24HourFormat, platformData.alwaysUse24HourFormat); expect(data.navigationMode, platformData.navigationMode); expect(data.gestureSettings, DeviceGestureSettings.fromView(tester.view)); @@ -400,7 +403,10 @@ void main() { data.onOffSwitchLabels, tester.platformDispatcher.accessibilityFeatures.onOffSwitchLabels, ); - expect(data.announce, tester.platformDispatcher.accessibilityFeatures.announce); + 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)); @@ -585,7 +591,7 @@ void main() { expect(copied.boldText, data.boldText); expect(copied.highContrast, data.highContrast); expect(copied.onOffSwitchLabels, data.onOffSwitchLabels); - expect(copied.announce, data.announce); + expect(copied.supportsAnnounce, data.supportsAnnounce); expect(copied.platformBrightness, data.platformBrightness); expect(copied.gestureSettings, data.gestureSettings); expect(copied.displayFeatures, data.displayFeatures); @@ -626,7 +632,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, - announce: true, + supportsAnnounce: true, platformBrightness: Brightness.dark, navigationMode: NavigationMode.directional, gestureSettings: gestureSettings, @@ -646,7 +652,7 @@ void main() { expect(copied.boldText, true); expect(copied.highContrast, true); expect(copied.onOffSwitchLabels, true); - expect(copied.announce, true); + expect(copied.supportsAnnounce, true); expect(copied.platformBrightness, Brightness.dark); expect(copied.navigationMode, NavigationMode.directional); expect(copied.gestureSettings, gestureSettings); @@ -685,7 +691,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, - announce: true, + supportsAnnounce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -722,7 +728,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); - expect(unpadded.announce, true); + expect(unpadded.supportsAnnounce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -761,7 +767,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, - announce: true, + supportsAnnounce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -795,7 +801,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); - expect(unpadded.announce, true); + expect(unpadded.supportsAnnounce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -834,7 +840,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, - announce: true, + supportsAnnounce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -871,7 +877,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); - expect(unpadded.announce, true); + expect(unpadded.supportsAnnounce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -910,7 +916,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, - announce: true, + supportsAnnounce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -944,7 +950,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); - expect(unpadded.announce, true); + expect(unpadded.supportsAnnounce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -983,7 +989,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, - announce: true, + supportsAnnounce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -1020,7 +1026,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); - expect(unpadded.announce, true); + expect(unpadded.supportsAnnounce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -1059,7 +1065,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, - announce: true, + supportsAnnounce: true, navigationMode: NavigationMode.directional, displayFeatures: displayFeatures, ), @@ -1093,7 +1099,7 @@ void main() { expect(unpadded.boldText, true); expect(unpadded.highContrast, true); expect(unpadded.onOffSwitchLabels, true); - expect(unpadded.announce, true); + expect(unpadded.supportsAnnounce, true); expect(unpadded.navigationMode, NavigationMode.directional); expect(unpadded.displayFeatures, displayFeatures); }); @@ -1198,20 +1204,20 @@ void main() { expect(insideOnOffSwitchLabels, true); }); - testWidgets('MediaQuery.announce', (WidgetTester tester) async { - late bool outsideAnnounce; - late bool insideAnnounce; + testWidgets('MediaQuery.supportsAnnounce', (WidgetTester tester) async { + late bool outsideSupportsAnnounce; + late bool insideSupportsAnnounce; tester.platformDispatcher.accessibilityFeaturesTestValue = const FakeAccessibilityFeatures(); await tester.pumpWidget( Builder( builder: (BuildContext context) { - outsideAnnounce = MediaQuery.announceOf(context); + outsideSupportsAnnounce = MediaQuery.supportsAnnounceOf(context); return MediaQuery( - data: const MediaQueryData(announce: true), + data: const MediaQueryData(supportsAnnounce: true), child: Builder( builder: (BuildContext context) { - insideAnnounce = MediaQuery.announceOf(context); + insideSupportsAnnounce = MediaQuery.supportsAnnounceOf(context); return Container(); }, ), @@ -1220,8 +1226,8 @@ void main() { ), ); - expect(outsideAnnounce, false); - expect(insideAnnounce, true); + expect(outsideSupportsAnnounce, false); + expect(insideSupportsAnnounce, true); }); testWidgets('MediaQuery.boldTextOf', (WidgetTester tester) async { @@ -1363,7 +1369,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, - announce: true, + supportsAnnounce: true, displayFeatures: displayFeatures, ), child: Builder( @@ -1395,7 +1401,7 @@ void main() { expect(subScreenMediaQuery.boldText, true); expect(subScreenMediaQuery.highContrast, true); expect(subScreenMediaQuery.onOffSwitchLabels, true); - expect(subScreenMediaQuery.announce, true); + expect(subScreenMediaQuery.supportsAnnounce, true); expect(subScreenMediaQuery.displayFeatures, isEmpty); }); @@ -1442,7 +1448,7 @@ void main() { boldText: true, highContrast: true, onOffSwitchLabels: true, - announce: true, + supportsAnnounce: true, displayFeatures: displayFeatures, ), child: Builder( @@ -1480,7 +1486,7 @@ void main() { expect(subScreenMediaQuery.boldText, true); expect(subScreenMediaQuery.highContrast, true); expect(subScreenMediaQuery.onOffSwitchLabels, true); - expect(subScreenMediaQuery.announce, true); + expect(subScreenMediaQuery.supportsAnnounce, true); expect(subScreenMediaQuery.displayFeatures, [cutoutDisplayFeature]); }); @@ -1745,8 +1751,14 @@ void main() { MediaQuery.maybeOnOffSwitchLabelsOf, MediaQueryData(onOffSwitchLabels: true), ), - const _MediaQueryAspectCase(MediaQuery.announceOf, MediaQueryData(announce: true)), - const _MediaQueryAspectCase(MediaQuery.maybeAnnounceOf, MediaQueryData(announce: true)), + const _MediaQueryAspectCase( + MediaQuery.supportsAnnounceOf, + MediaQueryData(supportsAnnounce: true), + ), + const _MediaQueryAspectCase( + MediaQuery.maybeSupportsAnnounceOf, + MediaQueryData(supportsAnnounce: true), + ), const _MediaQueryAspectCase( MediaQuery.disableAnimationsOf, MediaQueryData(disableAnimations: true), From 1d220de40f23266b7c3adfc6f52935728b4a1091 Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Fri, 13 Jun 2025 14:23:48 -0700 Subject: [PATCH 08/16] remove outdated comment --- packages/flutter/lib/src/material/input_decorator.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index abb4047ef8e5e..d238673681635 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -322,7 +322,6 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta void initState() { super.initState(); _controller = AnimationController(duration: _kTransitionDuration, vsync: this); - // TODO(ash2moon): https://github.com/flutter/flutter/issues/168022 if (_hasError) { _error = _buildError(); _controller.value = 1.0; From 1beffa983d6a0e5f8d702308a99bf95abdab04e7 Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Fri, 13 Jun 2025 14:28:38 -0700 Subject: [PATCH 09/16] rename from announce to supportsAnnounce --- .../test/engine/semantics/semantics_test.dart | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart index 835f788afd764..2422498815d4d 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -305,12 +305,12 @@ void _testEngineAccessibilityBuilder() { expect(features.onOffSwitchLabels, isTrue); }); - test('announce', () { - // By default this starts off true, see EngineAccessibilityFeatures.announce - expect(features.announce, isTrue); - builder.announce = false; + test('supportsAnnounce', () { + // By default this starts off true, see EngineAccessibilityFeatures.supportsAnnounce + expect(features.supportsAnnounce, isTrue); + builder.supportsAnnounce = false; features = builder.build(); - expect(features.announce, isFalse); + expect(features.supportsAnnounce, isFalse); }); test('reduce motion', () { @@ -416,7 +416,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, false); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); - expect(copy.announce, false); + expect(copy.supportsAnnounce, false); expect(copy.reduceMotion, false); copy = original.copyWith(boldText: true); @@ -435,7 +435,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, false); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); - expect(copy.announce, false); + expect(copy.supportsAnnounce, false); expect(copy.reduceMotion, false); copy = original.copyWith(highContrast: true); @@ -445,7 +445,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, true); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); - expect(copy.announce, false); + expect(copy.supportsAnnounce, false); expect(copy.reduceMotion, false); copy = original.copyWith(invertColors: true); @@ -455,7 +455,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, false); expect(copy.invertColors, true); expect(copy.onOffSwitchLabels, false); - expect(copy.announce, false); + expect(copy.supportsAnnounce, false); expect(copy.reduceMotion, false); copy = original.copyWith(onOffSwitchLabels: true); @@ -467,14 +467,14 @@ void _testEngineSemanticsOwner() { expect(copy.onOffSwitchLabels, true); expect(copy.reduceMotion, false); - copy = original.copyWith(announce: true); + copy = original.copyWith(supportsAnnounce: true); expect(copy.accessibleNavigation, false); expect(copy.boldText, false); expect(copy.disableAnimations, false); expect(copy.highContrast, false); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); - expect(copy.announce, true); + expect(copy.supportsAnnounce, true); expect(copy.reduceMotion, false); copy = original.copyWith(reduceMotion: true); @@ -484,7 +484,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, false); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); - expect(copy.announce, false); + expect(copy.supportsAnnounce, false); expect(copy.reduceMotion, true); }); From cc1107b02439685353082b2b1983e1ef5b5b822a Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Fri, 13 Jun 2025 14:36:32 -0700 Subject: [PATCH 10/16] rename from announce to supportsAnnounce --- engine/src/flutter/lib/web_ui/lib/window.dart | 2 +- packages/flutter_test/lib/src/window.dart | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/lib/window.dart b/engine/src/flutter/lib/web_ui/lib/window.dart index 251b719678ea6..5564cb5678248 100644 --- a/engine/src/flutter/lib/web_ui/lib/window.dart +++ b/engine/src/flutter/lib/web_ui/lib/window.dart @@ -115,7 +115,7 @@ abstract class AccessibilityFeatures { bool get reduceMotion; bool get highContrast; bool get onOffSwitchLabels; - bool get announce; + bool get supportsAnnounce; } enum Brightness { dark, light } diff --git a/packages/flutter_test/lib/src/window.dart b/packages/flutter_test/lib/src/window.dart index f2eb6f8a0468e..d0b0385cea932 100644 --- a/packages/flutter_test/lib/src/window.dart +++ b/packages/flutter_test/lib/src/window.dart @@ -32,7 +32,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { this.reduceMotion = false, this.highContrast = false, this.onOffSwitchLabels = false, - this.announce = false, + this.supportsAnnounce = false, }); /// An instance of [AccessibilityFeatures] where all the features are enabled. @@ -44,7 +44,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { reduceMotion: true, highContrast: true, onOffSwitchLabels: true, - announce: true, + supportsAnnounce: true, ); @override @@ -69,7 +69,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { final bool onOffSwitchLabels; @override - final bool announce; + final bool supportsSupportsAnnounce; @override bool operator ==(Object other) { @@ -84,7 +84,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { other.reduceMotion == reduceMotion && other.highContrast == highContrast && other.onOffSwitchLabels == onOffSwitchLabels && - other.announce == announce; + other.supportsAnnounce == supportsAnnounce; } @override @@ -97,7 +97,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { reduceMotion, highContrast, onOffSwitchLabels, - announce, + supportsAnnounce, ); } From cd916fbb0c056fd1ae70a095a2c4031d90ec46ff Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Fri, 13 Jun 2025 15:01:13 -0700 Subject: [PATCH 11/16] rename from announce to supportsAnnounce on engine --- engine/src/flutter/lib/ui/window.dart | 6 ++--- .../lib/src/engine/semantics/semantics.dart | 22 +++++++++-------- engine/src/flutter/lib/web_ui/lib/window.dart | 2 +- .../test/engine/semantics/semantics_test.dart | 24 +++++++++---------- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/engine/src/flutter/lib/ui/window.dart b/engine/src/flutter/lib/ui/window.dart index 3db2091b15d3d..f85460a0789e0 100644 --- a/engine/src/flutter/lib/ui/window.dart +++ b/engine/src/flutter/lib/ui/window.dart @@ -981,7 +981,7 @@ class AccessibilityFeatures { /// Use this flag to conditionally avoid making announcements on Android. // This index check is inverted (== 0 vs != 0); far more platforms support // "announce" than discourage it. - bool get announce => _kNoAnnounceIndex & _index == 0; + bool get supportsAnnounce => _kNoAnnounceIndex & _index == 0; @override String toString() { @@ -1007,8 +1007,8 @@ class AccessibilityFeatures { if (onOffSwitchLabels) { features.add('onOffSwitchLabels'); } - if (announce) { - features.add('announce'); + if (supportsAnnounce) { + features.add('supportsAnnounce'); } return 'AccessibilityFeatures$features'; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart index 7c886cc48a83c..4ea2cc7131e53 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -75,9 +75,9 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures { @override bool get onOffSwitchLabels => _kOnOffSwitchLabelsIndex & _index != 0; // This index check is inverted (== 0 vs != 0); far more platforms support - // "announce" than discourage it. + // announce than discourage it. @override - bool get announce => _kNoAnnounceIndex & _index == 0; + bool get supportsAnnounce => _kNoAnnounceIndex & _index == 0; @override String toString() { @@ -103,8 +103,8 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures { if (onOffSwitchLabels) { features.add('onOffSwitchLabels'); } - if (announce) { - features.add('announce'); + if (supportsAnnounce) { + features.add('supportsAnnounce'); } return 'AccessibilityFeatures$features'; } @@ -128,7 +128,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures { bool? reduceMotion, bool? highContrast, bool? onOffSwitchLabels, - bool? announce, + bool? supportsAnnounce, }) { final EngineAccessibilityFeaturesBuilder builder = EngineAccessibilityFeaturesBuilder(0); @@ -139,7 +139,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures { builder.reduceMotion = reduceMotion ?? this.reduceMotion; builder.highContrast = highContrast ?? this.highContrast; builder.onOffSwitchLabels = onOffSwitchLabels ?? this.onOffSwitchLabels; - builder.announce = announce ?? this.announce; + builder.supportsAnnounce = supportsAnnounce ?? this.supportsAnnounce; return builder.build(); } @@ -158,8 +158,8 @@ class EngineAccessibilityFeaturesBuilder { bool get highContrast => EngineAccessibilityFeatures._kHighContrastIndex & _index != 0; bool get onOffSwitchLabels => EngineAccessibilityFeatures._kOnOffSwitchLabelsIndex & _index != 0; // This index check is inverted (== 0 vs != 0); far more platforms support - // "announce" than discourage it. - bool get announce => EngineAccessibilityFeatures._kNoAnnounceIndex & _index == 0; + // announce than discourage it. + bool get supportsAnnounce => EngineAccessibilityFeatures._kNoAnnounceIndex & _index == 0; set accessibleNavigation(bool value) { const int accessibleNavigation = EngineAccessibilityFeatures._kAccessibleNavigation; @@ -196,9 +196,11 @@ class EngineAccessibilityFeaturesBuilder { _index = value ? _index | onOffSwitchLabels : _index & ~onOffSwitchLabels; } - set announce(bool value) { + // This setter uses an inverted check (!value instead of value) to set the noAnnounce + // field in EngineAccessibilityFeatures since far more platforms support announce + // than not. + set supportsAnnounce(bool value) { const int noAnnounce = EngineAccessibilityFeatures._kNoAnnounceIndex; - // Since we are using noAnnounce for the embedder, we need to flip the value. _index = !value ? _index | noAnnounce : _index & ~noAnnounce; } diff --git a/engine/src/flutter/lib/web_ui/lib/window.dart b/engine/src/flutter/lib/web_ui/lib/window.dart index 251b719678ea6..5564cb5678248 100644 --- a/engine/src/flutter/lib/web_ui/lib/window.dart +++ b/engine/src/flutter/lib/web_ui/lib/window.dart @@ -115,7 +115,7 @@ abstract class AccessibilityFeatures { bool get reduceMotion; bool get highContrast; bool get onOffSwitchLabels; - bool get announce; + bool get supportsAnnounce; } enum Brightness { dark, light } diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart index fdb1b3f821653..b51180d001508 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -375,12 +375,12 @@ void _testEngineAccessibilityBuilder() { expect(features.onOffSwitchLabels, isTrue); }); - test('announce', () { - // By default this starts off true, see EngineAccessibilityFeatures.announce - expect(features.announce, isTrue); - builder.announce = false; + test('supportsAnnounce', () { + // By default this starts off true, see EngineAccessibilityFeatures.supportsAnnounce + expect(features.supportsAnnounce, isTrue); + builder.supportsAnnounce = false; features = builder.build(); - expect(features.announce, isFalse); + expect(features.supportsAnnounce, isFalse); }); test('reduce motion', () { @@ -486,7 +486,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, false); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); - expect(copy.announce, false); + expect(copy.supportsAnnounce, false); expect(copy.reduceMotion, false); copy = original.copyWith(boldText: true); @@ -505,7 +505,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, false); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); - expect(copy.announce, false); + expect(copy.supportsAnnounce, false); expect(copy.reduceMotion, false); copy = original.copyWith(highContrast: true); @@ -515,7 +515,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, true); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); - expect(copy.announce, false); + expect(copy.supportsAnnounce, false); expect(copy.reduceMotion, false); copy = original.copyWith(invertColors: true); @@ -525,7 +525,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, false); expect(copy.invertColors, true); expect(copy.onOffSwitchLabels, false); - expect(copy.announce, false); + expect(copy.supportsAnnounce, false); expect(copy.reduceMotion, false); copy = original.copyWith(onOffSwitchLabels: true); @@ -537,14 +537,14 @@ void _testEngineSemanticsOwner() { expect(copy.onOffSwitchLabels, true); expect(copy.reduceMotion, false); - copy = original.copyWith(announce: true); + copy = original.copyWith(supportsAnnounce: true); expect(copy.accessibleNavigation, false); expect(copy.boldText, false); expect(copy.disableAnimations, false); expect(copy.highContrast, false); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); - expect(copy.announce, true); + expect(copy.supportsAnnounce, true); expect(copy.reduceMotion, false); copy = original.copyWith(reduceMotion: true); @@ -554,7 +554,7 @@ void _testEngineSemanticsOwner() { expect(copy.highContrast, false); expect(copy.invertColors, false); expect(copy.onOffSwitchLabels, false); - expect(copy.announce, false); + expect(copy.supportsAnnounce, false); expect(copy.reduceMotion, true); }); From d746e6d056b06a993ea9c0d4a73b3f00084992d7 Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Fri, 13 Jun 2025 15:02:59 -0700 Subject: [PATCH 12/16] rename from announce to supportsAnnounce on engine --- packages/flutter_test/lib/src/window.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_test/lib/src/window.dart b/packages/flutter_test/lib/src/window.dart index d0b0385cea932..514894229475e 100644 --- a/packages/flutter_test/lib/src/window.dart +++ b/packages/flutter_test/lib/src/window.dart @@ -69,7 +69,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { final bool onOffSwitchLabels; @override - final bool supportsSupportsAnnounce; + final bool supportsAnnounce; @override bool operator ==(Object other) { From 6af9a17ca1886def98bc7bb8ded92488c89f7aa7 Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Mon, 16 Jun 2025 14:28:46 -0700 Subject: [PATCH 13/16] update docs on use of `supportsAnnounce` --- engine/src/flutter/lib/ui/window.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/engine/src/flutter/lib/ui/window.dart b/engine/src/flutter/lib/ui/window.dart index f85460a0789e0..bf2899303bc6d 100644 --- a/engine/src/flutter/lib/ui/window.dart +++ b/engine/src/flutter/lib/ui/window.dart @@ -969,16 +969,20 @@ class AccessibilityFeatures { /// Only supported on iOS. bool get onOffSwitchLabels => _kOnOffSwitchLabelsIndex & _index != 0; - /// Whether accessibility announcements (like [SemanticsService.announce]) - /// are supported on the current platform. + /// Whether the platform supports accessibility announcement API. + /// i.e. [SemanticsService.announce]. Some platforms do not + /// support or discourage the use of announcement. Using + /// [SemanticsService.announce] on those platform may be ignored. + /// Consider using other way to convey message to the user. For + /// example, Android discourages the uses of direct message announcement, + /// and rather encourages using other semantic properties such as + /// [SemanticsProperties.liveRegion] to convey message to the user. /// /// 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) - /// - /// Use this flag to conditionally avoid making announcements on Android. // This index check is inverted (== 0 vs != 0); far more platforms support // "announce" than discourage it. bool get supportsAnnounce => _kNoAnnounceIndex & _index == 0; From ab90d340786bbb30d7285110f1486d2802951312 Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Mon, 16 Jun 2025 16:08:54 -0700 Subject: [PATCH 14/16] add missing file --- packages/flutter_test/lib/src/window.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/flutter_test/lib/src/window.dart b/packages/flutter_test/lib/src/window.dart index f2eb6f8a0468e..12e316b46578f 100644 --- a/packages/flutter_test/lib/src/window.dart +++ b/packages/flutter_test/lib/src/window.dart @@ -32,7 +32,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { this.reduceMotion = false, this.highContrast = false, this.onOffSwitchLabels = false, - this.announce = false, + this.supportsAnnounce = false, }); /// An instance of [AccessibilityFeatures] where all the features are enabled. @@ -44,7 +44,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { reduceMotion: true, highContrast: true, onOffSwitchLabels: true, - announce: true, + supportsAnnounce: true, ); @override @@ -69,7 +69,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { final bool onOffSwitchLabels; @override - final bool announce; + final bool supportsAnnounce; @override bool operator ==(Object other) { @@ -97,7 +97,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { reduceMotion, highContrast, onOffSwitchLabels, - announce, + supportsAnnounce, ); } From 2b5174143f6548bff61812a4919d3989318ff72d Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Mon, 16 Jun 2025 17:54:15 -0700 Subject: [PATCH 15/16] add missing file --- packages/flutter_test/lib/src/window.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_test/lib/src/window.dart b/packages/flutter_test/lib/src/window.dart index 12e316b46578f..514894229475e 100644 --- a/packages/flutter_test/lib/src/window.dart +++ b/packages/flutter_test/lib/src/window.dart @@ -84,7 +84,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures { other.reduceMotion == reduceMotion && other.highContrast == highContrast && other.onOffSwitchLabels == onOffSwitchLabels && - other.announce == announce; + other.supportsAnnounce == supportsAnnounce; } @override From 99781179633a62ef3a922dff9a83e94472fecb5f Mon Sep 17 00:00:00 2001 From: Ash Zarif Date: Tue, 24 Jun 2025 14:34:31 -0700 Subject: [PATCH 16/16] fix docs --- packages/flutter/lib/src/widgets/media_query.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/widgets/media_query.dart b/packages/flutter/lib/src/widgets/media_query.dart index c1f55b18a7df2..a4a2c667fc658 100644 --- a/packages/flutter/lib/src/widgets/media_query.dart +++ b/packages/flutter/lib/src/widgets/media_query.dart @@ -591,7 +591,7 @@ class MediaQueryData { /// originates. final bool boldText; - /// Whether accessibility announcements (like [SemanticsService.supportsAnnounce]) + /// Whether accessibility announcements (like [SemanticsService.announce]) /// are supported on the current platform. /// /// Returns `false` on platforms where announcements are deprecated or