-
Notifications
You must be signed in to change notification settings - Fork 29.8k
TextField and TextFormField can use a MaterialStatesController #133977
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ffda5df
b54c5dc
b6f078c
be2acbb
f225a63
9740e96
116aba9
e9bc016
c25790d
3ce3219
18bccf2
b23e3cc
0fc36f6
b53d17f
244269c
f9caa8d
03d59fb
7e0e4ff
ac59d06
a6f5170
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -273,6 +273,7 @@ class TextField extends StatefulWidget { | |||||||||||||||||
| this.toolbarOptions, | ||||||||||||||||||
| this.showCursor, | ||||||||||||||||||
| this.autofocus = false, | ||||||||||||||||||
| this.statesController, | ||||||||||||||||||
| this.obscuringCharacter = '•', | ||||||||||||||||||
| this.obscureText = false, | ||||||||||||||||||
| this.autocorrect = true, | ||||||||||||||||||
|
|
@@ -457,6 +458,27 @@ class TextField extends StatefulWidget { | |||||||||||||||||
| /// {@macro flutter.widgets.editableText.autofocus} | ||||||||||||||||||
| final bool autofocus; | ||||||||||||||||||
|
|
||||||||||||||||||
| /// Represents the interactive "state" of this widget in terms of a set of | ||||||||||||||||||
| /// [MaterialState]s, including [MaterialState.disabled], [MaterialState.hovered], | ||||||||||||||||||
| /// [MaterialState.error], and [MaterialState.focused]. | ||||||||||||||||||
| /// | ||||||||||||||||||
| /// Classes based on this one can provide their own | ||||||||||||||||||
| /// [MaterialStatesController] to which they've added listeners. | ||||||||||||||||||
| /// They can also update the controller's [MaterialStatesController.value] | ||||||||||||||||||
| /// however, this may only be done when it's safe to call | ||||||||||||||||||
| /// [State.setState], like in an event handler. | ||||||||||||||||||
| /// | ||||||||||||||||||
| /// The controller's [MaterialStatesController.value] represents the set of | ||||||||||||||||||
| /// states that a widget's visual properties, typically [MaterialStateProperty] | ||||||||||||||||||
| /// values, are resolved against. It is _not_ the intrinsic state of the widget. | ||||||||||||||||||
| /// The widget is responsible for ensuring that the controller's | ||||||||||||||||||
| /// [MaterialStatesController.value] tracks its intrinsic state. For example | ||||||||||||||||||
| /// one cannot request the keyboard focus for a widget by adding [MaterialState.focused] | ||||||||||||||||||
| /// to its controller. When the widget gains the or loses the focus it will | ||||||||||||||||||
| /// [MaterialStatesController.update] its controller's [MaterialStatesController.value] | ||||||||||||||||||
| /// and notify listeners of the change. | ||||||||||||||||||
| final MaterialStatesController? statesController; | ||||||||||||||||||
|
|
||||||||||||||||||
| /// {@macro flutter.widgets.editableText.obscuringCharacter} | ||||||||||||||||||
| final String obscuringCharacter; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -970,7 +992,11 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements | |||||||||||||||||
|
|
||||||||||||||||||
| int get _currentLength => _effectiveController.value.text.characters.length; | ||||||||||||||||||
|
|
||||||||||||||||||
| bool get _hasIntrinsicError => widget.maxLength != null && widget.maxLength! > 0 && _effectiveController.value.text.characters.length > widget.maxLength!; | ||||||||||||||||||
| bool get _hasIntrinsicError => widget.maxLength != null && | ||||||||||||||||||
| widget.maxLength! > 0 && | ||||||||||||||||||
| (widget.controller == null ? | ||||||||||||||||||
| !restorePending && _effectiveController.value.text.characters.length > widget.maxLength! : | ||||||||||||||||||
| _effectiveController.value.text.characters.length > widget.maxLength!); | ||||||||||||||||||
|
|
||||||||||||||||||
| bool get _hasError => widget.decoration?.errorText != null || widget.decoration?.error != null || _hasIntrinsicError; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -1055,6 +1081,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements | |||||||||||||||||
| } | ||||||||||||||||||
| _effectiveFocusNode.canRequestFocus = widget.canRequestFocus && _isEnabled; | ||||||||||||||||||
| _effectiveFocusNode.addListener(_handleFocusChanged); | ||||||||||||||||||
| _initStatesController(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| bool get _canRequestFocus { | ||||||||||||||||||
|
|
@@ -1096,6 +1123,20 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements | |||||||||||||||||
| _showSelectionHandles = !widget.readOnly; | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if (widget.statesController == oldWidget.statesController) { | ||||||||||||||||||
| _statesController.update(MaterialState.disabled, !_isEnabled); | ||||||||||||||||||
| _statesController.update(MaterialState.hovered, _isHovering); | ||||||||||||||||||
| _statesController.update(MaterialState.focused, _effectiveFocusNode.hasFocus); | ||||||||||||||||||
| _statesController.update(MaterialState.error, _hasError); | ||||||||||||||||||
| } else { | ||||||||||||||||||
| oldWidget.statesController?.removeListener(_handleStatesControllerChange); | ||||||||||||||||||
| if (widget.statesController != null) { | ||||||||||||||||||
| _internalStatesController?.dispose(); | ||||||||||||||||||
| _internalStatesController = null; | ||||||||||||||||||
| } | ||||||||||||||||||
| _initStatesController(); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| @override | ||||||||||||||||||
|
|
@@ -1128,6 +1169,8 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements | |||||||||||||||||
| _effectiveFocusNode.removeListener(_handleFocusChanged); | ||||||||||||||||||
| _focusNode?.dispose(); | ||||||||||||||||||
| _controller?.dispose(); | ||||||||||||||||||
| _statesController.removeListener(_handleStatesControllerChange); | ||||||||||||||||||
| _internalStatesController?.dispose(); | ||||||||||||||||||
| super.dispose(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -1172,6 +1215,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements | |||||||||||||||||
| // Rebuild the widget on focus change to show/hide the text selection | ||||||||||||||||||
| // highlight. | ||||||||||||||||||
| }); | ||||||||||||||||||
| _statesController.update(MaterialState.focused, _effectiveFocusNode.hasFocus); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) { | ||||||||||||||||||
|
|
@@ -1220,7 +1264,29 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements | |||||||||||||||||
| setState(() { | ||||||||||||||||||
| _isHovering = hovering; | ||||||||||||||||||
| }); | ||||||||||||||||||
| _statesController.update(MaterialState.hovered, _isHovering); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Material states controller. | ||||||||||||||||||
| MaterialStatesController? _internalStatesController; | ||||||||||||||||||
|
||||||||||||||||||
| Set<MaterialState> get _materialState { | |
| return <MaterialState>{ | |
| if (!_isEnabled) MaterialState.disabled, | |
| if (_isHovering) MaterialState.hovered, | |
| if (_effectiveFocusNode.hasFocus) MaterialState.focused, | |
| if (_hasError) MaterialState.error, | |
| }; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The purpose of this change is to let developers monitor material states changes, but changing the material states from outside of the TextField class shouldn't be allowed, since the source of the truth is TextFieldState I think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does makes sense that TextField would be the source of truth for its material state, but I also see the use-case for being able to programmatically update a TextField's material state to add support for more states.
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(home: Home()));
}
class PressedInputField extends StatefulWidget {
const PressedInputField({
super.key,
this.style,
required this.pressed,
required this.onPressed,
required this.onReleased,
});
final bool pressed;
final TextStyle? style;
final VoidCallback? onPressed;
final VoidCallback? onReleased;
@override
State<PressedInputField> createState() => _PressedInputFieldState();
}
class _PressedInputFieldState extends State<PressedInputField> {
late final MaterialStatesController statesController;
final TextEditingController controller = TextEditingController(text: 'some text');
@override
void initState() {
super.initState();
statesController = MaterialStatesController(
<MaterialState>{if (widget.pressed) MaterialState.pressed});
}
@override
void dispose() {
statesController.dispose();
controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(PressedInputField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.pressed != oldWidget.pressed) {
statesController.update(MaterialState.pressed, widget.pressed);
}
}
@override
Widget build(BuildContext context) {
return Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (PointerDownEvent event) {
widget.onPressed?.call();
},
child: TextField(
controller: controller,
statesController: statesController,
style: widget.style,
onTap: () {
widget.onReleased?.call();
},
),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
bool pressed = false;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: PressedInputField(
pressed: pressed,
style: MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return const TextStyle(color: Colors.white, backgroundColor: Colors.indigo);
}
return const TextStyle(color: Colors.teal);
}),
onPressed: () {
setState(() {
pressed = !pressed;
});
},
onReleased: () {
setState(() {
pressed = !pressed;
});
}
),
),
);
}
}I'm okay with holding off on that functionality until it is requested, but I also think when a user updates state through the controller using _myController.update(MaterialState.pressed, true) they might expect that the receiving widget reflects the value change, similar to other controllers like TextEditingController and doing _myController.value = myNewValue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when a user updates state through the controller using _myController.update(MaterialState.pressed, true) they might expect that the receiving widget reflects the value change, similar to other controllers like TextEditingController and doing _myController.value = myNewValue.
Do you mean if a TextField has focus but if the user calls update(.focused, false) on the controller then the text field should tell its focus node to unfocus? That sounds pretty strange to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the user adds the focused MaterialState after the component loses focus, then the component's visuals will make it appear that the component has the focus.
And if the component regains focus and loses focus again? Should the visual state of the component update as the intrinsic state of the component changes?
- If the answer is no then it sounds like we need an additional switch that the user can turn on to suppress the intrinsic state changes from affecting the visual state
- otherwise the controller will be pretty hard to use on a
TextField. For instance the app user can switch the focus to a different component, and to obscure the underlying state change I guess you'll have to listen to the state controller and change the material state back? That usually won't happen until the next frame. Average users may not notice that but that can make tests flaky.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you really wanted to prevent the MaterialStatesController from tracking the textfield's intrinsic focus (or whatever) state you could override the controller's update method. That said, this isn't a use case that has ever come up or seems worth designing for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok makes sense. I guess this should be in the documentation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's not explained well (at all) and some examples are needed. If you create an issue, you can assign it to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the MaterialStatesController docs in #134592
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does it only update .disabled but not the other properties?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and didUpdateWidget seem like the only reasonable places to listen for changes in _isEnabled, while the other state properties like focus (listens to changes in FocusNode) and hover (listens to MouseRegion callbacks) actively listen for changes. I'll also initialize the error state here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Focus is edge-triggered so you have to set the correct initial value otherwise it won't reflect the correct state until the next time you get notified of a focus state change right? The TextField can be given a FocusNode that may or may not have the focus when initState is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(what's the difference between this one and the existing doc template for this in
ink_well.dart?)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one just makes sure to point out the states reported by
TextField.{ MaterialState.disabled, MaterialState.hovered, MaterialState.error, MaterialState.focused }.ink_well.dartpoints outMaterialState.pressedwhichTextFielddoes not report at the moment.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: consider linking the part of the material state controller doc that explains that this is not the intrinsic states and if you change the value, future intrinsic states change may overwrite the change?(