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
This commit is contained in:
parent
9b6229ab56
commit
89fa4cc502
@ -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.
|
||||
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)
|
||||
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)
|
||||
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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user