Skip to content

Commit

Permalink
Support UIA ::SetFocus and basic property change notifications (#11674)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
FalseLobster committed Jun 7, 2023
1 parent 83a6379 commit 72e480d
Show file tree
Hide file tree
Showing 11 changed files with 343 additions and 35 deletions.
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

0 comments on commit 72e480d

Please sign in to comment.