Skip to content

Commit 72e480d

Browse files
authored
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
1 parent 83a6379 commit 72e480d

11 files changed

+343
-35
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Add UIA focus management and AdvisedEvents for prop changes",
4+
"packageName": "react-native-windows",
5+
"email": "adrum@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

packages/playground/windows/playground-composition/Playground-Composition.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
486486
case WM_GETOBJECT: {
487487
if (lparam == UiaRootObjectId) {
488488
auto windowData = WindowData::GetFromWindow(hwnd);
489-
if (!windowData->m_windowInited)
489+
if (windowData == nullptr || !windowData->m_windowInited)
490490
break;
491491

492492
auto hwndHost = windowData->m_CompositionHwndHost;

vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
#include "pch.h"
22
#include "CompositionDynamicAutomationProvider.h"
33
#include <Fabric/ComponentView.h>
4-
#pragma warning(push)
5-
#pragma warning(disable : 4229)
6-
#define IN
7-
#define OUT
8-
#include <atlsafe.h>
9-
#pragma warning(pop)
4+
#include <Unicode.h>
105
#include "RootComponentView.h"
116
#include "UiaHelpers.h"
127

@@ -41,16 +36,15 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetRuntimeId(SAFEARRAY *
4136
if (!strongView)
4237
return UIA_E_ELEMENTNOTAVAILABLE;
4338

44-
CComSafeArray<int32_t> runtimeId;
45-
auto hr = runtimeId.Create(2);
39+
*pRetVal = SafeArrayCreateVector(VT_I4, 0, 2);
4640

47-
if (FAILED(hr))
48-
return hr;
41+
if (*pRetVal == nullptr)
42+
return E_OUTOFMEMORY;
4943

50-
runtimeId[0] = UiaAppendRuntimeId;
51-
runtimeId[1] = strongView->tag();
52-
53-
*pRetVal = runtimeId.Detach();
44+
int runtimeId[] = {UiaAppendRuntimeId, strongView->tag()};
45+
for (long i = 0; i < 2; i++) {
46+
SafeArrayPutElement(*pRetVal, &i, static_cast<void *>(&runtimeId[i]));
47+
}
5448

5549
return S_OK;
5650
}
@@ -96,7 +90,7 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetEmbeddedFragmentRoots
9690
}
9791

9892
HRESULT __stdcall CompositionDynamicAutomationProvider::SetFocus(void) {
99-
return S_OK;
93+
return UiaSetFocusHelper(m_view);
10094
}
10195

10296
HRESULT __stdcall CompositionDynamicAutomationProvider::get_FragmentRoot(IRawElementProviderFragmentRoot **pRetVal) {
@@ -211,6 +205,8 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPropertyValue(PROPERT
211205
if (props == nullptr)
212206
return UIA_E_ELEMENTNOTAVAILABLE;
213207

208+
auto hr = S_OK;
209+
214210
switch (propertyId) {
215211
case UIA_ControlTypePropertyId: {
216212
pRetVal->vt = VT_I4;
@@ -220,21 +216,40 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPropertyValue(PROPERT
220216
}
221217
case UIA_AutomationIdPropertyId: {
222218
pRetVal->vt = VT_BSTR;
223-
auto testId = props->testId;
224-
CComBSTR temp(testId.c_str());
225-
pRetVal->bstrVal = temp.Detach();
219+
auto wideTestId = ::Microsoft::Common::Unicode::Utf8ToUtf16(props->testId);
220+
pRetVal->bstrVal = SysAllocString(wideTestId.c_str());
221+
hr = pRetVal->bstrVal != nullptr ? S_OK : E_OUTOFMEMORY;
226222
break;
227223
}
228224
case UIA_NamePropertyId: {
229225
pRetVal->vt = VT_BSTR;
230-
auto name = props->accessibilityLabel;
231-
CComBSTR temp(name.c_str());
232-
pRetVal->bstrVal = temp.Detach();
226+
auto wideName = ::Microsoft::Common::Unicode::Utf8ToUtf16(props->accessibilityLabel);
227+
pRetVal->bstrVal = SysAllocString(wideName.c_str());
228+
hr = pRetVal->bstrVal != nullptr ? S_OK : E_OUTOFMEMORY;
229+
break;
230+
}
231+
case UIA_IsKeyboardFocusablePropertyId: {
232+
pRetVal->vt = VT_BOOL;
233+
pRetVal->boolVal = props->focusable ? VARIANT_TRUE : VARIANT_FALSE;
234+
break;
235+
}
236+
case UIA_HasKeyboardFocusPropertyId: {
237+
auto rootCV = strongView->rootComponentView();
238+
if (rootCV == nullptr)
239+
return UIA_E_ELEMENTNOTAVAILABLE;
240+
241+
pRetVal->vt = VT_BOOL;
242+
pRetVal->boolVal = rootCV->GetFocusedComponent() == strongView.get() ? VARIANT_TRUE : VARIANT_FALSE;
243+
break;
244+
}
245+
case UIA_IsEnabledPropertyId: {
246+
pRetVal->vt = VT_BOOL;
247+
pRetVal->boolVal = !props->accessibilityState.disabled ? VARIANT_TRUE : VARIANT_FALSE;
233248
break;
234249
}
235250
}
236251

237-
return S_OK;
252+
return hr;
238253
}
239254

240255
HRESULT __stdcall CompositionDynamicAutomationProvider::get_HostRawElementProvider(

vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp

Lines changed: 155 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "pch.h"
22
#include "CompositionRootAutomationProvider.h"
3+
#include <algorithm>
34
#include "UiaHelpers.h"
45

56
namespace winrt::Microsoft::ReactNative::implementation {
@@ -32,7 +33,7 @@ HRESULT __stdcall CompositionRootAutomationProvider::GetEmbeddedFragmentRoots(SA
3233
}
3334

3435
HRESULT __stdcall CompositionRootAutomationProvider::SetFocus(void) {
35-
return S_OK;
36+
return UiaSetFocusHelper(m_view);
3637
}
3738

3839
HRESULT __stdcall CompositionRootAutomationProvider::GetPatternProvider(PATTERNID patternId, IUnknown **pRetVal) {
@@ -122,10 +123,7 @@ HRESULT __stdcall CompositionRootAutomationProvider::ElementProviderFromPoint(
122123
return UIA_E_ELEMENTNOTAVAILABLE;
123124
}
124125

125-
auto spRootView = strongView->rootComponentView();
126-
if (spRootView == nullptr) {
127-
return UIA_E_ELEMENTNOTAVAILABLE;
128-
}
126+
auto spRootView = std::static_pointer_cast<::Microsoft::ReactNative::RootComponentView>(strongView);
129127

130128
if (m_hwnd == nullptr || !IsWindow(m_hwnd)) {
131129
// TODO: Add support for non-HWND based hosting
@@ -189,4 +187,156 @@ HRESULT __stdcall CompositionRootAutomationProvider::Navigate(
189187
return S_OK;
190188
}
191189

190+
// RAII wrapper to unaccess SafeArray data so I can early return in the relevant functions
191+
class SafeArrayAccessScope {
192+
SAFEARRAY *m_pArray = nullptr;
193+
194+
public:
195+
SafeArrayAccessScope(SAFEARRAY *psa) noexcept : m_pArray(psa) {}
196+
~SafeArrayAccessScope() noexcept {
197+
if (m_pArray != nullptr)
198+
SafeArrayUnaccessData(m_pArray);
199+
}
200+
};
201+
202+
void AdviseEventAddedImpl(
203+
std::vector<CompositionRootAutomationProvider::AdvisedEvent> &advisedEvents,
204+
EVENTID idEvent) noexcept {
205+
auto it = std::find_if(
206+
advisedEvents.begin(),
207+
advisedEvents.end(),
208+
[idEvent](const CompositionRootAutomationProvider::AdvisedEvent &ae) noexcept { return ae.Event == idEvent; });
209+
210+
if (it == advisedEvents.end()) {
211+
advisedEvents.emplace_back(CompositionRootAutomationProvider::AdvisedEvent{idEvent, 1 /*Count*/});
212+
} else {
213+
it->Count++;
214+
}
215+
}
216+
217+
HRESULT CompositionRootAutomationProvider::AdvisePropertiesAdded(SAFEARRAY *psaProperties) noexcept {
218+
if (psaProperties == nullptr)
219+
return E_POINTER;
220+
221+
long *pValues = nullptr;
222+
auto hr = SafeArrayAccessData(psaProperties, reinterpret_cast<void **>(&pValues));
223+
if (FAILED(hr))
224+
return hr;
225+
226+
SafeArrayAccessScope accessScope(psaProperties);
227+
228+
if (SafeArrayGetDim(psaProperties) != 1)
229+
return E_INVALIDARG;
230+
231+
VARTYPE vt;
232+
hr = SafeArrayGetVartype(psaProperties, &vt);
233+
if (FAILED(hr) || vt != VT_I4)
234+
return E_INVALIDARG;
235+
236+
long lower;
237+
hr = SafeArrayGetLBound(psaProperties, 1, &lower);
238+
if (FAILED(hr))
239+
return hr;
240+
241+
long upper;
242+
hr = SafeArrayGetUBound(psaProperties, 1, &upper);
243+
if (FAILED(hr))
244+
return hr;
245+
246+
long count = upper - lower + 1;
247+
248+
for (int i = 0; i < count; i++) {
249+
AdviseEventAddedImpl(m_advisedProperties, pValues[i]);
250+
}
251+
return S_OK;
252+
}
253+
254+
HRESULT CompositionRootAutomationProvider::AdviseEventAdded(EVENTID idEvent, SAFEARRAY *psaProperties) {
255+
if (idEvent == UIA_AutomationPropertyChangedEventId) {
256+
return AdvisePropertiesAdded(psaProperties);
257+
}
258+
AdviseEventAddedImpl(m_advisedEvents, idEvent);
259+
return S_OK;
260+
}
261+
262+
HRESULT AdviseEventRemovedImpl(
263+
std::vector<CompositionRootAutomationProvider::AdvisedEvent> &advisedEvents,
264+
EVENTID idEvent) noexcept {
265+
auto it = std::find_if(
266+
advisedEvents.begin(),
267+
advisedEvents.end(),
268+
[idEvent](const CompositionRootAutomationProvider::AdvisedEvent &ae) noexcept { return ae.Event == idEvent; });
269+
270+
if (it == advisedEvents.end()) {
271+
assert(false);
272+
return UIA_E_INVALIDOPERATION;
273+
} else if (it->Count == 1) {
274+
advisedEvents.erase(it);
275+
} else {
276+
it->Count--;
277+
}
278+
return S_OK;
279+
}
280+
281+
HRESULT CompositionRootAutomationProvider::AdvisePropertiesRemoved(SAFEARRAY *psaProperties) noexcept {
282+
if (psaProperties == nullptr)
283+
return E_POINTER;
284+
285+
long *pValues = nullptr;
286+
auto hr = SafeArrayAccessData(psaProperties, reinterpret_cast<void **>(&pValues));
287+
if (FAILED(hr))
288+
return hr;
289+
290+
SafeArrayAccessScope accessScope(psaProperties);
291+
292+
if (SafeArrayGetDim(psaProperties) != 1)
293+
return E_INVALIDARG;
294+
295+
VARTYPE vt;
296+
hr = SafeArrayGetVartype(psaProperties, &vt);
297+
if (FAILED(hr) || vt != VT_I4)
298+
return E_INVALIDARG;
299+
300+
long lower;
301+
hr = SafeArrayGetLBound(psaProperties, 1, &lower);
302+
if (FAILED(hr))
303+
return hr;
304+
305+
long upper;
306+
hr = SafeArrayGetUBound(psaProperties, 1, &upper);
307+
if (FAILED(hr))
308+
return hr;
309+
310+
long count = upper - lower + 1;
311+
auto returnHr = S_OK;
312+
for (int i = 0; i < count; i++) {
313+
auto hr = AdviseEventRemovedImpl(m_advisedProperties, pValues[i]);
314+
if (FAILED(hr)) {
315+
returnHr = hr;
316+
}
317+
}
318+
return returnHr;
319+
}
320+
321+
HRESULT
322+
CompositionRootAutomationProvider::AdviseEventRemoved(EVENTID idEvent, SAFEARRAY *psaProperties) {
323+
if (idEvent == UIA_AutomationPropertyChangedEventId) {
324+
return AdvisePropertiesRemoved(psaProperties);
325+
}
326+
327+
return AdviseEventRemovedImpl(m_advisedEvents, idEvent);
328+
}
329+
330+
bool CompositionRootAutomationProvider::WasEventAdvised(EVENTID event) noexcept {
331+
return std::any_of(m_advisedEvents.begin(), m_advisedEvents.end(), [event](const AdvisedEvent &ae) {
332+
return ae.Event == event && ae.Count > 0;
333+
});
334+
}
335+
336+
bool CompositionRootAutomationProvider::WasPropertyAdvised(PROPERTYID prop) noexcept {
337+
return std::any_of(m_advisedProperties.begin(), m_advisedProperties.end(), [prop](const AdvisedEvent &ae) {
338+
return ae.Property == prop && ae.Count > 0;
339+
});
340+
}
341+
192342
} // namespace winrt::Microsoft::ReactNative::implementation

vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.h

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ class CompositionRootAutomationProvider : public winrt::implements<
1313
IInspectable,
1414
IRawElementProviderFragmentRoot,
1515
IRawElementProviderFragment,
16-
IRawElementProviderSimple> {
16+
IRawElementProviderSimple,
17+
IRawElementProviderAdviseEvents> {
1718
public:
1819
// inherited via IRawElementProviderFragmentRoot
1920
virtual HRESULT __stdcall ElementProviderFromPoint(double x, double y, IRawElementProviderFragment **pRetVal)
@@ -34,11 +35,39 @@ class CompositionRootAutomationProvider : public winrt::implements<
3435
virtual HRESULT __stdcall GetPropertyValue(PROPERTYID propertyId, VARIANT *pRetVal) override;
3536
virtual HRESULT __stdcall get_HostRawElementProvider(IRawElementProviderSimple **pRetVal) override;
3637

38+
// IRawElementProviderAdviseEvents
39+
virtual HRESULT __stdcall AdviseEventAdded(EVENTID idEvent, SAFEARRAY *psaProperties) override;
40+
virtual HRESULT __stdcall AdviseEventRemoved(EVENTID idEvent, SAFEARRAY *psaProperties) override;
41+
3742
CompositionRootAutomationProvider(
3843
const std::shared_ptr<::Microsoft::ReactNative::RootComponentView> &componentView) noexcept;
44+
3945
void SetHwnd(HWND hwnd) noexcept;
46+
bool WasPropertyAdvised(PROPERTYID prop) noexcept;
47+
bool WasEventAdvised(EVENTID event) noexcept;
48+
49+
// It's unlikely for the underlying primitive types for EVENTID and PROPERTYID to ever change, but let's make sure
50+
static_assert(std::is_same<EVENTID, PROPERTYID>::value);
51+
// Helper class for AdviseEventAdded/Removed. I could've simply used a std::pair, but I find using structs with named
52+
// members easier to read and more self-documenting than pair.first and pair.last. Since this is simply syntactic
53+
// sugar, I'm leveraging the fact that both EVENTID and PROPERTYID are ints under the hood to share
54+
// AdviseEventAddedImpl
55+
struct AdvisedEvent {
56+
union {
57+
EVENTID Event;
58+
PROPERTYID Property;
59+
};
60+
uint32_t Count;
61+
};
4062

4163
private:
64+
HRESULT AdvisePropertiesAdded(SAFEARRAY *psaProperties) noexcept;
65+
HRESULT AdvisePropertiesRemoved(SAFEARRAY *psaProperties) noexcept;
66+
67+
// Linear search on unsorted vectors is typically faster than more sophisticated data structures when N is small. In
68+
// practice ATs tend to only listen to a dozen or so props and events, so std::vector is likely better than maps.
69+
std::vector<AdvisedEvent> m_advisedEvents{};
70+
std::vector<AdvisedEvent> m_advisedProperties{};
4271
::Microsoft::ReactNative::ReactTaggedView m_view;
4372
HWND m_hwnd{nullptr};
4473
};

0 commit comments

Comments
 (0)