Skip to content

Commit 01fc13d

Browse files
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](flutter/flutter#145163)
1 parent 39bdff1 commit 01fc13d

File tree

4 files changed

+185
-8
lines changed

4 files changed

+185
-8
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
7+
/// Flutter code sample for [InputDecoration.helper].
8+
9+
void main() => runApp(const HelperExampleApp());
10+
11+
class HelperExampleApp extends StatelessWidget {
12+
const HelperExampleApp({super.key});
13+
14+
@override
15+
Widget build(BuildContext context) {
16+
return MaterialApp(
17+
theme: ThemeData(useMaterial3: true),
18+
home: Scaffold(
19+
appBar: AppBar(title: const Text('InputDecoration.helper Sample')),
20+
body: const HelperExample(),
21+
),
22+
);
23+
}
24+
}
25+
26+
class HelperExample extends StatelessWidget {
27+
const HelperExample({super.key});
28+
29+
@override
30+
Widget build(BuildContext context) {
31+
return const Center(
32+
child: TextField(
33+
decoration: InputDecoration(
34+
helper: Text.rich(
35+
TextSpan(
36+
children: <InlineSpan>[
37+
WidgetSpan(
38+
child: Text(
39+
'Helper Text ',
40+
),
41+
),
42+
WidgetSpan(
43+
child: Icon(
44+
Icons.help_outline,
45+
color: Colors.blue,
46+
size: 20.0,
47+
),
48+
),
49+
],
50+
),
51+
),
52+
),
53+
),
54+
);
55+
}
56+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_api_samples/material/input_decorator/input_decoration.helper.0.dart' as example;
7+
import 'package:flutter_test/flutter_test.dart';
8+
9+
void main() {
10+
testWidgets('InputDecorator helper', (WidgetTester tester) async {
11+
await tester.pumpWidget(
12+
const example.HelperExampleApp(),
13+
);
14+
15+
expect(find.byType(TextField), findsOneWidget);
16+
expect(find.text('Helper Text '), findsOneWidget);
17+
expect(find.byIcon(Icons.help_outline), findsOneWidget);
18+
});
19+
}

packages/flutter/lib/src/material/input_decorator.dart

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ class _Shaker extends AnimatedWidget {
308308
class _HelperError extends StatefulWidget {
309309
const _HelperError({
310310
this.textAlign,
311+
this.helper,
311312
this.helperText,
312313
this.helperStyle,
313314
this.helperMaxLines,
@@ -318,6 +319,7 @@ class _HelperError extends StatefulWidget {
318319
});
319320

320321
final TextAlign? textAlign;
322+
final Widget? helper;
321323
final String? helperText;
322324
final TextStyle? helperStyle;
323325
final int? helperMaxLines;
@@ -339,6 +341,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
339341
Widget? _helper;
340342
Widget? _error;
341343

344+
bool get _hasHelper => widget.helperText != null || widget.helper != null;
342345
bool get _hasError => widget.errorText != null || widget.error != null;
343346

344347
@override
@@ -351,7 +354,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
351354
if (_hasError) {
352355
_error = _buildError();
353356
_controller.value = 1.0;
354-
} else if (widget.helperText != null) {
357+
} else if (_hasHelper) {
355358
_helper = _buildHelper();
356359
}
357360
_controller.addListener(_handleChange);
@@ -375,20 +378,23 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
375378

376379
final Widget? newError = widget.error;
377380
final String? newErrorText = widget.errorText;
381+
final Widget? newHelper = widget.helper;
378382
final String? newHelperText = widget.helperText;
379383
final Widget? oldError = old.error;
380384
final String? oldErrorText = old.errorText;
385+
final Widget? oldHelper = old.helper;
381386
final String? oldHelperText = old.helperText;
382387

383388
final bool errorStateChanged = (newError != null) != (oldError != null);
384389
final bool errorTextStateChanged = (newErrorText != null) != (oldErrorText != null);
390+
final bool helperStateChanged = (newHelper != null) != (oldHelper != null);
385391
final bool helperTextStateChanged = newErrorText == null && (newHelperText != null) != (oldHelperText != null);
386392

387-
if (errorStateChanged || errorTextStateChanged || helperTextStateChanged) {
393+
if (errorStateChanged || errorTextStateChanged || helperStateChanged || helperTextStateChanged) {
388394
if (newError != null || newErrorText != null) {
389395
_error = _buildError();
390396
_controller.forward();
391-
} else if (newHelperText != null) {
397+
} else if (newHelper != null || newHelperText != null) {
392398
_helper = _buildHelper();
393399
_controller.reverse();
394400
} else {
@@ -398,12 +404,12 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
398404
}
399405

400406
Widget _buildHelper() {
401-
assert(widget.helperText != null);
407+
assert(widget.helper != null || widget.helperText != null);
402408
return Semantics(
403409
container: true,
404410
child: FadeTransition(
405411
opacity: Tween<double>(begin: 1.0, end: 0.0).animate(_controller),
406-
child: Text(
412+
child: widget.helper ?? Text(
407413
widget.helperText!,
408414
style: widget.helperStyle,
409415
textAlign: widget.textAlign,
@@ -441,7 +447,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
441447
Widget build(BuildContext context) {
442448
if (_controller.isDismissed) {
443449
_error = null;
444-
if (widget.helperText != null) {
450+
if (_hasHelper) {
445451
return _helper = _buildHelper();
446452
} else {
447453
_helper = null;
@@ -463,7 +469,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
463469
return _buildError();
464470
}
465471

466-
if (_error == null && widget.helperText != null) {
472+
if (_error == null && _hasHelper) {
467473
return _buildHelper();
468474
}
469475

@@ -479,7 +485,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
479485
);
480486
}
481487

482-
if (widget.helperText != null) {
488+
if (_hasHelper) {
483489
return Stack(
484490
children: <Widget>[
485491
_buildHelper(),
@@ -2370,6 +2376,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
23702376

23712377
final Widget helperError = _HelperError(
23722378
textAlign: textAlign,
2379+
helper: decoration.helper,
23732380
helperText: decoration.helperText,
23742381
helperStyle: _getHelperStyle(themeData, defaults),
23752382
helperMaxLines: decoration.helperMaxLines,
@@ -2575,6 +2582,7 @@ class InputDecoration {
25752582
this.labelText,
25762583
this.labelStyle,
25772584
this.floatingLabelStyle,
2585+
this.helper,
25782586
this.helperText,
25792587
this.helperStyle,
25802588
this.helperMaxLines,
@@ -2622,6 +2630,7 @@ class InputDecoration {
26222630
this.alignLabelWithHint,
26232631
this.constraints,
26242632
}) : assert(!(label != null && labelText != null), 'Declaring both label and labelText is not supported.'),
2633+
assert(!(helper != null && helperText != null), 'Declaring both helper and helperText is not supported.'),
26252634
assert(!(prefix != null && prefixText != null), 'Declaring both prefix and prefixText is not supported.'),
26262635
assert(!(suffix != null && suffixText != null), 'Declaring both suffix and suffixText is not supported.'),
26272636
assert(!(error != null && errorText != null), 'Declaring both error and errorText is not supported.');
@@ -2649,6 +2658,7 @@ class InputDecoration {
26492658
labelText = null,
26502659
labelStyle = null,
26512660
floatingLabelStyle = null,
2661+
helper = null,
26522662
helperText = null,
26532663
helperStyle = null,
26542664
helperMaxLines = null,
@@ -2802,12 +2812,32 @@ class InputDecoration {
28022812
/// {@endtemplate}
28032813
final TextStyle? floatingLabelStyle;
28042814

2815+
/// Optional widget that appears below the [InputDecorator.child].
2816+
///
2817+
/// If non-null, the [helper] is displayed below the [InputDecorator.child], in
2818+
/// the same location as [error]. If a non-null [error] or [errorText] value is
2819+
/// specified then the [helper] is not shown.
2820+
///
2821+
/// {@tool dartpad}
2822+
/// This example shows a `TextField` with a [Text.rich] widget as the [helper].
2823+
/// The widget contains [Text] and [Icon] widgets with different styles.
2824+
///
2825+
/// ** See code in examples/api/lib/material/input_decorator/input_decoration.helper.0.dart **
2826+
/// {@end-tool}
2827+
///
2828+
/// Only one of [helper] and [helperText] can be specified.
2829+
final Widget? helper;
2830+
28052831
/// Text that provides context about the [InputDecorator.child]'s value, such
28062832
/// as how the value will be used.
28072833
///
28082834
/// If non-null, the text is displayed below the [InputDecorator.child], in
28092835
/// the same location as [errorText]. If a non-null [errorText] value is
28102836
/// specified then the helper text is not shown.
2837+
///
2838+
/// If a more elaborate helper text is required, consider using [helper] instead.
2839+
///
2840+
/// Only one of [helper] and [helperText] can be specified.
28112841
final String? helperText;
28122842

28132843
/// The style to use for the [helperText].
@@ -3536,6 +3566,7 @@ class InputDecoration {
35363566
String? labelText,
35373567
TextStyle? labelStyle,
35383568
TextStyle? floatingLabelStyle,
3569+
Widget? helper,
35393570
String? helperText,
35403571
TextStyle? helperStyle,
35413572
int? helperMaxLines,
@@ -3590,6 +3621,7 @@ class InputDecoration {
35903621
labelText: labelText ?? this.labelText,
35913622
labelStyle: labelStyle ?? this.labelStyle,
35923623
floatingLabelStyle: floatingLabelStyle ?? this.floatingLabelStyle,
3624+
helper: helper ?? this.helper,
35933625
helperText: helperText ?? this.helperText,
35943626
helperStyle: helperStyle ?? this.helperStyle,
35953627
helperMaxLines : helperMaxLines ?? this.helperMaxLines,
@@ -3695,6 +3727,7 @@ class InputDecoration {
36953727
&& other.labelText == labelText
36963728
&& other.labelStyle == labelStyle
36973729
&& other.floatingLabelStyle == floatingLabelStyle
3730+
&& other.helper == helper
36983731
&& other.helperText == helperText
36993732
&& other.helperStyle == helperStyle
37003733
&& other.helperMaxLines == helperMaxLines
@@ -3752,6 +3785,7 @@ class InputDecoration {
37523785
labelText,
37533786
floatingLabelStyle,
37543787
labelStyle,
3788+
helper,
37553789
helperText,
37563790
helperStyle,
37573791
helperMaxLines,
@@ -3810,6 +3844,7 @@ class InputDecoration {
38103844
if (label != null) 'label: $label',
38113845
if (labelText != null) 'labelText: "$labelText"',
38123846
if (floatingLabelStyle != null) 'floatingLabelStyle: "$floatingLabelStyle"',
3847+
if (helper != null) 'helper: "$helper"',
38133848
if (helperText != null) 'helperText: "$helperText"',
38143849
if (helperMaxLines != null) 'helperMaxLines: "$helperMaxLines"',
38153850
if (hintText != null) 'hintText: "$hintText"',

packages/flutter/test/material/input_decorator_test.dart

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2747,6 +2747,34 @@ void main() {
27472747
});
27482748
});
27492749

2750+
group('Helper widget', () {
2751+
testWidgets('InputDecorator shows helper widget', (WidgetTester tester) async {
2752+
await tester.pumpWidget(
2753+
buildInputDecorator(
2754+
decoration: const InputDecoration(
2755+
helper: Text('helper', style: TextStyle(fontSize: 20.0)),
2756+
),
2757+
),
2758+
);
2759+
2760+
expect(find.text('helper'), findsOneWidget);
2761+
});
2762+
2763+
testWidgets('InputDecorator throws when helper text and helper widget are provided', (WidgetTester tester) async {
2764+
expect(
2765+
() {
2766+
buildInputDecorator(
2767+
decoration: InputDecoration(
2768+
helperText: 'helperText',
2769+
helper: const Text('helper', style: TextStyle(fontSize: 20.0)),
2770+
),
2771+
);
2772+
},
2773+
throwsAssertionError,
2774+
);
2775+
});
2776+
});
2777+
27502778
group('Error widget', () {
27512779
testWidgets('InputDecorator shows error widget', (WidgetTester tester) async {
27522780
await tester.pumpWidget(
@@ -5939,6 +5967,45 @@ void main() {
59395967
expect(tester.getBottomLeft(find.text(kHelper1)), const Offset(12.0, 76.0));
59405968
});
59415969

5970+
testWidgets('InputDecorator shows helper text', (WidgetTester tester) async {
5971+
await tester.pumpWidget(
5972+
buildInputDecoratorM2(
5973+
decoration: const InputDecoration(
5974+
helperText: 'helperText',
5975+
),
5976+
),
5977+
);
5978+
5979+
expect(find.text('helperText'), findsOneWidget);
5980+
});
5981+
5982+
testWidgets('InputDecorator shows helper widget', (WidgetTester tester) async {
5983+
await tester.pumpWidget(
5984+
buildInputDecoratorM2(
5985+
decoration: const InputDecoration(
5986+
helper: Text('helper', style: TextStyle(fontSize: 20.0)),
5987+
),
5988+
),
5989+
);
5990+
5991+
expect(find.text('helper'), findsOneWidget);
5992+
});
5993+
5994+
testWidgets('InputDecorator throws when helper text and helper widget are provided',
5995+
(WidgetTester tester) async {
5996+
expect(
5997+
() {
5998+
buildInputDecoratorM2(
5999+
decoration: InputDecoration(
6000+
helperText: 'helperText',
6001+
helper: const Text('helper', style: TextStyle(fontSize: 20.0)),
6002+
),
6003+
);
6004+
},
6005+
throwsAssertionError,
6006+
);
6007+
});
6008+
59426009
testWidgets('InputDecorator shows error text', (WidgetTester tester) async {
59436010
await tester.pumpWidget(
59446011
buildInputDecoratorM2(

0 commit comments

Comments
 (0)