diff --git a/dev/tools/gen_defaults/lib/input_decorator_template.dart b/dev/tools/gen_defaults/lib/input_decorator_template.dart index 0e4c76b7596..a73472f0676 100644 --- a/dev/tools/gen_defaults/lib/input_decorator_template.dart +++ b/dev/tools/gen_defaults/lib/input_decorator_template.dart @@ -21,6 +21,16 @@ class _${blockName}DefaultsM3 extends InputDecorationTheme { late final ColorScheme _colors = Theme.of(context).colorScheme; late final TextTheme _textTheme = Theme.of(context).textTheme; + // For InputDecorator, focused state should take precedence over hovered state. + // For instance, the focused state increases border width (2dp) and applies bright + // colors (primary color or error color) while the hovered state has the same border + // than the non-focused state (1dp) and uses a color a little darker than non-focused + // state. On desktop, it is also very common that a text field is focused and hovered + // because users often rely on mouse selection. + // For other widgets, hovered state takes precedence over focused state, because it + // is mainly used to determine the overlay color, + // see https://github.com/flutter/flutter/pull/125905. + @override TextStyle? get hintStyle => MaterialStateTextStyle.resolveWith((Set states) { if (states.contains(MaterialState.disabled)) { @@ -139,20 +149,20 @@ class _${blockName}DefaultsM3 extends InputDecorationTheme { return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.disabled.label-text')}); } if (states.contains(MaterialState.error)) { - if (states.contains(MaterialState.hovered)) { - return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.error.hover.label-text')}); - } if (states.contains(MaterialState.focused)) { return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.error.focus.label-text')}); } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.error.hover.label-text')}); + } return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.error.label-text')}); } - if (states.contains(MaterialState.hovered)) { - return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.hover.label-text')}); - } if (states.contains(MaterialState.focused)) { return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.focus.label-text')}); } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.hover.label-text')}); + } return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.label-text')}); }); @@ -163,20 +173,20 @@ class _${blockName}DefaultsM3 extends InputDecorationTheme { return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.disabled.label-text')}); } if (states.contains(MaterialState.error)) { - if (states.contains(MaterialState.hovered)) { - return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.error.hover.label-text')}); - } if (states.contains(MaterialState.focused)) { return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.error.focus.label-text')}); } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.error.hover.label-text')}); + } return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.error.label-text')}); } - if (states.contains(MaterialState.hovered)) { - return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.hover.label-text')}); - } if (states.contains(MaterialState.focused)) { return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.focus.label-text')}); } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.hover.label-text')}); + } return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.label-text')}); }); @@ -185,12 +195,12 @@ class _${blockName}DefaultsM3 extends InputDecorationTheme { final TextStyle textStyle = ${textStyle("md.comp.filled-text-field.supporting-text")} ?? const TextStyle(); if (states.contains(MaterialState.disabled)) { return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.disabled.supporting-text')}); - }${componentColor('md.comp.filled-text-field.hover.supporting-text') == componentColor('md.comp.filled-text-field.supporting-text') ? '' : ''' - if (states.contains(MaterialState.hovered)) { - return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.hover.supporting-text')}); - }'''}${componentColor('md.comp.filled-text-field.focus.supporting-text') == componentColor('md.comp.filled-text-field.supporting-text') ? '' : ''' + }${componentColor('md.comp.filled-text-field.focus.supporting-text') == componentColor('md.comp.filled-text-field.supporting-text') ? '' : ''' if (states.contains(MaterialState.focused)) { return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.focus.supporting-text')}); + }'''}${componentColor('md.comp.filled-text-field.hover.supporting-text') == componentColor('md.comp.filled-text-field.supporting-text') ? '' : ''' + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.hover.supporting-text')}); }'''} return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.supporting-text')}); }); @@ -198,11 +208,11 @@ class _${blockName}DefaultsM3 extends InputDecorationTheme { @override TextStyle? get errorStyle => MaterialStateTextStyle.resolveWith((Set states) { final TextStyle textStyle = ${textStyle("md.comp.filled-text-field.supporting-text")} ?? const TextStyle();${componentColor('md.comp.filled-text-field.error.hover.supporting-text') == componentColor('md.comp.filled-text-field.error.supporting-text') ? '' : ''' - if (states.contains(MaterialState.hovered)) { - return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.error.hover.supporting-text')}); - }'''}${componentColor('md.comp.filled-text-field.error.focus.supporting-text') == componentColor('md.comp.filled-text-field.error.supporting-text') ? '' : ''' if (states.contains(MaterialState.focused)) { return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.error.focus.supporting-text')}); + }'''}${componentColor('md.comp.filled-text-field.error.focus.supporting-text') == componentColor('md.comp.filled-text-field.error.supporting-text') ? '' : ''' + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.error.hover.supporting-text')}); }'''} return textStyle.copyWith(color: ${componentColor('md.comp.filled-text-field.error.supporting-text')}); }); diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index c37c55e536f..b9a449f35c0 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -4617,6 +4617,16 @@ class _InputDecoratorDefaultsM3 extends InputDecorationTheme { late final ColorScheme _colors = Theme.of(context).colorScheme; late final TextTheme _textTheme = Theme.of(context).textTheme; + // For InputDecorator, focused state should take precedence over hovered state. + // For instance, the focused state increases border width (2dp) and applies bright + // colors (primary color or error color) while the hovered state has the same border + // than the non-focused state (1dp) and uses a color a little darker than non-focused + // state. On desktop, it is also very common that a text field is focused and hovered + // because users often rely on mouse selection. + // For other widgets, hovered state takes precedence over focused state, because it + // is mainly used to determine the overlay color, + // see https://github.com/flutter/flutter/pull/125905. + @override TextStyle? get hintStyle => MaterialStateTextStyle.resolveWith((Set states) { if (states.contains(MaterialState.disabled)) { @@ -4705,20 +4715,20 @@ class _InputDecoratorDefaultsM3 extends InputDecorationTheme { return textStyle.copyWith(color: _colors.onSurface.withOpacity(0.38)); } if (states.contains(MaterialState.error)) { - if (states.contains(MaterialState.hovered)) { - return textStyle.copyWith(color: _colors.onErrorContainer); - } if (states.contains(MaterialState.focused)) { return textStyle.copyWith(color: _colors.error); } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: _colors.onErrorContainer); + } return textStyle.copyWith(color: _colors.error); } - if (states.contains(MaterialState.hovered)) { - return textStyle.copyWith(color: _colors.onSurfaceVariant); - } if (states.contains(MaterialState.focused)) { return textStyle.copyWith(color: _colors.primary); } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: _colors.onSurfaceVariant); + } return textStyle.copyWith(color: _colors.onSurfaceVariant); }); @@ -4729,20 +4739,20 @@ class _InputDecoratorDefaultsM3 extends InputDecorationTheme { return textStyle.copyWith(color: _colors.onSurface.withOpacity(0.38)); } if (states.contains(MaterialState.error)) { - if (states.contains(MaterialState.hovered)) { - return textStyle.copyWith(color: _colors.onErrorContainer); - } if (states.contains(MaterialState.focused)) { return textStyle.copyWith(color: _colors.error); } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: _colors.onErrorContainer); + } return textStyle.copyWith(color: _colors.error); } - if (states.contains(MaterialState.hovered)) { - return textStyle.copyWith(color: _colors.onSurfaceVariant); - } if (states.contains(MaterialState.focused)) { return textStyle.copyWith(color: _colors.primary); } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: _colors.onSurfaceVariant); + } return textStyle.copyWith(color: _colors.onSurfaceVariant); }); diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 528f64b0704..7061dbf4ce5 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -1634,6 +1634,328 @@ void main() { }); }); + group('Material3 - InputDecoration label', () { + group('for filled text field', () { + group('when field is enabled', () { + testWidgets('label text has correct style', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + decoration: const InputDecoration( + filled: true, + labelText: labelText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + final Color expectedColor = theme.colorScheme.onSurfaceVariant; + // Current input decorator implementation forces line height to 1.0, + // this is not compliant with M3 spec. + final TextStyle expectedStyle = theme.textTheme.bodyLarge!.copyWith(color: expectedColor, height: 1.0); + expect(getLabelStyle(tester), expectedStyle); + }); + }); + + group('when field is disabled', () { + testWidgets('label text has correct color', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + decoration: const InputDecoration( + filled: true, + enabled: false, + labelText: labelText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.onSurface.withOpacity(0.38)); + }); + }); + + group('when field is hovered', () { + testWidgets('label text has correct color', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + isHovering: true, + decoration: const InputDecoration( + filled: true, + labelText: labelText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.onSurfaceVariant); + }); + }); + + group('when field is focused', () { + testWidgets('label text has correct color', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + isFocused: true, + decoration: const InputDecoration( + filled: true, + labelText: labelText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.primary); + }); + + testWidgets('label text has correct color when focused and hovered', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/146565. + await tester.pumpWidget( + buildInputDecorator( + isFocused: true, + isHovering: true, + decoration: const InputDecoration( + filled: true, + labelText: labelText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.primary); + }); + }); + + group('when field is in error', () { + testWidgets('label text has correct color', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + decoration: const InputDecoration( + filled: true, + labelText: labelText, + errorText: errorText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.error); + }); + + testWidgets('label text has correct color when focused', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + isFocused: true, + decoration: const InputDecoration( + filled: true, + labelText: labelText, + errorText: errorText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.error); + }); + + testWidgets('label text has correct style when hovered', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + isHovering: true, + decoration: const InputDecoration( + filled: true, + labelText: labelText, + errorText: errorText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.onErrorContainer); + }); + + testWidgets('label text has correct style when focused and hovered', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/146565. + await tester.pumpWidget( + buildInputDecorator( + isFocused: true, + isHovering: true, + decoration: const InputDecoration( + filled: true, + labelText: labelText, + errorText: errorText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.error); + }); + }); + }); + + group('for outlined text field', () { + group('when field is enabled', () { + testWidgets('label text has correct style', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: labelText, + helperText: helperText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + final Color expectedColor = theme.colorScheme.onSurfaceVariant; + // Current input decorator implementation forces line height to 1.0, + // this is not compliant with M3 spec. + final TextStyle expectedStyle = theme.textTheme.bodyLarge!.copyWith(color: expectedColor, height: 1.0); + expect(getLabelStyle(tester), expectedStyle); + }); + }); + + group('when field is disabled', () { + testWidgets('label text has correct color', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + decoration: const InputDecoration( + border: OutlineInputBorder(), + enabled: false, + labelText: labelText, + helperText: helperText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.onSurface.withOpacity(0.38)); + }); + }); + + group('when field is hovered', () { + testWidgets('label text has correct color', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + isHovering: true, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: labelText, + helperText: helperText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.onSurfaceVariant); + }); + }); + + group('when field is focused', () { + testWidgets('label text has correct color', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + isFocused: true, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: labelText, + helperText: helperText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.primary); + }); + + + testWidgets('label text has correct color when focused and hovered', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/146565. + await tester.pumpWidget( + buildInputDecorator( + isFocused: true, + isHovering: true, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: labelText, + helperText: helperText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.primary); + }); + }); + + group('when field is in error', () { + testWidgets('label text has correct color', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: labelText, + errorText: errorText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.error); + }); + + testWidgets('label text has correct color when focused', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + isFocused: true, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: labelText, + errorText: errorText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.error); + }); + + testWidgets('label text has correct color when hovered', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + isHovering: true, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: labelText, + errorText: errorText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.onErrorContainer); + }); + + testWidgets('label text has correct color when focused and hovered', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/146565. + await tester.pumpWidget( + buildInputDecorator( + isFocused: true, + isHovering: true, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: labelText, + errorText: errorText, + ), + ), + ); + + final ThemeData theme = Theme.of(tester.element(findDecorator())); + expect(getLabelStyle(tester).color, theme.colorScheme.error); + }); + }); + }); + }); + group('Material3 - InputDecoration labelText layout', () { testWidgets('The label appears above input', (WidgetTester tester) async { await tester.pumpWidget( @@ -3957,40 +4279,6 @@ void main() { expect(getIconStyle(tester, Icons.close)?.color, theme.colorScheme.error); }); - testWidgets('InputDecoration default floatingLabelStyle resolves hovered/focused states', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); - addTearDown(focusNode.dispose); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField( - focusNode: focusNode, - decoration: const InputDecoration( - labelText: 'label', - ), - ), - ), - ), - ); - - // Focused. - focusNode.requestFocus(); - await tester.pump(kTransitionDuration); - final ThemeData theme = Theme.of(tester.element(find.byType(TextField))); - expect(getLabelStyle(tester).color, theme.colorScheme.primary); - - // Hovered. - final Offset center = tester.getCenter(find.byType(TextField)); - final TestGesture gesture = await tester.createGesture( - kind: PointerDeviceKind.mouse, - ); - await gesture.addPointer(); - await gesture.moveTo(center); - await tester.pump(kTransitionDuration); - expect(getLabelStyle(tester).color, theme.colorScheme.onSurfaceVariant); - }); - testWidgets('FloatingLabelAlignment.toString()', (WidgetTester tester) async { expect(FloatingLabelAlignment.start.toString(), 'FloatingLabelAlignment.start'); expect(FloatingLabelAlignment.center.toString(), 'FloatingLabelAlignment.center');