Fix collapsed InputDecorator minimum height (#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 https://github.com/flutter/flutter/issues/150763

## Tests

Adds 4 tests, updates 2.
This commit is contained in:
Bruno Leroux 2024-06-25 22:52:08 +02:00 committed by GitHub
parent 8334a3172c
commit 8950c26f2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 123 additions and 31 deletions

View file

@ -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) {
@ -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;
@ -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
@ -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;
@ -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
@ -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;

View file

@ -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,
@ -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();