diff --git a/dev/tools/gen_defaults/lib/search_bar_template.dart b/dev/tools/gen_defaults/lib/search_bar_template.dart index 5a6d7a660c..670121b9cb 100644 --- a/dev/tools/gen_defaults/lib/search_bar_template.dart +++ b/dev/tools/gen_defaults/lib/search_bar_template.dart @@ -71,6 +71,9 @@ class _SearchBarDefaultsM3 extends SearchBarThemeData { @override BoxConstraints get constraints => const BoxConstraints(minWidth: 360.0, maxWidth: 800.0, minHeight: ${getToken('md.comp.search-bar.container.height')}); + + @override + TextCapitalization get textCapitalization => TextCapitalization.none; } '''; } diff --git a/packages/flutter/lib/src/material/search_anchor.dart b/packages/flutter/lib/src/material/search_anchor.dart index fbb4682c8e..6cb8c22c1c 100644 --- a/packages/flutter/lib/src/material/search_anchor.dart +++ b/packages/flutter/lib/src/material/search_anchor.dart @@ -126,6 +126,7 @@ class SearchAnchor extends StatefulWidget { this.headerHintStyle, this.dividerColor, this.viewConstraints, + this.textCapitalization, required this.builder, required this.suggestionsBuilder, }); @@ -170,6 +171,7 @@ class SearchAnchor extends StatefulWidget { BoxConstraints? viewConstraints, bool? isFullScreen, SearchController searchController, + TextCapitalization textCapitalization, required SuggestionsBuilder suggestionsBuilder }) = _SearchAnchorWithSearchBar; @@ -286,6 +288,9 @@ class SearchAnchor extends StatefulWidget { /// ``` final BoxConstraints? viewConstraints; + /// {@macro flutter.widgets.editableText.textCapitalization} + final TextCapitalization? textCapitalization; + /// 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. @@ -361,6 +366,7 @@ class _SearchAnchorState extends State { anchorKey: _anchorKey, searchController: _searchController, suggestionsBuilder: widget.suggestionsBuilder, + textCapitalization: widget.textCapitalization, )); } @@ -426,6 +432,7 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> { this.viewHeaderHintStyle, this.dividerColor, this.viewConstraints, + this.textCapitalization, required this.showFullScreenView, required this.anchorKey, required this.searchController, @@ -447,6 +454,7 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> { final TextStyle? viewHeaderHintStyle; final Color? dividerColor; final BoxConstraints? viewConstraints; + final TextCapitalization? textCapitalization; final bool showFullScreenView; final GlobalKey anchorKey; final SearchController searchController; @@ -595,6 +603,7 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> { viewBuilder: viewBuilder, searchController: searchController, suggestionsBuilder: suggestionsBuilder, + textCapitalization: textCapitalization, ), ); } @@ -620,6 +629,7 @@ class _ViewContent extends StatefulWidget { this.viewHeaderTextStyle, this.viewHeaderHintStyle, this.dividerColor, + this.textCapitalization, required this.showFullScreenView, required this.topPadding, required this.animation, @@ -644,6 +654,7 @@ class _ViewContent extends StatefulWidget { final TextStyle? viewHeaderTextStyle; final TextStyle? viewHeaderHintStyle; final Color? dividerColor; + final TextCapitalization? textCapitalization; final bool showFullScreenView; final double topPadding; final Animation animation; @@ -824,6 +835,7 @@ class _ViewContentState extends State<_ViewContent> { onChanged: (_) { updateSuggestions(); }, + textCapitalization: widget.textCapitalization, ), ), ), @@ -884,6 +896,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor { super.viewConstraints, super.isFullScreen, super.searchController, + super.textCapitalization, required super.suggestionsBuilder }) : super( viewHintText: viewHintText ?? barHintText, @@ -911,6 +924,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor { padding: barPadding ?? const MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 16.0)), leading: barLeading ?? const Icon(Icons.search), trailing: barTrailing, + textCapitalization: textCapitalization, ); } ); @@ -1022,6 +1036,7 @@ class SearchBar extends StatefulWidget { this.padding, this.textStyle, this.hintStyle, + this.textCapitalization, }); /// Controls the text being edited in the search bar's text field. @@ -1140,6 +1155,9 @@ class SearchBar extends StatefulWidget { /// The default text color is [ColorScheme.onSurfaceVariant]. final MaterialStateProperty? hintStyle; + /// {@macro flutter.widgets.editableText.textCapitalization} + final TextCapitalization? textCapitalization; + @override State createState() => _SearchBarState(); } @@ -1190,6 +1208,7 @@ class _SearchBarState extends State { final BorderSide? effectiveSide = resolve(widget.side, searchBarTheme.side, defaults.side); final EdgeInsetsGeometry? effectivePadding = resolve(widget.padding, searchBarTheme.padding, defaults.padding); final MaterialStateProperty? effectiveOverlayColor = widget.overlayColor ?? searchBarTheme.overlayColor ?? defaults.overlayColor; + final TextCapitalization effectiveTextCapitalization = widget.textCapitalization ?? searchBarTheme.textCapitalization ?? defaults.textCapitalization!; final Set states = _internalStatesController.value; final TextStyle? effectiveHintStyle = widget.hintStyle?.resolve(states) @@ -1273,6 +1292,7 @@ class _SearchBarState extends State { // smaller than 48.0 isDense: true, )), + textCapitalization: effectiveTextCapitalization, ), ), ) @@ -1353,6 +1373,9 @@ class _SearchBarDefaultsM3 extends SearchBarThemeData { @override BoxConstraints get constraints => const BoxConstraints(minWidth: 360.0, maxWidth: 800.0, minHeight: 56.0); + + @override + TextCapitalization get textCapitalization => TextCapitalization.none; } // END GENERATED TOKEN PROPERTIES - SearchBar diff --git a/packages/flutter/lib/src/material/search_bar_theme.dart b/packages/flutter/lib/src/material/search_bar_theme.dart index c738d0e9e7..94e225dd0c 100644 --- a/packages/flutter/lib/src/material/search_bar_theme.dart +++ b/packages/flutter/lib/src/material/search_bar_theme.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import '../../services.dart'; import 'material_state.dart'; import 'theme.dart'; @@ -47,6 +48,7 @@ class SearchBarThemeData with Diagnosticable { this.textStyle, this.hintStyle, this.constraints, + this.textCapitalization, }); /// Overrides the default value of the [SearchBar.elevation]. @@ -82,6 +84,9 @@ class SearchBarThemeData with Diagnosticable { /// Overrides the value of size constraints for [SearchBar]. final BoxConstraints? constraints; + /// Overrides the value of [SearchBar.textCapitalization]. + final TextCapitalization? textCapitalization; + /// Creates a copy of this object but with the given fields replaced with the /// new values. SearchBarThemeData copyWith({ @@ -96,6 +101,7 @@ class SearchBarThemeData with Diagnosticable { MaterialStateProperty? textStyle, MaterialStateProperty? hintStyle, BoxConstraints? constraints, + TextCapitalization? textCapitalization, }) { return SearchBarThemeData( elevation: elevation ?? this.elevation, @@ -109,6 +115,7 @@ class SearchBarThemeData with Diagnosticable { textStyle: textStyle ?? this.textStyle, hintStyle: hintStyle ?? this.hintStyle, constraints: constraints ?? this.constraints, + textCapitalization: textCapitalization ?? this.textCapitalization, ); } @@ -131,6 +138,7 @@ class SearchBarThemeData with Diagnosticable { textStyle: MaterialStateProperty.lerp(a?.textStyle, b?.textStyle, t, TextStyle.lerp), hintStyle: MaterialStateProperty.lerp(a?.hintStyle, b?.hintStyle, t, TextStyle.lerp), constraints: BoxConstraints.lerp(a?.constraints, b?.constraints, t), + textCapitalization: t < 0.5 ? a?.textCapitalization : b?.textCapitalization, ); } @@ -147,6 +155,7 @@ class SearchBarThemeData with Diagnosticable { textStyle, hintStyle, constraints, + textCapitalization, ); @override @@ -168,7 +177,8 @@ class SearchBarThemeData with Diagnosticable { && other.padding == padding && other.textStyle == textStyle && other.hintStyle == hintStyle - && other.constraints == constraints; + && other.constraints == constraints + && other.textCapitalization == textCapitalization; } @override @@ -185,6 +195,7 @@ class SearchBarThemeData with Diagnosticable { properties.add(DiagnosticsProperty>('textStyle', textStyle, defaultValue: null)); properties.add(DiagnosticsProperty>('hintStyle', hintStyle, defaultValue: null)); properties.add(DiagnosticsProperty('constraints', constraints, defaultValue: null)); + properties.add(DiagnosticsProperty('textCapitalization', textCapitalization, defaultValue: null)); } // Special case because BorderSide.lerp() doesn't support null arguments diff --git a/packages/flutter/test/material/search_anchor_test.dart b/packages/flutter/test/material/search_anchor_test.dart index 60251704e9..c044bdfc0a 100644 --- a/packages/flutter/test/material/search_anchor_test.dart +++ b/packages/flutter/test/material/search_anchor_test.dart @@ -691,6 +691,108 @@ void main() { expect(inputText.style.color, focusedColor); }); + testWidgets('SearchBar respects textCapitalization property', (WidgetTester tester) async { + Widget buildSearchBar(TextCapitalization textCapitalization) { + return MaterialApp( + home: Center( + child: Material( + child: SearchBar( + textCapitalization: textCapitalization, + ), + ), + ), + ); + } + await tester.pumpWidget(buildSearchBar(TextCapitalization.characters)); + await tester.pump(); + TextField textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.characters); + + await tester.pumpWidget(buildSearchBar(TextCapitalization.sentences)); + await tester.pump(); + textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.sentences); + + await tester.pumpWidget(buildSearchBar(TextCapitalization.words)); + await tester.pump(); + textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.words); + + await tester.pumpWidget(buildSearchBar(TextCapitalization.none)); + await tester.pump(); + textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.none); + }); + + testWidgets('SearchAnchor respects textCapitalization property', (WidgetTester tester) async { + Widget buildSearchAnchor(TextCapitalization textCapitalization) { + return MaterialApp( + home: Center( + child: Material( + child: SearchAnchor( + textCapitalization: textCapitalization, + builder: (BuildContext context, SearchController controller) { + return IconButton( + icon: const Icon(Icons.ac_unit), + onPressed: () { + controller.openView(); + }, + ); + }, + suggestionsBuilder: (BuildContext context, SearchController controller) { + return []; + }, + ), + ), + ), + ); + } + await tester.pumpWidget(buildSearchAnchor(TextCapitalization.characters)); + await tester.pump(); + await tester.tap(find.widgetWithIcon(IconButton, Icons.ac_unit)); + await tester.pumpAndSettle(); + TextField textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.characters); + await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back)); + await tester.pump(); + + await tester.pumpWidget(buildSearchAnchor(TextCapitalization.none)); + await tester.pump(); + await tester.tap(find.widgetWithIcon(IconButton, Icons.ac_unit)); + await tester.pumpAndSettle(); + textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.none); + }); + + testWidgets('SearchAnchor.bar respects textCapitalization property', (WidgetTester tester) async { + Widget buildSearchAnchor(TextCapitalization textCapitalization) { + return MaterialApp( + home: Center( + child: Material( + child: SearchAnchor.bar( + textCapitalization: textCapitalization, + suggestionsBuilder: (BuildContext context, SearchController controller) { + return []; + }, + ), + ), + ), + ); + } + await tester.pumpWidget(buildSearchAnchor(TextCapitalization.characters)); + await tester.pump(); + await tester.tap(find.byType(SearchBar)); // Open search view. + await tester.pumpAndSettle(); + final Finder textFieldFinder = find.descendant(of: findViewContent(), matching: find.byType(TextField)); + final TextField textFieldInView = tester.widget(textFieldFinder); + expect(textFieldInView.textCapitalization, TextCapitalization.characters); + // Close search view. + await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back)); + await tester.pumpAndSettle(); + final TextField textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.characters); + }); + testWidgets('hintStyle can override textStyle for hintText', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/material/search_bar_theme_test.dart b/packages/flutter/test/material/search_bar_theme_test.dart index 626fe20912..781984124b 100644 --- a/packages/flutter/test/material/search_bar_theme_test.dart +++ b/packages/flutter/test/material/search_bar_theme_test.dart @@ -35,6 +35,7 @@ void main() { expect(themeData.textStyle, null); expect(themeData.hintStyle, null); expect(themeData.constraints, null); + expect(themeData.textCapitalization, null); const SearchBarTheme theme = SearchBarTheme(data: SearchBarThemeData(), child: SizedBox()); expect(theme.data.elevation, null); @@ -48,6 +49,7 @@ void main() { expect(theme.data.textStyle, null); expect(theme.data.hintStyle, null); expect(theme.data.constraints, null); + expect(theme.data.textCapitalization, null); }); testWidgetsWithLeakTracking('Default SearchBarThemeData debugFillProperties', (WidgetTester tester) async { @@ -77,6 +79,7 @@ void main() { textStyle: MaterialStatePropertyAll(TextStyle(fontSize: 24.0)), hintStyle: MaterialStatePropertyAll(TextStyle(fontSize: 16.0)), constraints: BoxConstraints(minWidth: 350, maxWidth: 850), + textCapitalization: TextCapitalization.characters, ).debugFillProperties(builder); final List description = builder.properties @@ -95,6 +98,7 @@ void main() { expect(description[8], 'textStyle: MaterialStatePropertyAll(TextStyle(inherit: true, size: 24.0))'); expect(description[9], 'hintStyle: MaterialStatePropertyAll(TextStyle(inherit: true, size: 16.0))'); expect(description[10], 'constraints: BoxConstraints(350.0<=w<=850.0, 0.0<=h<=Infinity)'); + expect(description[11], 'textCapitalization: TextCapitalization.characters'); }); group('[Theme, SearchBarTheme, SearchBar properties overrides]', () { @@ -120,6 +124,7 @@ void main() { const MaterialStateProperty textStyle = MaterialStatePropertyAll(textStyleValue); const MaterialStateProperty hintStyle = MaterialStatePropertyAll(hintStyleValue); const BoxConstraints constraints = BoxConstraints(minWidth: 250.0, maxWidth: 300.0, minHeight: 80.0); + const TextCapitalization textCapitalization = TextCapitalization.words; const SearchBarThemeData searchBarTheme = SearchBarThemeData( elevation: elevation, @@ -133,6 +138,7 @@ void main() { textStyle: textStyle, hintStyle: hintStyle, constraints: constraints, + textCapitalization: textCapitalization, ); Widget buildFrame({ @@ -164,6 +170,7 @@ void main() { textStyle: textStyle, hintStyle: hintStyle, constraints: constraints, + textCapitalization: textCapitalization, ); }, ); @@ -223,6 +230,7 @@ void main() { final EditableText inputText = tester.widget(find.text('input')); expect(inputText.style.color, textStyleValue.color); expect(inputText.style.fontSize, textStyleValue.fontSize); + expect(inputText.textCapitalization, textCapitalization); final Rect barRect = tester.getRect(find.byType(SearchBar)); final Rect leadingRect = tester.getRect(find.byIcon(Icons.search));