From 72e480d188a58cb1f51eca45086ff1377f62f103 Mon Sep 17 00:00:00 2001 From: Alicia Drummond Date: Wed, 7 Jun 2023 11:19:36 -0700 Subject: [PATCH] Support UIA ::SetFocus and basic property change notifications (#11674) * Add better focus management * better focus management * Add support for AdviseEventAdded Removed * A little clean up * Change files * yarn format yarn change * more comments * Remove change from bad merge * Remote atlsafe dependency * yarn format --- ...-67407575-cd5b-4bf1-b812-6e0d80d943c3.json | 7 + .../Playground-Composition.cpp | 2 +- .../CompositionDynamicAutomationProvider.cpp | 59 ++++--- .../CompositionRootAutomationProvider.cpp | 160 +++++++++++++++++- .../CompositionRootAutomationProvider.h | 31 +++- .../CompositionViewComponentView.cpp | 36 +++- .../CompositionViewComponentView.h | 3 + .../Fabric/Composition/RootComponentView.cpp | 15 +- .../Fabric/Composition/RootComponentView.h | 1 + .../Fabric/Composition/UiaHelpers.cpp | 54 ++++++ .../Fabric/Composition/UiaHelpers.h | 10 ++ 11 files changed, 343 insertions(+), 35 deletions(-) create mode 100644 change/react-native-windows-67407575-cd5b-4bf1-b812-6e0d80d943c3.json diff --git a/change/react-native-windows-67407575-cd5b-4bf1-b812-6e0d80d943c3.json b/change/react-native-windows-67407575-cd5b-4bf1-b812-6e0d80d943c3.json new file mode 100644 index 00000000000..65cc7b635d8 --- /dev/null +++ b/change/react-native-windows-67407575-cd5b-4bf1-b812-6e0d80d943c3.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Add UIA focus management and AdvisedEvents for prop changes", + "packageName": "react-native-windows", + "email": "adrum@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/playground/windows/playground-composition/Playground-Composition.cpp b/packages/playground/windows/playground-composition/Playground-Composition.cpp index f1e98d7296c..10683ddad3f 100644 --- a/packages/playground/windows/playground-composition/Playground-Composition.cpp +++ b/packages/playground/windows/playground-composition/Playground-Composition.cpp @@ -486,7 +486,7 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) case WM_GETOBJECT: { if (lparam == UiaRootObjectId) { auto windowData = WindowData::GetFromWindow(hwnd); - if (!windowData->m_windowInited) + if (windowData == nullptr || !windowData->m_windowInited) break; auto hwndHost = windowData->m_CompositionHwndHost; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp index 8b873f050ef..fb9533bb034 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp @@ -1,12 +1,7 @@ #include "pch.h" #include "CompositionDynamicAutomationProvider.h" #include -#pragma warning(push) -#pragma warning(disable : 4229) -#define IN -#define OUT -#include -#pragma warning(pop) +#include #include "RootComponentView.h" #include "UiaHelpers.h" @@ -41,16 +36,15 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetRuntimeId(SAFEARRAY * if (!strongView) return UIA_E_ELEMENTNOTAVAILABLE; - CComSafeArray runtimeId; - auto hr = runtimeId.Create(2); + *pRetVal = SafeArrayCreateVector(VT_I4, 0, 2); - if (FAILED(hr)) - return hr; + if (*pRetVal == nullptr) + return E_OUTOFMEMORY; - runtimeId[0] = UiaAppendRuntimeId; - runtimeId[1] = strongView->tag(); - - *pRetVal = runtimeId.Detach(); + int runtimeId[] = {UiaAppendRuntimeId, strongView->tag()}; + for (long i = 0; i < 2; i++) { + SafeArrayPutElement(*pRetVal, &i, static_cast(&runtimeId[i])); + } return S_OK; } @@ -96,7 +90,7 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetEmbeddedFragmentRoots } HRESULT __stdcall CompositionDynamicAutomationProvider::SetFocus(void) { - return S_OK; + return UiaSetFocusHelper(m_view); } HRESULT __stdcall CompositionDynamicAutomationProvider::get_FragmentRoot(IRawElementProviderFragmentRoot **pRetVal) { @@ -211,6 +205,8 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPropertyValue(PROPERT if (props == nullptr) return UIA_E_ELEMENTNOTAVAILABLE; + auto hr = S_OK; + switch (propertyId) { case UIA_ControlTypePropertyId: { pRetVal->vt = VT_I4; @@ -220,21 +216,40 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPropertyValue(PROPERT } case UIA_AutomationIdPropertyId: { pRetVal->vt = VT_BSTR; - auto testId = props->testId; - CComBSTR temp(testId.c_str()); - pRetVal->bstrVal = temp.Detach(); + auto wideTestId = ::Microsoft::Common::Unicode::Utf8ToUtf16(props->testId); + pRetVal->bstrVal = SysAllocString(wideTestId.c_str()); + hr = pRetVal->bstrVal != nullptr ? S_OK : E_OUTOFMEMORY; break; } case UIA_NamePropertyId: { pRetVal->vt = VT_BSTR; - auto name = props->accessibilityLabel; - CComBSTR temp(name.c_str()); - pRetVal->bstrVal = temp.Detach(); + auto wideName = ::Microsoft::Common::Unicode::Utf8ToUtf16(props->accessibilityLabel); + pRetVal->bstrVal = SysAllocString(wideName.c_str()); + hr = pRetVal->bstrVal != nullptr ? S_OK : E_OUTOFMEMORY; + break; + } + case UIA_IsKeyboardFocusablePropertyId: { + pRetVal->vt = VT_BOOL; + pRetVal->boolVal = props->focusable ? VARIANT_TRUE : VARIANT_FALSE; + break; + } + case UIA_HasKeyboardFocusPropertyId: { + auto rootCV = strongView->rootComponentView(); + if (rootCV == nullptr) + return UIA_E_ELEMENTNOTAVAILABLE; + + pRetVal->vt = VT_BOOL; + pRetVal->boolVal = rootCV->GetFocusedComponent() == strongView.get() ? VARIANT_TRUE : VARIANT_FALSE; + break; + } + case UIA_IsEnabledPropertyId: { + pRetVal->vt = VT_BOOL; + pRetVal->boolVal = !props->accessibilityState.disabled ? VARIANT_TRUE : VARIANT_FALSE; break; } } - return S_OK; + return hr; } HRESULT __stdcall CompositionDynamicAutomationProvider::get_HostRawElementProvider( diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp index 8ed4f784d5e..f0dd6e35880 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "CompositionRootAutomationProvider.h" +#include #include "UiaHelpers.h" namespace winrt::Microsoft::ReactNative::implementation { @@ -32,7 +33,7 @@ HRESULT __stdcall CompositionRootAutomationProvider::GetEmbeddedFragmentRoots(SA } HRESULT __stdcall CompositionRootAutomationProvider::SetFocus(void) { - return S_OK; + return UiaSetFocusHelper(m_view); } HRESULT __stdcall CompositionRootAutomationProvider::GetPatternProvider(PATTERNID patternId, IUnknown **pRetVal) { @@ -122,10 +123,7 @@ HRESULT __stdcall CompositionRootAutomationProvider::ElementProviderFromPoint( return UIA_E_ELEMENTNOTAVAILABLE; } - auto spRootView = strongView->rootComponentView(); - if (spRootView == nullptr) { - return UIA_E_ELEMENTNOTAVAILABLE; - } + auto spRootView = std::static_pointer_cast<::Microsoft::ReactNative::RootComponentView>(strongView); if (m_hwnd == nullptr || !IsWindow(m_hwnd)) { // TODO: Add support for non-HWND based hosting @@ -189,4 +187,156 @@ HRESULT __stdcall CompositionRootAutomationProvider::Navigate( return S_OK; } +// RAII wrapper to unaccess SafeArray data so I can early return in the relevant functions +class SafeArrayAccessScope { + SAFEARRAY *m_pArray = nullptr; + + public: + SafeArrayAccessScope(SAFEARRAY *psa) noexcept : m_pArray(psa) {} + ~SafeArrayAccessScope() noexcept { + if (m_pArray != nullptr) + SafeArrayUnaccessData(m_pArray); + } +}; + +void AdviseEventAddedImpl( + std::vector &advisedEvents, + EVENTID idEvent) noexcept { + auto it = std::find_if( + advisedEvents.begin(), + advisedEvents.end(), + [idEvent](const CompositionRootAutomationProvider::AdvisedEvent &ae) noexcept { return ae.Event == idEvent; }); + + if (it == advisedEvents.end()) { + advisedEvents.emplace_back(CompositionRootAutomationProvider::AdvisedEvent{idEvent, 1 /*Count*/}); + } else { + it->Count++; + } +} + +HRESULT CompositionRootAutomationProvider::AdvisePropertiesAdded(SAFEARRAY *psaProperties) noexcept { + if (psaProperties == nullptr) + return E_POINTER; + + long *pValues = nullptr; + auto hr = SafeArrayAccessData(psaProperties, reinterpret_cast(&pValues)); + if (FAILED(hr)) + return hr; + + SafeArrayAccessScope accessScope(psaProperties); + + if (SafeArrayGetDim(psaProperties) != 1) + return E_INVALIDARG; + + VARTYPE vt; + hr = SafeArrayGetVartype(psaProperties, &vt); + if (FAILED(hr) || vt != VT_I4) + return E_INVALIDARG; + + long lower; + hr = SafeArrayGetLBound(psaProperties, 1, &lower); + if (FAILED(hr)) + return hr; + + long upper; + hr = SafeArrayGetUBound(psaProperties, 1, &upper); + if (FAILED(hr)) + return hr; + + long count = upper - lower + 1; + + for (int i = 0; i < count; i++) { + AdviseEventAddedImpl(m_advisedProperties, pValues[i]); + } + return S_OK; +} + +HRESULT CompositionRootAutomationProvider::AdviseEventAdded(EVENTID idEvent, SAFEARRAY *psaProperties) { + if (idEvent == UIA_AutomationPropertyChangedEventId) { + return AdvisePropertiesAdded(psaProperties); + } + AdviseEventAddedImpl(m_advisedEvents, idEvent); + return S_OK; +} + +HRESULT AdviseEventRemovedImpl( + std::vector &advisedEvents, + EVENTID idEvent) noexcept { + auto it = std::find_if( + advisedEvents.begin(), + advisedEvents.end(), + [idEvent](const CompositionRootAutomationProvider::AdvisedEvent &ae) noexcept { return ae.Event == idEvent; }); + + if (it == advisedEvents.end()) { + assert(false); + return UIA_E_INVALIDOPERATION; + } else if (it->Count == 1) { + advisedEvents.erase(it); + } else { + it->Count--; + } + return S_OK; +} + +HRESULT CompositionRootAutomationProvider::AdvisePropertiesRemoved(SAFEARRAY *psaProperties) noexcept { + if (psaProperties == nullptr) + return E_POINTER; + + long *pValues = nullptr; + auto hr = SafeArrayAccessData(psaProperties, reinterpret_cast(&pValues)); + if (FAILED(hr)) + return hr; + + SafeArrayAccessScope accessScope(psaProperties); + + if (SafeArrayGetDim(psaProperties) != 1) + return E_INVALIDARG; + + VARTYPE vt; + hr = SafeArrayGetVartype(psaProperties, &vt); + if (FAILED(hr) || vt != VT_I4) + return E_INVALIDARG; + + long lower; + hr = SafeArrayGetLBound(psaProperties, 1, &lower); + if (FAILED(hr)) + return hr; + + long upper; + hr = SafeArrayGetUBound(psaProperties, 1, &upper); + if (FAILED(hr)) + return hr; + + long count = upper - lower + 1; + auto returnHr = S_OK; + for (int i = 0; i < count; i++) { + auto hr = AdviseEventRemovedImpl(m_advisedProperties, pValues[i]); + if (FAILED(hr)) { + returnHr = hr; + } + } + return returnHr; +} + +HRESULT +CompositionRootAutomationProvider::AdviseEventRemoved(EVENTID idEvent, SAFEARRAY *psaProperties) { + if (idEvent == UIA_AutomationPropertyChangedEventId) { + return AdvisePropertiesRemoved(psaProperties); + } + + return AdviseEventRemovedImpl(m_advisedEvents, idEvent); +} + +bool CompositionRootAutomationProvider::WasEventAdvised(EVENTID event) noexcept { + return std::any_of(m_advisedEvents.begin(), m_advisedEvents.end(), [event](const AdvisedEvent &ae) { + return ae.Event == event && ae.Count > 0; + }); +} + +bool CompositionRootAutomationProvider::WasPropertyAdvised(PROPERTYID prop) noexcept { + return std::any_of(m_advisedProperties.begin(), m_advisedProperties.end(), [prop](const AdvisedEvent &ae) { + return ae.Property == prop && ae.Count > 0; + }); +} + } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.h index 6951f7be4b9..8ba2f23a4e4 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.h @@ -13,7 +13,8 @@ class CompositionRootAutomationProvider : public winrt::implements< IInspectable, IRawElementProviderFragmentRoot, IRawElementProviderFragment, - IRawElementProviderSimple> { + IRawElementProviderSimple, + IRawElementProviderAdviseEvents> { public: // inherited via IRawElementProviderFragmentRoot virtual HRESULT __stdcall ElementProviderFromPoint(double x, double y, IRawElementProviderFragment **pRetVal) @@ -34,11 +35,39 @@ class CompositionRootAutomationProvider : public winrt::implements< virtual HRESULT __stdcall GetPropertyValue(PROPERTYID propertyId, VARIANT *pRetVal) override; virtual HRESULT __stdcall get_HostRawElementProvider(IRawElementProviderSimple **pRetVal) override; + // IRawElementProviderAdviseEvents + virtual HRESULT __stdcall AdviseEventAdded(EVENTID idEvent, SAFEARRAY *psaProperties) override; + virtual HRESULT __stdcall AdviseEventRemoved(EVENTID idEvent, SAFEARRAY *psaProperties) override; + CompositionRootAutomationProvider( const std::shared_ptr<::Microsoft::ReactNative::RootComponentView> &componentView) noexcept; + void SetHwnd(HWND hwnd) noexcept; + bool WasPropertyAdvised(PROPERTYID prop) noexcept; + bool WasEventAdvised(EVENTID event) noexcept; + + // It's unlikely for the underlying primitive types for EVENTID and PROPERTYID to ever change, but let's make sure + static_assert(std::is_same::value); + // Helper class for AdviseEventAdded/Removed. I could've simply used a std::pair, but I find using structs with named + // members easier to read and more self-documenting than pair.first and pair.last. Since this is simply syntactic + // sugar, I'm leveraging the fact that both EVENTID and PROPERTYID are ints under the hood to share + // AdviseEventAddedImpl + struct AdvisedEvent { + union { + EVENTID Event; + PROPERTYID Property; + }; + uint32_t Count; + }; private: + HRESULT AdvisePropertiesAdded(SAFEARRAY *psaProperties) noexcept; + HRESULT AdvisePropertiesRemoved(SAFEARRAY *psaProperties) noexcept; + + // Linear search on unsorted vectors is typically faster than more sophisticated data structures when N is small. In + // practice ATs tend to only listen to a dozen or so props and events, so std::vector is likely better than maps. + std::vector m_advisedEvents{}; + std::vector m_advisedProperties{}; ::Microsoft::ReactNative::ReactTaggedView m_view; HWND m_hwnd{nullptr}; }; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index 6ad03c5fabf..d5683d3dfee 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -15,6 +15,7 @@ #include "CompositionDynamicAutomationProvider.h" #include "CompositionHelpers.h" #include "RootComponentView.h" +#include "UiaHelpers.h" #include "d2d1helper.h" namespace Microsoft::ReactNative { @@ -37,7 +38,6 @@ RootComponentView *CompositionBaseComponentView::rootComponentView() noexcept { if (m_parent) return m_parent->rootComponentView(); - assert(false); return nullptr; } @@ -78,6 +78,10 @@ bool CompositionBaseComponentView::runOnChildren(bool forward, Mso::FunctoronBlur(); showFocusVisual(false); + if (UiaClientsAreListening()) { + winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty( + EnsureUiaProvider(), UIA_HasKeyboardFocusPropertyId, true, false); + } } void CompositionBaseComponentView::onFocusGained() noexcept { @@ -85,6 +89,14 @@ void CompositionBaseComponentView::onFocusGained() noexcept { if (m_enableFocusVisual) { showFocusVisual(true); } + if (UiaClientsAreListening()) { + auto spProviderSimple = EnsureUiaProvider().try_as(); + if (spProviderSimple != nullptr) { + winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty( + m_uiaProvider, UIA_HasKeyboardFocusPropertyId, false, true); + UiaRaiseAutomationEvent(spProviderSimple.get(), UIA_AutomationFocusChangedEventId); + } + } } void CompositionBaseComponentView::updateEventEmitter( @@ -1030,6 +1042,27 @@ void CompositionBaseComponentView::updateBorderProps( } } +void CompositionBaseComponentView::updateAccessibilityProps( + const facebook::react::ViewProps &oldViewProps, + const facebook::react::ViewProps &newViewProps) noexcept { + if (!UiaClientsAreListening()) + return; + + auto provider = EnsureUiaProvider(); + + winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty( + provider, UIA_IsKeyboardFocusablePropertyId, oldViewProps.focusable, newViewProps.focusable); + + winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty( + provider, UIA_NamePropertyId, oldViewProps.accessibilityLabel, newViewProps.accessibilityLabel); + + winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty( + provider, + UIA_IsEnabledPropertyId, + !oldViewProps.accessibilityState.disabled, + !newViewProps.accessibilityState.disabled); +} + void CompositionBaseComponentView::updateBorderLayoutMetrics( facebook::react::LayoutMetrics const &layoutMetrics, const facebook::react::ViewProps &viewProps) noexcept { @@ -1206,6 +1239,7 @@ void CompositionViewComponentView::updateProps( m_visual.Opacity(newViewProps.opacity); } + updateAccessibilityProps(oldViewProps, newViewProps); updateBorderProps(oldViewProps, newViewProps); // Shadow diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h index cb89637f25f..8c6c145bc16 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h @@ -50,6 +50,9 @@ struct CompositionBaseComponentView : public IComponentView, void updateBorderProps( const facebook::react::ViewProps &oldViewProps, const facebook::react::ViewProps &newViewProps) noexcept; + void updateAccessibilityProps( + const facebook::react::ViewProps &oldView, + const facebook::react::ViewProps &newViewProps) noexcept; void updateBorderLayoutMetrics( facebook::react::LayoutMetrics const &layoutMetrics, const facebook::react::ViewProps &viewProps) noexcept; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp index c0c6929e7c7..a44e8ce5d43 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp @@ -84,6 +84,14 @@ bool RootComponentView::NavigateFocus(const winrt::Microsoft::ReactNative::Focus return view != nullptr; } +bool RootComponentView::TrySetFocusedComponent(IComponentView &view) noexcept { + if (view.focusable()) { + view.rootComponentView()->SetFocusedComponent(&view); + return true; + } + return false; +} + bool RootComponentView::TryMoveFocus(bool next) noexcept { if (!m_focusedComponent) { return NavigateFocus(winrt::Microsoft::ReactNative::FocusNavigationRequest( @@ -94,11 +102,8 @@ bool RootComponentView::TryMoveFocus(bool next) noexcept { Mso::Functor fn = [currentlyFocused = m_focusedComponent](IComponentView &view) noexcept { if (&view == currentlyFocused) return false; - if (view.focusable()) { - view.rootComponentView()->SetFocusedComponent(&view); - return true; - } - return false; + + return view.rootComponentView()->TrySetFocusedComponent(view); }; return walkTree(*m_focusedComponent, next, fn); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h index 032907025eb..375a9bed869 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h @@ -25,6 +25,7 @@ struct RootComponentView : CompositionViewComponentView { ::Microsoft::ReactNative::IComponentView *GetFocusedComponent() noexcept; void SetFocusedComponent(::Microsoft::ReactNative::IComponentView *value) noexcept; + bool TrySetFocusedComponent(::Microsoft::ReactNative::IComponentView &view) noexcept; bool NavigateFocus(const winrt::Microsoft::ReactNative::FocusNavigationRequest &request) noexcept; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp index 2cc2f1d542b..7fac3a34e51 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp @@ -1,7 +1,10 @@ #include "pch.h" #include "UiaHelpers.h" #include +#include #include +#include "CompositionRootAutomationProvider.h" +#include "RootComponentView.h" namespace winrt::Microsoft::ReactNative::implementation { @@ -86,4 +89,55 @@ HRESULT UiaGetBoundingRectangleHelper(::Microsoft::ReactNative::ReactTaggedView return S_OK; } +HRESULT UiaSetFocusHelper(::Microsoft::ReactNative::ReactTaggedView &view) noexcept { + auto strongView = view.view(); + + if (!strongView) + return UIA_E_ELEMENTNOTAVAILABLE; + + auto rootCV = strongView->rootComponentView(); + if (rootCV == nullptr) + return UIA_E_ELEMENTNOTAVAILABLE; + + return rootCV->TrySetFocusedComponent(*strongView) ? S_OK : E_FAIL; +} + +bool WasUiaPropertyAdvised(winrt::com_ptr &providerSimple, PROPERTYID propId) noexcept { + auto spFragment = providerSimple.try_as(); + if (spFragment == nullptr) + return false; + + winrt::com_ptr spFragmentRoot; + spFragment->get_FragmentRoot(spFragmentRoot.put()); + if (spFragmentRoot == nullptr) + return false; + + auto rootProvider = static_cast(spFragmentRoot.get()); + + return rootProvider->WasPropertyAdvised(propId); +} + +void UpdateUiaProperty(winrt::IInspectable provider, PROPERTYID propId, bool oldValue, bool newValue) noexcept { + auto spProviderSimple = provider.try_as(); + + if (spProviderSimple == nullptr || oldValue == newValue || !WasUiaPropertyAdvised(spProviderSimple, propId)) + return; + + UiaRaiseAutomationPropertyChangedEvent(spProviderSimple.get(), propId, CComVariant(oldValue), CComVariant(newValue)); +} + +void UpdateUiaProperty( + winrt::IInspectable provider, + PROPERTYID propId, + const std::string &oldValue, + const std::string &newValue) noexcept { + auto spProviderSimple = provider.try_as(); + + if (spProviderSimple == nullptr || oldValue == newValue || !WasUiaPropertyAdvised(spProviderSimple, propId)) + return; + + UiaRaiseAutomationPropertyChangedEvent( + spProviderSimple.get(), propId, CComVariant(oldValue.c_str()), CComVariant(newValue.c_str())); +} + } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h index e296786c0f8..ba72f692c15 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h @@ -15,4 +15,14 @@ UiaNavigateHelper( HRESULT UiaGetBoundingRectangleHelper(::Microsoft::ReactNative::ReactTaggedView &view, UiaRect &rect) noexcept; +HRESULT UiaSetFocusHelper(::Microsoft::ReactNative::ReactTaggedView &view) noexcept; + +void UpdateUiaProperty(winrt::IInspectable provider, PROPERTYID propId, bool oldValue, bool newValue) noexcept; + +void UpdateUiaProperty( + winrt::IInspectable provider, + PROPERTYID propId, + const std::string &oldValue, + const std::string &newValue) noexcept; + } // namespace winrt::Microsoft::ReactNative::implementation