Unverified Commit 89fa4cc5 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Add InputDecoration alignLabelWithHint parameter (#24993)

* InputDecorator param for alignment of label

* Put baseline/center code in the right place where label is layed out

* Fix existing test

* Test top label positioning

* Rename to alignLabelWithHint, and make it a bool

* Test for TextField with and without alignLabelWithHint set

* alignLabelWithHint in theme as well

* debugFillProperties addition and test

* Small style fixes for review

* Fix analyze const error
parent 9b6229ab
......@@ -449,6 +449,7 @@ class _Decoration {
this.helperError,
this.counter,
this.container,
this.alignLabelWithHint,
}) : assert(contentPadding != null),
assert(isCollapsed != null),
assert(floatingLabelHeight != null),
......@@ -460,6 +461,7 @@ class _Decoration {
final double floatingLabelProgress;
final InputBorder border;
final _InputBorderGap borderGap;
final bool alignLabelWithHint;
final Widget icon;
final Widget input;
final Widget label;
......@@ -494,7 +496,8 @@ class _Decoration {
&& typedOther.suffixIcon == suffixIcon
&& typedOther.helperError == helperError
&& typedOther.counter == counter
&& typedOther.container == container;
&& typedOther.container == container
&& typedOther.alignLabelWithHint == alignLabelWithHint;
}
@override
......@@ -516,6 +519,7 @@ class _Decoration {
helperError,
counter,
container,
alignLabelWithHint,
);
}
}
......@@ -839,8 +843,15 @@ class _RenderDecoration extends RenderBox {
+ contentPadding.right));
boxConstraints = boxConstraints.copyWith(maxWidth: inputWidth);
if (label != null) // The label is not baseline aligned.
label.layout(boxConstraints, parentUsesSize: true);
if (label != null) {
if (decoration.alignLabelWithHint) {
// The label is aligned with the hint, at the baseline
layoutLineBox(label);
} else {
// The label is centered, not baseline aligned
label.layout(boxConstraints, parentUsesSize: true);
}
}
boxConstraints = boxConstraints.copyWith(minWidth: inputWidth);
layoutLineBox(hint);
......@@ -1037,8 +1048,13 @@ class _RenderDecoration extends RenderBox {
start += contentPadding.left;
start -= centerLayout(prefixIcon, start - prefixIcon.size.width);
}
if (label != null)
centerLayout(label, start - label.size.width);
if (label != null) {
if (decoration.alignLabelWithHint) {
baselineLayout(label, start - label.size.width);
} else {
centerLayout(label, start - label.size.width);
}
}
if (prefix != null)
start -= baselineLayout(prefix, start - prefix.size.width);
if (input != null)
......@@ -1061,7 +1077,11 @@ class _RenderDecoration extends RenderBox {
start += centerLayout(prefixIcon, start);
}
if (label != null)
centerLayout(label, start);
if (decoration.alignLabelWithHint) {
baselineLayout(label, start);
} else {
centerLayout(label, start);
}
if (prefix != null)
start += baselineLayout(prefix, start);
if (input != null)
......@@ -1891,6 +1911,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
icon: icon,
input: widget.child,
label: label,
alignLabelWithHint: decoration.alignLabelWithHint,
hint: hint,
prefix: prefix,
suffix: suffix,
......@@ -1972,6 +1993,7 @@ class InputDecoration {
this.border,
this.enabled = true,
this.semanticCounterText,
this.alignLabelWithHint,
}) : assert(enabled != null),
assert(!(prefix != null && prefixText != null), 'Declaring both prefix and prefixText is not allowed'),
assert(!(suffix != null && suffixText != null), 'Declaring both suffix and suffixText is not allowed'),
......@@ -2018,7 +2040,8 @@ class InputDecoration {
focusedErrorBorder = null,
disabledBorder = null,
enabledBorder = null,
semanticCounterText = null;
semanticCounterText = null,
alignLabelWithHint = false;
/// An icon to show before the input field and outside of the decoration's
/// container.
......@@ -2449,6 +2472,13 @@ class InputDecoration {
/// If provided, this replaces the semantic label of the [counterText].
final String semanticCounterText;
/// Typically set to true when the [InputDecorator] contains a multiline
/// [TextField] ([TextField.maxLines] is null or > 1) to override the default
/// behavior of aligning the label with the center of the [TextField].
///
/// Defaults to false.
final bool alignLabelWithHint;
/// Creates a copy of this input decoration with the given fields replaced
/// by the new values.
///
......@@ -2488,6 +2518,7 @@ class InputDecoration {
InputBorder border,
bool enabled,
String semanticCounterText,
bool alignLabelWithHint,
}) {
return InputDecoration(
icon: icon ?? this.icon,
......@@ -2524,6 +2555,7 @@ class InputDecoration {
border: border ?? this.border,
enabled: enabled ?? this.enabled,
semanticCounterText: semanticCounterText ?? this.semanticCounterText,
alignLabelWithHint: alignLabelWithHint ?? this.alignLabelWithHint,
);
}
......@@ -2553,6 +2585,7 @@ class InputDecoration {
disabledBorder: disabledBorder ?? theme.disabledBorder,
enabledBorder: enabledBorder ?? theme.enabledBorder,
border: border ?? theme.border,
alignLabelWithHint: alignLabelWithHint ?? theme.alignLabelWithHint,
);
}
......@@ -2597,7 +2630,8 @@ class InputDecoration {
&& typedOther.enabledBorder == enabledBorder
&& typedOther.border == border
&& typedOther.enabled == enabled
&& typedOther.semanticCounterText == semanticCounterText;
&& typedOther.semanticCounterText == semanticCounterText
&& typedOther.alignLabelWithHint == alignLabelWithHint;
}
@override
......@@ -2647,6 +2681,7 @@ class InputDecoration {
border,
enabled,
semanticCounterText,
alignLabelWithHint,
),
);
}
......@@ -2718,6 +2753,8 @@ class InputDecoration {
description.add('enabled: false');
if (semanticCounterText != null)
description.add('semanticCounterText: $semanticCounterText');
if (alignLabelWithHint != null)
description.add('alignLabelWithHint: $alignLabelWithHint');
return 'InputDecoration(${description.join(', ')})';
}
}
......@@ -2759,9 +2796,11 @@ class InputDecorationTheme extends Diagnosticable {
this.disabledBorder,
this.enabledBorder,
this.border,
this.alignLabelWithHint = false,
}) : assert(isDense != null),
assert(isCollapsed != null),
assert(filled != null);
assert(filled != null),
assert(alignLabelWithHint != null);
/// The style to use for [InputDecoration.labelText] when the label is
/// above (i.e., vertically adjacent to) the input field.
......@@ -3020,6 +3059,11 @@ class InputDecorationTheme extends Diagnosticable {
/// rounded rectangle around the input decorator's container.
final InputBorder border;
/// Typically set to true when the [InputDecorator] contains a multiline
/// [TextField] ([TextField.maxLines] is null or > 1) to override the default
/// behavior of aligning the label with the center of the [TextField].
final bool alignLabelWithHint;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
......@@ -3040,9 +3084,10 @@ class InputDecorationTheme extends Diagnosticable {
properties.add(DiagnosticsProperty<Color>('fillColor', fillColor, defaultValue: defaultTheme.fillColor));
properties.add(DiagnosticsProperty<InputBorder>('errorBorder', errorBorder, defaultValue: defaultTheme.errorBorder));
properties.add(DiagnosticsProperty<InputBorder>('focusedBorder', focusedBorder, defaultValue: defaultTheme.focusedErrorBorder));
properties.add(DiagnosticsProperty<InputBorder>('focusedErrorborder', focusedErrorBorder, defaultValue: defaultTheme.focusedErrorBorder));
properties.add(DiagnosticsProperty<InputBorder>('focusedErrorBorder', focusedErrorBorder, defaultValue: defaultTheme.focusedErrorBorder));
properties.add(DiagnosticsProperty<InputBorder>('disabledBorder', disabledBorder, defaultValue: defaultTheme.disabledBorder));
properties.add(DiagnosticsProperty<InputBorder>('enabledBorder', enabledBorder, defaultValue: defaultTheme.enabledBorder));
properties.add(DiagnosticsProperty<InputBorder>('border', border, defaultValue: defaultTheme.border));
properties.add(DiagnosticsProperty<bool>('alignLabelWithHint', alignLabelWithHint, defaultValue: defaultTheme.alignLabelWithHint));
}
}
......@@ -248,6 +248,56 @@ void main() {
expect(tester.getTopLeft(find.text('label')).dy, 20.0);
expect(tester.getBottomLeft(find.text('label')).dy, 36.0);
expect(getBorderColor(tester), Colors.transparent);
// alignLabelWithHint: true positions the label at the text baseline,
// aligned with the hint.
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
isFocused: false,
decoration: const InputDecoration(
labelText: 'label',
alignLabelWithHint: true,
hintText: 'hint',
),
),
);
await tester.pumpAndSettle();
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy);
expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
});
testWidgets('InputDecorator alignLabelWithHint for multiline TextField', (WidgetTester tester) async {
Widget buildFrame(bool alignLabelWithHint) {
return MaterialApp(
home: Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: TextField(
maxLines: 8,
decoration: InputDecoration(
labelText: 'label',
alignLabelWithHint: alignLabelWithHint,
hintText: 'hint',
),
),
),
),
);
}
// alignLabelWithHint: false centers the label in the TextField
await tester.pumpWidget(buildFrame(false));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('label')).dy, 76.0);
expect(tester.getBottomLeft(find.text('label')).dy, 92.0);
// alignLabelWithHint: true aligns the label with the hint.
await tester.pumpWidget(buildFrame(true));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy);
expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
});
// Overall height for this InputDecorator is 40.0dps
......@@ -1552,6 +1602,7 @@ void main() {
filled: true,
fillColor: Colors.red,
border: InputBorder.none,
alignLabelWithHint: true,
)
);
......@@ -1567,6 +1618,7 @@ void main() {
expect(decoration.filled, true);
expect(decoration.fillColor, Colors.red);
expect(decoration.border, InputBorder.none);
expect(decoration.alignLabelWithHint, true);
// InputDecoration (baseDecoration) defines InputDecoration properties
decoration = const InputDecoration(
......@@ -1582,6 +1634,7 @@ void main() {
filled: false,
fillColor: Colors.blue,
border: OutlineInputBorder(),
alignLabelWithHint: false,
).applyDefaults(
const InputDecorationTheme(
labelStyle: themeStyle,
......@@ -1597,6 +1650,7 @@ void main() {
filled: true,
fillColor: Colors.red,
border: InputBorder.none,
alignLabelWithHint: true,
),
);
......@@ -1613,6 +1667,7 @@ void main() {
expect(decoration.filled, false);
expect(decoration.fillColor, Colors.blue);
expect(decoration.border, const OutlineInputBorder());
expect(decoration.alignLabelWithHint, false);
});
testWidgets('InputDecorator OutlineInputBorder fillColor is clipped by border', (WidgetTester tester) async {
......@@ -2017,4 +2072,51 @@ void main() {
expect(underlineInputBorder.hashCode, const UnderlineInputBorder(borderSide: BorderSide(color: Colors.blue)).hashCode);
expect(underlineInputBorder.hashCode, isNot(const UnderlineInputBorder().hashCode));
});
testWidgets('InputDecorationTheme implements debugFillDescription', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const InputDecorationTheme(
labelStyle: TextStyle(),
helperStyle: TextStyle(),
hintStyle: TextStyle(),
errorMaxLines: 5,
hasFloatingPlaceholder: false,
contentPadding: EdgeInsetsDirectional.only(start: 40.0, top: 12.0, bottom: 12.0),
prefixStyle: TextStyle(),
suffixStyle: TextStyle(),
counterStyle: TextStyle(),
filled: true,
fillColor: Colors.red,
errorBorder: UnderlineInputBorder(),
focusedBorder: UnderlineInputBorder(),
focusedErrorBorder: UnderlineInputBorder(),
disabledBorder: UnderlineInputBorder(),
enabledBorder: UnderlineInputBorder(),
border: UnderlineInputBorder(),
alignLabelWithHint: true,
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode n) => !n.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[
'labelStyle: TextStyle(<all styles inherited>)',
'helperStyle: TextStyle(<all styles inherited>)',
'hintStyle: TextStyle(<all styles inherited>)',
'errorMaxLines: 5',
'hasFloatingPlaceholder: false',
'contentPadding: EdgeInsetsDirectional(40.0, 12.0, 0.0, 12.0)',
'prefixStyle: TextStyle(<all styles inherited>)',
'suffixStyle: TextStyle(<all styles inherited>)',
'counterStyle: TextStyle(<all styles inherited>)',
'filled: true',
'fillColor: MaterialColor(primary value: Color(0xfff44336))',
'errorBorder: UnderlineInputBorder()',
'focusedBorder: UnderlineInputBorder()',
'focusedErrorBorder: UnderlineInputBorder()',
'disabledBorder: UnderlineInputBorder()',
'enabledBorder: UnderlineInputBorder()',
'border: UnderlineInputBorder()',
'alignLabelWithHint: true',
]);
});
}
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