diff --git a/change/react-native-windows-0d909f6f-4b48-46c7-97ec-c61c5f2b8087.json b/change/react-native-windows-0d909f6f-4b48-46c7-97ec-c61c5f2b8087.json
new file mode 100644
index 00000000000..db5044f8113
--- /dev/null
+++ b/change/react-native-windows-0d909f6f-4b48-46c7-97ec-c61c5f2b8087.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "Support AccessibilityState: Busy",
+ "packageName": "react-native-windows",
+ "email": "34109996+chiaramooney@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/react-native-windows-408fe985-f784-4dff-aed3-9045a0c7a420.json b/change/react-native-windows-408fe985-f784-4dff-aed3-9045a0c7a420.json
new file mode 100644
index 00000000000..a4b38758eac
--- /dev/null
+++ b/change/react-native-windows-408fe985-f784-4dff-aed3-9045a0c7a420.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "Fix RootComponentView leak",
+ "packageName": "react-native-windows",
+ "email": "53619745+rnbot@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/react-native-windows-4cf47ab7-542b-4ccb-bc2e-46c79be1bb02.json b/change/react-native-windows-4cf47ab7-542b-4ccb-bc2e-46c79be1bb02.json
new file mode 100644
index 00000000000..35229b9592b
--- /dev/null
+++ b/change/react-native-windows-4cf47ab7-542b-4ccb-bc2e-46c79be1bb02.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "[Fabric] implement view tooltip property",
+ "packageName": "react-native-windows",
+ "email": "30809111+acoates-ms@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/react-native-windows-9696c96d-9a44-4ffc-84ea-dd5451818206.json b/change/react-native-windows-9696c96d-9a44-4ffc-84ea-dd5451818206.json
new file mode 100644
index 00000000000..38c2fdeb383
--- /dev/null
+++ b/change/react-native-windows-9696c96d-9a44-4ffc-84ea-dd5451818206.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "Implement IExpandCollapseProvider",
+ "packageName": "react-native-windows",
+ "email": "34109996+chiaramooney@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js b/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js
index ddf1bb03330..1e793fc10e1 100644
--- a/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js
+++ b/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js
@@ -544,6 +544,7 @@ class AccessibilityExample extends React.Component<
> {
state: {tap: number} = {
tap: 0,
+ expanded: true,
};
render(): React.Node {
@@ -554,10 +555,10 @@ class AccessibilityExample extends React.Component<
accessibilityRole="button"
accessibilityValue={{now: this.state.tap}}
accessibilityActions={[
- {name: 'cut', label: 'cut'},
- {name: 'copy', label: 'copy'},
- {name: 'paste', label: 'paste'},
+ {name: 'expand', label: 'expand'},
+ {name: 'collapse', label: 'collapse'},
]}
+ accessibilityState={{expanded: this.state.expanded, busy: true}}
accessibilityPosInSet={1}
accessibilitySetSize={1}
accessibilityLiveRegion='polite'
@@ -566,19 +567,19 @@ class AccessibilityExample extends React.Component<
focusable
onAccessibilityAction={event => {
switch (event.nativeEvent.actionName) {
- case 'cut':
- Alert.alert('Alert', 'cut action success');
- break;
- case 'copy':
- Alert.alert('Alert', 'copy action success');
- break;
- case 'paste':
- Alert.alert('Alert', 'paste action success');
+ case 'expand':
+ this.setState({expanded: true})
break;
+ case 'collapse':
+ this.setState({expanded: false})
}
}}
onAccessibilityTap={() => {
this.setState({tap: this.state.tap + 1});
+ }}
+ onPress={()=>{
+ this.setState({expanded: !this.state.expanded});
+ console.log('Pressed');
}}>
A View with accessibility values.
Current Number of Accessibility Taps: {this.state.tap}
diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap
index 1cb3315b5f0..58a81a88a53 100644
--- a/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap
+++ b/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap
@@ -1094,8 +1094,10 @@ exports[`View Tests Views can have customized accessibility 1`] = `
"Automation Tree": {
"AutomationId": "accessibility",
"ControlType": 50000,
+ "ExpandCollapsePattern.ExpandCollapseState": "Expanded",
"HelpText": "Accessibility Hint",
"IsKeyboardFocusable": true,
+ "ItemStatus": "Busy",
"LiveSetting": "Polite",
"LocalizedControlType": "button",
"Name": "A View with accessibility values",
@@ -6084,12 +6086,24 @@ exports[`View Tests Views can have tooltips 1`] = `
"_Props": {},
},
{
- "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView",
+ "Type": "Microsoft.ReactNative.Composition.ViewComponentView",
"_Props": {},
+ "__Children": [
+ {
+ "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView",
+ "_Props": {},
+ },
+ ],
},
{
- "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView",
+ "Type": "Microsoft.ReactNative.Composition.ViewComponentView",
"_Props": {},
+ "__Children": [
+ {
+ "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView",
+ "_Props": {},
+ },
+ ],
},
],
},
@@ -6125,6 +6139,25 @@ exports[`View Tests Views can have tooltips 1`] = `
"Offset": "0, 0, 0",
"Size": "916, 15",
"Visual Type": "SpriteVisual",
+ "__Children": [
+ {
+ "Offset": "0, 0, 0",
+ "Size": "916, 15",
+ "Visual Type": "SpriteVisual",
+ "__Children": [
+ {
+ "Offset": "0, 0, 0",
+ "Size": "916, 15",
+ "Visual Type": "SpriteVisual",
+ },
+ {
+ "Offset": "0, 0, 0",
+ "Size": "0, 0",
+ "Visual Type": "SpriteVisual",
+ },
+ ],
+ },
+ ],
},
{
"Offset": "0, 0, 0",
@@ -6135,13 +6168,32 @@ exports[`View Tests Views can have tooltips 1`] = `
},
{
"Offset": "0, 29, 0",
- "Size": "916, 16",
+ "Size": "916, 14",
"Visual Type": "SpriteVisual",
"__Children": [
{
"Offset": "0, 0, 0",
- "Size": "916, 16",
+ "Size": "916, 14",
"Visual Type": "SpriteVisual",
+ "__Children": [
+ {
+ "Offset": "0, 0, 0",
+ "Size": "916, 16",
+ "Visual Type": "SpriteVisual",
+ "__Children": [
+ {
+ "Offset": "0, 0, 0",
+ "Size": "916, 16",
+ "Visual Type": "SpriteVisual",
+ },
+ {
+ "Offset": "0, 0, 0",
+ "Size": "0, 0",
+ "Visual Type": "SpriteVisual",
+ },
+ ],
+ },
+ ],
},
{
"Offset": "0, 0, 0",
diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap
index a781314d364..7684c69701c 100644
--- a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap
+++ b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap
@@ -77745,16 +77745,12 @@ exports[`snapshotAllPages View 22`] = `
accessibilityActions={
[
{
- "label": "cut",
- "name": "cut",
+ "label": "expand",
+ "name": "expand",
},
{
- "label": "copy",
- "name": "copy",
- },
- {
- "label": "paste",
- "name": "paste",
+ "label": "collapse",
+ "name": "collapse",
},
]
}
@@ -77764,6 +77760,12 @@ exports[`snapshotAllPages View 22`] = `
accessibilityPosInSet={1}
accessibilityRole="button"
accessibilitySetSize={1}
+ accessibilityState={
+ {
+ "busy": true,
+ "expanded": true,
+ }
+ }
accessibilityValue={
{
"now": 0,
@@ -77773,6 +77775,7 @@ exports[`snapshotAllPages View 22`] = `
focusable={true}
onAccessibilityAction={[Function]}
onAccessibilityTap={[Function]}
+ onPress={[Function]}
testID="accessibility"
>
diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp
index 1b3c1a05bd3..c258f306c22 100644
--- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp
+++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp
@@ -314,6 +314,23 @@ void InsertToggleStateValueIfNotDefault(
}
}
+void InsertExpandCollapseStateValueIfNotDefault(
+ const winrt::Windows::Data::Json::JsonObject &obj,
+ winrt::hstring name,
+ ExpandCollapseState value,
+ ExpandCollapseState defaultValue = ExpandCollapseState::ExpandCollapseState_Collapsed) {
+ if (value != defaultValue) {
+ switch (value) {
+ case 0:
+ obj.Insert(name, winrt::Windows::Data::Json::JsonValue::CreateStringValue(L"Collapsed"));
+ break;
+ case 1:
+ obj.Insert(name, winrt::Windows::Data::Json::JsonValue::CreateStringValue(L"Expanded"));
+ break;
+ }
+ }
+}
+
winrt::Windows::Data::Json::JsonObject ListErrors(winrt::Windows::Data::Json::JsonValue payload) {
winrt::Windows::Data::Json::JsonObject result;
winrt::Windows::Data::Json::JsonArray jsonErrors;
@@ -339,6 +356,7 @@ void DumpUIAPatternInfo(IUIAutomationElement *pTarget, const winrt::Windows::Dat
BOOL isReadOnly;
ToggleState toggleState;
IValueProvider *valuePattern;
+ ExpandCollapseState expandCollapseState;
HRESULT hr;
// Dump IValueProvider Information
@@ -365,6 +383,18 @@ void DumpUIAPatternInfo(IUIAutomationElement *pTarget, const winrt::Windows::Dat
}
togglePattern->Release();
}
+
+ // Dump IExpandCollapseProvider Information
+ IExpandCollapseProvider *expandCollapsePattern;
+ hr = pTarget->GetCurrentPattern(UIA_ExpandCollapsePatternId, reinterpret_cast(&expandCollapsePattern));
+ if (SUCCEEDED(hr) && expandCollapsePattern) {
+ hr = expandCollapsePattern->get_ExpandCollapseState(&expandCollapseState);
+ if (SUCCEEDED(hr)) {
+ InsertExpandCollapseStateValueIfNotDefault(
+ result, L"ExpandCollapsePattern.ExpandCollapseState", expandCollapseState);
+ }
+ expandCollapsePattern->Release();
+ }
}
winrt::Windows::Data::Json::JsonObject DumpUIATreeRecurse(
@@ -381,6 +411,7 @@ winrt::Windows::Data::Json::JsonObject DumpUIATreeRecurse(
int positionInSet = 0;
int sizeOfSet = 0;
LiveSetting liveSetting = LiveSetting::Off;
+ BSTR itemStatus;
pTarget->get_CurrentAutomationId(&automationId);
pTarget->get_CurrentControlType(&controlType);
@@ -389,6 +420,7 @@ winrt::Windows::Data::Json::JsonObject DumpUIATreeRecurse(
pTarget->get_CurrentIsKeyboardFocusable(&isKeyboardFocusable);
pTarget->get_CurrentLocalizedControlType(&localizedControlType);
pTarget->get_CurrentName(&name);
+ pTarget->get_CurrentItemStatus(&itemStatus);
IUIAutomationElement4 *pTarget4;
HRESULT hr = pTarget->QueryInterface(__uuidof(IUIAutomationElement4), reinterpret_cast(&pTarget4));
if (SUCCEEDED(hr) && pTarget4) {
@@ -408,6 +440,7 @@ winrt::Windows::Data::Json::JsonObject DumpUIATreeRecurse(
InsertIntValueIfNotDefault(result, L"PositionInSet", positionInSet);
InsertIntValueIfNotDefault(result, L"SizeofSet", sizeOfSet);
InsertLiveSettingValueIfNotDefault(result, L"LiveSetting", liveSetting);
+ InsertStringValueIfNotEmpty(result, L"ItemStatus", itemStatus);
DumpUIAPatternInfo(pTarget, result);
IUIAutomationElement *pChild;
@@ -423,6 +456,11 @@ winrt::Windows::Data::Json::JsonObject DumpUIATreeRecurse(
if (children.Size() > 0) {
result.Insert(L"__Children", children);
}
+ ::SysFreeString(automationId);
+ ::SysFreeString(helpText);
+ ::SysFreeString(localizedControlType);
+ ::SysFreeString(name);
+ ::SysFreeString(itemStatus);
return result;
}
diff --git a/packages/e2e-test-app/test/__snapshots__/ViewComponentTest.test.ts.snap b/packages/e2e-test-app/test/__snapshots__/ViewComponentTest.test.ts.snap
index 631f203d108..9399d081acf 100644
--- a/packages/e2e-test-app/test/__snapshots__/ViewComponentTest.test.ts.snap
+++ b/packages/e2e-test-app/test/__snapshots__/ViewComponentTest.test.ts.snap
@@ -363,6 +363,8 @@ exports[`ViewTests Views can have a custom nativeID 1`] = `
exports[`ViewTests Views can have accessibility customization 1`] = `
{
"AccessibilityRole": "Button",
+ "AccessibilityStateBusy": true,
+ "AccessibilityStateExpanded": true,
"AutomationId": "accessibility",
"AutomationPositionInSet": 1,
"AutomationSizeOfSet": 1,
diff --git a/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj b/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj
index 2a73b805d36..2031723bd0f 100644
--- a/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj
+++ b/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj
@@ -103,6 +103,7 @@
comsuppw.lib;
Shlwapi.lib;
Version.lib;
+ Dwmapi.lib;
WindowsApp_downlevel.lib;
%(AdditionalDependencies)
diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp
index 0f5a12f6435..83db26575dd 100644
--- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp
@@ -14,6 +14,7 @@
#include
#include "AbiEventEmitter.h"
#include "AbiShadowNode.h"
+#include "ReactCoreInjection.h"
namespace winrt::Microsoft::ReactNative::Composition::implementation {
struct RootComponentView;
@@ -262,6 +263,17 @@ void ComponentView::HandleCommand(const winrt::Microsoft::ReactNative::HandleCom
}
}
+HWND ComponentView::GetHwndForParenting() noexcept {
+ if (m_parent) {
+ return winrt::get_self(m_parent)
+ ->GetHwndForParenting();
+ }
+
+ // Fallback if we do not know any more specific HWND
+ return reinterpret_cast(winrt::Microsoft::ReactNative::implementation::ReactCoreInjection::GetTopLevelWindowId(
+ m_reactContext.Properties().Handle()));
+}
+
winrt::Microsoft::ReactNative::Composition::implementation::RootComponentView *ComponentView::rootComponentView()
const noexcept {
if (m_rootView)
diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h
index 540a2714b85..74bcb459c69 100644
--- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h
@@ -209,6 +209,9 @@ struct ComponentView : public ComponentViewT {
// Notify up the tree to bring the rect into view by scrolling as needed
virtual void StartBringIntoView(BringIntoViewOptions &&args) noexcept;
+ // Eventually PopupContentLink and similar APIs will remove the need for this.
+ virtual HWND GetHwndForParenting() noexcept;
+
virtual const winrt::Microsoft::ReactNative::IComponentProps userProps(
facebook::react::Props::Shared const &props) noexcept;
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp
index e27ed2b265b..2a92f6d75f5 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp
@@ -129,6 +129,18 @@ bool accessibilityValueHasValue(const facebook::react::AccessibilityValue &value
return (value.min.has_value() && value.max.has_value()) || value.now.has_value() || value.text.has_value();
}
+bool expandableControl(const facebook::react::SharedViewProps props) {
+ if (props->accessibilityState.has_value() && props->accessibilityState->expanded.has_value())
+ return true;
+ auto accessibilityActions = props->accessibilityActions;
+ for (size_t i = 0; i < accessibilityActions.size(); i++) {
+ if (accessibilityActions[i].name == "expand" || accessibilityActions[i].name == "collapse") {
+ return true;
+ }
+ }
+ return false;
+}
+
HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTERNID patternId, IUnknown **pRetVal) {
if (pRetVal == nullptr)
return E_POINTER;
@@ -181,6 +193,15 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTE
AddRef();
}
+ if (patternId == UIA_ExpandCollapsePatternId &&
+ (accessibilityRole == "combobox" || accessibilityRole == "splitbutton" || accessibilityRole == "treeitem" ||
+ (expandableControl(props) &&
+ (accessibilityRole == "toolbar" || accessibilityRole == "menuitem" || accessibilityRole == "menubar" ||
+ accessibilityRole == "listitem" || accessibilityRole == "group" || accessibilityRole == "button")))) {
+ *pRetVal = static_cast(this);
+ AddRef();
+ }
+
return S_OK;
}
@@ -349,6 +370,13 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPropertyValue(PROPERT
pRetVal->lVal = GetLiveSetting(props->accessibilityLiveRegion);
break;
}
+ case UIA_ItemStatusPropertyId: {
+ pRetVal->vt = VT_BSTR;
+ pRetVal->bstrVal = (props->accessibilityState.has_value() && props->accessibilityState->busy)
+ ? SysAllocString(L"Busy")
+ : SysAllocString(L"");
+ break;
+ }
}
return hr;
@@ -496,4 +524,42 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::Toggle() {
return S_OK;
}
+HRESULT __stdcall CompositionDynamicAutomationProvider::get_ExpandCollapseState(ExpandCollapseState *pRetVal) {
+ if (pRetVal == nullptr)
+ return E_POINTER;
+ auto strongView = m_view.view();
+
+ if (!strongView)
+ return UIA_E_ELEMENTNOTAVAILABLE;
+
+ auto props = std::static_pointer_cast(
+ winrt::get_self(strongView)->props());
+
+ if (props == nullptr)
+ return UIA_E_ELEMENTNOTAVAILABLE;
+
+ *pRetVal = props->accessibilityState->expanded.has_value()
+ ? GetExpandCollapseState(props->accessibilityState->expanded.value())
+ : ExpandCollapseState_Collapsed;
+ return S_OK;
+}
+
+HRESULT __stdcall CompositionDynamicAutomationProvider::Expand() {
+ auto strongView = m_view.view();
+
+ if (!strongView)
+ return UIA_E_ELEMENTNOTAVAILABLE;
+ DispatchAccessibilityAction(m_view, "expand");
+ return S_OK;
+}
+
+HRESULT __stdcall CompositionDynamicAutomationProvider::Collapse() {
+ auto strongView = m_view.view();
+
+ if (!strongView)
+ return UIA_E_ELEMENTNOTAVAILABLE;
+ DispatchAccessibilityAction(m_view, "collapse");
+ return S_OK;
+}
+
} // namespace winrt::Microsoft::ReactNative::implementation
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h
index b089efd7e84..fd37624b580 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h
@@ -16,7 +16,8 @@ class CompositionDynamicAutomationProvider : public winrt::implements<
IInvokeProvider,
IScrollItemProvider,
IValueProvider,
- IToggleProvider> {
+ IToggleProvider,
+ IExpandCollapseProvider> {
public:
CompositionDynamicAutomationProvider(
const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView) noexcept;
@@ -47,10 +48,15 @@ class CompositionDynamicAutomationProvider : public winrt::implements<
virtual HRESULT __stdcall get_Value(BSTR *pRetVal) override;
virtual HRESULT __stdcall get_IsReadOnly(BOOL *pRetVal) override;
- // inherited via IToggleProivder
+ // inherited via IToggleProvider
virtual HRESULT __stdcall get_ToggleState(ToggleState *pRetVal) override;
virtual HRESULT __stdcall Toggle() override;
+ // inherited via IExpandCollapseProvider
+ virtual HRESULT __stdcall get_ExpandCollapseState(ExpandCollapseState *pRetVal) override;
+ virtual HRESULT __stdcall Expand() override;
+ virtual HRESULT __stdcall Collapse() override;
+
private:
::Microsoft::ReactNative::ReactTaggedView m_view;
};
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp
index baf6a03ee7c..82d34716515 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp
@@ -23,6 +23,7 @@
#include "CompositionHelpers.h"
#include "RootComponentView.h"
#include "Theme.h"
+#include "TooltipService.h"
#include "UiaHelpers.h"
#include "d2d1helper.h"
@@ -43,6 +44,13 @@ ComponentView::ComponentView(
m_outerVisual.InsertAt(m_focusVisual.InnerVisual(), 0);
}
+ComponentView::~ComponentView() {
+ if (m_tooltipTracked) {
+ TooltipService::GetCurrent(m_reactContext.Properties())->StopTracking(*this);
+ m_tooltipTracked = false;
+ }
+}
+
facebook::react::Tag ComponentView::Tag() const noexcept {
return m_tag;
}
@@ -130,6 +138,16 @@ void ComponentView::updateProps(
updateShadowProps(oldViewProps, newViewProps);
}
+ if (oldViewProps.tooltip != newViewProps.tooltip) {
+ if (!m_tooltipTracked && newViewProps.tooltip) {
+ TooltipService::GetCurrent(m_reactContext.Properties())->StartTracking(*this);
+ m_tooltipTracked = true;
+ } else if (m_tooltipTracked && !newViewProps.tooltip) {
+ TooltipService::GetCurrent(m_reactContext.Properties())->StopTracking(*this);
+ m_tooltipTracked = false;
+ }
+ }
+
base_type::updateProps(props, oldProps);
}
@@ -1303,6 +1321,12 @@ void ComponentView::updateAccessibilityProps(
!(oldViewProps.accessibilityState && oldViewProps.accessibilityState->disabled),
!(newViewProps.accessibilityState && newViewProps.accessibilityState->disabled));
+ winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty(
+ m_uiaProvider,
+ UIA_IsEnabledPropertyId,
+ !(oldViewProps.accessibilityState && oldViewProps.accessibilityState->busy),
+ !(newViewProps.accessibilityState && newViewProps.accessibilityState->busy));
+
winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty(
m_uiaProvider, UIA_ControlTypePropertyId, oldViewProps.accessibilityRole, newViewProps.accessibilityRole);
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h
index d2482ec8475..dc3e0dd8c4f 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h
@@ -30,6 +30,7 @@ struct ComponentView : public ComponentViewT<
facebook::react::Tag tag,
winrt::Microsoft::ReactNative::ReactContext const &reactContext,
ComponentViewFeatures flags);
+ virtual ~ComponentView();
virtual winrt::Microsoft::ReactNative::Composition::Experimental::IVisual Visual() const noexcept {
return nullptr;
@@ -151,6 +152,7 @@ struct ComponentView : public ComponentViewT<
const facebook::react::ViewProps &viewProps) noexcept;
bool m_FinalizeTransform{false};
+ bool m_tooltipTracked{false};
ComponentViewFeatures m_flags;
void showFocusVisual(bool show) noexcept;
winrt::Microsoft::ReactNative::Composition::Experimental::IFocusVisual m_focusVisual{nullptr};
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp
index d01e812cd5d..96bdb1b91c6 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp
@@ -157,6 +157,7 @@ void ReactNativeIsland::ReactViewHost(winrt::Microsoft::ReactNative::IReactViewH
}
if (m_reactViewHost) {
+ UninitRootView();
m_reactViewHost.DetachViewInstance();
}
@@ -332,7 +333,7 @@ winrt::IInspectable ReactNativeIsland::GetUiaProvider() noexcept {
if (m_uiaProvider == nullptr) {
m_uiaProvider =
winrt::make(*this);
- if (m_hwnd) {
+ if (m_hwnd && !m_island) {
auto pRootProvider =
static_cast(
m_uiaProvider.as().get());
@@ -348,6 +349,10 @@ void ReactNativeIsland::SetWindow(uint64_t hwnd) noexcept {
m_hwnd = reinterpret_cast(hwnd);
}
+HWND ReactNativeIsland::GetHwndForParenting() noexcept {
+ return m_hwnd;
+}
+
int64_t ReactNativeIsland::SendMessage(uint32_t msg, uint64_t wParam, int64_t lParam) noexcept {
if (m_rootTag == -1)
return 0;
@@ -367,7 +372,7 @@ int64_t ReactNativeIsland::SendMessage(uint32_t msg, uint64_t wParam, int64_t lP
bool ReactNativeIsland::CapturePointer(
const winrt::Microsoft::ReactNative::Composition::Input::Pointer &pointer,
facebook::react::Tag tag) noexcept {
- if (m_hwnd) {
+ if (m_hwnd && !m_island) {
SetCapture(m_hwnd);
}
return m_CompositionEventHandler->CapturePointer(pointer, tag);
@@ -377,7 +382,7 @@ void ReactNativeIsland::ReleasePointerCapture(
const winrt::Microsoft::ReactNative::Composition::Input::Pointer &pointer,
facebook::react::Tag tag) noexcept {
if (m_CompositionEventHandler->ReleasePointerCapture(pointer, tag)) {
- if (m_hwnd) {
+ if (m_hwnd && !m_island) {
if (m_hwnd == GetCapture()) {
ReleaseCapture();
}
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h
index 8f292357515..f280ddc6a1d 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h
@@ -82,6 +82,7 @@ struct ReactNativeIsland
void AddRenderedVisual(const winrt::Microsoft::ReactNative::Composition::Experimental::IVisual &visual) noexcept;
void RemoveRenderedVisual(const winrt::Microsoft::ReactNative::Composition::Experimental::IVisual &visual) noexcept;
bool TrySetFocus() noexcept;
+ HWND GetHwndForParenting() noexcept;
winrt::Microsoft::ReactNative::Composition::ICustomResourceLoader Resources() noexcept;
void Resources(const winrt::Microsoft::ReactNative::Composition::ICustomResourceLoader &resources) noexcept;
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp
index 9d3cd495d62..282cba603b8 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp
@@ -231,4 +231,15 @@ winrt::Microsoft::ReactNative::implementation::ClipState RootComponentView::getC
return winrt::Microsoft::ReactNative::implementation::ClipState::NoClip;
}
+HWND RootComponentView::GetHwndForParenting() noexcept {
+ if (auto rootView = m_wkRootView.get()) {
+ auto hwnd = winrt::get_self(rootView)
+ ->GetHwndForParenting();
+ if (hwnd)
+ return hwnd;
+ }
+
+ return base_type::GetHwndForParenting();
+}
+
} // namespace winrt::Microsoft::ReactNative::Composition::implementation
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h
index 42b3ec70468..ddd61b7e7fa 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h
@@ -64,6 +64,8 @@ struct RootComponentView : RootComponentViewT, std::equal_to<>>
s_lightColors = {
@@ -326,7 +328,9 @@ bool Theme::TryGetPlatformColor(const std::string &platformColor, winrt::Windows
{"ControlStrongFillColorDefault", {0x72, 0x00, 0x00, 0x00}},
{"ControlStrongFillColorDisabled", {0x51, 0x00, 0x00, 0x00}},
{"AcrylicInAppFillColorDefault", {0x9E, 0xFF, 0xFF, 0xFF}},
- };
+ {"SystemChromeMediumLowColor", {0xFF, 0xF2, 0xF2, 0xF2}},
+ {"SystemControlForegroundBaseHighColor", {0xFF, 0x00, 0x00, 0x00}},
+ {"SystemControlTransientBorderColor", {0x24, 0x00, 0x00, 0x00}}};
static std::unordered_map, std::equal_to<>>
s_darkColors = {
@@ -356,7 +360,9 @@ bool Theme::TryGetPlatformColor(const std::string &platformColor, winrt::Windows
{"ControlStrongFillColorDefault", {0x8B, 0xFF, 0xFF, 0xFF}},
{"ControlStrongFillColorDisabled", {0x3F, 0xFF, 0xFF, 0xFF}},
{"AcrylicInAppFillColorDefault", {0x9E, 0x00, 0x00, 0x00}},
- };
+ {"SystemChromeMediumLowColor", {0xFF, 0x2B, 0x2B, 0x2B}},
+ {"SystemControlForegroundBaseHighColor", {0xFF, 0xFF, 0xFF, 0xFF}},
+ {"SystemControlTransientBorderColor", {0x5C, 0x00, 0x00, 0x00}}};
static std::unordered_map<
std::string,
@@ -391,7 +397,9 @@ bool Theme::TryGetPlatformColor(const std::string &platformColor, winrt::Windows
{"SubtleFillColorSecondary", {winrt::Windows::UI::ViewManagement::UIElementType::ButtonFace, {}}},
{"ControlStrongFillColorDefault", {winrt::Windows::UI::ViewManagement::UIElementType::ButtonFace, {}}},
{"ControlStrongFillColorDisabled", {winrt::Windows::UI::ViewManagement::UIElementType::ButtonFace, {}}},
- };
+ {"SystemChromeMediumLowColor", {winrt::Windows::UI::ViewManagement::UIElementType::ButtonFace, {}}},
+ {"SystemControlForegroundBaseHighColor", {winrt::Windows::UI::ViewManagement::UIElementType::ButtonText, {}}},
+ {"SystemControlTransientBorderColor", {winrt::Windows::UI::ViewManagement::UIElementType::ButtonText, {}}}};
auto alias = s_xamlAliasedColors.find(platformColor);
if (alias != s_xamlAliasedColors.end()) {
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp
new file mode 100644
index 00000000000..dde9878d10b
--- /dev/null
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp
@@ -0,0 +1,338 @@
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#include "TooltipService.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include "TextDrawing.h"
+#include "dwmapi.h"
+
+namespace winrt::Microsoft::ReactNative {
+
+constexpr PCWSTR c_tooltipWindowClassName = L"RN_TOOLTIP";
+constexpr auto TooltipDataProperty = L"TooltipData";
+constexpr float tooltipFontSize = 12;
+constexpr float tooltipMaxHeight = 1000;
+constexpr float tooltipMaxWidth = 320;
+constexpr float tooltipHorizontalPadding = 8;
+constexpr float tooltipTopPadding = 5;
+constexpr float tooltipBottomPadding = 7;
+constexpr float toolTipBorderThickness = 1;
+constexpr int toolTipPlacementMargin = 12;
+constexpr int toolTipAnimationTimeMs = 200;
+constexpr int toolTipTimeToShowMs = 1000;
+
+struct TooltipData {
+ TooltipData(const winrt::Microsoft::ReactNative::ComponentView &view) : view(view) {}
+
+ static TooltipData *GetFromWindow(HWND hwnd) {
+ auto data = reinterpret_cast(GetProp(hwnd, TooltipDataProperty));
+ return data;
+ }
+
+ int width;
+ int height;
+ ::Microsoft::ReactNative::ReactTaggedView view;
+ winrt::com_ptr<::IDWriteTextLayout> textLayout;
+ facebook::react::AttributedStringBox attributedString;
+ winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext compositionContext;
+};
+
+facebook::react::AttributedStringBox CreateTooltipAttributedString(const std::string &tooltip) noexcept {
+ auto attributedString = facebook::react::AttributedString{};
+ auto fragment = facebook::react::AttributedString::Fragment{};
+ fragment.string = tooltip;
+ fragment.textAttributes.fontSize = tooltipFontSize;
+ attributedString.appendFragment(fragment);
+ return facebook::react::AttributedStringBox{attributedString};
+}
+
+LRESULT CALLBACK TooltipWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) noexcept {
+ switch (message) {
+ case WM_DESTROY: {
+ delete TooltipData::GetFromWindow(hwnd);
+ SetProp(hwnd, TooltipDataProperty, 0);
+ return 0;
+ }
+ case WM_PRINTCLIENT:
+ case WM_PAINT: {
+ HDC hdc;
+ PAINTSTRUCT ps;
+
+ if (message != WM_PRINTCLIENT)
+ hdc = BeginPaint(hwnd, &ps);
+ else
+ hdc = (HDC)wparam;
+ auto data = TooltipData::GetFromWindow(hwnd);
+
+ if (auto view = data->view.view()) {
+ auto scaleFactor = view.LayoutMetrics().PointScaleFactor;
+
+ auto ccInterop = data->compositionContext
+ .as<::Microsoft::ReactNative::Composition::Experimental::ICompositionContextInterop>();
+ winrt::com_ptr factory;
+ ccInterop->D2DFactory(factory.put());
+
+ winrt::com_ptr renderTarget;
+
+ D2D1_RENDER_TARGET_PROPERTIES props = D2D1::RenderTargetProperties(
+ D2D1_RENDER_TARGET_TYPE_DEFAULT,
+ D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE),
+ 0,
+ 0,
+ D2D1_RENDER_TARGET_USAGE_NONE,
+ D2D1_FEATURE_LEVEL_DEFAULT);
+ winrt::check_hresult(factory->CreateDCRenderTarget(&props, renderTarget.put()));
+ RECT rc;
+ GetClientRect(hwnd, &rc);
+ winrt::check_hresult(renderTarget->BindDC(hdc, &rc));
+ auto theme = view.as().Theme();
+ auto selfTheme = winrt::get_self(theme);
+
+ renderTarget->BeginDraw();
+ renderTarget->Clear(selfTheme->D2DPlatformColor("ToolTipBackground"));
+
+ auto textAttributes = facebook::react::TextAttributes{};
+ facebook::react::Color fgColor;
+ fgColor.m_platformColor.push_back("ToolTipForeground");
+ textAttributes.foregroundColor = fgColor;
+
+ winrt::Microsoft::ReactNative::Composition::RenderText(
+ *renderTarget,
+ *data->textLayout,
+ data->attributedString.getValue(),
+ textAttributes,
+ {std::round(tooltipHorizontalPadding * scaleFactor), std::round(tooltipTopPadding * scaleFactor)},
+ scaleFactor,
+ *selfTheme);
+
+ auto hr = renderTarget->EndDraw();
+ }
+
+ if (message != WM_PRINTCLIENT)
+ EndPaint(hwnd, &ps);
+ return 0;
+ }
+ case WM_NCCREATE: {
+ auto cs = reinterpret_cast(lparam);
+ auto data = static_cast(cs->lpCreateParams);
+ WINRT_ASSERT(data);
+ SetProp(hwnd, TooltipDataProperty, reinterpret_cast(data));
+ break;
+ }
+ }
+
+ return DefWindowProc(hwnd, message, wparam, lparam);
+}
+
+void RegisterTooltipWndClass() noexcept {
+ static bool registered = false;
+ if (registered) {
+ return;
+ }
+
+ HINSTANCE hInstance =
+ GetModuleHandle(NULL); // returns a handle to the file used to create the calling process (.exe file)
+
+ WNDCLASSEX wcex = {}; // contains window class information
+ wcex.cbSize = sizeof(wcex); // size of windows class (bytes)
+ wcex.style = CS_HREDRAW | CS_VREDRAW; // class style (redraw window on size adjustment)
+ wcex.lpfnWndProc = &TooltipWndProc; // pointer to windows procedure
+ wcex.cbClsExtra = DLGWINDOWEXTRA; // extra bytes to allocate
+ wcex.cbWndExtra = sizeof(TooltipData *); // extra bytes to allocate
+ wcex.hInstance = hInstance;
+ wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); // handle to class cursor
+ wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // background color
+ wcex.lpszClassName = c_tooltipWindowClassName; // specify resource name
+ ATOM classId = RegisterClassEx(&wcex); // register new windows class
+ WINRT_VERIFY(classId); // 0 = fail
+ winrt::check_win32(!classId);
+
+ registered = true;
+}
+
+TooltipTracker::TooltipTracker(
+ const winrt::Microsoft::ReactNative::ComponentView &view,
+ const winrt::Microsoft::ReactNative::ReactPropertyBag &properties,
+ TooltipService *outer)
+ : m_view(view), m_properties(properties), m_outer(outer) {
+ view.PointerEntered({this, &TooltipTracker::OnPointerEntered});
+ view.PointerExited({this, &TooltipTracker::OnPointerExited});
+ view.PointerMoved({this, &TooltipTracker::OnPointerMoved});
+ view.Unmounted({this, &TooltipTracker::OnUnmounted});
+}
+
+TooltipTracker::~TooltipTracker() {
+ DestroyTimer();
+ DestroyTooltip();
+}
+
+facebook::react::Tag TooltipTracker::Tag() const noexcept {
+ return m_view.Tag();
+}
+
+void TooltipTracker::OnPointerEntered(
+ const winrt::Windows::Foundation::IInspectable &sender,
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept {
+ if (args.Pointer().PointerDeviceType() !=
+ winrt::Microsoft::ReactNative::Composition::Input::PointerDeviceType::Mouse &&
+ args.Pointer().PointerDeviceType() != winrt::Microsoft::ReactNative::Composition::Input::PointerDeviceType::Pen)
+ return;
+
+ auto pp = args.GetCurrentPoint(-1);
+ m_pos = pp.Position();
+
+ m_timer = winrt::Microsoft::ReactNative::Timer::Create(m_properties.Handle());
+ m_timer.Interval(std::chrono::milliseconds(toolTipTimeToShowMs));
+ m_timer.Tick({this, &TooltipTracker::OnTick});
+ m_timer.Start();
+}
+
+void TooltipTracker::OnPointerMoved(
+ const winrt::Windows::Foundation::IInspectable &sender,
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept {
+ if (args.Pointer().PointerDeviceType() !=
+ winrt::Microsoft::ReactNative::Composition::Input::PointerDeviceType::Mouse &&
+ args.Pointer().PointerDeviceType() != winrt::Microsoft::ReactNative::Composition::Input::PointerDeviceType::Pen)
+ return;
+
+ auto pp = args.GetCurrentPoint(-1);
+ m_pos = pp.Position();
+}
+
+void TooltipTracker::OnTick(
+ const winrt::Windows::Foundation::IInspectable &,
+ const winrt::Windows::Foundation::IInspectable &) noexcept {
+ ShowTooltip(m_view.view());
+}
+
+void TooltipTracker::OnPointerExited(
+ const winrt::Windows::Foundation::IInspectable &sender,
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept {
+ if (args.Pointer().PointerDeviceType() !=
+ winrt::Microsoft::ReactNative::Composition::Input::PointerDeviceType::Mouse &&
+ args.Pointer().PointerDeviceType() != winrt::Microsoft::ReactNative::Composition::Input::PointerDeviceType::Pen)
+ return;
+ DestroyTimer();
+ DestroyTooltip();
+}
+
+void TooltipTracker::OnUnmounted(
+ const winrt::Windows::Foundation::IInspectable &,
+ const winrt::Microsoft::ReactNative::ComponentView &) noexcept {
+ DestroyTimer();
+ DestroyTooltip();
+}
+
+void TooltipTracker::ShowTooltip(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
+ auto viewCompView = view.as();
+
+ auto selfView =
+ winrt::get_self(viewCompView);
+ auto parentHwnd = selfView->GetHwndForParenting();
+ DestroyTimer();
+
+ if (!m_hwndTip) {
+ auto tooltipData = std::make_unique(view);
+ tooltipData->attributedString = CreateTooltipAttributedString(*selfView->viewProps()->tooltip);
+
+ tooltipData->compositionContext = selfView->CompositionContext();
+ tooltipData->view = view;
+
+ auto scaleFactor = view.LayoutMetrics().PointScaleFactor;
+ facebook::react::LayoutConstraints layoutConstraints;
+ layoutConstraints.layoutDirection = facebook::react::LayoutDirection::Undefined;
+ layoutConstraints.maximumSize.height = tooltipMaxHeight * scaleFactor;
+ layoutConstraints.maximumSize.width = tooltipMaxWidth * scaleFactor;
+ layoutConstraints.minimumSize.height = 0;
+ layoutConstraints.minimumSize.width = 0;
+
+ facebook::react::TextLayoutManager::GetTextLayout(
+ tooltipData->attributedString, {} /*paragraphAttributes*/, layoutConstraints, tooltipData->textLayout);
+
+ DWRITE_TEXT_METRICS tm;
+ winrt::check_hresult(tooltipData->textLayout->GetMetrics(&tm));
+
+ tooltipData->width =
+ static_cast(tm.width + ((tooltipHorizontalPadding + tooltipHorizontalPadding) * scaleFactor));
+ tooltipData->height = static_cast(tm.height + ((tooltipTopPadding + tooltipBottomPadding) * scaleFactor));
+
+ POINT pt = {static_cast(m_pos.X), static_cast(m_pos.Y)};
+ ClientToScreen(parentHwnd, &pt);
+
+ RegisterTooltipWndClass();
+ HINSTANCE hInstance = GetModuleHandle(NULL);
+ m_hwndTip = CreateWindow(
+ c_tooltipWindowClassName,
+ L"Tooltip",
+ WS_POPUP,
+ pt.x - tooltipData->width / 2,
+ static_cast(pt.y - tooltipData->height - (toolTipPlacementMargin * scaleFactor)),
+ tooltipData->width,
+ tooltipData->height,
+ parentHwnd,
+ NULL,
+ hInstance,
+ tooltipData.get());
+
+ DWM_WINDOW_CORNER_PREFERENCE preference = DWMWCP_ROUNDSMALL;
+ UINT borderThickness = 0;
+ DwmSetWindowAttribute(m_hwndTip, DWMWA_WINDOW_CORNER_PREFERENCE, &preference, sizeof(preference));
+
+ tooltipData.release();
+ AnimateWindow(m_hwndTip, toolTipAnimationTimeMs, AW_BLEND);
+ }
+}
+
+void TooltipTracker::DestroyTooltip() noexcept {
+ if (m_hwndTip) {
+ AnimateWindow(m_hwndTip, toolTipAnimationTimeMs, AW_BLEND | AW_HIDE);
+ DestroyWindow(m_hwndTip);
+ m_hwndTip = nullptr;
+ }
+}
+
+void TooltipTracker::DestroyTimer() noexcept {
+ if (m_timer) {
+ m_timer.Stop();
+ m_timer = nullptr;
+ }
+}
+
+TooltipService::TooltipService(const winrt::Microsoft::ReactNative::ReactPropertyBag &properties)
+ : m_properties(properties) {}
+
+void TooltipService::StartTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
+ m_trackers.push_back(std::make_shared(view, m_properties, this));
+}
+
+void TooltipService::StopTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
+ for (auto it = m_trackers.begin(); it != m_trackers.end();) {
+ if ((*it)->Tag() == view.Tag())
+ it = m_trackers.erase(it);
+ else
+ ++it;
+ }
+}
+
+static const ReactPropertyId>>
+ &TooltipServicePropertyId() noexcept {
+ static const ReactPropertyId>> prop{
+ L"ReactNative", L"TooltipService"};
+ return prop;
+}
+
+std::shared_ptr TooltipService::GetCurrent(
+ const winrt::Microsoft::ReactNative::ReactPropertyBag &properties) noexcept {
+ return *properties.GetOrCreate(TooltipServicePropertyId(), [properties]() -> std::shared_ptr {
+ return std::make_shared(properties);
+ });
+}
+
+} // namespace winrt::Microsoft::ReactNative
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.h b/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.h
new file mode 100644
index 00000000000..b3090061a54
--- /dev/null
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.h
@@ -0,0 +1,66 @@
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#pragma once
+
+#include
+#include
+
+namespace winrt::Microsoft::ReactNative {
+
+struct TooltipService;
+
+struct TooltipTracker {
+ TooltipTracker(
+ const winrt::Microsoft::ReactNative::ComponentView &view,
+ const winrt::Microsoft::ReactNative::ReactPropertyBag &properties,
+ TooltipService *outer);
+ ~TooltipTracker();
+
+ void OnPointerEntered(
+ const winrt::Windows::Foundation::IInspectable &sender,
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept;
+ void OnPointerMoved(
+ const winrt::Windows::Foundation::IInspectable &sender,
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept;
+ void OnPointerExited(
+ const winrt::Windows::Foundation::IInspectable &sender,
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept;
+ void OnTick(
+ const winrt::Windows::Foundation::IInspectable &,
+ const winrt::Windows::Foundation::IInspectable &) noexcept;
+ void OnUnmounted(
+ const winrt::Windows::Foundation::IInspectable &,
+ const winrt::Microsoft::ReactNative::ComponentView &) noexcept;
+
+ facebook::react::Tag Tag() const noexcept;
+
+ private:
+ void ShowTooltip(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept;
+ void DestroyTimer() noexcept;
+ void DestroyTooltip() noexcept;
+
+ TooltipService *m_outer;
+ winrt::Windows::Foundation::Point m_pos;
+ ::Microsoft::ReactNative::ReactTaggedView m_view;
+ winrt::Microsoft::ReactNative::ITimer m_timer;
+ HWND m_hwndTip{nullptr};
+ winrt::Microsoft::ReactNative::ReactPropertyBag m_properties;
+};
+
+struct TooltipService {
+ TooltipService(const winrt::Microsoft::ReactNative::ReactPropertyBag &properties);
+ void StartTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept;
+ void StopTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept;
+
+ static std::shared_ptr GetCurrent(
+ const winrt::Microsoft::ReactNative::ReactPropertyBag &properties) noexcept;
+
+ private:
+ std::vector> m_enteredTrackers;
+ std::vector> m_trackers;
+ winrt::Microsoft::ReactNative::ReactPropertyBag m_properties;
+};
+
+} // namespace winrt::Microsoft::ReactNative
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp
index b678219c06c..9689b046f60 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp
@@ -206,4 +206,12 @@ void DispatchAccessibilityAction(::Microsoft::ReactNative::ReactTaggedView &view
}
}
+ExpandCollapseState GetExpandCollapseState(const bool &expanded) noexcept {
+ if (expanded) {
+ return ExpandCollapseState_Expanded;
+ } else {
+ return ExpandCollapseState_Collapsed;
+ }
+}
+
} // namespace winrt::Microsoft::ReactNative::implementation
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h
index 28a6fcbebcb..42a31aa131e 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h
@@ -35,4 +35,5 @@ std::string extractAccessibilityValue(const facebook::react::AccessibilityValue
void DispatchAccessibilityAction(::Microsoft::ReactNative::ReactTaggedView &view, const std::string &action) noexcept;
+ExpandCollapseState GetExpandCollapseState(const bool &expanded) noexcept;
} // namespace winrt::Microsoft::ReactNative::implementation
diff --git a/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp b/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp
index 9d4e999e232..64f8a0cc4b0 100644
--- a/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp
@@ -155,6 +155,9 @@ void FabricUIManager::startSurface(
void FabricUIManager::stopSurface(facebook::react::SurfaceId surfaceId) noexcept {
m_surfaceManager->stopSurface(surfaceId);
+ auto &rootDescriptor = m_registry.componentViewDescriptorWithTag(surfaceId);
+ m_registry.enqueueComponentViewWithComponentHandle(
+ facebook::react::RootShadowNode::Handle(), surfaceId, rootDescriptor);
}
winrt::Microsoft::ReactNative::ReactNativeIsland FabricUIManager::GetReactNativeIsland(
diff --git a/vnext/Microsoft.ReactNative/Fabric/ReactTaggedView.h b/vnext/Microsoft.ReactNative/Fabric/ReactTaggedView.h
index 66ec48fe1f3..37b9059a066 100644
--- a/vnext/Microsoft.ReactNative/Fabric/ReactTaggedView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/ReactTaggedView.h
@@ -31,6 +31,10 @@ struct ReactTaggedView {
return strongView;
}
+ facebook::react::Tag Tag() const noexcept {
+ return m_tag;
+ }
+
private:
facebook::react::Tag m_tag;
winrt::weak_ref m_view;
diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp
index 7e4638224cc..dfdb20cdc49 100644
--- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp
@@ -27,6 +27,10 @@ HostPlatformViewProps::HostPlatformViewProps(
CoreFeatures::enablePropIteratorSetter
? sourceProps.focusable
: convertRawProp(context, rawProps, "focusable", sourceProps.focusable, {})),
+ tooltip(
+ CoreFeatures::enablePropIteratorSetter
+ ? sourceProps.tooltip
+ : convertRawProp(context, rawProps, "tooltip", sourceProps.tooltip, {})),
accessibilityPosInSet(
CoreFeatures::enablePropIteratorSetter
? sourceProps.accessibilityPosInSet
@@ -82,6 +86,7 @@ void HostPlatformViewProps::setProp(
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLiveRegion);
RAW_SET_PROP_SWITCH_CASE_BASIC(keyDownEvents);
RAW_SET_PROP_SWITCH_CASE_BASIC(keyUpEvents);
+ RAW_SET_PROP_SWITCH_CASE_BASIC(tooltip);
}
}
diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.h b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.h
index b2fce99065b..885dd8cbf18 100644
--- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.h
+++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.h
@@ -29,7 +29,7 @@ class HostPlatformViewProps : public BaseViewProps {
std::string accessibilityLiveRegion{"none"};
// std::optional overflowAnchor{};
- // std::optional tooltip{};
+ std::optional tooltip{};
std::vector keyDownEvents{};
std::vector keyUpEvents{};
};
diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewTraitsInitializer.h b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewTraitsInitializer.h
index 57d71a25e54..a71a16389bd 100644
--- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewTraitsInitializer.h
+++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewTraitsInitializer.h
@@ -13,7 +13,7 @@ inline bool formsStackingContext(ViewProps const &viewProps) {
// Only Views which are marked as focusable can actually trigger the events, which will already avoid being collapsed.
constexpr decltype(WindowsViewEvents::bits) focusEventsMask = {
(1 << (int)WindowsViewEvents::Offset::Focus) & (1 << (int)WindowsViewEvents::Offset::Blur)};
- return (viewProps.windowsEvents.bits & focusEventsMask).any();
+ return (viewProps.windowsEvents.bits & focusEventsMask).any() || viewProps.tooltip;
}
inline bool formsView(ViewProps const &viewProps) {
diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj
index 4fb9b143880..9e8df4b4325 100644
--- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj
+++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj
@@ -144,7 +144,7 @@
ProgramDatabase
- winsqlite3.lib;ChakraRT.lib;dxguid.lib;dloadhelper.lib;OneCoreUap_apiset.lib;%(AdditionalDependencies)
+ winsqlite3.lib;ChakraRT.lib;dxguid.lib;dloadhelper.lib;OneCoreUap_apiset.lib;Dwmapi.lib;%(AdditionalDependencies)
api-ms-win-core-file-l1-2-0.dll;
api-ms-win-core-windowserrorreporting-l1-1-0.dll;
diff --git a/vnext/Microsoft.ReactNative/ReactCoreInjection.h b/vnext/Microsoft.ReactNative/ReactCoreInjection.h
index 89e105bf2f4..c14e7cd0df2 100644
--- a/vnext/Microsoft.ReactNative/ReactCoreInjection.h
+++ b/vnext/Microsoft.ReactNative/ReactCoreInjection.h
@@ -53,7 +53,6 @@ struct ReactCoreInjection : ReactCoreInjectionT {
static uint64_t GetTopLevelWindowId(const IReactPropertyBag &properties) noexcept;
static void SetTopLevelWindowId(const IReactPropertyBag &properties, uint64_t windowId) noexcept;
- static ITimer CreateTimer(const IReactPropertyBag &properties);
static TimerFactory GetTimerFactory(const IReactPropertyBag &properties) noexcept;
static void SetTimerFactory(const IReactPropertyBag &properties, const TimerFactory &timerFactory) noexcept;
};
diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems
index 1d7b31ad7c9..00bff89a92d 100644
--- a/vnext/Shared/Shared.vcxitems
+++ b/vnext/Shared/Shared.vcxitems
@@ -150,6 +150,9 @@
true
+
+ true
+
true
diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters
index bca7ec269d2..ba47e68d52a 100644
--- a/vnext/Shared/Shared.vcxitems.filters
+++ b/vnext/Shared/Shared.vcxitems.filters
@@ -333,6 +333,7 @@
+