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

Skip to content

How to share focus with popovers and other sub-tasks #106923

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
matthew-carroll opened this issue Jul 1, 2022 · 28 comments
Open

How to share focus with popovers and other sub-tasks #106923

matthew-carroll opened this issue Jul 1, 2022 · 28 comments
Labels
a: text input Entering text in a text field or keyboard related problems c: new feature Nothing broken; request for a new capability c: proposal A detailed proposal for a change to Flutter f: focus Focus traversal, gaining or losing focus f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. P3 Issues that are less important to the Flutter project team-framework Owned by Framework team triaged-framework Triaged by Framework team

Comments

@matthew-carroll
Copy link
Contributor

There's a conflict in text-based focus behavior in Flutter and I'm wondering if there's an intended approach to this problem.

Typically, when entering text into something like a form field, the moment focus moves away from the field, the selection is gone. For example, you'll see a caret and type text into a "name" field. Then, focus moves to an "email" field. At this point, there is no caret or selection in the "name" field any longer, and there is a caret in the "email" field. However, this focus behavior is not universal.

When composing text, there are a number of occasions in which some kind of popover might appear. For example, a popover to select text styles, enter a URL for a link, or select a compound character. In these situations, the popover includes focusable elements, and the user should be able to move focus around those elements. But that focus change should not impact the selection within the text field. Additionally, the moment the popover disappears, focus should return to the text field with the selection unchanged.

Does Flutter have an approach for handling these different situations?

Visual examples:

  • Editing the link URL in a selection toolbar (this example use SuperEditor but without keyboard as the input source, not IME)
    Screenshot 2022-06-28 at 23 46 13

  • Right-click on selected to show the context menu
    Screenshot 2022-06-28 at 23 49 08

  • Press and hold character key that can be written with different accents depending on the language (e.g. Portuguese)
    Screenshot 2022-06-28 at 23 50 53

@matthew-carroll
Copy link
Contributor Author

CC @justinmc

@exaby73 exaby73 added the in triage Presently being triaged by the triage team label Jul 1, 2022
@exaby73
Copy link
Member

exaby73 commented Jul 1, 2022

Hello @matthew-carroll. Thank you for filing this issue. Can you include the output of flutter doctor -v as well as a minimal, reproducible example along with reproduction steps?

@exaby73 exaby73 added the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Jul 1, 2022
@matthew-carroll
Copy link
Contributor Author

The Flutter version doesn't matter for this. It's a question about the approach. All necessary information is in the ticket. I'd like to hear the perspective from someone working on text input, e.g., @justinmc, @gspencergoog, @LongCatIsLooong

@github-actions github-actions bot removed the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Jul 1, 2022
@justinmc
Copy link
Contributor

justinmc commented Jul 1, 2022

Is this related to this problem with FocusTrap? #86972

Taps outside of an input will always steal focus from that input because of FocusTrap.

@matthew-carroll
Copy link
Contributor Author

That issue may or may not be related. But the root question here is how a Flutter developer should know when losing focus means "your input isn't active anymore" vs situations that mean "your input is still active, but something else has focus temporarily". You can see those examples in the original post.

@LongCatIsLooong
Copy link
Contributor

LongCatIsLooong commented Jul 2, 2022

the moment focus moves away from the field, the selection is gone

TextField is considered focused as long as a descendant in its focus tree is focused, even if the TextField is not the primary focus, because it mostly relies on hasFocus to determine if it's receiving input.

When composing text, there are a number of occasions in which some kind of popover might appear.

If elements in the popover are themselves focusable and have to use dedicated FocusNodes, then in theory as long as these FocusNodes are descendants of the FocusNode you gave to the TextField, TextField should still consider itself as focused and appear as focused. This may require the popover implementation to find the focus node that belongs to the text field, if the popover is implemented as a separate OverlayEntry, since you can't rely on the widget tree hierarchy to automatically do that for you.

@matthew-carroll
Copy link
Contributor Author

It sounds like you're saying that FocusNode hierarchy might be different from Widget hierarchy. Am I understanding you correctly?

If so, don't Focus widgets automatically reparent FocusNodes such that a FocusNode in an overlay can't be a descendent of a FocusNode in the regular tree?

Furthermore, what's the story around platform interop? You'll see in the original post that there are multiple situations where the popover is a system UI, e.g., a context menu or a compound character selection.

@LongCatIsLooong
Copy link
Contributor

LongCatIsLooong commented Jul 2, 2022

That's right. I think the Focus widget uses Focus.of to build the focus tree. So if you want the text field to stay focused when something in the popover gains focus, right now you'll probably have to manage the FocusNode subtree yourself.

Furthermore, what's the story around platform interop? You'll see in the original post that there are multiple situations where the popover is a system UI, e.g., a context menu or a compound character selection.

As far as the text input plugin goes I think the flutter framework handles focus separately from the platform's focus system. For key input if the system decides to hand a key event to the FlutterView then the embedder sends the key event to the framework.

@matthew-carroll
Copy link
Contributor Author

Can we get an official Flutter team example of each situation, so that everyone is clear on the intended approach? E.g., a case where focus moves to an in-app popover and back, and a case where focus moves to an OS popover and back?

I'm also wondering if Flutter should offer a generalized capability to achieve this FocusNode hierarchy. We can handle a custom hierarchy when the same developer is developing the document editor and the popovers. But consider that super_editor is developing the document editor and app developers are creating any number of popovers. How do we deal with open-ended possibilities for this use-case?

@matthew-carroll
Copy link
Contributor Author

I'll add another detail to this issue. I started to implement custom management of a FocusNode's parent. However, my custom parenting wasn't being respected. The parent-child relationship kept getting changed by something else.

I discovered that TextField passes its FocusNode to EditableText, which then includes a Focus widget within itself. Therefore, it looks like TextField is forcing reparenting based on the widget tree, preventing me from solving this problem. Is there an intended workaround for this situation?

@exaby73 exaby73 added a: text input Entering text in a text field or keyboard related problems c: new feature Nothing broken; request for a new capability c: proposal A detailed proposal for a change to Flutter and removed in triage Presently being triaged by the triage team labels Jul 4, 2022
@exaby73
Copy link
Member

exaby73 commented Jul 4, 2022

@matthew-carroll I've assigned labels to this issue. I'm not every familiar with the internals of TextField so I am not able to follow the discussion well, but please do confirm if the labels are correct

@matthew-carroll
Copy link
Contributor Author

@exaby73 I'm not sure if the labels are correct or not. I don't know if this is a bug or a new feature. And I don't know if I'm proposing something new, or if there's an intended approach here. That's why I asked the questions above. We need additional input from the Flutter team in multiple areas.

@maheshj01 maheshj01 added framework flutter/packages/flutter repository. See also f: labels. f: focus Focus traversal, gaining or losing focus f: material design flutter/packages/flutter/material repository. labels Jul 4, 2022
@matthew-carroll
Copy link
Contributor Author

Two ideas based on what I'm seeing on my end:

  • Add a bool to Focus that enables/disables re-parenting altogether. That way, clients can use a Focus widget exclusively to respond to key presses without breaking a focus tree structure that was configured elsewhere.
  • Add an optional parentNode to Focus. When provided, the Focus widget will always reparent to the parentNode, rather than re-parent based on the widget tree structure.

CC @gspencergoog

@gspencergoog
Copy link
Contributor

Does Flutter have an approach for handling these different situations?

In short, no.

There's no concept of multiple focus, or a "focus stack" other than what happens when you enter and leave a focus scope. You might be able to get something like the functionality you want by wrapping the contents of the overlay in a FocusScope and moving the focus there when it pops up (probably via autofocus), and then focusing the text field's FocusScopeNode when the overlay goes away. This would not, however, continue to show the focus highlight in the underlying field, it would just return the focus to the right place when the overlay went away.

It sounds like you're saying that FocusNode hierarchy might be different from Widget hierarchy.

Yes, the focus tree is, in fact, separate from the widget tree and doesn't have to mirror it (even though 99% of the time it does), but in general, it's a bad idea to manipulate FocusNode hierarchies outside of the Focus and FocusScope widgets, since they can reset any attributes of the focus node that they manage, including reparenting them. You would need to manage your own FocusAttachment, which the Focus widget normally does for you. EditableText used to be the only instance in the framework of this, but it now uses a Focus widget like everything else.

The existence of public API on FocusNode other than requestFocus is purely because we didn't want to break old apps that might be using those APIs at the time we did the refactor (probably four years ago now). At this point, they should probably be deprecated. Adding more API to manipulate the focus tree manually is going in the wrong direction, IMO, and complicating an API that is already too complicated.

If what you'd like to do is respond to keypresses regardless of focus, you can add a handler to HardwareKeyboard, but I suspect that isn't sufficient.

Some concept of a "focus stack" where there is a stack of currently focused nodes instead of a single primary focus is probably what you need, and the overlain dialogs could push and pop the focus to keep the focus visible below them. This would be non-trivial to implement in a non-breaking way.

@matthew-carroll
Copy link
Contributor Author

matthew-carroll commented Jul 8, 2022

@gspencergoog can you take a look at what we're doing in SuperEditor related to this problem?
superlistapp/super_editor#671

I'm not sure I see how creating more focus scopes would solve the problem.

You mentioned that you want to get rid of the properties on FocusNode. I believe that would make our use-case impossible.

I think this SuperEditor use-case is likely to proliferate on desktop where we deal with many more intricate popover use-cases. Based on your description of the current state and intentions, it sounds like the focus system might be moving in the opposite direction from where it needs to go.

Or, to state this problem another way: In a world full of popovers, focus structure does NOT match the widget tree structure.

@gspencergoog
Copy link
Contributor

I'm not sure I see how creating more focus scopes would solve the problem.

I'm sure it won't solve the problem. It would solve part of the problem, which is preserving the correct focus when the popover goes away, since the FocusScopeNode does keep track of a stack of previously focused nodes. There's still a lot left that it wouldn't solve.

Based on your description of the current state and intentions, it sounds like the focus system might be moving in the opposite direction from where it needs to go.

There currently aren't any plans to change the focus system, so it's not really moving in any direction.

Or, to state this problem another way: In a world full of popovers, focus structure does NOT match the widget tree structure.

I'm not sure that having separate structures necessarily follows from having popovers. There are plenty of designs that would allow the two trees to continue have similar structure, which has the distinct advantage that most developers never need to know that the focus tree could have a different structure.

@matthew-carroll
Copy link
Contributor Author

matthew-carroll commented Jul 8, 2022

I'm not sure that having separate structures necessarily follows from having popovers. There are plenty of designs that would allow the two trees to continue have similar structure, which has the distinct advantage that most developers never need to know that the focus tree could have a different structure.

Can you describe this with some specifics? The fact that there are 2 trees would seem to suggest that the focus structure is incompatible with the tree structure. For example, the Focus widget in the Overlay walks up the widget tree to attach a parent FocusNode. That Overlay branch that it's climbing is necessarily separate and independent from the primary UI branch that displays the screen/body. How would one treat these independent trees as a "similar structure" for focus?

@matthew-carroll
Copy link
Contributor Author

I ended up merging in my linked changes into super_editor: superlistapp/super_editor#671

I included a smoke test for the behavior in questions: https://github.com/superlistapp/super_editor/pull/671/files#diff-e291e61fe66a09ed12351d0e94c6c4a789fa6ab6b4ce101fd460b5fffcded826 - hopefully these tests will be added to the test registry soon.

We've ended up with two regrettable changes. First, we had to create a widget that parents a FocusNode to a specified parent, instead of the natural upstream parent. Second, and more significantly, we had to copy all of Focus and add adjustments to prevent automatic reparenting: https://github.com/superlistapp/super_editor/pull/671/files#diff-474c1650d6279380617b75f98826dc9be6ef1287b2ce87749421ba23ef7d3bf9

If Flutter thinks that it makes this use-case possible with existing tooling, I'd like to see a specific example. If such an example can be assembled, then I would recommend placing it in the framework tests to ensure that the use-case remains functional.

@matthew-carroll
Copy link
Contributor Author

We've run into another issue with sharing focus. As I mentioned in the previous comment, we were able to work around the focus problem with widgets that we fully control. However, widgets like DropdownButton specifically require items of type DropdownMenuItem, which seem to enforce the standard Focus reparenting. I haven't found a place to stop that behavior, and so now I'm not sure how we're supposed to share focus between our toolbar dropdown and the editor.

Screen Shot 2022-07-13 at 3 05 09 PM

@matthew-carroll
Copy link
Contributor Author

@gspencergoog - I posted a stripped down example of a few possible approaches to shared focus: https://github.com/Flutter-Bounty-Hunters/investigation_overlay_focus

Can you take a look at those approaches and let me know if I'm missing an intended approach to this problem?

Currently, I don't see any way to work with dropdown menus from a popover toolbar when sharing focus.

@gspencergoog
Copy link
Contributor

@matthew-carroll Thanks for the thorough examples.

Would it help if popup menus didn't steal focus? There's no reason why they need to: buttons don't typically take focus when they are clicked on, and the fact that popup menus push a route is just an implementation detail. If they didn't push a route, and didn't steal focus, then this would be a non-issue, if I understand correctly.

As another alternative, would it help to have a Focus-type widget that allows you to specify the parent focus node (or just an optional argument to Focus itself)? Then it would maintain the parent relationship for you, allowing you to create focus trees that don't match the widget hierarchy. I have a feeling that this will have a lot of foot-guns in it, so I'm not sure it is a viable solution, but it might work.

@matthew-carroll
Copy link
Contributor Author

@gspencergoog I think that both of those changes sound reasonable at this point. Maybe Super Editor could try to integrate a framework PR to prove the fix?

For the Focus change, if you're worried about misuse, I think that you can probably include sternly worded Dart Docs about why that capability exists, why it's risky, and deter developers from using it unless they fully understand the consequences. As I mentioned before, at the moment, we've replicated all of Focus just to add that one property. That duplication, and our need to maintain it, is certainly a much bigger foot gun for us than the existence of that property in the framework :)

But if there's a better approach that fully solves the problem in a relatively simple way, I'm open to that, too. I'm not sure what it would be, because I think that fundamentally we're looking at focus relationships that truly don't reflect the widget/layout relationships. So I think that, maybe, it is what it is.

@Pierre-Monier
Copy link

just an optional argument to Focus itself

This could be very nice :)

@gspencergoog
Copy link
Contributor

I've created a PR to allow setting of the parent focus node explicitly, in #113655 . Let me know if that addresses your use case.

@angelosilvestre
Copy link
Contributor

@gspencergoog I'm trying to make a DropdownButton work with the new parent focus property. I created this example to simulate a toolbar being displayed inside an OverLayEntry, but, as soon as I click on the DropDownButton, it steals the focus from the main FocusNode.

Code sample
import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: Overlay(
        initialEntries: [
          OverlayEntry(
            builder: (context) {
              return FocusApp();
            },
          )
        ],
      ),
    ),
  );
}

class FocusApp extends StatefulWidget {
  const FocusApp({super.key});

  @override
  State<FocusApp> createState() => _FocusAppState();
}

class _FocusAppState extends State<FocusApp> {
  final FocusNode _mainFocusNode = FocusNode();
  OverlayEntry? _toolbarOverlayEntry;
  String? _dropDownValue = "1";

  @override
  void initState() {
    super.initState();
    _mainFocusNode.requestFocus();
    _mainFocusNode.addListener(_onFocusChanged);
  }

  @override
  void dispose() {
    _mainFocusNode.dispose();
    super.dispose();
  }

  void _onFocusChanged() {
    setState(() {});
  }

  void _onTap() {
    _showToolbar();
  }

  void _showToolbar() {
    if (_toolbarOverlayEntry != null) {
      return;
    }

    _toolbarOverlayEntry = OverlayEntry(
      builder: (context) {
        // FocusScope with the new property.
        return FocusScope(
          parentNode: _mainFocusNode,
          child: _buildToolbar(),
        );
      },
    );

    final overlay = Overlay.of(context);
    overlay.insert(_toolbarOverlayEntry!);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onTap: _onTap,
        child: Focus(
          focusNode: _mainFocusNode,
          child: Container(
            color: Colors.blue,
            child: Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text('Click here to show the overlay'),
                  SizedBox(height: 30),
                  Text('Is mainFocus focused: ${_mainFocusNode.hasFocus}'),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildToolbar() {
    return Material(
      color: Colors.transparent,
      elevation: 5,
      clipBehavior: Clip.hardEdge,
      child: Stack(
        children: [
          Positioned(
            top: 10,
            left: 10,
            child: SizedBox(
              child: DropdownButton<String>(
                value: _dropDownValue,
                icon: const Icon(Icons.arrow_drop_down),
                onChanged: (value) {
                  setState(() {
                    _dropDownValue = value;
                  });
                },
                items: [
                  DropdownMenuItem<String>(
                    value: "1",
                    child: Text('Option 1'),
                  ),
                  DropdownMenuItem<String>(
                    child: Text('Option 2'),
                    value: "2",
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

@hillelcoren
Copy link
Member

@gspencergoog do you have any suggestions to resolve the issue in @angelosilvestre's sample?

After upgrading to Flutter 3.7 we're now also facing this problem in our app using @matthew-carroll's Super Editor package.

Thanks! Sorry for the spam comment, just sad to have to hide this useful feature in our app for now.

@matthew-carroll
Copy link
Contributor Author

@justinmc I wanted to put this back on your radar, in case you're able to push this forward somewhere. It looks like Greg hasn't had an opportunity to circle back on this.

It was brought to my attention that part of the Super Editor demo is still broken (it's been broken for a year now), and blocked by this issue. It looks like Hillel is blocked, too.

Is there anyone who can get some forward movement on this, so we can get the Super Editor demo working, and also help our customers who depend upon the ability to open dropdown menus and enter text into popup overlays?

@flutter-triage-bot flutter-triage-bot bot added multiteam-retriage-candidate team-design Owned by Design Languages team triaged-design Triaged by Design Languages team labels Jul 8, 2023
@gnprice gnprice added team-framework Owned by Framework team triaged-framework Triaged by Framework team and removed team-design Owned by Design Languages team triaged-design Triaged by Design Languages team labels Jul 18, 2023
@flutter-triage-bot flutter-triage-bot bot removed the triaged-framework Triaged by Framework team label Sep 12, 2023
@flutter-triage-bot
Copy link

This issue is missing a priority label. Please set a priority label when adding the triaged-framework label.

@goderbauer goderbauer added P3 Issues that are less important to the Flutter project triaged-framework Triaged by Framework team labels Sep 12, 2023
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 c: new feature Nothing broken; request for a new capability c: proposal A detailed proposal for a change to Flutter f: focus Focus traversal, gaining or losing focus f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. P3 Issues that are less important to the Flutter project team-framework Owned by Framework team triaged-framework Triaged by Framework team
Projects
None yet
Development

No branches or pull requests