Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

camfrandsen
Copy link

@camfrandsen camfrandsen commented Feb 17, 2025

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.

This allows overriding the default behavior of highlighting all the text
 on focus when using web or desktop
@github-actions github-actions bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. labels Feb 17, 2025
Copy link
Contributor

@justinmc justinmc left a 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
Copy link
Contributor

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
Copy link
Contributor

@justinmc justinmc Feb 26, 2025

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?

@camfrandsen
Copy link
Author

@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!!
For us, this is a web only application. Which normally, we would want it to select all the text, but unfortunately in a few cases we don't... And for us, if they were to tab into these certain fields we also wouldn't want it to select everything.
Having said that, I love the idea of making it a function. It give a lot for flexibility in what would happen. I can make that change. Would I just call it onFocusChange similar to a focus node?
Actually, clarification question. Do you think it would be a void Function() that would be called instead of setting selection (if not null) or should it just be a TextSelection Function()?

Copy link
Contributor

@justinmc justinmc left a 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;
Copy link
Contributor

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
@camfrandsen
Copy link
Author

@justinmc No worries! I realize that you have probably a ton of stuff going on 😆
So, I personally love the callback idea! It allows for a lot of customization but keeps the API simple. They don't really need access to the private variables since you can access them through the public getters.
I pushed up my attempt at it. Let me know what you think 😄

Copy link
Contributor

@justinmc justinmc left a 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:

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
Copy link
Contributor

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,
Copy link
Contributor

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.

@camfrandsen
Copy link
Author

@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:

  • widget.selectionEnabled - This was passed in, so they should know this value
  • (kIsWeb || isDesktop) - This is available to everyone already
  • !_isMultiline - This is just if they set maxLines > 1, so I feel like they would already know this
    The next two (!_nextFocusChangeIsInternal and !_justResumed) are tricker... They could replicate the AppLifecycleListener and focus node listeners... but it would be better if it didn't have to be duplicated...

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?

Copy link
Contributor

@justinmc justinmc left a 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,
Copy link
Contributor

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?

@github-actions github-actions bot added the f: cupertino flutter/packages/flutter/cupertino repository label Apr 15, 2025
@camfrandsen camfrandsen force-pushed the highlightAllOnFocus branch from 8db6817 to e77c811 Compare April 15, 2025 17:41
@camfrandsen camfrandsen force-pushed the highlightAllOnFocus branch from e77c811 to 9fea2b0 Compare April 15, 2025 20:05
@camfrandsen
Copy link
Author

@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 😆

Copy link
Contributor

@justinmc justinmc left a 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.

Comment on lines 1804 to 1805
/// Whether or not this field should highlight all text when gaining focus on
/// web or desktop.
Copy link
Contributor

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.

Copy link
Contributor

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,
Copy link
Contributor

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.

Copy link
Author

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?

Copy link
Contributor

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) &&
Copy link
Contributor

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.

Comment on lines 1807 to 1808
/// By default this will highlight the text field on web and desktop, and can
/// only be turned off on those two platforms.
Copy link
Contributor

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 {
Copy link
Contributor

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
@dkwingsmt dkwingsmt changed the title Add Option to disable full selection on focus on TextField, TestFormField, and EditableText Add Option to disable full selection on focus on TextField, TextFormField, and EditableText Apr 30, 2025
Copy link
Contributor

@justinmc justinmc left a 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,
Copy link
Contributor

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,
Copy link
Contributor

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?

Copy link
Author

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
Copy link
Contributor

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,
Copy link
Contributor

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?

Copy link
Author

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?

Copy link
Author

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

Copy link
Member

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.

Copy link
Member

@gnprice gnprice left a 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.

Comment on lines 2049 to 2050
/// The default value for [selectAllOnFocus].
static bool get defaultSelectAllOnFocus {
Copy link
Member

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,
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// 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
@camfrandsen
Copy link
Author

@gnprice @justinmc Thank you both again, I really appreciate the help (and opportunity) on this 😆

Copy link
Contributor

@justinmc justinmc left a 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,
Copy link
Contributor

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.

Copy link
Member

@gnprice gnprice left a 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,
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: text input Entering text in a text field or keyboard related problems f: cupertino flutter/packages/flutter/cupertino repository f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants