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
Expand Up @@ -96,7 +96,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 +211,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 @@ -232,9 +234,28 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPropertyValue(PROPERT
pRetVal->bstrVal = temp.Detach();
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,6 +1,13 @@
#include "pch.h"
#include "CompositionRootAutomationProvider.h"
#include <algorithm>
#include "UiaHelpers.h"
#pragma warning(push)
#pragma warning(disable : 4229)
#define IN
#define OUT
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this for? SAFEARRAY?
Most of the code is using std::vector, so where's the SAFEARRAY dependency being forced?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, a lot of the UIA APIs use SAFEARRAY for certain parameters.

#include <atlsafe.h>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not use ATLSafe.h

Copy link
Member

@asklar asklar May 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this PR adds SAFEARRAY support to WIL which we should be using instead:
microsoft/wil#145

could you perhaps work with @chrisglein 's team to see that PR through and use it here?

Copy link
Member

@asklar asklar May 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+@carlos-zamora who filed the original wil issue. Can you help? :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I wasn't aware of the WIL API. @asklar I've been pulled off this work for now. Would you be OK with me just using the SAFEARRAY directly and opening an issue to move this to the WIL stuff?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonthysell we can interface manually (which I think is what @FalseLobster is proposing) but it's going to be a lot more code than necessary and hard to maintain/reason over. WIL is header only and designed to simplify calling win32 apis so your code becomes a lot more concise. Ideally we can move to that sooner than later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FalseLobster I'm okay with taking things as you've written them and opening an issue to replace ATL with WIL and assign it to me. There's probably other places the (Fabric) codebase could benefit from access to WIL helpers to deal with all this old Win32 stuff.

@asklar Yes, WIL is header only, but it's also "yet another re-wrapping of standard C++" to add to this project. Like do we really need every implementation of smart pointers that have ever been made? We have stl, boost, Mso, cppwinrt, now wil.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the alternative being used here is to use ATL. We should not be using ATL.
WIL provides convenient wrappers for this specific use case. If STL, Boost, MSO or CppWinRT had RAII wrappers for these types, great. I don't believe they do though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@asklar , @jonthysell , could we avoid dependency on the new library?
SAFEARRAY are not so difficult to use directly.
I would expect that C++/WinRT has all the required wrappers for them.

C++/WinRT is also header-only library, and we know the overall impact on the developer experience.
I would stay away from the header-only libraries since they put a huge pressure on the compiler: such headers must be often compiled again and again for each .cpp that slows down compilation no matter how powerful your computer is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C++/WinRT is for working with WinRT, but these are not WinRT apis, so C++/WinRT doesn't help in this case. Feel free to use SAFEARRAY directly.

#pragma warning(pop)

namespace winrt::Microsoft::ReactNative::implementation {

Expand Down Expand Up @@ -32,7 +39,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 +129,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 +193,126 @@ HRESULT __stdcall CompositionRootAutomationProvider::Navigate(
return S_OK;
}

// The old C-style interface for SAFEARRAY is tedious to use. ATL provides CComSafeArray which allows us to interact
// with the data structure using much more modern methods. However, AdviseEventAdded/Removed don't expect us to
// deallocate their In param, so this is a simple RAII wrapper to Attach/Detach in the scope of those functions and
// perform a little validation that the incoming SAFEARRAY is well formed.
class UIAPropertyArray {
CComSafeArray<PROPERTYID> m_propArray{};

public:
UIAPropertyArray(SAFEARRAY *psaProperties) noexcept {
VARTYPE vt;
if (psaProperties && SUCCEEDED(SafeArrayGetVartype(psaProperties, &vt)) && vt == m_propArray.GetType()) {
m_propArray.Attach(psaProperties);
}
}
~UIAPropertyArray() noexcept {
if (m_propArray.GetSafeArrayPtr() != nullptr)
m_propArray.Detach();
}
bool IsValid() noexcept {
return m_propArray.GetSafeArrayPtr() != nullptr && m_propArray.GetDimensions() == 1 && m_propArray.GetCount() > 0;
}
CComSafeArray<PROPERTYID> *operator->() noexcept {
return &m_propArray;
}
};

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 {
UIAPropertyArray props(psaProperties);

if (!props.IsValid()) {
return E_INVALIDARG;
}

// Note SAFEARRAY's upperbound is inclusive
for (auto i = props->GetLowerBound(); i <= props->GetUpperBound(); i++) {
auto prop = props->GetAt(i);
AdviseEventAddedImpl(m_advisedProperties, prop);
}
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 {
UIAPropertyArray props(psaProperties);

if (!props.IsValid()) {
return E_INVALIDARG;
}

auto returnHr = S_OK;
for (auto i = props->GetLowerBound(); i <= props->GetUpperBound(); i++) {
auto prop = props->GetAt(i);
auto hr = AdviseEventRemovedImpl(m_advisedProperties, prop);
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "CompositionDynamicAutomationProvider.h"
#include "CompositionHelpers.h"
#include "RootComponentView.h"
#include "UiaHelpers.h"
#include "d2d1helper.h"

namespace Microsoft::ReactNative {
Expand All @@ -37,7 +38,6 @@ RootComponentView *CompositionBaseComponentView::rootComponentView() noexcept {
if (m_parent)
return m_parent->rootComponentView();

assert(false);
return nullptr;
}

Expand Down Expand Up @@ -78,13 +78,25 @@ bool CompositionBaseComponentView::runOnChildren(bool forward, Mso::Functor<bool
void CompositionBaseComponentView::onFocusLost() noexcept {
m_eventEmitter->onBlur();
showFocusVisual(false);
if (UiaClientsAreListening()) {
winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty(
EnsureUiaProvider(), UIA_HasKeyboardFocusPropertyId, true, false);
}
}

void CompositionBaseComponentView::onFocusGained() noexcept {
m_eventEmitter->onFocus();
if (m_enableFocusVisual) {
showFocusVisual(true);
}
if (UiaClientsAreListening()) {
auto spProviderSimple = EnsureUiaProvider().try_as<IRawElementProviderSimple>();
if (spProviderSimple != nullptr) {
winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty(
m_uiaProvider, UIA_HasKeyboardFocusPropertyId, false, true);
UiaRaiseAutomationEvent(spProviderSimple.get(), UIA_AutomationFocusChangedEventId);
}
}
}

void CompositionBaseComponentView::updateEventEmitter(
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1206,6 +1239,7 @@ void CompositionViewComponentView::updateProps(
m_visual.Opacity(newViewProps.opacity);
}

updateAccessibilityProps(oldViewProps, newViewProps);
updateBorderProps(oldViewProps, newViewProps);

// Shadow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -94,11 +102,8 @@ bool RootComponentView::TryMoveFocus(bool next) noexcept {
Mso::Functor<bool(IComponentView &)> 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);
Expand Down
Loading