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

Use SemanticsService.announce to announce form text validation error
This commit is contained in:
hangyu 2023-03-29 16:00:24 -07:00 committed by GitHub
parent dd3dc5efbf
commit 32b75f08fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 77 additions and 20 deletions

View file

@ -402,7 +402,6 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
assert(widget.errorText != null);
return Semantics(
container: true,
liveRegion: true,
child: FadeTransition(
opacity: _controller,
child: FractionalTranslation(

View file

@ -2,12 +2,21 @@
// Use of this source code is governed by a BSD-style license that can be
// 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 'navigator.dart';
import 'restoration.dart';
import 'restoration_properties.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:
// late BuildContext context;
@ -235,8 +244,22 @@ class FormState extends State<Form> {
bool _validate() {
bool hasError = false;
String errorMessage = '';
for (final FormFieldState<dynamic> field in _fields) {
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;
}

View file

@ -802,10 +802,9 @@ void main() {
testWidgets('cursor has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
child: const TextField(
),
),
overlay(
child: const TextField(),
),
);
final TextField textField = tester.firstWidget(find.byType(TextField));
@ -816,11 +815,11 @@ void main() {
testWidgets('cursor has expected radius value', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
child: const TextField(
cursorRadius: Radius.circular(3.0),
),
overlay(
child: const TextField(
cursorRadius: Radius.circular(3.0),
),
),
);
final TextField textField = tester.firstWidget(find.byType(TextField));
@ -831,8 +830,7 @@ void main() {
testWidgets('clipBehavior has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
child: const TextField(
),
child: const TextField(),
),
);
@ -8047,9 +8045,6 @@ void main() {
children: <TestSemantics>[
TestSemantics(
label: 'oh no!',
flags: <SemanticsFlag>[
SemanticsFlag.isLiveRegion,
],
textDirection: TextDirection.ltr,
),
],
@ -8066,16 +8061,16 @@ void main() {
MaterialApp(
home: Scaffold(
body: MediaQuery(
data: const MediaQueryData(textScaleFactor: 4.0),
child: Center(
child: TextField(
decoration: const InputDecoration(labelText: 'Label', border: UnderlineInputBorder()),
controller: controller,
),
data: const MediaQueryData(textScaleFactor: 4.0),
child: Center(
child: TextField(
decoration: const InputDecoration(labelText: 'Label', border: UnderlineInputBorder()),
controller: controller,
),
),
),
),
),
);
await tester.tap(find.byType(TextField));

View file

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@ -138,6 +139,45 @@ void main() {
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 {
final GlobalKey<FormFieldState<String>> fieldKey1 = GlobalKey<FormFieldState<String>>();
final GlobalKey<FormFieldState<String>> fieldKey2 = GlobalKey<FormFieldState<String>>();