Skip to content

Commit 4aa8b3a

Browse files
authored
Work around XAML Islands issues with TextCommandBarFlyout (#8249)
* Work around XAML Islands issue with TextCommandBarFlyout As reported in microsoft/microsoft-ui-xaml#5341, the singleton TextCommandBarFlyout for the XAML TextBox and TextBlock components are limited to one window. When the flyout opens on a different window after already being opened on another, the first window the flyout was opened on gets focus, and the flyout is immediately closed. This change creates a TextCommandBarFlyout per TextInput native view and per selection TextBlock native view. * Change files * Adds helper to share logic between TextInputViewManager and TextViewManager * Adds fix for microsoft-ui-xaml#3529 for proofing menu * Clear flyout property settings when TextBlock is no longer selectable * Ensure multi-window flyout issue is also applied to PasswordBox
1 parent 912ff89 commit 4aa8b3a

9 files changed

+139
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Work around XAML Islands issue with TextCommandBarFlyout",
4+
"packageName": "react-native-windows",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj

+2
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@
306306
<ClInclude Include="Utils\UwpPreparedScriptStore.h" />
307307
<ClInclude Include="Utils\UwpScriptStore.h" />
308308
<ClInclude Include="Utils\ValueUtils.h" />
309+
<ClInclude Include="Utils\XamlIslandUtils.h" />
309310
<ClInclude Include="Views\ActivityIndicatorViewManager.h" />
310311
<ClInclude Include="Views\ControlViewManager.h" />
311312
<ClInclude Include="Views\DevMenu.h" />
@@ -610,6 +611,7 @@
610611
<ClCompile Include="Utils\UwpPreparedScriptStore.cpp" />
611612
<ClCompile Include="Utils\UwpScriptStore.cpp" />
612613
<ClCompile Include="Utils\ValueUtils.cpp" />
614+
<ClCompile Include="Utils\XamlIslandUtils.cpp" />
613615
<ClCompile Include="Views\ActivityIndicatorViewManager.cpp" />
614616
<ClCompile Include="Views\ConfigureBundlerDlg.cpp" />
615617
<ClCompile Include="Views\ControlViewManager.cpp" />

vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters

+6
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,9 @@
290290
<ClCompile Include="Utils\ValueUtils.cpp">
291291
<Filter>Utils</Filter>
292292
</ClCompile>
293+
<ClCompile Include="Utils\XamlIslandUtils.cpp">
294+
<Filter>Utils</Filter>
295+
</ClCompile>
293296
<ClCompile Include="JsiApi.cpp" />
294297
<ClCompile Include="RedBoxErrorInfo.cpp" />
295298
<ClCompile Include="RedBoxErrorFrameInfo.cpp" />
@@ -631,6 +634,9 @@
631634
<ClInclude Include="Utils\ValueUtils.h">
632635
<Filter>Utils</Filter>
633636
</ClInclude>
637+
<ClInclude Include="Utils\XamlIslandUtils.h">
638+
<Filter>Utils</Filter>
639+
</ClInclude>
634640
<ClInclude Include="XamlLoadState.h" />
635641
<ClInclude Include="XamlView.h" />
636642
<ClInclude Include="ReactHost\IReactInstance.h">

vnext/Microsoft.ReactNative/Utils/Helpers.cpp

+8
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ bool IsAPIContractV8Available() {
7878
return IsAPIContractVxAvailable<8>();
7979
}
8080

81+
bool IsAPIContractV12Available() {
82+
return IsAPIContractVxAvailable<12>();
83+
}
84+
8185
bool IsRS3OrHigher() {
8286
return IsAPIContractV5Available();
8387
}
@@ -94,6 +98,10 @@ bool Is19H1OrHigher() {
9498
return IsAPIContractV8Available();
9599
}
96100

101+
bool Is21H1OrHigher() {
102+
return IsAPIContractV12Available();
103+
}
104+
97105
bool IsXamlIsland() {
98106
AppPolicyWindowingModel e;
99107
if (FAILED(AppPolicyGetWindowingModel(GetCurrentThreadEffectiveToken(), &e)) ||

vnext/Microsoft.ReactNative/Utils/Helpers.h

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ bool IsRS3OrHigher();
2929
bool IsRS4OrHigher();
3030
bool IsRS5OrHigher();
3131
bool Is19H1OrHigher();
32+
bool Is21H1OrHigher();
3233

3334
bool IsXamlIsland();
3435
bool IsWinUI3Island();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
#include "Utils/XamlIslandUtils.h"
5+
6+
#include <UI.Xaml.Input.h>
7+
8+
namespace Microsoft::ReactNative {
9+
10+
struct CustomAppBarButton : xaml::Controls::AppBarButtonT<CustomAppBarButton> {
11+
void OnPointerExited(xaml::Input::PointerRoutedEventArgs const &e) {
12+
// This method crashes in the superclass, so we purposely don't call super. But the superclass
13+
// implementation likely cancels a timer that will show the submenu shortly after pointer enter.
14+
// Since we don't have access to that timer, instead we reset the Flyout property, which resets
15+
// the timer. This also fixes a crash where you can get a zombie submenu showing if the app
16+
// loses focus while this timer is scheduled to show the submenu.
17+
if (auto flyout = this->Flyout()) {
18+
this->Flyout(nullptr);
19+
this->Flyout(flyout);
20+
}
21+
22+
// The superclass implementation resets the button to the normal state, so we do this ourselves.
23+
this->SetValue(xaml::Controls::Primitives::ButtonBase::IsPointerOverProperty(), winrt::box_value(false));
24+
xaml::VisualStateManager::GoToState(*this, L"Normal", false);
25+
}
26+
27+
void OnPointerPressed(xaml::Input::PointerRoutedEventArgs const &e) {
28+
// Clicking AppBarButton by default will dismiss the menu, but since we only use this class for
29+
// submenus we override it to be a no-op so it behaves like MenuFlyoutSubItem.
30+
}
31+
};
32+
33+
void FixProofingMenuCrashForXamlIsland(xaml::Controls::TextCommandBarFlyout const &flyout) {
34+
flyout.Opening([](winrt::IInspectable const &sender, auto &&) {
35+
const auto &flyout = sender.as<xaml::Controls::TextCommandBarFlyout>();
36+
if (const auto &textBox = flyout.Target().try_as<xaml::Controls::TextBox>()) {
37+
const auto &commands = flyout.SecondaryCommands();
38+
for (uint32_t i = 0; i < commands.Size(); ++i) {
39+
if (const auto &appBarButton = commands.GetAt(i).try_as<xaml::Controls::AppBarButton>()) {
40+
if (appBarButton.Flyout() == textBox.ProofingMenuFlyout()) {
41+
// Replace the AppBarButton for the proofing menu with one that doesn't crash
42+
const auto customAppBarButton = winrt::make<CustomAppBarButton>();
43+
customAppBarButton.Label(appBarButton.Label());
44+
customAppBarButton.Icon(appBarButton.Icon());
45+
customAppBarButton.Flyout(appBarButton.Flyout());
46+
commands.RemoveAt(i);
47+
commands.InsertAt(i, customAppBarButton);
48+
49+
// There is only one proofing menu option
50+
break;
51+
}
52+
}
53+
}
54+
}
55+
});
56+
}
57+
58+
} // namespace Microsoft::ReactNative
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
#pragma once
5+
6+
#include "Utils/Helpers.h"
7+
8+
#include <UI.Xaml.Controls.Primitives.h>
9+
#include <UI.Xaml.Controls.h>
10+
11+
namespace Microsoft::ReactNative {
12+
13+
void FixProofingMenuCrashForXamlIsland(xaml::Controls::TextCommandBarFlyout const &flyout);
14+
15+
template <typename T>
16+
inline void EnsureUniqueTextFlyoutForXamlIsland(T const &textView) {
17+
// This works around a XAML Islands bug where the XamlRoot of the first
18+
// window the flyout is shown on takes ownership of the flyout and attempts
19+
// to show the flyout on other windows cause the first window to get focus.
20+
// https://github.com/microsoft/microsoft-ui-xaml/issues/5341
21+
if (IsXamlIsland()) {
22+
xaml::Controls::TextCommandBarFlyout flyout;
23+
flyout.Placement(xaml::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedLeft);
24+
25+
// This works around a XAML Islands bug where the Proofing sub-menu for
26+
// TextBox causes a crash while animating to open / close before 21H1.
27+
// https://github.com/microsoft/microsoft-ui-xaml/issues/3529
28+
if constexpr (std::is_same_v<T, xaml::Controls::TextBox>) {
29+
if (!Is21H1OrHigher()) {
30+
FixProofingMenuCrashForXamlIsland(flyout);
31+
}
32+
}
33+
34+
textView.ContextFlyout(flyout);
35+
textView.SelectionFlyout(flyout);
36+
}
37+
}
38+
39+
inline void ClearUniqueTextFlyoutForXamlIsland(xaml::Controls::TextBlock const &textBlock) {
40+
textBlock.ClearValue(xaml::UIElement::ContextFlyoutProperty());
41+
textBlock.ClearValue(xaml::Controls::TextBlock::SelectionFlyoutProperty());
42+
}
43+
44+
} // namespace Microsoft::ReactNative

vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "TextInputViewManager.h"
77

88
#include "Unicode.h"
9+
#include "Utils/XamlIslandUtils.h"
910

1011
#include <UI.Xaml.Controls.h>
1112
#include <UI.Xaml.Input.h>
@@ -206,11 +207,13 @@ void TextInputShadowNode::registerEvents() {
206207
m_passwordBoxPasswordChangedRevoker = {};
207208
m_passwordBoxPasswordChangingRevoker = {};
208209
auto textBox = control.as<xaml::Controls::TextBox>();
210+
EnsureUniqueTextFlyoutForXamlIsland(textBox);
209211
m_textBoxTextChangingRevoker = textBox.TextChanging(
210212
winrt::auto_revoke, [=](auto &&, auto &&) { dispatchTextInputChangeEvent(textBox.Text()); });
211213
} else {
212214
m_textBoxTextChangingRevoker = {};
213215
auto passwordBox = control.as<xaml::Controls::PasswordBox>();
216+
EnsureUniqueTextFlyoutForXamlIsland(passwordBox);
214217
if (control.try_as<xaml::Controls::IPasswordBox4>()) {
215218
m_passwordBoxPasswordChangingRevoker = passwordBox.PasswordChanging(
216219
winrt::auto_revoke, [=](auto &&, auto &&) { dispatchTextInputChangeEvent(passwordBox.Password()); });

vnext/Microsoft.ReactNative/Views/TextViewManager.cpp

+10-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "pch.h"
55

66
#include "TextViewManager.h"
7+
#include "Utils/XamlIslandUtils.h"
78

89
#include <Views/RawTextViewManager.h>
910
#include <Views/ShadowNodeBase.h>
@@ -211,10 +212,16 @@ bool TextViewManager::UpdateProperty(
211212
textBlock.ClearValue(xaml::Controls::TextBlock::LineStackingStrategyProperty());
212213
}
213214
} else if (propertyName == "selectable") {
214-
if (propertyValue.Type() == winrt::Microsoft::ReactNative::JSValueType::Boolean)
215-
textBlock.IsTextSelectionEnabled(propertyValue.AsBoolean());
216-
else if (propertyValue.IsNull())
215+
if (propertyValue.Type() == winrt::Microsoft::ReactNative::JSValueType::Boolean) {
216+
const auto selectable = propertyValue.AsBoolean();
217+
textBlock.IsTextSelectionEnabled(selectable);
218+
if (selectable) {
219+
EnsureUniqueTextFlyoutForXamlIsland(textBlock);
220+
}
221+
} else if (propertyValue.IsNull()) {
217222
textBlock.ClearValue(xaml::Controls::TextBlock::IsTextSelectionEnabledProperty());
223+
ClearUniqueTextFlyoutForXamlIsland(textBlock);
224+
}
218225
} else if (propertyName == "allowFontScaling") {
219226
if (propertyValue.Type() == winrt::Microsoft::ReactNative::JSValueType::Boolean) {
220227
textBlock.IsTextScaleFactorEnabled(propertyValue.AsBoolean());

0 commit comments

Comments
 (0)