Unverified Commit 01fc13d9 authored by Vatsal Bhesaniya's avatar Vatsal Bhesaniya Committed by GitHub

Add helper widget parameter to InputDecoration (#145157)

This pull request introduces a new field named `helper` to the InputDecoration class. This field allows for specifying a widget containing contextual information about the InputDecorator.child's value. Unlike `helperText`, which accepts a plain string, `helper` supports widgets, enabling functionalities like tappable links for further explanation. This change aligns with the established pattern of `error`, `label`, `prefix`, and `suffix`.

fixes [#145163](https://github.com/flutter/flutter/issues/145163)
parent 39bdff16
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// Flutter code sample for [InputDecoration.helper].
void main() => runApp(const HelperExampleApp());
class HelperExampleApp extends StatelessWidget {
const HelperExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: Scaffold(
appBar: AppBar(title: const Text('InputDecoration.helper Sample')),
body: const HelperExample(),
),
);
}
}
class HelperExample extends StatelessWidget {
const HelperExample({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: TextField(
decoration: InputDecoration(
helper: Text.rich(
TextSpan(
children: <InlineSpan>[
WidgetSpan(
child: Text(
'Helper Text ',
),
),
WidgetSpan(
child: Icon(
Icons.help_outline,
color: Colors.blue,
size: 20.0,
),
),
],
),
),
),
),
);
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/input_decorator/input_decoration.helper.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('InputDecorator helper', (WidgetTester tester) async {
await tester.pumpWidget(
const example.HelperExampleApp(),
);
expect(find.byType(TextField), findsOneWidget);
expect(find.text('Helper Text '), findsOneWidget);
expect(find.byIcon(Icons.help_outline), findsOneWidget);
});
}
...@@ -308,6 +308,7 @@ class _Shaker extends AnimatedWidget { ...@@ -308,6 +308,7 @@ class _Shaker extends AnimatedWidget {
class _HelperError extends StatefulWidget { class _HelperError extends StatefulWidget {
const _HelperError({ const _HelperError({
this.textAlign, this.textAlign,
this.helper,
this.helperText, this.helperText,
this.helperStyle, this.helperStyle,
this.helperMaxLines, this.helperMaxLines,
...@@ -318,6 +319,7 @@ class _HelperError extends StatefulWidget { ...@@ -318,6 +319,7 @@ class _HelperError extends StatefulWidget {
}); });
final TextAlign? textAlign; final TextAlign? textAlign;
final Widget? helper;
final String? helperText; final String? helperText;
final TextStyle? helperStyle; final TextStyle? helperStyle;
final int? helperMaxLines; final int? helperMaxLines;
...@@ -339,6 +341,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -339,6 +341,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
Widget? _helper; Widget? _helper;
Widget? _error; Widget? _error;
bool get _hasHelper => widget.helperText != null || widget.helper != null;
bool get _hasError => widget.errorText != null || widget.error != null; bool get _hasError => widget.errorText != null || widget.error != null;
@override @override
...@@ -351,7 +354,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -351,7 +354,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
if (_hasError) { if (_hasError) {
_error = _buildError(); _error = _buildError();
_controller.value = 1.0; _controller.value = 1.0;
} else if (widget.helperText != null) { } else if (_hasHelper) {
_helper = _buildHelper(); _helper = _buildHelper();
} }
_controller.addListener(_handleChange); _controller.addListener(_handleChange);
...@@ -375,20 +378,23 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -375,20 +378,23 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
final Widget? newError = widget.error; final Widget? newError = widget.error;
final String? newErrorText = widget.errorText; final String? newErrorText = widget.errorText;
final Widget? newHelper = widget.helper;
final String? newHelperText = widget.helperText; final String? newHelperText = widget.helperText;
final Widget? oldError = old.error; final Widget? oldError = old.error;
final String? oldErrorText = old.errorText; final String? oldErrorText = old.errorText;
final Widget? oldHelper = old.helper;
final String? oldHelperText = old.helperText; final String? oldHelperText = old.helperText;
final bool errorStateChanged = (newError != null) != (oldError != null); final bool errorStateChanged = (newError != null) != (oldError != null);
final bool errorTextStateChanged = (newErrorText != null) != (oldErrorText != null); final bool errorTextStateChanged = (newErrorText != null) != (oldErrorText != null);
final bool helperStateChanged = (newHelper != null) != (oldHelper != null);
final bool helperTextStateChanged = newErrorText == null && (newHelperText != null) != (oldHelperText != null); final bool helperTextStateChanged = newErrorText == null && (newHelperText != null) != (oldHelperText != null);
if (errorStateChanged || errorTextStateChanged || helperTextStateChanged) { if (errorStateChanged || errorTextStateChanged || helperStateChanged || helperTextStateChanged) {
if (newError != null || newErrorText != null) { if (newError != null || newErrorText != null) {
_error = _buildError(); _error = _buildError();
_controller.forward(); _controller.forward();
} else if (newHelperText != null) { } else if (newHelper != null || newHelperText != null) {
_helper = _buildHelper(); _helper = _buildHelper();
_controller.reverse(); _controller.reverse();
} else { } else {
...@@ -398,12 +404,12 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -398,12 +404,12 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
} }
Widget _buildHelper() { Widget _buildHelper() {
assert(widget.helperText != null); assert(widget.helper != null || widget.helperText != null);
return Semantics( return Semantics(
container: true, container: true,
child: FadeTransition( child: FadeTransition(
opacity: Tween<double>(begin: 1.0, end: 0.0).animate(_controller), opacity: Tween<double>(begin: 1.0, end: 0.0).animate(_controller),
child: Text( child: widget.helper ?? Text(
widget.helperText!, widget.helperText!,
style: widget.helperStyle, style: widget.helperStyle,
textAlign: widget.textAlign, textAlign: widget.textAlign,
...@@ -441,7 +447,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -441,7 +447,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_controller.isDismissed) { if (_controller.isDismissed) {
_error = null; _error = null;
if (widget.helperText != null) { if (_hasHelper) {
return _helper = _buildHelper(); return _helper = _buildHelper();
} else { } else {
_helper = null; _helper = null;
...@@ -463,7 +469,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -463,7 +469,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
return _buildError(); return _buildError();
} }
if (_error == null && widget.helperText != null) { if (_error == null && _hasHelper) {
return _buildHelper(); return _buildHelper();
} }
...@@ -479,7 +485,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta ...@@ -479,7 +485,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
); );
} }
if (widget.helperText != null) { if (_hasHelper) {
return Stack( return Stack(
children: <Widget>[ children: <Widget>[
_buildHelper(), _buildHelper(),
...@@ -2370,6 +2376,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -2370,6 +2376,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
final Widget helperError = _HelperError( final Widget helperError = _HelperError(
textAlign: textAlign, textAlign: textAlign,
helper: decoration.helper,
helperText: decoration.helperText, helperText: decoration.helperText,
helperStyle: _getHelperStyle(themeData, defaults), helperStyle: _getHelperStyle(themeData, defaults),
helperMaxLines: decoration.helperMaxLines, helperMaxLines: decoration.helperMaxLines,
...@@ -2575,6 +2582,7 @@ class InputDecoration { ...@@ -2575,6 +2582,7 @@ class InputDecoration {
this.labelText, this.labelText,
this.labelStyle, this.labelStyle,
this.floatingLabelStyle, this.floatingLabelStyle,
this.helper,
this.helperText, this.helperText,
this.helperStyle, this.helperStyle,
this.helperMaxLines, this.helperMaxLines,
...@@ -2622,6 +2630,7 @@ class InputDecoration { ...@@ -2622,6 +2630,7 @@ class InputDecoration {
this.alignLabelWithHint, this.alignLabelWithHint,
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(!(helper != null && helperText != null), 'Declaring both helper and helperText 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.'); assert(!(error != null && errorText != null), 'Declaring both error and errorText is not supported.');
...@@ -2649,6 +2658,7 @@ class InputDecoration { ...@@ -2649,6 +2658,7 @@ class InputDecoration {
labelText = null, labelText = null,
labelStyle = null, labelStyle = null,
floatingLabelStyle = null, floatingLabelStyle = null,
helper = null,
helperText = null, helperText = null,
helperStyle = null, helperStyle = null,
helperMaxLines = null, helperMaxLines = null,
...@@ -2802,12 +2812,32 @@ class InputDecoration { ...@@ -2802,12 +2812,32 @@ class InputDecoration {
/// {@endtemplate} /// {@endtemplate}
final TextStyle? floatingLabelStyle; final TextStyle? floatingLabelStyle;
/// Optional widget that appears below the [InputDecorator.child].
///
/// If non-null, the [helper] is displayed below the [InputDecorator.child], in
/// the same location as [error]. If a non-null [error] or [errorText] value is
/// specified then the [helper] is not shown.
///
/// {@tool dartpad}
/// This example shows a `TextField` with a [Text.rich] widget as the [helper].
/// The widget contains [Text] and [Icon] widgets with different styles.
///
/// ** See code in examples/api/lib/material/input_decorator/input_decoration.helper.0.dart **
/// {@end-tool}
///
/// Only one of [helper] and [helperText] can be specified.
final Widget? helper;
/// Text that provides context about the [InputDecorator.child]'s value, such /// Text that provides context about the [InputDecorator.child]'s value, such
/// as how the value will be used. /// as how the value will be used.
/// ///
/// If non-null, the text is displayed below the [InputDecorator.child], in /// If non-null, the text is displayed below the [InputDecorator.child], in
/// the same location as [errorText]. If a non-null [errorText] value is /// the same location as [errorText]. If a non-null [errorText] value is
/// specified then the helper text is not shown. /// specified then the helper text is not shown.
///
/// If a more elaborate helper text is required, consider using [helper] instead.
///
/// Only one of [helper] and [helperText] can be specified.
final String? helperText; final String? helperText;
/// The style to use for the [helperText]. /// The style to use for the [helperText].
...@@ -3536,6 +3566,7 @@ class InputDecoration { ...@@ -3536,6 +3566,7 @@ class InputDecoration {
String? labelText, String? labelText,
TextStyle? labelStyle, TextStyle? labelStyle,
TextStyle? floatingLabelStyle, TextStyle? floatingLabelStyle,
Widget? helper,
String? helperText, String? helperText,
TextStyle? helperStyle, TextStyle? helperStyle,
int? helperMaxLines, int? helperMaxLines,
...@@ -3590,6 +3621,7 @@ class InputDecoration { ...@@ -3590,6 +3621,7 @@ class InputDecoration {
labelText: labelText ?? this.labelText, labelText: labelText ?? this.labelText,
labelStyle: labelStyle ?? this.labelStyle, labelStyle: labelStyle ?? this.labelStyle,
floatingLabelStyle: floatingLabelStyle ?? this.floatingLabelStyle, floatingLabelStyle: floatingLabelStyle ?? this.floatingLabelStyle,
helper: helper ?? this.helper,
helperText: helperText ?? this.helperText, helperText: helperText ?? this.helperText,
helperStyle: helperStyle ?? this.helperStyle, helperStyle: helperStyle ?? this.helperStyle,
helperMaxLines : helperMaxLines ?? this.helperMaxLines, helperMaxLines : helperMaxLines ?? this.helperMaxLines,
...@@ -3695,6 +3727,7 @@ class InputDecoration { ...@@ -3695,6 +3727,7 @@ class InputDecoration {
&& other.labelText == labelText && other.labelText == labelText
&& other.labelStyle == labelStyle && other.labelStyle == labelStyle
&& other.floatingLabelStyle == floatingLabelStyle && other.floatingLabelStyle == floatingLabelStyle
&& other.helper == helper
&& other.helperText == helperText && other.helperText == helperText
&& other.helperStyle == helperStyle && other.helperStyle == helperStyle
&& other.helperMaxLines == helperMaxLines && other.helperMaxLines == helperMaxLines
...@@ -3752,6 +3785,7 @@ class InputDecoration { ...@@ -3752,6 +3785,7 @@ class InputDecoration {
labelText, labelText,
floatingLabelStyle, floatingLabelStyle,
labelStyle, labelStyle,
helper,
helperText, helperText,
helperStyle, helperStyle,
helperMaxLines, helperMaxLines,
...@@ -3810,6 +3844,7 @@ class InputDecoration { ...@@ -3810,6 +3844,7 @@ class InputDecoration {
if (label != null) 'label: $label', if (label != null) 'label: $label',
if (labelText != null) 'labelText: "$labelText"', if (labelText != null) 'labelText: "$labelText"',
if (floatingLabelStyle != null) 'floatingLabelStyle: "$floatingLabelStyle"', if (floatingLabelStyle != null) 'floatingLabelStyle: "$floatingLabelStyle"',
if (helper != null) 'helper: "$helper"',
if (helperText != null) 'helperText: "$helperText"', if (helperText != null) 'helperText: "$helperText"',
if (helperMaxLines != null) 'helperMaxLines: "$helperMaxLines"', if (helperMaxLines != null) 'helperMaxLines: "$helperMaxLines"',
if (hintText != null) 'hintText: "$hintText"', if (hintText != null) 'hintText: "$hintText"',
......
...@@ -2747,6 +2747,34 @@ void main() { ...@@ -2747,6 +2747,34 @@ void main() {
}); });
}); });
group('Helper widget', () {
testWidgets('InputDecorator shows helper widget', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(
decoration: const InputDecoration(
helper: Text('helper', style: TextStyle(fontSize: 20.0)),
),
),
);
expect(find.text('helper'), findsOneWidget);
});
testWidgets('InputDecorator throws when helper text and helper widget are provided', (WidgetTester tester) async {
expect(
() {
buildInputDecorator(
decoration: InputDecoration(
helperText: 'helperText',
helper: const Text('helper', style: TextStyle(fontSize: 20.0)),
),
);
},
throwsAssertionError,
);
});
});
group('Error widget', () { group('Error widget', () {
testWidgets('InputDecorator shows error widget', (WidgetTester tester) async { testWidgets('InputDecorator shows error widget', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -5939,6 +5967,45 @@ void main() { ...@@ -5939,6 +5967,45 @@ 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 helper text', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecoratorM2(
decoration: const InputDecoration(
helperText: 'helperText',
),
),
);
expect(find.text('helperText'), findsOneWidget);
});
testWidgets('InputDecorator shows helper widget', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecoratorM2(
decoration: const InputDecoration(
helper: Text('helper', style: TextStyle(fontSize: 20.0)),
),
),
);
expect(find.text('helper'), findsOneWidget);
});
testWidgets('InputDecorator throws when helper text and helper widget are provided',
(WidgetTester tester) async {
expect(
() {
buildInputDecoratorM2(
decoration: InputDecoration(
helperText: 'helperText',
helper: const Text('helper', style: TextStyle(fontSize: 20.0)),
),
);
},
throwsAssertionError,
);
});
testWidgets('InputDecorator shows error text', (WidgetTester tester) async { testWidgets('InputDecorator shows error text', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecoratorM2( buildInputDecoratorM2(
......
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