From 97cdc0ec07d2b9bfbda4ce1fa461a691964737c9 Mon Sep 17 00:00:00 2001 From: Renzo Olivares Date: Fri, 8 Sep 2023 15:01:50 -0700 Subject: [PATCH] InputDecoration.error should activate error state (#134001) When passed an `error` widget, `InputDecoration` should activate its error state. Before this change the `errorBorder` would only activate if an `errorText` was provided. This change solves this issue by accounting for a provided `error` widget. --- .../lib/src/material/input_decorator.dart | 14 +-- .../flutter/lib/src/material/text_field.dart | 2 +- .../test/material/input_decorator_test.dart | 99 +++++++++++++++++++ .../test/material/text_field_test.dart | 36 +++++++ 4 files changed, 143 insertions(+), 8 deletions(-) diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 1a99e298cb..9e3a67ee4d 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -1971,6 +1971,7 @@ class _InputDecoratorState extends State with TickerProviderStat TextAlign? get textAlign => widget.textAlign; bool get isFocused => widget.isFocused; + bool get _hasError => decoration.errorText != null || decoration.error != null; bool get isHovering => widget.isHovering && decoration.enabled; bool get isEmpty => widget.isEmpty; bool get _floatingLabelEnabled { @@ -2011,7 +2012,7 @@ class _InputDecoratorState extends State with TickerProviderStat ? Colors.transparent : themeData.disabledColor; } - if (decoration.errorText != null) { + if (_hasError) { return themeData.colorScheme.error; } if (isFocused) { @@ -2107,7 +2108,7 @@ class _InputDecoratorState extends State with TickerProviderStat TextStyle _getFloatingLabelStyle(ThemeData themeData, InputDecorationTheme defaults) { TextStyle defaultTextStyle = MaterialStateProperty.resolveAs(defaults.floatingLabelStyle!, materialState); - if (decoration.errorText != null && decoration.errorStyle?.color != null) { + if (_hasError && decoration.errorStyle?.color != null) { defaultTextStyle = defaultTextStyle.copyWith(color: decoration.errorStyle?.color); } defaultTextStyle = defaultTextStyle.merge(decoration.floatingLabelStyle ?? decoration.labelStyle); @@ -2137,7 +2138,7 @@ class _InputDecoratorState extends State with TickerProviderStat if (!decoration.enabled) MaterialState.disabled, if (isFocused) MaterialState.focused, if (isHovering) MaterialState.hovered, - if (decoration.errorText != null) MaterialState.error, + if (_hasError) MaterialState.error, }; } @@ -2205,14 +2206,13 @@ class _InputDecoratorState extends State with TickerProviderStat ), ); - final bool isError = decoration.errorText != null; InputBorder? border; if (!decoration.enabled) { - border = isError ? decoration.errorBorder : decoration.disabledBorder; + border = _hasError ? decoration.errorBorder : decoration.disabledBorder; } else if (isFocused) { - border = isError ? decoration.focusedErrorBorder : decoration.focusedBorder; + border = _hasError ? decoration.focusedErrorBorder : decoration.focusedBorder; } else { - border = isError ? decoration.errorBorder : decoration.enabledBorder; + border = _hasError ? decoration.errorBorder : decoration.enabledBorder; } border ??= _getDefaultBorder(themeData, defaults); diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 3e0cfe9f90..9679f0c47d 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -945,7 +945,7 @@ class _TextFieldState extends State with RestorationMixin implements bool get _hasIntrinsicError => widget.maxLength != null && widget.maxLength! > 0 && _effectiveController.value.text.characters.length > widget.maxLength!; - bool get _hasError => widget.decoration?.errorText != null || _hasIntrinsicError; + bool get _hasError => widget.decoration?.errorText != null || widget.decoration?.error != null || _hasIntrinsicError; Color get _errorColor => widget.decoration?.errorStyle?.color ?? Theme.of(context).colorScheme.error; diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 27e57c2707..dbcca1b819 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -1668,6 +1668,105 @@ void runAllTests({ required bool useMaterial3 }) { expect(find.text('errorText'), findsOneWidget); }); + testWidgets('InputDecoration shows error border for errorText and error widget', (WidgetTester tester) async { + const InputBorder errorBorder = OutlineInputBorder( + borderSide: BorderSide(color: Colors.red, width: 1.5), + ); + const InputBorder focusedErrorBorder = OutlineInputBorder( + borderSide: BorderSide(color: Colors.teal, width: 5.0), + ); + + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + isFocused: true, + decoration: const InputDecoration( + errorText: 'error', + // enabled: true (default) + errorBorder: errorBorder, + focusedErrorBorder: focusedErrorBorder, + ), + ), + ); + await tester.pumpAndSettle(); // Border changes are animated. + expect(getBorder(tester), focusedErrorBorder); + + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + // isFocused: false (default) + decoration: const InputDecoration( + errorText: 'error', + // enabled: true (default) + errorBorder: errorBorder, + focusedErrorBorder: focusedErrorBorder, + ), + ), + ); + await tester.pumpAndSettle(); // Border changes are animated. + expect(getBorder(tester), errorBorder); + + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + // isFocused: false (default) + decoration: const InputDecoration( + errorText: 'error', + enabled: false, + errorBorder: errorBorder, + focusedErrorBorder: focusedErrorBorder, + ), + ), + ); + await tester.pumpAndSettle(); // Border changes are animated. + expect(getBorder(tester), errorBorder); + + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + isFocused: true, + decoration: const InputDecoration( + error: Text('error'), + // enabled: true (default) + errorBorder: errorBorder, + focusedErrorBorder: focusedErrorBorder, + ), + ), + ); + await tester.pumpAndSettle(); // Border changes are animated. + expect(getBorder(tester), focusedErrorBorder); + + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + // isFocused: false (default) + decoration: const InputDecoration( + error: Text('error'), + // enabled: true (default) + errorBorder: errorBorder, + focusedErrorBorder: focusedErrorBorder, + ), + ), + ); + await tester.pumpAndSettle(); // Border changes are animated. + expect(getBorder(tester), errorBorder); + + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + // isFocused: false (default) + decoration: const InputDecoration( + error: Text('error'), + enabled: false, + errorBorder: errorBorder, + focusedErrorBorder: focusedErrorBorder, + ), + ), + ); + await tester.pumpAndSettle(); // Border changes are animated. + expect(getBorder(tester), errorBorder); + }); + testWidgetsWithLeakTracking('InputDecorator shows error widget', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index fff87d55eb..6e4d478571 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -787,6 +787,42 @@ void main() { expect(state.widget.cursorColor, cursorColor); }); + testWidgets('Use error cursor color when an InputDecoration with an errorText or error widget is provided', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: TextField( + autofocus: true, + decoration: InputDecoration( + error: Text('error'), + errorStyle: TextStyle(color: Colors.teal), + ), + ), + ), + ), + ); + await tester.pump(); + EditableTextState state = tester.state(find.byType(EditableText)); + expect(state.widget.cursorColor, Colors.teal); + + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: TextField( + autofocus: true, + decoration: InputDecoration( + errorText: 'error', + errorStyle: TextStyle(color: Colors.teal), + ), + ), + ), + ), + ); + await tester.pump(); + state = tester.state(find.byType(EditableText)); + expect(state.widget.cursorColor, Colors.teal); + }); + testWidgetsWithLeakTracking('sets cursorOpacityAnimates on EditableText correctly', (WidgetTester tester) async { // True