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();
if (!_focusNode.hasFocus) {
_focusNode.requestFocus(); _focusNode.requestFocus();
}
}, },
overlayColor: effectiveOverlayColor, overlayColor: effectiveOverlayColor,
customBorder: effectiveShape?.copyWith(side: effectiveSide), customBorder: effectiveShape?.copyWith(side: effectiveSide),
...@@ -1323,10 +1323,12 @@ class _SearchBarState extends State<SearchBar> { ...@@ -1323,10 +1323,12 @@ 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,
onTap: widget.onTap,
onTapAlwaysCalled: true,
focusNode: _focusNode, focusNode: _focusNode,
onChanged: widget.onChanged, onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted, onSubmitted: widget.onSubmitted,
...@@ -1336,7 +1338,6 @@ class _SearchBarState extends State<SearchBar> { ...@@ -1336,7 +1338,6 @@ class _SearchBarState extends State<SearchBar> {
hintText: widget.hintText, hintText: widget.hintText,
).applyDefaults(InputDecorationTheme( ).applyDefaults(InputDecorationTheme(
hintStyle: effectiveHintStyle, hintStyle: effectiveHintStyle,
// The configuration below is to make sure that the text field // The configuration below is to make sure that the text field
// in `SearchBar` will not be overridden by the overall `InputDecorationTheme` // in `SearchBar` will not be overridden by the overall `InputDecorationTheme`
enabledBorder: InputBorder.none, enabledBorder: InputBorder.none,
...@@ -1350,7 +1351,6 @@ class _SearchBarState extends State<SearchBar> { ...@@ -1350,7 +1351,6 @@ class _SearchBarState extends State<SearchBar> {
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();
} }
} }
......
...@@ -4,11 +4,53 @@ ...@@ -4,11 +4,53 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
void main() { void main() {
// Returns the RenderEditable at the given index, or the first if not given.
RenderEditable findRenderEditable(WidgetTester tester, {int index = 0}) {
final RenderObject root = tester.renderObject(find.byType(EditableText).at(index));
expect(root, isNotNull);
late RenderEditable renderEditable;
void recursiveFinder(RenderObject child) {
if (child is RenderEditable) {
renderEditable = child;
return;
}
child.visitChildren(recursiveFinder);
}
root.visitChildren(recursiveFinder);
expect(renderEditable, isNotNull);
return renderEditable;
}
List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) {
return points.map<TextSelectionPoint>((TextSelectionPoint point) {
return TextSelectionPoint(
box.localToGlobal(point.point),
point.direction,
);
}).toList();
}
Offset textOffsetToPosition(WidgetTester tester, int offset, {int index = 0}) {
final RenderEditable renderEditable = findRenderEditable(tester, index: index);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(
TextSelection.collapsed(offset: offset),
),
renderEditable,
);
expect(endpoints.length, 1);
return endpoints[0].point + const Offset(kIsWeb? 1.0 : 0.0, -2.0);
}
testWidgetsWithLeakTracking('SearchBar defaults', (WidgetTester tester) async { testWidgetsWithLeakTracking('SearchBar defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true); final ThemeData theme = ThemeData(useMaterial3: true);
final ColorScheme colorScheme = theme.colorScheme; final ColorScheme colorScheme = theme.colorScheme;
...@@ -420,14 +462,16 @@ void main() { ...@@ -420,14 +462,16 @@ void main() {
// On pressed. // On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar))); await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump(); await tester.pumpAndSettle();
await gesture.removePointer();
material = tester.widget<Material>(searchBarMaterial); material = tester.widget<Material>(searchBarMaterial);
expect(material.elevation, pressedElevation); expect(material.elevation, pressedElevation);
// On focused. // On focused.
await tester.tap(find.byType(SearchBar)); await gesture.up();
await tester.pump();
// Remove the pointer so we are no longer hovering.
await gesture.removePointer();
await tester.pump(); await tester.pump();
material = tester.widget<Material>(searchBarMaterial); material = tester.widget<Material>(searchBarMaterial);
expect(material.elevation, focusedElevation); expect(material.elevation, focusedElevation);
...@@ -460,14 +504,16 @@ void main() { ...@@ -460,14 +504,16 @@ void main() {
// On pressed. // On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar))); await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump(); await tester.pumpAndSettle();
await gesture.removePointer();
material = tester.widget<Material>(searchBarMaterial); material = tester.widget<Material>(searchBarMaterial);
expect(material.color, pressedColor); expect(material.color, pressedColor);
// On focused. // On focused.
await tester.tap(find.byType(SearchBar)); await gesture.up();
await tester.pump();
// Remove the pointer so we are no longer hovering.
await gesture.removePointer();
await tester.pump(); await tester.pump();
material = tester.widget<Material>(searchBarMaterial); material = tester.widget<Material>(searchBarMaterial);
expect(material.color, focusedColor); expect(material.color, focusedColor);
...@@ -500,14 +546,16 @@ void main() { ...@@ -500,14 +546,16 @@ void main() {
// On pressed. // On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar))); await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump(); await tester.pumpAndSettle();
await gesture.removePointer();
material = tester.widget<Material>(searchBarMaterial); material = tester.widget<Material>(searchBarMaterial);
expect(material.shadowColor, pressedColor); expect(material.shadowColor, pressedColor);
// On focused. // On focused.
await tester.tap(find.byType(SearchBar)); await gesture.up();
await tester.pump();
// Remove the pointer so we are no longer hovering.
await gesture.removePointer();
await tester.pump(); await tester.pump();
material = tester.widget<Material>(searchBarMaterial); material = tester.widget<Material>(searchBarMaterial);
expect(material.shadowColor, focusedColor); expect(material.shadowColor, focusedColor);
...@@ -540,14 +588,16 @@ void main() { ...@@ -540,14 +588,16 @@ void main() {
// On pressed. // On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar))); await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump(); await tester.pumpAndSettle();
await gesture.removePointer();
material = tester.widget<Material>(searchBarMaterial); material = tester.widget<Material>(searchBarMaterial);
expect(material.surfaceTintColor, pressedColor); expect(material.surfaceTintColor, pressedColor);
// On focused. // On focused.
await tester.tap(find.byType(SearchBar)); await gesture.up();
await tester.pump();
// Remove the pointer so we are no longer hovering.
await gesture.removePointer();
await tester.pump(); await tester.pump();
material = tester.widget<Material>(searchBarMaterial); material = tester.widget<Material>(searchBarMaterial);
expect(material.surfaceTintColor, focusedColor); expect(material.surfaceTintColor, focusedColor);
...@@ -579,16 +629,18 @@ void main() { ...@@ -579,16 +629,18 @@ void main() {
// On pressed. // On pressed.
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.startGesture(tester.getCenter(find.byType(SearchBar))); await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect()..rect(color: pressedColor.withOpacity(1.0))); expect(inkFeatures, paints..rect()..rect(color: pressedColor.withOpacity(1.0)));
await gesture.removePointer();
// On focused. // On focused.
await tester.pumpAndSettle(); await tester.pumpAndSettle();
focusNode.requestFocus(); await gesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Remove the pointer so we are no longer hovering.
await gesture.removePointer();
await tester.pump();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect()..rect(color: focusedColor.withOpacity(1.0))); expect(inkFeatures, paints..rect()..rect(color: focusedColor.withOpacity(1.0)));
}); });
...@@ -654,14 +706,16 @@ void main() { ...@@ -654,14 +706,16 @@ void main() {
// On pressed. // On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar))); await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump(); await tester.pumpAndSettle();
await gesture.removePointer();
material = tester.widget<Material>(searchBarMaterial); material = tester.widget<Material>(searchBarMaterial);
expect(material.shape, pressedShape.copyWith(side: pressedSide)); expect(material.shape, pressedShape.copyWith(side: pressedSide));
// On focused. // On focused.
await tester.tap(find.byType(SearchBar)); await gesture.up();
await tester.pump();
// Remove the pointer so we are no longer hovering.
await gesture.removePointer();
await tester.pump(); await tester.pump();
material = tester.widget<Material>(searchBarMaterial); material = tester.widget<Material>(searchBarMaterial);
expect(material.shape, focusedShape.copyWith(side: focusedSide)); expect(material.shape, focusedShape.copyWith(side: focusedSide));
...@@ -717,13 +771,15 @@ void main() { ...@@ -717,13 +771,15 @@ void main() {
// On pressed. // On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar))); await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump(); await tester.pumpAndSettle();
helperText = tester.widget(find.text('hint text')); helperText = tester.widget(find.text('hint text'));
expect(helperText.style?.color, pressedColor); expect(helperText.style?.color, pressedColor);
await gesture.removePointer();
// On focused. // On focused.
await tester.tap(find.byType(SearchBar)); await gesture.up();
await tester.pump();
// Remove the pointer so we are no longer hovering.
await gesture.removePointer();
await tester.pump(); await tester.pump();
helperText = tester.widget(find.text('hint text')); helperText = tester.widget(find.text('hint text'));
expect(helperText.style?.color, focusedColor); expect(helperText.style?.color, focusedColor);
...@@ -754,13 +810,15 @@ void main() { ...@@ -754,13 +810,15 @@ void main() {
// On pressed. // On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar))); await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump(); await tester.pumpAndSettle();
await gesture.removePointer();
inputText = tester.widget(find.text('input text')); inputText = tester.widget(find.text('input text'));
expect(inputText.style.color, pressedColor); expect(inputText.style.color, pressedColor);
// On focused. // On focused.
await tester.tap(find.byType(SearchBar)); await gesture.up();
await tester.pump();
// Remove the pointer so we are no longer hovering.
await gesture.removePointer();
await tester.pump(); await tester.pump();
inputText = tester.widget(find.text('input text')); inputText = tester.widget(find.text('input text'));
expect(inputText.style.color, focusedColor); expect(inputText.style.color, focusedColor);
...@@ -1003,13 +1061,15 @@ void main() { ...@@ -1003,13 +1061,15 @@ void main() {
// On pressed. // On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar))); await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump(); await tester.pumpAndSettle();
await gesture.removePointer();
helperText = tester.widget(find.text('hint text')); helperText = tester.widget(find.text('hint text'));
expect(helperText.style?.color, pressedColor); expect(helperText.style?.color, pressedColor);
// On focused. // On focused.
await tester.tap(find.byType(SearchBar)); await gesture.up();
await tester.pump();
// Remove the pointer so we are no longer hovering.
await gesture.removePointer();
await tester.pump(); await tester.pump();
helperText = tester.widget(find.text('hint text')); helperText = tester.widget(find.text('hint text'));
expect(helperText.style?.color, focusedColor); expect(helperText.style?.color, focusedColor);
...@@ -1922,7 +1982,8 @@ void main() { ...@@ -1922,7 +1982,8 @@ void main() {
final SearchController controller = SearchController(); final SearchController controller = SearchController();
addTearDown(controller.dispose); addTearDown(controller.dispose);
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(
MaterialApp(
home: Material( home: Material(
child: SearchAnchor.bar( child: SearchAnchor.bar(
searchController: controller, searchController: controller,
...@@ -1938,7 +1999,8 @@ void main() { ...@@ -1938,7 +1999,8 @@ void main() {
]; ];
}, },
), ),
),), ),
),
); );
expect(controller.isOpen, false); expect(controller.isOpen, false);
...@@ -1954,7 +2016,8 @@ void main() { ...@@ -1954,7 +2016,8 @@ void main() {
}); });
testWidgetsWithLeakTracking('Search view does not go off the screen - LTR', (WidgetTester tester) async { testWidgetsWithLeakTracking('Search view does not go off the screen - LTR', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(
MaterialApp(
home: Material( home: Material(
child: Align( child: Align(
// Put the search anchor on the bottom-right corner of the screen to test // Put the search anchor on the bottom-right corner of the screen to test
...@@ -1975,7 +2038,8 @@ void main() { ...@@ -1975,7 +2038,8 @@ void main() {
}, },
), ),
), ),
),), ),
),
); );
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search); final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
...@@ -1991,7 +2055,8 @@ void main() { ...@@ -1991,7 +2055,8 @@ void main() {
}); });
testWidgetsWithLeakTracking('Search view does not go off the screen - RTL', (WidgetTester tester) async { testWidgetsWithLeakTracking('Search view does not go off the screen - RTL', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(
MaterialApp(
home: Directionality( home: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: Material( child: Material(
...@@ -2015,7 +2080,8 @@ void main() { ...@@ -2015,7 +2080,8 @@ void main() {
), ),
), ),
), ),
),), ),
),
); );
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search); final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
...@@ -2057,7 +2123,8 @@ void main() { ...@@ -2057,7 +2123,8 @@ void main() {
}, },
), ),
), ),
),); ),
);
} }
// Test LTR text direction. // Test LTR text direction.
...@@ -2095,7 +2162,8 @@ void main() { ...@@ -2095,7 +2162,8 @@ void main() {
tester.view.physicalSize = const Size(500.0, 600.0); tester.view.physicalSize = const Size(500.0, 600.0);
tester.view.devicePixelRatio = 1.0; tester.view.devicePixelRatio = 1.0;
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(
MaterialApp(
home: Material( home: Material(
child: SearchAnchor( child: SearchAnchor(
isFullScreen: false, isFullScreen: false,
...@@ -2115,7 +2183,8 @@ void main() { ...@@ -2115,7 +2183,8 @@ void main() {
}, },
), ),
), ),
)); ),
);
// Open the search view // Open the search view
await tester.tap(find.byIcon(Icons.search)); await tester.tap(find.byIcon(Icons.search));
...@@ -2134,7 +2203,8 @@ void main() { ...@@ -2134,7 +2203,8 @@ void main() {
tester.view.physicalSize = const Size(500.0, 600.0); tester.view.physicalSize = const Size(500.0, 600.0);
tester.view.devicePixelRatio = 1.0; tester.view.devicePixelRatio = 1.0;
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(
MaterialApp(
home: Material( home: Material(
child: SearchAnchor( child: SearchAnchor(
isFullScreen: true, isFullScreen: true,
...@@ -2154,7 +2224,8 @@ void main() { ...@@ -2154,7 +2224,8 @@ void main() {
}, },
), ),
), ),
)); ),
);
// Open a full-screen search view // Open a full-screen search view
await tester.tap(find.byIcon(Icons.search)); await tester.tap(find.byIcon(Icons.search));
...@@ -2170,7 +2241,8 @@ void main() { ...@@ -2170,7 +2241,8 @@ void main() {
testWidgetsWithLeakTracking('Search view route does not throw exception during pop animation', (WidgetTester tester) async { testWidgetsWithLeakTracking('Search view route does not throw exception during pop animation', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/126590. // Regression test for https://github.com/flutter/flutter/issues/126590.
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(
MaterialApp(
home: Material( home: Material(
child: Center( child: Center(
child: SearchAnchor( child: SearchAnchor(
...@@ -2195,7 +2267,8 @@ void main() { ...@@ -2195,7 +2267,8 @@ void main() {
}), }),
), ),
), ),
)); ),
);
// Open search view // Open search view
await tester.tap(find.byIcon(Icons.search)); await tester.tap(find.byIcon(Icons.search));
...@@ -2211,7 +2284,8 @@ void main() { ...@@ -2211,7 +2284,8 @@ void main() {
testWidgetsWithLeakTracking('Docked search should position itself correctly based on closest navigator', (WidgetTester tester) async { testWidgetsWithLeakTracking('Docked search should position itself correctly based on closest navigator', (WidgetTester tester) async {
const double rootSpacing = 100.0; const double rootSpacing = 100.0;
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(
MaterialApp(
builder: (BuildContext context, Widget? child) { builder: (BuildContext context, Widget? child) {
return Scaffold( return Scaffold(
body: Padding( body: Padding(
...@@ -2236,7 +2310,8 @@ void main() { ...@@ -2236,7 +2310,8 @@ void main() {
}, },
), ),
), ),
)); ),
);
await tester.tap(find.byIcon(Icons.search)); await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -2252,7 +2327,8 @@ void main() { ...@@ -2252,7 +2327,8 @@ void main() {
const double rootSpacing = 100.0; const double rootSpacing = 100.0;
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(
MaterialApp(
builder: (BuildContext context, Widget? child) { builder: (BuildContext context, Widget? child) {
return Scaffold( return Scaffold(
body: Padding( body: Padding(
...@@ -2280,7 +2356,8 @@ void main() { ...@@ -2280,7 +2356,8 @@ void main() {
), ),
), ),
), ),
)); ),
);
await tester.tap(find.byIcon(Icons.search)); await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -2289,6 +2366,170 @@ void main() { ...@@ -2289,6 +2366,170 @@ void main() {
expect(searchViewRect.bottomRight, equals(const Offset(300.0, 300.0))); expect(searchViewRect.bottomRight, equals(const Offset(300.0, 300.0)));
}); });
// Regression tests for https://github.com/flutter/flutter/issues/128332
group('SearchAnchor text selection', () {
testWidgetsWithLeakTracking('can right-click to select word', (WidgetTester tester) async {
const String defaultText = 'initial text';
final SearchController controller = SearchController();
addTearDown(controller.dispose);
controller.text = defaultText;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor.bar(
searchController: controller,
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
);
expect(controller.value.text, defaultText);
expect(find.text(defaultText), findsOneWidget);
final TestGesture gesture = await tester.startGesture(
textOffsetToPosition(tester, 4) + const Offset(0.0, -9.0),
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.value.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
await gesture.removePointer();
}, variant: TargetPlatformVariant.only(TargetPlatform.macOS));
testWidgetsWithLeakTracking('can click to set position', (WidgetTester tester) async {
const String defaultText = 'initial text';
final SearchController controller = SearchController();
addTearDown(controller.dispose);
controller.text = defaultText;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor.bar(
searchController: controller,
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
);
expect(controller.value.text, defaultText);
expect(find.text(defaultText), findsOneWidget);
final TestGesture gesture = await _pointGestureToSearchBar(tester);
await gesture.down(textOffsetToPosition(tester, 2) + const Offset(0.0, -9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.value.selection, const TextSelection.collapsed(offset: 2));
await gesture.down(textOffsetToPosition(tester, 9, index: 1) + const Offset(0.0, -9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.value.selection, const TextSelection.collapsed(offset: 9));
await gesture.removePointer();
}, variant: TargetPlatformVariant.desktop());
testWidgetsWithLeakTracking('can double-click to select word', (WidgetTester tester) async {
const String defaultText = 'initial text';
final SearchController controller = SearchController();
addTearDown(controller.dispose);
controller.text = defaultText;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor.bar(
searchController: controller,
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
);
expect(controller.value.text, defaultText);
expect(find.text(defaultText), findsOneWidget);
final TestGesture gesture = await _pointGestureToSearchBar(tester);
final Offset targetPosition = textOffsetToPosition(tester, 4) + const Offset(0.0, -9.0);
await gesture.down(targetPosition);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
final Offset targetPositionAfterViewOpened = textOffsetToPosition(tester, 4, index: 1) + const Offset(0.0, -9.0);
await gesture.down(targetPositionAfterViewOpened);
await tester.pumpAndSettle();
await gesture.up();
await tester.pump();
await gesture.down(targetPositionAfterViewOpened);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.value.selection, const TextSelection(baseOffset: 0, extentOffset: 7));
await gesture.removePointer();
}, variant: TargetPlatformVariant.desktop());
testWidgetsWithLeakTracking('can triple-click to select field', (WidgetTester tester) async {
const String defaultText = 'initial text';
final SearchController controller = SearchController();
addTearDown(controller.dispose);
controller.text = defaultText;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor.bar(
searchController: controller,
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
);
expect(controller.value.text, defaultText);
expect(find.text(defaultText), findsOneWidget);
final TestGesture gesture = await _pointGestureToSearchBar(tester);
final Offset targetPosition = textOffsetToPosition(tester, 4) + const Offset(0.0, -9.0);
await gesture.down(targetPosition);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
final Offset targetPositionAfterViewOpened = textOffsetToPosition(tester, 4, index: 1) + const Offset(0.0, -9.0);
await gesture.down(targetPositionAfterViewOpened);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(targetPositionAfterViewOpened);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(targetPositionAfterViewOpened);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.value.selection, const TextSelection(baseOffset: 0, extentOffset: 12));
await gesture.removePointer();
}, variant: TargetPlatformVariant.desktop());
});
// Regression tests for https://github.com/flutter/flutter/issues/126623 // Regression tests for https://github.com/flutter/flutter/issues/126623
group('Overall InputDecorationTheme does not impact SearchBar and SearchView', () { group('Overall InputDecorationTheme does not impact SearchBar and SearchView', () {
......
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