Unverified Commit 32b75f08 authored by hangyu's avatar hangyu Committed by GitHub

Use SemanticsService.announce to announce form text validation error (#123373)

Use SemanticsService.announce to announce form text validation error
parent dd3dc5ef
...@@ -402,7 +402,6 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -402,7 +402,6 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
assert(widget.errorText != null); assert(widget.errorText != null);
return Semantics( return Semantics(
container: true, container: true,
liveRegion: true,
child: FadeTransition( child: FadeTransition(
opacity: _controller, opacity: _controller,
child: FractionalTranslation( child: FractionalTranslation(
......
...@@ -2,12 +2,21 @@ ...@@ -2,12 +2,21 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'navigator.dart'; import 'navigator.dart';
import 'restoration.dart'; import 'restoration.dart';
import 'restoration_properties.dart'; import 'restoration_properties.dart';
import 'will_pop_scope.dart'; import 'will_pop_scope.dart';
// Duration for delay before announcement in IOS so that the announcement won't be interrupted.
const Duration _kIOSAnnouncementDelayDuration = Duration(seconds: 1);
// Examples can assume: // Examples can assume:
// late BuildContext context; // late BuildContext context;
...@@ -235,8 +244,22 @@ class FormState extends State<Form> { ...@@ -235,8 +244,22 @@ class FormState extends State<Form> {
bool _validate() { bool _validate() {
bool hasError = false; bool hasError = false;
String errorMessage = '';
for (final FormFieldState<dynamic> field in _fields) { for (final FormFieldState<dynamic> field in _fields) {
hasError = !field.validate() || hasError; hasError = !field.validate() || hasError;
errorMessage += field.errorText ?? '';
}
if(errorMessage.isNotEmpty) {
final TextDirection directionality = Directionality.of(context);
if (defaultTargetPlatform == TargetPlatform.iOS) {
unawaited(Future<void>(() async {
await Future<void>.delayed(_kIOSAnnouncementDelayDuration);
SemanticsService.announce(errorMessage, directionality, assertiveness: Assertiveness.assertive);
}));
} else {
SemanticsService.announce(errorMessage, directionality, assertiveness: Assertiveness.assertive);
}
} }
return !hasError; return !hasError;
} }
......
...@@ -802,10 +802,9 @@ void main() { ...@@ -802,10 +802,9 @@ void main() {
testWidgets('cursor has expected defaults', (WidgetTester tester) async { testWidgets('cursor has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const TextField( child: const TextField(),
), ),
),
); );
final TextField textField = tester.firstWidget(find.byType(TextField)); final TextField textField = tester.firstWidget(find.byType(TextField));
...@@ -816,11 +815,11 @@ void main() { ...@@ -816,11 +815,11 @@ void main() {
testWidgets('cursor has expected radius value', (WidgetTester tester) async { testWidgets('cursor has expected radius value', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const TextField( child: const TextField(
cursorRadius: Radius.circular(3.0), cursorRadius: Radius.circular(3.0),
),
), ),
),
); );
final TextField textField = tester.firstWidget(find.byType(TextField)); final TextField textField = tester.firstWidget(find.byType(TextField));
...@@ -831,8 +830,7 @@ void main() { ...@@ -831,8 +830,7 @@ void main() {
testWidgets('clipBehavior has expected defaults', (WidgetTester tester) async { testWidgets('clipBehavior has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const TextField( child: const TextField(),
),
), ),
); );
...@@ -8047,9 +8045,6 @@ void main() { ...@@ -8047,9 +8045,6 @@ void main() {
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics( TestSemantics(
label: 'oh no!', label: 'oh no!',
flags: <SemanticsFlag>[
SemanticsFlag.isLiveRegion,
],
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
], ],
...@@ -8066,16 +8061,16 @@ void main() { ...@@ -8066,16 +8061,16 @@ void main() {
MaterialApp( MaterialApp(
home: Scaffold( home: Scaffold(
body: MediaQuery( body: MediaQuery(
data: const MediaQueryData(textScaleFactor: 4.0), data: const MediaQueryData(textScaleFactor: 4.0),
child: Center( child: Center(
child: TextField( child: TextField(
decoration: const InputDecoration(labelText: 'Label', border: UnderlineInputBorder()), decoration: const InputDecoration(labelText: 'Label', border: UnderlineInputBorder()),
controller: controller, controller: controller,
),
), ),
), ),
), ),
), ),
),
); );
await tester.tap(find.byType(TextField)); await tester.tap(find.byType(TextField));
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -138,6 +139,45 @@ void main() { ...@@ -138,6 +139,45 @@ void main() {
await checkErrorText(''); await checkErrorText('');
}); });
testWidgets('Should announce error text when validate returns error', (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: TextFormField(
validator: (_)=> 'error',
),
),
),
),
),
),
),
);
formKey.currentState!.reset();
await tester.enterText(find.byType(TextFormField), '');
await tester.pump();
// Manually validate.
expect(find.text('error'), findsNothing);
formKey.currentState!.validate();
await tester.pump();
expect(find.text('error'), findsOneWidget);
final CapturedAccessibilityAnnouncement announcement = tester.takeAnnouncements().single;
expect(announcement.message, 'error');
expect(announcement.textDirection, TextDirection.ltr);
expect(announcement.assertiveness, Assertiveness.assertive);
});
testWidgets('isValid returns true when a field is valid', (WidgetTester tester) async { testWidgets('isValid returns true when a field is valid', (WidgetTester tester) async {
final GlobalKey<FormFieldState<String>> fieldKey1 = GlobalKey<FormFieldState<String>>(); final GlobalKey<FormFieldState<String>> fieldKey1 = GlobalKey<FormFieldState<String>>();
final GlobalKey<FormFieldState<String>> fieldKey2 = GlobalKey<FormFieldState<String>>(); final GlobalKey<FormFieldState<String>> fieldKey2 = GlobalKey<FormFieldState<String>>();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment