From a714c97c0bef47c316f877d515dede2c32b386e8 Mon Sep 17 00:00:00 2001 From: Darren Austin Date: Mon, 14 Jun 2021 11:24:02 -0700 Subject: [PATCH] Ensure the Autocomplete options scroll as needed. (#83863) --- .../lib/src/material/autocomplete.dart | 20 +++-- .../test/material/autocomplete_test.dart | 86 +++++++++++++++---- 2 files changed, 85 insertions(+), 21 deletions(-) diff --git a/packages/flutter/lib/src/material/autocomplete.dart b/packages/flutter/lib/src/material/autocomplete.dart index c4e5d5397a..19b0d5e7c2 100644 --- a/packages/flutter/lib/src/material/autocomplete.dart +++ b/packages/flutter/lib/src/material/autocomplete.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'ink_well.dart'; @@ -292,15 +293,24 @@ class _AutocompleteOptions extends StatelessWidget { itemCount: options.length, itemBuilder: (BuildContext context, int index) { final T option = options.elementAt(index); - final bool highlight = AutocompleteHighlightedOption.of(context) == index; return InkWell( onTap: () { onSelected(option); }, - child: Container( - color: highlight ? Theme.of(context).focusColor : null, - padding: const EdgeInsets.all(16.0), - child: Text(displayStringForOption(option)), + child: Builder( + builder: (BuildContext context) { + final bool highlight = AutocompleteHighlightedOption.of(context) == index; + if (highlight) { + SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) { + Scrollable.ensureVisible(context, alignment: 0.5); + }); + } + return Container( + color: highlight ? Theme.of(context).focusColor : null, + padding: const EdgeInsets.all(16.0), + child: Text(displayStringForOption(option)), + ); + } ), ); }, diff --git a/packages/flutter/test/material/autocomplete_test.dart b/packages/flutter/test/material/autocomplete_test.dart index 4f12097b3f..641befef40 100644 --- a/packages/flutter/test/material/autocomplete_test.dart +++ b/packages/flutter/test/material/autocomplete_test.dart @@ -402,20 +402,21 @@ void main() { expect(lastSelection, 'lemur'); }); - testWidgets('keyboard navigation of the options properly highlights the option', (WidgetTester tester) async { - - void checkOptionHighlight(String label, Color? color) { - final RenderBox renderBox = tester.renderObject(find.ancestor(matching: find.byType(Container), of: find.text(label))); - if (color != null) { - // Check to see that the container is painted with the highlighted background color. - expect(renderBox, paints..rect(color: color)); - } else { - // There should only be a paragraph painted. - expect(renderBox, paintsExactlyCountTimes(const Symbol('drawRect'), 0)); - expect(renderBox, paints..paragraph()); - } + // Ensures that the option with the given label has a given background color + // if given, or no background if color is null. + void checkOptionHighlight(WidgetTester tester, String label, Color? color) { + final RenderBox renderBox = tester.renderObject(find.ancestor(matching: find.byType(Container), of: find.text(label))); + if (color != null) { + // Check to see that the container is painted with the highlighted background color. + expect(renderBox, paints..rect(color: color)); + } else { + // There should only be a paragraph painted. + expect(renderBox, paintsExactlyCountTimes(const Symbol('drawRect'), 0)); + expect(renderBox, paints..paragraph()); } + } + testWidgets('keyboard navigation of the options properly highlights the option', (WidgetTester tester) async { const Color highlightColor = Color(0xFF112233); await tester.pumpWidget( MaterialApp( @@ -442,15 +443,68 @@ void main() { expect(list.semanticChildCount, 2); // Initially the first option should be highlighted - checkOptionHighlight('chameleon', highlightColor); - checkOptionHighlight('elephant', null); + checkOptionHighlight(tester, 'chameleon', highlightColor); + checkOptionHighlight(tester, 'elephant', null); // Move the selection down await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); // Highlight should be moved to the second item - checkOptionHighlight('chameleon', null); - checkOptionHighlight('elephant', highlightColor); + checkOptionHighlight(tester, 'chameleon', null); + checkOptionHighlight(tester, 'elephant', highlightColor); + }); + + testWidgets('keyboard navigation keeps the highlighted option scrolled into view', (WidgetTester tester) async { + const Color highlightColor = Color(0xFF112233); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData.light().copyWith( + focusColor: highlightColor, + ), + home: Scaffold( + body: Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + return kOptions.where((String option) { + return option.contains(textEditingValue.text.toLowerCase()); + }); + }, + ), + ), + ), + ); + + await tester.tap(find.byType(TextFormField)); + await tester.enterText(find.byType(TextFormField), 'e'); + await tester.pump(); + expect(find.byType(ListView), findsOneWidget); + final ListView list = find.byType(ListView).evaluate().first.widget as ListView; + expect(list.semanticChildCount, 6); + + // Highlighted item should be at the top + expect(tester.getTopLeft(find.text('chameleon')).dy, equals(64.0)); + checkOptionHighlight(tester, 'chameleon', highlightColor); + + // Move down the list of options + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + await tester.pump(); + + // First item should have scrolled off the top, and not be selected. + expect(find.text('chameleon'), findsNothing); + + // Highlighted item 'lemur' should be centered in the options popup + expect(tester.getTopLeft(find.text('mouse')).dy, equals(187.0)); + checkOptionHighlight(tester, 'mouse', highlightColor); + + // The other items on screen should not be selected. + checkOptionHighlight(tester, 'goose', null); + checkOptionHighlight(tester, 'lemur', null); + checkOptionHighlight(tester, 'northern white rhinoceros', null); }); }