From 71fbaa57cf28dbb228464d7ae293ba8903efb99e Mon Sep 17 00:00:00 2001 From: Chiara Mooney <34109996+chiaramooney@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:05:02 -0700 Subject: [PATCH 1/4] Implement IExpandCollapseProvider --- .../js/examples/View/ViewExample.windows.js | 23 ++++---- .../RNTesterApp-Fabric/RNTesterApp-Fabric.cpp | 30 ++++++++++ .../CompositionDynamicAutomationProvider.cpp | 55 +++++++++++++++++++ .../CompositionDynamicAutomationProvider.h | 10 +++- .../Fabric/Composition/UiaHelpers.cpp | 8 +++ .../Fabric/Composition/UiaHelpers.h | 1 + 6 files changed, 114 insertions(+), 13 deletions(-) diff --git a/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js b/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js index 3ff467b4c55..b22b0729609 100644 --- a/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js +++ b/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js @@ -544,6 +544,7 @@ class AccessibilityExample extends React.Component< > { state: {tap: number} = { tap: 0, + expanded: false, }; render(): React.Node { @@ -554,10 +555,10 @@ class AccessibilityExample extends React.Component< accessibilityRole="button" accessibilityValue={{now: this.state.tap}} accessibilityActions={[ - {name: 'cut', label: 'cut'}, - {name: 'copy', label: 'copy'}, - {name: 'paste', label: 'paste'}, + {name: 'expand', label: 'expand'}, + {name: 'collapse', label: 'collapse'}, ]} + accessibilityState={{expanded: this.state.expanded}} accessibilityPosInSet={1} accessibilitySetSize={1} accessibilityLiveRegion='polite' @@ -566,19 +567,19 @@ class AccessibilityExample extends React.Component< focusable onAccessibilityAction={event => { switch (event.nativeEvent.actionName) { - case 'cut': - Alert.alert('Alert', 'cut action success'); - break; - case 'copy': - Alert.alert('Alert', 'copy action success'); - break; - case 'paste': - Alert.alert('Alert', 'paste action success'); + case 'expand': + this.setState({expanded: true}) break; + case 'collapse': + this.setState({expanded: false}) } }} onAccessibilityTap={() => { this.setState({tap: this.state.tap + 1}); + }} + onPress={()=>{ + this.setState({expanded: !this.state.expanded}); + console.log('Pressed'); }}> A View with accessibility values. Current Number of Accessibility Taps: {this.state.tap} diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp index 7179a0329e3..41b3104e1f5 100644 --- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp +++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp @@ -313,6 +313,23 @@ void InsertToggleStateValueIfNotDefault( } } +void InsertExpandCollapseStateValueIfNotDefault( + const winrt::Windows::Data::Json::JsonObject &obj, + winrt::hstring name, + ExpandCollapseState value, + ExpandCollapseState defaultValue = ExpandCollapseState::ExpandCollapseState_Collapsed) { + if (value != defaultValue) { + switch (value) { + case 0: + obj.Insert(name, winrt::Windows::Data::Json::JsonValue::CreateStringValue(L"Collapsed")); + break; + case 1: + obj.Insert(name, winrt::Windows::Data::Json::JsonValue::CreateStringValue(L"Expanded")); + break; + } + } +} + winrt::Windows::Data::Json::JsonObject ListErrors(winrt::Windows::Data::Json::JsonValue payload) { winrt::Windows::Data::Json::JsonObject result; winrt::Windows::Data::Json::JsonArray jsonErrors; @@ -338,6 +355,7 @@ void DumpUIAPatternInfo(IUIAutomationElement *pTarget, const winrt::Windows::Dat BOOL isReadOnly; ToggleState toggleState; IValueProvider *valuePattern; + ExpandCollapseState expandCollapseState; HRESULT hr; // Dump IValueProvider Information @@ -364,6 +382,18 @@ void DumpUIAPatternInfo(IUIAutomationElement *pTarget, const winrt::Windows::Dat } togglePattern->Release(); } + + // Dump IExpandCollapseProvider Information + IExpandCollapseProvider *expandCollapsePattern; + hr = pTarget->GetCurrentPattern(UIA_ExpandCollapsePatternId, reinterpret_cast(&expandCollapsePattern)); + if (SUCCEEDED(hr) && expandCollapsePattern) { + hr = expandCollapsePattern->get_ExpandCollapseState(&expandCollapseState); + if (SUCCEEDED(hr)) { + InsertExpandCollapseStateValueIfNotDefault(result, L"ExpandCollapsePattern.ExpandCollapseState", expandCollapseState); + } + expandCollapsePattern->Release(); + } + } winrt::Windows::Data::Json::JsonObject DumpUIATreeRecurse( diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp index e27ed2b265b..6a43d6eacf9 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp @@ -129,6 +129,18 @@ bool accessibilityValueHasValue(const facebook::react::AccessibilityValue &value return (value.min.has_value() && value.max.has_value()) || value.now.has_value() || value.text.has_value(); } +bool expandableControl(const facebook::react::SharedViewProps props){ + if (props->accessibilityState.has_value() && props->accessibilityState->expanded.has_value()) + return true; + auto accessibilityActions = props->accessibilityActions; + for (size_t i = 0; i < accessibilityActions.size(); i++) { + if (accessibilityActions[i].name == "expand" || accessibilityActions[i].name == "collapse") { + return true; + } + } + return false; +} + HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTERNID patternId, IUnknown **pRetVal) { if (pRetVal == nullptr) return E_POINTER; @@ -181,6 +193,13 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTE AddRef(); } + if (patternId == UIA_ExpandCollapsePatternId && + (accessibilityRole == "combobox" || accessibilityRole == "splitbutton" || accessibilityRole == "treeitem" || + (expandableControl(props) && (accessibilityRole == "toolbar" || accessibilityRole == "menuitem" || accessibilityRole == "menubar" || accessibilityRole == "listitem" || accessibilityRole == "group" || accessibilityRole == "button")))){ + *pRetVal = static_cast(this); + AddRef(); + } + return S_OK; } @@ -496,4 +515,40 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::Toggle() { return S_OK; } +HRESULT __stdcall CompositionDynamicAutomationProvider::get_ExpandCollapseState(ExpandCollapseState *pRetVal) { + if (pRetVal == nullptr) + return E_POINTER; + auto strongView = m_view.view(); + + if (!strongView) + return UIA_E_ELEMENTNOTAVAILABLE; + + auto props = std::static_pointer_cast( + winrt::get_self(strongView)->props()); + + if (props == nullptr) + return UIA_E_ELEMENTNOTAVAILABLE; + + *pRetVal = props->accessibilityState->expanded.has_value() ? GetExpandCollapseState(props->accessibilityState->expanded.value()) : ExpandCollapseState_Collapsed; + return S_OK; +} + +HRESULT __stdcall CompositionDynamicAutomationProvider::Expand() { + auto strongView = m_view.view(); + + if (!strongView) + return UIA_E_ELEMENTNOTAVAILABLE; + DispatchAccessibilityAction(m_view, "expand"); + return S_OK; +} + +HRESULT __stdcall CompositionDynamicAutomationProvider::Collapse() { + auto strongView = m_view.view(); + + if (!strongView) + return UIA_E_ELEMENTNOTAVAILABLE; + DispatchAccessibilityAction(m_view, "collapse"); + return S_OK; +} + } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h index b089efd7e84..fd37624b580 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h @@ -16,7 +16,8 @@ class CompositionDynamicAutomationProvider : public winrt::implements< IInvokeProvider, IScrollItemProvider, IValueProvider, - IToggleProvider> { + IToggleProvider, + IExpandCollapseProvider> { public: CompositionDynamicAutomationProvider( const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView) noexcept; @@ -47,10 +48,15 @@ class CompositionDynamicAutomationProvider : public winrt::implements< virtual HRESULT __stdcall get_Value(BSTR *pRetVal) override; virtual HRESULT __stdcall get_IsReadOnly(BOOL *pRetVal) override; - // inherited via IToggleProivder + // inherited via IToggleProvider virtual HRESULT __stdcall get_ToggleState(ToggleState *pRetVal) override; virtual HRESULT __stdcall Toggle() override; + // inherited via IExpandCollapseProvider + virtual HRESULT __stdcall get_ExpandCollapseState(ExpandCollapseState *pRetVal) override; + virtual HRESULT __stdcall Expand() override; + virtual HRESULT __stdcall Collapse() override; + private: ::Microsoft::ReactNative::ReactTaggedView m_view; }; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp index b678219c06c..b0ec32a63a5 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp @@ -206,4 +206,12 @@ void DispatchAccessibilityAction(::Microsoft::ReactNative::ReactTaggedView &view } } +ExpandCollapseState GetExpandCollapseState(const bool &expanded) noexcept { + if (expanded) { + return ExpandCollapseState_Expanded; + }else{ + return ExpandCollapseState_Collapsed; + } +} + } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h index 28a6fcbebcb..42a31aa131e 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h @@ -35,4 +35,5 @@ std::string extractAccessibilityValue(const facebook::react::AccessibilityValue void DispatchAccessibilityAction(::Microsoft::ReactNative::ReactTaggedView &view, const std::string &action) noexcept; +ExpandCollapseState GetExpandCollapseState(const bool &expanded) noexcept; } // namespace winrt::Microsoft::ReactNative::implementation From 007447dac175f6f45fd968c7b154d6ef847b2556 Mon Sep 17 00:00:00 2001 From: Chiara Mooney <34109996+chiaramooney@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:12:42 -0700 Subject: [PATCH 2/4] Change files --- ...ative-windows-9696c96d-9a44-4ffc-84ea-dd5451818206.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-9696c96d-9a44-4ffc-84ea-dd5451818206.json diff --git a/change/react-native-windows-9696c96d-9a44-4ffc-84ea-dd5451818206.json b/change/react-native-windows-9696c96d-9a44-4ffc-84ea-dd5451818206.json new file mode 100644 index 00000000000..38c2fdeb383 --- /dev/null +++ b/change/react-native-windows-9696c96d-9a44-4ffc-84ea-dd5451818206.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement IExpandCollapseProvider", + "packageName": "react-native-windows", + "email": "34109996+chiaramooney@users.noreply.github.com", + "dependentChangeType": "patch" +} From 3dbc61ae5cf924a0abcd4e1492854fd1067f3261 Mon Sep 17 00:00:00 2001 From: Chiara Mooney <34109996+chiaramooney@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:22:45 -0700 Subject: [PATCH 3/4] Adjust Example --- .../tester/src/js/examples/View/ViewExample.windows.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js b/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js index b22b0729609..268a06b842e 100644 --- a/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js +++ b/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js @@ -544,7 +544,7 @@ class AccessibilityExample extends React.Component< > { state: {tap: number} = { tap: 0, - expanded: false, + expanded: true, }; render(): React.Node { From 2e7a128d166bd6239c9abe774d7cdbb1883e51bb Mon Sep 17 00:00:00 2001 From: Chiara Mooney <34109996+chiaramooney@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:19:10 -0700 Subject: [PATCH 4/4] Format + Update Snapshots --- .../ViewComponentTest.test.ts.snap | 1 + .../__snapshots__/snapshotPages.test.js.snap | 18 ++++++++++-------- .../RNTesterApp-Fabric/RNTesterApp-Fabric.cpp | 4 ++-- .../ViewComponentTest.test.ts.snap | 1 + .../CompositionDynamicAutomationProvider.cpp | 16 ++++++++++------ .../Fabric/Composition/UiaHelpers.cpp | 2 +- 6 files changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap index 1cb3315b5f0..90015e53205 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap @@ -1094,6 +1094,7 @@ exports[`View Tests Views can have customized accessibility 1`] = ` "Automation Tree": { "AutomationId": "accessibility", "ControlType": 50000, + "ExpandCollapsePattern.ExpandCollapseState": "Expanded", "HelpText": "Accessibility Hint", "IsKeyboardFocusable": true, "LiveSetting": "Polite", diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap index c299dc2003f..28f561e23e7 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap @@ -69528,16 +69528,12 @@ exports[`snapshotAllPages View 22`] = ` accessibilityActions={ [ { - "label": "cut", - "name": "cut", + "label": "expand", + "name": "expand", }, { - "label": "copy", - "name": "copy", - }, - { - "label": "paste", - "name": "paste", + "label": "collapse", + "name": "collapse", }, ] } @@ -69547,6 +69543,11 @@ exports[`snapshotAllPages View 22`] = ` accessibilityPosInSet={1} accessibilityRole="button" accessibilitySetSize={1} + accessibilityState={ + { + "expanded": true, + } + } accessibilityValue={ { "now": 0, @@ -69556,6 +69557,7 @@ exports[`snapshotAllPages View 22`] = ` focusable={true} onAccessibilityAction={[Function]} onAccessibilityTap={[Function]} + onPress={[Function]} testID="accessibility" > diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp index 41b3104e1f5..c7bbd53609a 100644 --- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp +++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp @@ -389,11 +389,11 @@ void DumpUIAPatternInfo(IUIAutomationElement *pTarget, const winrt::Windows::Dat if (SUCCEEDED(hr) && expandCollapsePattern) { hr = expandCollapsePattern->get_ExpandCollapseState(&expandCollapseState); if (SUCCEEDED(hr)) { - InsertExpandCollapseStateValueIfNotDefault(result, L"ExpandCollapsePattern.ExpandCollapseState", expandCollapseState); + InsertExpandCollapseStateValueIfNotDefault( + result, L"ExpandCollapsePattern.ExpandCollapseState", expandCollapseState); } expandCollapsePattern->Release(); } - } winrt::Windows::Data::Json::JsonObject DumpUIATreeRecurse( diff --git a/packages/e2e-test-app/test/__snapshots__/ViewComponentTest.test.ts.snap b/packages/e2e-test-app/test/__snapshots__/ViewComponentTest.test.ts.snap index 631f203d108..decda6dae53 100644 --- a/packages/e2e-test-app/test/__snapshots__/ViewComponentTest.test.ts.snap +++ b/packages/e2e-test-app/test/__snapshots__/ViewComponentTest.test.ts.snap @@ -363,6 +363,7 @@ exports[`ViewTests Views can have a custom nativeID 1`] = ` exports[`ViewTests Views can have accessibility customization 1`] = ` { "AccessibilityRole": "Button", + "AccessibilityStateExpanded": true, "AutomationId": "accessibility", "AutomationPositionInSet": 1, "AutomationSizeOfSet": 1, diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp index 6a43d6eacf9..88330c1e804 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp @@ -129,7 +129,7 @@ bool accessibilityValueHasValue(const facebook::react::AccessibilityValue &value return (value.min.has_value() && value.max.has_value()) || value.now.has_value() || value.text.has_value(); } -bool expandableControl(const facebook::react::SharedViewProps props){ +bool expandableControl(const facebook::react::SharedViewProps props) { if (props->accessibilityState.has_value() && props->accessibilityState->expanded.has_value()) return true; auto accessibilityActions = props->accessibilityActions; @@ -193,9 +193,11 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTE AddRef(); } - if (patternId == UIA_ExpandCollapsePatternId && - (accessibilityRole == "combobox" || accessibilityRole == "splitbutton" || accessibilityRole == "treeitem" || - (expandableControl(props) && (accessibilityRole == "toolbar" || accessibilityRole == "menuitem" || accessibilityRole == "menubar" || accessibilityRole == "listitem" || accessibilityRole == "group" || accessibilityRole == "button")))){ + if (patternId == UIA_ExpandCollapsePatternId && + (accessibilityRole == "combobox" || accessibilityRole == "splitbutton" || accessibilityRole == "treeitem" || + (expandableControl(props) && + (accessibilityRole == "toolbar" || accessibilityRole == "menuitem" || accessibilityRole == "menubar" || + accessibilityRole == "listitem" || accessibilityRole == "group" || accessibilityRole == "button")))) { *pRetVal = static_cast(this); AddRef(); } @@ -522,14 +524,16 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::get_ExpandCollapseState( if (!strongView) return UIA_E_ELEMENTNOTAVAILABLE; - + auto props = std::static_pointer_cast( winrt::get_self(strongView)->props()); if (props == nullptr) return UIA_E_ELEMENTNOTAVAILABLE; - *pRetVal = props->accessibilityState->expanded.has_value() ? GetExpandCollapseState(props->accessibilityState->expanded.value()) : ExpandCollapseState_Collapsed; + *pRetVal = props->accessibilityState->expanded.has_value() + ? GetExpandCollapseState(props->accessibilityState->expanded.value()) + : ExpandCollapseState_Collapsed; return S_OK; } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp index b0ec32a63a5..9689b046f60 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp @@ -209,7 +209,7 @@ void DispatchAccessibilityAction(::Microsoft::ReactNative::ReactTaggedView &view ExpandCollapseState GetExpandCollapseState(const bool &expanded) noexcept { if (expanded) { return ExpandCollapseState_Expanded; - }else{ + } else { return ExpandCollapseState_Collapsed; } }