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 @@ +