flutter_typeahead icon indicating copy to clipboard operation
flutter_typeahead copied to clipboard

How do I close suggestion box when touched outside its bounds.

Open vikalpnagar opened this issue 4 years ago • 7 comments

vikalpnagar avatar Apr 06 '20 10:04 vikalpnagar

Hi @vikalpnagar Did you find a solution?

jasteeman avatar Apr 11 '20 03:04 jasteeman

Yes, I found a workaround to close suggestions when touched outside. I am showing a modal barrier just like flutter's dropdown widget does. I don't know if it's the right way to do it but it is working for now. I did it in a hurry so it might not be optimized.

// Add this method in class _TypeAheadFieldState in flutter_typeahead.dart
Widget _buildModalBarrier() {
    return Container(
      child: NoRouteModalBarrier(
          dismissible: true, // changedInternalState is called if this updates
          semanticsLabel: '', // changedInternalState is called if this updates
          barrierSemanticsDismissible: true,
          onDismiss: () {
            if (this._suggestionsBox.isOpened) {
              this._suggestionsBox.close();
              this._effectiveFocusNode.unfocus();
            }
          }
      ),);
  }
// Replace this block of code in initState of _TypeAheadFieldState
WidgetsBinding.instance.addPostFrameCallback((duration) {
      if (mounted) {
        this._initOverlayEntry();
        this._suggestionsBox._overlayEntryBarrier =
            OverlayEntry(builder: (context) {
              return _buildModalBarrier();
            },);

        // calculate initial suggestions list size
        this._suggestionsBox.resize();

        // in case we already missed the focus event
        if (this._effectiveFocusNode.hasFocus) {
          this._suggestionsBox.open();
        }
      }
    });
// Add the overlay entry barrier inside _SuggestionsBox
OverlayEntry _overlayEntryBarrier;

// Replace the open and close function
void open() {
    if (this.isOpened) return;
    assert(this._overlayEntryBarrier != null);
    Overlay.of(context).insert(_overlayEntryBarrier);
    assert(this._overlayEntry != null);
    Overlay.of(context).insert(this._overlayEntry);
    this.isOpened = true;
  }

  void close() {
    if (!this.isOpened) return;
    assert(this._overlayEntryBarrier != null);
    this._overlayEntryBarrier.remove();
    assert(this._overlayEntry != null);
    this._overlayEntry.remove();
    this.isOpened = false;
  }
// Here is NoRouteModalBarrier which is a copy of ModalBarrier with some minor tweaks. Basically we are passing a onDissmiss funtion to this class which will be triggered when touched outside of this widget's bounds.

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

class NoRouteModalBarrier extends StatelessWidget {
  final Function onDismiss;

  /// Creates a widget that blocks user interaction.
  const NoRouteModalBarrier({
    Key key,
    this.color,
    this.dismissible = true,
    this.semanticsLabel,
    this.barrierSemanticsDismissible = true,
    this.onDismiss,
  }) : super(key: key);

  /// If non-null, fill the barrier with this color.
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierColor], which controls this property for the
  ///    [ModalBarrier] built by [ModalRoute] pages.
  final Color color;

  /// Whether touching the barrier will pop the current route off the [Navigator].
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierDismissible], which controls this property for the
  ///    [ModalBarrier] built by [ModalRoute] pages.
  final bool dismissible;

  /// Whether the modal barrier semantics are included in the semantics tree.
  ///
  /// See also:
  ///
  ///  * [ModalRoute.semanticsDismissible], which controls this property for
  ///    the [ModalBarrier] built by [ModalRoute] pages.
  final bool barrierSemanticsDismissible;

  /// Semantics label used for the barrier if it is [dismissible].
  ///
  /// The semantics label is read out by accessibility tools (e.g. TalkBack
  /// on Android and VoiceOver on iOS) when the barrier is focused.
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierLabel], which controls this property for the
  ///    [ModalBarrier] built by [ModalRoute] pages.
  final String semanticsLabel;

  @override
  Widget build(BuildContext context) {
    assert(!dismissible ||
        semanticsLabel == null ||
        debugCheckHasDirectionality(context));
    bool platformSupportsDismissingBarrier;
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        platformSupportsDismissingBarrier = false;
        break;
      case TargetPlatform.iOS:
        platformSupportsDismissingBarrier = true;
        break;
    }
    assert(platformSupportsDismissingBarrier != null);
    final bool semanticsDismissible =
        dismissible && platformSupportsDismissingBarrier;
    final bool modalBarrierSemanticsDismissible =
        barrierSemanticsDismissible ?? semanticsDismissible;
    return BlockSemantics(
      child: ExcludeSemantics(
        // On Android, the back button is used to dismiss a modal. On iOS, some
        // modal barriers are not dismissible in accessibility mode.
        excluding: !semanticsDismissible || !modalBarrierSemanticsDismissible,
        child: _ModalBarrierGestureDetector(
          onDismiss: () {
            print('NoRouteModalBarrier onDismiss $dismissible');
            if (dismissible && onDismiss != null) {
              onDismiss();
            }
          },
          child: Semantics(
            label: semanticsDismissible ? semanticsLabel : null,
            textDirection: semanticsDismissible && semanticsLabel != null
                ? Directionality.of(context)
                : null,
            child: MouseRegion(
              opaque: true,
              child: ConstrainedBox(
                constraints: const BoxConstraints.expand(),
                child: color == null
                    ? null
                    : DecoratedBox(
                        decoration: BoxDecoration(
                          color: color,
                        ),
                      ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _ModalBarrierGestureDetector extends StatelessWidget {
  const _ModalBarrierGestureDetector({
    Key key,
    @required this.child,
    @required this.onDismiss,
  })  : assert(child != null),
        assert(onDismiss != null),
        super(key: key);

  /// The widget below this widget in the tree.
  /// See [RawGestureDetector.child].
  final Widget child;

  /// Immediately called when an event that should dismiss the modal barrier
  /// has happened.
  final VoidCallback onDismiss;

  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures =
        <Type, GestureRecognizerFactory>{
      _AnyTapGestureRecognizer:
          _AnyTapGestureRecognizerFactory(onAnyTapUp: onDismiss),
    };

    return RawGestureDetector(
      gestures: gestures,
      behavior: HitTestBehavior.opaque,
      semantics: _ModalBarrierSemanticsDelegate(onDismiss: onDismiss),
      child: child,
    );
  }
}

class _ModalBarrierSemanticsDelegate extends SemanticsGestureDelegate {
  const _ModalBarrierSemanticsDelegate({this.onDismiss});

  final VoidCallback onDismiss;

  @override
  void assignSemantics(RenderSemanticsGestureHandler renderObject) {
    renderObject.onTap = onDismiss;
  }
}

class _AnyTapGestureRecognizerFactory
    extends GestureRecognizerFactory<_AnyTapGestureRecognizer> {
  const _AnyTapGestureRecognizerFactory({this.onAnyTapUp});

  final VoidCallback onAnyTapUp;

  @override
  _AnyTapGestureRecognizer constructor() => _AnyTapGestureRecognizer();

  @override
  void initializer(_AnyTapGestureRecognizer instance) {
    instance.onAnyTapUp = onAnyTapUp;
  }
}

// Recognizes tap down by any pointer button.
//
// It is similar to [TapGestureRecognizer.onTapDown], but accepts any single
// button, which means the gesture also takes parts in gesture arenas.
class _AnyTapGestureRecognizer extends BaseTapGestureRecognizer {
  _AnyTapGestureRecognizer({Object debugOwner}) : super(debugOwner: debugOwner);

  VoidCallback onAnyTapUp;

  @protected
  @override
  bool isPointerAllowed(PointerDownEvent event) {
    if (onAnyTapUp == null) return false;
    return super.isPointerAllowed(event);
  }

  @protected
  @override
  void handleTapDown({PointerDownEvent down}) {
    // Do nothing.
  }

  @protected
  @override
  void handleTapUp({PointerDownEvent down, PointerUpEvent up}) {
    if (onAnyTapUp != null) onAnyTapUp();
  }

  @protected
  @override
  void handleTapCancel(
      {PointerDownEvent down, PointerCancelEvent cancel, String reason}) {
    // Do nothing.
  }

  @override
  String get debugDescription => 'any tap';
}

The suggestions won't be closed on the back press. In order to that, it will have to be pushed as a route, not just as an overlay entry and that requires a whole lot of changes.

vikalpnagar avatar Apr 11 '20 04:04 vikalpnagar

@vikalpnagar I really want to add the method but I can't find class _TypeAheadFieldState in flutter_typeahead.dart.

qlssk8737 avatar Aug 04 '20 14:08 qlssk8737

@AbdulRahmanAlHamali are there any plans to support this behavior natively in the package? For web and desktop use cases, this would be a very common behavior/expectation.

Nash0x7E2 avatar Sep 21 '20 01:09 Nash0x7E2

My ugly one workaround in this source:

1. initialize bool

bool _suggestionBoxVisible = false;

@override
  Widget build(BuildContext context) {
    ...
}

2. Wrap the content of the page with GestureDetector widget with these conditions FocusManager hides the keyboard and also the SuggestionBox

e.g.:

 @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      body: SafeArea(
        child: SingleChildScrollView(
          child: GestureDetector(
            onTap: !_suggestionBoxVisible
                ? () {}
                : () => FocusManager.instance.primaryFocus.unfocus(),
            child: Column(
              children: [
                buildWaveContents(context),
                buildBottomContents(),
              ],
            ),
          ),
        ),
      ),
    );
  }
  1. Set the bool var to true when is SuggestionBox displayed (transitionBuilder) Set it to false when validate/save form
TypeAheadFormField(
              textFieldConfiguration: TextFieldConfiguration(
                controller: _textEditingController,
                decoration: InputDecoration(
                  suffixIcon: SizedBox(
                    width: 50.0,
                    child: RaisedButton(
                      padding: EdgeInsets.all(0.0),
                      onPressed: () {
                        _suggestionBoxVisible = false;

                       ...
                      },
                    ),
                  ),
                ),
              ),
              ...
              transitionBuilder: (context, suggestionsBox, controller) {
                _suggestionBoxVisible = true;

                return SizedBox(
                  height: 150.0,
                  child: suggestionsBox,
                );
              },
              ...
            ),

mzdm avatar Oct 22 '20 16:10 mzdm

For those who are seeking for an answer the easiest and the best I could came up with is described here https://stackoverflow.com/questions/51652897/how-to-hide-soft-input-keyboard-on-flutter-after-clicking-outside-textfield-anyw

SanjiKir avatar Jun 16 '21 19:06 SanjiKir

This works for me:

  • Wrap the Scaffold on GestureDetector. return GestureDetector( onTap: () => FocusManager.instance.primaryFocus?.unfocus(), child: Scaffold( appBar: AppBar( title: Text('Login'), ), body: Body(), ), );
  • Enabled the hideSuggestionsOnKeyboardHide on TypeAheadField widget. hideSuggestionsOnKeyboardHide: true,

For those who are seeking for an answer the easiest and the best I could came up with is described here https://stackoverflow.com/questions/51652897/how-to-hide-soft-input-keyboard-on-flutter-after-clicking-outside-textfield-anyw

Tesam avatar Jun 23 '22 14:06 Tesam

I found another solution.

  1. Add GestureDetector on top of the screen
GestureDetector(    onTap: () => FocusManager.instance.primaryFocus?.unfocus(),

  1. Add suggestionBoxCtrl and use the below function.
onSuggestionsBoxToggle: (isOpen) {
          if (!focusNode.hasFocus && isOpen) {
            suggestionBoxCtrl.suggestionsBox?.close();
          }
        },

Those who want to use hideSuggestionsOnKeyboardHide: false can try this one.

manojeeva avatar Jun 02 '23 07:06 manojeeva