Unverified Commit 3225aa58 authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

Fix text selection in `SearchAnchor/SearchBar` (#137636)

This changes fixes text selection gestures on the search field when using `SearchAnchor`.

Before this change text selection gestures did not work due to an `IgnorePointer` in the widget tree.

This change:
* Removes the `IgnorePointer` so the underlying `TextField` can receive pointer events.
* Introduces `TextField.onTapAlwaysCalled` and `TextSelectionGestureDetector.onUserTapAlwaysCalled`, so a user provided on tap callback can be called on consecutive taps. This is so that the user provided on tap callback for `SearchAnchor/SearchBar` that was previously only handled by `InkWell` will still work if a tap occurs in the `TextField`s hit box. The `TextField`s default behavior is maintained outside of the context of `SearchAnchor/SearchBar`.

Fixes https://github.com/flutter/flutter/issues/128332 and #134965
parent 8b150bd0
...@@ -707,7 +707,6 @@ class _ViewContentState extends State<_ViewContent> { ...@@ -707,7 +707,6 @@ class _ViewContentState extends State<_ViewContent> {
late Rect _viewRect; late Rect _viewRect;
late final SearchController _controller; late final SearchController _controller;
Iterable<Widget> result = <Widget>[]; Iterable<Widget> result = <Widget>[];
final FocusNode _focusNode = FocusNode();
@override @override
void initState() { void initState() {
...@@ -715,10 +714,6 @@ class _ViewContentState extends State<_ViewContent> { ...@@ -715,10 +714,6 @@ class _ViewContentState extends State<_ViewContent> {
_viewRect = widget.viewRect; _viewRect = widget.viewRect;
_controller = widget.searchController; _controller = widget.searchController;
_controller.addListener(updateSuggestions); _controller.addListener(updateSuggestions);
if (!_focusNode.hasFocus) {
_focusNode.requestFocus();
}
} }
@override @override
...@@ -748,7 +743,6 @@ class _ViewContentState extends State<_ViewContent> { ...@@ -748,7 +743,6 @@ class _ViewContentState extends State<_ViewContent> {
@override @override
void dispose() { void dispose() {
_controller.removeListener(updateSuggestions); _controller.removeListener(updateSuggestions);
_focusNode.dispose();
super.dispose(); super.dispose();
} }
...@@ -865,8 +859,8 @@ class _ViewContentState extends State<_ViewContent> { ...@@ -865,8 +859,8 @@ class _ViewContentState extends State<_ViewContent> {
top: false, top: false,
bottom: false, bottom: false,
child: SearchBar( child: SearchBar(
autoFocus: true,
constraints: widget.showFullScreenView ? BoxConstraints(minHeight: _SearchViewDefaultsM3.fullScreenBarHeight) : null, constraints: widget.showFullScreenView ? BoxConstraints(minHeight: _SearchViewDefaultsM3.fullScreenBarHeight) : null,
focusNode: _focusNode,
leading: widget.viewLeading ?? defaultLeading, leading: widget.viewLeading ?? defaultLeading,
trailing: widget.viewTrailing ?? defaultTrailing, trailing: widget.viewTrailing ?? defaultTrailing,
hintText: widget.viewHintText, hintText: widget.viewHintText,
...@@ -1091,6 +1085,7 @@ class SearchBar extends StatefulWidget { ...@@ -1091,6 +1085,7 @@ class SearchBar extends StatefulWidget {
this.textStyle, this.textStyle,
this.hintStyle, this.hintStyle,
this.textCapitalization, this.textCapitalization,
this.autoFocus = false,
}); });
/// Controls the text being edited in the search bar's text field. /// Controls the text being edited in the search bar's text field.
...@@ -1212,6 +1207,9 @@ class SearchBar extends StatefulWidget { ...@@ -1212,6 +1207,9 @@ class SearchBar extends StatefulWidget {
/// {@macro flutter.widgets.editableText.textCapitalization} /// {@macro flutter.widgets.editableText.textCapitalization}
final TextCapitalization? textCapitalization; final TextCapitalization? textCapitalization;
/// {@macro flutter.widgets.editableText.autofocus}
final bool autoFocus;
@override @override
State<SearchBar> createState() => _SearchBarState(); State<SearchBar> createState() => _SearchBarState();
} }
...@@ -1311,7 +1309,9 @@ class _SearchBarState extends State<SearchBar> { ...@@ -1311,7 +1309,9 @@ class _SearchBarState extends State<SearchBar> {
child: InkWell( child: InkWell(
onTap: () { onTap: () {
widget.onTap?.call(); widget.onTap?.call();
_focusNode.requestFocus(); if (!_focusNode.hasFocus) {
_focusNode.requestFocus();
}
}, },
overlayColor: effectiveOverlayColor, overlayColor: effectiveOverlayColor,
customBorder: effectiveShape?.copyWith(side: effectiveSide), customBorder: effectiveShape?.copyWith(side: effectiveSide),
...@@ -1323,34 +1323,34 @@ class _SearchBarState extends State<SearchBar> { ...@@ -1323,34 +1323,34 @@ class _SearchBarState extends State<SearchBar> {
children: <Widget>[ children: <Widget>[
if (leading != null) leading, if (leading != null) leading,
Expanded( Expanded(
child: IgnorePointer( child: Padding(
child: Padding( padding: effectivePadding,
padding: effectivePadding, child: TextField(
child: TextField( autofocus: widget.autoFocus,
focusNode: _focusNode, onTap: widget.onTap,
onChanged: widget.onChanged, onTapAlwaysCalled: true,
onSubmitted: widget.onSubmitted, focusNode: _focusNode,
controller: widget.controller, onChanged: widget.onChanged,
style: effectiveTextStyle, onSubmitted: widget.onSubmitted,
decoration: InputDecoration( controller: widget.controller,
hintText: widget.hintText, style: effectiveTextStyle,
).applyDefaults(InputDecorationTheme( decoration: InputDecoration(
hintStyle: effectiveHintStyle, hintText: widget.hintText,
).applyDefaults(InputDecorationTheme(
// The configuration below is to make sure that the text field hintStyle: effectiveHintStyle,
// in `SearchBar` will not be overridden by the overall `InputDecorationTheme` // The configuration below is to make sure that the text field
enabledBorder: InputBorder.none, // in `SearchBar` will not be overridden by the overall `InputDecorationTheme`
border: InputBorder.none, enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none, border: InputBorder.none,
contentPadding: EdgeInsets.zero, focusedBorder: InputBorder.none,
// Setting `isDense` to true to allow the text field height to be contentPadding: EdgeInsets.zero,
// smaller than 48.0 // Setting `isDense` to true to allow the text field height to be
isDense: true, // smaller than 48.0
)), isDense: true,
textCapitalization: effectiveTextCapitalization, )),
), textCapitalization: effectiveTextCapitalization,
), ),
) ),
), ),
if (trailing != null) ...trailing, if (trailing != null) ...trailing,
], ],
......
...@@ -69,6 +69,13 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete ...@@ -69,6 +69,13 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
void onSingleTapUp(TapDragUpDetails details) { void onSingleTapUp(TapDragUpDetails details) {
super.onSingleTapUp(details); super.onSingleTapUp(details);
_state._requestKeyboard(); _state._requestKeyboard();
}
@override
bool get onUserTapAlwaysCalled => _state.widget.onTapAlwaysCalled;
@override
void onUserTap() {
_state.widget.onTap?.call(); _state.widget.onTap?.call();
} }
...@@ -297,6 +304,7 @@ class TextField extends StatefulWidget { ...@@ -297,6 +304,7 @@ class TextField extends StatefulWidget {
bool? enableInteractiveSelection, bool? enableInteractiveSelection,
this.selectionControls, this.selectionControls,
this.onTap, this.onTap,
this.onTapAlwaysCalled = false,
this.onTapOutside, this.onTapOutside,
this.mouseCursor, this.mouseCursor,
this.buildCounter, this.buildCounter,
...@@ -636,7 +644,7 @@ class TextField extends StatefulWidget { ...@@ -636,7 +644,7 @@ class TextField extends StatefulWidget {
bool get selectionEnabled => enableInteractiveSelection; bool get selectionEnabled => enableInteractiveSelection;
/// {@template flutter.material.textfield.onTap} /// {@template flutter.material.textfield.onTap}
/// Called for each distinct tap except for every second tap of a double tap. /// Called for the first tap in a series of taps.
/// ///
/// The text field builds a [GestureDetector] to handle input events like tap, /// The text field builds a [GestureDetector] to handle input events like tap,
/// to trigger focus requests, to move the caret, adjust the selection, etc. /// to trigger focus requests, to move the caret, adjust the selection, etc.
...@@ -655,8 +663,17 @@ class TextField extends StatefulWidget { ...@@ -655,8 +663,17 @@ class TextField extends StatefulWidget {
/// To listen to arbitrary pointer events without competing with the /// To listen to arbitrary pointer events without competing with the
/// text field's internal gesture detector, use a [Listener]. /// text field's internal gesture detector, use a [Listener].
/// {@endtemplate} /// {@endtemplate}
///
/// If [onTapAlwaysCalled] is enabled, this will also be called for consecutive
/// taps.
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
/// Whether [onTap] should be called for every tap.
///
/// Defaults to false, so [onTap] is only called for each distinct tap. When
/// enabled, [onTap] is called for every tap including consecutive taps.
final bool onTapAlwaysCalled;
/// {@macro flutter.widgets.editableText.onTapOutside} /// {@macro flutter.widgets.editableText.onTapOutside}
/// ///
/// {@tool dartpad} /// {@tool dartpad}
......
...@@ -2268,6 +2268,27 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2268,6 +2268,27 @@ class TextSelectionGestureDetectorBuilder {
} }
} }
/// Whether the provided [onUserTap] callback should be dispatched on every
/// tap or only non-consecutive taps.
///
/// Defaults to false.
@protected
bool get onUserTapAlwaysCalled => false;
/// Handler for [TextSelectionGestureDetector.onUserTap].
///
/// By default, it serves as placeholder to enable subclass override.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onUserTap], which triggers this
/// callback.
/// * [TextSelectionGestureDetector.onUserTapAlwaysCalled], which controls
/// whether this callback is called only on the first tap in a series
/// of taps.
@protected
void onUserTap() { /* Subclass should override this method if needed. */ }
/// Handler for [TextSelectionGestureDetector.onSingleTapUp]. /// Handler for [TextSelectionGestureDetector.onSingleTapUp].
/// ///
/// By default, it selects word edge if selection is enabled. /// By default, it selects word edge if selection is enabled.
...@@ -2371,7 +2392,7 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2371,7 +2392,7 @@ class TextSelectionGestureDetectorBuilder {
/// Handler for [TextSelectionGestureDetector.onSingleTapCancel]. /// Handler for [TextSelectionGestureDetector.onSingleTapCancel].
/// ///
/// By default, it services as place holder to enable subclass override. /// By default, it serves as placeholder to enable subclass override.
/// ///
/// See also: /// See also:
/// ///
...@@ -2992,6 +3013,7 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2992,6 +3013,7 @@ class TextSelectionGestureDetectorBuilder {
onSecondaryTapDown: onSecondaryTapDown, onSecondaryTapDown: onSecondaryTapDown,
onSingleTapUp: onSingleTapUp, onSingleTapUp: onSingleTapUp,
onSingleTapCancel: onSingleTapCancel, onSingleTapCancel: onSingleTapCancel,
onUserTap: onUserTap,
onSingleLongTapStart: onSingleLongTapStart, onSingleLongTapStart: onSingleLongTapStart,
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate, onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
onSingleLongTapEnd: onSingleLongTapEnd, onSingleLongTapEnd: onSingleLongTapEnd,
...@@ -3000,6 +3022,7 @@ class TextSelectionGestureDetectorBuilder { ...@@ -3000,6 +3022,7 @@ class TextSelectionGestureDetectorBuilder {
onDragSelectionStart: onDragSelectionStart, onDragSelectionStart: onDragSelectionStart,
onDragSelectionUpdate: onDragSelectionUpdate, onDragSelectionUpdate: onDragSelectionUpdate,
onDragSelectionEnd: onDragSelectionEnd, onDragSelectionEnd: onDragSelectionEnd,
onUserTapAlwaysCalled: onUserTapAlwaysCalled,
behavior: behavior, behavior: behavior,
child: child, child: child,
); );
...@@ -3033,6 +3056,7 @@ class TextSelectionGestureDetector extends StatefulWidget { ...@@ -3033,6 +3056,7 @@ class TextSelectionGestureDetector extends StatefulWidget {
this.onSecondaryTapDown, this.onSecondaryTapDown,
this.onSingleTapUp, this.onSingleTapUp,
this.onSingleTapCancel, this.onSingleTapCancel,
this.onUserTap,
this.onSingleLongTapStart, this.onSingleLongTapStart,
this.onSingleLongTapMoveUpdate, this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd, this.onSingleLongTapEnd,
...@@ -3041,6 +3065,7 @@ class TextSelectionGestureDetector extends StatefulWidget { ...@@ -3041,6 +3065,7 @@ class TextSelectionGestureDetector extends StatefulWidget {
this.onDragSelectionStart, this.onDragSelectionStart,
this.onDragSelectionUpdate, this.onDragSelectionUpdate,
this.onDragSelectionEnd, this.onDragSelectionEnd,
this.onUserTapAlwaysCalled = false,
this.behavior, this.behavior,
required this.child, required this.child,
}); });
...@@ -3083,6 +3108,13 @@ class TextSelectionGestureDetector extends StatefulWidget { ...@@ -3083,6 +3108,13 @@ class TextSelectionGestureDetector extends StatefulWidget {
/// another gesture from the touch is recognized. /// another gesture from the touch is recognized.
final GestureCancelCallback? onSingleTapCancel; final GestureCancelCallback? onSingleTapCancel;
/// Called for the first tap in a series of taps when [onUserTapAlwaysCalled] is
/// disabled, which is the default behavior.
///
/// When [onUserTapAlwaysCalled] is enabled, this is called for every tap,
/// including consecutive taps.
final GestureTapCallback? onUserTap;
/// Called for a single long tap that's sustained for longer than /// Called for a single long tap that's sustained for longer than
/// [kLongPressTimeout] but not necessarily lifted. Not called for a /// [kLongPressTimeout] but not necessarily lifted. Not called for a
/// double-tap-hold, which calls [onDoubleTapDown] instead. /// double-tap-hold, which calls [onDoubleTapDown] instead.
...@@ -3111,6 +3143,11 @@ class TextSelectionGestureDetector extends StatefulWidget { ...@@ -3111,6 +3143,11 @@ class TextSelectionGestureDetector extends StatefulWidget {
/// Called when a mouse that was previously dragging is released. /// Called when a mouse that was previously dragging is released.
final GestureTapDragEndCallback? onDragSelectionEnd; final GestureTapDragEndCallback? onDragSelectionEnd;
/// Whether [onUserTap] will be called for all taps including consecutive taps.
///
/// Defaults to false, so [onUserTap] is only called for each distinct tap.
final bool onUserTapAlwaysCalled;
/// How this gesture detector should behave during hit testing. /// How this gesture detector should behave during hit testing.
/// ///
/// This defaults to [HitTestBehavior.deferToChild]. /// This defaults to [HitTestBehavior.deferToChild].
...@@ -3189,6 +3226,9 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec ...@@ -3189,6 +3226,9 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
void _handleTapUp(TapDragUpDetails details) { void _handleTapUp(TapDragUpDetails details) {
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 1) { if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 1) {
widget.onSingleTapUp?.call(details); widget.onSingleTapUp?.call(details);
widget.onUserTap?.call();
} else if (widget.onUserTapAlwaysCalled) {
widget.onUserTap?.call();
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment