Make InputDecorator start padding compliant with M3 spec (#162157)

## Description

This PR makes InputDecorator padding for the input content, the helper
and the counter compliant with the M3 spec.
The padding should be 16 pixels instead of 12 pixels (which is the
current value).

See
https://m3.material.io/components/text-fields/specs#0d36c3fe-7948-4ec2-ab8a-4fe39cca19cc
for filled text fields and
https://m3.material.io/components/text-fields/specs#605e24f1-1c1f-4c00-b385-4bf50733a5ef
for outlined text fields.

### Before:

The paddings for the input content, the helper and the counter are not
compliant with the M3 spec (12 pixels instead of 16 pixels):


![image](https://github.com/user-attachments/assets/fe74de74-6a6d-4a28-9574-a28f3e5c6084)


### After:

The paddings for the input content, the helper and the counter are
compliant with the M3 spec (16 pixels):


![image](https://github.com/user-attachments/assets/602554da-dc55-4c24-b7af-1c4951a301e9)


## Related Issue

Fixes [Outlined TextField Label start position doesn't meet Material
Design Specs](https://github.com/flutter/flutter/issues/67707)

## Tests

Adds 8 tests.
Updates several tests.
This commit is contained in:
Bruno Leroux 2025-04-22 22:26:27 +02:00 committed by GitHub
parent cb3fd95ff6
commit 69c5526b23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 235 additions and 65 deletions

View File

@ -48,7 +48,7 @@ const double _kMinimumWidth = 112.0;
const double _kDefaultHorizontalPadding = 12.0; const double _kDefaultHorizontalPadding = 12.0;
const double _kLeadingIconToInputPadding = 4.0; const double _kInputStartGap = 4.0;
/// Defines a [DropdownMenu] menu button that represents one item view in the menu. /// Defines a [DropdownMenu] menu button that represents one item view in the menu.
/// ///
@ -641,12 +641,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
return; return;
} }
setState(() { setState(() {
final double? leadingWidgetWidth = getWidth(_leadingKey); leadingPadding = getWidth(_leadingKey);
if (leadingWidgetWidth != null) {
leadingPadding = leadingWidgetWidth + _kLeadingIconToInputPadding;
} else {
leadingPadding = leadingWidgetWidth;
}
}); });
}, debugLabel: 'DropdownMenu.refreshLeadingPadding'); }, debugLabel: 'DropdownMenu.refreshLeadingPadding');
} }
@ -718,7 +713,9 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
int? focusedIndex, int? focusedIndex,
bool enableScrollToHighlight = true, bool enableScrollToHighlight = true,
bool excludeSemantics = false, bool excludeSemantics = false,
bool? useMaterial3,
}) { }) {
final double effectiveInputStartGap = useMaterial3 ?? false ? _kInputStartGap : 0.0;
final List<Widget> result = <Widget>[]; final List<Widget> result = <Widget>[];
for (int i = 0; i < filteredEntries.length; i++) { for (int i = 0; i < filteredEntries.length; i++) {
final DropdownMenuEntry<T> entry = filteredEntries[i]; final DropdownMenuEntry<T> entry = filteredEntries[i];
@ -735,14 +732,9 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
: _kDefaultHorizontalPadding; : _kDefaultHorizontalPadding;
ButtonStyle effectiveStyle = ButtonStyle effectiveStyle =
entry.style ?? entry.style ??
switch (textDirection) { MenuItemButton.styleFrom(
TextDirection.rtl => MenuItemButton.styleFrom( padding: EdgeInsetsDirectional.only(start: padding, end: _kDefaultHorizontalPadding),
padding: EdgeInsets.only(left: _kDefaultHorizontalPadding, right: padding), );
),
TextDirection.ltr => MenuItemButton.styleFrom(
padding: EdgeInsets.only(left: padding, right: _kDefaultHorizontalPadding),
),
};
final ButtonStyle? themeStyle = MenuButtonTheme.of(context).style; final ButtonStyle? themeStyle = MenuButtonTheme.of(context).style;
@ -797,7 +789,8 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
Widget label = entry.labelWidget ?? Text(entry.label); Widget label = entry.labelWidget ?? Text(entry.label);
if (widget.width != null) { if (widget.width != null) {
final double horizontalPadding = padding + _kDefaultHorizontalPadding; final double horizontalPadding =
padding + _kDefaultHorizontalPadding + effectiveInputStartGap;
label = ConstrainedBox( label = ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.width! - horizontalPadding), constraints: BoxConstraints(maxWidth: widget.width! - horizontalPadding),
child: label, child: label,
@ -809,13 +802,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
child: MenuItemButton( child: MenuItemButton(
key: enableScrollToHighlight ? buttonItemKeys[i] : null, key: enableScrollToHighlight ? buttonItemKeys[i] : null,
style: effectiveStyle, style: effectiveStyle,
leadingIcon: leadingIcon: entry.leadingIcon,
entry.leadingIcon != null
? Padding(
padding: const EdgeInsetsDirectional.only(end: _kLeadingIconToInputPadding),
child: entry.leadingIcon,
)
: null,
trailingIcon: entry.trailingIcon, trailingIcon: entry.trailingIcon,
closeOnActivate: widget.closeBehavior == DropdownMenuCloseBehavior.all, closeOnActivate: widget.closeBehavior == DropdownMenuCloseBehavior.all,
onPressed: onPressed:
@ -834,8 +821,16 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
} }
: null, : null,
requestFocusOnHover: false, requestFocusOnHover: false,
// MenuItemButton implementation is based on M3 spec for menu which specifies a
// horizontal padding of 12 pixels.
// In the context of DropdownMenu the M3 spec specifies that the menu item and the text
// field content should be aligned. The text field has a horizontal padding of 16 pixels.
// To conform with the 16 pixels padding, a 4 pixels padding is added in front of the item label.
child: Padding(
padding: EdgeInsetsDirectional.only(start: effectiveInputStartGap),
child: label, child: label,
), ),
),
); );
result.add(menuItemButton); result.add(menuItemButton);
} }
@ -924,6 +919,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool useMaterial3 = Theme.of(context).useMaterial3;
final TextDirection textDirection = Directionality.of(context); final TextDirection textDirection = Directionality.of(context);
_initialMenu ??= _buildButtons( _initialMenu ??= _buildButtons(
widget.dropdownMenuEntries, widget.dropdownMenuEntries,
@ -931,6 +927,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
enableScrollToHighlight: false, enableScrollToHighlight: false,
// The _initialMenu is invisible, we should not add semantics nodes to it // The _initialMenu is invisible, we should not add semantics nodes to it
excludeSemantics: true, excludeSemantics: true,
useMaterial3: useMaterial3,
); );
final DropdownMenuThemeData theme = DropdownMenuTheme.of(context); final DropdownMenuThemeData theme = DropdownMenuTheme.of(context);
final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context); final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context);
@ -963,6 +960,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
filteredEntries, filteredEntries,
textDirection, textDirection,
focusedIndex: currentHighlight, focusedIndex: currentHighlight,
useMaterial3: useMaterial3,
); );
final TextStyle? effectiveTextStyle = widget.textStyle ?? theme.textStyle ?? defaults.textStyle; final TextStyle? effectiveTextStyle = widget.textStyle ?? theme.textStyle ?? defaults.textStyle;

View File

@ -38,6 +38,13 @@ const Duration _kTransitionDuration = Duration(milliseconds: 167);
const Curve _kTransitionCurve = Curves.fastOutSlowIn; const Curve _kTransitionCurve = Curves.fastOutSlowIn;
const double _kFinalLabelScale = 0.75; const double _kFinalLabelScale = 0.75;
// From the M3 spec, horizontal padding is 12 pixels for the prefix icon and
// 16 pixels for the input content.
// InputDecorator default padding is set to 12 pixels because 16 pixels will move
// the prefix icon too far.
// An extra padding should be added for the input content to comply with the 16 pixels padding.
const double _kInputExtraPadding = 4.0;
typedef _SubtextSize = ({double ascent, double bottomHeight, double subtextHeight}); typedef _SubtextSize = ({double ascent, double bottomHeight, double subtextHeight});
typedef _ChildBaselineGetter = double Function(RenderBox child, BoxConstraints constraints); typedef _ChildBaselineGetter = double Function(RenderBox child, BoxConstraints constraints);
@ -565,6 +572,7 @@ class _Decoration {
required this.isDense, required this.isDense,
required this.isEmpty, required this.isEmpty,
required this.visualDensity, required this.visualDensity,
required this.inputGap,
required this.maintainHintSize, required this.maintainHintSize,
this.icon, this.icon,
this.input, this.input,
@ -590,6 +598,7 @@ class _Decoration {
final bool? isDense; final bool? isDense;
final bool isEmpty; final bool isEmpty;
final VisualDensity visualDensity; final VisualDensity visualDensity;
final double inputGap;
final bool maintainHintSize; final bool maintainHintSize;
final Widget? icon; final Widget? icon;
final Widget? input; final Widget? input;
@ -623,6 +632,7 @@ class _Decoration {
other.isDense == isDense && other.isDense == isDense &&
other.isEmpty == isEmpty && other.isEmpty == isEmpty &&
other.visualDensity == visualDensity && other.visualDensity == visualDensity &&
other.inputGap == inputGap &&
other.maintainHintSize == maintainHintSize && other.maintainHintSize == maintainHintSize &&
other.icon == icon && other.icon == icon &&
other.input == input && other.input == input &&
@ -649,6 +659,7 @@ class _Decoration {
isDense, isDense,
isEmpty, isEmpty,
visualDensity, visualDensity,
inputGap,
maintainHintSize, maintainHintSize,
icon, icon,
input, input,
@ -657,8 +668,7 @@ class _Decoration {
prefix, prefix,
suffix, suffix,
prefixIcon, prefixIcon,
suffixIcon, Object.hash(suffixIcon, helperError, counter, container),
Object.hash(helperError, counter, container),
); );
} }
@ -967,10 +977,14 @@ class _RenderDecoration extends RenderBox
start: start:
iconWidth + iconWidth +
prefixSize.width + prefixSize.width +
(prefixIcon == null ? contentPadding.start : prefixIconSize.width + prefixToInputGap), (prefixIcon == null
? contentPadding.start + decoration.inputGap
: prefixIconSize.width + prefixToInputGap),
end: end:
suffixSize.width + suffixSize.width +
(suffixIcon == null ? contentPadding.end : suffixIconSize.width + inputToSuffixGap), (suffixIcon == null
? contentPadding.end + decoration.inputGap
: suffixIconSize.width + inputToSuffixGap),
); );
final double inputWidth = math.max( final double inputWidth = math.max(
@ -987,7 +1001,11 @@ class _RenderDecoration extends RenderBox
final double labelWidth = math.max( final double labelWidth = math.max(
0.0, 0.0,
constraints.maxWidth - constraints.maxWidth -
(iconWidth + contentPadding.horizontal + prefixIconSize.width + suffixIconSpace), (decoration.inputGap * 2 +
iconWidth +
contentPadding.horizontal +
prefixIconSize.width +
suffixIconSpace),
); );
// Increase the available width for the label when it is scaled down. // Increase the available width for the label when it is scaled down.
@ -1168,13 +1186,13 @@ class _RenderDecoration extends RenderBox
? math.max(_minWidth(input, height), _minWidth(hint, height)) ? math.max(_minWidth(input, height), _minWidth(hint, height))
: _minWidth(input, height); : _minWidth(input, height);
return _minWidth(icon, height) + return _minWidth(icon, height) +
(prefixIcon != null ? prefixToInputGap : contentPadding.start) + (prefixIcon != null ? prefixToInputGap : contentPadding.start + decoration.inputGap) +
_minWidth(prefixIcon, height) + _minWidth(prefixIcon, height) +
_minWidth(prefix, height) + _minWidth(prefix, height) +
contentWidth + contentWidth +
_minWidth(suffix, height) + _minWidth(suffix, height) +
_minWidth(suffixIcon, height) + _minWidth(suffixIcon, height) +
(suffixIcon != null ? inputToSuffixGap : contentPadding.end); (suffixIcon != null ? inputToSuffixGap : contentPadding.end + decoration.inputGap);
} }
@override @override
@ -1184,13 +1202,13 @@ class _RenderDecoration extends RenderBox
? math.max(_maxWidth(input, height), _maxWidth(hint, height)) ? math.max(_maxWidth(input, height), _maxWidth(hint, height))
: _maxWidth(input, height); : _maxWidth(input, height);
return _maxWidth(icon, height) + return _maxWidth(icon, height) +
(prefixIcon != null ? prefixToInputGap : contentPadding.start) + (prefixIcon != null ? prefixToInputGap : contentPadding.start + decoration.inputGap) +
_maxWidth(prefixIcon, height) + _maxWidth(prefixIcon, height) +
_maxWidth(prefix, height) + _maxWidth(prefix, height) +
contentWidth + contentWidth +
_maxWidth(suffix, height) + _maxWidth(suffix, height) +
_maxWidth(suffixIcon, height) + _maxWidth(suffixIcon, height) +
(suffixIcon != null ? inputToSuffixGap : contentPadding.end); (suffixIcon != null ? inputToSuffixGap : contentPadding.end + decoration.inputGap);
} }
double _lineHeight(double width, List<RenderBox?> boxes) { double _lineHeight(double width, List<RenderBox?> boxes) {
@ -1375,10 +1393,13 @@ class _RenderDecoration extends RenderBox
case TextDirection.ltr: case TextDirection.ltr:
start = contentPadding.start + _boxSize(icon).width; start = contentPadding.start + _boxSize(icon).width;
end = overallWidth - contentPadding.end; end = overallWidth - contentPadding.end;
_boxParentData(helperError).offset = Offset(start, subtextBaseline - helperErrorBaseline); _boxParentData(helperError).offset = Offset(
start + decoration.inputGap,
subtextBaseline - helperErrorBaseline,
);
if (counter != null) { if (counter != null) {
_boxParentData(counter).offset = Offset( _boxParentData(counter).offset = Offset(
end - counter.size.width, end - counter.size.width - decoration.inputGap,
subtextBaseline - counterBaseline, subtextBaseline - counterBaseline,
); );
} }
@ -1386,11 +1407,14 @@ class _RenderDecoration extends RenderBox
start = overallWidth - contentPadding.start - _boxSize(icon).width; start = overallWidth - contentPadding.start - _boxSize(icon).width;
end = contentPadding.end; end = contentPadding.end;
_boxParentData(helperError).offset = Offset( _boxParentData(helperError).offset = Offset(
start - helperError.size.width, start - helperError.size.width - decoration.inputGap,
subtextBaseline - helperErrorBaseline, subtextBaseline - helperErrorBaseline,
); );
if (counter != null) { if (counter != null) {
_boxParentData(counter).offset = Offset(end, subtextBaseline - counterBaseline); _boxParentData(counter).offset = Offset(
end + decoration.inputGap,
subtextBaseline - counterBaseline,
);
} }
} }
@ -1410,6 +1434,8 @@ class _RenderDecoration extends RenderBox
start += contentPadding.start; start += contentPadding.start;
start -= centerLayout(prefixIcon!, start - prefixIcon!.size.width); start -= centerLayout(prefixIcon!, start - prefixIcon!.size.width);
start -= prefixToInputGap; start -= prefixToInputGap;
} else {
start -= decoration.inputGap;
} }
if (label != null) { if (label != null) {
if (decoration.alignLabelWithHint) { if (decoration.alignLabelWithHint) {
@ -1431,6 +1457,8 @@ class _RenderDecoration extends RenderBox
end -= contentPadding.end; end -= contentPadding.end;
end += centerLayout(suffixIcon!, end); end += centerLayout(suffixIcon!, end);
end += inputToSuffixGap; end += inputToSuffixGap;
} else {
end += decoration.inputGap;
} }
if (suffix != null) { if (suffix != null) {
end += baselineLayout(suffix!, end); end += baselineLayout(suffix!, end);
@ -1443,6 +1471,8 @@ class _RenderDecoration extends RenderBox
start -= contentPadding.start; start -= contentPadding.start;
start += centerLayout(prefixIcon!, start); start += centerLayout(prefixIcon!, start);
start += prefixToInputGap; start += prefixToInputGap;
} else {
start += decoration.inputGap;
} }
if (label != null) { if (label != null) {
if (decoration.alignLabelWithHint) { if (decoration.alignLabelWithHint) {
@ -1464,6 +1494,8 @@ class _RenderDecoration extends RenderBox
end += contentPadding.end; end += contentPadding.end;
end -= centerLayout(suffixIcon!, end - suffixIcon!.size.width); end -= centerLayout(suffixIcon!, end - suffixIcon!.size.width);
end -= inputToSuffixGap; end -= inputToSuffixGap;
} else {
end -= decoration.inputGap;
} }
if (suffix != null) { if (suffix != null) {
end -= baselineLayout(suffix!, end - suffix!.size.width); end -= baselineLayout(suffix!, end - suffix!.size.width);
@ -2258,10 +2290,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context); final ThemeData themeData = Theme.of(context);
final VisualDensity visualDensity = decoration.visualDensity ?? themeData.visualDensity; final VisualDensity visualDensity = decoration.visualDensity ?? themeData.visualDensity;
final bool useMaterial3 = Theme.of(context).useMaterial3;
final InputDecorationTheme defaults = final InputDecorationTheme defaults =
themeData.useMaterial3 useMaterial3 ? _InputDecoratorDefaultsM3(context) : _InputDecoratorDefaultsM2(context);
? _InputDecoratorDefaultsM3(context)
: _InputDecoratorDefaultsM2(context);
final InputDecorationTheme inputDecorationTheme = themeData.inputDecorationTheme; final InputDecorationTheme inputDecorationTheme = themeData.inputDecorationTheme;
final IconButtonThemeData iconButtonTheme = IconButtonTheme.of(context); final IconButtonThemeData iconButtonTheme = IconButtonTheme.of(context);
@ -2551,7 +2582,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
if (decoration.filled ?? false) { if (decoration.filled ?? false) {
contentPadding = contentPadding =
decorationContentPadding ?? decorationContentPadding ??
(Theme.of(context).useMaterial3 (useMaterial3
? decorationIsDense ? decorationIsDense
? const EdgeInsetsDirectional.fromSTEB(12.0, 4.0, 12.0, 4.0) ? const EdgeInsetsDirectional.fromSTEB(12.0, 4.0, 12.0, 4.0)
: const EdgeInsetsDirectional.fromSTEB(12.0, 8.0, 12.0, 8.0) : const EdgeInsetsDirectional.fromSTEB(12.0, 8.0, 12.0, 8.0)
@ -2564,7 +2595,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
// the most noticeable layout change introduced by #13734. // the most noticeable layout change introduced by #13734.
contentPadding = contentPadding =
decorationContentPadding ?? decorationContentPadding ??
(Theme.of(context).useMaterial3 (useMaterial3
? decorationIsDense ? decorationIsDense
? const EdgeInsetsDirectional.fromSTEB(0.0, 4.0, 0.0, 4.0) ? const EdgeInsetsDirectional.fromSTEB(0.0, 4.0, 0.0, 4.0)
: const EdgeInsetsDirectional.fromSTEB(0.0, 8.0, 0.0, 8.0) : const EdgeInsetsDirectional.fromSTEB(0.0, 8.0, 0.0, 8.0)
@ -2576,7 +2607,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
floatingLabelHeight = 0.0; floatingLabelHeight = 0.0;
contentPadding = contentPadding =
decorationContentPadding ?? decorationContentPadding ??
(Theme.of(context).useMaterial3 (useMaterial3
? decorationIsDense ? decorationIsDense
? const EdgeInsetsDirectional.fromSTEB(12.0, 16.0, 12.0, 8.0) ? const EdgeInsetsDirectional.fromSTEB(12.0, 16.0, 12.0, 8.0)
: const EdgeInsetsDirectional.fromSTEB(12.0, 20.0, 12.0, 12.0) : const EdgeInsetsDirectional.fromSTEB(12.0, 20.0, 12.0, 12.0)
@ -2585,10 +2616,20 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
: const EdgeInsetsDirectional.fromSTEB(12.0, 24.0, 12.0, 16.0)); : const EdgeInsetsDirectional.fromSTEB(12.0, 24.0, 12.0, 16.0));
} }
double inputGap = 0.0;
if (useMaterial3) {
if (border is OutlineInputBorder) {
inputGap = border.gapPadding;
} else {
inputGap = border.isOutline || (decoration.filled ?? false) ? _kInputExtraPadding : 0.0;
}
}
final _Decorator decorator = _Decorator( final _Decorator decorator = _Decorator(
decoration: _Decoration( decoration: _Decoration(
contentPadding: contentPadding, contentPadding: contentPadding,
isCollapsed: decoration.isCollapsed ?? themeData.inputDecorationTheme.isCollapsed, isCollapsed: decoration.isCollapsed ?? themeData.inputDecorationTheme.isCollapsed,
inputGap: inputGap,
floatingLabelHeight: floatingLabelHeight, floatingLabelHeight: floatingLabelHeight,
floatingLabelAlignment: decoration.floatingLabelAlignment!, floatingLabelAlignment: decoration.floatingLabelAlignment!,
floatingLabelProgress: _floatingLabelAnimation.value, floatingLabelProgress: _floatingLabelAnimation.value,

View File

@ -624,7 +624,7 @@ void main() {
final Finder textField = find.byType(TextField); final Finder textField = find.byType(TextField);
final double anchorWidth = tester.getSize(textField).width; final double anchorWidth = tester.getSize(textField).width;
expect(anchorWidth, closeTo(180.5, 0.1)); expect(anchorWidth, closeTo(184.5, 0.1));
await tester.tap(find.byType(DropdownMenu<TestMenu>)); await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@ -634,7 +634,7 @@ void main() {
.ancestor(of: find.byType(SingleChildScrollView), matching: find.byType(Material)) .ancestor(of: find.byType(SingleChildScrollView), matching: find.byType(Material))
.first; .first;
final double menuWidth = tester.getSize(menuMaterial).width; final double menuWidth = tester.getSize(menuMaterial).width;
expect(menuWidth, closeTo(180.5, 0.1)); expect(menuWidth, closeTo(184.5, 0.1));
// The text field should have same width as the menu // The text field should have same width as the menu
// when the width property is not null. // when the width property is not null.
@ -741,10 +741,14 @@ void main() {
final double width = tester.getSize(find.byType(DropdownMenu<int>)).width; final double width = tester.getSize(find.byType(DropdownMenu<int>)).width;
const double menuEntryPadding = 24.0; // See _kDefaultHorizontalPadding. const double menuEntryPadding = 24.0; // See _kDefaultHorizontalPadding.
const double decorationStartGap = 4.0; // See _kInputStartGap.
const double leadingWidth = 16.0; const double leadingWidth = 16.0;
const double trailingWidth = 56.0; const double trailingWidth = 56.0;
expect(width, entryLabelWidth + leadingWidth + trailingWidth + menuEntryPadding); expect(
width,
entryLabelWidth + leadingWidth + trailingWidth + menuEntryPadding + decorationStartGap,
);
}); });
testWidgets('The width is determined by the label when it is longer than menu entries', ( testWidgets('The width is determined by the label when it is longer than menu entries', (
@ -994,7 +998,6 @@ void main() {
.ancestor(of: find.byType(SingleChildScrollView), matching: find.byType(Padding)) .ancestor(of: find.byType(SingleChildScrollView), matching: find.byType(Padding))
.first; .first;
final Size menuViewSize = tester.getSize(menuView); final Size menuViewSize = tester.getSize(menuView);
expect(menuViewSize.width, closeTo(180.6, 0.1));
expect(menuViewSize.height, equals(304.0)); // 304 = 288 + vertical padding(2 * 8) expect(menuViewSize.height, equals(304.0)); // 304 = 288 + vertical padding(2 * 8)
// Constrains the menu height. // Constrains the menu height.
@ -1011,7 +1014,6 @@ void main() {
.first; .first;
final Size updatedMenuSize = tester.getSize(updatedMenu); final Size updatedMenuSize = tester.getSize(updatedMenu);
expect(updatedMenuSize.width, closeTo(180.6, 0.1));
expect(updatedMenuSize.height, equals(100.0)); expect(updatedMenuSize.height, equals(100.0));
}, },
); );

View File

@ -2286,6 +2286,37 @@ void main() {
group('Material3 - InputDecoration label', () { group('Material3 - InputDecoration label', () {
group('for filled text field', () { group('for filled text field', () {
testWidgets('label and input horizontal positions are M3 compliant in LTR', (
WidgetTester tester,
) async {
await tester.pumpWidget(
buildInputDecorator(
decoration: const InputDecoration(filled: true, labelText: labelText),
),
);
expect(getDecoratorRect(tester).size, const Size(800.0, 56.0));
const double labelAndInputStart = 12.0 + 4.0; // Content left padding + default input gap.
expect(getLabelRect(tester).left, labelAndInputStart);
expect(getInputRect(tester).left, labelAndInputStart);
});
testWidgets('label and input horizontal positions are M3 compliant in RTL', (
WidgetTester tester,
) async {
await tester.pumpWidget(
buildInputDecorator(
decoration: const InputDecoration(filled: true, labelText: labelText),
textDirection: TextDirection.rtl,
),
);
expect(getDecoratorRect(tester).size, const Size(800.0, 56.0));
const double labelAndInputStart = 12.0 + 4.0; // Content left padding + default input gap.
expect(getLabelRect(tester).right, 800.0 - labelAndInputStart);
expect(getInputRect(tester).right, 800.0 - labelAndInputStart);
});
group('when field is enabled', () { group('when field is enabled', () {
testWidgets('label text has correct style', (WidgetTester tester) async { testWidgets('label text has correct style', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
@ -2434,6 +2465,76 @@ void main() {
}); });
group('for outlined text field', () { group('for outlined text field', () {
testWidgets('label and input horizontal positions are M3 compliant in LTR', (
WidgetTester tester,
) async {
await tester.pumpWidget(
buildInputDecorator(
decoration: const InputDecoration(border: OutlineInputBorder(), labelText: labelText),
),
);
expect(getDecoratorRect(tester).size, const Size(800.0, 56.0));
const double labelAndInputStart = 12.0 + 4.0; // Content left padding + default input gap.
expect(getLabelRect(tester).left, labelAndInputStart);
expect(getInputRect(tester).left, labelAndInputStart);
});
testWidgets('label and input horizontal positions are M3 compliant in RTL', (
WidgetTester tester,
) async {
await tester.pumpWidget(
buildInputDecorator(
decoration: const InputDecoration(border: OutlineInputBorder(), labelText: labelText),
textDirection: TextDirection.rtl,
),
);
expect(getDecoratorRect(tester).size, const Size(800.0, 56.0));
const double labelAndInputStart = 12.0 + 4.0; // Content left padding + default input gap.
expect(getLabelRect(tester).right, 800 - labelAndInputStart);
expect(getInputRect(tester).right, 800 - labelAndInputStart);
});
testWidgets('label and input horizontal positions can be adjusted in LTR', (
WidgetTester tester,
) async {
const double customGap = 6.0;
await tester.pumpWidget(
buildInputDecorator(
decoration: const InputDecoration(
border: OutlineInputBorder(gapPadding: customGap),
labelText: labelText,
),
),
);
expect(getDecoratorRect(tester).size, const Size(800.0, 56.0));
const double labelAndInputStart = 12.0 + customGap; // Content left padding + input gap.
expect(getLabelRect(tester).left, labelAndInputStart);
expect(getInputRect(tester).left, labelAndInputStart);
});
testWidgets('label and input horizontal positions can be adjusted in RTL', (
WidgetTester tester,
) async {
const double customGap = 6.0;
await tester.pumpWidget(
buildInputDecorator(
decoration: const InputDecoration(
border: OutlineInputBorder(gapPadding: customGap),
labelText: labelText,
),
textDirection: TextDirection.rtl,
),
);
expect(getDecoratorRect(tester).size, const Size(800.0, 56.0));
const double labelAndInputStart = 12.0 + customGap; // Content left padding + input gap.
expect(getLabelRect(tester).right, 800.0 - labelAndInputStart);
expect(getInputRect(tester).right, 800.0 - labelAndInputStart);
});
group('when field is enabled', () { group('when field is enabled', () {
testWidgets('label text has correct style', (WidgetTester tester) async { testWidgets('label text has correct style', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
@ -2602,6 +2703,35 @@ void main() {
}); });
}); });
testWidgets(
'Label and input for non-filled and non-outlined text field have no horizontal padding in LTR',
(WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(decoration: const InputDecoration(labelText: labelText)),
);
expect(getDecoratorRect(tester).size, const Size(800.0, 56.0));
expect(getLabelRect(tester).left, 0.0);
expect(getInputRect(tester).left, 0.0);
},
);
testWidgets(
'Label and input for non-filled and non-outlined text field have no horizontal padding in RTL',
(WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(
decoration: const InputDecoration(labelText: labelText),
textDirection: TextDirection.rtl,
),
);
expect(getDecoratorRect(tester).size, const Size(800.0, 56.0));
expect(getLabelRect(tester).right, 800.0);
expect(getInputRect(tester).right, 800.0);
},
);
testWidgets('floatingLabelStyle overrides default style', (WidgetTester tester) async { testWidgets('floatingLabelStyle overrides default style', (WidgetTester tester) async {
const TextStyle floatingLabelStyle = TextStyle(color: Colors.indigo, fontSize: 16.0); const TextStyle floatingLabelStyle = TextStyle(color: Colors.indigo, fontSize: 16.0);
@ -2881,43 +3011,43 @@ void main() {
await pumpDecorator(focused: false); await pumpDecorator(focused: false);
await tester.pump(kTransitionDuration); await tester.pump(kTransitionDuration);
const Size labelSize = Size(82.5, 16); const Size labelSize = Size(82.5, 16);
expect(getLabelRect(tester).topLeft, equals(const Offset(12, 20))); expect(getLabelRect(tester).topLeft, equals(const Offset(16, 20)));
expect(getLabelRect(tester).size, equals(labelSize)); expect(getLabelRect(tester).size, equals(labelSize));
await pumpDecorator(focused: false, empty: false); await pumpDecorator(focused: false, empty: false);
await tester.pump(kTransitionDuration); await tester.pump(kTransitionDuration);
expect(getLabelRect(tester).topLeft, equals(const Offset(12, -5.5))); expect(getLabelRect(tester).topLeft, equals(const Offset(16, -5.5)));
expect(getLabelRect(tester).size, equals(labelSize * 0.75)); expect(getLabelRect(tester).size, equals(labelSize * 0.75));
await pumpDecorator(focused: true); await pumpDecorator(focused: true);
await tester.pump(kTransitionDuration); await tester.pump(kTransitionDuration);
expect(getLabelRect(tester).topLeft, equals(const Offset(12, -5.5))); expect(getLabelRect(tester).topLeft, equals(const Offset(16, -5.5)));
expect(getLabelRect(tester).size, equals(labelSize * 0.75)); expect(getLabelRect(tester).size, equals(labelSize * 0.75));
await pumpDecorator(focused: true, empty: false); await pumpDecorator(focused: true, empty: false);
await tester.pump(kTransitionDuration); await tester.pump(kTransitionDuration);
expect(getLabelRect(tester).topLeft, equals(const Offset(12, -5.5))); expect(getLabelRect(tester).topLeft, equals(const Offset(16, -5.5)));
expect(getLabelRect(tester).size, equals(labelSize * 0.75)); expect(getLabelRect(tester).size, equals(labelSize * 0.75));
await pumpDecorator(focused: false, enabled: false); await pumpDecorator(focused: false, enabled: false);
await tester.pump(kTransitionDuration); await tester.pump(kTransitionDuration);
expect(getLabelRect(tester).topLeft, equals(const Offset(12, 20))); expect(getLabelRect(tester).topLeft, equals(const Offset(16, 20)));
expect(getLabelRect(tester).size, equals(labelSize)); expect(getLabelRect(tester).size, equals(labelSize));
await pumpDecorator(focused: false, empty: false, enabled: false); await pumpDecorator(focused: false, empty: false, enabled: false);
await tester.pump(kTransitionDuration); await tester.pump(kTransitionDuration);
expect(getLabelRect(tester).topLeft, equals(const Offset(12, -5.5))); expect(getLabelRect(tester).topLeft, equals(const Offset(16, -5.5)));
expect(getLabelRect(tester).size, equals(labelSize * 0.75)); expect(getLabelRect(tester).size, equals(labelSize * 0.75));
// Focused and disabled happens with NavigationMode.directional. // Focused and disabled happens with NavigationMode.directional.
await pumpDecorator(focused: true, enabled: false); await pumpDecorator(focused: true, enabled: false);
await tester.pump(kTransitionDuration); await tester.pump(kTransitionDuration);
expect(getLabelRect(tester).topLeft, equals(const Offset(12, 20))); expect(getLabelRect(tester).topLeft, equals(const Offset(16, 20)));
expect(getLabelRect(tester).size, equals(labelSize)); expect(getLabelRect(tester).size, equals(labelSize));
await pumpDecorator(focused: true, empty: false, enabled: false); await pumpDecorator(focused: true, empty: false, enabled: false);
await tester.pump(kTransitionDuration); await tester.pump(kTransitionDuration);
expect(getLabelRect(tester).topLeft, equals(const Offset(12, -5.5))); expect(getLabelRect(tester).topLeft, equals(const Offset(16, -5.5)));
expect(getLabelRect(tester).size, equals(labelSize * 0.75)); expect(getLabelRect(tester).size, equals(labelSize * 0.75));
}); });
@ -5128,9 +5258,8 @@ void main() {
const double fullHeight = containerHeight + helperGap + helperHeight; // 76.0 const double fullHeight = containerHeight + helperGap + helperHeight; // 76.0
const double errorHeight = helperHeight; const double errorHeight = helperHeight;
const double hintHeight = inputHeight; const double hintHeight = inputHeight;
// TODO(bleroux): consider changing this padding because, from the M3 specification, it should be 16. const double helperStartPadding = 16.0;
const double helperStartPadding = 12.0; const double counterEndPadding = 16.0;
const double counterEndPadding = 12.0;
group('for filled text field', () { group('for filled text field', () {
group('when field is enabled', () { group('when field is enabled', () {

View File

@ -5529,12 +5529,12 @@ void main() {
), ),
); );
final double iconRight = tester.getTopRight(find.byType(Icon)).dx; final double iconRight = tester.getTopRight(find.byType(Icon)).dx;
// Per https://material.io/go/design-text-fields#text-fields-layout // There's a 16 pixels gap between the right edge of the icon and the text field's
// There's a 16 dps gap between the right edge of the icon and the text field's // container, and, per https://material.io/go/design-text-fields#text-fields-layout,
// container, and the 12dps more padding between the left edge of the container // 16 pixels more padding between the left edge of the container and the left edge
// and the left edge of the input and label. // of the input and label.
expect(iconRight + 28.0, equals(tester.getTopLeft(find.text('label')).dx)); expect(iconRight + 16.0 + 16.0, equals(tester.getTopLeft(find.text('label')).dx));
expect(iconRight + 28.0, equals(tester.getTopLeft(find.byType(EditableText)).dx)); expect(iconRight + 16.0 + 16.0, equals(tester.getTopLeft(find.byType(EditableText)).dx));
}); });
testWidgets('Collapsed hint text placement', (WidgetTester tester) async { testWidgets('Collapsed hint text placement', (WidgetTester tester) async {