Support for disabling TextField, TextFormField (#16027)
diff --git a/examples/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart b/examples/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart
index 3d5b2d4..02a6e67 100644
--- a/examples/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart
+++ b/examples/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart
@@ -138,8 +138,10 @@
padding: const EdgeInsets.all(16.0),
children: <Widget>[
new TextField(
+ enabled: true,
decoration: const InputDecoration(
labelText: 'Event name',
+ border: const OutlineInputBorder(),
),
style: Theme.of(context).textTheme.display1,
),
diff --git a/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart b/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart
index 6493455..4262bfb 100644
--- a/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart
+++ b/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart
@@ -249,17 +249,21 @@
fieldKey: _passwordFieldKey,
helperText: 'No more than 8 characters.',
labelText: 'Password *',
- onSaved: (String value) { person.password = value; },
+ onFieldSubmitted: (String value) {
+ setState(() {
+ person.password = value;
+ });
+ },
),
const SizedBox(height: 24.0),
new TextFormField(
+ enabled: person.password != null && person.password.isNotEmpty,
decoration: const InputDecoration(
border: const UnderlineInputBorder(),
filled: true,
labelText: 'Re-type password',
),
maxLength: 8,
- onFieldSubmitted: (String value) { person.password = value; },
obscureText: true,
validator: _validatePassword,
),
diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart
index 1038c97..358527c 100644
--- a/packages/flutter/lib/src/material/input_decorator.dart
+++ b/packages/flutter/lib/src/material/input_decorator.dart
@@ -1459,6 +1459,9 @@
}
Color _getDefaultIconColor(ThemeData themeData) {
+ if (!decoration.enabled)
+ return themeData.disabledColor;
+
switch (themeData.brightness) {
case Brightness.dark:
return Colors.white70;
@@ -1478,7 +1481,7 @@
// i.e. when they appear in place of the empty text field.
TextStyle _getInlineStyle(ThemeData themeData) {
return themeData.textTheme.subhead.merge(widget.baseStyle)
- .copyWith(color: themeData.hintColor);
+ .copyWith(color: decoration.enabled ? themeData.hintColor : themeData.disabledColor);
}
TextStyle _getFloatingLabelStyle(ThemeData themeData) {
@@ -1487,16 +1490,18 @@
: _getActiveColor(themeData);
final TextStyle style = themeData.textTheme.subhead.merge(widget.baseStyle);
return style
- .copyWith(color: color)
+ .copyWith(color: decoration.enabled ? color : themeData.disabledColor)
.merge(decoration.labelStyle);
}
TextStyle _getHelperStyle(ThemeData themeData) {
- return themeData.textTheme.caption.copyWith(color: themeData.hintColor).merge(decoration.helperStyle);
+ final Color color = decoration.enabled ? themeData.hintColor : Colors.transparent;
+ return themeData.textTheme.caption.copyWith(color: color).merge(decoration.helperStyle);
}
TextStyle _getErrorStyle(ThemeData themeData) {
- return themeData.textTheme.caption.copyWith(color: themeData.errorColor).merge(decoration.errorStyle);
+ final Color color = decoration.enabled ? themeData.errorColor : Colors.transparent;
+ return themeData.textTheme.caption.copyWith(color: color).merge(decoration.errorStyle);
}
double get _borderWeight {
@@ -1506,6 +1511,11 @@
}
Color _getBorderColor(ThemeData themeData) {
+ if (!decoration.enabled) {
+ if (decoration.filled == true && !decoration.border.isOutline)
+ return Colors.transparent;
+ return themeData.disabledColor;
+ }
return decoration.errorText == null
? _getActiveColor(themeData)
: themeData.errorColor;
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart
index 9d01644..5d37747 100644
--- a/packages/flutter/lib/src/material/text_field.dart
+++ b/packages/flutter/lib/src/material/text_field.dart
@@ -111,6 +111,7 @@
this.onChanged,
this.onSubmitted,
this.inputFormatters,
+ this.enabled,
}) : assert(keyboardType != null),
assert(textAlign != null),
assert(autofocus != null),
@@ -137,7 +138,7 @@
/// By default, draws a horizontal line under the text field but can be
/// configured to show an icon, label, hint text, and error text.
///
- /// Set this field to null to remove the decoration entirely (including the
+ /// Specify null to remove the decoration entirely (including the
/// extra padding introduced by the decoration to save space for the labels).
final InputDecoration decoration;
@@ -261,6 +262,13 @@
/// Formatters are run in the provided order when the text input changes.
final List<TextInputFormatter> inputFormatters;
+ /// If false the textfield is "disabled": it ignores taps and its
+ /// [decoration] is rendered in grey.
+ ///
+ /// If non-null this property overrides the [decoration]'s
+ /// [Decoration.enabled] property.
+ final bool enabled;
+
@override
_TextFieldState createState() => new _TextFieldState();
@@ -299,7 +307,10 @@
InputDecoration _getEffectiveDecoration() {
final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
- .applyDefaults(Theme.of(context).inputDecorationTheme);
+ .applyDefaults(Theme.of(context).inputDecorationTheme)
+ .copyWith(
+ enabled: widget.enabled,
+ );
if (!needsCounter)
return effectiveDecoration;
@@ -495,14 +506,17 @@
_controller.selection = new TextSelection.collapsed(offset: _controller.text.length);
_requestKeyboard();
},
- child: new GestureDetector(
- behavior: HitTestBehavior.translucent,
- onTapDown: _handleTapDown,
- onTap: _handleTap,
- onTapCancel: _handleTapCancel,
- onLongPress: _handleLongPress,
- excludeFromSemantics: true,
- child: child,
+ child: new IgnorePointer(
+ ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
+ child: new GestureDetector(
+ behavior: HitTestBehavior.translucent,
+ onTapDown: _handleTapDown,
+ onTap: _handleTap,
+ onTapCancel: _handleTapCancel,
+ onLongPress: _handleLongPress,
+ excludeFromSemantics: true,
+ child: child,
+ ),
),
);
}
diff --git a/packages/flutter/lib/src/material/text_form_field.dart b/packages/flutter/lib/src/material/text_form_field.dart
index f33423d..86de88c 100644
--- a/packages/flutter/lib/src/material/text_form_field.dart
+++ b/packages/flutter/lib/src/material/text_form_field.dart
@@ -67,6 +67,7 @@
FormFieldSetter<String> onSaved,
FormFieldValidator<String> validator,
List<TextInputFormatter> inputFormatters,
+ bool enabled,
}) : assert(initialValue == null || controller == null),
assert(keyboardType != null),
assert(textAlign != null),
@@ -101,6 +102,7 @@
onChanged: field.didChange,
onSubmitted: onFieldSubmitted,
inputFormatters: inputFormatters,
+ enabled: enabled,
);
},
);
diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart
index d088675..f7b339e 100644
--- a/packages/flutter/test/material/input_decorator_test.dart
+++ b/packages/flutter/test/material/input_decorator_test.dart
@@ -60,17 +60,21 @@
return box.size.height;
}
-double getBorderWeight(WidgetTester tester) {
+BorderSide getBorderSide(WidgetTester tester) {
if (!tester.any(findBorderPainter()))
- return 0.0;
+ return null;
final CustomPaint customPaint = tester.widget(findBorderPainter());
final dynamic/* _InputBorderPainter */ inputBorderPainter = customPaint.foregroundPainter;
final dynamic/*_InputBorderTween */ inputBorderTween = inputBorderPainter.border;
final Animation<double> animation = inputBorderPainter.borderAnimation;
final dynamic/*_InputBorder */ border = inputBorderTween.evaluate(animation);
- return border.borderSide.width;
+ return border.borderSide;
}
+double getBorderWeight(WidgetTester tester) => getBorderSide(tester)?.width;
+
+Color getBorderColor(WidgetTester tester) => getBorderSide(tester)?.color;
+
double getHintOpacity(WidgetTester tester) {
final Opacity opacityWidget = tester.widget<Opacity>(
find.ancestor(
@@ -190,7 +194,8 @@
expect(getBorderBottom(tester), 56.0);
expect(getBorderWeight(tester), 2.0);
- // enabled: false causes the border to disappear
+ // enabled: false produces a hairline border if filled: false (the default)
+ // The widget's size and layout is the same as for enabled: true.
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
@@ -208,6 +213,27 @@
expect(tester.getTopLeft(find.text('label')).dy, 20.0);
expect(tester.getBottomLeft(find.text('label')).dy, 36.0);
expect(getBorderWeight(tester), 0.0);
+
+ // enabled: false produces a transparent border if filled: true.
+ // The widget's size and layout is the same as for enabled: true.
+ await tester.pumpWidget(
+ buildInputDecorator(
+ isEmpty: true,
+ isFocused: false,
+ decoration: const InputDecoration(
+ labelText: 'label',
+ enabled: false,
+ filled: true,
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+ expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
+ expect(tester.getTopLeft(find.text('text')).dy, 28.0);
+ expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
+ expect(tester.getTopLeft(find.text('label')).dy, 20.0);
+ expect(tester.getBottomLeft(find.text('label')).dy, 36.0);
+ expect(getBorderColor(tester), Colors.transparent);
});
// Overall height for this InputDecorator is 40.0dps