Textfield InputDecorator height can grow to accomodate error text (#17292)
This commit is contained in:
parent
feb16d8d01
commit
f8d0d877f5
@ -242,6 +242,7 @@ class _HelperError extends StatefulWidget {
|
||||
this.helperStyle,
|
||||
this.errorText,
|
||||
this.errorStyle,
|
||||
this.errorMaxLines,
|
||||
}) : super(key: key);
|
||||
|
||||
final TextAlign textAlign;
|
||||
@ -249,6 +250,7 @@ class _HelperError extends StatefulWidget {
|
||||
final TextStyle helperStyle;
|
||||
final String errorText;
|
||||
final TextStyle errorStyle;
|
||||
final int errorMaxLines;
|
||||
|
||||
@override
|
||||
_HelperErrorState createState() => new _HelperErrorState();
|
||||
@ -343,6 +345,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta
|
||||
style: widget.errorStyle,
|
||||
textAlign: widget.textAlign,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: widget.errorMaxLines,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -444,7 +447,7 @@ class _Decoration {
|
||||
assert(floatingLabelHeight != null),
|
||||
assert(floatingLabelProgress != null);
|
||||
|
||||
final EdgeInsets contentPadding;
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
final bool isCollapsed;
|
||||
final double floatingLabelHeight;
|
||||
final double floatingLabelProgress;
|
||||
@ -800,11 +803,21 @@ class _RenderDecoration extends RenderBox {
|
||||
double subtextBaseline = 0.0;
|
||||
double subtextHeight = 0.0;
|
||||
if (helperError != null || counter != null) {
|
||||
boxConstraints = layoutConstraints.loosen();
|
||||
aboveBaseline = 0.0;
|
||||
belowBaseline = 0.0;
|
||||
layoutLineBox(helperError);
|
||||
layoutLineBox(counter);
|
||||
|
||||
// The helper or error text can occupy the full width less the space
|
||||
// occupied by the icon and counter.
|
||||
boxConstraints = boxConstraints.copyWith(
|
||||
maxWidth: boxConstraints.maxWidth
|
||||
- _boxSize(icon).width
|
||||
- _boxSize(counter).width
|
||||
- contentPadding.horizontal,
|
||||
);
|
||||
layoutLineBox(helperError);
|
||||
|
||||
if (aboveBaseline + belowBaseline > 0.0) {
|
||||
const double subtextGap = 8.0;
|
||||
subtextBaseline = containerHeight + subtextGap + aboveBaseline;
|
||||
@ -1637,39 +1650,44 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
|
||||
helperStyle: _getHelperStyle(themeData),
|
||||
errorText: decoration.errorText,
|
||||
errorStyle: _getErrorStyle(themeData),
|
||||
errorMaxLines: decoration.errorMaxLines,
|
||||
);
|
||||
|
||||
final Widget counter = decoration.counterText == null ? null :
|
||||
new Text(
|
||||
decoration.counterText,
|
||||
style: _getHelperStyle(themeData).merge(decoration.counterStyle),
|
||||
textAlign: textAlign == TextAlign.end ? TextAlign.start : TextAlign.end,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
|
||||
// The _Decoration widget and _RenderDecoration assume that contentPadding
|
||||
// has been resolved to EdgeInsets.
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
final EdgeInsets decorationContentPadding = decoration.contentPadding?.resolve(textDirection);
|
||||
|
||||
EdgeInsets contentPadding;
|
||||
double floatingLabelHeight;
|
||||
if (decoration.isCollapsed) {
|
||||
floatingLabelHeight = 0.0;
|
||||
contentPadding = decoration.contentPadding ?? EdgeInsets.zero;
|
||||
contentPadding = decorationContentPadding ?? EdgeInsets.zero;
|
||||
} else if (!decoration.border.isOutline) {
|
||||
// 4.0: the vertical gap between the inline elements and the floating label.
|
||||
floatingLabelHeight = 4.0 + 0.75 * inlineLabelStyle.fontSize;
|
||||
if (decoration.filled == true) { // filled == null same as filled == false
|
||||
contentPadding = decoration.contentPadding ?? (decorationIsDense
|
||||
contentPadding = decorationContentPadding ?? (decorationIsDense
|
||||
? const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0)
|
||||
: const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 12.0));
|
||||
} else {
|
||||
// Not left or right padding for underline borders that aren't filled
|
||||
// is a small concession to backwards compatibility. This eliminates
|
||||
// the most noticeable layout change introduced by #13734.
|
||||
contentPadding = decoration.contentPadding ?? (decorationIsDense
|
||||
contentPadding = decorationContentPadding ?? (decorationIsDense
|
||||
? const EdgeInsets.fromLTRB(0.0, 8.0, 0.0, 8.0)
|
||||
: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 12.0));
|
||||
}
|
||||
} else {
|
||||
floatingLabelHeight = 0.0;
|
||||
contentPadding = decoration.contentPadding ?? (decorationIsDense
|
||||
contentPadding = decorationContentPadding ?? (decorationIsDense
|
||||
? const EdgeInsets.fromLTRB(12.0, 20.0, 12.0, 12.0)
|
||||
: const EdgeInsets.fromLTRB(12.0, 24.0, 12.0, 16.0));
|
||||
}
|
||||
@ -1720,8 +1738,9 @@ class InputDecoration {
|
||||
///
|
||||
/// Unless specified by [ThemeData.inputDecorationTheme],
|
||||
/// [InputDecorator] defaults [isDense] to true, and [filled] to false,
|
||||
/// Similarly, the default border is an instance of [UnderlineInputBorder].
|
||||
/// If [border] is [InputBorder.none] then no border is drawn.
|
||||
/// and [maxLines] to 1. The default border is an instance
|
||||
/// of [UnderlineInputBorder]. If [border] is [InputBorder.none] then
|
||||
/// no border is drawn.
|
||||
///
|
||||
/// The [enabled] argument must not be null.
|
||||
const InputDecoration({
|
||||
@ -1734,6 +1753,7 @@ class InputDecoration {
|
||||
this.hintStyle,
|
||||
this.errorText,
|
||||
this.errorStyle,
|
||||
this.errorMaxLines,
|
||||
this.isDense,
|
||||
this.contentPadding,
|
||||
this.prefixIcon,
|
||||
@ -1770,6 +1790,7 @@ class InputDecoration {
|
||||
helperStyle = null,
|
||||
errorText = null,
|
||||
errorStyle = null,
|
||||
errorMaxLines = null,
|
||||
isDense = false,
|
||||
contentPadding = EdgeInsets.zero,
|
||||
isCollapsed = true,
|
||||
@ -1861,6 +1882,16 @@ class InputDecoration {
|
||||
/// input field and the current [Theme].
|
||||
final TextStyle errorStyle;
|
||||
|
||||
|
||||
/// The maximum number of lines the [errorText] can occupy.
|
||||
///
|
||||
/// Defaults to null, which means that the [errorText] will be limited
|
||||
/// to a single line with [TextOverflow.ellipsis].
|
||||
///
|
||||
/// This value is passed along to the [Text.maxLines] attribute
|
||||
/// of the [Text] widget used to display the error.
|
||||
final int errorMaxLines;
|
||||
|
||||
/// Whether the input [child] is part of a dense form (i.e., uses less vertical
|
||||
/// space).
|
||||
///
|
||||
@ -1877,7 +1908,7 @@ class InputDecoration {
|
||||
/// By default the `contentPadding` reflects [isDense] and the type of the
|
||||
/// [border]. If [isCollapsed] is true then `contentPadding` is
|
||||
/// [EdgeInsets.zero].
|
||||
final EdgeInsets contentPadding;
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
|
||||
/// Whether the decoration is the same size as the input field.
|
||||
///
|
||||
@ -2024,8 +2055,9 @@ class InputDecoration {
|
||||
TextStyle hintStyle,
|
||||
String errorText,
|
||||
TextStyle errorStyle,
|
||||
int errorMaxLines,
|
||||
bool isDense,
|
||||
EdgeInsets contentPadding,
|
||||
EdgeInsetsGeometry contentPadding,
|
||||
Widget prefixIcon,
|
||||
String prefixText,
|
||||
TextStyle prefixStyle,
|
||||
@ -2049,6 +2081,7 @@ class InputDecoration {
|
||||
hintStyle: hintStyle ?? this.hintStyle,
|
||||
errorText: errorText ?? this.errorText,
|
||||
errorStyle: errorStyle ?? this.errorStyle,
|
||||
errorMaxLines: errorMaxLines ?? this.errorMaxLines,
|
||||
isDense: isDense ?? this.isDense,
|
||||
contentPadding: contentPadding ?? this.contentPadding,
|
||||
prefixIcon: prefixIcon ?? this.prefixIcon,
|
||||
@ -2077,6 +2110,7 @@ class InputDecoration {
|
||||
helperStyle: helperStyle ?? theme.helperStyle,
|
||||
hintStyle: hintStyle ?? theme.hintStyle,
|
||||
errorStyle: errorStyle ?? theme.errorStyle,
|
||||
errorMaxLines: errorMaxLines ?? theme.errorMaxLines,
|
||||
isDense: isDense ?? theme.isDense,
|
||||
contentPadding: contentPadding ?? theme.contentPadding,
|
||||
prefixStyle: prefixStyle ?? theme.prefixStyle,
|
||||
@ -2104,6 +2138,7 @@ class InputDecoration {
|
||||
&& typedOther.hintStyle == hintStyle
|
||||
&& typedOther.errorText == errorText
|
||||
&& typedOther.errorStyle == errorStyle
|
||||
&& typedOther.errorMaxLines == errorMaxLines
|
||||
&& typedOther.isDense == isDense
|
||||
&& typedOther.contentPadding == contentPadding
|
||||
&& typedOther.isCollapsed == isCollapsed
|
||||
@ -2129,11 +2164,12 @@ class InputDecoration {
|
||||
labelStyle,
|
||||
helperText,
|
||||
helperStyle,
|
||||
hintText,
|
||||
hashValues( // Over 20 fields...
|
||||
hintText,
|
||||
hintStyle,
|
||||
errorText,
|
||||
errorStyle,
|
||||
errorMaxLines,
|
||||
isDense,
|
||||
contentPadding,
|
||||
isCollapsed,
|
||||
@ -2166,6 +2202,10 @@ class InputDecoration {
|
||||
description.add('hintText: "$hintText"');
|
||||
if (errorText != null)
|
||||
description.add('errorText: "$errorText"');
|
||||
if (errorStyle != null)
|
||||
description.add('errorStyle: "$errorStyle"');
|
||||
if (errorMaxLines != null)
|
||||
description.add('errorMaxLines: "$errorMaxLines"');
|
||||
if (isDense ?? false)
|
||||
description.add('isDense: $isDense');
|
||||
if (contentPadding != null)
|
||||
@ -2221,6 +2261,7 @@ class InputDecorationTheme extends Diagnosticable {
|
||||
this.helperStyle,
|
||||
this.hintStyle,
|
||||
this.errorStyle,
|
||||
this.errorMaxLines,
|
||||
this.isDense: false,
|
||||
this.contentPadding,
|
||||
this.isCollapsed: false,
|
||||
@ -2264,6 +2305,15 @@ class InputDecorationTheme extends Diagnosticable {
|
||||
/// input field and the current [Theme].
|
||||
final TextStyle errorStyle;
|
||||
|
||||
/// The maximum number of lines the [errorText] can occupy.
|
||||
///
|
||||
/// Defaults to null, which means that the [errorText] will be limited
|
||||
/// to a single line with [TextOverflow.ellipsis].
|
||||
///
|
||||
/// This value is passed along to the [Text.maxLines] attribute
|
||||
/// of the [Text] widget used to display the error.
|
||||
final int errorMaxLines;
|
||||
|
||||
/// Whether the input decorator's child is part of a dense form (i.e., uses
|
||||
/// less vertical space).
|
||||
///
|
||||
@ -2282,7 +2332,7 @@ class InputDecorationTheme extends Diagnosticable {
|
||||
/// By default the `contentPadding` reflects [isDense] and the type of the
|
||||
/// [border]. If [isCollapsed] is true then `contentPadding` is
|
||||
/// [EdgeInsets.zero].
|
||||
final EdgeInsets contentPadding;
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
|
||||
/// Whether the decoration is the same size as the input field.
|
||||
///
|
||||
@ -2355,6 +2405,7 @@ class InputDecorationTheme extends Diagnosticable {
|
||||
properties.add(new DiagnosticsProperty<TextStyle>('helperStyle', helperStyle, defaultValue: defaultTheme.helperStyle));
|
||||
properties.add(new DiagnosticsProperty<TextStyle>('hintStyle', hintStyle, defaultValue: defaultTheme.hintStyle));
|
||||
properties.add(new DiagnosticsProperty<TextStyle>('errorStyle', errorStyle, defaultValue: defaultTheme.errorStyle));
|
||||
properties.add(new DiagnosticsProperty<int>('errorMaxLines', errorMaxLines, defaultValue: defaultTheme.errorMaxLines));
|
||||
properties.add(new DiagnosticsProperty<bool>('isDense', isDense, defaultValue: defaultTheme.isDense));
|
||||
properties.add(new DiagnosticsProperty<EdgeInsets>('contentPadding', contentPadding, defaultValue: defaultTheme.contentPadding));
|
||||
properties.add(new DiagnosticsProperty<bool>('isCollapsed', isCollapsed, defaultValue: defaultTheme.isCollapsed));
|
||||
|
@ -596,6 +596,82 @@ void main() {
|
||||
expect(tester.getTopRight(find.text('counter')), const Offset(788.0, 56.0));
|
||||
});
|
||||
|
||||
testWidgets('InputDecoration errorMaxLines', (WidgetTester tester) async {
|
||||
const String kError1 = 'e0';
|
||||
const String kError2 = 'e0\ne1';
|
||||
const String kError3 = 'e0\ne1\ne2';
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildInputDecorator(
|
||||
isEmpty: true,
|
||||
// isFocused: false (default)
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'label',
|
||||
helperText: 'helper',
|
||||
errorText: kError3,
|
||||
errorMaxLines: 3,
|
||||
filled: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Overall height for this InputDecorator is 100dps:
|
||||
//
|
||||
// 12 - top padding
|
||||
// 12 - floating label (ahem font size 16dps * 0.75 = 12)
|
||||
// 4 - floating label / input text gap
|
||||
// 16 - input text (ahem font size 16dps)
|
||||
// 12 - bottom padding
|
||||
// 8 - below the border padding
|
||||
// 36 - error text (3 lines, ahem font size 12dps)
|
||||
|
||||
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 100.0));
|
||||
expect(tester.getTopLeft(find.text(kError3)), const Offset(12.0, 64.0));
|
||||
expect(tester.getBottomLeft(find.text(kError3)), const Offset(12.0, 100.0));
|
||||
|
||||
// Overall height for this InputDecorator is 12 less than the first
|
||||
// one, 88dps, because errorText only occupies two lines.
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildInputDecorator(
|
||||
isEmpty: true,
|
||||
// isFocused: false (default)
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'label',
|
||||
helperText: 'helper',
|
||||
errorText: kError2,
|
||||
errorMaxLines: 3,
|
||||
filled: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 88.0));
|
||||
expect(tester.getTopLeft(find.text(kError2)), const Offset(12.0, 64.0));
|
||||
expect(tester.getBottomLeft(find.text(kError2)), const Offset(12.0, 88.0));
|
||||
|
||||
// Overall height for this InputDecorator is 24 less than the first
|
||||
// one, 88dps, because errorText only occupies one line.
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildInputDecorator(
|
||||
isEmpty: true,
|
||||
// isFocused: false (default)
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'label',
|
||||
helperText: 'helper',
|
||||
errorText: kError1,
|
||||
errorMaxLines: 3,
|
||||
filled: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 76.0));
|
||||
expect(tester.getTopLeft(find.text(kError1)), const Offset(12.0, 64.0));
|
||||
expect(tester.getBottomLeft(find.text(kError1)), const Offset(12.0, 76.0));
|
||||
});
|
||||
|
||||
testWidgets('InputDecorator prefix/suffix', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
buildInputDecorator(
|
||||
@ -760,6 +836,46 @@ void main() {
|
||||
expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopLeft(find.text('p')).dx));
|
||||
});
|
||||
|
||||
testWidgets('InputDecorator contentPadding RTL layout', (WidgetTester tester) async {
|
||||
// LTR: content left edge is contentPadding.start: 40.0
|
||||
await tester.pumpWidget(
|
||||
buildInputDecorator(
|
||||
// isEmpty: false (default)
|
||||
// isFocused: false (default)
|
||||
textDirection: TextDirection.ltr,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: const EdgeInsetsDirectional.only(start: 40.0, top: 12.0, bottom: 12.0),
|
||||
labelText: 'label',
|
||||
hintText: 'hint',
|
||||
filled: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
|
||||
expect(tester.getTopLeft(find.text('text')).dx, 40.0);
|
||||
expect(tester.getTopLeft(find.text('label')).dx, 40.0);
|
||||
expect(tester.getTopLeft(find.text('hint')).dx, 40.0);
|
||||
|
||||
// RTL: content right edge is 800 - contentPadding.start: 760.0.
|
||||
await tester.pumpWidget(
|
||||
buildInputDecorator(
|
||||
// isEmpty: false (default)
|
||||
isFocused: true, // label is floating, still adjusted for contentPadding
|
||||
textDirection: TextDirection.rtl,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: const EdgeInsetsDirectional.only(start: 40.0, top: 12.0, bottom: 12.0),
|
||||
labelText: 'label',
|
||||
hintText: 'hint',
|
||||
filled: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
|
||||
expect(tester.getTopRight(find.text('text')).dx, 760.0);
|
||||
expect(tester.getTopRight(find.text('label')).dx, 760.0);
|
||||
expect(tester.getTopRight(find.text('hint')).dx, 760.0);
|
||||
});
|
||||
|
||||
testWidgets('InputDecorator prefix/suffix dense layout', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
buildInputDecorator(
|
||||
@ -1194,6 +1310,7 @@ void main() {
|
||||
helperStyle: themeStyle,
|
||||
hintStyle: themeStyle,
|
||||
errorStyle: themeStyle,
|
||||
errorMaxLines: 4,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.all(1.0),
|
||||
prefixStyle: themeStyle,
|
||||
@ -1209,6 +1326,7 @@ void main() {
|
||||
expect(decoration.helperStyle, decorationStyle);
|
||||
expect(decoration.hintStyle, decorationStyle);
|
||||
expect(decoration.errorStyle, decorationStyle);
|
||||
expect(decoration.errorMaxLines, 4);
|
||||
expect(decoration.isDense, false);
|
||||
expect(decoration.contentPadding, const EdgeInsets.all(4.0));
|
||||
expect(decoration.prefixStyle, decorationStyle);
|
||||
|
Loading…
x
Reference in New Issue
Block a user