Skip to content

Commit 3225aa5

Browse files
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 flutter/flutter#128332 and #134965
1 parent 8b150bd commit 3225aa5

File tree

4 files changed

+531
-233
lines changed

4 files changed

+531
-233
lines changed

packages/flutter/lib/src/material/search_anchor.dart

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -707,18 +707,13 @@ class _ViewContentState extends State<_ViewContent> {
707707
late Rect _viewRect;
708708
late final SearchController _controller;
709709
Iterable<Widget> result = <Widget>[];
710-
final FocusNode _focusNode = FocusNode();
711710

712711
@override
713712
void initState() {
714713
super.initState();
715714
_viewRect = widget.viewRect;
716715
_controller = widget.searchController;
717716
_controller.addListener(updateSuggestions);
718-
719-
if (!_focusNode.hasFocus) {
720-
_focusNode.requestFocus();
721-
}
722717
}
723718

724719
@override
@@ -748,7 +743,6 @@ class _ViewContentState extends State<_ViewContent> {
748743
@override
749744
void dispose() {
750745
_controller.removeListener(updateSuggestions);
751-
_focusNode.dispose();
752746
super.dispose();
753747
}
754748

@@ -865,8 +859,8 @@ class _ViewContentState extends State<_ViewContent> {
865859
top: false,
866860
bottom: false,
867861
child: SearchBar(
862+
autoFocus: true,
868863
constraints: widget.showFullScreenView ? BoxConstraints(minHeight: _SearchViewDefaultsM3.fullScreenBarHeight) : null,
869-
focusNode: _focusNode,
870864
leading: widget.viewLeading ?? defaultLeading,
871865
trailing: widget.viewTrailing ?? defaultTrailing,
872866
hintText: widget.viewHintText,
@@ -1091,6 +1085,7 @@ class SearchBar extends StatefulWidget {
10911085
this.textStyle,
10921086
this.hintStyle,
10931087
this.textCapitalization,
1088+
this.autoFocus = false,
10941089
});
10951090

10961091
/// Controls the text being edited in the search bar's text field.
@@ -1212,6 +1207,9 @@ class SearchBar extends StatefulWidget {
12121207
/// {@macro flutter.widgets.editableText.textCapitalization}
12131208
final TextCapitalization? textCapitalization;
12141209

1210+
/// {@macro flutter.widgets.editableText.autofocus}
1211+
final bool autoFocus;
1212+
12151213
@override
12161214
State<SearchBar> createState() => _SearchBarState();
12171215
}
@@ -1311,7 +1309,9 @@ class _SearchBarState extends State<SearchBar> {
13111309
child: InkWell(
13121310
onTap: () {
13131311
widget.onTap?.call();
1314-
_focusNode.requestFocus();
1312+
if (!_focusNode.hasFocus) {
1313+
_focusNode.requestFocus();
1314+
}
13151315
},
13161316
overlayColor: effectiveOverlayColor,
13171317
customBorder: effectiveShape?.copyWith(side: effectiveSide),
@@ -1323,34 +1323,34 @@ class _SearchBarState extends State<SearchBar> {
13231323
children: <Widget>[
13241324
if (leading != null) leading,
13251325
Expanded(
1326-
child: IgnorePointer(
1327-
child: Padding(
1328-
padding: effectivePadding,
1329-
child: TextField(
1330-
focusNode: _focusNode,
1331-
onChanged: widget.onChanged,
1332-
onSubmitted: widget.onSubmitted,
1333-
controller: widget.controller,
1334-
style: effectiveTextStyle,
1335-
decoration: InputDecoration(
1336-
hintText: widget.hintText,
1337-
).applyDefaults(InputDecorationTheme(
1338-
hintStyle: effectiveHintStyle,
1339-
1340-
// The configuration below is to make sure that the text field
1341-
// in `SearchBar` will not be overridden by the overall `InputDecorationTheme`
1342-
enabledBorder: InputBorder.none,
1343-
border: InputBorder.none,
1344-
focusedBorder: InputBorder.none,
1345-
contentPadding: EdgeInsets.zero,
1346-
// Setting `isDense` to true to allow the text field height to be
1347-
// smaller than 48.0
1348-
isDense: true,
1349-
)),
1350-
textCapitalization: effectiveTextCapitalization,
1351-
),
1326+
child: Padding(
1327+
padding: effectivePadding,
1328+
child: TextField(
1329+
autofocus: widget.autoFocus,
1330+
onTap: widget.onTap,
1331+
onTapAlwaysCalled: true,
1332+
focusNode: _focusNode,
1333+
onChanged: widget.onChanged,
1334+
onSubmitted: widget.onSubmitted,
1335+
controller: widget.controller,
1336+
style: effectiveTextStyle,
1337+
decoration: InputDecoration(
1338+
hintText: widget.hintText,
1339+
).applyDefaults(InputDecorationTheme(
1340+
hintStyle: effectiveHintStyle,
1341+
// The configuration below is to make sure that the text field
1342+
// in `SearchBar` will not be overridden by the overall `InputDecorationTheme`
1343+
enabledBorder: InputBorder.none,
1344+
border: InputBorder.none,
1345+
focusedBorder: InputBorder.none,
1346+
contentPadding: EdgeInsets.zero,
1347+
// Setting `isDense` to true to allow the text field height to be
1348+
// smaller than 48.0
1349+
isDense: true,
1350+
)),
1351+
textCapitalization: effectiveTextCapitalization,
13521352
),
1353-
)
1353+
),
13541354
),
13551355
if (trailing != null) ...trailing,
13561356
],

packages/flutter/lib/src/material/text_field.dart

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
6969
void onSingleTapUp(TapDragUpDetails details) {
7070
super.onSingleTapUp(details);
7171
_state._requestKeyboard();
72+
}
73+
74+
@override
75+
bool get onUserTapAlwaysCalled => _state.widget.onTapAlwaysCalled;
76+
77+
@override
78+
void onUserTap() {
7279
_state.widget.onTap?.call();
7380
}
7481

@@ -297,6 +304,7 @@ class TextField extends StatefulWidget {
297304
bool? enableInteractiveSelection,
298305
this.selectionControls,
299306
this.onTap,
307+
this.onTapAlwaysCalled = false,
300308
this.onTapOutside,
301309
this.mouseCursor,
302310
this.buildCounter,
@@ -636,7 +644,7 @@ class TextField extends StatefulWidget {
636644
bool get selectionEnabled => enableInteractiveSelection;
637645

638646
/// {@template flutter.material.textfield.onTap}
639-
/// Called for each distinct tap except for every second tap of a double tap.
647+
/// Called for the first tap in a series of taps.
640648
///
641649
/// The text field builds a [GestureDetector] to handle input events like tap,
642650
/// to trigger focus requests, to move the caret, adjust the selection, etc.
@@ -655,8 +663,17 @@ class TextField extends StatefulWidget {
655663
/// To listen to arbitrary pointer events without competing with the
656664
/// text field's internal gesture detector, use a [Listener].
657665
/// {@endtemplate}
666+
///
667+
/// If [onTapAlwaysCalled] is enabled, this will also be called for consecutive
668+
/// taps.
658669
final GestureTapCallback? onTap;
659670

671+
/// Whether [onTap] should be called for every tap.
672+
///
673+
/// Defaults to false, so [onTap] is only called for each distinct tap. When
674+
/// enabled, [onTap] is called for every tap including consecutive taps.
675+
final bool onTapAlwaysCalled;
676+
660677
/// {@macro flutter.widgets.editableText.onTapOutside}
661678
///
662679
/// {@tool dartpad}

packages/flutter/lib/src/widgets/text_selection.dart

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2268,6 +2268,27 @@ class TextSelectionGestureDetectorBuilder {
22682268
}
22692269
}
22702270

2271+
/// Whether the provided [onUserTap] callback should be dispatched on every
2272+
/// tap or only non-consecutive taps.
2273+
///
2274+
/// Defaults to false.
2275+
@protected
2276+
bool get onUserTapAlwaysCalled => false;
2277+
2278+
/// Handler for [TextSelectionGestureDetector.onUserTap].
2279+
///
2280+
/// By default, it serves as placeholder to enable subclass override.
2281+
///
2282+
/// See also:
2283+
///
2284+
/// * [TextSelectionGestureDetector.onUserTap], which triggers this
2285+
/// callback.
2286+
/// * [TextSelectionGestureDetector.onUserTapAlwaysCalled], which controls
2287+
/// whether this callback is called only on the first tap in a series
2288+
/// of taps.
2289+
@protected
2290+
void onUserTap() { /* Subclass should override this method if needed. */ }
2291+
22712292
/// Handler for [TextSelectionGestureDetector.onSingleTapUp].
22722293
///
22732294
/// By default, it selects word edge if selection is enabled.
@@ -2371,7 +2392,7 @@ class TextSelectionGestureDetectorBuilder {
23712392

23722393
/// Handler for [TextSelectionGestureDetector.onSingleTapCancel].
23732394
///
2374-
/// By default, it services as place holder to enable subclass override.
2395+
/// By default, it serves as placeholder to enable subclass override.
23752396
///
23762397
/// See also:
23772398
///
@@ -2992,6 +3013,7 @@ class TextSelectionGestureDetectorBuilder {
29923013
onSecondaryTapDown: onSecondaryTapDown,
29933014
onSingleTapUp: onSingleTapUp,
29943015
onSingleTapCancel: onSingleTapCancel,
3016+
onUserTap: onUserTap,
29953017
onSingleLongTapStart: onSingleLongTapStart,
29963018
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
29973019
onSingleLongTapEnd: onSingleLongTapEnd,
@@ -3000,6 +3022,7 @@ class TextSelectionGestureDetectorBuilder {
30003022
onDragSelectionStart: onDragSelectionStart,
30013023
onDragSelectionUpdate: onDragSelectionUpdate,
30023024
onDragSelectionEnd: onDragSelectionEnd,
3025+
onUserTapAlwaysCalled: onUserTapAlwaysCalled,
30033026
behavior: behavior,
30043027
child: child,
30053028
);
@@ -3033,6 +3056,7 @@ class TextSelectionGestureDetector extends StatefulWidget {
30333056
this.onSecondaryTapDown,
30343057
this.onSingleTapUp,
30353058
this.onSingleTapCancel,
3059+
this.onUserTap,
30363060
this.onSingleLongTapStart,
30373061
this.onSingleLongTapMoveUpdate,
30383062
this.onSingleLongTapEnd,
@@ -3041,6 +3065,7 @@ class TextSelectionGestureDetector extends StatefulWidget {
30413065
this.onDragSelectionStart,
30423066
this.onDragSelectionUpdate,
30433067
this.onDragSelectionEnd,
3068+
this.onUserTapAlwaysCalled = false,
30443069
this.behavior,
30453070
required this.child,
30463071
});
@@ -3083,6 +3108,13 @@ class TextSelectionGestureDetector extends StatefulWidget {
30833108
/// another gesture from the touch is recognized.
30843109
final GestureCancelCallback? onSingleTapCancel;
30853110

3111+
/// Called for the first tap in a series of taps when [onUserTapAlwaysCalled] is
3112+
/// disabled, which is the default behavior.
3113+
///
3114+
/// When [onUserTapAlwaysCalled] is enabled, this is called for every tap,
3115+
/// including consecutive taps.
3116+
final GestureTapCallback? onUserTap;
3117+
30863118
/// Called for a single long tap that's sustained for longer than
30873119
/// [kLongPressTimeout] but not necessarily lifted. Not called for a
30883120
/// double-tap-hold, which calls [onDoubleTapDown] instead.
@@ -3111,6 +3143,11 @@ class TextSelectionGestureDetector extends StatefulWidget {
31113143
/// Called when a mouse that was previously dragging is released.
31123144
final GestureTapDragEndCallback? onDragSelectionEnd;
31133145

3146+
/// Whether [onUserTap] will be called for all taps including consecutive taps.
3147+
///
3148+
/// Defaults to false, so [onUserTap] is only called for each distinct tap.
3149+
final bool onUserTapAlwaysCalled;
3150+
31143151
/// How this gesture detector should behave during hit testing.
31153152
///
31163153
/// This defaults to [HitTestBehavior.deferToChild].
@@ -3189,6 +3226,9 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
31893226
void _handleTapUp(TapDragUpDetails details) {
31903227
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 1) {
31913228
widget.onSingleTapUp?.call(details);
3229+
widget.onUserTap?.call();
3230+
} else if (widget.onUserTapAlwaysCalled) {
3231+
widget.onUserTap?.call();
31923232
}
31933233
}
31943234

0 commit comments

Comments
 (0)