SearchBar context menu (#154833)

SearchBar and SearchAnchor can now control their context menu. They both received new contextMenuBuilder parameters. See the docs for EditableText.contextMenuBuilder for how to use this, including how to use the native context menu on iOS and to control the browser's context menu on web.
This commit is contained in:
Justin McCandless 2024-09-10 09:54:00 -07:00 committed by GitHub
parent f964f15dcb
commit 24d0b1db0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 87 additions and 4 deletions

View File

@ -8,6 +8,7 @@ import 'dart:ui';
import 'package:flutter/widgets.dart';
import 'adaptive_text_selection_toolbar.dart';
import 'back_button.dart';
import 'button_style.dart';
import 'color_scheme.dart';
@ -186,6 +187,7 @@ class SearchAnchor extends StatefulWidget {
TextInputAction? textInputAction,
TextInputType? keyboardType,
EdgeInsets scrollPadding,
EditableTextContextMenuBuilder contextMenuBuilder,
}) = _SearchAnchorWithSearchBar;
/// Whether the search view grows to fill the entire screen when the
@ -1053,6 +1055,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor {
super.textInputAction,
super.keyboardType,
EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
EditableTextContextMenuBuilder contextMenuBuilder = SearchBar._defaultContextMenuBuilder,
}) : super(
viewHintText: viewHintText ?? barHintText,
headerHeight: viewHeaderHeight,
@ -1087,6 +1090,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor {
textInputAction: textInputAction,
keyboardType: keyboardType,
scrollPadding: scrollPadding,
contextMenuBuilder: contextMenuBuilder,
);
}
);
@ -1208,6 +1212,7 @@ class SearchBar extends StatefulWidget {
this.textInputAction,
this.keyboardType,
this.scrollPadding = const EdgeInsets.all(20.0),
this.contextMenuBuilder = _defaultContextMenuBuilder,
});
/// Controls the text being edited in the search bar's text field.
@ -1356,6 +1361,23 @@ class SearchBar extends StatefulWidget {
/// {@macro flutter.widgets.editableText.scrollPadding}
final EdgeInsets scrollPadding;
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
///
/// If not provided, will build a default menu based on the platform.
///
/// See also:
///
/// * [AdaptiveTextSelectionToolbar], which is built by default.
/// * [BrowserContextMenu], which allows the browser's context menu on web to
/// be disabled and Flutter-rendered context menus to appear.
final EditableTextContextMenuBuilder? contextMenuBuilder;
static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
return AdaptiveTextSelectionToolbar.editableText(
editableTextState: editableTextState,
);
}
@override
State<SearchBar> createState() => _SearchBarState();
}
@ -1497,6 +1519,7 @@ class _SearchBarState extends State<SearchBar> {
textInputAction: widget.textInputAction,
keyboardType: widget.keyboardType,
scrollPadding: widget.scrollPadding,
contextMenuBuilder: widget.contextMenuBuilder,
),
),
),

View File

@ -838,6 +838,8 @@ class TextField extends StatefulWidget {
/// See also:
///
/// * [AdaptiveTextSelectionToolbar], which is built by default.
/// * [BrowserContextMenu], which allows the browser's context menu on web to
/// be disabled and Flutter-rendered context menus to appear.
final EditableTextContextMenuBuilder? contextMenuBuilder;
/// Determine whether this text field can request the primary focus.

View File

@ -1902,10 +1902,10 @@ class EditableText extends StatefulWidget {
/// The [TextSelectionToolbarLayoutDelegate] class may be particularly useful
/// in honoring the preferred anchor positions.
///
/// For backwards compatibility, when [selectionControls] is set to an object
/// that does not mix in [TextSelectionHandleControls], [contextMenuBuilder]
/// is ignored and the [TextSelectionControls.buildToolbar] method is used
/// instead.
/// For backwards compatibility, when [EditableText.selectionControls] is set
/// to an object that does not mix in [TextSelectionHandleControls],
/// [contextMenuBuilder] is ignored and the
/// [TextSelectionControls.buildToolbar] method is used instead.
///
/// {@tool dartpad}
/// This example shows how to customize the menu, in this case by keeping the

View File

@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
@ -3361,6 +3362,63 @@ void main() {
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(editableText.scrollPadding, scrollPadding);
});
group('contextMenuBuilder', () {
setUp(() async {
if (!kIsWeb) {
return;
}
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.contextMenu,
(MethodCall call) {
// Just complete successfully, so that BrowserContextMenu thinks that
// the engine successfully received its call.
return Future<void>.value();
},
);
await BrowserContextMenu.disableContextMenu();
});
tearDown(() async {
if (!kIsWeb) {
return;
}
await BrowserContextMenu.enableContextMenu();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, null);
});
testWidgets('SearchAnchor.bar.contextMenuBuilder is passed through to EditableText', (WidgetTester tester) async {
Widget contextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
return const Placeholder();
}
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor.bar(
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
contextMenuBuilder: contextMenuBuilder,
),
),
),
);
expect(find.byType(EditableText), findsOneWidget);
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(editableText.contextMenuBuilder, contextMenuBuilder);
expect(find.byType(Placeholder), findsNothing);
await tester.tap(
find.byType(SearchBar),
buttons: kSecondaryButton,
);
await tester.pumpAndSettle();
expect(find.byType(Placeholder), findsOneWidget);
});
});
}
Future<void> checkSearchBarDefaults(WidgetTester tester, ColorScheme colorScheme, Material material) async {