Skip to content

Commit e68da97

Browse files
authored
[VPAT][A11y] Announce Autocomplete search results status (flutter#173480)
Status announcements 'Search results found' and 'no results found' were observed on a search view on the iOS sheets app. Fixes [[VPAT][A11y] autocomplete must announce status when search result is available](flutter#173064) Fixes b/429094918
1 parent ee3551f commit e68da97

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+818
-78
lines changed

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import 'editable_text.dart';
1919
import 'focus_manager.dart';
2020
import 'framework.dart';
2121
import 'inherited_notifier.dart';
22+
import 'localizations.dart';
23+
import 'media_query.dart';
2224
import 'overlay.dart';
2325
import 'shortcuts.dart';
2426
import 'tap_region.dart';
@@ -404,6 +406,17 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
404406
}
405407
}
406408

409+
void _announceSemantics(bool resultsAvailable) {
410+
if (!MediaQuery.supportsAnnounceOf(context)) {
411+
return;
412+
}
413+
final WidgetsLocalizations localizations = WidgetsLocalizations.of(context);
414+
final String optionsHint = resultsAvailable
415+
? localizations.searchResultsFound
416+
: localizations.noResultsFound;
417+
SemanticsService.announce(optionsHint, localizations.textDirection);
418+
}
419+
407420
// Assigning an ID to every call of _onChangedField is necessary to avoid a
408421
// situation where _options is updated by an older call when multiple
409422
// _onChangedField calls are running simultaneously.
@@ -426,6 +439,9 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
426439
if (callId != _onChangedCallId || !shouldUpdateOptions) {
427440
return;
428441
}
442+
if (_options.isEmpty != options.isEmpty) {
443+
_announceSemantics(options.isNotEmpty);
444+
}
429445
_options = options;
430446
_updateHighlight(_highlightedOptionIndex.value);
431447
final T? selection = _selection;

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,14 @@ abstract class WidgetsLocalizations {
194194
/// list one space right in the list.
195195
String get reorderItemRight;
196196

197+
/// The semantics label used for [RawAutocomplete] when the options list goes
198+
/// from empty to non-empty.
199+
String get searchResultsFound => 'Search results found';
200+
201+
/// The semantics label used for [RawAutocomplete] when the options list goes
202+
/// from non-empty to empty.
203+
String get noResultsFound => 'No results found';
204+
197205
/// Label for "copy" edit buttons and menu items.
198206
String get copyButtonLabel;
199207

@@ -284,6 +292,12 @@ class DefaultWidgetsLocalizations implements WidgetsLocalizations {
284292
@override
285293
String get reorderItemToStart => 'Move to the start';
286294

295+
@override
296+
String get searchResultsFound => 'Search results found';
297+
298+
@override
299+
String get noResultsFound => 'No results found';
300+
287301
@override
288302
String get copyButtonLabel => 'Copy';
289303

packages/flutter/test/widgets/autocomplete_test.dart

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3271,6 +3271,78 @@ void main() {
32713271
expect(find.byKey(optionsKey), findsOneWidget);
32723272
});
32733273

3274+
testWidgets('Autocomplete Semantics announcement', (WidgetTester tester) async {
3275+
final SemanticsHandle handle = tester.ensureSemantics();
3276+
final GlobalKey fieldKey = GlobalKey();
3277+
final GlobalKey optionsKey = GlobalKey();
3278+
late Iterable<String> lastOptions;
3279+
late FocusNode focusNode;
3280+
late TextEditingController textEditingController;
3281+
const DefaultWidgetsLocalizations localizations = DefaultWidgetsLocalizations();
3282+
3283+
await tester.pumpWidget(
3284+
MaterialApp(
3285+
home: Scaffold(
3286+
body: RawAutocomplete<String>(
3287+
optionsBuilder: (TextEditingValue textEditingValue) {
3288+
return kOptions.where((String option) {
3289+
return option.contains(textEditingValue.text.toLowerCase());
3290+
});
3291+
},
3292+
fieldViewBuilder:
3293+
(
3294+
BuildContext context,
3295+
TextEditingController fieldTextEditingController,
3296+
FocusNode fieldFocusNode,
3297+
VoidCallback onFieldSubmitted,
3298+
) {
3299+
focusNode = fieldFocusNode;
3300+
textEditingController = fieldTextEditingController;
3301+
return TextField(
3302+
key: fieldKey,
3303+
focusNode: focusNode,
3304+
controller: textEditingController,
3305+
);
3306+
},
3307+
optionsViewBuilder:
3308+
(
3309+
BuildContext context,
3310+
AutocompleteOnSelected<String> onSelected,
3311+
Iterable<String> options,
3312+
) {
3313+
lastOptions = options;
3314+
return Container(key: optionsKey);
3315+
},
3316+
),
3317+
),
3318+
),
3319+
);
3320+
3321+
expect(find.byKey(fieldKey), findsOneWidget);
3322+
expect(find.byKey(optionsKey), findsNothing);
3323+
3324+
expect(tester.takeAnnouncements(), isEmpty);
3325+
3326+
focusNode.requestFocus();
3327+
await tester.pump();
3328+
expect(find.byKey(optionsKey), findsOneWidget);
3329+
expect(lastOptions.length, kOptions.length);
3330+
expect(tester.takeAnnouncements().first.message, localizations.searchResultsFound);
3331+
3332+
await tester.enterText(find.byKey(fieldKey), 'a');
3333+
await tester.pump();
3334+
expect(find.byKey(optionsKey), findsOneWidget);
3335+
expect(lastOptions.length, greaterThan(0));
3336+
expect(tester.takeAnnouncements(), isEmpty);
3337+
3338+
await tester.enterText(find.byKey(fieldKey), 'zzzz');
3339+
await tester.pump();
3340+
expect(find.byKey(optionsKey), findsNothing);
3341+
expect(tester.takeAnnouncements().first.message, localizations.noResultsFound);
3342+
3343+
handle.dispose();
3344+
});
3345+
32743346
testWidgets('RawAutocomplete renders at zero area', (WidgetTester tester) async {
32753347
await tester.pumpWidget(
32763348
MaterialApp(

packages/flutter/test/widgets/localizations_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ void main() {
2222
expect(localizations.reorderItemRight, isNotNull);
2323
expect(localizations.reorderItemToEnd, isNotNull);
2424
expect(localizations.reorderItemToStart, isNotNull);
25+
expect(localizations.searchResultsFound, isNotNull);
26+
expect(localizations.noResultsFound, isNotNull);
2527
expect(localizations.copyButtonLabel, isNotNull);
2628
expect(localizations.cutButtonLabel, isNotNull);
2729
expect(localizations.pasteButtonLabel, isNotNull);

0 commit comments

Comments
 (0)