Sitelet https://github.com/flutter/flutter/pull/168992/files
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9bd8d56
Add back "Use live region in error text input decorator for Android #…
ash2moon May 14, 2025
cb5647c
fix _buildError state transition issue
ash2moon May 18, 2025
f0eb874
Merge branch 'master' into feat/form-live-region
ash2moon May 19, 2025
6392cf1
fix _buildError state transition issue
ash2moon May 19, 2025
8d2a154
Merge branch 'master' into feat/form-live-region
ash2moon May 19, 2025
b974b61
Merge branch 'master' into feat/form-live-region
ash2moon May 20, 2025
7b7ca86
Merge branch 'master' into feat/form-live-region
ash2moon May 23, 2025
6eec1fa
Merge branch 'master' into feat/form-live-region
ash2moon May 27, 2025
cb6f843
Merge branch 'master' into feat/form-live-region
ash2moon May 27, 2025
9e8d9ed
Merge branch 'master' into feat/form-live-region
ash2moon May 28, 2025
805eb8c
Merge branch 'master' into feat/form-live-region
ash2moon May 28, 2025
7242050
add teardown for valuenotifier in test case
ash2moon May 28, 2025
85ff24b
Merge remote-tracking branch 'origin/feat/form-live-region' into feat…
ash2moon May 28, 2025
cd71a4d
Merge branch 'master' into feat/form-live-region
ash2moon May 28, 2025
820c23b
Merge branch 'master' into feat/form-live-region
ash2moon May 29, 2025
007a035
Merge branch 'feat/form-live-region' of https://github.com/ash2moon/f…
ash2moon Jun 9, 2025
615669c
Merge branch 'master' of https://github.com/flutter/flutter into feat…
ash2moon Jun 9, 2025
aa8b30d
update docs
ash2moon Jun 9, 2025
c10da58
add comments to the announce setter
ash2moon Jun 13, 2025
2e50f29
rename from announce to supportsAnnounce
ash2moon Jun 13, 2025
1d220de
remove outdated comment
ash2moon Jun 13, 2025
3593935
Merge branch 'master' into feat/form-live-region
ash2moon Jun 13, 2025
1beffa9
rename from announce to supportsAnnounce
ash2moon Jun 13, 2025
ffb5304
Merge remote-tracking branch 'origin/feat/form-live-region' into feat…
ash2moon Jun 13, 2025
cc1107b
rename from announce to supportsAnnounce
ash2moon Jun 13, 2025
cd916fb
rename from announce to supportsAnnounce on engine
ash2moon Jun 13, 2025
d746e6d
rename from announce to supportsAnnounce on engine
ash2moon Jun 13, 2025
6af9a17
update docs on use of `supportsAnnounce`
ash2moon Jun 16, 2025
ab90d34
add missing file
ash2moon Jun 16, 2025
2b51741
add missing file
ash2moon Jun 17, 2025
f859495
Merge branch 'master' into feat/rename-announce(engine)
ash2moon Jun 17, 2025
3d87457
Merge branch 'feat/rename-announce(engine)' into feat/form-live-region
ash2moon Jun 17, 2025
3a51cb0
Merge branch 'master' into feat/form-live-region
ash2moon Jun 24, 2025
9978117
fix docs
ash2moon Jun 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 28 additions & 20 deletions packages/flutter/lib/src/material/input_decorator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -400,26 +400,33 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta

Widget _buildError() {
assert(widget.error != null || widget.errorText != null);
return Semantics(
container: true,
child: FadeTransition(
opacity: _controller,
child: FractionalTranslation(
translation: Tween<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,
),
),
),
final Widget? capturedError = widget.error;
final String? capturedErrorText = widget.errorText;
return Builder(
builder: (BuildContext context) {
return Semantics(
container: true,
liveRegion: !MediaQuery.supportsAnnounceOf(context),
child: FadeTransition(
opacity: _controller,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: const Offset(0.0, -0.25),
end: Offset.zero,
).evaluate(_controller.view),
child:
capturedError ??
Text(
capturedErrorText!,
style: widget.errorStyle,
textAlign: widget.textAlign,
overflow: TextOverflow.ellipsis,
maxLines: widget.errorMaxLines,
),
),
),
);
},
);
}

Expand Down Expand Up @@ -3898,6 +3905,7 @@ class InputDecoration {
bool? alignLabelWithHint,
BoxConstraints? constraints,
VisualDensity? visualDensity,
SemanticsService? semanticsService,
}) {
return InputDecoration(
icon: icon ?? this.icon,
Expand Down
13 changes: 2 additions & 11 deletions packages/flutter/lib/src/semantics/semantics_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,8 +33,8 @@ abstract final class SemanticsService {
/// Currently, this is only supported by the web engine and has no effect on
/// other platforms. The default mode is [Assertiveness.polite].
///
/// Not all platforms support announcements. Check to see if
/// [isAnnounceSupported] before calling this method.
/// Not all platforms support announcements. Check to see if it is supported using
/// [MediaQuery.supportsAnnounceOf] before calling this method.
///
/// ### Android
/// Android has [deprecated announcement events][1] due to its disruptive
Expand Down Expand Up @@ -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;
}
}
3 changes: 2 additions & 1 deletion packages/flutter/lib/src/widgets/form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -369,7 +370,7 @@ class FormState extends State<Form> {
}
}

if (errorMessage.isNotEmpty) {
if (errorMessage.isNotEmpty && MediaQuery.supportsAnnounceOf(context)) {
final TextDirection directionality = Directionality.of(context);
if (defaultTargetPlatform == TargetPlatform.iOS) {
unawaited(
Expand Down
51 changes: 51 additions & 0 deletions packages/flutter/lib/src/widgets/media_query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ enum _MediaQueryAspect {
/// Specifies the aspect corresponding to [MediaQueryData.boldText].
boldText,

/// Specifies the aspect corresponding to [MediaQueryData.supportsAnnounce].
supportsAnnounce,

/// Specifies the aspect corresponding to [MediaQueryData.navigationMode].
navigationMode,

Expand Down Expand Up @@ -210,6 +213,7 @@ class MediaQueryData {
this.onOffSwitchLabels = false,
this.disableAnimations = false,
this.boldText = false,
this.supportsAnnounce = false,
this.navigationMode = NavigationMode.traditional,
this.gestureSettings = const DeviceGestureSettings(touchSlop: kTouchSlop),
this.displayFeatures = const <ui.DisplayFeature>[],
Expand Down Expand Up @@ -295,6 +299,9 @@ class MediaQueryData {
platformData?.disableAnimations ??
view.platformDispatcher.accessibilityFeatures.disableAnimations,
boldText = platformData?.boldText ?? view.platformDispatcher.accessibilityFeatures.boldText,
supportsAnnounce =
platformData?.supportsAnnounce ??
view.platformDispatcher.accessibilityFeatures.supportsAnnounce,
highContrast =
platformData?.highContrast ?? view.platformDispatcher.accessibilityFeatures.highContrast,
onOffSwitchLabels =
Expand Down Expand Up @@ -584,6 +591,21 @@ class MediaQueryData {
/// originates.
final bool boldText;

/// Whether accessibility announcements (like [SemanticsService.announce])
/// are supported on the current platform.
///
/// Returns `false` on platforms where announcements are deprecated or
/// unsupported by the underlying platform.
///
/// Returns `true` on platforms where such announcements are
/// generally supported without discouragement. (iOS, web etc)
///
/// See also:
///
/// * [dart:ui.PlatformDispatcher.accessibilityFeatures], where the setting
/// originates.
final bool supportsAnnounce;

/// Describes the navigation mode requested by the platform.
///
/// Some user interfaces are better navigated using a directional pad (DPAD)
Expand Down Expand Up @@ -665,6 +687,7 @@ class MediaQueryData {
bool? invertColors,
bool? accessibleNavigation,
bool? boldText,
bool? supportsAnnounce,
NavigationMode? navigationMode,
DeviceGestureSettings? gestureSettings,
List<ui.DisplayFeature>? displayFeatures,
Expand All @@ -690,6 +713,7 @@ class MediaQueryData {
disableAnimations: disableAnimations ?? this.disableAnimations,
accessibleNavigation: accessibleNavigation ?? this.accessibleNavigation,
boldText: boldText ?? this.boldText,
supportsAnnounce: supportsAnnounce ?? this.supportsAnnounce,
navigationMode: navigationMode ?? this.navigationMode,
gestureSettings: gestureSettings ?? this.gestureSettings,
displayFeatures: displayFeatures ?? this.displayFeatures,
Expand Down Expand Up @@ -892,6 +916,7 @@ class MediaQueryData {
other.invertColors == invertColors &&
other.accessibleNavigation == accessibleNavigation &&
other.boldText == boldText &&
other.supportsAnnounce == supportsAnnounce &&
other.navigationMode == navigationMode &&
other.gestureSettings == gestureSettings &&
listEquals(other.displayFeatures, displayFeatures) &&
Expand Down Expand Up @@ -1713,6 +1738,30 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> {
static bool? maybeBoldTextOf(BuildContext context) =>
_maybeOf(context, _MediaQueryAspect.boldText)?.boldText;

/// Returns the [MediaQueryData.supportsAnnounce] accessibility setting for the
/// nearest [MediaQuery] ancestor or false, if no such ancestor exists.
///
/// Use of this method will cause the given [context] to rebuild any time that
/// the [MediaQueryData.supportsAnnounce] property of the ancestor [MediaQuery]
/// changes. This is especially important for supportsAnnounce because supportsAnnounce has a
/// low frequency change rate. The performance difference between rebuilding
/// for all media query data changes and only rebuilding for supportsAnnounce is a
/// dramatic difference.
///
Comment thread
ash2moon marked this conversation as resolved.
/// {@macro flutter.widgets.media_query.MediaQuery.dontUseOf}
static bool supportsAnnounceOf(BuildContext context) => maybeSupportsAnnounceOf(context) ?? false;

/// Returns the [MediaQueryData.supportsAnnounce] accessibility setting for the
/// nearest [MediaQuery] ancestor or null, if no such ancestor exists.
///
/// Use of this method will cause the given [context] to rebuild any time that
/// the [MediaQueryData.supportsAnnounce] property of the ancestor [MediaQuery]
/// changes.
///
/// {@macro flutter.widgets.media_query.MediaQuery.dontUseMaybeOf}
static bool? maybeSupportsAnnounceOf(BuildContext context) =>
_maybeOf(context, _MediaQueryAspect.supportsAnnounce)?.supportsAnnounce;

/// Returns [MediaQueryData.navigationMode] for the nearest [MediaQuery]
/// ancestor or throws an exception, if no such ancestor exists.
///
Expand Down Expand Up @@ -1843,6 +1892,8 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> {
_MediaQueryAspect.disableAnimations =>
data.disableAnimations != oldWidget.data.disableAnimations,
_MediaQueryAspect.boldText => data.boldText != oldWidget.data.boldText,
_MediaQueryAspect.supportsAnnounce =>
data.supportsAnnounce != oldWidget.data.supportsAnnounce,
_MediaQueryAspect.navigationMode =>
data.navigationMode != oldWidget.data.navigationMode,
_MediaQueryAspect.gestureSettings =>
Expand Down
37 changes: 37 additions & 0 deletions packages/flutter/test/material/input_decorator_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14899,4 +14899,41 @@ void main() {
final RenderBox box = tester.renderObject(find.byType(InputDecorator));
expect(box, isNot(paints..drrect()));
});

testWidgets(
'InputDecorator _buildError with errorText correctly updates on BuildContext updates',
(WidgetTester tester) async {
final ValueNotifier<String?> errorTextNotifier = ValueNotifier<String?>('initial error');
const String helperTextValue = 'helper text';

addTearDown(errorTextNotifier.dispose);

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ValueListenableBuilder<String?>(
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);
},
);
}
98 changes: 55 additions & 43 deletions packages/flutter/test/material/text_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9247,55 +9247,67 @@ void main() {
semantics.dispose();
});

testWidgets('InputDecoration errorText semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = _textEditingController();
final Key key = UniqueKey();
for (final bool supportsAnnounce in <bool>[true, false]) {
testWidgets('InputDecoration errorText semantics (supportsAnnounce=$supportsAnnounce)', (
WidgetTester tester,
) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = _textEditingController();
final Key key = UniqueKey();

await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
errorText: 'oh no!',
await tester.pumpWidget(
overlay(
child: MediaQuery(
data: MediaQueryData(supportsAnnounce: supportsAnnounce),
child: TextField(
key: key,
controller: controller,
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
errorText: 'oh no!',
),
),
),
),
),
);
);

expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
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),
],
),
],
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 (!supportsAnnounce) SemanticsFlag.isLiveRegion],
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
);

semantics.dispose();
});
semantics.dispose();
debugDefaultTargetPlatformOverride = null;
});
}

testWidgets('floating label does not overlap with value at large textScaleFactors', (
WidgetTester tester,
Expand Down
Loading