diff --git a/packages/flutter/lib/src/material/search.dart b/packages/flutter/lib/src/material/search.dart index ab7eee7f49..e465c5492e 100644 --- a/packages/flutter/lib/src/material/search.dart +++ b/packages/flutter/lib/src/material/search.dart @@ -211,6 +211,15 @@ abstract class SearchDelegate { /// PreferredSizeWidget? buildBottom(BuildContext context) => null; + /// Widget to display a flexible space in the [AppBar]. + /// + /// Returns null by default, i.e. a flexible space widget is not included. + /// + /// See also: + /// + /// * [AppBar.flexibleSpace], the intended use for the return value of this method. + Widget? buildFlexibleSpace(BuildContext context) => null; + /// The theme used to configure the search page. /// /// The returned [ThemeData] will be used to wrap the entire search page, @@ -581,11 +590,10 @@ class _SearchPageState extends State<_SearchPage> { style: widget.delegate.searchFieldStyle ?? theme.textTheme.titleLarge, textInputAction: widget.delegate.textInputAction, keyboardType: widget.delegate.keyboardType, - onSubmitted: (String _) { - widget.delegate.showResults(context); - }, + onSubmitted: (String _) => widget.delegate.showResults(context), decoration: InputDecoration(hintText: searchFieldLabel), ), + flexibleSpace: widget.delegate.buildFlexibleSpace(context), actions: widget.delegate.buildActions(context), bottom: widget.delegate.buildBottom(context), ), diff --git a/packages/flutter/test/material/search_test.dart b/packages/flutter/test/material/search_test.dart index 3bf869d453..be9a1d6a24 100644 --- a/packages/flutter/test/material/search_test.dart +++ b/packages/flutter/test/material/search_test.dart @@ -589,6 +589,163 @@ void main() { expect(tester.testTextInput.setClientArgs!['inputAction'], TextInputAction.done.toString()); }); + testWidgets('Custom flexibleSpace value', (WidgetTester tester) async { + const Widget flexibleSpace = Text('custom flexibleSpace'); + final _TestSearchDelegate delegate = _TestSearchDelegate(flexibleSpace: flexibleSpace); + + await tester.pumpWidget(TestHomePage(delegate: delegate)); + await tester.tap(find.byTooltip('Search')); + await tester.pumpAndSettle(); + + expect(find.byWidget(flexibleSpace), findsOneWidget); + }); + + + group('contributes semantics with custom flexibleSpace', () { + const Widget flexibleSpace = Text('FlexibleSpace'); + + TestSemantics buildExpected({ required String routeName }) { + return TestSemantics.root( + children: [ + TestSemantics( + id: 1, + textDirection: TextDirection.ltr, + children: [ + TestSemantics( + id: 2, + children: [ + TestSemantics( + id: 3, + flags: [ + SemanticsFlag.scopesRoute, + SemanticsFlag.namesRoute, + ], + label: routeName, + textDirection: TextDirection.ltr, + children: [ + TestSemantics( + id: 4, + children: [ + TestSemantics( + id: 6, + children: [ + TestSemantics( + id: 8, + flags: [ + SemanticsFlag.hasEnabledState, + SemanticsFlag.isButton, + SemanticsFlag.isEnabled, + SemanticsFlag.isFocusable, + ], + actions: [SemanticsAction.tap], + tooltip: 'Back', + textDirection: TextDirection.ltr, + ), + TestSemantics( + id: 9, + flags: [ + SemanticsFlag.isTextField, + SemanticsFlag.isFocused, + SemanticsFlag.isHeader, + if (debugDefaultTargetPlatformOverride != TargetPlatform.iOS && + debugDefaultTargetPlatformOverride != TargetPlatform.macOS) SemanticsFlag.namesRoute, + ], + actions: [ + if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS || + debugDefaultTargetPlatformOverride == TargetPlatform.windows) + SemanticsAction.didGainAccessibilityFocus, + SemanticsAction.tap, + SemanticsAction.setSelection, + SemanticsAction.setText, + SemanticsAction.paste, + ], + label: 'Search', + textDirection: TextDirection.ltr, + textSelection: const TextSelection(baseOffset: 0, extentOffset: 0), + ), + TestSemantics( + id: 10, + label: 'Bottom', + textDirection: TextDirection.ltr, + ), + ], + ), + TestSemantics( + id: 7, + children: [ + TestSemantics( + id: 11, + label: 'FlexibleSpace', + textDirection: TextDirection.ltr, + ), + ], + ), + ], + ), + TestSemantics( + id: 5, + flags: [ + SemanticsFlag.hasEnabledState, + SemanticsFlag.isButton, + SemanticsFlag.isEnabled, + SemanticsFlag.isFocusable, + ], + actions: [SemanticsAction.tap], + label: 'Suggestions', + textDirection: TextDirection.ltr, + ), + ], + ), + ], + ), + ], + ), + ], + ); + } + + testWidgets('includes routeName on Android', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + final _TestSearchDelegate delegate = _TestSearchDelegate(flexibleSpace: flexibleSpace); + await tester.pumpWidget(TestHomePage( + delegate: delegate, + )); + + await tester.tap(find.byTooltip('Search')); + await tester.pumpAndSettle(); + + expect(semantics, hasSemantics( + buildExpected(routeName: 'Search'), + ignoreId: true, + ignoreRect: true, + ignoreTransform: true, + )); + + semantics.dispose(); + }); + + testWidgets('does not include routeName', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + final _TestSearchDelegate delegate = _TestSearchDelegate(flexibleSpace: flexibleSpace); + await tester.pumpWidget(TestHomePage( + delegate: delegate, + )); + + await tester.tap(find.byTooltip('Search')); + await tester.pumpAndSettle(); + + expect(semantics, hasSemantics( + buildExpected(routeName: ''), + ignoreId: true, + ignoreRect: true, + ignoreTransform: true, + )); + + semantics.dispose(); + }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); + }); + + group('contributes semantics', () { TestSemantics buildExpected({ required String routeName }) { return TestSemantics.root( @@ -749,10 +906,10 @@ void main() { await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); - final Material appBarBackground = tester.widget(find.descendant( + final Material appBarBackground = tester.widgetList(find.descendant( of: find.byType(AppBar), matching: find.byType(Material), - )); + )).first; expect(appBarBackground.color, Colors.white); final TextField textField = tester.widget(find.byType(TextField)); @@ -777,10 +934,10 @@ void main() { await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); - final Material appBarBackground = tester.widget(find.descendant( + final Material appBarBackground = tester.widgetList(find.descendant( of: find.byType(AppBar), matching: find.byType(Material), - )); + )).first; expect(appBarBackground.color, themeData.primaryColor); final TextField textField = tester.widget(find.byType(TextField)); @@ -789,9 +946,9 @@ void main() { }); // Regression test for: https://github.com/flutter/flutter/issues/78144 - testWidgets('`Leading` and `Actions` nullable test', (WidgetTester tester) async { + testWidgets('`Leading`, `Actions` and `FlexibleSpace` nullable test', (WidgetTester tester) async { // The search delegate page is displayed with no issues - // even with a null return values for [buildLeading] and [buildActions]. + // even with a null return values for [buildLeading], [buildActions] and [flexibleSpace]. final _TestEmptySearchDelegate delegate = _TestEmptySearchDelegate(); final List selectedResults = []; @@ -980,6 +1137,7 @@ class _TestSearchDelegate extends SearchDelegate { this.suggestions = 'Suggestions', this.result = 'Result', this.actions = const [], + this.flexibleSpace , this.defaultAppBarTheme = false, super.searchFieldDecorationTheme, super.searchFieldStyle, @@ -993,6 +1151,7 @@ class _TestSearchDelegate extends SearchDelegate { final String suggestions; final String result; final List actions; + final Widget? flexibleSpace; static const Color hintTextColor = Colors.green; @override @@ -1048,6 +1207,11 @@ class _TestSearchDelegate extends SearchDelegate { return actions; } + @override + Widget? buildFlexibleSpace(BuildContext context) { + return flexibleSpace; + } + @override PreferredSizeWidget buildBottom(BuildContext context) { return const PreferredSize(