-
Notifications
You must be signed in to change notification settings - Fork 28.5k
Add Option to disable full selection on focus on TextField, TextFormField, and EditableText #163491
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
base: master
Are you sure you want to change the base?
Conversation
This allows overriding the default behavior of highlighting all the text on focus when using web or desktop
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.
Thanks for jumping in and submitting a PR for this. Some questions here. I always hesitate to add a new parameter to TextField, so I want to make sure we get it right.
When the user dismisses the modal, we want the focus to go back the text field with the same selection that it had before.
What platform is this on? Is this the same behavior that you would expect if you tab into that field?
Some other possible paths here for completeness:
- Keep the boolean parameter as-is in this PR, but set it to a default value that considers the current platform.
- Somehow give the user more generic control over what happens when the user focuses the field (make the parameter a function instead of a bool?).
/// web or desktop | ||
/// | ||
/// By default this will highlight the text field on web and desktop, and can | ||
/// only be turned off on those two platforms |
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.
Missing a period at the end of the sentence here and above.
@@ -1792,6 +1793,15 @@ class EditableText extends StatefulWidget { | |||
/// {@endtemplate} | |||
bool get selectionEnabled => enableInteractiveSelection; | |||
|
|||
/// {@template flutter.widgets.editableText.highlightAllOnFocus} | |||
/// Whether or not this field should highlight all text when gaining focus on | |||
/// web or desktop |
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 special case for web and desktop makes me hesitate here. I know that's how it works now, though. What happens in a native iOS or Android app if you connect a hardware keyboard and use the tab key to move between fields?
@justinmc Yeah! Thank you for taking the time to help me with this! The last thing I want to do is mess up flutter 😆 So I appreciate the questions!! |
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.
@camfrandsen Sorry for the slow response (vacation). Have you had any luck with the function? I'm thinking of something that would completely replace _adjustedSelectionWhenFocused, but that might not be practical since _adjustedSelectionWhenFocused uses private members currently. But would that kind of approach work for your use case or is it not doable?
Otherwise, what if your existing boolean parameter defaulted to something like this?
static bool get defaultHighlightAllOnFocus {
if (kIsWeb) {
return true;
}
return switch (defaultTargetPlatform) {
TargetPlatform.android => false,
TargetPlatform.iOS => false,
TargetPlatform.fuchsia => false,
TargetPlatform.linux => true,
TargetPlatform.macOS => true,
TargetPlatform.windows => true,
};
}
Then we could remove the line about highlightAllOnFocus only applying to web and desktop? I'm just trying to make sure that this parameter makes sense and doesn't have any strange behavior that's dependent on our current private approach.
/// By default this will highlight the text field on web and desktop, and can | ||
/// only be turned off on those two platforms | ||
/// {@endtemplate} | ||
final bool highlightAllOnFocus; |
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.
Actually this should probably be called selectAllOnFocus
.
setCustomSelectionOnFocus allows developers to set the selection when the text field gets focus
@justinmc No worries! I realize that you have probably a ton of stuff going on 😆 |
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.
Does this approach work for your use case? I worry about the private variables in the current _adjustedSelectionWhenFocused implementation:
flutter/packages/flutter/lib/src/widgets/editable_text.dart
Lines 4641 to 4646 in a0b1b32
final bool shouldSelectAll = | |
widget.selectionEnabled && | |
(kIsWeb || isDesktop) && | |
!_isMultiline && | |
!_nextFocusChangeIsInternal && | |
!_justResumed; |
Those won't be available to app developers that write their own setCustomSelectionOnFocus, so they won't be able to recreate the existing behavior exactly.
@@ -1798,6 +1802,14 @@ class EditableText extends StatefulWidget { | |||
/// {@endtemplate} | |||
bool get selectionEnabled => enableInteractiveSelection; | |||
|
|||
/// {@template flutter.widgets.editableText.setCustomSelectionOnFocus} | |||
/// Set a custom text selection when focus is given |
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.
Missing a period here.
style: Typography.material2018().black.titleMedium!, | ||
cursorColor: Colors.blue, | ||
backgroundCursorColor: Colors.grey, | ||
setCustomSelectionOnFocus: () => controller.selection, |
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: Maybe set this to something different so that it's more obvious that it worked, and not that it just kept the existing selection.
@justinmc Great question, it does work for us, but that doesn't mean it would work for everyone. This is how I broke it down in my head:
I could pass those two variables into the function so that developers could use them. Would it be better to add them later once there is a use case for them, or add them now so that if they are added later, there isn't a migration change for them? |
…selection changed Add missing period to document
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.
tl;dr I think we should go back to your boolean approach.
I'm sorry for all of the back and forth here, but I feel more confident in the boolean approach after exploring the function approach first. I think the function approach is probably still the best way to do this, but given how EditableText is currently written, it's not practical to do it that way without bigger refactoring and/or an API that might be a big maintenance burden.
If we were going to do the function approach, I think we would need _adjustedSelectionWhenFocused to be rewritten as a static default value for setCustomSelectionOnFocus. That way developers could fall back to the default behavior, something like this:
TextField(
setCustomSelectionOnFocus: (...) {
if (mySpecialCase) {
return TextSelection(...);
}
return EditableText.defaultSetCustomSelectionOnFocus(...);
},
),
But the API will get pretty tricky if we do that. I say go back to the boolean. Is it possible to do it with a default value like I mentioned in #163491 (review)?
@@ -163,6 +163,7 @@ class TextFormField extends FormField<String> { | |||
Brightness? keyboardAppearance, | |||
EdgeInsets scrollPadding = const EdgeInsets.all(20.0), | |||
bool? enableInteractiveSelection, | |||
SetCustomSelectionOnFocus? setCustomSelectionOnFocus, |
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.
Could you also pipe the parameter through for CupertinoTextField?
8db6817
to
e77c811
Compare
Defaults to true on web
e77c811
to
9fea2b0
Compare
@justinmc I changed it back to a boolean. Thank you again for your help on this! Sometimes we have to try something to realize some of the pros and cons, so I am not worried at all about switching it to a function and back 😆 |
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.
I appreciate your flexibility! I'm convinced that the boolean+default is the way to do it now looking at this code. Just some smaller improvements here.
/// Whether or not this field should highlight all text when gaining focus on | ||
/// web or desktop. |
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.
"highlight" => "select". Just to make sure that we're consistent with our wording.
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.
In another comment in this review I suggested that this parameter should apply equally to all platforms. If so then you should also remove "on web or desktop" here.
@@ -289,6 +289,7 @@ class CupertinoTextField extends StatefulWidget { | |||
this.scrollPadding = const EdgeInsets.all(20.0), | |||
this.dragStartBehavior = DragStartBehavior.start, | |||
bool? enableInteractiveSelection, | |||
this.selectAllOnFocus, |
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.
Can you also set the same defaultSelectAllOnFocus default value for all of the parameters that you added, even these passthrough ones? In my opinion that's the most unambiguous way to handle passthrough parameters with default values.
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.
@justinmc I totally agree that is the most unambiguous way. I can do that easily for text_form_field (and did in my last commit), but material and cupertino text_field are not so straight forward since their constructors are currently const. If I wanted to default selectAllOnFocus to EditableText.defaultSelectAllOnFocus I would need to remove const from the constructor. Is that still worth it?
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 good point. I say leave them with no default in the case of const constructors.
@@ -4644,6 +4670,7 @@ class EditableTextState extends State<EditableText> | |||
TargetPlatform.macOS || TargetPlatform.linux || TargetPlatform.windows => true, | |||
}; | |||
final bool shouldSelectAll = | |||
widget.selectAllOnFocus && | |||
widget.selectionEnabled && | |||
(kIsWeb || isDesktop) && |
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.
I think we can get rid of this line. This logic is encoded in defaultSelectAllOnFocus. Furthermore, if we remove this line, then selectAllOnFocus applies equally to all platforms, which I think is more clear for developers that are trying to understand what this parameter does.
/// By default this will highlight the text field on web and desktop, and can | ||
/// only be turned off on those two platforms. |
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.
And again along with my comment that this parameter should apply equally to all platforms, you would have to remove "and can only be turned off on those two platforms."
@@ -16212,6 +16212,35 @@ void main() { | |||
skip: !kIsWeb, // [intended] | |||
); | |||
|
|||
// Regression test for https://github.com/flutter/flutter/issues/163399. | |||
testWidgets('when selectAllOnFocus is turned off', (WidgetTester tester) async { |
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.
Can you also test when this is set to true? With my comment about how the parameter should apply equally to all platforms, the behavior of true
is different than the default behavior.
Fix comments now that selectAllOnFocus is not platform specific
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.
A few comments, otherwise good to go. I'll look for a secondary reviewer.
@@ -289,6 +289,7 @@ class CupertinoTextField extends StatefulWidget { | |||
this.scrollPadding = const EdgeInsets.all(20.0), | |||
this.dragStartBehavior = DragStartBehavior.start, | |||
bool? enableInteractiveSelection, | |||
this.selectAllOnFocus, |
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 good point. I say leave them with no default in the case of const constructors.
@@ -882,6 +882,7 @@ class EditableText extends StatefulWidget { | |||
this.keyboardAppearance = Brightness.light, | |||
this.dragStartBehavior = DragStartBehavior.start, | |||
bool? enableInteractiveSelection, | |||
bool? selectAllOnFocus, |
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 could default to defaultSelectAllOnFocus right?
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.
Yeah :) I default it on line 927 since defaultSelectAllOnFocus isn't a const value
@@ -1798,6 +1800,13 @@ class EditableText extends StatefulWidget { | |||
/// {@endtemplate} | |||
bool get selectionEnabled => enableInteractiveSelection; | |||
|
|||
/// {@template flutter.widgets.editableText.selectAllOnFocus} | |||
/// Whether or not this field should select all text when gaining focus |
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.
Period at the end here and the next sentence.
@@ -163,6 +163,7 @@ class TextFormField extends FormField<String> { | |||
Brightness? keyboardAppearance, | |||
EdgeInsets scrollPadding = const EdgeInsets.all(20.0), | |||
bool? enableInteractiveSelection, | |||
bool? selectAllOnFocus, |
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.
And this one could have a default 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.
Yeah, having said that, it is not obvious that it defaults to defaultSelectAllOnFocus on line 287... I could put that in the comment, but I don't see how I can make defaultSelectAllOnFocus const... What are your thoughts?
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.
I am merging this conversation and #163491 (comment)
I am taking away the default since it isn't obvious and making defaultSelectAllOnFocus private
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.
Yeah, I think this ends up being like a lot of the other parameters here — the default is complicated, and the reader is left to look it up in the documentation of the corresponding field.
On this particular class and constructor, since this isn't a this.foo
field parameter, that lookup is a bit more complicated than usual; but the constructor itself says what to do:
/// […]
/// For documentation about the various parameters, see the [TextField] class
/// and [TextField.new], the constructor.
TextFormField({
and the [TextField.selectAllOnFocus] doc will have the answer.
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.
Thanks for the contribution! I agree this simple API, with a boolean, seems good. Small comments below.
/// The default value for [selectAllOnFocus]. | ||
static bool get defaultSelectAllOnFocus { |
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 can stay private, right? It doesn't seem like API that's needed from outside this class.
@@ -283,6 +284,7 @@ class TextFormField extends FormField<String> { | |||
keyboardAppearance: keyboardAppearance, | |||
enableInteractiveSelection: | |||
enableInteractiveSelection ?? (!obscureText || !readOnly), | |||
selectAllOnFocus: selectAllOnFocus ?? EditableText.defaultSelectAllOnFocus, |
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.
I guess it currently gets used here. But I think this can just pass through the value it has, whether that's null or otherwise — seems like that's cleanest anyway, by minimizing the number of details about EditableText that TextFormField needs to know.
/// {@template flutter.widgets.editableText.selectAllOnFocus} | ||
/// Whether or not this field should select all text when gaining focus | ||
/// | ||
/// By default this will select all text on web and desktop |
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.
/// By default this will select all text on web and desktop | |
/// Defaults to true on web and desktop platforms, | |
/// and false on mobile platforms. |
@@ -1798,6 +1800,13 @@ class EditableText extends StatefulWidget { | |||
/// {@endtemplate} | |||
bool get selectionEnabled => enableInteractiveSelection; | |||
|
|||
/// {@template flutter.widgets.editableText.selectAllOnFocus} | |||
/// Whether or not this field should select all text when gaining focus |
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.
/// Whether or not this field should select all text when gaining focus | |
/// Whether this field should select all text when gaining focus. | |
/// | |
/// When false, focusing this text field will leave its | |
/// existing text selection unchanged. |
Fix grammer and clarify comments
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.
LGTM 👍
@@ -923,6 +924,7 @@ class EditableText extends StatefulWidget { | |||
), | |||
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'), | |||
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText), | |||
selectAllOnFocus = selectAllOnFocus ?? _defaultSelectAllOnFocus, |
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.
Can't this be done inline on line 885? this.selectAllOnFocus = _defaultSelectAllOnFocus
.
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.
Thanks! LGTM.
@@ -163,6 +163,7 @@ class TextFormField extends FormField<String> { | |||
Brightness? keyboardAppearance, | |||
EdgeInsets scrollPadding = const EdgeInsets.all(20.0), | |||
bool? enableInteractiveSelection, | |||
bool? selectAllOnFocus, |
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.
Yeah, I think this ends up being like a lot of the other parameters here — the default is complicated, and the reader is left to look it up in the documentation of the corresponding field.
On this particular class and constructor, since this isn't a this.foo
field parameter, that lookup is a bit more complicated than usual; but the constructor itself says what to do:
/// […]
/// For documentation about the various parameters, see the [TextField] class
/// and [TextField.new], the constructor.
TextFormField({
and the [TextField.selectAllOnFocus] doc will have the answer.
This allows overriding the default behavior of highlighting all the text
on focus when using web or desktop
This is adding a field to EditableText, TextField, and TextFormField to disable highlighting the entire field on web and desktop when it receives focus. The field is called highlightAllOnFocus. It does nothing on other platforms because the other platforms don't highlight the entire field on focus.
Note: I am not attached to this variable name, but it was the best I could think of... But I am very open to better suggestions 😆
Thank you for everything!
Issue: #163399
Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.