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

Refactor how focusable is handled in RCTView #1437

Merged
merged 6 commits into from
Oct 26, 2022
Merged

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
React/Views/RCTView.m Outdated Show resolved Hide resolved
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.

4 participants