Fix label text color is wrong for a focused and hovered TextField (#146572)

Before, hovering a focused TextField would incorrect change the label color. Now it does not, which is correct per the spec.
This commit is contained in:
Bruno Leroux 2024-04-12 23:07:03 +02:00 committed by GitHub
parent 7db26b09bb
commit 58ac0dc16b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 373 additions and 65 deletions

View file

@ -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<MaterialState> 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<MaterialState> 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')});
});

View file

@ -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<MaterialState> 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);
});

View file

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