Skip to content

Commit

Permalink
Suppresses auto-focus under certain conditions (#8393)
Browse files Browse the repository at this point in the history
* Suppresses auto-focus under certain conditions

In multi-window XAML Islands apps that share a single UI thread for each
window, focusing a view on a blurred window causes that window to get
focus. Generally speaking, focusing a window should be explicit, and
it's likely very rare that you would want a view getting focus to cause
the entire window to get focus.

This manifests as problematic in two ways:
1. If you have a flyout shown and the flyout gets dismissed because you
switch to another window, the parent window for the flyout would steal
focus again when react-native-windows attempts to focus the target
element.
2. If you re-render a view with a TextInput with `autoFocus={true}` and
the parent window for that TextInput is blurred, the window will steal
focus.

This change forces the user to first focus the window and update the
active XamlRoot via the `XamlUIService::SetXamlRoot` API before a view
in the window can get focus.

* Change files
  • Loading branch information
rozele committed Mar 31, 2023
1 parent fdcbab5 commit fecd9a4
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Suppresses auto-focus under certain conditions",
"packageName": "react-native-windows",
"email": "erozell@outlook.com",
"dependentChangeType": "patch"
}
22 changes: 21 additions & 1 deletion vnext/Microsoft.ReactNative/Modules/NativeUIManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "pch.h"

#include <QuirkSettings.h>
#include <UI.Xaml.Controls.h>
#include <UI.Xaml.Input.h>
#include <UI.Xaml.Media.h>
Expand Down Expand Up @@ -1138,7 +1139,26 @@ void NativeUIManager::findSubviewIn(

void NativeUIManager::focus(int64_t reactTag) {
if (auto shadowNode = static_cast<ShadowNodeBase *>(m_host->FindShadowNodeForTag(reactTag))) {
xaml::Input::FocusManager::TryFocusAsync(shadowNode->GetView(), winrt::FocusState::Programmatic);
const auto view = shadowNode->GetView();

// Only allow focus on an element with a XamlRoot that matches the current XamlRoot or that
// has a different CoreDispatcher for the XamlRoot, otherwise focusing the element will steal
// window focus, which is generally not desired behavior.
const auto properties = React::ReactPropertyBag(m_context.Properties());
if (winrt::Microsoft::ReactNative::implementation::QuirkSettings::GetSuppressWindowFocusOnViewFocus(properties)) {
if (const auto uiElement = view.try_as<xaml::UIElement>()) {
if (const auto activeXamlRoot = React::XamlUIService::GetXamlRoot(properties.Handle())) {
const auto activeDispatcher = activeXamlRoot.Content() ? activeXamlRoot.Content().Dispatcher() : nullptr;
if (uiElement.XamlRoot() != activeXamlRoot && uiElement.Dispatcher() == activeDispatcher) {
const auto suppressedFocusMessage = L"Suppressed focus on view in XamlRoot not currently in focus.";
m_context.CallJSFunction(L"RCTLog", L"logToConsole", L"warn", suppressedFocusMessage);
return;
}
}
}
}

xaml::Input::FocusManager::TryFocusAsync(view, winrt::FocusState::Programmatic);
}
}

Expand Down
17 changes: 17 additions & 0 deletions vnext/Microsoft.ReactNative/QuirkSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ winrt::Microsoft::ReactNative::ReactPropertyId<bool> MapWindowDeactivatedToAppSt
properties.Set(MapWindowDeactivatedToAppStateInactiveProperty(), value);
}

winrt::Microsoft::ReactNative::ReactPropertyId<bool> SuppressWindowFocusOnViewFocusProperty() noexcept {
winrt::Microsoft::ReactNative::ReactPropertyId<bool> propId{
L"ReactNative.QuirkSettings", L"SuppressWindowFocusOnViewFocus"};

return propId;
}

#pragma region IDL interface

/*static*/ void QuirkSettings::SetMatchAndroidAndIOSStretchBehavior(
Expand Down Expand Up @@ -96,6 +103,12 @@ winrt::Microsoft::ReactNative::ReactPropertyId<bool> MapWindowDeactivatedToAppSt
SetMapWindowDeactivatedToAppStateInactive(ReactPropertyBag(settings.Properties()), value);
}

/*static*/ void QuirkSettings::SetSuppressWindowFocusOnViewFocus(
winrt::Microsoft::ReactNative::ReactInstanceSettings settings,
bool value) noexcept {
ReactPropertyBag(settings.Properties()).Set(SuppressWindowFocusOnViewFocusProperty(), value);
}

#pragma endregion IDL interface

/*static*/ bool QuirkSettings::GetMatchAndroidAndIOSStretchBehavior(ReactPropertyBag properties) noexcept {
Expand All @@ -120,4 +133,8 @@ winrt::Microsoft::ReactNative::ReactPropertyId<bool> MapWindowDeactivatedToAppSt
return properties.Get(MapWindowDeactivatedToAppStateInactiveProperty()).value_or(false);
}

/*static*/ bool QuirkSettings::GetSuppressWindowFocusOnViewFocus(ReactPropertyBag properties) noexcept {
return properties.Get(SuppressWindowFocusOnViewFocusProperty()).value_or(false);
}

} // namespace winrt::Microsoft::ReactNative::implementation
5 changes: 5 additions & 0 deletions vnext/Microsoft.ReactNative/QuirkSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ struct QuirkSettings : QuirkSettingsT<QuirkSettings> {
static bool GetAcceptSelfSigned(winrt::Microsoft::ReactNative::ReactPropertyBag properties) noexcept;
static winrt::Microsoft::ReactNative::BackNavigationHandlerKind GetBackHandlerKind(
winrt::Microsoft::ReactNative::ReactPropertyBag properties) noexcept;
static bool GetSuppressWindowFocusOnViewFocus(winrt::Microsoft::ReactNative::ReactPropertyBag properties) noexcept;

static bool GetEnableFabric(winrt::Microsoft::ReactNative::ReactPropertyBag properties) noexcept;

Expand All @@ -54,6 +55,10 @@ struct QuirkSettings : QuirkSettingsT<QuirkSettings> {
static void SetMapWindowDeactivatedToAppStateInactive(
winrt::Microsoft::ReactNative::ReactInstanceSettings settings,
bool value) noexcept;

static void SetSuppressWindowFocusOnViewFocus(
winrt::Microsoft::ReactNative::ReactInstanceSettings settings,
bool value) noexcept;
#pragma endregion Public API - part of IDL interface
};

Expand Down
6 changes: 6 additions & 0 deletions vnext/Microsoft.ReactNative/QuirkSettings.idl
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,11 @@ namespace Microsoft.ReactNative
"`inactive` tracks the [Window.Activated Event](https://docs.microsoft.com/uwp/api/windows.ui.core.corewindow.activated) when the window is deactivated.")
DOC_DEFAULT("false")
static void SetMapWindowDeactivatedToAppStateInactive(ReactInstanceSettings settings, Boolean value);

DOC_STRING(
"When running multiple windows from a single UI thread, focusing a native view causes the parent "
"window of that view to get focus as well. Set this setting to true to prevent focus of a blurred "
"window when a view in that window is programmatically focused.")
static void SetSuppressWindowFocusOnViewFocus(ReactInstanceSettings settings, Boolean value);
}
} // namespace Microsoft.ReactNative
6 changes: 4 additions & 2 deletions vnext/Microsoft.ReactNative/Views/FlyoutViewManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,14 @@ void FlyoutShadowNode::createView(const winrt::Microsoft::ReactNative::JSValueOb

m_flyoutClosedRevoker = m_flyout.Closed(winrt::auto_revoke, [=](auto &&, auto &&) {
if (!m_updating) {
if (m_targetElement != nullptr) {
if (m_targetTag > 0) {
// When the flyout closes, attempt to move focus to
// its anchor element to prevent cases where focus can land on
// an outer flyout content and therefore trigger a unexpected flyout
// dismissal
xaml::Input::FocusManager::TryFocusAsync(m_targetElement, winrt::FocusState::Programmatic);
if (auto uiManager = GetNativeUIManager(GetViewManager()->GetReactContext()).lock()) {
uiManager->focus(m_targetTag);
}
}

OnFlyoutClosed(GetViewManager()->GetReactContext(), m_tag, false);
Expand Down
4 changes: 3 additions & 1 deletion vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,9 @@ void TextInputShadowNode::registerEvents() {

m_controlLoadedRevoker = control.Loaded(winrt::auto_revoke, [=](auto &&, auto &&) {
if (m_autoFocus) {
control.Focus(xaml::FocusState::Keyboard);
if (auto uiManager = GetNativeUIManager(GetViewManager()->GetReactContext()).lock()) {
uiManager->focus(m_tag);
}
}

auto contentElement = control.GetTemplateChild(L"ContentElement");
Expand Down

0 comments on commit fecd9a4

Please sign in to comment.