From 6673fe5cb14aebc14b975d3922ee58ac05c449b7 Mon Sep 17 00:00:00 2001 From: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:08:52 -0800 Subject: [PATCH] Add `onSubmitted` and `onChanged` for `SearchAnchor` and `SearchAnchor.bar` (#136840) Fixes #130687 and #132915 This PR is to add two properties: `viewOnChanged` and `viewOnSubmitted` to `SearchAnchor` and `SearchAnchor.bar` so we can control the search bar on the search view. --- .../lib/src/material/search_anchor.dart | 45 ++++++- .../test/material/search_anchor_test.dart | 112 ++++++++++++++++++ 2 files changed, 155 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/material/search_anchor.dart b/packages/flutter/lib/src/material/search_anchor.dart index 9c5a25fed4..e4b2696ae9 100644 --- a/packages/flutter/lib/src/material/search_anchor.dart +++ b/packages/flutter/lib/src/material/search_anchor.dart @@ -127,6 +127,8 @@ class SearchAnchor extends StatefulWidget { this.dividerColor, this.viewConstraints, this.textCapitalization, + this.viewOnChanged, + this.viewOnSubmitted, required this.builder, required this.suggestionsBuilder, }); @@ -147,6 +149,8 @@ class SearchAnchor extends StatefulWidget { Iterable? barTrailing, String? barHintText, GestureTapCallback? onTap, + ValueChanged? onSubmitted, + ValueChanged? onChanged, MaterialStateProperty? barElevation, MaterialStateProperty? barBackgroundColor, MaterialStateProperty? barOverlayColor, @@ -289,6 +293,24 @@ class SearchAnchor extends StatefulWidget { /// {@macro flutter.widgets.editableText.textCapitalization} final TextCapitalization? textCapitalization; + /// Called each time the user modifies the search view's text field. + /// + /// See also: + /// + /// * [viewOnSubmitted], which is called when the user indicates that they + /// are done editing the search view's text field. + final ValueChanged? viewOnChanged; + + /// Called when the user indicates that they are done editing the text in the + /// text field of a search view. Typically this is called when the user presses + /// the enter key. + /// + /// See also: + /// + /// * [viewOnChanged], which is called when the user modifies the text field + /// of the search view. + final ValueChanged? viewOnSubmitted; + /// Called to create a widget which can open a search view route when it is tapped. /// /// The widget returned by this builder is faded out when it is tapped. @@ -351,6 +373,8 @@ class _SearchAnchorState extends State { void _openView() { final NavigatorState navigator = Navigator.of(context); navigator.push(_SearchViewRoute( + viewOnChanged: widget.viewOnChanged, + viewOnSubmitted: widget.viewOnSubmitted, viewLeading: widget.viewLeading, viewTrailing: widget.viewTrailing, viewHintText: widget.viewHintText, @@ -422,6 +446,8 @@ class _SearchAnchorState extends State { class _SearchViewRoute extends PopupRoute<_SearchViewRoute> { _SearchViewRoute({ + this.viewOnChanged, + this.viewOnSubmitted, this.toggleVisibility, this.textDirection, this.viewBuilder, @@ -445,6 +471,8 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> { required this.capturedThemes, }); + final ValueChanged? viewOnChanged; + final ValueChanged? viewOnSubmitted; final ValueGetter? toggleVisibility; final TextDirection? textDirection; final ViewBuilder? viewBuilder; @@ -587,6 +615,8 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> { ), child: capturedThemes.wrap( _ViewContent( + viewOnChanged: viewOnChanged, + viewOnSubmitted: viewOnSubmitted, viewLeading: viewLeading, viewTrailing: viewTrailing, viewHintText: viewHintText, @@ -621,6 +651,8 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> { class _ViewContent extends StatefulWidget { const _ViewContent({ + this.viewOnChanged, + this.viewOnSubmitted, this.viewBuilder, this.viewLeading, this.viewTrailing, @@ -643,6 +675,8 @@ class _ViewContent extends StatefulWidget { required this.suggestionsBuilder, }); + final ValueChanged? viewOnChanged; + final ValueChanged? viewOnSubmitted; final ViewBuilder? viewBuilder; final Widget? viewLeading; final Iterable? viewTrailing; @@ -842,9 +876,11 @@ class _ViewContentState extends State<_ViewContent> { textStyle: MaterialStatePropertyAll(effectiveTextStyle), hintStyle: MaterialStatePropertyAll(effectiveHintStyle), controller: _controller, - onChanged: (_) { + onChanged: (String value) { + widget.viewOnChanged?.call(value); updateSuggestions(); }, + onSubmitted: widget.viewOnSubmitted, textCapitalization: widget.textCapitalization, ), ), @@ -907,11 +943,15 @@ class _SearchAnchorWithSearchBar extends SearchAnchor { super.isFullScreen, super.searchController, super.textCapitalization, + ValueChanged? onChanged, + ValueChanged? onSubmitted, required super.suggestionsBuilder }) : super( viewHintText: viewHintText ?? barHintText, headerTextStyle: viewHeaderTextStyle, headerHintStyle: viewHeaderHintStyle, + viewOnSubmitted: onSubmitted, + viewOnChanged: onChanged, builder: (BuildContext context, SearchController controller) { return SearchBar( constraints: constraints, @@ -920,9 +960,10 @@ class _SearchAnchorWithSearchBar extends SearchAnchor { controller.openView(); onTap?.call(); }, - onChanged: (_) { + onChanged: (String value) { controller.openView(); }, + onSubmitted: onSubmitted, hintText: barHintText, hintStyle: barHintStyle, textStyle: barTextStyle, diff --git a/packages/flutter/test/material/search_anchor_test.dart b/packages/flutter/test/material/search_anchor_test.dart index eabb49cb1e..976fb728bc 100644 --- a/packages/flutter/test/material/search_anchor_test.dart +++ b/packages/flutter/test/material/search_anchor_test.dart @@ -839,6 +839,65 @@ void main() { expect(textField.textCapitalization, TextCapitalization.none); }); + testWidgetsWithLeakTracking('SearchAnchor respects viewOnChanged and viewOnSubmitted properties', (WidgetTester tester) async { + final SearchController controller = SearchController(); + addTearDown(controller.dispose); + int onChangedCalled = 0; + int onSubmittedCalled = 0; + await tester.pumpWidget(MaterialApp( + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Center( + child: Material( + child: SearchAnchor( + searchController: controller, + viewOnChanged: (String value) { + setState(() { + onChangedCalled = onChangedCalled + 1; + }); + }, + viewOnSubmitted: (String value) { + setState(() { + onSubmittedCalled = onSubmittedCalled + 1; + }); + controller.closeView(value); + }, + builder: (BuildContext context, SearchController controller) { + return SearchBar( + onTap: () { + if (!controller.isOpen) { + controller.openView(); + } + }, + ); + }, + suggestionsBuilder: (BuildContext context, SearchController controller) { + return []; + }, + ), + ), + ); + } + ), + )); + await tester.tap(find.byType(SearchBar)); // Open search view. + await tester.pumpAndSettle(); + expect(controller.isOpen, true); + + final Finder barOnView = find.descendant( + of: findViewContent(), + matching: find.byType(TextField) + ); + await tester.enterText(barOnView, 'a'); + expect(onChangedCalled, 1); + await tester.enterText(barOnView, 'abc'); + expect(onChangedCalled, 2); + + await tester.testTextInput.receiveAction(TextInputAction.done); + expect(onSubmittedCalled, 1); + expect(controller.isOpen, false); + }); + testWidgetsWithLeakTracking('SearchAnchor.bar respects textCapitalization property', (WidgetTester tester) async { Widget buildSearchAnchor(TextCapitalization textCapitalization) { return MaterialApp( @@ -868,6 +927,59 @@ void main() { expect(textField.textCapitalization, TextCapitalization.characters); }); + testWidgetsWithLeakTracking('SearchAnchor.bar respects onChanged and onSubmitted properties', (WidgetTester tester) async { + final SearchController controller = SearchController(); + addTearDown(controller.dispose); + int onChangedCalled = 0; + int onSubmittedCalled = 0; + await tester.pumpWidget(MaterialApp( + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Center( + child: Material( + child: SearchAnchor.bar( + searchController: controller, + onSubmitted: (String value) { + setState(() { + onSubmittedCalled = onSubmittedCalled + 1; + }); + controller.closeView(value); + }, + onChanged: (String value) { + setState(() { + onChangedCalled = onChangedCalled + 1; + }); + }, + suggestionsBuilder: (BuildContext context, SearchController controller) { + return []; + }, + ), + ), + ); + } + ), + )); + await tester.tap(find.byType(SearchBar)); // Open search view. + await tester.pumpAndSettle(); + expect(controller.isOpen, true); + + final Finder barOnView = find.descendant( + of: findViewContent(), + matching: find.byType(TextField) + ); + await tester.enterText(barOnView, 'a'); + expect(onChangedCalled, 1); + await tester.enterText(barOnView, 'abc'); + expect(onChangedCalled, 2); + + await tester.testTextInput.receiveAction(TextInputAction.done); + expect(onSubmittedCalled, 1); + expect(controller.isOpen, false); + + await tester.testTextInput.receiveAction(TextInputAction.done); + expect(onSubmittedCalled, 2); + }); + testWidgetsWithLeakTracking('hintStyle can override textStyle for hintText', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp(