Skip to content

Conversation

@Saadnajmi
Copy link
Collaborator

@Saadnajmi Saadnajmi commented Sep 19, 2022

Please select one of the following

  • I am removing an existing difference between facebook/react-native and microsoft/react-native-macos 👍
  • I am cherry-picking a change from Facebook's react-native into microsoft/react-native-macos 👍
  • I am making a fix / change for the macOS implementation of react-native
  • I am making a change required for Microsoft usage of react-native

Summary

This PR does slight refactoring on how the focusable prop is handled by React Native macOS. [RCTView acceptsFirstResponder] now doesn't depend on [NSApp isFullKeyboardAccessEnabled], and only depends on the prop focusable.

We initially only accepted first responder status if both focusable and [NSApp isFullKeyboardAccessEnabled] were true to properly respect a specific system preference, and only make <View focusable> into a key view (AKA: tab stop) if the user allowed it. However, that's actually the wrong method to override, NSView's canBecomeKeyView is.

Looking at some old Apple docs:

The acceptsFirstResponder method controls whether a responder accepts first responder status when its window asks it to (that is, when makeFirstResponder: is called with the responder as the parameter). The canBecomeKeyView method controls whether the Application Kit allows tabbing to a view. It calls acceptsFirstResponder, but it also checks for other information before determining the value to return, such as whether the view is hidden and whether full keyboard access is on. The canBecomeKeyView method is rarely overridden while acceptsFirstResponder is frequently overridden.

It seems that the default implementation of [NSView canBecomeKeyView] will check that OS preference for us, so we can do less work :).

Discussion

This work is done to unblock a bug in FluentUI React Native, where we want our custom NSMenu Replacement always take keyboard focus, regardless of the OS preference (menus are a special case that shouldn't respect that OS preference).

Natively in Appkit, there are two relevant NSView methods that control keyboard focus: acceptsFirstResponder and `canBecomeKeyView.

  • [NSView acceptsFirstResponder]: controls whether a view can receive focus at all (programmatically, via the key view loop, etc).
  • [NSView canBecomeKeyView]: controls whether a view can receive focus from the key view loop (AKA: Can you press tab to get focus on the view, AKA: Is it a tab stop).
  • [NSApp isFullKeyboardAccessEnabled]: System preference that determines how many controls get keyboard focus. By default it's turned off, so most native Appkit views do not get keyboard focus. See https://microsoft.github.io/apple-ux-guide/KeyboardFocus.html .

In React Native, the View prop focusable is a cross platform prop that determines whether a View receives keyboard focus. We implemented it natively by having acceptsFirstResponder (AKA, can a View receive focus at all) only return true if both focusable is true and [NSApp isFullKeyboardAccessEnabled] is true. This behavior approximately matches the equivalent SwiftUI Modifier.

However, this leaves JS developers at an impasse. There is no way in JS for a developer to "force" a view to always be focusable. I needed this for my custom NSMenu replacement. To further complicate matters, Apple has confusingly introduced a new, similarly named system accessibility preference Full Keyboard Access that allows the user to force every view to be keyboard focusable. This new preference is on both iOS and macOS and seems to use an entirely different keyboard focus ring / OS layer / whatever you want to call it. So it feels like Apple might have forgotten about the original preference [NSApp isFullKeyboardAccessEnabled]? Or at least, would rather you forgot about it and use their new system preference?

Last weird bit... I also needed to override [NSView needsPanelToBecomeKey] so that clicking on a <View focusable> / <Touchable> / <Pressable> wouldn't place keyboard focus on it. That was quite jarring, and doesn't match what an NSControl like NSButton does natively. For more info, see https://stackoverflow.com/questions/55078226/first-responder-on-mouse-down-behavior-nscontrol-and-nsview

While this change doesn't make it easier for JS developers to force a view to always be focusable, it does unblock a native module like FocusZone to force focus. It's not perfect, but I think it's a better state than we were before :).

Changelog

[macOS] [Fixed] - Refactor how focusable is handled natively on macOS

Test Plan

Tested the View test page in RNTester. Notice that when the preference is OFF, only the TextField can receive focus (as it should be).

Screen.Recording.2022-10-25.at.3.30.06.PM.mov

@Saadnajmi Saadnajmi requested a review from a team as a code owner September 19, 2022 17:53
@Saadnajmi
Copy link
Collaborator Author

Tabling this for a bit while I explore an alternate fix.

@mischreiber mischreiber self-requested a review September 27, 2022 17:18
@Saadnajmi Saadnajmi changed the title Update focusable to not depend on [NSApp isFullKeyboardAccessEnabled] Refactor how focusable is handled in RCTView on macPS Oct 24, 2022
@Saadnajmi Saadnajmi changed the title Refactor how focusable is handled in RCTView on macPS Refactor how focusable is handled in RCTView on macOS Oct 24, 2022
@Saadnajmi Saadnajmi changed the title Refactor how focusable is handled in RCTView on macOS Refactor how focusable is handled in RCTView Oct 25, 2022
@Saadnajmi Saadnajmi merged commit dc96e0e into main Oct 26, 2022
@Saadnajmi Saadnajmi deleted the Saadnajmi-patch-1 branch December 8, 2022 17:46
Saadnajmi added a commit that referenced this pull request Sep 19, 2025
Needs #2690 to land first.

## Summary:

Implement focus on RCTViewComponentView. Much of the implementation is
taken from #1437, #2117 and comparing against `RCTView`. The border path
used for `drawFocusRingMask` is the same as what is used for box shadows
and cursors.

## Test Plan:

The focus loop seems nonexistent on both paper and Fabric in RNTester...
but I can verify that calling `ref.current?/.focus()` on a Pressable
displays the focus ring
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants