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> {
late Rect _viewRect;
late final SearchController _controller;
Iterable<Widget> result = <Widget>[];
final FocusNode _focusNode = FocusNode();
@override
void initState() {
......@@ -715,10 +714,6 @@ class _ViewContentState extends State<_ViewContent> {
_viewRect = widget.viewRect;
_controller = widget.searchController;
_controller.addListener(updateSuggestions);
if (!_focusNode.hasFocus) {
_focusNode.requestFocus();
}
}
@override
......@@ -748,7 +743,6 @@ class _ViewContentState extends State<_ViewContent> {
@override
void dispose() {
_controller.removeListener(updateSuggestions);
_focusNode.dispose();
super.dispose();
}
......@@ -865,8 +859,8 @@ class _ViewContentState extends State<_ViewContent> {
top: false,
bottom: false,
child: SearchBar(
autoFocus: true,
constraints: widget.showFullScreenView ? BoxConstraints(minHeight: _SearchViewDefaultsM3.fullScreenBarHeight) : null,
focusNode: _focusNode,
leading: widget.viewLeading ?? defaultLeading,
trailing: widget.viewTrailing ?? defaultTrailing,
hintText: widget.viewHintText,
......@@ -1091,6 +1085,7 @@ class SearchBar extends StatefulWidget {
this.textStyle,
this.hintStyle,
this.textCapitalization,
this.autoFocus = false,
});
/// Controls the text being edited in the search bar's text field.
......@@ -1212,6 +1207,9 @@ class SearchBar extends StatefulWidget {
/// {@macro flutter.widgets.editableText.textCapitalization}
final TextCapitalization? textCapitalization;
/// {@macro flutter.widgets.editableText.autofocus}
final bool autoFocus;
@override
State<SearchBar> createState() => _SearchBarState();
}
......@@ -1311,7 +1309,9 @@ class _SearchBarState extends State<SearchBar> {
child: InkWell(
onTap: () {
widget.onTap?.call();
_focusNode.requestFocus();
if (!_focusNode.hasFocus) {
_focusNode.requestFocus();
}
},
overlayColor: effectiveOverlayColor,
customBorder: effectiveShape?.copyWith(side: effectiveSide),
......@@ -1323,34 +1323,34 @@ class _SearchBarState extends State<SearchBar> {
children: <Widget>[
if (leading != null) leading,
Expanded(
child: IgnorePointer(
child: Padding(
padding: effectivePadding,
child: TextField(
focusNode: _focusNode,
onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted,
controller: widget.controller,
style: effectiveTextStyle,
decoration: InputDecoration(
hintText: widget.hintText,
).applyDefaults(InputDecorationTheme(
hintStyle: effectiveHintStyle,
// The configuration below is to make sure that the text field
// in `SearchBar` will not be overridden by the overall `InputDecorationTheme`
enabledBorder: InputBorder.none,
border: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
// Setting `isDense` to true to allow the text field height to be
// smaller than 48.0
isDense: true,
)),
textCapitalization: effectiveTextCapitalization,
),
child: Padding(
padding: effectivePadding,
child: TextField(
autofocus: widget.autoFocus,
onTap: widget.onTap,
onTapAlwaysCalled: true,
focusNode: _focusNode,
onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted,
controller: widget.controller,
style: effectiveTextStyle,
decoration: InputDecoration(
hintText: widget.hintText,
).applyDefaults(InputDecorationTheme(
hintStyle: effectiveHintStyle,
// The configuration below is to make sure that the text field
// in `SearchBar` will not be overridden by the overall `InputDecorationTheme`
enabledBorder: InputBorder.none,
border: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
// Setting `isDense` to true to allow the text field height to be
// smaller than 48.0
isDense: true,
)),
textCapitalization: effectiveTextCapitalization,
),
)
),
),
if (trailing != null) ...trailing,
],
......
......@@ -69,6 +69,13 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
void onSingleTapUp(TapDragUpDetails details) {
super.onSingleTapUp(details);
_state._requestKeyboard();
}
@override
bool get onUserTapAlwaysCalled => _state.widget.onTapAlwaysCalled;
@override
void onUserTap() {
_state.widget.onTap?.call();
}
......@@ -297,6 +304,7 @@ class TextField extends StatefulWidget {
bool? enableInteractiveSelection,
this.selectionControls,
this.onTap,
this.onTapAlwaysCalled = false,
this.onTapOutside,
this.mouseCursor,
this.buildCounter,
......@@ -636,7 +644,7 @@ class TextField extends StatefulWidget {
bool get selectionEnabled => enableInteractiveSelection;
/// {@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,
/// to trigger focus requests, to move the caret, adjust the selection, etc.
......@@ -655,8 +663,17 @@ class TextField extends StatefulWidget {
/// To listen to arbitrary pointer events without competing with the
/// text field's internal gesture detector, use a [Listener].
/// {@endtemplate}
///
/// If [onTapAlwaysCalled] is enabled, this will also be called for consecutive
/// taps.
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}
///
/// {@tool dartpad}
......
......@@ -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].
///
/// By default, it selects word edge if selection is enabled.
......@@ -2371,7 +2392,7 @@ class TextSelectionGestureDetectorBuilder {
/// 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:
///
......@@ -2992,6 +3013,7 @@ class TextSelectionGestureDetectorBuilder {
onSecondaryTapDown: onSecondaryTapDown,
onSingleTapUp: onSingleTapUp,
onSingleTapCancel: onSingleTapCancel,
onUserTap: onUserTap,
onSingleLongTapStart: onSingleLongTapStart,
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
onSingleLongTapEnd: onSingleLongTapEnd,
......@@ -3000,6 +3022,7 @@ class TextSelectionGestureDetectorBuilder {
onDragSelectionStart: onDragSelectionStart,
onDragSelectionUpdate: onDragSelectionUpdate,
onDragSelectionEnd: onDragSelectionEnd,
onUserTapAlwaysCalled: onUserTapAlwaysCalled,
behavior: behavior,
child: child,
);
......@@ -3033,6 +3056,7 @@ class TextSelectionGestureDetector extends StatefulWidget {
this.onSecondaryTapDown,
this.onSingleTapUp,
this.onSingleTapCancel,
this.onUserTap,
this.onSingleLongTapStart,
this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd,
......@@ -3041,6 +3065,7 @@ class TextSelectionGestureDetector extends StatefulWidget {
this.onDragSelectionStart,
this.onDragSelectionUpdate,
this.onDragSelectionEnd,
this.onUserTapAlwaysCalled = false,
this.behavior,
required this.child,
});
......@@ -3083,6 +3108,13 @@ class TextSelectionGestureDetector extends StatefulWidget {
/// another gesture from the touch is recognized.
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
/// [kLongPressTimeout] but not necessarily lifted. Not called for a
/// double-tap-hold, which calls [onDoubleTapDown] instead.
......@@ -3111,6 +3143,11 @@ class TextSelectionGestureDetector extends StatefulWidget {
/// Called when a mouse that was previously dragging is released.
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.
///
/// This defaults to [HitTestBehavior.deferToChild].
......@@ -3189,6 +3226,9 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
void _handleTapUp(TapDragUpDetails details) {
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 1) {
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