Skip to content

Commit

Permalink
Fix collapsed InputDecorator minimum height (#150770)
Browse files Browse the repository at this point in the history
## 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/flutter#150763

## Tests

Adds 4 tests, updates 2.
  • Loading branch information
bleroux authored Jun 25, 2024
1 parent 8334a31 commit 8950c26
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 31 deletions.
19 changes: 9 additions & 10 deletions packages/flutter/lib/src/material/input_decorator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,8 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
return !decoration.isCollapsed && decoration.border.isOutline;
}

Offset get _densityOffset => decoration.visualDensity.baseSizeAdjustment;

@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (icon != null) {
Expand Down Expand Up @@ -1025,9 +1027,8 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
// The height of the input needs to accommodate label above and counter and
// helperError below, when they exist.
final double bottomHeight = subtextSize?.bottomHeight ?? 0.0;
final Offset densityOffset = decoration.visualDensity.baseSizeAdjustment;
final BoxConstraints inputConstraints = boxConstraints
.deflate(EdgeInsets.only(top: contentPadding.vertical + topHeight + bottomHeight + densityOffset.dy))
.deflate(EdgeInsets.only(top: contentPadding.vertical + topHeight + bottomHeight + _densityOffset.dy))
.tighten(width: inputWidth);

final RenderBox? input = this.input;
Expand Down Expand Up @@ -1067,10 +1068,10 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
+ inputHeight
+ fixBelowInput
+ contentPadding.bottom
+ densityOffset.dy,
+ _densityOffset.dy,
);
final double minContainerHeight = decoration.isDense! || decoration.isCollapsed || expands
? 0.0
? inputHeight
: kMinInteractiveDimension;
final double maxContainerHeight = math.max(0.0, boxConstraints.maxHeight - bottomHeight);
final double containerHeight = expands
Expand Down Expand Up @@ -1101,8 +1102,8 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
+ inputInternalBaseline
+ baselineAdjustment
+ interactiveAdjustment
+ densityOffset.dy / 2.0;
final double maxContentHeight = containerHeight - contentPadding.vertical - topHeight - densityOffset.dy;
+ _densityOffset.dy / 2.0;
final double maxContentHeight = containerHeight - contentPadding.vertical - topHeight - _densityOffset.dy;
final double alignableHeight = fixAboveInput + inputHeight + fixBelowInput;
final double maxVerticalOffset = maxContentHeight - alignableHeight;

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

final Offset densityOffset = decoration.visualDensity.baseSizeAdjustment;
final double contentHeight = contentPadding.top
+ (label == null ? 0.0 : decoration.floatingLabelHeight)
+ inputMaxHeight
+ contentPadding.bottom
+ densityOffset.dy;
+ _densityOffset.dy;
final double containerHeight = <double>[iconHeight, contentHeight, prefixIconHeight, suffixIconHeight].reduce(math.max);
final double minContainerHeight = decoration.isDense! || expands
? 0.0
Expand Down Expand Up @@ -1491,8 +1491,7 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
// Temporary opt-in fix for https://github.com/flutter/flutter/issues/54028
// Center the scaled label relative to the border.
final double outlinedFloatingY = (-labelHeight * _kFinalLabelScale) / 2.0 + borderWeight / 2.0;
final Offset densityOffset = decoration.visualDensity.baseSizeAdjustment;
final double floatingY = isOutlineBorder ? outlinedFloatingY : contentPadding.top + densityOffset.dy / 2;
final double floatingY = isOutlineBorder ? outlinedFloatingY : contentPadding.top + _densityOffset.dy / 2;
final double scale = lerpDouble(1.0, _kFinalLabelScale, t)!;
final double centeredFloatX = _boxParentData(container!).offset.dx +
_boxSize(container).width / 2.0 - floatWidth / 2.0;
Expand Down
135 changes: 114 additions & 21 deletions packages/flutter/test/material/input_decorator_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6463,6 +6463,120 @@ void main() {
});
});

group('Material3 - InputDecoration collapsed', () {
// Overall height for a collapsed InputDecorator is 24dp which is the input
// height (font size = 16, line height = 1.5).
const double inputHeight = 24.0;

testWidgets('Decoration height is set to input height on mobile', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(
decoration: const InputDecoration.collapsed(
hintText: hintText,
),
),
);

expect(getDecoratorRect(tester).size, const Size(800.0, inputHeight));
expect(getInputRect(tester).height, inputHeight);
expect(getInputRect(tester).top, 0.0);
expect(getHintOpacity(tester), 0.0);

// The hint should appear.
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
isFocused: true,
decoration: const InputDecoration.collapsed(
hintText: hintText,
),
),
);
await tester.pumpAndSettle();

expect(getDecoratorRect(tester).size, const Size(800.0, inputHeight));
expect(getInputRect(tester).height, inputHeight);
expect(getInputRect(tester).top, 0.0);
expect(getHintOpacity(tester), 1.0);
expect(getHintRect(tester).height, inputHeight);
expect(getHintRect(tester).top, 0.0);
}, variant: TargetPlatformVariant.mobile());

testWidgets('Decoration height is set to input height on desktop', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/150763.
await tester.pumpWidget(
buildInputDecorator(
decoration: const InputDecoration.collapsed(
hintText: hintText,
),
),
);

expect(getDecoratorRect(tester).size, const Size(800.0, inputHeight));
expect(getInputRect(tester).height, inputHeight);
expect(getInputRect(tester).top, 0.0);
expect(getHintOpacity(tester), 0.0);

// The hint should appear.
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
isFocused: true,
decoration: const InputDecoration.collapsed(
hintText: hintText,
),
),
);
await tester.pumpAndSettle();

expect(getDecoratorRect(tester).size, const Size(800.0, inputHeight));
expect(getInputRect(tester).height, inputHeight);
expect(getInputRect(tester).top, 0.0);
expect(getHintOpacity(tester), 1.0);
expect(getHintRect(tester).height, inputHeight);
expect(getHintRect(tester).top, 0.0);
}, variant: TargetPlatformVariant.desktop());

testWidgets('InputDecoration.collapsed defaults to no border', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(
decoration: const InputDecoration.collapsed(
hintText: hintText,
),
),
);

expect(getBorderWeight(tester), 0.0);
});

test('InputDecorationTheme.isCollapsed is applied', () {
final InputDecoration decoration = const InputDecoration(
hintText: 'Hello, Flutter!',
).applyDefaults(const InputDecorationTheme(
isCollapsed: true,
));

expect(decoration.isCollapsed, true);
});

test('InputDecorationTheme.isCollapsed defaults to false', () {
final InputDecoration decoration = const InputDecoration(
hintText: 'Hello, Flutter!',
).applyDefaults(const InputDecorationTheme());

expect(decoration.isCollapsed, false);
});

test('InputDecorationTheme.isCollapsed can be overriden', () {
final InputDecoration decoration = const InputDecoration(
isCollapsed: true,
hintText: 'Hello, Flutter!',
).applyDefaults(const InputDecorationTheme());

expect(decoration.isCollapsed, true);
});
});

testWidgets('InputDecorator counter text, widget, and null', (WidgetTester tester) async {
Widget buildFrame({
InputCounterWidgetBuilder? buildCounter,
Expand Down Expand Up @@ -7482,27 +7596,6 @@ void main() {
expect(tester.takeException(), isNull);
});

group('isCollapsed parameter works with themeData', () {
test('parameter is provided in InputDecorationTheme', () {
final InputDecoration decoration = const InputDecoration(
hintText: 'Hello, Flutter!',
).applyDefaults(const InputDecorationTheme(
isCollapsed: true,
));

expect(decoration.isCollapsed, true);
});

test('parameter is provided in InputDecoration', () {
final InputDecoration decoration = const InputDecoration(
isCollapsed: true,
hintText: 'Hello, Flutter!',
).applyDefaults(const InputDecorationTheme());

expect(decoration.isCollapsed, true);
});
});

testWidgets('Ensure the height of labelStyle remains unchanged when TextField is focused', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/141448.
final FocusNode focusNode = FocusNode();
Expand Down

0 comments on commit 8950c26

Please sign in to comment.