Skip to content

Commit 32b75f0

Browse files
authored
Use SemanticsService.announce to announce form text validation error (#123373)
Use SemanticsService.announce to announce form text validation error
1 parent dd3dc5e commit 32b75f0

File tree

4 files changed

+77
-20
lines changed

4 files changed

+77
-20
lines changed

packages/flutter/lib/src/material/input_decorator.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,6 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
402402
assert(widget.errorText != null);
403403
return Semantics(
404404
container: true,
405-
liveRegion: true,
406405
child: FadeTransition(
407406
opacity: _controller,
408407
child: FractionalTranslation(

packages/flutter/lib/src/widgets/form.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:async';
6+
7+
import 'package:flutter/foundation.dart';
8+
import 'package:flutter/rendering.dart';
9+
10+
import 'basic.dart';
511
import 'framework.dart';
612
import 'navigator.dart';
713
import 'restoration.dart';
814
import 'restoration_properties.dart';
915
import 'will_pop_scope.dart';
1016

17+
// Duration for delay before announcement in IOS so that the announcement won't be interrupted.
18+
const Duration _kIOSAnnouncementDelayDuration = Duration(seconds: 1);
19+
1120
// Examples can assume:
1221
// late BuildContext context;
1322

@@ -235,8 +244,22 @@ class FormState extends State<Form> {
235244

236245
bool _validate() {
237246
bool hasError = false;
247+
String errorMessage = '';
238248
for (final FormFieldState<dynamic> field in _fields) {
239249
hasError = !field.validate() || hasError;
250+
errorMessage += field.errorText ?? '';
251+
}
252+
253+
if(errorMessage.isNotEmpty) {
254+
final TextDirection directionality = Directionality.of(context);
255+
if (defaultTargetPlatform == TargetPlatform.iOS) {
256+
unawaited(Future<void>(() async {
257+
await Future<void>.delayed(_kIOSAnnouncementDelayDuration);
258+
SemanticsService.announce(errorMessage, directionality, assertiveness: Assertiveness.assertive);
259+
}));
260+
} else {
261+
SemanticsService.announce(errorMessage, directionality, assertiveness: Assertiveness.assertive);
262+
}
240263
}
241264
return !hasError;
242265
}

packages/flutter/test/material/text_field_test.dart

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -802,10 +802,9 @@ void main() {
802802

803803
testWidgets('cursor has expected defaults', (WidgetTester tester) async {
804804
await tester.pumpWidget(
805-
overlay(
806-
child: const TextField(
807-
),
808-
),
805+
overlay(
806+
child: const TextField(),
807+
),
809808
);
810809

811810
final TextField textField = tester.firstWidget(find.byType(TextField));
@@ -816,11 +815,11 @@ void main() {
816815

817816
testWidgets('cursor has expected radius value', (WidgetTester tester) async {
818817
await tester.pumpWidget(
819-
overlay(
820-
child: const TextField(
821-
cursorRadius: Radius.circular(3.0),
822-
),
818+
overlay(
819+
child: const TextField(
820+
cursorRadius: Radius.circular(3.0),
823821
),
822+
),
824823
);
825824

826825
final TextField textField = tester.firstWidget(find.byType(TextField));
@@ -831,8 +830,7 @@ void main() {
831830
testWidgets('clipBehavior has expected defaults', (WidgetTester tester) async {
832831
await tester.pumpWidget(
833832
overlay(
834-
child: const TextField(
835-
),
833+
child: const TextField(),
836834
),
837835
);
838836

@@ -8047,9 +8045,6 @@ void main() {
80478045
children: <TestSemantics>[
80488046
TestSemantics(
80498047
label: 'oh no!',
8050-
flags: <SemanticsFlag>[
8051-
SemanticsFlag.isLiveRegion,
8052-
],
80538048
textDirection: TextDirection.ltr,
80548049
),
80558050
],
@@ -8066,16 +8061,16 @@ void main() {
80668061
MaterialApp(
80678062
home: Scaffold(
80688063
body: MediaQuery(
8069-
data: const MediaQueryData(textScaleFactor: 4.0),
8070-
child: Center(
8071-
child: TextField(
8072-
decoration: const InputDecoration(labelText: 'Label', border: UnderlineInputBorder()),
8073-
controller: controller,
8074-
),
8064+
data: const MediaQueryData(textScaleFactor: 4.0),
8065+
child: Center(
8066+
child: TextField(
8067+
decoration: const InputDecoration(labelText: 'Label', border: UnderlineInputBorder()),
8068+
controller: controller,
80758069
),
80768070
),
80778071
),
80788072
),
8073+
),
80798074
);
80808075

80818076
await tester.tap(find.byType(TextField));

packages/flutter/test/widgets/form_test.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'package:flutter/material.dart';
6+
import 'package:flutter/rendering.dart';
67
import 'package:flutter/services.dart';
78
import 'package:flutter_test/flutter_test.dart';
89

@@ -138,6 +139,45 @@ void main() {
138139
await checkErrorText('');
139140
});
140141

142+
testWidgets('Should announce error text when validate returns error', (WidgetTester tester) async {
143+
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
144+
await tester.pumpWidget(
145+
MaterialApp(
146+
home: MediaQuery(
147+
data: const MediaQueryData(),
148+
child: Directionality(
149+
textDirection: TextDirection.ltr,
150+
child: Center(
151+
child: Material(
152+
child: Form(
153+
key: formKey,
154+
child: TextFormField(
155+
validator: (_)=> 'error',
156+
),
157+
),
158+
),
159+
),
160+
),
161+
),
162+
),
163+
);
164+
formKey.currentState!.reset();
165+
await tester.enterText(find.byType(TextFormField), '');
166+
await tester.pump();
167+
168+
// Manually validate.
169+
expect(find.text('error'), findsNothing);
170+
formKey.currentState!.validate();
171+
await tester.pump();
172+
expect(find.text('error'), findsOneWidget);
173+
174+
final CapturedAccessibilityAnnouncement announcement = tester.takeAnnouncements().single;
175+
expect(announcement.message, 'error');
176+
expect(announcement.textDirection, TextDirection.ltr);
177+
expect(announcement.assertiveness, Assertiveness.assertive);
178+
179+
});
180+
141181
testWidgets('isValid returns true when a field is valid', (WidgetTester tester) async {
142182
final GlobalKey<FormFieldState<String>> fieldKey1 = GlobalKey<FormFieldState<String>>();
143183
final GlobalKey<FormFieldState<String>> fieldKey2 = GlobalKey<FormFieldState<String>>();

0 commit comments

Comments
 (0)