flutter_typeahead
flutter_typeahead copied to clipboard
How do I close suggestion box when touched outside its bounds.
Hi @vikalpnagar Did you find a solution?
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 I really want to add the method but I can't find class _TypeAheadFieldState in flutter_typeahead.dart.
@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.
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(),
],
),
),
),
),
);
}
- 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,
);
},
...
),
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
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
I found another solution.
- Add GestureDetector on top of the screen
GestureDetector( onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
- 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.