From 010c8a806bccad64ab9972286b85dd24ca98441f Mon Sep 17 00:00:00 2001 From: jacobsimionato Date: Thu, 16 Jan 2025 06:01:59 +1030 Subject: [PATCH] Revert "Autocomplete Options Width" (#161666) Reverts flutter/flutter#143249 This broke some Google tests sadly - see b/390128156 --- .../lib/src/material/autocomplete.dart | 8 +- .../flutter/lib/src/widgets/autocomplete.dart | 276 +--- .../test/material/autocomplete_test.dart | 1 - .../test/widgets/autocomplete_test.dart | 1336 +---------------- 4 files changed, 98 insertions(+), 1523 deletions(-) diff --git a/packages/flutter/lib/src/material/autocomplete.dart b/packages/flutter/lib/src/material/autocomplete.dart index d917393cd4..6f3ca62653 100644 --- a/packages/flutter/lib/src/material/autocomplete.dart +++ b/packages/flutter/lib/src/material/autocomplete.dart @@ -134,7 +134,7 @@ class Autocomplete extends StatelessWidget { onSelected: onSelected, options: options, openDirection: optionsViewOpenDirection, - optionsMaxHeight: optionsMaxHeight, + maxOptionsHeight: optionsMaxHeight, ); }, onSelected: onSelected, @@ -176,7 +176,7 @@ class _AutocompleteOptions extends StatelessWidget { required this.onSelected, required this.openDirection, required this.options, - required this.optionsMaxHeight, + required this.maxOptionsHeight, }); final AutocompleteOptionToString displayStringForOption; @@ -185,7 +185,7 @@ class _AutocompleteOptions extends StatelessWidget { final OptionsViewOpenDirection openDirection; final Iterable options; - final double optionsMaxHeight; + final double maxOptionsHeight; @override Widget build(BuildContext context) { @@ -198,7 +198,7 @@ class _AutocompleteOptions extends StatelessWidget { child: Material( elevation: 4.0, child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: optionsMaxHeight), + constraints: BoxConstraints(maxHeight: maxOptionsHeight), child: ListView.builder( padding: EdgeInsets.zero, shrinkWrap: true, diff --git a/packages/flutter/lib/src/widgets/autocomplete.dart b/packages/flutter/lib/src/widgets/autocomplete.dart index 958ce9e4ae..8cd23f5635 100644 --- a/packages/flutter/lib/src/widgets/autocomplete.dart +++ b/packages/flutter/lib/src/widgets/autocomplete.dart @@ -6,24 +6,18 @@ library; import 'dart:async'; -import 'dart:math' show max, min; -import 'package:flutter/rendering.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'actions.dart'; import 'basic.dart'; -import 'constants.dart'; import 'editable_text.dart'; import 'focus_manager.dart'; import 'framework.dart'; import 'inherited_notifier.dart'; -import 'layout_builder.dart'; import 'overlay.dart'; import 'shortcuts.dart'; import 'tap_region.dart'; -import 'value_listenable_builder.dart'; // Examples can assume: // late BuildContext context; @@ -219,10 +213,10 @@ class RawAutocomplete extends StatefulWidget { /// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder} /// Builds the selectable options widgets from a list of options objects. /// - /// The options are displayed floating below or above the field inside of an - /// [Overlay], not at the same place in the widget tree as [RawAutocomplete]. - /// To control whether it opens upward or downward, use - /// [optionsViewOpenDirection]. + /// The options are displayed floating below or above the field using a + /// [CompositedTransformFollower] inside of an [Overlay], not at the same + /// place in the widget tree as [RawAutocomplete]. To control whether it opens + /// upward or downward, use [optionsViewOpenDirection]. /// /// In order to track which item is highlighted by keyboard navigation, the /// resulting options will be wrapped in an inherited @@ -313,10 +307,6 @@ class RawAutocomplete extends StatefulWidget { class _RawAutocompleteState extends State> { final GlobalKey _fieldKey = GlobalKey(); final LayerLink _optionsLayerLink = LayerLink(); - - /// The box constraints that the field was last built with. - final ValueNotifier _fieldBoxConstraints = ValueNotifier(null); - final OverlayPortalController _optionsViewController = OverlayPortalController( debugLabel: '_RawAutocompleteState', ); @@ -449,22 +439,30 @@ class _RawAutocompleteState extends State> } Widget _buildOptionsView(BuildContext context) { - return ValueListenableBuilder( - valueListenable: _fieldBoxConstraints, - builder: (BuildContext context, BoxConstraints? constraints, Widget? child) { - return _RawAutocompleteOptions( - fieldKey: _fieldKey, - optionsLayerLink: _optionsLayerLink, - optionsViewOpenDirection: widget.optionsViewOpenDirection, - overlayContext: context, - textDirection: Directionality.maybeOf(context), + final TextDirection textDirection = Directionality.of(context); + final Alignment followerAlignment = switch (widget.optionsViewOpenDirection) { + OptionsViewOpenDirection.up => AlignmentDirectional.bottomStart, + OptionsViewOpenDirection.down => AlignmentDirectional.topStart, + }.resolve(textDirection); + final Alignment targetAnchor = switch (widget.optionsViewOpenDirection) { + OptionsViewOpenDirection.up => AlignmentDirectional.topStart, + OptionsViewOpenDirection.down => AlignmentDirectional.bottomStart, + }.resolve(textDirection); + + return CompositedTransformFollower( + link: _optionsLayerLink, + showWhenUnlinked: false, + targetAnchor: targetAnchor, + followerAnchor: followerAlignment, + child: TextFieldTapRegion( + child: AutocompleteHighlightedOption( highlightIndexNotifier: _highlightedOptionIndex, - fieldConstraints: _fieldBoxConstraints.value!, - builder: (BuildContext context) { - return widget.optionsViewBuilder(context, _select, _options); - }, - ); - }, + child: Builder( + builder: + (BuildContext context) => widget.optionsViewBuilder(context, _select, _options), + ), + ), + ), ); } @@ -506,7 +504,6 @@ class _RawAutocompleteState extends State> widget.focusNode?.removeListener(_updateOptionsViewVisibility); _internalFocusNode?.dispose(); _highlightedOptionIndex.dispose(); - _fieldBoxConstraints.dispose(); super.dispose(); } @@ -520,224 +517,25 @@ class _RawAutocompleteState extends State> _onFieldSubmitted, ) ?? const SizedBox.shrink(); - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - // TODO(victorsanni): Also track the width of the field box so that the - // options view maintains the same width as the field if its width - // changes but its constraints remain unchanged. - _fieldBoxConstraints.value = constraints; - return OverlayPortal.targetsRootOverlay( + return OverlayPortal.targetsRootOverlay( + controller: _optionsViewController, + overlayChildBuilder: _buildOptionsView, + child: TextFieldTapRegion( + child: SizedBox( key: _fieldKey, - controller: _optionsViewController, - overlayChildBuilder: _buildOptionsView, - child: TextFieldTapRegion( - child: Shortcuts( - shortcuts: _shortcuts, - child: Actions( - actions: _actionMap, - child: CompositedTransformTarget(link: _optionsLayerLink, child: fieldView), - ), + child: Shortcuts( + shortcuts: _shortcuts, + child: Actions( + actions: _actionMap, + child: CompositedTransformTarget(link: _optionsLayerLink, child: fieldView), ), ), - ); - }, - ); - } -} - -class _RawAutocompleteOptions extends StatefulWidget { - const _RawAutocompleteOptions({ - required this.fieldKey, - required this.optionsLayerLink, - required this.optionsViewOpenDirection, - required this.overlayContext, - required this.textDirection, - required this.highlightIndexNotifier, - required this.builder, - required this.fieldConstraints, - }); - - final WidgetBuilder builder; - final GlobalKey fieldKey; - - final LayerLink optionsLayerLink; - final OptionsViewOpenDirection optionsViewOpenDirection; - final BuildContext overlayContext; - final TextDirection? textDirection; - final ValueNotifier highlightIndexNotifier; - final BoxConstraints fieldConstraints; - - @override - State<_RawAutocompleteOptions> createState() => _RawAutocompleteOptionsState(); -} - -class _RawAutocompleteOptionsState extends State<_RawAutocompleteOptions> { - VoidCallback? removeCompositionCallback; - Offset fieldOffset = Offset.zero; - - // Get the field offset if the field's position changes when its layer tree - // is composited, which occurs for example if the field is in a scroll view. - Offset _getFieldOffset() { - final RenderBox? fieldRenderBox = - widget.fieldKey.currentContext?.findRenderObject() as RenderBox?; - final RenderBox? overlay = - Overlay.of(widget.overlayContext).context.findRenderObject() as RenderBox?; - return fieldRenderBox?.localToGlobal(Offset.zero, ancestor: overlay) ?? Offset.zero; - } - - void _onLeaderComposition(Layer leaderLayer) { - SchedulerBinding.instance.addPostFrameCallback((Duration duration) { - if (!mounted) { - return; - } - final Offset nextFieldOffset = _getFieldOffset(); - if (nextFieldOffset != fieldOffset) { - setState(() { - fieldOffset = nextFieldOffset; - }); - } - }); - } - - @override - void initState() { - super.initState(); - removeCompositionCallback = widget.optionsLayerLink.leader?.addCompositionCallback( - _onLeaderComposition, - ); - } - - @override - void didUpdateWidget(_RawAutocompleteOptions oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.optionsLayerLink.leader != oldWidget.optionsLayerLink.leader) { - removeCompositionCallback?.call(); - removeCompositionCallback = widget.optionsLayerLink.leader?.addCompositionCallback( - _onLeaderComposition, - ); - } - } - - @override - void dispose() { - removeCompositionCallback?.call(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return CompositedTransformFollower( - link: widget.optionsLayerLink, - followerAnchor: switch (widget.optionsViewOpenDirection) { - OptionsViewOpenDirection.up => Alignment.bottomLeft, - OptionsViewOpenDirection.down => Alignment.topLeft, - }, - // When the field goes offscreen, don't show the options. - showWhenUnlinked: false, - child: CustomSingleChildLayout( - delegate: _RawAutocompleteOptionsLayoutDelegate( - layerLink: widget.optionsLayerLink, - fieldOffset: fieldOffset, - optionsViewOpenDirection: widget.optionsViewOpenDirection, - textDirection: Directionality.of(context), - fieldConstraints: widget.fieldConstraints, - ), - child: TextFieldTapRegion( - child: AutocompleteHighlightedOption( - highlightIndexNotifier: widget.highlightIndexNotifier, - // optionsViewBuilder must be able to look up - // AutocompleteHighlightedOption in its context. - child: Builder(builder: widget.builder), - ), ), ), ); } } -/// Positions the options view. -class _RawAutocompleteOptionsLayoutDelegate extends SingleChildLayoutDelegate { - _RawAutocompleteOptionsLayoutDelegate({ - required this.layerLink, - required this.fieldOffset, - required this.optionsViewOpenDirection, - required this.textDirection, - required this.fieldConstraints, - }) : assert(layerLink.leaderSize != null); - - /// Links the options in [RawAutocomplete.optionsViewBuilder] to the field in - /// [RawAutocomplete.fieldViewBuilder]. - final LayerLink layerLink; - - /// The position of the field in [RawAutocomplete.fieldViewBuilder]. - final Offset fieldOffset; - - /// A direction in which to open the options view overlay. - final OptionsViewOpenDirection optionsViewOpenDirection; - - /// The [TextDirection] of this part of the widget tree. - final TextDirection textDirection; - - /// The [BoxConstraints] for the field in [RawAutocomplete.fieldViewBuilder]. - final BoxConstraints fieldConstraints; - - // A big enough height for about one item in the default - // Autocomplete.optionsViewBuilder. The assumption is that the user likely - // wants the list of options to move to stay on the screen rather than get any - // smaller than this. Allows Autocomplete to work when it has very little - // screen height available (as in b/317115348) by positioning itself on top of - // the field, while in other cases to size itself based on the height under - // the field. - static const double _kMinUsableHeight = kMinInteractiveDimension; - - // Limits the child to the space above/below the field, with a minimum, and - // with the same maxWidth constraint as the field has. - @override - BoxConstraints getConstraintsForChild(BoxConstraints constraints) { - final Size fieldSize = layerLink.leaderSize!; - return BoxConstraints( - // The field width may be zero if this is a split RawAutocomplete with no - // field of its own. In that case, don't change the constraints width. - maxWidth: fieldSize.width == 0.0 ? constraints.maxWidth : fieldSize.width, - maxHeight: max(_kMinUsableHeight, switch (optionsViewOpenDirection) { - OptionsViewOpenDirection.down => constraints.maxHeight - fieldOffset.dy - fieldSize.height, - OptionsViewOpenDirection.up => fieldOffset.dy, - }), - ); - } - - // Positions the child above/below the field and aligned with the left/right - // side based on text direction. - @override - Offset getPositionForChild(Size size, Size childSize) { - final Size fieldSize = layerLink.leaderSize!; - final double dx = switch (textDirection) { - TextDirection.ltr => 0.0, - TextDirection.rtl => fieldSize.width - childSize.width, - }; - final double dy = switch (optionsViewOpenDirection) { - OptionsViewOpenDirection.down => min( - fieldSize.height, - size.height - childSize.height - fieldOffset.dy, - ), - OptionsViewOpenDirection.up => size.height - min(childSize.height, fieldOffset.dy), - }; - return Offset(dx, dy); - } - - @override - bool shouldRelayout(_RawAutocompleteOptionsLayoutDelegate oldDelegate) { - if (!fieldOffset.isFinite || !layerLink.leaderSize!.isFinite) { - return false; - } - return layerLink != oldDelegate.layerLink || - fieldOffset != oldDelegate.fieldOffset || - optionsViewOpenDirection != oldDelegate.optionsViewOpenDirection || - textDirection != oldDelegate.textDirection || - fieldConstraints != oldDelegate.fieldConstraints; - } -} - class _AutocompleteCallbackAction extends CallbackAction { _AutocompleteCallbackAction({required super.onInvoke, required this.isEnabledCallback}); diff --git a/packages/flutter/test/material/autocomplete_test.dart b/packages/flutter/test/material/autocomplete_test.dart index 3b7c72f6f9..a085b64d99 100644 --- a/packages/flutter/test/material/autocomplete_test.dart +++ b/packages/flutter/test/material/autocomplete_test.dart @@ -592,7 +592,6 @@ void main() { await tester.tap(find.byType(RawAutocomplete)); await tester.enterText(find.byType(RawAutocomplete), 'a'); - await tester.pump(); expect(find.text('aa').hitTestable(), findsOneWidget); }); }); diff --git a/packages/flutter/test/widgets/autocomplete_test.dart b/packages/flutter/test/widgets/autocomplete_test.dart index c32b120a32..0333d8b7d0 100644 --- a/packages/flutter/test/widgets/autocomplete_test.dart +++ b/packages/flutter/test/widgets/autocomplete_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -136,70 +135,60 @@ void main() { expect(lastOptions.elementAt(5), 'northern white rhinoceros'); }); - testWidgets('can split the field and options', (WidgetTester tester) async { + testWidgets('tapping on an option selects it', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable lastOptions; - late AutocompleteOnSelected lastOnSelected; - - final GlobalKey autocompleteKey = GlobalKey(); - final TextEditingController textEditingController = TextEditingController(); - final FocusNode focusNode = FocusNode(); - addTearDown(textEditingController.dispose); - addTearDown(focusNode.dispose); + late FocusNode focusNode; + late TextEditingController textEditingController; await tester.pumpWidget( MaterialApp( home: Scaffold( - appBar: AppBar( - // The field is in the AppBar, not actually a child of RawAutocomplete. - title: TextFormField( - key: fieldKey, - controller: textEditingController, - focusNode: focusNode, - decoration: const InputDecoration(hintText: 'Split RawAutocomplete App'), - onFieldSubmitted: (String value) { - RawAutocomplete.onFieldSubmitted(autocompleteKey); - }, - ), - ), - body: Align( - alignment: Alignment.topLeft, - child: RawAutocomplete( - key: autocompleteKey, - focusNode: focusNode, - textEditingController: textEditingController, - optionsBuilder: (TextEditingValue textEditingValue) { - return kOptions.where((String option) { - return option.contains(textEditingValue.text.toLowerCase()); - }).toList(); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - lastOptions = options; - lastOnSelected = onSelected; - return Material( + body: RawAutocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + return kOptions.where((String option) { + return option.contains(textEditingValue.text.toLowerCase()); + }); + }, + fieldViewBuilder: ( + BuildContext context, + TextEditingController fieldTextEditingController, + FocusNode fieldFocusNode, + VoidCallback onFieldSubmitted, + ) { + focusNode = fieldFocusNode; + textEditingController = fieldTextEditingController; + return TextField( + key: fieldKey, + focusNode: focusNode, + controller: textEditingController, + ); + }, + optionsViewBuilder: ( + BuildContext context, + AutocompleteOnSelected onSelected, + Iterable options, + ) { + lastOptions = options; + return Material( + elevation: 4.0, + child: ListView.builder( key: optionsKey, - elevation: 4.0, - child: ListView( - children: - options - .map( - (String option) => GestureDetector( - onTap: () { - onSelected(option); - }, - child: ListTile(title: Text(option)), - ), - ) - .toList(), - ), - ); - }, - ), + padding: const EdgeInsets.all(8.0), + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final String option = options.elementAt(index); + return GestureDetector( + onTap: () { + onSelected(option); + }, + child: ListTile(title: Text(option)), + ); + }, + ), + ); + }, ), ), ), @@ -209,412 +198,20 @@ void main() { expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); - // Focus the empty field. All the options are displayed. - focusNode.requestFocus(); + // Tap on the text field to open the options. + await tester.tap(find.byKey(fieldKey)); await tester.pump(); expect(find.byKey(optionsKey), findsOneWidget); expect(lastOptions.length, kOptions.length); - expect(tester.getSize(find.byKey(optionsKey)).width, greaterThan(0.0)); - // Enter text. The options are filtered by the text. - textEditingController.value = const TextEditingValue( - text: 'ele', - selection: TextSelection(baseOffset: 3, extentOffset: 3), - ); + await tester.tap(find.text(kOptions[2])); await tester.pump(); - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - expect(lastOptions.length, 2); - expect(lastOptions.elementAt(0), 'chameleon'); - expect(lastOptions.elementAt(1), 'elephant'); - // Select an option. The options hide and the field updates to show the - // selection. - final String selection = lastOptions.elementAt(1); - lastOnSelected(selection); - await tester.pump(); - expect(find.byKey(fieldKey), findsOneWidget); expect(find.byKey(optionsKey), findsNothing); - expect(textEditingController.text, selection); - // Modify the field text. The options appear again and are filtered. - textEditingController.value = const TextEditingValue( - text: 'e', - selection: TextSelection(baseOffset: 1, extentOffset: 1), - ); - await tester.pump(); - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - expect(lastOptions.length, 6); - expect(lastOptions.elementAt(0), 'chameleon'); - expect(lastOptions.elementAt(1), 'elephant'); - expect(lastOptions.elementAt(2), 'goose'); - expect(lastOptions.elementAt(3), 'lemur'); - expect(lastOptions.elementAt(4), 'mouse'); - expect(lastOptions.elementAt(5), 'northern white rhinoceros'); + expect(textEditingController.text, equals(kOptions[2])); }); - for (final OptionsViewOpenDirection openDirection in OptionsViewOpenDirection.values) { - testWidgets('tapping on an option selects it ($openDirection)', (WidgetTester tester) async { - final GlobalKey fieldKey = GlobalKey(); - final GlobalKey optionsKey = GlobalKey(); - late Iterable lastOptions; - late FocusNode focusNode; - late TextEditingController textEditingController; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Column( - children: [ - const SizedBox(height: 200), - RawAutocomplete( - optionsViewOpenDirection: openDirection, - optionsBuilder: (TextEditingValue textEditingValue) { - return kOptions.where((String option) { - return option.contains(textEditingValue.text.toLowerCase()); - }); - }, - fieldViewBuilder: ( - BuildContext context, - TextEditingController fieldTextEditingController, - FocusNode fieldFocusNode, - VoidCallback onFieldSubmitted, - ) { - focusNode = fieldFocusNode; - textEditingController = fieldTextEditingController; - return TextField( - key: fieldKey, - focusNode: focusNode, - controller: textEditingController, - ); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - lastOptions = options; - return Material( - elevation: 4.0, - child: ListView.builder( - key: optionsKey, - padding: const EdgeInsets.all(8.0), - itemCount: options.length, - itemBuilder: (BuildContext context, int index) { - final String option = options.elementAt(index); - return GestureDetector( - onTap: () { - onSelected(option); - }, - child: ListTile(title: Text(option)), - ); - }, - ), - ); - }, - ), - ], - ), - ), - ), - ); - - // The field is always rendered, but the options are not unless needed. - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - - // Tap on the text field to open the options. - await tester.tap(find.byKey(fieldKey)); - // Two pumps required due to post frame callback. - await tester.pump(); - await tester.pump(); - expect(find.byKey(optionsKey), findsOneWidget); - expect(lastOptions.length, kOptions.length); - - await tester.tap(find.text(kOptions[2])); - await tester.pump(); - - expect(find.byKey(optionsKey), findsNothing); - - expect(textEditingController.text, equals(kOptions[2])); - }); - - testWidgets('when not enough room for options, options cover field ($openDirection)', ( - WidgetTester tester, - ) async { - const double padding = 32.0; - final GlobalKey fieldKey = GlobalKey(); - final GlobalKey optionsKey = GlobalKey(); - late StateSetter setState; - Alignment alignment = Alignment.bottomCenter; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: StatefulBuilder( - builder: (BuildContext context, StateSetter setter) { - setState = setter; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: padding), - child: Align( - alignment: alignment, - child: RawAutocomplete( - optionsViewOpenDirection: openDirection, - optionsBuilder: (TextEditingValue textEditingValue) { - return kOptions.where((String option) { - return option.contains(textEditingValue.text.toLowerCase()); - }); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - return ListView.builder( - key: optionsKey, - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: options.length, - itemBuilder: (BuildContext context, int index) { - final String option = options.elementAt(index); - return InkWell( - onTap: () { - onSelected(option); - }, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(option), - ), - ); - }, - ); - }, - fieldViewBuilder: ( - BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onSubmitted, - ) { - return TextField( - key: fieldKey, - focusNode: focusNode, - controller: textEditingController, - ); - }, - ), - ), - ); - }, - ), - ), - ), - ); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - - await tester.tap(find.byKey(fieldKey)); - await tester.pump(); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - - await tester.enterText(find.byKey(fieldKey), 'go'); // 3 results. - await tester.pump(); - - switch (openDirection) { - case OptionsViewOpenDirection.up: - // Options are positioned and sized like normal. - expect(find.byType(InkWell), findsNWidgets(3)); - final double optionHeight = tester.getSize(find.byType(InkWell).first).height; - final double topOfField = tester.getTopLeft(find.byKey(fieldKey)).dy; - expect( - tester.getTopLeft(find.byType(InkWell).first), - Offset(padding, topOfField - 3 * optionHeight), - ); - expect(tester.getBottomLeft(find.byType(InkWell).at(2)), Offset(padding, topOfField)); - case OptionsViewOpenDirection.down: - expect(find.byType(InkWell), findsNWidgets(1)); - final Size optionsSize = tester.getSize(find.byKey(optionsKey)); - expect(optionsSize.height, kMinInteractiveDimension); - // Options are positioned as low as possible while still fitting on screen. - final double bottomOfField = tester.getBottomLeft(find.byKey(optionsKey)).dy; - expect( - tester.getTopLeft(find.byKey(optionsKey)), - Offset(padding, bottomOfField - optionsSize.height), - ); - } - - // Add an extra pump to account for any potential frame delays introduced - // by the post frame callback in the _RawAutocompleteOptions - // implementation. - await tester.pump(); - setState(() { - alignment = Alignment.topCenter; - }); - - // One frame for the field to move and one frame for the options to - // follow. - await tester.pump(); - await tester.pump(); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - - switch (openDirection) { - case OptionsViewOpenDirection.up: - // Options are positioned as high as possible while still fitting on - // the screen. - expect(find.byType(InkWell), findsNWidgets(1)); - final Size optionsSize = tester.getSize(find.byKey(optionsKey)); - expect(optionsSize.height, kMinInteractiveDimension); - expect(tester.getTopLeft(find.byKey(optionsKey)), const Offset(padding, 0.0)); - expect(tester.getBottomLeft(find.byKey(optionsKey)), Offset(padding, optionsSize.height)); - case OptionsViewOpenDirection.down: - // Options are positioned and sized like normal. - expect(find.byType(InkWell), findsNWidgets(3)); - final double optionHeight = tester.getSize(find.byType(InkWell).first).height; - final double bottomOfField = tester.getBottomLeft(find.byKey(fieldKey)).dy; - expect(tester.getTopLeft(find.byType(InkWell).first), Offset(padding, bottomOfField)); - expect( - tester.getBottomLeft(find.byType(InkWell).at(2)), - Offset(padding, bottomOfField + 3 * optionHeight), - ); - } - }); - - testWidgets('correct options alignment for RTL in direction $openDirection', ( - WidgetTester tester, - ) async { - final GlobalKey fieldKey = GlobalKey(); - final GlobalKey optionsKey = GlobalKey(); - const double kOptionsWidth = 100.0; - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Directionality( - textDirection: TextDirection.rtl, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0), - child: RawAutocomplete( - optionsViewOpenDirection: openDirection, - optionsBuilder: (TextEditingValue textEditingValue) { - return kOptions.where((String option) { - return option.contains(textEditingValue.text.toLowerCase()); - }); - }, - fieldViewBuilder: ( - BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onSubmitted, - ) { - return TextField( - key: fieldKey, - focusNode: focusNode, - controller: textEditingController, - ); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - return SizedBox(width: kOptionsWidth, key: optionsKey); - }, - ), - ), - ), - ), - ), - ); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - - await tester.tap(find.byType(TextField)); - await tester.pump(); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - - final RenderBox optionsBox = tester.renderObject(find.byKey(optionsKey)); - expect(optionsBox.size.width, kOptionsWidth); - expect( - tester.getTopRight(find.byKey(optionsKey)).dx, - tester.getTopRight(find.byKey(fieldKey)).dx, - ); - }); - - testWidgets('options width matches field width with open direction $openDirection', ( - WidgetTester tester, - ) async { - final GlobalKey fieldKey = GlobalKey(); - final GlobalKey optionsKey = GlobalKey(); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0), - child: Center( - child: RawAutocomplete( - optionsViewOpenDirection: openDirection, - optionsBuilder: (TextEditingValue textEditingValue) { - return kOptions.where((String option) { - return option.contains(textEditingValue.text.toLowerCase()); - }); - }, - fieldViewBuilder: ( - BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onSubmitted, - ) { - return TextField( - key: fieldKey, - focusNode: focusNode, - controller: textEditingController, - ); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - return Container(key: optionsKey); - }, - ), - ), - ), - ), - ), - ); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - - await tester.tap(find.byType(TextField)); - // Two pumps required due to post frame callback. - await tester.pump(); - await tester.pump(); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - - final RenderBox fieldBox = tester.renderObject(find.byKey(fieldKey)); - final RenderBox optionsBox = tester.renderObject(find.byKey(optionsKey)); - expect(optionsBox.size.width, equals(fieldBox.size.width)); - expect(tester.getTopLeft(find.byKey(optionsKey)).dy, switch (openDirection) { - OptionsViewOpenDirection.down => - tester.getTopLeft(find.byKey(fieldKey)).dy + fieldBox.size.height, - OptionsViewOpenDirection.up => - tester.getTopLeft(find.byKey(fieldKey)).dy - optionsBox.size.height, - }); - }); - } - testWidgets('can filter and select a list of custom User options', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); @@ -893,7 +490,6 @@ void main() { ), ); await tester.showKeyboard(find.byType(TextField)); - await tester.pump(); expect( tester.getBottomLeft(find.byType(TextField)), offsetMoreOrLessEquals(tester.getTopLeft(find.text('a'))), @@ -930,7 +526,6 @@ void main() { ), ); await tester.showKeyboard(find.byType(TextField)); - await tester.pump(); expect( tester.getBottomLeft(find.byType(TextField)), offsetMoreOrLessEquals(tester.getTopLeft(find.text('a'))), @@ -966,7 +561,6 @@ void main() { ), ); await tester.showKeyboard(find.byType(TextField)); - await tester.pump(); expect( tester.getTopLeft(find.byType(TextField)), offsetMoreOrLessEquals(tester.getBottomLeft(find.text('a'))), @@ -1008,7 +602,6 @@ void main() { ), ); await tester.showKeyboard(find.byType(TextField)); - await tester.pump(); expect( tester.getBottomLeft(find.byKey(autocompleteKey)), offsetMoreOrLessEquals(tester.getTopLeft(find.text('a'))), @@ -1049,7 +642,6 @@ void main() { ), ); await tester.showKeyboard(find.byType(TextField)); - await tester.pump(); expect( tester.getTopLeft(find.byKey(autocompleteKey)), offsetMoreOrLessEquals(tester.getBottomLeft(find.text('a'))), @@ -1124,14 +716,10 @@ void main() { expect(find.byKey(optionsKey), findsOneWidget); // Options are just below the field. - final Offset optionsTopLeft = tester.getTopLeft(find.byKey(optionsKey)); - final Offset fieldOffset = tester.getTopLeft(find.byKey(fieldKey)); + final Offset optionsOffset = tester.getTopLeft(find.byKey(optionsKey)); + Offset fieldOffset = tester.getTopLeft(find.byKey(fieldKey)); final Size fieldSize = tester.getSize(find.byKey(fieldKey)); - expect(optionsTopLeft.dy, fieldOffset.dy + fieldSize.height); - - // Add an extra pump to account for any potential frame delays introduced by - // the post frame callback in the _RawAutocompleteOptions implementation. - await tester.pump(); + expect(optionsOffset.dy, fieldOffset.dy + fieldSize.height); // Move the field (similar to as if the keyboard opened). The options move // to follow the field. @@ -1139,68 +727,10 @@ void main() { alignment = Alignment.topCenter; }); await tester.pump(); - final Offset fieldOffsetFrame1 = tester.getTopLeft(find.byKey(fieldKey)); - final Offset optionsTopLeftOpenFrame1 = tester.getTopLeft(find.byKey(optionsKey)); - - expect(fieldOffsetFrame1.dy, lessThan(fieldOffset.dy)); - expect(optionsTopLeftOpenFrame1.dy, isNot(equals(optionsTopLeft.dy))); - expect(optionsTopLeftOpenFrame1.dy, fieldOffsetFrame1.dy + fieldSize.height); - }); - - testWidgets('options are shown one frame after tapping in field', (WidgetTester tester) async { - final GlobalKey fieldKey = GlobalKey(); - final GlobalKey optionsKey = GlobalKey(); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topCenter, - child: RawAutocomplete( - optionsBuilder: (TextEditingValue textEditingValue) { - return kOptions.where((String option) { - return option.contains(textEditingValue.text.toLowerCase()); - }); - }, - fieldViewBuilder: ( - BuildContext context, - TextEditingController fieldTextEditingController, - FocusNode fieldFocusNode, - VoidCallback onFieldSubmitted, - ) { - return TextFormField( - controller: fieldTextEditingController, - focusNode: fieldFocusNode, - key: fieldKey, - ); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - return ListView( - key: optionsKey, - children: options.map((String option) => Text(option)).toList(), - ); - }, - ), - ), - ), - ), - ); - - // Field is shown but not options. - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - expect(find.text('aardvark'), findsNothing); - - // Tap to show the options. - await tester.tap(find.byKey(fieldKey)); - await tester.pump(); - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - expect(find.text('aardvark'), findsOneWidget); + fieldOffset = tester.getTopLeft(find.byKey(fieldKey)); + final Offset optionsOffsetOpen = tester.getTopLeft(find.byKey(optionsKey)); + expect(optionsOffsetOpen.dy, isNot(equals(optionsOffset.dy))); + expect(optionsOffsetOpen.dy, fieldOffset.dy + fieldSize.height); }); testWidgets('can prevent options from showing by returning an empty iterable', ( @@ -1941,758 +1471,6 @@ void main() { expect(find.byKey(optionsKey), findsNothing); }); - testWidgets('options width matches field width after rebuilding', (WidgetTester tester) async { - final GlobalKey fieldKey = GlobalKey(); - final GlobalKey optionsKey = GlobalKey(); - late StateSetter setState; - double width = 100.0; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0), - child: StatefulBuilder( - builder: (BuildContext context, StateSetter localStateSetter) { - setState = localStateSetter; - return SizedBox( - width: width, - child: RawAutocomplete( - optionsBuilder: (TextEditingValue textEditingValue) { - return kOptions.where((String option) { - return option.contains(textEditingValue.text.toLowerCase()); - }); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - return Container(key: optionsKey); - }, - fieldViewBuilder: ( - BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onSubmitted, - ) { - return TextField( - key: fieldKey, - focusNode: focusNode, - controller: textEditingController, - ); - }, - ), - ); - }, - ), - ), - ), - ), - ); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - - final RenderBox fieldBox = tester.renderObject(find.byKey(fieldKey)); - expect(fieldBox.size.width, 100.0); - - await tester.tap(find.byType(TextField)); - await tester.pump(); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - - final RenderBox optionsBox = tester.renderObject(find.byKey(optionsKey)); - expect(optionsBox.size.width, 100.0); - - // Add an extra pump to account for any potential frame delays introduced by - // the post frame callback in the _RawAutocompleteOptions implementation. - await tester.pump(); - setState(() { - width = 200.0; - }); - await tester.pump(); - - // The options width changes to match the field width. - expect(fieldBox.size.width, 200.0); - expect(optionsBox.size.width, 200.0); - }); - - testWidgets('options width matches field width after changing', (WidgetTester tester) async { - final GlobalKey fieldKey = GlobalKey(); - final GlobalKey optionsKey = GlobalKey(); - late StateSetter setState; - double width = 100.0; - - final RawAutocomplete autocomplete = RawAutocomplete( - optionsBuilder: (TextEditingValue textEditingValue) { - return kOptions.where((String option) { - return option.contains(textEditingValue.text.toLowerCase()); - }); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - return Container(key: optionsKey); - }, - fieldViewBuilder: ( - BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onSubmitted, - ) { - return TextField(key: fieldKey, focusNode: focusNode, controller: textEditingController); - }, - ); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0), - child: StatefulBuilder( - builder: (BuildContext context, StateSetter localStateSetter) { - setState = localStateSetter; - return SizedBox(width: width, child: autocomplete); - }, - ), - ), - ), - ), - ); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - - final RenderBox fieldBox = tester.renderObject(find.byKey(fieldKey)); - expect(fieldBox.size.width, 100.0); - - await tester.tap(find.byType(TextField)); - await tester.pump(); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - - final RenderBox optionsBox = tester.renderObject(find.byKey(optionsKey)); - expect(fieldBox.size.width, 100.0); - expect(optionsBox.size.width, 100.0); - - // Add an extra pump to account for any potential frame delays introduced by - // the post frame callback in the _RawAutocompleteOptions implementation. - await tester.pump(); - setState(() { - width = 200.0; - }); - await tester.pump(); - - // The options width changes to match the field width. - expect(fieldBox.size.width, 200.0); - expect(optionsBox.size.width, 200.0); - }); - - group('screen size', () { - Future pumpRawAutocomplete( - WidgetTester tester, { - GlobalKey? fieldKey, - GlobalKey? optionsKey, - OptionsViewOpenDirection optionsViewOpenDirection = OptionsViewOpenDirection.down, - Alignment alignment = Alignment.topLeft, - }) { - return tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0), - child: Align( - alignment: alignment, - child: RawAutocomplete( - optionsViewOpenDirection: optionsViewOpenDirection, - optionsBuilder: (TextEditingValue textEditingValue) { - return kOptions.where((String option) { - return option.contains(textEditingValue.text.toLowerCase()); - }); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - return ListView.builder( - key: optionsKey, - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: options.length, - itemBuilder: (BuildContext context, int index) { - final String option = options.elementAt(index); - return InkWell( - onTap: () { - onSelected(option); - }, - child: Padding(padding: const EdgeInsets.all(16.0), child: Text(option)), - ); - }, - ); - }, - fieldViewBuilder: ( - BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onSubmitted, - ) { - return TextField( - key: fieldKey, - focusNode: focusNode, - controller: textEditingController, - ); - }, - ), - ), - ), - ), - ), - ); - } - - testWidgets('options when screen changes landscape to portrait', (WidgetTester tester) async { - // Start with a portrait-sized window, with enough space for all of the - // options. - const Size wideWindowSize = Size(1920.0, 1080.0); - tester.view.physicalSize = wideWindowSize; - tester.view.devicePixelRatio = 1.0; - addTearDown(tester.view.reset); - - final GlobalKey fieldKey = GlobalKey(); - final GlobalKey optionsKey = GlobalKey(); - - await pumpRawAutocomplete(tester, fieldKey: fieldKey, optionsKey: optionsKey); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - - await tester.tap(find.byType(TextField)); - await tester.pumpAndSettle(); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - expect(find.byType(InkWell), findsNWidgets(kOptions.length)); - - final Size fieldSize1 = tester.getSize(find.byKey(fieldKey)); - final Offset optionsTopLeft1 = tester.getTopLeft(find.byKey(optionsKey)); - expect( - optionsTopLeft1, - Offset( - tester.getTopLeft(find.byKey(fieldKey)).dx, - tester.getTopLeft(find.byKey(fieldKey)).dy + fieldSize1.height, - ), - ); - final Offset optionsBottomRight1 = tester.getBottomRight(find.byKey(optionsKey)); - final double optionHeight = tester.getSize(find.byType(InkWell).first).height; - expect( - optionsBottomRight1, - Offset( - tester.getTopLeft(find.byKey(fieldKey)).dx + fieldSize1.width, - tester.getTopLeft(find.byKey(fieldKey)).dy + - fieldSize1.height + - optionHeight * kOptions.length, - ), - ); - - // Change the screen size to portrait. - const Size narrowWindowSize = Size(1070.0, 1770.0); - tester.view.physicalSize = narrowWindowSize; - tester.view.devicePixelRatio = 1.0; - await tester.pumpAndSettle(); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byType(InkWell), findsNWidgets(kOptions.length)); - expect(tester.getTopLeft(find.byKey(optionsKey)), optionsTopLeft1); - final Size fieldSize2 = tester.getSize(find.byKey(fieldKey)); - expect(fieldSize1.width, greaterThan(fieldSize2.width)); - expect(fieldSize1.height, fieldSize2.height); - final Offset optionsBottomRight2 = tester.getBottomRight(find.byKey(optionsKey)); - final Offset fieldTopLeft2 = tester.getTopLeft(find.byKey(fieldKey)); - expect(optionsBottomRight2.dx, lessThan(optionsBottomRight1.dx)); - expect(optionsBottomRight2.dy, optionsBottomRight1.dy); - expect( - optionsBottomRight2, - Offset( - fieldTopLeft2.dx + fieldSize2.width, - fieldTopLeft2.dy + fieldSize2.height + optionHeight * kOptions.length, - ), - ); - }); - - testWidgets('options when screen changes portrait to landscape and overflows', ( - WidgetTester tester, - ) async { - // Start with a portrait-sized window, with enough space for all of the - // options. - const Size narrowWindowSize = Size(1070.0, 1770.0); - tester.view.physicalSize = narrowWindowSize; - tester.view.devicePixelRatio = 1.0; - addTearDown(tester.view.reset); - - final GlobalKey fieldKey = GlobalKey(); - final GlobalKey optionsKey = GlobalKey(); - - await pumpRawAutocomplete(tester, fieldKey: fieldKey, optionsKey: optionsKey); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - - await tester.tap(find.byType(TextField)); - await tester.pumpAndSettle(); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - expect(find.byType(InkWell), findsNWidgets(kOptions.length)); - - final Size fieldSize1 = tester.getSize(find.byKey(fieldKey)); - final Offset optionsTopLeft1 = tester.getTopLeft(find.byKey(optionsKey)); - expect( - optionsTopLeft1, - Offset( - tester.getTopLeft(find.byKey(fieldKey)).dx, - tester.getTopLeft(find.byKey(fieldKey)).dy + fieldSize1.height, - ), - ); - final Offset optionsBottomRight1 = tester.getBottomRight(find.byKey(optionsKey)); - final double optionHeight = tester.getSize(find.byType(InkWell).first).height; - expect( - optionsBottomRight1, - Offset( - tester.getTopLeft(find.byKey(fieldKey)).dx + fieldSize1.width, - tester.getTopLeft(find.byKey(fieldKey)).dy + - fieldSize1.height + - optionHeight * kOptions.length, - ), - ); - - // Change the screen size to landscape where the options can't all fit on - // the screen. - const Size wideWindowSize = Size(1920.0, 580.0); - tester.view.physicalSize = wideWindowSize; - tester.view.devicePixelRatio = 1.0; - await tester.pumpAndSettle(); - expect(find.byKey(fieldKey), findsOneWidget); - - final int visibleOptions = (wideWindowSize.height / optionHeight).floor(); - expect(visibleOptions, lessThan(kOptions.length)); - expect(find.byType(InkWell), findsNWidgets(visibleOptions)); - expect(tester.getTopLeft(find.byKey(optionsKey)), optionsTopLeft1); - final Size fieldSize2 = tester.getSize(find.byKey(fieldKey)); - expect(fieldSize1.width, lessThan(fieldSize2.width)); - expect(fieldSize1.height, fieldSize2.height); - final Offset optionsBottomRight2 = tester.getBottomRight(find.byKey(optionsKey)); - final Offset fieldTopLeft2 = tester.getTopLeft(find.byKey(fieldKey)); - expect(optionsBottomRight2.dx, greaterThan(optionsBottomRight1.dx)); - expect(optionsBottomRight2.dy, lessThan(optionsBottomRight1.dy)); - expect( - optionsBottomRight2, - Offset( - fieldTopLeft2.dx + fieldSize2.width, - // Options are taking all available space below the field. - wideWindowSize.height, - ), - ); - }); - - testWidgets('screen changes portrait to landscape and overflows', (WidgetTester tester) async { - // Start with a portrait-sized window, with enough space for all of the - // options. - const Size narrowWindowSize = Size(1070.0, 1770.0); - tester.view.physicalSize = narrowWindowSize; - tester.view.devicePixelRatio = 1.0; - addTearDown(tester.view.reset); - - final GlobalKey fieldKey = GlobalKey(); - final GlobalKey optionsKey = GlobalKey(); - - await pumpRawAutocomplete(tester, fieldKey: fieldKey, optionsKey: optionsKey); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - - await tester.tap(find.byType(TextField)); - await tester.pumpAndSettle(); - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - - final double optionHeight = tester.getSize(find.byType(InkWell).first).height; - final double optionsHeight1 = tester.getSize(find.byKey(optionsKey)).height; - final int visibleOptions1 = (optionsHeight1 / optionHeight).ceil(); - expect(find.byType(InkWell), findsNWidgets(visibleOptions1)); - final Size fieldSize1 = tester.getSize(find.byKey(fieldKey)); - final Offset optionsTopLeft1 = tester.getTopLeft(find.byKey(optionsKey)); - final Offset fieldTopLeft1 = tester.getTopLeft(find.byKey(fieldKey)); - expect(optionsTopLeft1, Offset(fieldTopLeft1.dx, fieldTopLeft1.dy + fieldSize1.height)); - final Offset optionsBottomRight1 = tester.getBottomRight(find.byKey(optionsKey)); - expect( - optionsBottomRight1, - Offset( - fieldTopLeft1.dx + fieldSize1.width, - fieldTopLeft1.dy + fieldSize1.height + optionsHeight1, - ), - ); - - // Change the screen size to landscape where the options can't all fit on - // the screen. - const Size wideWindowSize = Size(1920.0, 580.0); - tester.view.physicalSize = wideWindowSize; - tester.view.devicePixelRatio = 1.0; - await tester.pumpAndSettle(); - expect(find.byKey(fieldKey), findsOneWidget); - - final double optionsHeight2 = tester.getSize(find.byKey(optionsKey)).height; - final int visibleOptions2 = (optionsHeight2 / optionHeight).ceil(); - expect(visibleOptions2, lessThan(kOptions.length)); - expect(find.byType(InkWell), findsNWidgets(visibleOptions2)); - final Offset optionsTopLeft2 = tester.getTopLeft(find.byKey(optionsKey)); - expect(optionsTopLeft2, optionsTopLeft1); - final Size fieldSize2 = tester.getSize(find.byKey(fieldKey)); - expect(fieldSize1.width, lessThan(fieldSize2.width)); - expect(fieldSize1.height, fieldSize2.height); - final Offset optionsBottomRight2 = tester.getBottomRight(find.byKey(optionsKey)); - final Offset fieldTopLeft2 = tester.getTopLeft(find.byKey(fieldKey)); - expect(optionsBottomRight2.dx, greaterThan(optionsBottomRight1.dx)); - expect( - optionsBottomRight2, - Offset( - fieldTopLeft2.dx + fieldSize2.width, - // Options are taking all available space below the field. - wideWindowSize.height, - ), - ); - - // Shrink the screen further so that the options become smaller than - // kMinInteractiveDimension and move to overlap the field. - const Size shortWindowSize = Size(1920.0, 90.0); - tester.view.physicalSize = shortWindowSize; - tester.view.devicePixelRatio = 1.0; - await tester.pumpAndSettle(); - expect(find.byKey(fieldKey), findsOneWidget); - - const int visibleOptions3 = 1; - expect(find.byType(InkWell), findsNWidgets(visibleOptions3)); - final Offset optionsTopLeft3 = tester.getTopLeft(find.byKey(optionsKey)); - expect(optionsTopLeft3.dx, optionsTopLeft1.dx); - // The options have moved up, overlapping the field, to still be able to - // show kMinInteractiveDimension. - expect(optionsTopLeft3.dy, lessThan(optionsTopLeft1.dy)); - final Size fieldSize3 = tester.getSize(find.byKey(fieldKey)); - final Offset fieldTopLeft3 = tester.getTopLeft(find.byKey(fieldKey)); - expect(optionsTopLeft3.dy, lessThan(fieldTopLeft3.dy + fieldSize3.height)); - expect(fieldSize3.width, fieldSize2.width); - expect(fieldSize1.height, fieldSize3.height); - final Offset optionsBottomRight3 = tester.getBottomRight(find.byKey(optionsKey)); - expect(optionsBottomRight3.dx, greaterThan(optionsBottomRight1.dx)); - expect( - optionsBottomRight3, - Offset(fieldTopLeft3.dx + fieldSize3.width, shortWindowSize.height), - ); - }); - - testWidgets('when opening up screen changes portrait to landscape and overflows', ( - WidgetTester tester, - ) async { - // Start with a portrait-sized window, with enough space for all of the - // options. - const Size narrowWindowSize = Size(1070.0, 1770.0); - tester.view.physicalSize = narrowWindowSize; - tester.view.devicePixelRatio = 1.0; - addTearDown(tester.view.reset); - - final GlobalKey fieldKey = GlobalKey(); - final GlobalKey optionsKey = GlobalKey(); - - await pumpRawAutocomplete( - tester, - fieldKey: fieldKey, - optionsKey: optionsKey, - optionsViewOpenDirection: OptionsViewOpenDirection.up, - alignment: Alignment.center, - ); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - - await tester.tap(find.byType(TextField)); - await tester.pumpAndSettle(); - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - - final double optionHeight = tester.getSize(find.byType(InkWell).first).height; - final double optionsHeight1 = tester.getSize(find.byKey(optionsKey)).height; - final int visibleOptions1 = (optionsHeight1 / optionHeight).ceil(); - expect(find.byType(InkWell), findsNWidgets(visibleOptions1)); - final Size fieldSize1 = tester.getSize(find.byKey(fieldKey)); - final Offset optionsTopLeft1 = tester.getTopLeft(find.byKey(optionsKey)); - final Offset fieldTopLeft1 = tester.getTopLeft(find.byKey(fieldKey)); - expect(optionsTopLeft1, Offset(fieldTopLeft1.dx, fieldTopLeft1.dy - optionsHeight1)); - expect(optionsTopLeft1.dy, greaterThan(0.0)); - final Offset optionsBottomRight1 = tester.getBottomRight(find.byKey(optionsKey)); - expect(optionsBottomRight1, Offset(fieldTopLeft1.dx + fieldSize1.width, fieldTopLeft1.dy)); - - // Change the screen size to landscape where the options can't all fit on - // the screen. - const Size wideWindowSize = Size(1920.0, 580.0); - tester.view.physicalSize = wideWindowSize; - tester.view.devicePixelRatio = 1.0; - await tester.pumpAndSettle(); - expect(find.byKey(fieldKey), findsOneWidget); - - final double optionsHeight2 = tester.getSize(find.byKey(optionsKey)).height; - expect(optionsHeight2, lessThan(optionsHeight1)); - final int visibleOptions2 = (optionsHeight2 / optionHeight).ceil(); - expect(visibleOptions2, lessThan(visibleOptions1)); - expect(find.byType(InkWell), findsNWidgets(visibleOptions2)); - final Offset optionsTopLeft2 = tester.getTopLeft(find.byKey(optionsKey)); - final Offset fieldTopLeft2 = tester.getTopLeft(find.byKey(fieldKey)); - expect(optionsTopLeft2, Offset(optionsTopLeft1.dx, fieldTopLeft2.dy - optionsHeight2)); - final Size fieldSize2 = tester.getSize(find.byKey(fieldKey)); - expect(fieldSize1.width, lessThan(fieldSize2.width)); - expect(fieldSize1.height, fieldSize2.height); - final Offset optionsBottomRight2 = tester.getBottomRight(find.byKey(optionsKey)); - expect(optionsBottomRight2.dx, greaterThan(optionsBottomRight1.dx)); - expect(optionsBottomRight2, Offset(fieldTopLeft2.dx + fieldSize2.width, fieldTopLeft2.dy)); - - // Shrink the screen further so that the options become smaller than - // kMinInteractiveDimension and move to overlap the field. - const Size shortWindowSize = Size(1920.0, 90.0); - tester.view.physicalSize = shortWindowSize; - tester.view.devicePixelRatio = 1.0; - await tester.pumpAndSettle(); - - expect(find.byKey(fieldKey), findsOneWidget); - const int visibleOptions3 = 1; - expect(find.byType(InkWell), findsNWidgets(visibleOptions3)); - final Offset optionsTopLeft3 = tester.getTopLeft(find.byKey(optionsKey)); - expect(optionsTopLeft3.dx, optionsTopLeft1.dx); - // The options have moved down, overlapping the field, to still be able to - // show kMinInteractiveDimension. - expect(optionsTopLeft3.dy, lessThan(optionsTopLeft1.dy)); - final Size fieldSize3 = tester.getSize(find.byKey(fieldKey)); - final Offset fieldTopLeft3 = tester.getTopLeft(find.byKey(fieldKey)); - expect(optionsTopLeft3.dy, lessThan(fieldTopLeft3.dy + fieldSize3.height)); - expect(fieldSize3.width, fieldSize2.width); - expect(fieldSize1.height, fieldSize3.height); - final Offset optionsBottomRight3 = tester.getBottomRight(find.byKey(optionsKey)); - expect(optionsBottomRight3.dx, greaterThan(optionsBottomRight1.dx)); - expect(optionsBottomRight3.dy, greaterThan(fieldTopLeft3.dy)); - expect(optionsBottomRight3.dx, fieldTopLeft3.dx + fieldSize3.width); - }); - }); - - testWidgets( - 'when field scrolled offscreen, options are hidden and not reshown when scrolled back on desktop and web', - (WidgetTester tester) async { - final GlobalKey fieldKey = GlobalKey(); - final GlobalKey optionsKey = GlobalKey(); - final ScrollController scrollController = ScrollController(); - addTearDown(scrollController.dispose); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ListView( - controller: scrollController, - children: [ - const SizedBox(height: 1000.0), - RawAutocomplete( - optionsBuilder: (TextEditingValue textEditingValue) { - return kOptions.where((String option) { - return option.contains(textEditingValue.text.toLowerCase()); - }); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - return ListView.builder( - key: optionsKey, - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: options.length, - itemBuilder: (BuildContext context, int index) { - final String option = options.elementAt(index); - return InkWell( - onTap: () { - onSelected(option); - }, - child: Padding(padding: const EdgeInsets.all(16.0), child: Text(option)), - ); - }, - ); - }, - fieldViewBuilder: ( - BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onSubmitted, - ) { - return TextField( - key: fieldKey, - focusNode: focusNode, - controller: textEditingController, - ); - }, - ), - const SizedBox(height: 1000.0), - ], - ), - ), - ), - ); - - expect(find.byKey(fieldKey), findsNothing); - expect(find.byKey(optionsKey), findsNothing); - - await tester.scrollUntilVisible(find.byKey(fieldKey), 500.0); - await tester.pumpAndSettle(); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - - await tester.tap(find.byKey(fieldKey)); - await tester.pump(); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - - // Jump to the beginning. The field is off screen and the options are not - // showing either. - scrollController.jumpTo(0.0); - await tester.pumpAndSettle(); - - expect(find.byKey(fieldKey), findsNothing); - expect(find.byKey(optionsKey), findsNothing); - - // Scroll back to the field and ensure it is visible. - await tester.scrollUntilVisible(find.byKey(fieldKey), 500.0); - await tester.pumpAndSettle(); - - // The options are no longer visible on desktop and web. - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - - // Jump to the end. The field is hidden again. - scrollController.jumpTo(2000.0); - await tester.pumpAndSettle(); - - expect(find.byKey(fieldKey), findsNothing); - expect(find.byKey(optionsKey), findsNothing); - }, - variant: TargetPlatformVariant.desktop(), - ); - - testWidgets( - 'when field scrolled offscreen, options are hidden and reshown when scrolled back on mobile', - (WidgetTester tester) async { - final GlobalKey fieldKey = GlobalKey(); - final GlobalKey optionsKey = GlobalKey(); - final ScrollController scrollController = ScrollController(); - addTearDown(scrollController.dispose); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ListView( - controller: scrollController, - children: [ - const SizedBox(height: 1000.0), - RawAutocomplete( - optionsBuilder: (TextEditingValue textEditingValue) { - return kOptions.where((String option) { - return option.contains(textEditingValue.text.toLowerCase()); - }); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - return ListView.builder( - key: optionsKey, - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: options.length, - itemBuilder: (BuildContext context, int index) { - final String option = options.elementAt(index); - return InkWell( - onTap: () { - onSelected(option); - }, - child: Padding(padding: const EdgeInsets.all(16.0), child: Text(option)), - ); - }, - ); - }, - fieldViewBuilder: ( - BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onSubmitted, - ) { - return TextField( - key: fieldKey, - focusNode: focusNode, - controller: textEditingController, - ); - }, - ), - const SizedBox(height: 1000.0), - ], - ), - ), - ), - ); - - expect(find.byKey(fieldKey), findsNothing); - expect(find.byKey(optionsKey), findsNothing); - - await tester.scrollUntilVisible(find.byKey(fieldKey), 500.0); - await tester.pumpAndSettle(); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsNothing); - - await tester.tap(find.byKey(fieldKey)); - await tester.pump(); - - expect(find.byKey(fieldKey), findsOneWidget); - expect(find.byKey(optionsKey), findsOneWidget); - - // Jump to the beginning. The field is off screen and the options are not - // showing either. - scrollController.jumpTo(0.0); - await tester.pumpAndSettle(); - - expect(find.byKey(fieldKey), findsNothing); - expect(find.byKey(optionsKey), findsNothing); - - // Scroll back to the field and ensure it is visible. - await tester.scrollUntilVisible(find.byKey(fieldKey), 500.0); - await tester.pumpAndSettle(); - - // The options remain visible on mobile, but not on web. - expect(find.byKey(fieldKey), findsOneWidget); - kIsWeb - ? expect(find.byKey(optionsKey), findsNothing) - : expect(find.byKey(optionsKey), findsOneWidget); - - // Jump to the end. The field is hidden again. - scrollController.jumpTo(2000.0); - await tester.pumpAndSettle(); - - expect(find.byKey(fieldKey), findsNothing); - expect(find.byKey(optionsKey), findsNothing); - }, - variant: TargetPlatformVariant.mobile(), - ); - testWidgets('can prevent older optionsBuilder results from replacing the new ones', ( WidgetTester tester, ) async {