Skip to content

Commit f3a4e5b

Browse files
blerouxpull[bot]
authored andcommitted
Fix collapsed InputDecorator minimum height (flutter#150770)
## Description This PR sets a minimal height for collapsed `InputDecoration`. Before this PR the minimum height was 0. On desktop, due to visual density reducing the height by 8 pixels, it leads to a collapsed text field height being too small to fit the input text vertically. The following screenshot shows a default collapsed M3 TextField on macOS. On M3 the font style is 16px with a 1.5 height, so the input height is 24. The decoration height is 16 because of the visual density reduction (this results in the border being misplaced, some letters overflowing and the cursor overflowing). ![image](https://github.com/flutter/flutter/assets/840911/0c854510-9d10-40a7-9a7e-8aa109f418e2) After this PR, the minimum height is the input height. ![image](https://github.com/flutter/flutter/assets/840911/fcc67270-fd19-46ed-a2c2-55406f953e97) ## Related Issue Fixes flutter#150763 ## Tests Adds 4 tests, updates 2.
1 parent c4b0c5b commit f3a4e5b

File tree

2 files changed

+123
-31
lines changed

2 files changed

+123
-31
lines changed

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,8 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
859859
return !decoration.isCollapsed && decoration.border.isOutline;
860860
}
861861

862+
Offset get _densityOffset => decoration.visualDensity.baseSizeAdjustment;
863+
862864
@override
863865
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
864866
if (icon != null) {
@@ -1025,9 +1027,8 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
10251027
// The height of the input needs to accommodate label above and counter and
10261028
// helperError below, when they exist.
10271029
final double bottomHeight = subtextSize?.bottomHeight ?? 0.0;
1028-
final Offset densityOffset = decoration.visualDensity.baseSizeAdjustment;
10291030
final BoxConstraints inputConstraints = boxConstraints
1030-
.deflate(EdgeInsets.only(top: contentPadding.vertical + topHeight + bottomHeight + densityOffset.dy))
1031+
.deflate(EdgeInsets.only(top: contentPadding.vertical + topHeight + bottomHeight + _densityOffset.dy))
10311032
.tighten(width: inputWidth);
10321033

10331034
final RenderBox? input = this.input;
@@ -1067,10 +1068,10 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
10671068
+ inputHeight
10681069
+ fixBelowInput
10691070
+ contentPadding.bottom
1070-
+ densityOffset.dy,
1071+
+ _densityOffset.dy,
10711072
);
10721073
final double minContainerHeight = decoration.isDense! || decoration.isCollapsed || expands
1073-
? 0.0
1074+
? inputHeight
10741075
: kMinInteractiveDimension;
10751076
final double maxContainerHeight = math.max(0.0, boxConstraints.maxHeight - bottomHeight);
10761077
final double containerHeight = expands
@@ -1101,8 +1102,8 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
11011102
+ inputInternalBaseline
11021103
+ baselineAdjustment
11031104
+ interactiveAdjustment
1104-
+ densityOffset.dy / 2.0;
1105-
final double maxContentHeight = containerHeight - contentPadding.vertical - topHeight - densityOffset.dy;
1105+
+ _densityOffset.dy / 2.0;
1106+
final double maxContentHeight = containerHeight - contentPadding.vertical - topHeight - _densityOffset.dy;
11061107
final double alignableHeight = fixAboveInput + inputHeight + fixBelowInput;
11071108
final double maxVerticalOffset = maxContentHeight - alignableHeight;
11081109

@@ -1234,12 +1235,11 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
12341235
final double inputHeight = _lineHeight(availableInputWidth, <RenderBox?>[input, hint]);
12351236
final double inputMaxHeight = <double>[inputHeight, prefixHeight, suffixHeight].reduce(math.max);
12361237

1237-
final Offset densityOffset = decoration.visualDensity.baseSizeAdjustment;
12381238
final double contentHeight = contentPadding.top
12391239
+ (label == null ? 0.0 : decoration.floatingLabelHeight)
12401240
+ inputMaxHeight
12411241
+ contentPadding.bottom
1242-
+ densityOffset.dy;
1242+
+ _densityOffset.dy;
12431243
final double containerHeight = <double>[iconHeight, contentHeight, prefixIconHeight, suffixIconHeight].reduce(math.max);
12441244
final double minContainerHeight = decoration.isDense! || expands
12451245
? 0.0
@@ -1491,8 +1491,7 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
14911491
// Temporary opt-in fix for https://github.com/flutter/flutter/issues/54028
14921492
// Center the scaled label relative to the border.
14931493
final double outlinedFloatingY = (-labelHeight * _kFinalLabelScale) / 2.0 + borderWeight / 2.0;
1494-
final Offset densityOffset = decoration.visualDensity.baseSizeAdjustment;
1495-
final double floatingY = isOutlineBorder ? outlinedFloatingY : contentPadding.top + densityOffset.dy / 2;
1494+
final double floatingY = isOutlineBorder ? outlinedFloatingY : contentPadding.top + _densityOffset.dy / 2;
14961495
final double scale = lerpDouble(1.0, _kFinalLabelScale, t)!;
14971496
final double centeredFloatX = _boxParentData(container!).offset.dx +
14981497
_boxSize(container).width / 2.0 - floatWidth / 2.0;

packages/flutter/test/material/input_decorator_test.dart

Lines changed: 114 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6463,6 +6463,120 @@ void main() {
64636463
});
64646464
});
64656465

6466+
group('Material3 - InputDecoration collapsed', () {
6467+
// Overall height for a collapsed InputDecorator is 24dp which is the input
6468+
// height (font size = 16, line height = 1.5).
6469+
const double inputHeight = 24.0;
6470+
6471+
testWidgets('Decoration height is set to input height on mobile', (WidgetTester tester) async {
6472+
await tester.pumpWidget(
6473+
buildInputDecorator(
6474+
decoration: const InputDecoration.collapsed(
6475+
hintText: hintText,
6476+
),
6477+
),
6478+
);
6479+
6480+
expect(getDecoratorRect(tester).size, const Size(800.0, inputHeight));
6481+
expect(getInputRect(tester).height, inputHeight);
6482+
expect(getInputRect(tester).top, 0.0);
6483+
expect(getHintOpacity(tester), 0.0);
6484+
6485+
// The hint should appear.
6486+
await tester.pumpWidget(
6487+
buildInputDecorator(
6488+
isEmpty: true,
6489+
isFocused: true,
6490+
decoration: const InputDecoration.collapsed(
6491+
hintText: hintText,
6492+
),
6493+
),
6494+
);
6495+
await tester.pumpAndSettle();
6496+
6497+
expect(getDecoratorRect(tester).size, const Size(800.0, inputHeight));
6498+
expect(getInputRect(tester).height, inputHeight);
6499+
expect(getInputRect(tester).top, 0.0);
6500+
expect(getHintOpacity(tester), 1.0);
6501+
expect(getHintRect(tester).height, inputHeight);
6502+
expect(getHintRect(tester).top, 0.0);
6503+
}, variant: TargetPlatformVariant.mobile());
6504+
6505+
testWidgets('Decoration height is set to input height on desktop', (WidgetTester tester) async {
6506+
// Regression test for https://github.com/flutter/flutter/issues/150763.
6507+
await tester.pumpWidget(
6508+
buildInputDecorator(
6509+
decoration: const InputDecoration.collapsed(
6510+
hintText: hintText,
6511+
),
6512+
),
6513+
);
6514+
6515+
expect(getDecoratorRect(tester).size, const Size(800.0, inputHeight));
6516+
expect(getInputRect(tester).height, inputHeight);
6517+
expect(getInputRect(tester).top, 0.0);
6518+
expect(getHintOpacity(tester), 0.0);
6519+
6520+
// The hint should appear.
6521+
await tester.pumpWidget(
6522+
buildInputDecorator(
6523+
isEmpty: true,
6524+
isFocused: true,
6525+
decoration: const InputDecoration.collapsed(
6526+
hintText: hintText,
6527+
),
6528+
),
6529+
);
6530+
await tester.pumpAndSettle();
6531+
6532+
expect(getDecoratorRect(tester).size, const Size(800.0, inputHeight));
6533+
expect(getInputRect(tester).height, inputHeight);
6534+
expect(getInputRect(tester).top, 0.0);
6535+
expect(getHintOpacity(tester), 1.0);
6536+
expect(getHintRect(tester).height, inputHeight);
6537+
expect(getHintRect(tester).top, 0.0);
6538+
}, variant: TargetPlatformVariant.desktop());
6539+
6540+
testWidgets('InputDecoration.collapsed defaults to no border', (WidgetTester tester) async {
6541+
await tester.pumpWidget(
6542+
buildInputDecorator(
6543+
decoration: const InputDecoration.collapsed(
6544+
hintText: hintText,
6545+
),
6546+
),
6547+
);
6548+
6549+
expect(getBorderWeight(tester), 0.0);
6550+
});
6551+
6552+
test('InputDecorationTheme.isCollapsed is applied', () {
6553+
final InputDecoration decoration = const InputDecoration(
6554+
hintText: 'Hello, Flutter!',
6555+
).applyDefaults(const InputDecorationTheme(
6556+
isCollapsed: true,
6557+
));
6558+
6559+
expect(decoration.isCollapsed, true);
6560+
});
6561+
6562+
test('InputDecorationTheme.isCollapsed defaults to false', () {
6563+
final InputDecoration decoration = const InputDecoration(
6564+
hintText: 'Hello, Flutter!',
6565+
).applyDefaults(const InputDecorationTheme());
6566+
6567+
expect(decoration.isCollapsed, false);
6568+
});
6569+
6570+
test('InputDecorationTheme.isCollapsed can be overriden', () {
6571+
final InputDecoration decoration = const InputDecoration(
6572+
isCollapsed: true,
6573+
hintText: 'Hello, Flutter!',
6574+
).applyDefaults(const InputDecorationTheme());
6575+
6576+
expect(decoration.isCollapsed, true);
6577+
});
6578+
});
6579+
64666580
testWidgets('InputDecorator counter text, widget, and null', (WidgetTester tester) async {
64676581
Widget buildFrame({
64686582
InputCounterWidgetBuilder? buildCounter,
@@ -7482,27 +7596,6 @@ void main() {
74827596
expect(tester.takeException(), isNull);
74837597
});
74847598

7485-
group('isCollapsed parameter works with themeData', () {
7486-
test('parameter is provided in InputDecorationTheme', () {
7487-
final InputDecoration decoration = const InputDecoration(
7488-
hintText: 'Hello, Flutter!',
7489-
).applyDefaults(const InputDecorationTheme(
7490-
isCollapsed: true,
7491-
));
7492-
7493-
expect(decoration.isCollapsed, true);
7494-
});
7495-
7496-
test('parameter is provided in InputDecoration', () {
7497-
final InputDecoration decoration = const InputDecoration(
7498-
isCollapsed: true,
7499-
hintText: 'Hello, Flutter!',
7500-
).applyDefaults(const InputDecorationTheme());
7501-
7502-
expect(decoration.isCollapsed, true);
7503-
});
7504-
});
7505-
75067599
testWidgets('Ensure the height of labelStyle remains unchanged when TextField is focused', (WidgetTester tester) async {
75077600
// Regression test for https://github.com/flutter/flutter/issues/141448.
75087601
final FocusNode focusNode = FocusNode();

0 commit comments

Comments
 (0)