flutter_typeahead icon indicating copy to clipboard operation
flutter_typeahead copied to clipboard

[Bug] [Regression from v4] Suggestions box not resizing after scroll

Open davidmartos96 opened this issue 7 months ago • 12 comments

I'm not sure if this is exactly the same as https://github.com/AbdulRahmanAlHamali/flutter_typeahead/issues/455, because that issue appears to exist on v4 unlike this one, but looks somewhat related.

Steps to reproduce

  1. Run the example provided
  2. Open the suggestions
  3. Scroll the ListView, not the suggestions box results

These steps can be run on version v4.8.0 and v5.x to see the difference

Expected results

The suggestions box should be resized automatically according to its constraints after the scroll action has finished. This was the behavior in version v4.

The original code from v4 which was taking care of this has been removed. https://github.com/AbdulRahmanAlHamali/flutter_typeahead/blob/c6ff9b23581a072b3208ec99d6040971b39db848/lib/src/material/field/typeahead_field.dart#L612C31-L612C31

Actual results

The box does not resize

Package Version

5.0.1

Platform

Android

Code sample

import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'TypeAhead Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ScrollExample(),
    );
  }
}

class ScrollExample extends StatelessWidget {
  final List<String> items = List.generate(50, (index) => "Item $index");

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Scrollbar(
        child: ListView(
          children: [
            Container(
              height: 200,
              color: Colors.red.withOpacity(0.3),
              child: const Center(
                child: Padding(
                  padding: EdgeInsets.all(8.0),
                  child: Text("Suggestion box should resize when scrolling"),
                ),
              ),
            ),
            // Typeahead V5
            /* TypeAheadField<String>(
              suggestionsCallback: (String pattern) async {
                return items.where((item) => item.toLowerCase().startsWith(pattern.toLowerCase())).toList();
              },
              itemBuilder: (context, String suggestion) {
                return ListTile(
                  title: Text(suggestion),
                );
              },
              onSelected: (String suggestion) {
                print("Suggestion selected");
              },
            ), */
            // Typeahead V4
            TypeAheadField<String>(
              suggestionsCallback: (String pattern) async {
                return items.where((item) => item.toLowerCase().startsWith(pattern.toLowerCase())).toList();
              },
              itemBuilder: (context, String suggestion) {
                return ListTile(
                  title: Text(suggestion),
                );
              },
              onSuggestionSelected: (String suggestion) {
                print("Suggestion selected");
              },
            ),
            Container(height: 1000, color: Colors.green),
          ],
        ),
      ),
    );
  }
}

davidmartos96 avatar Dec 27 '23 17:12 davidmartos96

Any update on this? I am facing this issue still

jooikwanw avatar Jan 11 '24 08:01 jooikwanw

Hi @davidmartos96, thanks for your issue.

It's currently unclear to me whether this would be desirable behaviour. I have to investigate whether this would be a good feature.

In the last version, this behaviour seemed to barely work correctly on desktop platforms. Scrolling the outer scrollview to expand the suggestions box even beyond the original layout where it was contained in does not sound like a good user experience on the face of it.

We might run into some issues with how we hide the box when it is out of view. We would also need to rewrite the layout calculation, accounting for a negative offset of the field in relation to the overlay in which the box is displayed.

When I wrote version 5, I have intentionally left this out, as it seemed janky and I was unsure whether we actually want it. I would be interested in hearing a compelling case for this feature though, and maybe a piece of sample code where this feature shows being worthwhile.

clragon avatar Jan 11 '24 21:01 clragon

@clragon Thank you for considering! Here is a more elaborated example (code below)

It's how I'm currently using typeahead in a scrollable UI. To improve UX, if the user focuses the field when it's almost at the bottom in the viewport, I scroll the necessary pixels in order to show some amount of the suggestions box.

With v5 I cannot manage to make the same behavior. What behavior barely worked on desktop? I'm trying the demo from this comment on both desktop and mobile and it works how I expect. If the box goes out of view it also hides correctly

The user would tap the typeahead when the UI is like this:

image

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'TypeAhead Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const ScrollExample(),
    );
  }
}

class ScrollExample extends StatelessWidget {
  const ScrollExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Scroll Example'),
      ),
      body: Scrollbar(
        child: ListView(
          primary: true,
          children: [
            const Center(
              child: Text(
                'BELOW THERE IS A TYPEAHEAD FIELD.',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
            ),
            for (int i = 0; i < 10; i++)
              Container(
                margin: const EdgeInsets.all(8.0),
                height: 50,
                color: Colors.red.withOpacity(0.3),
                child: Center(
                  child: Text("some UI above field $i"),
                ),
              ),
            Container(
              margin: const EdgeInsets.all(8.0),
              padding: const EdgeInsets.all(8.0),
              color: Colors.blue.withOpacity(0.3),
              child: const _TypeadFieldWrapper(),
            ),
            for (int i = 0; i < 20; i++)
              Container(
                margin: const EdgeInsets.all(8.0),
                height: 50,
                color: Colors.green.withOpacity(0.3),
                child: Center(
                  child: Text("some UI below field $i"),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

class _TypeadFieldWrapper extends StatefulWidget {
  const _TypeadFieldWrapper();

  @override
  State<_TypeadFieldWrapper> createState() => __TypeadFieldWrapperState();
}

class __TypeadFieldWrapperState extends State<_TypeadFieldWrapper> {
  final FocusNode _focusNode = FocusNode();
  final List<String> items = List.generate(50, (index) => "Item $index");

  //late final suggestionsController = SuggestionsController<String>();

  @override
  void initState() {
    super.initState();
    _focusNode.addListener(_ensureSuggestionsVisible);
  }

  @override
  void dispose() {
    _focusNode.removeListener(_ensureSuggestionsVisible);
    _focusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Typeahead v5
    /* TypeAheadField<String>(
      suggestionsController: suggestionsController,
      suggestionsCallback: (String pattern) async {
        return items.where((item) => item.toLowerCase().startsWith(pattern.toLowerCase())).toList();
      },
      itemBuilder: (context, String suggestion) {
        return ListTile(
          title: Text(suggestion),
        );
      },
      onSelected: (String suggestion) {
        print("Suggestion selected");
      },
    ), */

    // Typeahead v4
    return TypeAheadField<String>(
      suggestionsCallback: (String pattern) async {
        return items.where((item) => item.toLowerCase().startsWith(pattern.toLowerCase())).toList();
      },
      suggestionsBoxDecoration: const SuggestionsBoxDecoration(
        constraints: BoxConstraints(
          maxHeight: 300,
        ),
      ),
      itemBuilder: (context, String suggestion) {
        return ListTile(
          title: Text(suggestion),
        );
      },
      textFieldConfiguration: TextFieldConfiguration(
        focusNode: _focusNode,
      ),
      onSuggestionSelected: (String suggestion) {
        print("Suggestion selected");
      },
    );
  }

  Future<void> _ensureSuggestionsVisible() async {
    // Wait for keyboard open
    await Future<void>.delayed(const Duration(milliseconds: 600));

    if (!mounted || !_focusNode.hasFocus) return;

    final RenderObject fieldRenderObject = context.findRenderObject()!;
    final RenderAbstractViewport viewport = RenderAbstractViewport.of(fieldRenderObject);

    final ScrollableState scrollableState = Scrollable.of(context);

    final ScrollPosition position = scrollableState.position;

    final offsetToRevealField = viewport.getOffsetToReveal(fieldRenderObject, 1.0);

    // How much of the suggestions box we want to reveal
    const double boxRevealSize = 150;

    // Add boxRevealSize to offsetToReveal to account for the amount of the suggestions box
    final offsetToRevealBox = offsetToRevealField.offset + boxRevealSize;

    if (offsetToRevealBox < 0 || position.pixels >= offsetToRevealBox) {
      // The desired amount is already visible
      return;
    }

    // Scroll to reveal the suggestions box
    await position.animateTo(
      offsetToRevealBox,
      duration: const Duration(milliseconds: 100),
      curve: Curves.linear,
    );
  }
}

davidmartos96 avatar Jan 14 '24 13:01 davidmartos96

Thank you for the elaborate example. I understand the issue better now. I will investigate how we can fix this when I have time.

clragon avatar Jan 14 '24 19:01 clragon

@clragon Would exposing a resize method to be called from the user side be feasible to do? I believe that could work too, as we know when we need to resize the suggestions.

davidmartos96 avatar Apr 16 '24 08:04 davidmartos96

It would be nice to determine the position of the control in the screen and accordingly change the position of the suggestions (if at the bottom, show suggestions at the top).

frederikstonge avatar Apr 25 '24 11:04 frederikstonge

@davidmartos96 The resize method is already public and you can call it on the controller whenever you need to. @frederikstonge This behaviour is already part of the package, though it is unrelated to this issue here.

clragon avatar Apr 25 '24 12:04 clragon

@davidmartos96 The resize method is already public and you can call it on the controller whenever you need to. @frederikstonge This behaviour is already part of the package, though it is unrelated to this issue here.

Well it didn't work for me. It is related because when my control is too low on my screen, the suggestions are built under the keyboard, resulting in no suggestion box. When I scroll down, nothing happens because there's no resize.

frederikstonge avatar Apr 25 '24 12:04 frederikstonge

Also, just added a periodic timer to resize, if the suggestionbox was drawn shrinked because of the available space at the bottom of the screen, calling resize on the controller doesn't do anything.

frederikstonge avatar Apr 25 '24 12:04 frederikstonge

@frederikstonge Yes, it didn't work for me either when I tried it out back when I opened the thread. Otherwise I would have gone with the Timer approach too. There must be something else that prevents it from being resized correctly unlike with v4.

davidmartos96 avatar Apr 25 '24 12:04 davidmartos96