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 { ...@@ -449,6 +449,7 @@ class _Decoration {
this.helperError, this.helperError,
this.counter, this.counter,
this.container, this.container,
this.alignLabelWithHint,
}) : assert(contentPadding != null), }) : assert(contentPadding != null),
assert(isCollapsed != null), assert(isCollapsed != null),
assert(floatingLabelHeight != null), assert(floatingLabelHeight != null),
...@@ -460,6 +461,7 @@ class _Decoration { ...@@ -460,6 +461,7 @@ class _Decoration {
final double floatingLabelProgress; final double floatingLabelProgress;
final InputBorder border; final InputBorder border;
final _InputBorderGap borderGap; final _InputBorderGap borderGap;
final bool alignLabelWithHint;
final Widget icon; final Widget icon;
final Widget input; final Widget input;
final Widget label; final Widget label;
...@@ -494,7 +496,8 @@ class _Decoration { ...@@ -494,7 +496,8 @@ class _Decoration {
&& typedOther.suffixIcon == suffixIcon && typedOther.suffixIcon == suffixIcon
&& typedOther.helperError == helperError && typedOther.helperError == helperError
&& typedOther.counter == counter && typedOther.counter == counter
&& typedOther.container == container; && typedOther.container == container
&& typedOther.alignLabelWithHint == alignLabelWithHint;
} }
@override @override
...@@ -516,6 +519,7 @@ class _Decoration { ...@@ -516,6 +519,7 @@ class _Decoration {
helperError, helperError,
counter, counter,
container, container,
alignLabelWithHint,
); );
} }
} }
...@@ -839,8 +843,15 @@ class _RenderDecoration extends RenderBox { ...@@ -839,8 +843,15 @@ class _RenderDecoration extends RenderBox {
+ contentPadding.right)); + contentPadding.right));
boxConstraints = boxConstraints.copyWith(maxWidth: inputWidth); boxConstraints = boxConstraints.copyWith(maxWidth: inputWidth);
if (label != null) // The label is not baseline aligned. 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); label.layout(boxConstraints, parentUsesSize: true);
}
}
boxConstraints = boxConstraints.copyWith(minWidth: inputWidth); boxConstraints = boxConstraints.copyWith(minWidth: inputWidth);
layoutLineBox(hint); layoutLineBox(hint);
...@@ -1037,8 +1048,13 @@ class _RenderDecoration extends RenderBox { ...@@ -1037,8 +1048,13 @@ class _RenderDecoration extends RenderBox {
start += contentPadding.left; start += contentPadding.left;
start -= centerLayout(prefixIcon, start - prefixIcon.size.width); start -= centerLayout(prefixIcon, start - prefixIcon.size.width);
} }
if (label != null) if (label != null) {
if (decoration.alignLabelWithHint) {
baselineLayout(label, start - label.size.width);
} else {
centerLayout(label, start - label.size.width); centerLayout(label, start - label.size.width);
}
}
if (prefix != null) if (prefix != null)
start -= baselineLayout(prefix, start - prefix.size.width); start -= baselineLayout(prefix, start - prefix.size.width);
if (input != null) if (input != null)
...@@ -1061,7 +1077,11 @@ class _RenderDecoration extends RenderBox { ...@@ -1061,7 +1077,11 @@ class _RenderDecoration extends RenderBox {
start += centerLayout(prefixIcon, start); start += centerLayout(prefixIcon, start);
} }
if (label != null) if (label != null)
if (decoration.alignLabelWithHint) {
baselineLayout(label, start);
} else {
centerLayout(label, start); centerLayout(label, start);
}
if (prefix != null) if (prefix != null)
start += baselineLayout(prefix, start); start += baselineLayout(prefix, start);
if (input != null) if (input != null)
...@@ -1891,6 +1911,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -1891,6 +1911,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
icon: icon, icon: icon,
input: widget.child, input: widget.child,
label: label, label: label,
alignLabelWithHint: decoration.alignLabelWithHint,
hint: hint, hint: hint,
prefix: prefix, prefix: prefix,
suffix: suffix, suffix: suffix,
...@@ -1972,6 +1993,7 @@ class InputDecoration { ...@@ -1972,6 +1993,7 @@ class InputDecoration {
this.border, this.border,
this.enabled = true, this.enabled = true,
this.semanticCounterText, this.semanticCounterText,
this.alignLabelWithHint,
}) : assert(enabled != null), }) : assert(enabled != null),
assert(!(prefix != null && prefixText != null), 'Declaring both prefix and prefixText is not allowed'), 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'), assert(!(suffix != null && suffixText != null), 'Declaring both suffix and suffixText is not allowed'),
...@@ -2018,7 +2040,8 @@ class InputDecoration { ...@@ -2018,7 +2040,8 @@ class InputDecoration {
focusedErrorBorder = null, focusedErrorBorder = null,
disabledBorder = null, disabledBorder = null,
enabledBorder = null, enabledBorder = null,
semanticCounterText = null; semanticCounterText = null,
alignLabelWithHint = false;
/// An icon to show before the input field and outside of the decoration's /// An icon to show before the input field and outside of the decoration's
/// container. /// container.
...@@ -2449,6 +2472,13 @@ class InputDecoration { ...@@ -2449,6 +2472,13 @@ class InputDecoration {
/// If provided, this replaces the semantic label of the [counterText]. /// If provided, this replaces the semantic label of the [counterText].
final String semanticCounterText; 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 /// Creates a copy of this input decoration with the given fields replaced
/// by the new values. /// by the new values.
/// ///
...@@ -2488,6 +2518,7 @@ class InputDecoration { ...@@ -2488,6 +2518,7 @@ class InputDecoration {
InputBorder border, InputBorder border,
bool enabled, bool enabled,
String semanticCounterText, String semanticCounterText,
bool alignLabelWithHint,
}) { }) {
return InputDecoration( return InputDecoration(
icon: icon ?? this.icon, icon: icon ?? this.icon,
...@@ -2524,6 +2555,7 @@ class InputDecoration { ...@@ -2524,6 +2555,7 @@ class InputDecoration {
border: border ?? this.border, border: border ?? this.border,
enabled: enabled ?? this.enabled, enabled: enabled ?? this.enabled,
semanticCounterText: semanticCounterText ?? this.semanticCounterText, semanticCounterText: semanticCounterText ?? this.semanticCounterText,
alignLabelWithHint: alignLabelWithHint ?? this.alignLabelWithHint,
); );
} }
...@@ -2553,6 +2585,7 @@ class InputDecoration { ...@@ -2553,6 +2585,7 @@ class InputDecoration {
disabledBorder: disabledBorder ?? theme.disabledBorder, disabledBorder: disabledBorder ?? theme.disabledBorder,
enabledBorder: enabledBorder ?? theme.enabledBorder, enabledBorder: enabledBorder ?? theme.enabledBorder,
border: border ?? theme.border, border: border ?? theme.border,
alignLabelWithHint: alignLabelWithHint ?? theme.alignLabelWithHint,
); );
} }
...@@ -2597,7 +2630,8 @@ class InputDecoration { ...@@ -2597,7 +2630,8 @@ class InputDecoration {
&& typedOther.enabledBorder == enabledBorder && typedOther.enabledBorder == enabledBorder
&& typedOther.border == border && typedOther.border == border
&& typedOther.enabled == enabled && typedOther.enabled == enabled
&& typedOther.semanticCounterText == semanticCounterText; && typedOther.semanticCounterText == semanticCounterText
&& typedOther.alignLabelWithHint == alignLabelWithHint;
} }
@override @override
...@@ -2647,6 +2681,7 @@ class InputDecoration { ...@@ -2647,6 +2681,7 @@ class InputDecoration {
border, border,
enabled, enabled,
semanticCounterText, semanticCounterText,
alignLabelWithHint,
), ),
); );
} }
...@@ -2718,6 +2753,8 @@ class InputDecoration { ...@@ -2718,6 +2753,8 @@ class InputDecoration {
description.add('enabled: false'); description.add('enabled: false');
if (semanticCounterText != null) if (semanticCounterText != null)
description.add('semanticCounterText: $semanticCounterText'); description.add('semanticCounterText: $semanticCounterText');
if (alignLabelWithHint != null)
description.add('alignLabelWithHint: $alignLabelWithHint');
return 'InputDecoration(${description.join(', ')})'; return 'InputDecoration(${description.join(', ')})';
} }
} }
...@@ -2759,9 +2796,11 @@ class InputDecorationTheme extends Diagnosticable { ...@@ -2759,9 +2796,11 @@ class InputDecorationTheme extends Diagnosticable {
this.disabledBorder, this.disabledBorder,
this.enabledBorder, this.enabledBorder,
this.border, this.border,
this.alignLabelWithHint = false,
}) : assert(isDense != null), }) : assert(isDense != null),
assert(isCollapsed != null), assert(isCollapsed != null),
assert(filled != null); assert(filled != null),
assert(alignLabelWithHint != null);
/// The style to use for [InputDecoration.labelText] when the label is /// The style to use for [InputDecoration.labelText] when the label is
/// above (i.e., vertically adjacent to) the input field. /// above (i.e., vertically adjacent to) the input field.
...@@ -3020,6 +3059,11 @@ class InputDecorationTheme extends Diagnosticable { ...@@ -3020,6 +3059,11 @@ class InputDecorationTheme extends Diagnosticable {
/// rounded rectangle around the input decorator's container. /// rounded rectangle around the input decorator's container.
final InputBorder border; 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 @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
...@@ -3040,9 +3084,10 @@ class InputDecorationTheme extends Diagnosticable { ...@@ -3040,9 +3084,10 @@ class InputDecorationTheme extends Diagnosticable {
properties.add(DiagnosticsProperty<Color>('fillColor', fillColor, defaultValue: defaultTheme.fillColor)); properties.add(DiagnosticsProperty<Color>('fillColor', fillColor, defaultValue: defaultTheme.fillColor));
properties.add(DiagnosticsProperty<InputBorder>('errorBorder', errorBorder, defaultValue: defaultTheme.errorBorder)); properties.add(DiagnosticsProperty<InputBorder>('errorBorder', errorBorder, defaultValue: defaultTheme.errorBorder));
properties.add(DiagnosticsProperty<InputBorder>('focusedBorder', focusedBorder, defaultValue: defaultTheme.focusedErrorBorder)); 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>('disabledBorder', disabledBorder, defaultValue: defaultTheme.disabledBorder));
properties.add(DiagnosticsProperty<InputBorder>('enabledBorder', enabledBorder, defaultValue: defaultTheme.enabledBorder)); properties.add(DiagnosticsProperty<InputBorder>('enabledBorder', enabledBorder, defaultValue: defaultTheme.enabledBorder));
properties.add(DiagnosticsProperty<InputBorder>('border', border, defaultValue: defaultTheme.border)); properties.add(DiagnosticsProperty<InputBorder>('border', border, defaultValue: defaultTheme.border));
properties.add(DiagnosticsProperty<bool>('alignLabelWithHint', alignLabelWithHint, defaultValue: defaultTheme.alignLabelWithHint));
} }
} }
...@@ -248,6 +248,56 @@ void main() { ...@@ -248,6 +248,56 @@ void main() {
expect(tester.getTopLeft(find.text('label')).dy, 20.0); expect(tester.getTopLeft(find.text('label')).dy, 20.0);
expect(tester.getBottomLeft(find.text('label')).dy, 36.0); expect(tester.getBottomLeft(find.text('label')).dy, 36.0);
expect(getBorderColor(tester), Colors.transparent); 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 // Overall height for this InputDecorator is 40.0dps
...@@ -1552,6 +1602,7 @@ void main() { ...@@ -1552,6 +1602,7 @@ void main() {
filled: true, filled: true,
fillColor: Colors.red, fillColor: Colors.red,
border: InputBorder.none, border: InputBorder.none,
alignLabelWithHint: true,
) )
); );
...@@ -1567,6 +1618,7 @@ void main() { ...@@ -1567,6 +1618,7 @@ void main() {
expect(decoration.filled, true); expect(decoration.filled, true);
expect(decoration.fillColor, Colors.red); expect(decoration.fillColor, Colors.red);
expect(decoration.border, InputBorder.none); expect(decoration.border, InputBorder.none);
expect(decoration.alignLabelWithHint, true);
// InputDecoration (baseDecoration) defines InputDecoration properties // InputDecoration (baseDecoration) defines InputDecoration properties
decoration = const InputDecoration( decoration = const InputDecoration(
...@@ -1582,6 +1634,7 @@ void main() { ...@@ -1582,6 +1634,7 @@ void main() {
filled: false, filled: false,
fillColor: Colors.blue, fillColor: Colors.blue,
border: OutlineInputBorder(), border: OutlineInputBorder(),
alignLabelWithHint: false,
).applyDefaults( ).applyDefaults(
const InputDecorationTheme( const InputDecorationTheme(
labelStyle: themeStyle, labelStyle: themeStyle,
...@@ -1597,6 +1650,7 @@ void main() { ...@@ -1597,6 +1650,7 @@ void main() {
filled: true, filled: true,
fillColor: Colors.red, fillColor: Colors.red,
border: InputBorder.none, border: InputBorder.none,
alignLabelWithHint: true,
), ),
); );
...@@ -1613,6 +1667,7 @@ void main() { ...@@ -1613,6 +1667,7 @@ void main() {
expect(decoration.filled, false); expect(decoration.filled, false);
expect(decoration.fillColor, Colors.blue); expect(decoration.fillColor, Colors.blue);
expect(decoration.border, const OutlineInputBorder()); expect(decoration.border, const OutlineInputBorder());
expect(decoration.alignLabelWithHint, false);
}); });
testWidgets('InputDecorator OutlineInputBorder fillColor is clipped by border', (WidgetTester tester) async { testWidgets('InputDecorator OutlineInputBorder fillColor is clipped by border', (WidgetTester tester) async {
...@@ -2017,4 +2072,51 @@ void main() { ...@@ -2017,4 +2072,51 @@ void main() {
expect(underlineInputBorder.hashCode, const UnderlineInputBorder(borderSide: BorderSide(color: Colors.blue)).hashCode); expect(underlineInputBorder.hashCode, const UnderlineInputBorder(borderSide: BorderSide(color: Colors.blue)).hashCode);
expect(underlineInputBorder.hashCode, isNot(const UnderlineInputBorder().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