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

TextField inside Offstage and Stack not showing keyboard #17098

Closed
enricoquarantini opened this issue Apr 29, 2018 · 28 comments

Comments

@enricoquarantini
Copy link

commented Apr 29, 2018

Steps to Reproduce

I'm building a bottom navigation where every tab has its own navigation. I need to include a MaterialApp for each tab. I wrap each one inside an Offstage and all the offstages inside a Stack.

Everything is working except the Textfield, they never show the keyboard. They receive the tap but they won't show any keyboard (the physical keyboard of my mac won't work either).
The last child off the stack seems to be the problem.

This is what happens when i tap on the Textfields:
http://www.giphy.com/gifs/69oi3ZjKDYidS2vQmz

I simplified the code to reproduce the problem:

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    Stack stack = new Stack(
      children: <Widget>[
        _getStackPage(false),
        _getStackPage(true), // set offstageFlag to false to show keyboard
      ],
    );

    MaterialApp mainMaterialApp= new MaterialApp(
      home: new Scaffold(
        body: stack,
      ),
    );

    return mainMaterialApp;
  }

  Widget _getStackPage(bool offstageFlag) {
    Offstage offstage = new Offstage(
      offstage: offstageFlag,
      child: new MaterialApp(
        home: new Scaffold(
          body: new Center(
            child: new TextField(),
          ),
        ),
      ),
    );

    return offstage;
  }
}

Logs

Launching lib/main.dart on iPhone 6 in debug mode...
Running Xcode clean...                                       0.9s
Starting Xcode build...                                          
 ├─Assembling Flutter resources...                    3.4s
 └─Compiling, linking and signing...                  2.0s
Xcode build done                                             6.5s
    CADisplay.name = LCD;
    CADisplay.deviceName = PurpleMain;
    CADisplay.seed = 1;
    tags = 0;
    currentMode = <FBSDisplayMode: 0x604000099960; 375x667@2x (750x1334/2) 60Hz sRGB SDR>;
    safeOverscanRatio = {0.89999997615814209, 0.89999997615814209};
    nativeCenter = {375, 667};
    pixelSize = {750, 1334};
    bounds = {{0, 0}, {375, 667}};
    CADisplay = <CADisplay:LCD PurpleMain>;
}
Syncing files to device iPhone 6...                          1.9s

🔥  To hot reload your app on the fly, press "r". To restart the app entirely, press "R".
An Observatory debugger and profiler on iPhone 6 is available at: http://127.0.0.1:8100/
For a more detailed help message, press "h". To quit, press "q".

Flutter Doctor

[✓] Flutter (Channel beta, v0.2.8, on Mac OS X 10.13.4 17E199, locale en-IT)
    • Flutter version 0.2.8 at /Users/enrico/Projects/Frameworks/flutter
    • Framework revision b397406561 (4 weeks ago), 2018-04-02 13:53:20 -0700
    • Engine revision c903c217a1
    • Dart version 2.0.0-dev.43.0.flutter-52afcba357

[✓] Android toolchain - develop for Android devices (Android SDK 27.0.3)
    • Android SDK at /Users/enrico/Library/Android/sdk
    • Android NDK location not configured (optional; useful for native profiling support)
    • Platform android-27, build-tools 27.0.3
    • ANDROID_HOME = /Users/enrico/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1024-b01)
    • All Android licenses accepted.

[✓] iOS toolchain - develop for iOS devices (Xcode 9.3)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 9.3, Build version 9E145
    • ios-deploy 1.9.2
    • CocoaPods version 1.4.0

[✓] Android Studio (version 3.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1024-b01)

[✓] VS Code (version 1.22.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Dart Code extension version 2.12.0

[✓] Connected devices (1 available)
    • iPhone 6 • 1CB3B003-E6C2-41AA-BFB9-C6D3BBC565BA • ios • iOS 11.3 (simulator)

• No issues found!
@slightfoot

This comment has been minimized.

Copy link
Member

commented Apr 29, 2018

The issue is because you have a MaterialApp widget inside another MaterialApp. It's nothing related to Offstage. You also "can" but probably don't want Scaffold inside another Scaffold.

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Scaffold(
        body: new Stack(
          children: <Widget>[
            _getStackPage(false),
            _getStackPage(true), // set offstageFlag to false to show keyboard
          ],
        ),
      ),
    );
  }

  Widget _getStackPage(bool offstageFlag) {
    Offstage offstage = new Offstage(
      offstage: offstageFlag,
      child: new Center(
        child: new TextField(),
      ),
    );

    return offstage;
  }
}
@enricoquarantini

This comment has been minimized.

Copy link
Author

commented Apr 30, 2018

Your code solve the problem, but I need to have a navigation for each tab.
Each page inside the stack is a tab and must have its own navigation independent from the others.

Is there any way to achieve this without using a MaterialApp inside a MaterialApp?

@slightfoot

This comment has been minimized.

Copy link
Member

commented Apr 30, 2018

@enricoquarantini just put a Navigator in your code then. Create each tabs' Navigator with a different initialRoute and you might as well use the same onGenerateRoute callback to create the routes. Perhaps post one of your inner MaterialApp implementations and I can better explain how to integrate it.

@enricoquarantini

This comment has been minimized.

Copy link
Author

commented May 5, 2018

@slightfoot thank you, but it doesn't resolve the problem.
I replaced the nested MaterialApp with a Navigator, but the keyboard doesn't appear.

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    Stack stack = new Stack(
      children: <Widget>[
        _getStackPage(false),
        _getStackPage(true), // set offstageFlag to false to show keyboard
      ],
    );

    MaterialApp mainMaterialApp = new MaterialApp(
      home: stack,
    );

    return mainMaterialApp;
  }

  Widget _getStackPage(bool offstageFlag) {
    Navigator navigator =
        new Navigator(onGenerateRoute: (RouteSettings settings) {
      return new MaterialPageRoute(
        builder: (_) => new Scaffold(
              body: new Center(
                child: new TextField(),
              ),
            ),
      );
    });

    Offstage offstage = new Offstage(
      offstage: offstageFlag,
      child: navigator,
    );

    return offstage;
  }
}
@eddywm

This comment has been minimized.

Copy link

commented May 31, 2018

I just ran into the same issue with stacked views used in BottomNavBar.

@omarkamal

This comment has been minimized.

Copy link

commented Jun 11, 2018

I'm stuck right here too. This isn't good, I hope the good folks around here come up with a fix.

@zoechi

This comment has been minimized.

Copy link
Contributor

commented Jun 11, 2018

@sroddy

This comment has been minimized.

Copy link
Contributor

commented Jun 25, 2018

@Hixie The problem is that Offstage widgets are not excluded from the FocusScopeNode logic, and as Navigators create a FocusScope tree with autofocus true, the last one in the widget tree automatically gains focus no matter if it is actually visible or not.
The other problem is that when you tap on a text field that is inside a Navigator that is currently not focused, it doesn't gain focus and the Keyboard doesn't show up.

cc @xster as I saw him solve a similar issue here #14431

@Hixie

This comment has been minimized.

Copy link
Contributor

commented Jun 27, 2018

Creating a separate MaterialApp for each tab is likely to get you in all kinds of trouble. You're better off having a separate Navigator rather than a separate MaterialApp.

@sroddy

This comment has been minimized.

Copy link
Contributor

commented Jun 27, 2018

@Hixie that issue is not related to having separate MaterialApps. The problem is related to the fact that there are different Navigators inside the same stack on the same hierarchy level.

e.g.

new Stack(
  fit: StackFit.expand,
  children: [navigatorA, new Offstage(offstage: isActive, child: navigatorB)]
);

As I wrote above, navigatorB gets automatically focused as soon as it's first inserted into the Widget tree (the FocusScope that it generates has autofocus true) even if it's inside an Offstage widget with the offstage bool set to true.

Now, if you have a TextField inside navigatorA, even if navigatorB is offstage and not visible, and even if the user can interact with it (and with all the other elements in the active navigatorA's route), once they tap on it, the keyboard doesn't show up.

I really had an hard time trying to figure out why if a user explicitly taps on a textfield that is inside any FocusNode, the active FocusNode doesn't get set on all the parents up to the root, but only on the first one.

That would solve this specific issue although it would masquer what imho is the real issue: offstage FocusScopes should be entirely excluded from the FocusScopeNode logic, as items inside an offstage widgets wouldn't have any possible form of user interaction on them.

@Hixie

This comment has been minimized.

Copy link
Contributor

commented Jun 27, 2018

right, you want a Navigator on the outside of that Stack as well. Typically, the one provided by the MaterialApp.

OffStage doesn't do anything except turn off painting and make the layout ignore the child. It's not a way to remove widgets from the app. It's a way to measure widgets before showing them.

@sroddy

This comment has been minimized.

Copy link
Contributor

commented Jun 27, 2018

@Hixie let's assume for a moment that the two navigators inside the stack are not full screen and both of them are visible and occupy some portion of the screen: I don't get why if there is one text field on each of them, the interactions with the keyboard would work on just one of them and be ignored on the other one. Why the tap on the textfield doesn't apply the focus on all the parent FocusScopeNodes up to the root?

@Hixie

This comment has been minimized.

Copy link
Contributor

commented Jun 28, 2018

If they're just next to each other and there's no ancestor focus scope then they're both going to think they're the root, which will cause weirdness. That's why I was suggesting having a root MaterialApp with the two Navigators inside that.

It may still not work, in which case it's probably because our focus code is a bit primitive right now. :-)

@sroddy

This comment has been minimized.

Copy link
Contributor

commented Jun 28, 2018

Thanks for the suggestion @Hixie . I just gave a try to your proposed solution and unfortunately it doesn't seem to work. If you try this minimal example you will notice that if you tap on the top TextField, although you can see the ink splash, the keyboard won't open.
On the bottom one, everything is working as expected.

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Stack(
        fit: StackFit.expand,
        children: <Widget>[
          new Positioned(
            top: 100.0,
            left: 0.0,
            right: 0.0,
            height: 150.0,
            child: new Navigator(
              onGenerateRoute: (_) => new MaterialPageRoute(
                    builder: (_) => new Center(child: new TextField()),
                  ),
            ),
          ),
          new Positioned(
            top: 250.0,
            left: 0.0,
            right: 0.0,
            height: 150.0,
            child: new Navigator(
              onGenerateRoute: (_) => new MaterialPageRoute(
                    builder: (_) => new Center(child: new TextField()),
                  ),
            ),
          ),
        ],
      ), 
    );
  }
}
@sroddy

This comment has been minimized.

Copy link
Contributor

commented Jun 28, 2018

With the following change inside FocusScopeNode, the behaviour is fixed.

  void requestFocus(FocusNode node) {
    assert(node != null);

    // set the first focus on all parents up to the root
    FocusScopeNode current = this;
    while (current != null) {
      current._parent?.setFirstFocus(current);
      current = current._parent;
    }

    if (_focus == node)
      return;
    _focus?.unfocus();
    node._hasKeyboardToken = true;
    _setFocus(node);
  }

However reading the docs of that method, it seems that the former behaviour was intentional. I still don't understand what are the cases in which when you request the focus of an element you shouldn't bubble up the focus to all the parents but I might be missing the bigger picture.

@eseidelGoogle

This comment has been minimized.

Copy link
Contributor

commented Aug 1, 2018

My understanding is the original user intent (including from the tweet: https://twitter.com/omariskamal/status/1006131705495805953) is to have a permanent iOS-style navigation bar shared between screens, correct? Maybe we should file a separate bug about Flutter offering an official example of such? (FYI @xster)

@xster

This comment has been minimized.

Copy link
Contributor

commented Aug 1, 2018

@sroddy your last snippet is mostly on the right path. This is what the CupertinoTabScaffold does to manage focus (it also uses multiple navigators inside a stack of offstages) https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/tab_scaffold.dart#L229.

Is the root of your request that you want the Material tab bar to behave the same way navigation-wise with the Cupertino tab bar? There are some recent changes to the Material specs to this point #18740.

cc @willlarche who's collecting feature requests for the Material spec to take on more iOS-y patterns.

@long1eu

This comment has been minimized.

Copy link

commented Aug 5, 2018

any news on this?

@sroddy

This comment has been minimized.

Copy link
Contributor

commented Aug 6, 2018

@xster,

@Hixie was writing a couple of messages above in this thread:

OffStage doesn't do anything except turn off painting and make the layout ignore the child. It's not a way to remove widgets from the app. It's a way to measure widgets before showing them.

Considering that using OffStage to show/hide different subtrees in a complex widget hierarchy looks to be an accepted practice, even in the official code, I think that the FocusScope logic should be clever enough to exclude the parts of the widget tree that are actually not available for focus instead of relying on "fragile imperative tricks" like the one in the tab scaffold (no offenses here :) ) to achieve the desired outcomes.

@Hixie

This comment has been minimized.

Copy link
Contributor

commented Aug 6, 2018

To be clear, that isn't an accepted practice. To hide a subtree, just don't include it in the tree.

@sroddy

This comment has been minimized.

Copy link
Contributor

commented Aug 6, 2018

@Hixie

This comment has been minimized.

Copy link
Contributor

commented Aug 7, 2018

In that case we probably did it because we wanted to keep the contents alive -- e.g. if there's a Navigator in the tab view, we don't want to forget what page it was navigated to.

@AbdulRahmanAlHamali

This comment has been minimized.

Copy link

commented Aug 14, 2018

Hello,
Thank you all for your hard work.
Is there any solution/workaround that we can apply right now for this issue?

@wwwdata

This comment has been minimized.

Copy link

commented Aug 21, 2018

@sroddy the requestFocus fix from you is also working for me. Are there any plans on integrating this fix into the framework? This is currently a major issue for us because we need to use multiple Navigator instances.

@wwwdata

This comment has been minimized.

Copy link

commented Aug 21, 2018

@AbdulRahmanAlHamali My current workaround is that I manually use the FocusScope component and explicitly set the Focus in the correct Navigator. This is exactly how the iOS TabView component works.

so you will have something like this:

class Something extends StatelessWidget {
  FocusScopeNode _focusA;
  FocusScopeNode _focusB;

  @override
  void initState() {
    super.initState();
    _focusA = FocusScopeNode();
    _focusB = FocusScopeNode();
  }

  @override
  void dispose() {
    _focusA.detach();
    _focusA.detach();
  }

  @override
  Widget build(BuildContext context) {
  return Stack(
    children: <Widget>[
      FocusScope(node: _focusA, child: Navigator(...)),
      FocusScope(node: _focusB, child: Navigator(...)),
    ]);
  }
}

And then you need to manually set the FocusScopeNode which you want to use as focused. You can do it like this: FocusScope.of(context).setFirstFocus(_focusA);

@AbdulRahmanAlHamali

This comment has been minimized.

Copy link

commented Aug 28, 2018

@wwwdata great answer that worked for me too!

@zoechi zoechi added this to the Goals milestone Nov 30, 2018

@dvaldivia

This comment has been minimized.

Copy link

commented Jan 20, 2019

I'm having the same problem, TextField cannot be focused after scrolling or navigating using a Navigator

@sh0umik

This comment has been minimized.

Copy link

commented Jan 24, 2019

I am having the same problem, But i am using Visibility instead of Offstage but none of them seems to resolve the problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.