Skip to content

FocusScopeNode.descendantsAreFocusable set to false after disabling and enabling canRequestFocus #147256

Open

Description

@knopp

Happens with current main (3.22.0-16.0.pre.26).

Widget test to reproduce:

  testWidgets('FocusScope descendantsAreFocusable', (WidgetTester tester) async {
    final focusScopeNode = FocusScopeNode();
    await tester.pumpWidget(
      FocusScope(
        node: focusScopeNode,
        child: const SizedBox.shrink(),
      ),
    );
    expect(focusScopeNode.descendantsAreFocusable, isTrue);

    focusScopeNode.canRequestFocus = false;
    expect(focusScopeNode.descendantsAreFocusable, isFalse);
 
    focusScopeNode.canRequestFocus = true;
    expect(focusScopeNode.descendantsAreFocusable, isTrue);

    // Disabling canRequestFocus and updating the widget will permanently disable descendantsAreFocusable.
    focusScopeNode.canRequestFocus = false;

    // After this pump, setting canRequestFocus back will not restore
    // descendantsAreFocusable to true.
    await tester.pumpWidget(
      FocusScope(
        node: focusScopeNode,
        child: const SizedBox.shrink(),
      ),
    );

    focusScopeNode.canRequestFocus = true;

    // Test failure:
    // This should be back to true, but in FocusNode state didUpdateWidget the 
    // descendantsAreFocusable on the focusScopeNode got set to false.
    expect(focusScopeNode.descendantsAreFocusable, isTrue);
  });

This break focus in _ModalScopeState which sets canRequestFocus on the scope node and then rebuilds widget, which in turns sets the descendantsAreFocusable to false.

@gspencergoog, any thoughts? The focus code is quite convoluted, in this case the _FocusState.didUpdateWidget actually overrides property on FocusScopeNode, which is quite surprising.

Activity

  1. knopp commented on Apr 23, 2024

    @knopp
    MemberAuthor

    This is not a recent regression, but I think in our app we trigger rebuild in _ModalScopeState while the canRequestFocus property is set to false on the scope focus node, which leaves the focus scope node with descendantsAreFocusable permanently set to false. I think the primary problem is demonstrated in the example.

  2. darshankawar commented on Apr 24, 2024

    @darshankawar
    Member

    Below is the console error log after running the provided test:

    ══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
    The following TestFailure was thrown running a test:
    Expected: true
      Actual: <false>
    
    When the exception was thrown, this was the stack:
    #4      main.<anonymous closure> (file:///Users/dhs/Documents/NCFlutter/app_foo_stable/test/widget_test.dart:50:5)
    <asynchronous suspension>
    #5      testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:183:15)
    <asynchronous suspension>
    #6      TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1017:5)
    <asynchronous suspension>
    <asynchronous suspension>
    (elided one frame from package:stack_trace)
    
    This was caught by the test expectation on the following line:
      file:///Users/dhs/Documents/NCFlutter/app_foo_stable/test/widget_test.dart line 50
    The test description was:
      FocusScope descendantsAreFocusable
    ════════════════════════════════════════════════════════════════════════════════════════════════════
    
    Test failed. See exception logs above.
    The test description was: FocusScope descendantsAreFocusable
    

    Above occurs on latest master and stable.

    stable, master flutter doctor -v
    [!] Flutter (Channel stable, 3.19.6, on macOS 12.2.1 21D62 darwin-x64, locale
        en-GB)
        • Flutter version 3.19.6 on channel stable at
          /Users/dhs/documents/fluttersdk/flutter
        ! Warning: `flutter` on your path resolves to
          /Users/dhs/Documents/Fluttersdk/flutter/bin/flutter, which is not inside
          your current Flutter SDK checkout at
          /Users/dhs/documents/fluttersdk/flutter. Consider adding
          /Users/dhs/documents/fluttersdk/flutter/bin to the front of your path.
        ! Warning: `dart` on your path resolves to
          /Users/dhs/Documents/Fluttersdk/flutter/bin/dart, which is not inside your
          current Flutter SDK checkout at /Users/dhs/documents/fluttersdk/flutter.
          Consider adding /Users/dhs/documents/fluttersdk/flutter/bin to the front
          of your path.
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 54e66469a9 (6 days ago), 2024-04-17 13:08:03 -0700
        • Engine revision c4cd48e186
        • Dart version 3.3.4
        • DevTools version 2.31.1
        • If those were intentional, you can disregard the above warnings; however
          it is recommended to use "git" directly to perform update checks and
          upgrades.
    
    [!] Xcode - develop for iOS and macOS (Xcode 12.3)
        • Xcode at /Applications/Xcode.app/Contents/Developer
        ! Flutter recommends a minimum Xcode version of 13.
          Download the latest version or update via the Mac App Store.
        • CocoaPods version 1.11.2
    
    [✓] Chrome - develop for the web
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] VS Code (version 1.62.0)
        • VS Code at /Applications/Visual Studio Code.app/Contents
        • Flutter extension version 3.21.0
    
    [✓] Connected device (5 available)
        • SM G975F (mobile)       • RZ8M802WY0X • android-arm64   • Android 11 (API 30)
        • Darshan's iphone (mobile)  • 21150b119064aecc249dfcfe05e259197461ce23 •
          ios            • iOS 14.4.1 18D61
        • iPhone 12 Pro Max (mobile) • A5473606-0213-4FD8-BA16-553433949729     •
          ios            • com.apple.CoreSimulator.SimRuntime.iOS-14-3 (simulator)
        • macOS (desktop)            • macos                                    •
          darwin-x64     • Mac OS X 10.15.4 19E2269 darwin-x64
        • Chrome (web)               • chrome                                   •
          web-javascript • Google Chrome 98.0.4758.80
    
    [✓] HTTP Host Availability
        • All required HTTP hosts are available
    
    ! Doctor found issues in 1 category.
    
    [!] Flutter (Channel master, 3.22.0-14.0.pre.67, on macOS 12.2.1 21D62
        darwin-x64, locale en-GB)
        • Flutter version 3.22.0-14.0.pre.67 on channel master at
          /Users/dhs/documents/fluttersdk/flutter
        ! Warning: `flutter` on your path resolves to
          /Users/dhs/Documents/Fluttersdk/flutter/bin/flutter, which is not inside
          your current Flutter SDK checkout at
          /Users/dhs/documents/fluttersdk/flutter. Consider adding
          /Users/dhs/documents/fluttersdk/flutter/bin to the front of your path.
        ! Warning: `dart` on your path resolves to
          /Users/dhs/Documents/Fluttersdk/flutter/bin/dart, which is not inside your
          current Flutter SDK checkout at /Users/dhs/documents/fluttersdk/flutter.
          Consider adding /Users/dhs/documents/fluttersdk/flutter/bin to the front
          of your path.
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 1a905d508d (20 hours ago), 2024-04-21 06:44:23 -0400
        • Engine revision 75ca2195c9
        • Dart version 3.5.0 (build 3.5.0-83.0.dev)
        • DevTools version 2.35.0-dev.8
        • If those were intentional, you can disregard the above warnings; however
          it is recommended to use "git" directly to perform update checks and
          upgrades.
    
    [!] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
        • Android SDK at /Users/dhs/Library/Android/sdk
        ✗ cmdline-tools component is missing
          Run `path/to/sdkmanager --install "cmdline-tools;latest"`
          See https://developer.android.com/studio/command-line for more details.
        ✗ Android license status unknown.
          Run `flutter doctor --android-licenses` to accept the SDK licenses.
          See https://flutter.dev/docs/get-started/install/macos#android-setup for
          more details.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • Build 13C100
        • CocoaPods version 1.11.2
    
    [✓] Chrome - develop for the web
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] IntelliJ IDEA Ultimate Edition (version 2021.3.2)
        • IntelliJ at /Applications/IntelliJ IDEA.app
        • Flutter plugin version 65.1.4
        • Dart plugin version 213.7228
    
    [✓] VS Code (version 1.62.0)
        • VS Code at /Applications/Visual Studio Code.app/Contents
        • Flutter extension version 3.29.0
    
    [✓] Connected device (3 available)
        • Darshan's iphone (mobile) • 21150b119064aecc249dfcfe05e259197461ce23 • ios
          • iOS 15.3.1 19D52
        • macOS (desktop)           • macos                                    •
          darwin-x64     • macOS 12.2.1 21D62 darwin-x64
        • Chrome (web)              • chrome                                   •
          web-javascript • Google Chrome 109.0.5414.119
    
    [✓] Network resources
        • All expected network resources are available.
    
    ! Doctor found issues in 1 category.
          
    [!] Xcode - develop for iOS and macOS (Xcode 12.3)
        • Xcode at /Applications/Xcode.app/Contents/Developer
        ! Flutter recommends a minimum Xcode version of 13.
          Download the latest version or update via the Mac App Store.
        • CocoaPods version 1.11.2
    
    [✓] Chrome - develop for the web
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] VS Code (version 1.62.0)
        • VS Code at /Applications/Visual Studio Code.app/Contents
        • Flutter extension version 3.21.0
    
    [✓] Connected device (5 available)
        • SM G975F (mobile)       • RZ8M802WY0X • android-arm64   • Android 11 (API 30)
        • Darshan's iphone (mobile)  • 21150b119064aecc249dfcfe05e259197461ce23 •
          ios            • iOS 14.4.1 18D61
        • iPhone 12 Pro Max (mobile) • A5473606-0213-4FD8-BA16-553433949729     •
          ios            • com.apple.CoreSimulator.SimRuntime.iOS-14-3 (simulator)
        • macOS (desktop)            • macos                                    •
          darwin-x64     • Mac OS X 10.15.4 19E2269 darwin-x64
        • Chrome (web)               • chrome                                   •
          web-javascript • Google Chrome 98.0.4758.80
    
    [✓] HTTP Host Availability
        • All required HTTP hosts are available
    
    ! Doctor found issues in 1 category.
    
    
    
    
  3. added
    a: tests"flutter test", flutter_test, or one of our tests
    frameworkflutter/packages/flutter repository. See also f: labels.
    f: focusFocus traversal, gaining or losing focus
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    and removed
    in triagePresently being triaged by the triage team
    on Apr 24, 2024
  4. LongCatIsLooong commented on Apr 24, 2024

    @LongCatIsLooong
    Contributor

    I think using the Focus.withExternalFocusNode/FocusScope.withExternalFocusNode constructors would prevent the widget/element from modifying the node. Maybe the default constructors shouldn't take an external node?

  5. knopp commented on Apr 24, 2024

    @knopp
    MemberAuthor

    My issue here is that it modifies completely unrelated property but doesn't set it back. And the behavior depends on whether the widget gets rebuilt or not, which is quite confusing. This actually causes a bug in modal scope:

    builder: (BuildContext context, Widget? child) {
    final bool ignoreEvents = _shouldIgnoreFocusRequest;
    focusScopeNode.canRequestFocus = !ignoreEvents;
    return IgnorePointer(
    ignoring: ignoreEvents,
    child: child,
    );

    On line 1071, _ModalScopeState temporarily sets canRequestFocus to false. Then, in unlucky scenario, when the FocusScope widget of _ModalScopeState gets rebuilt while canRequestFocus is set to false, the node gets stuck with descendantsAreFocusable == false forever, essentially breaking focus in the application.

    So either _ModalScopeState (and everyone else setting canRequestFocus) must be aware that this can lead to disabling (but not reenabling) descendantsAreFocusable on the scope node, or the scope node should handle this better.

  6. knopp commented on Apr 24, 2024

    @knopp
    MemberAuthor

    As for default constructor not taking a node, that would be a pretty major breaking change. Though TBH the withExternalFocusNode constructor was always a bit confusing to me considering that default constructor also takes (optional) focus node.

  7. gspencergoog commented on Apr 25, 2024

    @gspencergoog
    Contributor

    (Sorry, I was OOO for a few weeks) The history here is that we would very much like to get rid of the node argument in the normal constructor, but it was too large of a breaking change to do. The added withExternalFocus constructors are an attempt to provide the right functionality without breaking things. If I had it to do over again, we would have not included node in the constructor.

    In any case, if you are modifying properties of the focus node after you supply it to the FocusScope widget, you should use the withExternalFocus constructor. Otherwise, the FocusScope widget may end up overwriting them in ways you don't expect.

  8. knopp commented on Apr 25, 2024

    @knopp
    MemberAuthor

    @gspencergoog, in this case it's not me modifying it, but modal route in the framework:

    focusScopeNode.canRequestFocus = !ignoreEvents;

    Should it be fixed to use withExternalFocusNode?

    Also, is there a valid use-case for specifying the node argument in default constructor rather than the .withExternalFocusNode constructor? If there isn't any then maybe it would be worth considering the doing breaking change.

  9. gspencergoog commented on Apr 25, 2024

    @gspencergoog
    Contributor

    Also, is there a valid use-case for specifying the node argument in default constructor rather than the .withExternalFocusNode constructor? If there isn't any then maybe it would be worth considering the doing breaking change.

    If the only reason you're passing the focus node is to be able to call requestFocus from someplace else, then it's valid to pass it in the default constructor.

    Should it be fixed to use withExternalFocusNode?

    Yes, I think you're right. This line:

    should be fixed to use withExternalFocusNode.

    I'll put together a PR.

  10. b3nni97 commented on Apr 25, 2024

    @b3nni97

    Is this issue potentially related to issue #146844?

  11. gspencergoog commented on Apr 25, 2024

    @gspencergoog
    Contributor

    Is this issue potentially related to issue #146844?

    Potentially, since the focus node's focusability is predicated on whether or not it's in a user gesture (like a back swipe). It depends on what the root cause is, though.

  12. gspencergoog commented on Apr 25, 2024

    @gspencergoog
    Contributor

    @knopp I created #147390 , but I was unable to find a case to test where it made a difference. It is still the right way to do things, but since the route's focusScopeNode is never changed (is immutable), the FocusScope widget shouldn't overwrite the skipTraversal or canRequestFocus attributes anyhow.

  13. changed the title [-]FocusScopeNode.descendantsAreFocusable struck to false after disabling and enabling canRequestFocus[/-] [+]`FocusScopeNode.descendantsAreFocusable` struck to false after disabling and enabling `canRequestFocus`[/+] on Apr 25, 2024
  14. changed the title [-]`FocusScopeNode.descendantsAreFocusable` struck to false after disabling and enabling `canRequestFocus`[/-] [+]`FocusScopeNode.descendantsAreFocusable` still set to false after disabling and enabling `canRequestFocus`[/+] on Apr 25, 2024
  15. changed the title [-]`FocusScopeNode.descendantsAreFocusable` still set to false after disabling and enabling `canRequestFocus`[/-] [+]`FocusScopeNode.descendantsAreFocusable` set to false after disabling and enabling `canRequestFocus`[/+] on Apr 25, 2024
  16. knopp commented on Apr 26, 2024

    @knopp
    MemberAuthor

    @gspencergoog, I think something similar to #146844 should work? For us the problem also manifested when using the backswipe gesture on iOS. I'm guessing something there triggers the rebuild in the modal scope state (which updates the FocusScope widget which in turn disables descendantsAreFocusable on the node).

  17. gspencergoog commented on Apr 26, 2024

    @gspencergoog
    Contributor

    @knopp Yeah, after I wrote that comment above, @b3nni97 mentioned that it fixed #146844, so it clearly is doing something that I should be able to test. Was this regression case only visible on iOS, or did it happen more generally?

  18. added
    P2Important issues not at the top of the work list
    on Apr 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work lista: tests"flutter test", flutter_test, or one of our testsf: focusFocus traversal, gaining or losing focusfound in release: 3.19Found to occur in 3.19found in release: 3.22Found to occur in 3.22frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions