Skip to content
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

Keyboard closes on tap outside of super editor in Mobile Web #1979

Open
hawkkiller opened this issue Apr 30, 2024 · 14 comments
Open

Keyboard closes on tap outside of super editor in Mobile Web #1979

hawkkiller opened this issue Apr 30, 2024 · 14 comments
Assignees
Labels
area_supereditor Pertains to SuperEditor area_supertextfield Pertains to SuperTextField bounty_junior f:superlist Funded by Superlist platform_android Applies to use on Android platform_ios Applies to use on iOS platform_web Applies to use on Web status_blocked_by_flutter Something in Flutter doesn't work time:2 type_bug Something isn't working

Comments

@hawkkiller
Copy link

hawkkiller commented Apr 30, 2024

Package Version
Github, main

To Reproduce
Steps to reproduce the behavior:

  1. Run code sample in mobile browser (I tested on iOS)
  2. Focus editor, so that the keyboard is opened
  3. Tap the toggle button
  4. See that the keyboard closes

Minimal Reproduction Code

Minimal, Runnable Code Sample
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:super_editor/super_editor.dart';

void main() {
runApp(const MainApp());
}

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

@override
State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
late final MutableDocument document;
late final Editor editor;
late final MutableDocumentComposer composer;
bool selected1 = false;
bool selected2 = false;
bool selected3 = false;

@override
void initState() {
  super.initState();

  document = MutableDocument.empty();
  composer = MutableDocumentComposer();
  editor =
      createDefaultDocumentEditor(document: document, composer: composer);
}

@override
Widget build(BuildContext context) {
  return MaterialApp(
    home: Builder(builder: (context) {
      return Scaffold(
        body: Padding(
          padding: MediaQuery.of(context).padding,
          child: CustomScrollView(
            slivers: [
              SliverList.list(
                children: [
                  ToggleButtons(
                    onPressed: (index) {},
                    isSelected: const [false, false, false],
                    children: const [
                      Icon(Icons.ac_unit),
                      Icon(Icons.call),
                      Icon(Icons.cake),
                    ],
                  ),
                  const SizedBox(height: 400),
                ],
              ),
              SliverToBoxAdapter(
                child: SuperEditor(
                  editor: editor,
                  document: document,
                  composer: composer,
                ),
              )
            ],
          ),
        ),
      );
    }),
  );
}
}

Actual behavior
Keyboard closes.

Expected behavior
The keyboard is not closed.

Platform
iOS Web.

Additional context
In flutter docs, there is a TextFieldTapRegion that seems to work with text fields (though with some problems).

I have tried wrapping toggle buttons in TapRegion and providing the same group id to SuperEditor, but it doesn't seem to work.

cc: @matthew-carroll @angelosilvestre

@hawkkiller hawkkiller added the type_bug Something isn't working label Apr 30, 2024
@matthew-carroll
Copy link
Contributor

At first glance, this doesn't look like a SuperEditor issue - it looks like a focus management issue.

The focused widget receives key events and IME input, so Super Editor can't keep the keyboard open if focus moves to a different widget. My guess is that tapping on the toggle button is changing focus to that button.

SuperEditor lets you specify a focusNode. It also lets you specify a tagRegionGroupId. Can you please try to use the available focus-related properties on SuperEditor to get the result that you want?

@angelosilvestre do you have any other thoughts on this?

@hawkkiller
Copy link
Author

hawkkiller commented May 1, 2024

It is not necessarily a button. Tapping on any surface in Mobile Web closes the keyboard (material TextField as well). Another problem is that the SuperEditor stays focused (or at least paints the cursor).

RPReplay_Final1714545251.mov

@matthew-carroll
Copy link
Contributor

Tapping on any surface in Mobile Web closes the keyboard (material TextField as well)

Right. I didn't say it was about the button. I said it was about the focus node that's probably inside the button, which is probably receiving focus when you tap on it.

Another problem is that the SuperEditor stays focused (or at least paints the cursor).

This may or may not be a bug. Depends on the exact situation.

@angelosilvestre
Copy link
Collaborator

By default, SuperEditor will close the IME connection when it loses focus. This will cause the keyboard to close.

If you want to keep the keyboard visible, you need to either:

  • Configure the editor to keep the IME connection open even without focus (by providing a SuperEditorImePolicies).
  • Configure the desired widgets to share focus with the editor, so when they are focused, the editor still has non-primary focus.

@matthew-carroll
Copy link
Contributor

@hawkkiller please let us know if Angelo's message resolved your issues.

@hawkkiller
Copy link
Author

Hi @matthew-carroll @angelosilvestre

I've just verified that neither setting tap regions, focus nodes, or configuring the policy doesn't work (at least for the code provided).

Do you have any suggestions to check?

Widget build(BuildContext context) => MaterialApp(
        home: Builder(builder: (context) {
          return Scaffold(
            body: Padding(
              padding: MediaQuery.of(context).padding,
              child: Focus(
                focusNode: _focusNode,
                child: CustomScrollView(
                  slivers: [
                    SliverToBoxAdapter(
                      child: Center(
                        child: TapRegion(
                          groupId: 'super_editor',
                          child: ToggleButtons(
                            onPressed: (index) {},
                            isSelected: const [false, false, false],
                            children: const [
                              Icon(Icons.ac_unit),
                              Icon(Icons.call),
                              Icon(Icons.cake),
                            ],
                          ),
                        ),
                      ),
                    ),
                    SliverToBoxAdapter(
                      child: SuperEditor(
                        imePolicies: const SuperEditorImePolicies(
                          closeKeyboardOnSelectionLost: false,
                          closeImeOnNonPrimaryFocusLost: false,
                          closeKeyboardOnLosePrimaryFocus: false,
                        ),
                        tapRegionGroupId: 'super_editor',
                        focusNode: _focusNode,
                        editor: editor,
                        document: document,
                        composer: composer,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          );
        }),
      );

@angelosilvestre
Copy link
Collaborator

angelosilvestre commented May 9, 2024

@hawkkiller Please try to do something like this:

Widget build(BuildContext context) => MaterialApp(
    home: Builder(builder: (context) {
      return Scaffold(
        body: Padding(
          padding: MediaQuery.of(context).padding,
          child: CustomScrollView(
            slivers: [
              SliverToBoxAdapter(
                child: Center(
                  child: Focus(
                    focusNode: _buttonsFocusNode,
                    parentNode: _editorFocusNode,
                    child: ToggleButtons(
                      onPressed: (index) {},
                      isSelected: const [false, false, false],
                      children: const [
                        Icon(Icons.ac_unit),
                        Icon(Icons.call),
                        Icon(Icons.cake),
                      ],
                    ),
                  ),
                ),
              ),
              SliverToBoxAdapter(
                child: SuperEditor(                                         
                  focusNode: _editorFocusNode,
                  editor: editor,
                  document: document,
                  composer: composer,
                ),
              ),
            ],
          ),
        ),
      );
    }),
  );

@hawkkiller
Copy link
Author

hawkkiller commented May 10, 2024

@angelosilvestre Keyboard still closes and this happens only on the Mobile Web (where clicking anywhere closes the keyboard by default for some reason, which is not typical for other platforms).

For example, this issue doesn't occur on mobile (if launched not via Web).

@hawkkiller
Copy link
Author

hawkkiller commented May 10, 2024

If you need assistance with debugging, testing, or experimenting with potential fixes for this problem, I'm happy to help you.

@angelosilvestre
Copy link
Collaborator

@hawkkiller Did you try to use my sample code and also provide the IME policies?

@hawkkiller
Copy link
Author

@angelosilvestre Yes!

@matthew-carroll matthew-carroll added area_supereditor Pertains to SuperEditor area_supertextfield Pertains to SuperTextField platform_web Applies to use on Web platform_ios Applies to use on iOS platform_android Applies to use on Android bounty_junior f:superlist Funded by Superlist time:2 labels May 26, 2024
@angelosilvestre
Copy link
Collaborator

@matthew-carroll @hawkkiller I'm afraid we might not be able to work around this. I took a look and this seems related to the events the browser send us.

When we tap outside of the currently focused input ( the invisible input that Flutter places to handle the input), the browser sends us a blur event.

The Flutter engine listens for blur events and closes the input connection when that happens:

    // Record start time of blur subscription.
    final Stopwatch blurWatch = Stopwatch()..start();

    // On iOS, blur is trigerred in the following cases:
    //
    // 1. The browser app is sent to the background (or the tab is changed). In
    //    this case, the window loses focus (see [windowHasFocus]),
    //    so we close the input connection with the framework.
    // 2. The user taps on another focusable element. In this case, we refocus
    //    the input field and wait for the framework to manage the focus change.
    // 3. The virtual keyboard is closed by tapping "done". We can't detect this
    //    programmatically, so we end up refocusing the input field. This is
    //    okay because the virtual keyboard will hide, and as soon as the user
    //    taps the text field again, the virtual keyboard will come up.
    // 4. Safari sometimes sends a blur event immediately after activating the
    //    input field. In this case, we want to keep the focus on the input field.
    //    In order to detect this, we measure how much time has passed since the
    //    input field was activated. If the time is too short, we re-focus the
    //    input element.
    subscriptions.add(DomSubscription(activeDomElement, 'blur',
            (_) {
              final bool isFastCallback = blurWatch.elapsed < _blurFastCallbackInterval;
              if (windowHasFocus && isFastCallback) {
                activeDomElement.focus();
              } else {
                owner.sendTextConnectionClosedToFrameworkIfAny(); <----- Here
              }
            }));
  }

Maybe this comment is outdated, or I'm not looking at the correct place, but I'm not seeing how the following case is being handled:

// 2. The user taps on another focusable element. In this case, we refocus
//    the input field and wait for the framework to manage the focus change.

@matthew-carroll
Copy link
Contributor

@justinmc @Renzo-Olivares - Do either of you have insights or info about this situation with Flutter on web when tapping outside the currently focused widget?

@Renzo-Olivares
Copy link
Contributor

I'm not entirely sure what the issue is here but it looks like the frameworks TextField is exhibiting the same behavior. I opened an issue to track this flutter/flutter#149685.

@angelosilvestre angelosilvestre added the status_blocked_by_flutter Something in Flutter doesn't work label Jun 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area_supereditor Pertains to SuperEditor area_supertextfield Pertains to SuperTextField bounty_junior f:superlist Funded by Superlist platform_android Applies to use on Android platform_ios Applies to use on iOS platform_web Applies to use on Web status_blocked_by_flutter Something in Flutter doesn't work time:2 type_bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants