mirror of
https://github.com/flutter/flutter
synced 2024-08-27 12:00:58 +00:00
Use SemanticsService.announce to announce form text validation error (#123373)
Use SemanticsService.announce to announce form text validation error
This commit is contained in:
parent
dd3dc5efbf
commit
32b75f08fe
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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>>();
|
||||
|
|
Loading…
Reference in a new issue