Skip to content

Commit 97cdc0e

Browse files
InputDecoration.error should activate error state (#134001)
When passed an `error` widget, `InputDecoration` should activate its error state. Before this change the `errorBorder` would only activate if an `errorText` was provided. This change solves this issue by accounting for a provided `error` widget.
1 parent fa9c301 commit 97cdc0e

File tree

4 files changed

+143
-8
lines changed

4 files changed

+143
-8
lines changed

packages/flutter/lib/src/material/input_decorator.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1971,6 +1971,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
19711971

19721972
TextAlign? get textAlign => widget.textAlign;
19731973
bool get isFocused => widget.isFocused;
1974+
bool get _hasError => decoration.errorText != null || decoration.error != null;
19741975
bool get isHovering => widget.isHovering && decoration.enabled;
19751976
bool get isEmpty => widget.isEmpty;
19761977
bool get _floatingLabelEnabled {
@@ -2011,7 +2012,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
20112012
? Colors.transparent
20122013
: themeData.disabledColor;
20132014
}
2014-
if (decoration.errorText != null) {
2015+
if (_hasError) {
20152016
return themeData.colorScheme.error;
20162017
}
20172018
if (isFocused) {
@@ -2107,7 +2108,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
21072108

21082109
TextStyle _getFloatingLabelStyle(ThemeData themeData, InputDecorationTheme defaults) {
21092110
TextStyle defaultTextStyle = MaterialStateProperty.resolveAs(defaults.floatingLabelStyle!, materialState);
2110-
if (decoration.errorText != null && decoration.errorStyle?.color != null) {
2111+
if (_hasError && decoration.errorStyle?.color != null) {
21112112
defaultTextStyle = defaultTextStyle.copyWith(color: decoration.errorStyle?.color);
21122113
}
21132114
defaultTextStyle = defaultTextStyle.merge(decoration.floatingLabelStyle ?? decoration.labelStyle);
@@ -2137,7 +2138,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
21372138
if (!decoration.enabled) MaterialState.disabled,
21382139
if (isFocused) MaterialState.focused,
21392140
if (isHovering) MaterialState.hovered,
2140-
if (decoration.errorText != null) MaterialState.error,
2141+
if (_hasError) MaterialState.error,
21412142
};
21422143
}
21432144

@@ -2205,14 +2206,13 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
22052206
),
22062207
);
22072208

2208-
final bool isError = decoration.errorText != null;
22092209
InputBorder? border;
22102210
if (!decoration.enabled) {
2211-
border = isError ? decoration.errorBorder : decoration.disabledBorder;
2211+
border = _hasError ? decoration.errorBorder : decoration.disabledBorder;
22122212
} else if (isFocused) {
2213-
border = isError ? decoration.focusedErrorBorder : decoration.focusedBorder;
2213+
border = _hasError ? decoration.focusedErrorBorder : decoration.focusedBorder;
22142214
} else {
2215-
border = isError ? decoration.errorBorder : decoration.enabledBorder;
2215+
border = _hasError ? decoration.errorBorder : decoration.enabledBorder;
22162216
}
22172217
border ??= _getDefaultBorder(themeData, defaults);
22182218

packages/flutter/lib/src/material/text_field.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
945945

946946
bool get _hasIntrinsicError => widget.maxLength != null && widget.maxLength! > 0 && _effectiveController.value.text.characters.length > widget.maxLength!;
947947

948-
bool get _hasError => widget.decoration?.errorText != null || _hasIntrinsicError;
948+
bool get _hasError => widget.decoration?.errorText != null || widget.decoration?.error != null || _hasIntrinsicError;
949949

950950
Color get _errorColor => widget.decoration?.errorStyle?.color ?? Theme.of(context).colorScheme.error;
951951

packages/flutter/test/material/input_decorator_test.dart

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1668,6 +1668,105 @@ void runAllTests({ required bool useMaterial3 }) {
16681668
expect(find.text('errorText'), findsOneWidget);
16691669
});
16701670

1671+
testWidgets('InputDecoration shows error border for errorText and error widget', (WidgetTester tester) async {
1672+
const InputBorder errorBorder = OutlineInputBorder(
1673+
borderSide: BorderSide(color: Colors.red, width: 1.5),
1674+
);
1675+
const InputBorder focusedErrorBorder = OutlineInputBorder(
1676+
borderSide: BorderSide(color: Colors.teal, width: 5.0),
1677+
);
1678+
1679+
await tester.pumpWidget(
1680+
buildInputDecorator(
1681+
useMaterial3: useMaterial3,
1682+
isFocused: true,
1683+
decoration: const InputDecoration(
1684+
errorText: 'error',
1685+
// enabled: true (default)
1686+
errorBorder: errorBorder,
1687+
focusedErrorBorder: focusedErrorBorder,
1688+
),
1689+
),
1690+
);
1691+
await tester.pumpAndSettle(); // Border changes are animated.
1692+
expect(getBorder(tester), focusedErrorBorder);
1693+
1694+
await tester.pumpWidget(
1695+
buildInputDecorator(
1696+
useMaterial3: useMaterial3,
1697+
// isFocused: false (default)
1698+
decoration: const InputDecoration(
1699+
errorText: 'error',
1700+
// enabled: true (default)
1701+
errorBorder: errorBorder,
1702+
focusedErrorBorder: focusedErrorBorder,
1703+
),
1704+
),
1705+
);
1706+
await tester.pumpAndSettle(); // Border changes are animated.
1707+
expect(getBorder(tester), errorBorder);
1708+
1709+
await tester.pumpWidget(
1710+
buildInputDecorator(
1711+
useMaterial3: useMaterial3,
1712+
// isFocused: false (default)
1713+
decoration: const InputDecoration(
1714+
errorText: 'error',
1715+
enabled: false,
1716+
errorBorder: errorBorder,
1717+
focusedErrorBorder: focusedErrorBorder,
1718+
),
1719+
),
1720+
);
1721+
await tester.pumpAndSettle(); // Border changes are animated.
1722+
expect(getBorder(tester), errorBorder);
1723+
1724+
await tester.pumpWidget(
1725+
buildInputDecorator(
1726+
useMaterial3: useMaterial3,
1727+
isFocused: true,
1728+
decoration: const InputDecoration(
1729+
error: Text('error'),
1730+
// enabled: true (default)
1731+
errorBorder: errorBorder,
1732+
focusedErrorBorder: focusedErrorBorder,
1733+
),
1734+
),
1735+
);
1736+
await tester.pumpAndSettle(); // Border changes are animated.
1737+
expect(getBorder(tester), focusedErrorBorder);
1738+
1739+
await tester.pumpWidget(
1740+
buildInputDecorator(
1741+
useMaterial3: useMaterial3,
1742+
// isFocused: false (default)
1743+
decoration: const InputDecoration(
1744+
error: Text('error'),
1745+
// enabled: true (default)
1746+
errorBorder: errorBorder,
1747+
focusedErrorBorder: focusedErrorBorder,
1748+
),
1749+
),
1750+
);
1751+
await tester.pumpAndSettle(); // Border changes are animated.
1752+
expect(getBorder(tester), errorBorder);
1753+
1754+
await tester.pumpWidget(
1755+
buildInputDecorator(
1756+
useMaterial3: useMaterial3,
1757+
// isFocused: false (default)
1758+
decoration: const InputDecoration(
1759+
error: Text('error'),
1760+
enabled: false,
1761+
errorBorder: errorBorder,
1762+
focusedErrorBorder: focusedErrorBorder,
1763+
),
1764+
),
1765+
);
1766+
await tester.pumpAndSettle(); // Border changes are animated.
1767+
expect(getBorder(tester), errorBorder);
1768+
});
1769+
16711770
testWidgetsWithLeakTracking('InputDecorator shows error widget', (WidgetTester tester) async {
16721771
await tester.pumpWidget(
16731772
buildInputDecorator(

packages/flutter/test/material/text_field_test.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,42 @@ void main() {
787787
expect(state.widget.cursorColor, cursorColor);
788788
});
789789

790+
testWidgets('Use error cursor color when an InputDecoration with an errorText or error widget is provided', (WidgetTester tester) async {
791+
await tester.pumpWidget(
792+
const MaterialApp(
793+
home: Material(
794+
child: TextField(
795+
autofocus: true,
796+
decoration: InputDecoration(
797+
error: Text('error'),
798+
errorStyle: TextStyle(color: Colors.teal),
799+
),
800+
),
801+
),
802+
),
803+
);
804+
await tester.pump();
805+
EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
806+
expect(state.widget.cursorColor, Colors.teal);
807+
808+
await tester.pumpWidget(
809+
const MaterialApp(
810+
home: Material(
811+
child: TextField(
812+
autofocus: true,
813+
decoration: InputDecoration(
814+
errorText: 'error',
815+
errorStyle: TextStyle(color: Colors.teal),
816+
),
817+
),
818+
),
819+
),
820+
);
821+
await tester.pump();
822+
state = tester.state<EditableTextState>(find.byType(EditableText));
823+
expect(state.widget.cursorColor, Colors.teal);
824+
});
825+
790826
testWidgetsWithLeakTracking('sets cursorOpacityAnimates on EditableText correctly', (WidgetTester tester) async {
791827

792828
// True

0 commit comments

Comments
 (0)