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" +} 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 34829aa273e..eef3753476c 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 @@ -551,6 +551,7 @@ class AccessibilityExample extends React.Component< > { state: {tap: number} = { tap: 0, + expanded: true, }; render(): React.Node { @@ -561,10 +562,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' @@ -573,19 +574,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/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 73ed893c300..56ba322bc7c 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 @@ -77763,16 +77763,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", }, ] } @@ -77782,6 +77778,11 @@ exports[`snapshotAllPages View 22`] = ` accessibilityPosInSet={1} accessibilityRole="button" accessibilitySetSize={1} + accessibilityState={ + { + "expanded": true, + } + } accessibilityValue={ { "now": 0, @@ -77791,6 +77792,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 1b3c1a05bd3..02b15b3254f 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 @@ -314,6 +314,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; @@ -339,6 +356,7 @@ void DumpUIAPatternInfo(IUIAutomationElement *pTarget, const winrt::Windows::Dat BOOL isReadOnly; ToggleState toggleState; IValueProvider *valuePattern; + ExpandCollapseState expandCollapseState; HRESULT hr; // Dump IValueProvider Information @@ -365,6 +383,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/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 e27ed2b265b..88330c1e804 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,15 @@ 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 +517,42 @@ 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..9689b046f60 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