Skip to content

Commit 24d0b1d

Browse files
authored
SearchBar context menu (flutter#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.
1 parent f964f15 commit 24d0b1d

File tree

4 files changed

+87
-4
lines changed

4 files changed

+87
-4
lines changed

packages/flutter/lib/src/material/search_anchor.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'dart:ui';
88

99
import 'package:flutter/widgets.dart';
1010

11+
import 'adaptive_text_selection_toolbar.dart';
1112
import 'back_button.dart';
1213
import 'button_style.dart';
1314
import 'color_scheme.dart';
@@ -186,6 +187,7 @@ class SearchAnchor extends StatefulWidget {
186187
TextInputAction? textInputAction,
187188
TextInputType? keyboardType,
188189
EdgeInsets scrollPadding,
190+
EditableTextContextMenuBuilder contextMenuBuilder,
189191
}) = _SearchAnchorWithSearchBar;
190192

191193
/// Whether the search view grows to fill the entire screen when the
@@ -1053,6 +1055,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor {
10531055
super.textInputAction,
10541056
super.keyboardType,
10551057
EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
1058+
EditableTextContextMenuBuilder contextMenuBuilder = SearchBar._defaultContextMenuBuilder,
10561059
}) : super(
10571060
viewHintText: viewHintText ?? barHintText,
10581061
headerHeight: viewHeaderHeight,
@@ -1087,6 +1090,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor {
10871090
textInputAction: textInputAction,
10881091
keyboardType: keyboardType,
10891092
scrollPadding: scrollPadding,
1093+
contextMenuBuilder: contextMenuBuilder,
10901094
);
10911095
}
10921096
);
@@ -1208,6 +1212,7 @@ class SearchBar extends StatefulWidget {
12081212
this.textInputAction,
12091213
this.keyboardType,
12101214
this.scrollPadding = const EdgeInsets.all(20.0),
1215+
this.contextMenuBuilder = _defaultContextMenuBuilder,
12111216
});
12121217

12131218
/// Controls the text being edited in the search bar's text field.
@@ -1356,6 +1361,23 @@ class SearchBar extends StatefulWidget {
13561361
/// {@macro flutter.widgets.editableText.scrollPadding}
13571362
final EdgeInsets scrollPadding;
13581363

1364+
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
1365+
///
1366+
/// If not provided, will build a default menu based on the platform.
1367+
///
1368+
/// See also:
1369+
///
1370+
/// * [AdaptiveTextSelectionToolbar], which is built by default.
1371+
/// * [BrowserContextMenu], which allows the browser's context menu on web to
1372+
/// be disabled and Flutter-rendered context menus to appear.
1373+
final EditableTextContextMenuBuilder? contextMenuBuilder;
1374+
1375+
static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
1376+
return AdaptiveTextSelectionToolbar.editableText(
1377+
editableTextState: editableTextState,
1378+
);
1379+
}
1380+
13591381
@override
13601382
State<SearchBar> createState() => _SearchBarState();
13611383
}
@@ -1497,6 +1519,7 @@ class _SearchBarState extends State<SearchBar> {
14971519
textInputAction: widget.textInputAction,
14981520
keyboardType: widget.keyboardType,
14991521
scrollPadding: widget.scrollPadding,
1522+
contextMenuBuilder: widget.contextMenuBuilder,
15001523
),
15011524
),
15021525
),

packages/flutter/lib/src/material/text_field.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,8 @@ class TextField extends StatefulWidget {
838838
/// See also:
839839
///
840840
/// * [AdaptiveTextSelectionToolbar], which is built by default.
841+
/// * [BrowserContextMenu], which allows the browser's context menu on web to
842+
/// be disabled and Flutter-rendered context menus to appear.
841843
final EditableTextContextMenuBuilder? contextMenuBuilder;
842844

843845
/// Determine whether this text field can request the primary focus.

packages/flutter/lib/src/widgets/editable_text.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,10 +1902,10 @@ class EditableText extends StatefulWidget {
19021902
/// The [TextSelectionToolbarLayoutDelegate] class may be particularly useful
19031903
/// in honoring the preferred anchor positions.
19041904
///
1905-
/// For backwards compatibility, when [selectionControls] is set to an object
1906-
/// that does not mix in [TextSelectionHandleControls], [contextMenuBuilder]
1907-
/// is ignored and the [TextSelectionControls.buildToolbar] method is used
1908-
/// instead.
1905+
/// For backwards compatibility, when [EditableText.selectionControls] is set
1906+
/// to an object that does not mix in [TextSelectionHandleControls],
1907+
/// [contextMenuBuilder] is ignored and the
1908+
/// [TextSelectionControls.buildToolbar] method is used instead.
19091909
///
19101910
/// {@tool dartpad}
19111911
/// This example shows how to customize the menu, in this case by keeping the

packages/flutter/test/material/search_anchor_test.dart

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart';
99
import 'package:flutter/gestures.dart';
1010
import 'package:flutter/material.dart';
1111
import 'package:flutter/rendering.dart';
12+
import 'package:flutter/services.dart';
1213
import 'package:flutter_test/flutter_test.dart';
1314

1415
import '../widgets/semantics_tester.dart';
@@ -3361,6 +3362,63 @@ void main() {
33613362
final EditableText editableText = tester.widget(find.byType(EditableText));
33623363
expect(editableText.scrollPadding, scrollPadding);
33633364
});
3365+
3366+
group('contextMenuBuilder', () {
3367+
setUp(() async {
3368+
if (!kIsWeb) {
3369+
return;
3370+
}
3371+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
3372+
SystemChannels.contextMenu,
3373+
(MethodCall call) {
3374+
// Just complete successfully, so that BrowserContextMenu thinks that
3375+
// the engine successfully received its call.
3376+
return Future<void>.value();
3377+
},
3378+
);
3379+
await BrowserContextMenu.disableContextMenu();
3380+
});
3381+
3382+
tearDown(() async {
3383+
if (!kIsWeb) {
3384+
return;
3385+
}
3386+
await BrowserContextMenu.enableContextMenu();
3387+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, null);
3388+
});
3389+
3390+
testWidgets('SearchAnchor.bar.contextMenuBuilder is passed through to EditableText', (WidgetTester tester) async {
3391+
Widget contextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
3392+
return const Placeholder();
3393+
}
3394+
await tester.pumpWidget(
3395+
MaterialApp(
3396+
home: Material(
3397+
child: SearchAnchor.bar(
3398+
suggestionsBuilder: (BuildContext context, SearchController controller) {
3399+
return <Widget>[];
3400+
},
3401+
contextMenuBuilder: contextMenuBuilder,
3402+
),
3403+
),
3404+
),
3405+
);
3406+
3407+
expect(find.byType(EditableText), findsOneWidget);
3408+
final EditableText editableText = tester.widget(find.byType(EditableText));
3409+
expect(editableText.contextMenuBuilder, contextMenuBuilder);
3410+
3411+
expect(find.byType(Placeholder), findsNothing);
3412+
3413+
await tester.tap(
3414+
find.byType(SearchBar),
3415+
buttons: kSecondaryButton,
3416+
);
3417+
await tester.pumpAndSettle();
3418+
3419+
expect(find.byType(Placeholder), findsOneWidget);
3420+
});
3421+
});
33643422
}
33653423

33663424
Future<void> checkSearchBarDefaults(WidgetTester tester, ColorScheme colorScheme, Material material) async {

0 commit comments

Comments
 (0)