Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support UIA ::SetFocus and basic property change notifications #11674

Merged
merged 13 commits into from
Jun 7, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Add UIA focus management and AdvisedEvents for prop changes",
"packageName": "react-native-windows",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
#include "pch.h"
#include "CompositionDynamicAutomationProvider.h"
#include <Fabric/ComponentView.h>
#pragma warning(push)
#pragma warning(disable : 4229)
#define IN
#define OUT
#include <atlsafe.h>
#pragma warning(pop)
#include <Unicode.h>
#include "RootComponentView.h"
#include "UiaHelpers.h"

Expand Down Expand Up @@ -41,16 +36,15 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetRuntimeId(SAFEARRAY *
if (!strongView)
return UIA_E_ELEMENTNOTAVAILABLE;

CComSafeArray<int32_t> 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<void *>(&runtimeId[i]));
}

return S_OK;
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "pch.h"
#include "CompositionRootAutomationProvider.h"
#include <algorithm>
#include "UiaHelpers.h"

namespace winrt::Microsoft::ReactNative::implementation {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<CompositionRootAutomationProvider::AdvisedEvent> &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<void **>(&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<CompositionRootAutomationProvider::AdvisedEvent> &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<void **>(&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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<EVENTID, PROPERTYID>::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<AdvisedEvent> m_advisedEvents{};
std::vector<AdvisedEvent> m_advisedProperties{};
::Microsoft::ReactNative::ReactTaggedView m_view;
HWND m_hwnd{nullptr};
};
Expand Down
Loading