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
This commit is contained in:
parent
8b150bd076
commit
3225aa5815
@ -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> {
|
|||||||
_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> {
|
|||||||
@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> {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// {@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> {
|
|||||||
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> {
|
|||||||
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
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
|||||||
|
|
||||||
/// 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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
/// 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
|
|||||||
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 @@
|
|||||||
|
|
||||||
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() {
|
|||||||
|
|
||||||
// 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() {
|
|||||||
|
|
||||||
// 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() {
|
|||||||
|
|
||||||
// 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() {
|
|||||||
|
|
||||||
// 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() {
|
|||||||
|
|
||||||
// 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() {
|
|||||||
|
|
||||||
// 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() {
|
|||||||
|
|
||||||
// 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() {
|
|||||||
|
|
||||||
// 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() {
|
|||||||
|
|
||||||
// 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,23 +1982,25 @@ void main() {
|
|||||||
final SearchController controller = SearchController();
|
final SearchController controller = SearchController();
|
||||||
addTearDown(controller.dispose);
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(
|
||||||
home: Material(
|
MaterialApp(
|
||||||
child: SearchAnchor.bar(
|
home: Material(
|
||||||
searchController: controller,
|
child: SearchAnchor.bar(
|
||||||
isFullScreen: false,
|
searchController: controller,
|
||||||
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
isFullScreen: false,
|
||||||
return <Widget>[
|
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
||||||
ListTile(
|
return <Widget>[
|
||||||
title: const Text('item 0'),
|
ListTile(
|
||||||
onTap: () {
|
title: const Text('item 0'),
|
||||||
controller.closeView('item 0');
|
onTap: () {
|
||||||
},
|
controller.closeView('item 0');
|
||||||
)
|
},
|
||||||
];
|
)
|
||||||
},
|
];
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(controller.isOpen, false);
|
expect(controller.isOpen, false);
|
||||||
@ -1954,51 +2016,13 @@ 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(
|
||||||
home: Material(
|
MaterialApp(
|
||||||
child: Align(
|
home: Material(
|
||||||
// Put the search anchor on the bottom-right corner of the screen to test
|
|
||||||
// if the search view goes off the window.
|
|
||||||
alignment: Alignment.bottomRight,
|
|
||||||
child: SearchAnchor(
|
|
||||||
isFullScreen: false,
|
|
||||||
builder: (BuildContext context, SearchController controller) {
|
|
||||||
return IconButton(
|
|
||||||
icon: const Icon(Icons.search),
|
|
||||||
onPressed: () {
|
|
||||||
controller.openView();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
|
||||||
return <Widget>[];
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),),
|
|
||||||
);
|
|
||||||
|
|
||||||
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
|
|
||||||
final Rect iconButton = tester.getRect(findIconButton);
|
|
||||||
// Icon button has a size of (48.0, 48.0) and the screen size is (800.0, 600.0).
|
|
||||||
expect(iconButton, equals(const Rect.fromLTRB(752.0, 552.0, 800.0, 600.0)));
|
|
||||||
|
|
||||||
await tester.tap(find.byIcon(Icons.search));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
|
|
||||||
expect(searchViewRect, equals(const Rect.fromLTRB(440.0, 200.0, 800.0, 600.0)));
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgetsWithLeakTracking('Search view does not go off the screen - RTL', (WidgetTester tester) async {
|
|
||||||
await tester.pumpWidget(MaterialApp(
|
|
||||||
home: Directionality(
|
|
||||||
textDirection: TextDirection.rtl,
|
|
||||||
child: Material(
|
|
||||||
child: Align(
|
child: Align(
|
||||||
// Put the search anchor on the bottom-left corner of the screen to test
|
// Put the search anchor on the bottom-right corner of the screen to test
|
||||||
// if the search view goes off the window when the text direction is right-to-left.
|
// if the search view goes off the window.
|
||||||
alignment: Alignment.bottomLeft,
|
alignment: Alignment.bottomRight,
|
||||||
child: SearchAnchor(
|
child: SearchAnchor(
|
||||||
isFullScreen: false,
|
isFullScreen: false,
|
||||||
builder: (BuildContext context, SearchController controller) {
|
builder: (BuildContext context, SearchController controller) {
|
||||||
@ -2015,7 +2039,49 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),),
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
|
||||||
|
final Rect iconButton = tester.getRect(findIconButton);
|
||||||
|
// Icon button has a size of (48.0, 48.0) and the screen size is (800.0, 600.0).
|
||||||
|
expect(iconButton, equals(const Rect.fromLTRB(752.0, 552.0, 800.0, 600.0)));
|
||||||
|
|
||||||
|
await tester.tap(find.byIcon(Icons.search));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
|
||||||
|
expect(searchViewRect, equals(const Rect.fromLTRB(440.0, 200.0, 800.0, 600.0)));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgetsWithLeakTracking('Search view does not go off the screen - RTL', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Directionality(
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
child: Material(
|
||||||
|
child: Align(
|
||||||
|
// Put the search anchor on the bottom-left corner of the screen to test
|
||||||
|
// if the search view goes off the window when the text direction is right-to-left.
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
child: SearchAnchor(
|
||||||
|
isFullScreen: false,
|
||||||
|
builder: (BuildContext context, SearchController controller) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
onPressed: () {
|
||||||
|
controller.openView();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
||||||
|
return <Widget>[];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
|
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
|
||||||
@ -2057,7 +2123,8 @@ void main() {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),);
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test LTR text direction.
|
// Test LTR text direction.
|
||||||
@ -2095,27 +2162,29 @@ 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(
|
||||||
home: Material(
|
MaterialApp(
|
||||||
child: SearchAnchor(
|
home: Material(
|
||||||
isFullScreen: false,
|
child: SearchAnchor(
|
||||||
builder: (BuildContext context, SearchController controller) {
|
isFullScreen: false,
|
||||||
return Align(
|
builder: (BuildContext context, SearchController controller) {
|
||||||
alignment: Alignment.bottomRight,
|
return Align(
|
||||||
child: IconButton(
|
alignment: Alignment.bottomRight,
|
||||||
icon: const Icon(Icons.search),
|
child: IconButton(
|
||||||
onPressed: () {
|
icon: const Icon(Icons.search),
|
||||||
controller.openView();
|
onPressed: () {
|
||||||
},
|
controller.openView();
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
},
|
||||||
return <Widget>[];
|
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
||||||
},
|
return <Widget>[];
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
);
|
||||||
|
|
||||||
// Open the search view
|
// Open the search view
|
||||||
await tester.tap(find.byIcon(Icons.search));
|
await tester.tap(find.byIcon(Icons.search));
|
||||||
@ -2134,27 +2203,29 @@ 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(
|
||||||
home: Material(
|
MaterialApp(
|
||||||
child: SearchAnchor(
|
home: Material(
|
||||||
isFullScreen: true,
|
child: SearchAnchor(
|
||||||
builder: (BuildContext context, SearchController controller) {
|
isFullScreen: true,
|
||||||
return Align(
|
builder: (BuildContext context, SearchController controller) {
|
||||||
alignment: Alignment.bottomRight,
|
return Align(
|
||||||
child: IconButton(
|
alignment: Alignment.bottomRight,
|
||||||
icon: const Icon(Icons.search),
|
child: IconButton(
|
||||||
onPressed: () {
|
icon: const Icon(Icons.search),
|
||||||
controller.openView();
|
onPressed: () {
|
||||||
},
|
controller.openView();
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
},
|
||||||
return <Widget>[];
|
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
||||||
},
|
return <Widget>[];
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
);
|
||||||
|
|
||||||
// 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,32 +2241,34 @@ 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(
|
||||||
home: Material(
|
MaterialApp(
|
||||||
child: Center(
|
home: Material(
|
||||||
child: SearchAnchor(
|
child: Center(
|
||||||
builder: (BuildContext context, SearchController controller) {
|
child: SearchAnchor(
|
||||||
return IconButton(
|
builder: (BuildContext context, SearchController controller) {
|
||||||
icon: const Icon(Icons.search),
|
return IconButton(
|
||||||
onPressed: () {
|
icon: const Icon(Icons.search),
|
||||||
controller.openView();
|
onPressed: () {
|
||||||
},
|
controller.openView();
|
||||||
);
|
},
|
||||||
},
|
|
||||||
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
|
||||||
return List<Widget>.generate(5, (int index) {
|
|
||||||
final String item = 'item $index';
|
|
||||||
return ListTile(
|
|
||||||
leading: const Icon(Icons.history),
|
|
||||||
title: Text(item),
|
|
||||||
trailing: const Icon(Icons.chevron_right),
|
|
||||||
onTap: () {},
|
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
}),
|
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
||||||
|
return List<Widget>.generate(5, (int index) {
|
||||||
|
final String item = 'item $index';
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.history),
|
||||||
|
title: Text(item),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
);
|
||||||
|
|
||||||
// Open search view
|
// Open search view
|
||||||
await tester.tap(find.byIcon(Icons.search));
|
await tester.tap(find.byIcon(Icons.search));
|
||||||
@ -2211,59 +2284,17 @@ 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(
|
||||||
builder: (BuildContext context, Widget? child) {
|
MaterialApp(
|
||||||
return Scaffold(
|
builder: (BuildContext context, Widget? child) {
|
||||||
body: Padding(
|
return Scaffold(
|
||||||
padding: const EdgeInsets.all(rootSpacing),
|
body: Padding(
|
||||||
child: child,
|
padding: const EdgeInsets.all(rootSpacing),
|
||||||
),
|
child: child,
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
home: Material(
|
},
|
||||||
child: SearchAnchor(
|
home: Material(
|
||||||
isFullScreen: false,
|
|
||||||
builder: (BuildContext context, SearchController controller) {
|
|
||||||
return IconButton(
|
|
||||||
icon: const Icon(Icons.search),
|
|
||||||
onPressed: () {
|
|
||||||
controller.openView();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
|
||||||
return <Widget>[];
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
|
|
||||||
await tester.tap(find.byIcon(Icons.search));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
|
|
||||||
expect(searchViewRect.topLeft, equals(const Offset(rootSpacing, rootSpacing)));
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgetsWithLeakTracking('Docked search view with nested navigator does not go off the screen', (WidgetTester tester) async {
|
|
||||||
addTearDown(tester.view.reset);
|
|
||||||
tester.view.physicalSize = const Size(400.0, 400.0);
|
|
||||||
tester.view.devicePixelRatio = 1.0;
|
|
||||||
|
|
||||||
const double rootSpacing = 100.0;
|
|
||||||
|
|
||||||
await tester.pumpWidget(MaterialApp(
|
|
||||||
builder: (BuildContext context, Widget? child) {
|
|
||||||
return Scaffold(
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(rootSpacing),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
home: Material(
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.bottomRight,
|
|
||||||
child: SearchAnchor(
|
child: SearchAnchor(
|
||||||
isFullScreen: false,
|
isFullScreen: false,
|
||||||
builder: (BuildContext context, SearchController controller) {
|
builder: (BuildContext context, SearchController controller) {
|
||||||
@ -2280,7 +2311,53 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byIcon(Icons.search));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
|
||||||
|
expect(searchViewRect.topLeft, equals(const Offset(rootSpacing, rootSpacing)));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgetsWithLeakTracking('Docked search view with nested navigator does not go off the screen', (WidgetTester tester) async {
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
tester.view.physicalSize = const Size(400.0, 400.0);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
|
||||||
|
const double rootSpacing = 100.0;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
builder: (BuildContext context, Widget? child) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(rootSpacing),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
home: Material(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: SearchAnchor(
|
||||||
|
isFullScreen: false,
|
||||||
|
builder: (BuildContext context, SearchController controller) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
onPressed: () {
|
||||||
|
controller.openView();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
||||||
|
return <Widget>[];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
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() {
|
|||||||
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', () {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user