-
Notifications
You must be signed in to change notification settings - Fork 27.8k
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
TextField and TextFormField can use a MaterialStatesController #133977
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; | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we actually need an internal states controller if the user didn't supply one? The reason we're adding it is that users want to add listeners to it right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is a good point we are currently not using it internally. We do keep track of our flutter/packages/flutter/lib/src/material/text_field.dart Lines 1215 to 1222 in ff5b5b5
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Do you mean if a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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?
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. Updated the MaterialStatesController docs in #134592 |
||||||||||||||||||
|
||||||||||||||||||
void _handleStatesControllerChange() { | ||||||||||||||||||
// Force a rebuild to resolve MaterialStateProperty properties. | ||||||||||||||||||
setState(() { }); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
MaterialStatesController get _statesController => widget.statesController ?? _internalStatesController!; | ||||||||||||||||||
|
||||||||||||||||||
void _initStatesController() { | ||||||||||||||||||
if (widget.statesController == null) { | ||||||||||||||||||
_internalStatesController = MaterialStatesController(); | ||||||||||||||||||
} | ||||||||||||||||||
_statesController.update(MaterialState.disabled, !_isEnabled); | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why does it only update There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here and There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||
_statesController.update(MaterialState.hovered, _isHovering); | ||||||||||||||||||
_statesController.update(MaterialState.focused, _effectiveFocusNode.hasFocus); | ||||||||||||||||||
_statesController.update(MaterialState.error, _hasError); | ||||||||||||||||||
_statesController.addListener(_handleStatesControllerChange); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// AutofillClient implementation start. | ||||||||||||||||||
|
@@ -1246,19 +1312,10 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements | |||||||||||||||||
} | ||||||||||||||||||
// AutofillClient implementation end. | ||||||||||||||||||
|
||||||||||||||||||
Set<MaterialState> get _materialState { | ||||||||||||||||||
return <MaterialState>{ | ||||||||||||||||||
if (!_isEnabled) MaterialState.disabled, | ||||||||||||||||||
if (_isHovering) MaterialState.hovered, | ||||||||||||||||||
if (_effectiveFocusNode.hasFocus) MaterialState.focused, | ||||||||||||||||||
if (_hasError) MaterialState.error, | ||||||||||||||||||
}; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
TextStyle _getInputStyleForState(TextStyle style) { | ||||||||||||||||||
final ThemeData theme = Theme.of(context); | ||||||||||||||||||
final TextStyle stateStyle = MaterialStateProperty.resolveAs(theme.useMaterial3 ? _m3StateInputStyle(context)! : _m2StateInputStyle(context)!, _materialState); | ||||||||||||||||||
final TextStyle providedStyle = MaterialStateProperty.resolveAs(style, _materialState); | ||||||||||||||||||
final TextStyle stateStyle = MaterialStateProperty.resolveAs(theme.useMaterial3 ? _m3StateInputStyle(context)! : _m2StateInputStyle(context)!, _statesController.value); | ||||||||||||||||||
final TextStyle providedStyle = MaterialStateProperty.resolveAs(style, _statesController.value); | ||||||||||||||||||
return providedStyle.merge(stateStyle); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
|
@@ -1275,7 +1332,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements | |||||||||||||||||
|
||||||||||||||||||
final ThemeData theme = Theme.of(context); | ||||||||||||||||||
final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(context); | ||||||||||||||||||
final TextStyle? providedStyle = MaterialStateProperty.resolveAs(widget.style, _materialState); | ||||||||||||||||||
final TextStyle? providedStyle = MaterialStateProperty.resolveAs(widget.style, _statesController.value); | ||||||||||||||||||
final TextStyle style = _getInputStyleForState(theme.useMaterial3 ? _m3InputStyle(context) : theme.textTheme.titleMedium!).merge(providedStyle); | ||||||||||||||||||
final Brightness keyboardAppearance = widget.keyboardAppearance ?? theme.brightness; | ||||||||||||||||||
final TextEditingController controller = _effectiveController; | ||||||||||||||||||
|
@@ -1490,7 +1547,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements | |||||||||||||||||
} | ||||||||||||||||||
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>( | ||||||||||||||||||
widget.mouseCursor ?? MaterialStateMouseCursor.textable, | ||||||||||||||||||
_materialState, | ||||||||||||||||||
_statesController.value, | ||||||||||||||||||
); | ||||||||||||||||||
|
||||||||||||||||||
final int? semanticsMaxValueLength; | ||||||||||||||||||
|
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.dart
points outMaterialState.pressed
whichTextField
does 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?(