From bba47ee2319246d6f4cfe947418dc589a2e423a8 Mon Sep 17 00:00:00 2001
From: Chiara Mooney <34109996+chiaramooney@users.noreply.github.com>
Date: Wed, 9 Oct 2024 10:30:32 -0700
Subject: [PATCH] [Fabric] Implement IExpandCollapseProvider (#13892)
* Implement IExpandCollapseProvider
* Change files
* Adjust Example
* Format + Update Snapshots
---
...-9696c96d-9a44-4ffc-84ea-dd5451818206.json | 7 +++
.../js/examples/View/ViewExample.windows.js | 23 ++++----
.../ViewComponentTest.test.ts.snap | 1 +
.../__snapshots__/snapshotPages.test.js.snap | 18 +++---
.../RNTesterApp-Fabric/RNTesterApp-Fabric.cpp | 30 ++++++++++
.../ViewComponentTest.test.ts.snap | 1 +
.../CompositionDynamicAutomationProvider.cpp | 59 +++++++++++++++++++
.../CompositionDynamicAutomationProvider.h | 10 +++-
.../Fabric/Composition/UiaHelpers.cpp | 8 +++
.../Fabric/Composition/UiaHelpers.h | 1 +
10 files changed, 137 insertions(+), 21 deletions(-)
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"
+}
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