Skip to content

Commit eebb1d6

Browse files
authored
Add option for flexible space on material SearchDelegate (#128132)
This pull request introduces the `buildFlexibleSpace` method to the `SearchDelegate` class in the material library. It allows users to add a flexible space widget to the `AppBar` in a `_SearchPage`, providing more customization options. This PR does not fix any specific issue as there are no open issues related to this feature.
1 parent 7e2f1f5 commit eebb1d6

File tree

2 files changed

+181
-9
lines changed

2 files changed

+181
-9
lines changed

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,15 @@ abstract class SearchDelegate<T> {
211211
///
212212
PreferredSizeWidget? buildBottom(BuildContext context) => null;
213213

214+
/// Widget to display a flexible space in the [AppBar].
215+
///
216+
/// Returns null by default, i.e. a flexible space widget is not included.
217+
///
218+
/// See also:
219+
///
220+
/// * [AppBar.flexibleSpace], the intended use for the return value of this method.
221+
Widget? buildFlexibleSpace(BuildContext context) => null;
222+
214223
/// The theme used to configure the search page.
215224
///
216225
/// The returned [ThemeData] will be used to wrap the entire search page,
@@ -581,11 +590,10 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
581590
style: widget.delegate.searchFieldStyle ?? theme.textTheme.titleLarge,
582591
textInputAction: widget.delegate.textInputAction,
583592
keyboardType: widget.delegate.keyboardType,
584-
onSubmitted: (String _) {
585-
widget.delegate.showResults(context);
586-
},
593+
onSubmitted: (String _) => widget.delegate.showResults(context),
587594
decoration: InputDecoration(hintText: searchFieldLabel),
588595
),
596+
flexibleSpace: widget.delegate.buildFlexibleSpace(context),
589597
actions: widget.delegate.buildActions(context),
590598
bottom: widget.delegate.buildBottom(context),
591599
),

packages/flutter/test/material/search_test.dart

Lines changed: 170 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,163 @@ void main() {
589589
expect(tester.testTextInput.setClientArgs!['inputAction'], TextInputAction.done.toString());
590590
});
591591

592+
testWidgets('Custom flexibleSpace value', (WidgetTester tester) async {
593+
const Widget flexibleSpace = Text('custom flexibleSpace');
594+
final _TestSearchDelegate delegate = _TestSearchDelegate(flexibleSpace: flexibleSpace);
595+
596+
await tester.pumpWidget(TestHomePage(delegate: delegate));
597+
await tester.tap(find.byTooltip('Search'));
598+
await tester.pumpAndSettle();
599+
600+
expect(find.byWidget(flexibleSpace), findsOneWidget);
601+
});
602+
603+
604+
group('contributes semantics with custom flexibleSpace', () {
605+
const Widget flexibleSpace = Text('FlexibleSpace');
606+
607+
TestSemantics buildExpected({ required String routeName }) {
608+
return TestSemantics.root(
609+
children: <TestSemantics>[
610+
TestSemantics(
611+
id: 1,
612+
textDirection: TextDirection.ltr,
613+
children: <TestSemantics>[
614+
TestSemantics(
615+
id: 2,
616+
children: <TestSemantics>[
617+
TestSemantics(
618+
id: 3,
619+
flags: <SemanticsFlag>[
620+
SemanticsFlag.scopesRoute,
621+
SemanticsFlag.namesRoute,
622+
],
623+
label: routeName,
624+
textDirection: TextDirection.ltr,
625+
children: <TestSemantics>[
626+
TestSemantics(
627+
id: 4,
628+
children: <TestSemantics>[
629+
TestSemantics(
630+
id: 6,
631+
children: <TestSemantics>[
632+
TestSemantics(
633+
id: 8,
634+
flags: <SemanticsFlag>[
635+
SemanticsFlag.hasEnabledState,
636+
SemanticsFlag.isButton,
637+
SemanticsFlag.isEnabled,
638+
SemanticsFlag.isFocusable,
639+
],
640+
actions: <SemanticsAction>[SemanticsAction.tap],
641+
tooltip: 'Back',
642+
textDirection: TextDirection.ltr,
643+
),
644+
TestSemantics(
645+
id: 9,
646+
flags: <SemanticsFlag>[
647+
SemanticsFlag.isTextField,
648+
SemanticsFlag.isFocused,
649+
SemanticsFlag.isHeader,
650+
if (debugDefaultTargetPlatformOverride != TargetPlatform.iOS &&
651+
debugDefaultTargetPlatformOverride != TargetPlatform.macOS) SemanticsFlag.namesRoute,
652+
],
653+
actions: <SemanticsAction>[
654+
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS ||
655+
debugDefaultTargetPlatformOverride == TargetPlatform.windows)
656+
SemanticsAction.didGainAccessibilityFocus,
657+
SemanticsAction.tap,
658+
SemanticsAction.setSelection,
659+
SemanticsAction.setText,
660+
SemanticsAction.paste,
661+
],
662+
label: 'Search',
663+
textDirection: TextDirection.ltr,
664+
textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),
665+
),
666+
TestSemantics(
667+
id: 10,
668+
label: 'Bottom',
669+
textDirection: TextDirection.ltr,
670+
),
671+
],
672+
),
673+
TestSemantics(
674+
id: 7,
675+
children: <TestSemantics>[
676+
TestSemantics(
677+
id: 11,
678+
label: 'FlexibleSpace',
679+
textDirection: TextDirection.ltr,
680+
),
681+
],
682+
),
683+
],
684+
),
685+
TestSemantics(
686+
id: 5,
687+
flags: <SemanticsFlag>[
688+
SemanticsFlag.hasEnabledState,
689+
SemanticsFlag.isButton,
690+
SemanticsFlag.isEnabled,
691+
SemanticsFlag.isFocusable,
692+
],
693+
actions: <SemanticsAction>[SemanticsAction.tap],
694+
label: 'Suggestions',
695+
textDirection: TextDirection.ltr,
696+
),
697+
],
698+
),
699+
],
700+
),
701+
],
702+
),
703+
],
704+
);
705+
}
706+
707+
testWidgets('includes routeName on Android', (WidgetTester tester) async {
708+
final SemanticsTester semantics = SemanticsTester(tester);
709+
final _TestSearchDelegate delegate = _TestSearchDelegate(flexibleSpace: flexibleSpace);
710+
await tester.pumpWidget(TestHomePage(
711+
delegate: delegate,
712+
));
713+
714+
await tester.tap(find.byTooltip('Search'));
715+
await tester.pumpAndSettle();
716+
717+
expect(semantics, hasSemantics(
718+
buildExpected(routeName: 'Search'),
719+
ignoreId: true,
720+
ignoreRect: true,
721+
ignoreTransform: true,
722+
));
723+
724+
semantics.dispose();
725+
});
726+
727+
testWidgets('does not include routeName', (WidgetTester tester) async {
728+
final SemanticsTester semantics = SemanticsTester(tester);
729+
final _TestSearchDelegate delegate = _TestSearchDelegate(flexibleSpace: flexibleSpace);
730+
await tester.pumpWidget(TestHomePage(
731+
delegate: delegate,
732+
));
733+
734+
await tester.tap(find.byTooltip('Search'));
735+
await tester.pumpAndSettle();
736+
737+
expect(semantics, hasSemantics(
738+
buildExpected(routeName: ''),
739+
ignoreId: true,
740+
ignoreRect: true,
741+
ignoreTransform: true,
742+
));
743+
744+
semantics.dispose();
745+
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
746+
});
747+
748+
592749
group('contributes semantics', () {
593750
TestSemantics buildExpected({ required String routeName }) {
594751
return TestSemantics.root(
@@ -749,10 +906,10 @@ void main() {
749906
await tester.tap(find.byTooltip('Search'));
750907
await tester.pumpAndSettle();
751908

752-
final Material appBarBackground = tester.widget<Material>(find.descendant(
909+
final Material appBarBackground = tester.widgetList<Material>(find.descendant(
753910
of: find.byType(AppBar),
754911
matching: find.byType(Material),
755-
));
912+
)).first;
756913
expect(appBarBackground.color, Colors.white);
757914

758915
final TextField textField = tester.widget<TextField>(find.byType(TextField));
@@ -777,10 +934,10 @@ void main() {
777934
await tester.tap(find.byTooltip('Search'));
778935
await tester.pumpAndSettle();
779936

780-
final Material appBarBackground = tester.widget<Material>(find.descendant(
937+
final Material appBarBackground = tester.widgetList<Material>(find.descendant(
781938
of: find.byType(AppBar),
782939
matching: find.byType(Material),
783-
));
940+
)).first;
784941
expect(appBarBackground.color, themeData.primaryColor);
785942

786943
final TextField textField = tester.widget<TextField>(find.byType(TextField));
@@ -789,9 +946,9 @@ void main() {
789946
});
790947

791948
// Regression test for: https://github.com/flutter/flutter/issues/78144
792-
testWidgets('`Leading` and `Actions` nullable test', (WidgetTester tester) async {
949+
testWidgets('`Leading`, `Actions` and `FlexibleSpace` nullable test', (WidgetTester tester) async {
793950
// The search delegate page is displayed with no issues
794-
// even with a null return values for [buildLeading] and [buildActions].
951+
// even with a null return values for [buildLeading], [buildActions] and [flexibleSpace].
795952
final _TestEmptySearchDelegate delegate = _TestEmptySearchDelegate();
796953
final List<String> selectedResults = <String>[];
797954

@@ -980,6 +1137,7 @@ class _TestSearchDelegate extends SearchDelegate<String> {
9801137
this.suggestions = 'Suggestions',
9811138
this.result = 'Result',
9821139
this.actions = const <Widget>[],
1140+
this.flexibleSpace ,
9831141
this.defaultAppBarTheme = false,
9841142
super.searchFieldDecorationTheme,
9851143
super.searchFieldStyle,
@@ -993,6 +1151,7 @@ class _TestSearchDelegate extends SearchDelegate<String> {
9931151
final String suggestions;
9941152
final String result;
9951153
final List<Widget> actions;
1154+
final Widget? flexibleSpace;
9961155
static const Color hintTextColor = Colors.green;
9971156

9981157
@override
@@ -1048,6 +1207,11 @@ class _TestSearchDelegate extends SearchDelegate<String> {
10481207
return actions;
10491208
}
10501209

1210+
@override
1211+
Widget? buildFlexibleSpace(BuildContext context) {
1212+
return flexibleSpace;
1213+
}
1214+
10511215
@override
10521216
PreferredSizeWidget buildBottom(BuildContext context) {
10531217
return const PreferredSize(

0 commit comments

Comments
 (0)