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();
if (!_focusNode.hasFocus) {
_focusNode.requestFocus();
}
},
overlayColor: effectiveOverlayColor,
customBorder: effectiveShape?.copyWith(side: effectiveSide),
......@@ -1323,10 +1323,12 @@ class _SearchBarState extends State<SearchBar> {
children: <Widget>[
if (leading != null) leading,
Expanded(
child: IgnorePointer(
child: Padding(
padding: effectivePadding,
child: TextField(
autofocus: widget.autoFocus,
onTap: widget.onTap,
onTapAlwaysCalled: true,
focusNode: _focusNode,
onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted,
......@@ -1336,7 +1338,6 @@ class _SearchBarState extends State<SearchBar> {
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,
......@@ -1350,7 +1351,6 @@ class _SearchBarState extends State<SearchBar> {
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();
}
}
......
......@@ -4,11 +4,53 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
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 {
final ThemeData theme = ThemeData(useMaterial3: true);
final ColorScheme colorScheme = theme.colorScheme;
......@@ -420,14 +462,16 @@ void main() {
// On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump();
await gesture.removePointer();
await tester.pumpAndSettle();
material = tester.widget<Material>(searchBarMaterial);
expect(material.elevation, pressedElevation);
// 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();
material = tester.widget<Material>(searchBarMaterial);
expect(material.elevation, focusedElevation);
......@@ -460,14 +504,16 @@ void main() {
// On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump();
await gesture.removePointer();
await tester.pumpAndSettle();
material = tester.widget<Material>(searchBarMaterial);
expect(material.color, pressedColor);
// 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();
material = tester.widget<Material>(searchBarMaterial);
expect(material.color, focusedColor);
......@@ -500,14 +546,16 @@ void main() {
// On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump();
await gesture.removePointer();
await tester.pumpAndSettle();
material = tester.widget<Material>(searchBarMaterial);
expect(material.shadowColor, pressedColor);
// 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();
material = tester.widget<Material>(searchBarMaterial);
expect(material.shadowColor, focusedColor);
......@@ -540,14 +588,16 @@ void main() {
// On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump();
await gesture.removePointer();
await tester.pumpAndSettle();
material = tester.widget<Material>(searchBarMaterial);
expect(material.surfaceTintColor, pressedColor);
// 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();
material = tester.widget<Material>(searchBarMaterial);
expect(material.surfaceTintColor, focusedColor);
......@@ -579,16 +629,18 @@ void main() {
// On pressed.
await tester.pumpAndSettle();
await tester.startGesture(tester.getCenter(find.byType(SearchBar)));
await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect()..rect(color: pressedColor.withOpacity(1.0)));
await gesture.removePointer();
// On focused.
await tester.pumpAndSettle();
focusNode.requestFocus();
await gesture.up();
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');
expect(inkFeatures, paints..rect()..rect(color: focusedColor.withOpacity(1.0)));
});
......@@ -654,14 +706,16 @@ void main() {
// On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump();
await gesture.removePointer();
await tester.pumpAndSettle();
material = tester.widget<Material>(searchBarMaterial);
expect(material.shape, pressedShape.copyWith(side: pressedSide));
// 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();
material = tester.widget<Material>(searchBarMaterial);
expect(material.shape, focusedShape.copyWith(side: focusedSide));
......@@ -717,13 +771,15 @@ void main() {
// On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump();
await tester.pumpAndSettle();
helperText = tester.widget(find.text('hint text'));
expect(helperText.style?.color, pressedColor);
await gesture.removePointer();
// 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();
helperText = tester.widget(find.text('hint text'));
expect(helperText.style?.color, focusedColor);
......@@ -754,13 +810,15 @@ void main() {
// On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump();
await gesture.removePointer();
await tester.pumpAndSettle();
inputText = tester.widget(find.text('input text'));
expect(inputText.style.color, pressedColor);
// 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();
inputText = tester.widget(find.text('input text'));
expect(inputText.style.color, focusedColor);
......@@ -1003,13 +1061,15 @@ void main() {
// On pressed.
await gesture.down(tester.getCenter(find.byType(SearchBar)));
await tester.pump();
await gesture.removePointer();
await tester.pumpAndSettle();
helperText = tester.widget(find.text('hint text'));
expect(helperText.style?.color, pressedColor);
// 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();
helperText = tester.widget(find.text('hint text'));
expect(helperText.style?.color, focusedColor);
......@@ -1922,7 +1982,8 @@ void main() {
final SearchController controller = SearchController();
addTearDown(controller.dispose);
await tester.pumpWidget(MaterialApp(
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor.bar(
searchController: controller,
......@@ -1938,7 +1999,8 @@ void main() {
];
},
),
),),
),
),
);
expect(controller.isOpen, false);
......@@ -1954,7 +2016,8 @@ void main() {
});
testWidgetsWithLeakTracking('Search view does not go off the screen - LTR', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Align(
// Put the search anchor on the bottom-right corner of the screen to test
......@@ -1975,7 +2038,8 @@ void main() {
},
),
),
),),
),
),
);
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
......@@ -1991,7 +2055,8 @@ void main() {
});
testWidgetsWithLeakTracking('Search view does not go off the screen - RTL', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Material(
......@@ -2015,7 +2080,8 @@ void main() {
),
),
),
),),
),
),
);
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
......@@ -2057,7 +2123,8 @@ void main() {
},
),
),
),);
),
);
}
// Test LTR text direction.
......@@ -2095,7 +2162,8 @@ void main() {
tester.view.physicalSize = const Size(500.0, 600.0);
tester.view.devicePixelRatio = 1.0;
await tester.pumpWidget(MaterialApp(
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor(
isFullScreen: false,
......@@ -2115,7 +2183,8 @@ void main() {
},
),
),
));
),
);
// Open the search view
await tester.tap(find.byIcon(Icons.search));
......@@ -2134,7 +2203,8 @@ void main() {
tester.view.physicalSize = const Size(500.0, 600.0);
tester.view.devicePixelRatio = 1.0;
await tester.pumpWidget(MaterialApp(
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor(
isFullScreen: true,
......@@ -2154,7 +2224,8 @@ void main() {
},
),
),
));
),
);
// Open a full-screen search view
await tester.tap(find.byIcon(Icons.search));
......@@ -2170,7 +2241,8 @@ void main() {
testWidgetsWithLeakTracking('Search view route does not throw exception during pop animation', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/126590.
await tester.pumpWidget(MaterialApp(
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SearchAnchor(
......@@ -2195,7 +2267,8 @@ void main() {
}),
),
),
));
),
);
// Open search view
await tester.tap(find.byIcon(Icons.search));
......@@ -2211,7 +2284,8 @@ void main() {
testWidgetsWithLeakTracking('Docked search should position itself correctly based on closest navigator', (WidgetTester tester) async {
const double rootSpacing = 100.0;
await tester.pumpWidget(MaterialApp(
await tester.pumpWidget(
MaterialApp(
builder: (BuildContext context, Widget? child) {
return Scaffold(
body: Padding(
......@@ -2236,7 +2310,8 @@ void main() {
},
),
),
));
),
);
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
......@@ -2252,7 +2327,8 @@ void main() {
const double rootSpacing = 100.0;
await tester.pumpWidget(MaterialApp(
await tester.pumpWidget(
MaterialApp(
builder: (BuildContext context, Widget? child) {
return Scaffold(
body: Padding(
......@@ -2280,7 +2356,8 @@ void main() {
),
),
),
));
),
);
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
......@@ -2289,6 +2366,170 @@ void main() {
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
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