Unverified Commit 8d837062 authored by Bruno Leroux's avatar Bruno Leroux Committed by GitHub

Add InputDecorator.error to allow error message customization (#129275)

## Description

This PR is a fork of https://github.com/flutter/flutter/pull/118610 with some changes (mainly applying @justinmc comments).

~~This can be used by KishanBusa8 to update https://github.com/flutter/flutter/pull/118610 or can become a non WIP PR if KishanBusa8 does not respond or can not work on the update.~~

## Related Issue

fixes https://github.com/flutter/flutter/issues/11068

## Tests

Adds 3 tests.
parent b6b417c2
...@@ -302,6 +302,7 @@ class _HelperError extends StatefulWidget { ...@@ -302,6 +302,7 @@ class _HelperError extends StatefulWidget {
this.helperText, this.helperText,
this.helperStyle, this.helperStyle,
this.helperMaxLines, this.helperMaxLines,
this.error,
this.errorText, this.errorText,
this.errorStyle, this.errorStyle,
this.errorMaxLines, this.errorMaxLines,
...@@ -311,6 +312,7 @@ class _HelperError extends StatefulWidget { ...@@ -311,6 +312,7 @@ class _HelperError extends StatefulWidget {
final String? helperText; final String? helperText;
final TextStyle? helperStyle; final TextStyle? helperStyle;
final int? helperMaxLines; final int? helperMaxLines;
final Widget? error;
final String? errorText; final String? errorText;
final TextStyle? errorStyle; final TextStyle? errorStyle;
final int? errorMaxLines; final int? errorMaxLines;
...@@ -328,6 +330,8 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -328,6 +330,8 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
Widget? _helper; Widget? _helper;
Widget? _error; Widget? _error;
bool get _hasError => widget.errorText != null || widget.error != null;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
...@@ -335,7 +339,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -335,7 +339,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
duration: _kTransitionDuration, duration: _kTransitionDuration,
vsync: this, vsync: this,
); );
if (widget.errorText != null) { if (_hasError) {
_error = _buildError(); _error = _buildError();
_controller.value = 1.0; _controller.value = 1.0;
} else if (widget.helperText != null) { } else if (widget.helperText != null) {
...@@ -360,16 +364,19 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -360,16 +364,19 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
void didUpdateWidget(_HelperError old) { void didUpdateWidget(_HelperError old) {
super.didUpdateWidget(old); super.didUpdateWidget(old);
final Widget? newError = widget.error;
final String? newErrorText = widget.errorText; final String? newErrorText = widget.errorText;
final String? newHelperText = widget.helperText; final String? newHelperText = widget.helperText;
final Widget? oldError = old.error;
final String? oldErrorText = old.errorText; final String? oldErrorText = old.errorText;
final String? oldHelperText = old.helperText; final String? oldHelperText = old.helperText;
final bool errorStateChanged = (newError != null) != (oldError != null);
final bool errorTextStateChanged = (newErrorText != null) != (oldErrorText != null); final bool errorTextStateChanged = (newErrorText != null) != (oldErrorText != null);
final bool helperTextStateChanged = newErrorText == null && (newHelperText != null) != (oldHelperText != null); final bool helperTextStateChanged = newErrorText == null && (newHelperText != null) != (oldHelperText != null);
if (errorTextStateChanged || helperTextStateChanged) { if (errorStateChanged || errorTextStateChanged || helperTextStateChanged) {
if (newErrorText != null) { if (newError != null || newErrorText != null) {
_error = _buildError(); _error = _buildError();
_controller.forward(); _controller.forward();
} else if (newHelperText != null) { } else if (newHelperText != null) {
...@@ -399,7 +406,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -399,7 +406,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
} }
Widget _buildError() { Widget _buildError() {
assert(widget.errorText != null); assert(widget.error != null || widget.errorText != null);
return Semantics( return Semantics(
container: true, container: true,
child: FadeTransition( child: FadeTransition(
...@@ -409,7 +416,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -409,7 +416,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
begin: const Offset(0.0, -0.25), begin: const Offset(0.0, -0.25),
end: Offset.zero, end: Offset.zero,
).evaluate(_controller.view), ).evaluate(_controller.view),
child: Text( child: widget.error ?? Text(
widget.errorText!, widget.errorText!,
style: widget.errorStyle, style: widget.errorStyle,
textAlign: widget.textAlign, textAlign: widget.textAlign,
...@@ -435,7 +442,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -435,7 +442,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
if (_controller.isCompleted) { if (_controller.isCompleted) {
_helper = null; _helper = null;
if (widget.errorText != null) { if (_hasError) {
return _error = _buildError(); return _error = _buildError();
} else { } else {
_error = null; _error = null;
...@@ -443,7 +450,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -443,7 +450,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
} }
} }
if (_helper == null && widget.errorText != null) { if (_helper == null && _hasError) {
return _buildError(); return _buildError();
} }
...@@ -451,7 +458,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -451,7 +458,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
return _buildHelper(); return _buildHelper();
} }
if (widget.errorText != null) { if (_hasError) {
return Stack( return Stack(
children: <Widget>[ children: <Widget>[
FadeTransition( FadeTransition(
...@@ -2365,6 +2372,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -2365,6 +2372,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
helperText: decoration.helperText, helperText: decoration.helperText,
helperStyle: _getHelperStyle(themeData, defaults), helperStyle: _getHelperStyle(themeData, defaults),
helperMaxLines: decoration.helperMaxLines, helperMaxLines: decoration.helperMaxLines,
error: decoration.error,
errorText: decoration.errorText, errorText: decoration.errorText,
errorStyle: _getErrorStyle(themeData, defaults), errorStyle: _getErrorStyle(themeData, defaults),
errorMaxLines: decoration.errorMaxLines, errorMaxLines: decoration.errorMaxLines,
...@@ -2562,6 +2570,7 @@ class InputDecoration { ...@@ -2562,6 +2570,7 @@ class InputDecoration {
this.hintStyle, this.hintStyle,
this.hintTextDirection, this.hintTextDirection,
this.hintMaxLines, this.hintMaxLines,
this.error,
this.errorText, this.errorText,
this.errorStyle, this.errorStyle,
this.errorMaxLines, this.errorMaxLines,
...@@ -2601,7 +2610,8 @@ class InputDecoration { ...@@ -2601,7 +2610,8 @@ class InputDecoration {
this.constraints, this.constraints,
}) : assert(!(label != null && labelText != null), 'Declaring both label and labelText is not supported.'), }) : assert(!(label != null && labelText != null), 'Declaring both label and labelText is not supported.'),
assert(!(prefix != null && prefixText != null), 'Declaring both prefix and prefixText is not supported.'), assert(!(prefix != null && prefixText != null), 'Declaring both prefix and prefixText is not supported.'),
assert(!(suffix != null && suffixText != null), 'Declaring both suffix and suffixText is not supported.'); assert(!(suffix != null && suffixText != null), 'Declaring both suffix and suffixText is not supported.'),
assert(!(error != null && errorText != null), 'Declaring both error and errorText is not supported.');
/// Defines an [InputDecorator] that is the same size as the input field. /// Defines an [InputDecorator] that is the same size as the input field.
/// ///
...@@ -2630,6 +2640,7 @@ class InputDecoration { ...@@ -2630,6 +2640,7 @@ class InputDecoration {
helperStyle = null, helperStyle = null,
helperMaxLines = null, helperMaxLines = null,
hintMaxLines = null, hintMaxLines = null,
error = null,
errorText = null, errorText = null,
errorStyle = null, errorStyle = null,
errorMaxLines = null, errorMaxLines = null,
...@@ -2842,6 +2853,13 @@ class InputDecoration { ...@@ -2842,6 +2853,13 @@ class InputDecoration {
/// used to handle the overflow when it is limited to single line. /// used to handle the overflow when it is limited to single line.
final int? hintMaxLines; final int? hintMaxLines;
/// Optional widget that appears below the [InputDecorator.child] and the border.
///
/// If non-null, the border's color animates to red and the [helperText] is not shown.
///
/// Only one of [error] and [errorText] can be specified.
final Widget? error;
/// Text that appears below the [InputDecorator.child] and the border. /// Text that appears below the [InputDecorator.child] and the border.
/// ///
/// If non-null, the border's color animates to red and the [helperText] is /// If non-null, the border's color animates to red and the [helperText] is
...@@ -2849,6 +2867,10 @@ class InputDecoration { ...@@ -2849,6 +2867,10 @@ class InputDecoration {
/// ///
/// In a [TextFormField], this is overridden by the value returned from /// In a [TextFormField], this is overridden by the value returned from
/// [TextFormField.validator], if that is not null. /// [TextFormField.validator], if that is not null.
///
/// If a more elaborate error is required, consider using [error] instead.
///
/// Only one of [error] and [errorText] can be specified.
final String? errorText; final String? errorText;
/// {@template flutter.material.inputDecoration.errorStyle} /// {@template flutter.material.inputDecoration.errorStyle}
...@@ -3485,6 +3507,7 @@ class InputDecoration { ...@@ -3485,6 +3507,7 @@ class InputDecoration {
TextStyle? hintStyle, TextStyle? hintStyle,
TextDirection? hintTextDirection, TextDirection? hintTextDirection,
int? hintMaxLines, int? hintMaxLines,
Widget? error,
String? errorText, String? errorText,
TextStyle? errorStyle, TextStyle? errorStyle,
int? errorMaxLines, int? errorMaxLines,
...@@ -3537,6 +3560,7 @@ class InputDecoration { ...@@ -3537,6 +3560,7 @@ class InputDecoration {
hintStyle: hintStyle ?? this.hintStyle, hintStyle: hintStyle ?? this.hintStyle,
hintTextDirection: hintTextDirection ?? this.hintTextDirection, hintTextDirection: hintTextDirection ?? this.hintTextDirection,
hintMaxLines: hintMaxLines ?? this.hintMaxLines, hintMaxLines: hintMaxLines ?? this.hintMaxLines,
error: error ?? this.error,
errorText: errorText ?? this.errorText, errorText: errorText ?? this.errorText,
errorStyle: errorStyle ?? this.errorStyle, errorStyle: errorStyle ?? this.errorStyle,
errorMaxLines: errorMaxLines ?? this.errorMaxLines, errorMaxLines: errorMaxLines ?? this.errorMaxLines,
...@@ -3639,6 +3663,7 @@ class InputDecoration { ...@@ -3639,6 +3663,7 @@ class InputDecoration {
&& other.hintStyle == hintStyle && other.hintStyle == hintStyle
&& other.hintTextDirection == hintTextDirection && other.hintTextDirection == hintTextDirection
&& other.hintMaxLines == hintMaxLines && other.hintMaxLines == hintMaxLines
&& other.error == error
&& other.errorText == errorText && other.errorText == errorText
&& other.errorStyle == errorStyle && other.errorStyle == errorStyle
&& other.errorMaxLines == errorMaxLines && other.errorMaxLines == errorMaxLines
...@@ -3694,6 +3719,7 @@ class InputDecoration { ...@@ -3694,6 +3719,7 @@ class InputDecoration {
hintStyle, hintStyle,
hintTextDirection, hintTextDirection,
hintMaxLines, hintMaxLines,
error,
errorText, errorText,
errorStyle, errorStyle,
errorMaxLines, errorMaxLines,
...@@ -3747,6 +3773,7 @@ class InputDecoration { ...@@ -3747,6 +3773,7 @@ class InputDecoration {
if (helperMaxLines != null) 'helperMaxLines: "$helperMaxLines"', if (helperMaxLines != null) 'helperMaxLines: "$helperMaxLines"',
if (hintText != null) 'hintText: "$hintText"', if (hintText != null) 'hintText: "$hintText"',
if (hintMaxLines != null) 'hintMaxLines: "$hintMaxLines"', if (hintMaxLines != null) 'hintMaxLines: "$hintMaxLines"',
if (error != null) 'error: "$error"',
if (errorText != null) 'errorText: "$errorText"', if (errorText != null) 'errorText: "$errorText"',
if (errorStyle != null) 'errorStyle: "$errorStyle"', if (errorStyle != null) 'errorStyle: "$errorStyle"',
if (errorMaxLines != null) 'errorMaxLines: "$errorMaxLines"', if (errorMaxLines != null) 'errorMaxLines: "$errorMaxLines"',
......
...@@ -1643,6 +1643,47 @@ void main() { ...@@ -1643,6 +1643,47 @@ void main() {
expect(tester.getBottomLeft(find.text(kHelper1)), const Offset(12.0, 76.0)); expect(tester.getBottomLeft(find.text(kHelper1)), const Offset(12.0, 76.0));
}); });
testWidgets('InputDecorator shows error text', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(
useMaterial3: useMaterial3,
decoration: const InputDecoration(
errorText: 'errorText',
),
),
);
expect(find.text('errorText'), findsOneWidget);
});
testWidgets('InputDecorator shows error widget', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(
useMaterial3: useMaterial3,
decoration: const InputDecoration(
error: Text('error', style: TextStyle(fontSize: 20.0)),
),
),
);
expect(find.text('error'), findsOneWidget);
});
testWidgets('InputDecorator throws when error text and error widget are provided', (WidgetTester tester) async {
expect(
() {
buildInputDecorator(
useMaterial3: useMaterial3,
decoration: InputDecoration(
errorText: 'errorText',
error: const Text('error', style: TextStyle(fontSize: 20.0)),
),
);
},
throwsAssertionError,
);
});
testWidgets('InputDecorator prefix/suffix texts', (WidgetTester tester) async { testWidgets('InputDecorator prefix/suffix texts', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecorator( buildInputDecorator(
...@@ -5217,7 +5258,6 @@ void main() { ...@@ -5217,7 +5258,6 @@ void main() {
useMaterial3: useMaterial3, useMaterial3: useMaterial3,
// isFocused: false (default) // isFocused: false (default)
decoration: const InputDecoration( decoration: const InputDecoration(
// errorText: false (default)
enabled: false, enabled: false,
errorBorder: errorBorder, errorBorder: errorBorder,
focusedBorder: focusedBorder, focusedBorder: focusedBorder,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment