Reverts "Use live region in error text input decorator for Android (#165531)" (#168848)

<!-- start_original_pr_link -->
Reverts: flutter/flutter#165531
<!-- end_original_pr_link -->
<!-- start_initiating_author -->
Initiated by: matanlurey
<!-- end_initiating_author -->
<!-- start_revert_reason -->
Reason for reverting: Breaks google client tests
<!-- end_revert_reason -->
<!-- start_original_pr_author -->
Original PR Author: ash2moon
<!-- end_original_pr_author -->

<!-- start_reviewers -->
Reviewed By: {chunhtai, reidbaker, hannah-hyj}
<!-- end_reviewers -->

<!-- start_revert_body -->
This change reverts the following previous change:
Resolves partly https://github.com/flutter/flutter/issues/165510


**Context:** This issue originates from
https://github.com/flutter/flutter/issues/99715, where it was reported
that `liveRegion` alone was insufficient for announcing form validation
errors. While `liveRegion` announces the first error encountered,
subsequent submissions with the same error message on Android would not
trigger a re-announcement.

**Original Solution:** Pull request
https://github.com/flutter/flutter/pull/123373 addressed this by
implementing the `announce` event to ensure error messages were
consistently announced, even for repeated submissions.

**Native Android Behavior (Jetpack Compose):** In native Android
development using Jetpack Compose, setting the `isError` property of a
`TextField` to `true` triggers Talkback to announce "Error invalid
input." This announcement occurs *only* on the initial change to the
error state. Subsequent errors, even if the `isError` property remains
`true`, are not re-announced. This behavior closely mirrors the
functionality of `liveRegion`, with the key difference being that
`liveRegion` also announces the specific error text, in addition to the
general error state. Testing in a native Jetpack Compose application
confirms this behavior and provides a valuable comparison point against
the current Flutter form example.

**Suggested Action:** **Fork** the behavior in
https://github.com/flutter/flutter/pull/123373. Reinstate the use of
`liveRegion` for error announcements within `widgets/Form` for Android
and keep other platforms the same.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

<!-- end_revert_body -->

Co-authored-by: auto-submit[bot] <flutter-engprod-team@google.com>
This commit is contained in:
auto-submit[bot] 2025-05-14 17:47:38 +00:00 committed by GitHub
parent 494b08b420
commit 606bb06c0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 135 additions and 371 deletions

View File

@ -931,7 +931,6 @@ 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;
@ -969,20 +968,6 @@ 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<String> features = <String>[];
@ -1007,9 +992,6 @@ class AccessibilityFeatures {
if (onOffSwitchLabels) {
features.add('onOffSwitchLabels');
}
if (announce) {
features.add('announce');
}
return 'AccessibilityFeatures$features';
}

View File

@ -48,7 +48,6 @@ enum class AccessibilityFeatureFlag : int32_t {
kReduceMotion = 1 << 4,
kHighContrast = 1 << 5,
kOnOffSwitchLabels = 1 << 6,
kNoAnnounce = 1 << 7,
};
//--------------------------------------------------------------------------

View File

@ -54,7 +54,6 @@ 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;
@ -73,10 +72,6 @@ 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() {
@ -102,9 +97,6 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
if (onOffSwitchLabels) {
features.add('onOffSwitchLabels');
}
if (announce) {
features.add('announce');
}
return 'AccessibilityFeatures$features';
}
@ -127,7 +119,6 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
bool? reduceMotion,
bool? highContrast,
bool? onOffSwitchLabels,
bool? announce,
}) {
final EngineAccessibilityFeaturesBuilder builder = EngineAccessibilityFeaturesBuilder(0);
@ -138,7 +129,6 @@ 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();
}
@ -156,9 +146,6 @@ 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;
@ -195,12 +182,6 @@ 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);

View File

@ -115,7 +115,6 @@ abstract class AccessibilityFeatures {
bool get reduceMotion;
bool get highContrast;
bool get onOffSwitchLabels;
bool get announce;
}
enum Brightness { dark, light }

View File

@ -299,14 +299,6 @@ 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;
@ -399,10 +391,7 @@ void _testEngineSemanticsOwner() {
});
test('accessibilityFeatures copyWith function works', () {
// 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);
const EngineAccessibilityFeatures original = EngineAccessibilityFeatures(0);
EngineAccessibilityFeatures copy = original.copyWith(accessibleNavigation: true);
expect(copy.accessibleNavigation, true);
expect(copy.boldText, false);
@ -410,7 +399,6 @@ 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);
@ -429,7 +417,6 @@ 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);
@ -439,7 +426,6 @@ 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);
@ -449,7 +435,6 @@ 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);
@ -461,16 +446,6 @@ 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);
@ -478,7 +453,6 @@ void _testEngineSemanticsOwner() {
expect(copy.highContrast, false);
expect(copy.invertColors, false);
expect(copy.onOffSwitchLabels, false);
expect(copy.announce, false);
expect(copy.reduceMotion, true);
});

View File

@ -490,7 +490,6 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
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);
@ -2171,8 +2170,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
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
NO_ANNOUNCE(1 << 7);
ON_OFF_SWITCH_LABELS(1 << 6); // NOT SUPPORTED
final int value;

View File

@ -66,11 +66,6 @@ import org.robolectric.annotation.Config;
@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();
@ -140,26 +135,6 @@ public class AccessibilityBridgeTest {
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);
@ -183,20 +158,18 @@ public class AccessibilityBridgeTest {
verify(mockManager).addTouchExplorationStateChangeListener(listenerCaptor.capture());
assertEquals(accessibilityBridge.getAccessibleNavigation(), false);
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
verify(mockChannel).setAccessibilityFeatures(0);
reset(mockChannel);
// Simulate assistive technology accessing accessibility tree.
accessibilityBridge.createAccessibilityNodeInfo(0);
verify(mockChannel)
.setAccessibilityFeatures(
ACCESSIBILITY_FEATURE_NAVIGATION | ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
verify(mockChannel).setAccessibilityFeatures(1);
assertEquals(accessibilityBridge.getAccessibleNavigation(), true);
// Simulate turning off TalkBack.
reset(mockChannel);
listenerCaptor.getValue().onTouchExplorationStateChanged(false);
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
verify(mockChannel).setAccessibilityFeatures(0);
assertEquals(accessibilityBridge.getAccessibleNavigation(), false);
}
@ -1184,9 +1157,7 @@ public class AccessibilityBridgeTest {
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
/*platformViewsAccessibilityDelegate=*/ null);
verify(mockChannel)
.setAccessibilityFeatures(
ACCESSIBILITY_FEATURE_BOLD_TEXT | ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
verify(mockChannel).setAccessibilityFeatures(1 << 3);
reset(mockChannel);
// Now verify that clearing the BOLD_TEXT flag doesn't touch any of the other flags.
@ -1208,9 +1179,7 @@ public class AccessibilityBridgeTest {
// constructor, verify that the latest argument is correct
ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
verify(mockChannel, atLeastOnce()).setAccessibilityFeatures(captor.capture());
assertEquals(
ACCESSIBILITY_FEATURE_DISABLE_ANIMATIONS | ACCESSIBILITY_FEATURE_NO_ANNOUNCE,
captor.getValue().intValue());
assertEquals(1 << 2 /* DISABLE_ANIMATION */, captor.getValue().intValue());
// Set back to default
Settings.Global.putFloat(null, "transition_animation_scale", 1.0f);
@ -1904,21 +1873,19 @@ public class AccessibilityBridgeTest {
ContentObserver observer = observerCaptor.getValue();
// Initial state
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
verify(mockChannel).setAccessibilityFeatures(0);
reset(mockChannel);
// Animations are disabled
Settings.Global.putFloat(mockContentResolver, "transition_animation_scale", 0.0f);
observer.onChange(false);
verify(mockChannel)
.setAccessibilityFeatures(
ACCESSIBILITY_FEATURE_DISABLE_ANIMATIONS | ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
verify(mockChannel).setAccessibilityFeatures(1 << 2);
reset(mockChannel);
// Animations are enabled
Settings.Global.putFloat(mockContentResolver, "transition_animation_scale", 1.0f);
observer.onChange(false);
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
verify(mockChannel).setAccessibilityFeatures(0);
}
@Test

View File

@ -105,8 +105,6 @@ 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.

View File

@ -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;
@ -400,31 +399,26 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
Widget _buildError() {
assert(widget.error != null || widget.errorText != null);
return Builder(
builder: (BuildContext context) {
return Semantics(
container: true,
liveRegion: !MediaQuery.announceOf(context),
child: FadeTransition(
opacity: _controller,
child: FractionalTranslation(
translation: Tween<Offset>(
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 Semantics(
container: true,
child: FadeTransition(
opacity: _controller,
child: FractionalTranslation(
translation: Tween<Offset>(
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,
),
),
),
);
}
@ -3933,7 +3927,6 @@ class InputDecoration {
bool? alignLabelWithHint,
BoxConstraints? constraints,
VisualDensity? visualDensity,
SemanticsService? semanticsService,
}) {
return InputDecoration(
icon: icon ?? this.icon,

View File

@ -7,6 +7,7 @@ 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;
@ -33,8 +34,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 it is supported using
/// [MediaQuery.announceOf] before calling this method.
/// Not all platforms support announcements. Check to see if
/// [isAnnounceSupported] before calling this method.
///
/// ### Android
/// Android has [deprecated announcement events][1] due to its disruptive
@ -65,4 +66,12 @@ 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;
}
}

View File

@ -16,7 +16,6 @@ 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';
@ -370,7 +369,7 @@ class FormState extends State<Form> {
}
}
if (errorMessage.isNotEmpty && MediaQuery.announceOf(context)) {
if (errorMessage.isNotEmpty) {
final TextDirection directionality = Directionality.of(context);
if (defaultTargetPlatform == TargetPlatform.iOS) {
unawaited(

View File

@ -103,9 +103,6 @@ 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,
@ -213,7 +210,6 @@ 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 <ui.DisplayFeature>[],
@ -299,7 +295,6 @@ 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 =
@ -589,23 +584,6 @@ 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)
@ -687,7 +665,6 @@ class MediaQueryData {
bool? invertColors,
bool? accessibleNavigation,
bool? boldText,
bool? announce,
NavigationMode? navigationMode,
DeviceGestureSettings? gestureSettings,
List<ui.DisplayFeature>? displayFeatures,
@ -713,7 +690,6 @@ 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,
@ -916,7 +892,6 @@ class MediaQueryData {
other.invertColors == invertColors &&
other.accessibleNavigation == accessibleNavigation &&
other.boldText == boldText &&
other.announce == announce &&
other.navigationMode == navigationMode &&
other.gestureSettings == gestureSettings &&
listEquals(other.displayFeatures, displayFeatures) &&
@ -1738,27 +1713,6 @@ 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.
///
@ -1889,7 +1843,6 @@ 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 =>

View File

@ -8945,67 +8945,55 @@ void main() {
semantics.dispose();
});
for (final bool announce in <bool>[true, false]) {
testWidgets('InputDecoration errorText semantics (announce=$announce)', (
WidgetTester tester,
) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = _textEditingController();
final Key key = UniqueKey();
testWidgets('InputDecoration errorText semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = _textEditingController();
final Key key = UniqueKey();
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!',
),
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
errorText: 'oh no!',
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'label',
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
],
inputType: ui.SemanticsInputType.text,
currentValueLength: 0,
children: <TestSemantics>[
TestSemantics(label: 'oh no!', textDirection: TextDirection.ltr),
],
),
),
],
),
);
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'label',
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
],
inputType: ui.SemanticsInputType.text,
currentValueLength: 0,
children: <TestSemantics>[
TestSemantics(
label: 'oh no!',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[if (!announce) SemanticsFlag.isLiveRegion],
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
semantics.dispose();
debugDefaultTargetPlatformOverride = null;
});
}
semantics.dispose();
});
testWidgets('floating label does not overlap with value at large textScaleFactors', (
WidgetTester tester,

View File

@ -2,8 +2,9 @@
// 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;
import 'package:flutter/services.dart' show SystemChannels, TargetPlatform;
import 'package:flutter_test/flutter_test.dart';
void main() {
@ -44,4 +45,12 @@ 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;
});
}
}

View File

@ -169,67 +169,50 @@ void main() {
await checkErrorText('');
});
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<FormState> formKey = GlobalKey<FormState>();
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: <Widget>[
TextFormField(validator: (_) => 'First error message'),
TextFormField(validator: (_) => 'Second error message'),
],
),
testWidgets('Should announce only the first error message when validate returns errors', (
WidgetTester tester,
) async {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
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: <Widget>[
TextFormField(validator: (_) => 'First error message'),
TextFormField(validator: (_) => 'Second error message'),
],
),
),
),
),
),
),
);
formKey.currentState!.reset();
await tester.enterText(find.byType(TextFormField).first, '');
await tester.pump();
),
);
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);
// 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);
}
});
}
final CapturedAccessibilityAnnouncement announcement = tester.takeAnnouncements().single;
expect(announcement.message, 'First error message');
expect(announcement.textDirection, TextDirection.ltr);
expect(announcement.assertiveness, Assertiveness.assertive);
});
testWidgets('isValid returns true when a field is valid', (WidgetTester tester) async {
final GlobalKey<FormFieldState<String>> fieldKey1 = GlobalKey<FormFieldState<String>>();
@ -405,7 +388,7 @@ void main() {
Widget builder() {
return MaterialApp(
home: MediaQuery(
data: const MediaQueryData(announce: true),
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
@ -1632,9 +1615,3 @@ void main() {
);
});
}
class _PlatformAnnounceScenario {
_PlatformAnnounceScenario({required this.announce, required this.testName});
final bool announce;
final String testName;
}

View File

@ -144,7 +144,6 @@ 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));
@ -155,7 +154,6 @@ 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);
@ -171,7 +169,6 @@ void main() {
boldText: true,
highContrast: true,
onOffSwitchLabels: true,
announce: true,
alwaysUse24HourFormat: true,
navigationMode: NavigationMode.directional,
);
@ -205,7 +202,6 @@ 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));
@ -259,7 +255,6 @@ 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));
@ -279,7 +274,6 @@ void main() {
boldText: true,
highContrast: true,
onOffSwitchLabels: true,
announce: true,
alwaysUse24HourFormat: true,
navigationMode: NavigationMode.directional,
);
@ -327,7 +321,6 @@ 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));
@ -400,7 +393,6 @@ 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));
@ -585,7 +577,6 @@ 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);
@ -626,7 +617,6 @@ void main() {
boldText: true,
highContrast: true,
onOffSwitchLabels: true,
announce: true,
platformBrightness: Brightness.dark,
navigationMode: NavigationMode.directional,
gestureSettings: gestureSettings,
@ -646,7 +636,6 @@ 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);
@ -685,7 +674,6 @@ void main() {
boldText: true,
highContrast: true,
onOffSwitchLabels: true,
announce: true,
navigationMode: NavigationMode.directional,
displayFeatures: displayFeatures,
),
@ -722,7 +710,6 @@ 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);
});
@ -761,7 +748,6 @@ void main() {
boldText: true,
highContrast: true,
onOffSwitchLabels: true,
announce: true,
navigationMode: NavigationMode.directional,
displayFeatures: displayFeatures,
),
@ -795,7 +781,6 @@ 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);
});
@ -834,7 +819,6 @@ void main() {
boldText: true,
highContrast: true,
onOffSwitchLabels: true,
announce: true,
navigationMode: NavigationMode.directional,
displayFeatures: displayFeatures,
),
@ -871,7 +855,6 @@ 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);
});
@ -910,7 +893,6 @@ void main() {
boldText: true,
highContrast: true,
onOffSwitchLabels: true,
announce: true,
navigationMode: NavigationMode.directional,
displayFeatures: displayFeatures,
),
@ -944,7 +926,6 @@ 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);
});
@ -983,7 +964,6 @@ void main() {
boldText: true,
highContrast: true,
onOffSwitchLabels: true,
announce: true,
navigationMode: NavigationMode.directional,
displayFeatures: displayFeatures,
),
@ -1020,7 +1000,6 @@ 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);
});
@ -1059,7 +1038,6 @@ void main() {
boldText: true,
highContrast: true,
onOffSwitchLabels: true,
announce: true,
navigationMode: NavigationMode.directional,
displayFeatures: displayFeatures,
),
@ -1093,7 +1071,6 @@ 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);
});
@ -1198,32 +1175,6 @@ 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;
@ -1363,7 +1314,6 @@ void main() {
boldText: true,
highContrast: true,
onOffSwitchLabels: true,
announce: true,
displayFeatures: displayFeatures,
),
child: Builder(
@ -1395,7 +1345,6 @@ void main() {
expect(subScreenMediaQuery.boldText, true);
expect(subScreenMediaQuery.highContrast, true);
expect(subScreenMediaQuery.onOffSwitchLabels, true);
expect(subScreenMediaQuery.announce, true);
expect(subScreenMediaQuery.displayFeatures, isEmpty);
});
@ -1442,7 +1391,6 @@ void main() {
boldText: true,
highContrast: true,
onOffSwitchLabels: true,
announce: true,
displayFeatures: displayFeatures,
),
child: Builder(
@ -1480,7 +1428,6 @@ void main() {
expect(subScreenMediaQuery.boldText, true);
expect(subScreenMediaQuery.highContrast, true);
expect(subScreenMediaQuery.onOffSwitchLabels, true);
expect(subScreenMediaQuery.announce, true);
expect(subScreenMediaQuery.displayFeatures, <DisplayFeature>[cutoutDisplayFeature]);
});
@ -1745,8 +1692,6 @@ 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),

View File

@ -32,7 +32,6 @@ 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.
@ -44,7 +43,6 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
reduceMotion: true,
highContrast: true,
onOffSwitchLabels: true,
announce: true,
);
@override
@ -68,9 +66,6 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
@override
final bool onOffSwitchLabels;
@override
final bool announce;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
@ -83,8 +78,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
other.boldText == boldText &&
other.reduceMotion == reduceMotion &&
other.highContrast == highContrast &&
other.onOffSwitchLabels == onOffSwitchLabels &&
other.announce == announce;
other.onOffSwitchLabels == onOffSwitchLabels;
}
@override
@ -97,7 +91,6 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
reduceMotion,
highContrast,
onOffSwitchLabels,
announce,
);
}