diff --git a/packages/flutter/lib/src/material/search_anchor.dart b/packages/flutter/lib/src/material/search_anchor.dart index ad41c023045..d78b39b95d6 100644 --- a/packages/flutter/lib/src/material/search_anchor.dart +++ b/packages/flutter/lib/src/material/search_anchor.dart @@ -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 createState() => _SearchBarState(); } @@ -1497,6 +1519,7 @@ class _SearchBarState extends State { textInputAction: widget.textInputAction, keyboardType: widget.keyboardType, scrollPadding: widget.scrollPadding, + contextMenuBuilder: widget.contextMenuBuilder, ), ), ), diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 6c8177a1bd4..86dff783a9f 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -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. diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 548e39bdd90..538dafbae7e 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -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 diff --git a/packages/flutter/test/material/search_anchor_test.dart b/packages/flutter/test/material/search_anchor_test.dart index a3100b94e80..bce75781245 100644 --- a/packages/flutter/test/material/search_anchor_test.dart +++ b/packages/flutter/test/material/search_anchor_test.dart @@ -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.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 []; + }, + 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 checkSearchBarDefaults(WidgetTester tester, ColorScheme colorScheme, Material material) async {