diff --git a/change/@react-native-windows-automation-commands-78e59fbb-fe4f-4921-b941-78c82219d869.json b/change/@react-native-windows-automation-commands-78e59fbb-fe4f-4921-b941-78c82219d869.json
new file mode 100644
index 00000000000..5ec0f7ca9c3
--- /dev/null
+++ b/change/@react-native-windows-automation-commands-78e59fbb-fe4f-4921-b941-78c82219d869.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Add name to accessibility type",
+ "packageName": "@react-native-windows/automation-commands",
+ "email": "30809111+acoates-ms@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
\ No newline at end of file
diff --git a/change/@react-native-windows-codegen-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json b/change/@react-native-windows-codegen-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json
new file mode 100644
index 00000000000..59206a25e87
--- /dev/null
+++ b/change/@react-native-windows-codegen-2e4144c5-b4d8-4b06-94d1-c239fbfcba18.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Add ability to customize native accessibility of custom native components",
+ "packageName": "@react-native-windows/codegen",
+ "email": "30809111+acoates-ms@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
\ No newline at end of file
diff --git a/change/react-native-windows-055a5eb5-8f2c-409e-a4b4-4ba382eeeae0.json b/change/react-native-windows-055a5eb5-8f2c-409e-a4b4-4ba382eeeae0.json
new file mode 100644
index 00000000000..58f6cda353a
--- /dev/null
+++ b/change/react-native-windows-055a5eb5-8f2c-409e-a4b4-4ba382eeeae0.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Add TSF support to TextInput",
+ "packageName": "react-native-windows",
+ "email": "30809111+acoates-ms@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
\ No newline at end of file
diff --git a/change/react-native-windows-0b14d2b4-ff2a-4e94-95f7-bc04edf1a1e4.json b/change/react-native-windows-0b14d2b4-ff2a-4e94-95f7-bc04edf1a1e4.json
new file mode 100644
index 00000000000..d049d2a1923
--- /dev/null
+++ b/change/react-native-windows-0b14d2b4-ff2a-4e94-95f7-bc04edf1a1e4.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "adding accessibility and UIA support for XAML fabric",
+ "packageName": "react-native-windows",
+ "email": "protikbiswas100@microsoft.com",
+ "dependentChangeType": "patch"
+}
\ No newline at end of file
diff --git a/change/react-native-windows-4a3033f2-c79a-4a04-859f-0c4647cf1b4d.json b/change/react-native-windows-4a3033f2-c79a-4a04-859f-0c4647cf1b4d.json
new file mode 100644
index 00000000000..ad992b9e57e
--- /dev/null
+++ b/change/react-native-windows-4a3033f2-c79a-4a04-859f-0c4647cf1b4d.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Fix DPI scaling for debugging overlay highlights",
+ "packageName": "react-native-windows",
+ "email": "74712637+iamAbhi-916@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
\ No newline at end of file
diff --git a/change/react-native-windows-4acbe4b2-89e0-4adb-b6b1-9e9bbf4a6220.json b/change/react-native-windows-4acbe4b2-89e0-4adb-b6b1-9e9bbf4a6220.json
new file mode 100644
index 00000000000..fbcbd5f7167
--- /dev/null
+++ b/change/react-native-windows-4acbe4b2-89e0-4adb-b6b1-9e9bbf4a6220.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Defer UIA accessibility provider initialization until requested",
+ "packageName": "react-native-windows",
+ "email": "198982749+Copilot@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
\ No newline at end of file
diff --git a/change/react-native-windows-5bfa5aa2-4028-4dd5-bb13-031b42470bd2.json b/change/react-native-windows-5bfa5aa2-4028-4dd5-bb13-031b42470bd2.json
new file mode 100644
index 00000000000..88785808361
--- /dev/null
+++ b/change/react-native-windows-5bfa5aa2-4028-4dd5-bb13-031b42470bd2.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "[Fabric] Fix UIA_LiveSettingPropertyId to use VT_I4 datatype instead of VT_BSTR",
+ "packageName": "react-native-windows",
+ "email": "ankudutt101@gmail.com",
+ "dependentChangeType": "patch"
+}
\ No newline at end of file
diff --git a/change/react-native-windows-8d23ba00-efa5-4506-b2cd-aff4b69cfa3f.json b/change/react-native-windows-8d23ba00-efa5-4506-b2cd-aff4b69cfa3f.json
new file mode 100644
index 00000000000..df62838c386
--- /dev/null
+++ b/change/react-native-windows-8d23ba00-efa5-4506-b2cd-aff4b69cfa3f.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Fix stackoverflow in StructInfo",
+ "packageName": "react-native-windows",
+ "email": "vmorozov@microsoft.com",
+ "dependentChangeType": "patch"
+}
\ No newline at end of file
diff --git a/change/react-native-windows-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json b/change/react-native-windows-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json
new file mode 100644
index 00000000000..63f1230472f
--- /dev/null
+++ b/change/react-native-windows-9c3b8d8a-fba5-44b2-928e-a8f920fc9b05.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Add ability to customize native accessibility of custom native components",
+ "packageName": "react-native-windows",
+ "email": "30809111+acoates-ms@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
\ No newline at end of file
diff --git a/change/react-native-windows-cc8b5ca3-c345-4091-87d2-b21198d72f2c.json b/change/react-native-windows-cc8b5ca3-c345-4091-87d2-b21198d72f2c.json
new file mode 100644
index 00000000000..159a080ba5e
--- /dev/null
+++ b/change/react-native-windows-cc8b5ca3-c345-4091-87d2-b21198d72f2c.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Tooltip positioned incorrectly on non 100% scale factor",
+ "packageName": "react-native-windows",
+ "email": "30809111+acoates-ms@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
\ No newline at end of file
diff --git a/change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json b/change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json
new file mode 100644
index 00000000000..30267e46d0e
--- /dev/null
+++ b/change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Pressables should take focus on press",
+ "packageName": "react-native-windows",
+ "email": "30809111+acoates-ms@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
\ No newline at end of file
diff --git a/change/react-native-windows-fdfd881c-5b04-4c05-ace6-fcca42dbd230.json b/change/react-native-windows-fdfd881c-5b04-4c05-ace6-fcca42dbd230.json
new file mode 100644
index 00000000000..c4fe7ef10b5
--- /dev/null
+++ b/change/react-native-windows-fdfd881c-5b04-4c05-ace6-fcca42dbd230.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Implements selectable for ",
+ "packageName": "react-native-windows",
+ "email": "74712637+iamAbhi-916@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
\ No newline at end of file
diff --git a/packages/@react-native-windows/automation-commands/src/dumpVisualTree.ts b/packages/@react-native-windows/automation-commands/src/dumpVisualTree.ts
index 00fa731102f..735d3c65a0b 100644
--- a/packages/@react-native-windows/automation-commands/src/dumpVisualTree.ts
+++ b/packages/@react-native-windows/automation-commands/src/dumpVisualTree.ts
@@ -37,6 +37,7 @@ export type AutomationNode = {
AutomationId?: string;
ControlType?: number;
LocalizedControlType?: string;
+ Name?: string;
__Children?: [AutomationNode];
};
diff --git a/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts b/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts
index 9a449abde03..b08d0e51f16 100644
--- a/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts
+++ b/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts
@@ -141,6 +141,12 @@ struct Base::_COMPONENT_NAME_:: {
winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept {
}
+ // CreateAutomationPeer will only be called if this method is overridden
+ virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/,
+ const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept {
+ return nullptr;
+ }
+
::_COMPONENT_VIEW_COMMAND_HANDLERS_::
::_COMPONENT_VIEW_COMMAND_HANDLER_::
@@ -222,6 +228,14 @@ void Register::_COMPONENT_NAME_::NativeComponent(
});
}
+ if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &Base::_COMPONENT_NAME_::::CreateAutomationPeer) {
+ builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
+ const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept {
+ auto userData = view.UserData().as();
+ return userData->CreateAutomationPeer(view, args);
+ });
+ }
+
compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
auto userData = winrt::make_self();
if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &Base::_COMPONENT_NAME_::::Initialize) {
diff --git a/packages/@react-native-windows/tester/src/js/examples-win/NativeComponents/CustomAccessibility.windows.js b/packages/@react-native-windows/tester/src/js/examples-win/NativeComponents/CustomAccessibility.windows.js
new file mode 100644
index 00000000000..591861b8fa6
--- /dev/null
+++ b/packages/@react-native-windows/tester/src/js/examples-win/NativeComponents/CustomAccessibility.windows.js
@@ -0,0 +1,33 @@
+'use strict';
+
+import React from 'react';
+import {View} from 'react-native';
+import {CustomAccessibility} from 'sample-custom-component';
+import RNTesterText from '../../components/RNTesterText';
+
+const CustomAccessibilityExample = () => {
+ return (
+
+ The below view should have custom accessibility
+
+
+ );
+}
+
+exports.displayName = 'CustomAccessibilityExample';
+exports.framework = 'React';
+exports.category = 'UI';
+exports.title = 'Custom Native Accessibility Example';
+exports.description =
+ 'Sample of a Custom Native Component overriding default accessibility';
+
+exports.examples = [
+ {
+ title: 'Custom Native Accessibility',
+ render: function (): React.Node {
+ return (
+
+ );
+ },
+ }
+];
diff --git a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js
index 4fb02a1a99b..a68931103ad 100644
--- a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js
+++ b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js
@@ -80,6 +80,10 @@ const Components: Array = [
key: 'Moving Light',
module: require('../examples-win/NativeComponents/MovingLight'),
},
+ {
+ key: 'Custom Native Accessibility',
+ module: require('../examples-win/NativeComponents/CustomAccessibility'),
+ },
{
key: 'Native Component',
module: require('../examples-win/NativeComponents/NativeComponent'),
diff --git a/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts b/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts
new file mode 100644
index 00000000000..31010001cae
--- /dev/null
+++ b/packages/e2e-test-app-fabric/test/CustomAccessibilityTest.test.ts
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT License.
+ *
+ * @format
+ */
+
+import {dumpVisualTree} from '@react-native-windows/automation-commands';
+import {goToComponentExample} from './RNTesterNavigation';
+import {app} from '@react-native-windows/automation';
+import {verifyNoErrorLogs} from './Helpers';
+
+beforeAll(async () => {
+ // If window is partially offscreen, tests will fail to click on certain elements
+ await app.setWindowPosition(0, 0);
+ await app.setWindowSize(1000, 1250);
+ await goToComponentExample('Custom Native Accessibility Example');
+});
+
+afterEach(async () => {
+ await verifyNoErrorLogs();
+});
+
+describe('Custom Accessibility Tests', () => {
+ test('Verify custom native component has UIA label from native', async () => {
+ const nativeComponent = await dumpVisualTree('custom-accessibility-1');
+
+ // Verify that the native component reports its accessiblity label from the native code
+ expect(nativeComponent['Automation Tree'].Name).toBe(
+ 'accessiblity label from native',
+ );
+
+ const dump = await dumpVisualTree('custom-accessibility-root-1');
+ expect(dump).toMatchSnapshot();
+ });
+});
diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/CustomAccessibilityTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/CustomAccessibilityTest.test.ts.snap
new file mode 100644
index 00000000000..136049c1614
--- /dev/null
+++ b/packages/e2e-test-app-fabric/test/__snapshots__/CustomAccessibilityTest.test.ts.snap
@@ -0,0 +1,84 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Custom Accessibility Tests Verify custom native component has UIA label from native 1`] = `
+{
+ "Automation Tree": {
+ "AutomationId": "custom-accessibility-root-1",
+ "ControlType": 50026,
+ "LocalizedControlType": "group",
+ "Name": "example root",
+ "__Children": [
+ {
+ "AutomationId": "",
+ "ControlType": 50020,
+ "LocalizedControlType": "text",
+ "Name": "The below view should have custom accessibility",
+ "TextRangePattern.GetText": "The below view should have custom accessibility",
+ },
+ {
+ "AutomationId": "custom-accessibility-1",
+ "ControlType": 50026,
+ "LocalizedControlType": "group",
+ "Name": "accessiblity label from native",
+ },
+ ],
+ },
+ "Component Tree": {
+ "Type": "Microsoft.ReactNative.Composition.ViewComponentView",
+ "_Props": {
+ "AccessibilityLabel": "example root",
+ "TestId": "custom-accessibility-root-1",
+ },
+ "__Children": [
+ {
+ "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView",
+ "_Props": {},
+ },
+ {
+ "Type": "Microsoft.ReactNative.Composition.ViewComponentView",
+ "_Props": {
+ "AccessibilityLabel": "accessibility should not show this, as native overrides it",
+ "TestId": "custom-accessibility-1",
+ },
+ },
+ ],
+ },
+ "Visual Tree": {
+ "Comment": "custom-accessibility-root-1",
+ "Offset": "0, 0, 0",
+ "Size": "998, 519",
+ "Visual Type": "SpriteVisual",
+ "__Children": [
+ {
+ "Offset": "0, 0, 0",
+ "Size": "998, 19",
+ "Visual Type": "SpriteVisual",
+ "__Children": [
+ {
+ "Offset": "0, 0, 0",
+ "Size": "998, 19",
+ "Visual Type": "SpriteVisual",
+ },
+ ],
+ },
+ {
+ "Offset": "0, 19, 0",
+ "Size": "500, 500",
+ "Visual Type": "SpriteVisual",
+ "__Children": [
+ {
+ "Brush": {
+ "Brush Type": "ColorBrush",
+ "Color": "rgba(0, 128, 0, 255)",
+ },
+ "Comment": "custom-accessibility-1",
+ "Offset": "0, 0, 0",
+ "Size": "500, 500",
+ "Visual Type": "SpriteVisual",
+ },
+ ],
+ },
+ ],
+ },
+}
+`;
diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap
index e768f28a56e..d637dcadfd6 100644
--- a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap
+++ b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap
@@ -1374,6 +1374,87 @@ exports[`Home UIA Tree Dump Crash 1`] = `
}
`;
+exports[`Home UIA Tree Dump Custom Native Accessibility Example 1`] = `
+{
+ "Automation Tree": {
+ "AutomationId": "Custom Native Accessibility Example",
+ "ControlType": 50026,
+ "IsKeyboardFocusable": true,
+ "LocalizedControlType": "group",
+ "Name": "Custom Native Accessibility Example Sample of a Custom Native Component overriding default accessibility",
+ "__Children": [
+ {
+ "AutomationId": "",
+ "ControlType": 50020,
+ "LocalizedControlType": "text",
+ "Name": "Custom Native Accessibility Example",
+ "TextRangePattern.GetText": "Custom Native Accessibility Example",
+ },
+ {
+ "AutomationId": "",
+ "ControlType": 50020,
+ "LocalizedControlType": "text",
+ "Name": "Sample of a Custom Native Component overriding default accessibility",
+ "TextRangePattern.GetText": "Sample of a Custom Native Component overriding default accessibility",
+ },
+ ],
+ },
+ "Component Tree": {
+ "Type": "Microsoft.ReactNative.Composition.ViewComponentView",
+ "_Props": {
+ "AccessibilityLabel": "Custom Native Accessibility Example Sample of a Custom Native Component overriding default accessibility",
+ "TestId": "Custom Native Accessibility Example",
+ },
+ "__Children": [
+ {
+ "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView",
+ "_Props": {},
+ },
+ {
+ "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView",
+ "_Props": {},
+ },
+ ],
+ },
+ "Visual Tree": {
+ "Brush": {
+ "Brush Type": "ColorBrush",
+ "Color": "rgba(255, 255, 255, 255)",
+ },
+ "Comment": "Custom Native Accessibility Example",
+ "Offset": "0, 0, 0",
+ "Size": "966, 78",
+ "Visual Type": "SpriteVisual",
+ "__Children": [
+ {
+ "Offset": "16, 16, 0",
+ "Size": "290, 25",
+ "Visual Type": "SpriteVisual",
+ "__Children": [
+ {
+ "Offset": "0, 0, 0",
+ "Size": "290, 25",
+ "Visual Type": "SpriteVisual",
+ },
+ ],
+ },
+ {
+ "Offset": "16, 45, 0",
+ "Size": "934, 17",
+ "Visual Type": "SpriteVisual",
+ "__Children": [
+ {
+ "Offset": "0, 0, 0",
+ "Size": "934, 17",
+ "Visual Type": "SpriteVisual",
+ },
+ ],
+ },
+ ],
+ },
+}
+`;
+
exports[`Home UIA Tree Dump Cxx TurboModule 1`] = `
{
"Automation Tree": {
@@ -1995,12 +2076,12 @@ exports[`Home UIA Tree Dump Fabric Native Component Yoga 1`] = `
"__Children": [
{
"Offset": "16, 16, 0",
- "Size": "246, 25",
+ "Size": "246, 24",
"Visual Type": "SpriteVisual",
"__Children": [
{
"Offset": "0, 0, 0",
- "Size": "246, 25",
+ "Size": "246, 24",
"Visual Type": "SpriteVisual",
},
],
@@ -2076,12 +2157,12 @@ exports[`Home UIA Tree Dump Fast Path Texts 1`] = `
"__Children": [
{
"Offset": "16, 16, 0",
- "Size": "115, 24",
+ "Size": "115, 25",
"Visual Type": "SpriteVisual",
"__Children": [
{
"Offset": "0, 0, 0",
- "Size": "115, 24",
+ "Size": "115, 25",
"Visual Type": "SpriteVisual",
},
],
@@ -2476,7 +2557,7 @@ exports[`Home UIA Tree Dump Image 1`] = `
},
"Comment": "Image",
"Offset": "0, 0, 0",
- "Size": "966, 78",
+ "Size": "966, 77",
"Visual Type": "SpriteVisual",
"__Children": [
{
@@ -2800,7 +2881,7 @@ exports[`Home UIA Tree Dump Keyboard extension Example 1`] = `
},
"Comment": "Keyboard extension Example",
"Offset": "0, 0, 0",
- "Size": "966, 77",
+ "Size": "966, 78",
"Visual Type": "SpriteVisual",
"__Children": [
{
@@ -3384,12 +3465,12 @@ exports[`Home UIA Tree Dump LegacySelectableTextTest 1`] = `
},
{
"Offset": "16, 45, 0",
- "Size": "934, 17",
+ "Size": "934, 16",
"Visual Type": "SpriteVisual",
"__Children": [
{
"Offset": "0, 0, 0",
- "Size": "934, 17",
+ "Size": "934, 16",
"Visual Type": "SpriteVisual",
},
],
@@ -3465,12 +3546,12 @@ exports[`Home UIA Tree Dump LegacyTextHitTestTest 1`] = `
},
{
"Offset": "16, 45, 0",
- "Size": "934, 16",
+ "Size": "934, 17",
"Visual Type": "SpriteVisual",
"__Children": [
{
"Offset": "0, 0, 0",
- "Size": "934, 16",
+ "Size": "934, 17",
"Visual Type": "SpriteVisual",
},
],
@@ -4096,7 +4177,7 @@ exports[`Home UIA Tree Dump New App Screen 1`] = `
},
"Comment": "New App Screen",
"Offset": "0, 0, 0",
- "Size": "966, 78",
+ "Size": "966, 77",
"Visual Type": "SpriteVisual",
"__Children": [
{
@@ -4258,7 +4339,7 @@ exports[`Home UIA Tree Dump Performance Comparison Examples 1`] = `
},
"Comment": "Performance Comparison Examples",
"Offset": "0, 0, 0",
- "Size": "966, 77",
+ "Size": "966, 78",
"Visual Type": "SpriteVisual",
"__Children": [
{
@@ -4923,12 +5004,12 @@ exports[`Home UIA Tree Dump ScrollViewAnimated 1`] = `
},
{
"Offset": "16, 45, 0",
- "Size": "934, 17",
+ "Size": "934, 16",
"Visual Type": "SpriteVisual",
"__Children": [
{
"Offset": "0, 0, 0",
- "Size": "934, 17",
+ "Size": "934, 16",
"Visual Type": "SpriteVisual",
},
],
@@ -5004,12 +5085,12 @@ exports[`Home UIA Tree Dump ScrollViewSimpleExample 1`] = `
},
{
"Offset": "16, 45, 0",
- "Size": "934, 16",
+ "Size": "934, 17",
"Visual Type": "SpriteVisual",
"__Children": [
{
"Offset": "0, 0, 0",
- "Size": "934, 16",
+ "Size": "934, 17",
"Visual Type": "SpriteVisual",
},
],
@@ -5583,7 +5664,7 @@ exports[`Home UIA Tree Dump TextInput 1`] = `
},
"Comment": "TextInput",
"Offset": "0, 0, 0",
- "Size": "966, 78",
+ "Size": "966, 77",
"Visual Type": "SpriteVisual",
"__Children": [
{
@@ -5664,7 +5745,7 @@ exports[`Home UIA Tree Dump TextInputs with key prop 1`] = `
},
"Comment": "TextInputs with key prop",
"Offset": "0, 0, 0",
- "Size": "966, 77",
+ "Size": "966, 78",
"Visual Type": "SpriteVisual",
"__Children": [
{
@@ -6317,12 +6398,12 @@ exports[`Home UIA Tree Dump View 1`] = `
"__Children": [
{
"Offset": "16, 16, 0",
- "Size": "38, 25",
+ "Size": "38, 24",
"Visual Type": "SpriteVisual",
"__Children": [
{
"Offset": "0, 0, 0",
- "Size": "38, 25",
+ "Size": "38, 24",
"Visual Type": "SpriteVisual",
},
],
@@ -6479,12 +6560,12 @@ exports[`Home UIA Tree Dump XAML 1`] = `
"__Children": [
{
"Offset": "16, 16, 0",
- "Size": "47, 24",
+ "Size": "47, 25",
"Visual Type": "SpriteVisual",
"__Children": [
{
"Offset": "0, 0, 0",
- "Size": "47, 24",
+ "Size": "47, 25",
"Visual Type": "SpriteVisual",
},
],
diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap
index 233307c0d84..5124f964cad 100644
--- a/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap
+++ b/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap
@@ -846,6 +846,13 @@ exports[`Pressable Tests Pressables can have event handlers, hover and click 2`]
"Name": "pressOut",
"TextRangePattern.GetText": "pressOut",
},
+ {
+ "AutomationId": "",
+ "ControlType": 50020,
+ "LocalizedControlType": "text",
+ "Name": "focus",
+ "TextRangePattern.GetText": "focus",
+ },
{
"AutomationId": "",
"ControlType": 50020,
@@ -891,6 +898,10 @@ exports[`Pressable Tests Pressables can have event handlers, hover and click 2`]
"Type": "Microsoft.ReactNative.Composition.ParagraphComponentView",
"_Props": {},
},
+ {
+ "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView",
+ "_Props": {},
+ },
],
},
"Visual Tree": {
@@ -991,6 +1002,18 @@ exports[`Pressable Tests Pressables can have event handlers, hover and click 2`]
},
],
},
+ {
+ "Offset": "11, 85, 0",
+ "Size": "874, 20",
+ "Visual Type": "SpriteVisual",
+ "__Children": [
+ {
+ "Offset": "0, 0, 0",
+ "Size": "874, 20",
+ "Visual Type": "SpriteVisual",
+ },
+ ],
+ },
],
},
}
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 accc9f765a2..d7aada68d93 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
@@ -10923,6 +10923,39 @@ exports[`snapshotAllPages Crash 1`] = `
`;
+exports[`snapshotAllPages Custom Native Accessibility Example 1`] = `
+
+
+ The below view should have custom accessibility
+
+
+
+`;
+
exports[`snapshotAllPages DevSettings 1`] = `
#include "winrt/AutomationChannel.h"
+// Includes from sample-custom-component
+#include
+
#include "AutolinkedNativeModules.g.h"
#include "NativeModules.h"
@@ -75,6 +78,7 @@ winrt::Microsoft::ReactNative::ReactNativeHost CreateReactNativeHost(
RegisterAutolinkedNativeModulePackages(host.PackageProviders());
host.PackageProviders().Append(winrt::make());
+ host.PackageProviders().Append(winrt::SampleCustomComponent::ReactPackageProvider());
#if BUNDLE
host.InstanceSettings().JavaScriptBundleFile(L"index.windows");
diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.vcxproj b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.vcxproj
index 57bf048a361..3971a57c32f 100644
--- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.vcxproj
+++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.vcxproj
@@ -128,6 +128,11 @@
+
+
+ {a8da218c-4cb5-48cb-a9ee-9e6337165d07}
+
+
This project references targets in your node_modules\react-native-windows folder. The missing file is {0}.
diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json
index 43da89c8432..9a384fa5f18 100644
--- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json
+++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json
@@ -108,6 +108,15 @@
"Folly": "[1.0.0, )",
"boost": "[1.83.0, )"
}
+ },
+ "samplecustomcomponent": {
+ "type": "Project",
+ "dependencies": {
+ "Microsoft.ReactNative": "[1.0.0, )",
+ "Microsoft.VCRTForwarders.140": "[1.0.2-rc, )",
+ "Microsoft.WindowsAppSDK": "[1.7.250401001, )",
+ "boost": "[1.83.0, )"
+ }
}
},
"native,Version=v0.0/win": {
diff --git a/packages/e2e-test-app/test/visitAllPages.test.ts b/packages/e2e-test-app/test/visitAllPages.test.ts
index 8e92597bfe2..baa4787e434 100644
--- a/packages/e2e-test-app/test/visitAllPages.test.ts
+++ b/packages/e2e-test-app/test/visitAllPages.test.ts
@@ -35,6 +35,7 @@ const componentExamples = testerList.Components.map(e => e.module.title);
describe('visitAllPages', () => {
for (const component of componentExamples) {
if (
+ component === 'Custom Native Accessibility Example' ||
component === 'Moving Light Example' ||
component === 'Drawing Island Example' ||
component === 'Fabric Native Component' ||
diff --git a/packages/playground/Samples/text.tsx b/packages/playground/Samples/text.tsx
index a3a20d59fab..e5ce13a5090 100644
--- a/packages/playground/Samples/text.tsx
+++ b/packages/playground/Samples/text.tsx
@@ -20,6 +20,23 @@ export default class Bootstrap extends React.Component {
selectable={true}>
Click here : This is a text with a tooltip.
+
+
+ Text Selection Test
+
+ This text is SELECTABLE. Try clicking and dragging to select it.
+
+
+ Hello 世界世界 World - Double-click to test CJK word selection!
+
+
+ This text is NOT selectable (selectable=false).
+
+
+ This text has no selectable prop (default behavior).
+
+
+
Bootstrap);
diff --git a/packages/sample-custom-component/src/CustomAccessibilityNativeComponent.ts b/packages/sample-custom-component/src/CustomAccessibilityNativeComponent.ts
new file mode 100644
index 00000000000..bfb1595e54f
--- /dev/null
+++ b/packages/sample-custom-component/src/CustomAccessibilityNativeComponent.ts
@@ -0,0 +1,7 @@
+import { codegenNativeComponent } from 'react-native';
+import type { ViewProps } from 'react-native';
+
+export interface CustomAccessibilityProps extends ViewProps {
+}
+
+export default codegenNativeComponent('CustomAccessibility');
diff --git a/packages/sample-custom-component/src/index.ts b/packages/sample-custom-component/src/index.ts
index 182564080ea..49b2bd07d43 100644
--- a/packages/sample-custom-component/src/index.ts
+++ b/packages/sample-custom-component/src/index.ts
@@ -1,11 +1,14 @@
import MovingLight from './MovingLight';
-import type {MovingLightHandle} from './MovingLight';
+import type { MovingLightHandle } from './MovingLight';
import DrawingIsland from './DrawingIsland';
import CalendarView from './FabricXamlCalendarViewNativeComponent'
+import CustomAccessibility from './CustomAccessibilityNativeComponent';
+
export {
+ CustomAccessibility,
DrawingIsland,
MovingLight,
MovingLightHandle,
diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.cpp
new file mode 100644
index 00000000000..3c8d96c1773
--- /dev/null
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.cpp
@@ -0,0 +1,123 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+
+#include "codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h"
+
+#ifdef RNW_NEW_ARCH
+#include
+#include
+#include
+#include
+#include
+
+namespace winrt::SampleCustomComponent {
+
+struct CustomAccessibilityAutomationPeer : public winrt::implements<
+ CustomAccessibilityAutomationPeer,
+ winrt::IInspectable,
+ IRawElementProviderFragment,
+ IRawElementProviderSimple> {
+ CustomAccessibilityAutomationPeer(const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs &args)
+ : m_inner(args.DefaultAutomationPeer()) {}
+
+ virtual HRESULT __stdcall Navigate(NavigateDirection direction, IRawElementProviderFragment **pRetVal) override {
+ winrt::com_ptr innerAsREPF = m_inner.try_as();
+ if (!innerAsREPF)
+ return E_FAIL;
+ return innerAsREPF->Navigate(direction, pRetVal);
+ }
+
+ virtual HRESULT __stdcall GetRuntimeId(SAFEARRAY **pRetVal) override {
+ winrt::com_ptr innerAsREPF = m_inner.try_as();
+ if (!innerAsREPF)
+ return E_FAIL;
+ return innerAsREPF->GetRuntimeId(pRetVal);
+ }
+
+ virtual HRESULT __stdcall get_BoundingRectangle(UiaRect *pRetVal) override {
+ winrt::com_ptr innerAsREPF = m_inner.try_as();
+ if (!innerAsREPF)
+ return E_FAIL;
+ return innerAsREPF->get_BoundingRectangle(pRetVal);
+ }
+
+ virtual HRESULT __stdcall GetEmbeddedFragmentRoots(SAFEARRAY **pRetVal) override {
+ winrt::com_ptr innerAsREPF = m_inner.try_as();
+ if (!innerAsREPF)
+ return E_FAIL;
+ return innerAsREPF->GetEmbeddedFragmentRoots(pRetVal);
+ }
+
+ virtual HRESULT __stdcall SetFocus(void) override {
+ winrt::com_ptr innerAsREPF = m_inner.try_as();
+ if (!innerAsREPF)
+ return E_FAIL;
+ return innerAsREPF->SetFocus();
+ }
+
+ virtual HRESULT __stdcall get_FragmentRoot(IRawElementProviderFragmentRoot **pRetVal) override {
+ winrt::com_ptr innerAsREPF = m_inner.try_as();
+ if (!innerAsREPF)
+ return E_FAIL;
+ return innerAsREPF->get_FragmentRoot(pRetVal);
+ }
+
+ // inherited via IRawElementProviderSimple
+ virtual HRESULT __stdcall get_ProviderOptions(ProviderOptions *pRetVal) override {
+ winrt::com_ptr innerAsREPS = m_inner.try_as();
+ if (!innerAsREPS)
+ return E_FAIL;
+ return innerAsREPS->get_ProviderOptions(pRetVal);
+ }
+
+ virtual HRESULT __stdcall GetPatternProvider(PATTERNID patternId, IUnknown **pRetVal) override {
+ winrt::com_ptr innerAsREPS = m_inner.try_as();
+ if (!innerAsREPS)
+ return E_FAIL;
+ return innerAsREPS->GetPatternProvider(patternId, pRetVal);
+ }
+
+ virtual HRESULT __stdcall GetPropertyValue(PROPERTYID propertyId, VARIANT *pRetVal) override {
+ winrt::com_ptr innerAsREPS = m_inner.try_as();
+ if (!innerAsREPS)
+ return E_FAIL;
+
+ if (propertyId == UIA_NamePropertyId) {
+ pRetVal->vt = VT_BSTR;
+ pRetVal->bstrVal = SysAllocString(L"accessiblity label from native");
+ return pRetVal->bstrVal != nullptr ? S_OK : E_OUTOFMEMORY;
+ }
+
+ return innerAsREPS->GetPropertyValue(propertyId, pRetVal);
+ }
+
+ virtual HRESULT __stdcall get_HostRawElementProvider(IRawElementProviderSimple **pRetVal) override {
+ winrt::com_ptr innerAsREPS = m_inner.try_as();
+ if (!innerAsREPS)
+ return E_FAIL;
+ return innerAsREPS->get_HostRawElementProvider(pRetVal);
+ }
+
+ private:
+ winrt::Windows::Foundation::IInspectable m_inner;
+};
+
+struct CustomAccessibility : public winrt::implements,
+ Codegen::BaseCustomAccessibility {
+ virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(
+ const winrt::Microsoft::ReactNative::ComponentView & /*view*/,
+ const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs &args) noexcept override {
+ return winrt::make(args);
+ }
+};
+
+} // namespace winrt::SampleCustomComponent
+
+void RegisterCustomAccessibilityComponentView(
+ winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) noexcept {
+ winrt::SampleCustomComponent::Codegen::RegisterCustomAccessibilityNativeComponent<
+ winrt::SampleCustomComponent::CustomAccessibility>(packageBuilder, {});
+}
+
+#endif // #ifdef RNW_NEW_ARCH
diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.h b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.h
new file mode 100644
index 00000000000..99c15405141
--- /dev/null
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/CustomAccessibility.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#if defined(RNW_NEW_ARCH)
+
+void RegisterCustomAccessibilityComponentView(
+ winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder);
+
+#endif // defined(RNW_NEW_ARCH)
diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp
index ba3a413f5a6..29f8c8905e1 100644
--- a/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/ReactPackageProvider.cpp
@@ -8,6 +8,7 @@
#endif
#include "CalendarView.h"
+#include "CustomAccessibility.h"
#include "DrawingIsland.h"
#include "MovingLight.h"
@@ -22,6 +23,7 @@ void ReactPackageProvider::CreatePackage(IReactPackageBuilder const &packageBuil
RegisterDrawingIslandComponentView(packageBuilder);
RegisterMovingLightNativeComponent(packageBuilder);
RegisterCalendarViewComponentView(packageBuilder);
+ RegisterCustomAccessibilityComponentView(packageBuilder);
#endif // #ifdef RNW_NEW_ARCH
}
diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj b/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj
index a86fa2cd6dd..af442f14fab 100644
--- a/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/SampleCustomComponent.vcxproj
@@ -103,6 +103,7 @@
+
DrawingIsland.idl
@@ -115,6 +116,7 @@
+
Create
diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CalendarView.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CalendarView.g.h
index 7df163dc929..2cb98aa9acc 100644
--- a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CalendarView.g.h
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CalendarView.g.h
@@ -115,6 +115,12 @@ struct BaseCalendarView {
winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept {
}
+ // CreateAutomationPeer will only be called if this method is overridden
+ virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/,
+ const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept {
+ return nullptr;
+ }
+
const std::shared_ptr& EventEmitter() const { return m_eventEmitter; }
@@ -190,6 +196,14 @@ void RegisterCalendarViewNativeComponent(
});
}
+ if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseCalendarView::CreateAutomationPeer) {
+ builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
+ const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept {
+ auto userData = view.UserData().as();
+ return userData->CreateAutomationPeer(view, args);
+ });
+ }
+
compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
auto userData = winrt::make_self();
if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseCalendarView::Initialize) {
diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h
new file mode 100644
index 00000000000..d372f4e7d32
--- /dev/null
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h
@@ -0,0 +1,211 @@
+
+/*
+ * This file is auto-generated from CustomAccessibilityNativeComponent spec file in flow / TypeScript.
+ */
+// clang-format off
+#pragma once
+
+#include
+
+#ifdef RNW_NEW_ARCH
+#include
+
+#include
+#include
+#endif // #ifdef RNW_NEW_ARCH
+
+#ifdef RNW_NEW_ARCH
+
+namespace winrt::SampleCustomComponent::Codegen {
+
+REACT_STRUCT(CustomAccessibilityProps)
+struct CustomAccessibilityProps : winrt::implements {
+ CustomAccessibilityProps(winrt::Microsoft::ReactNative::ViewProps props, const winrt::Microsoft::ReactNative::IComponentProps& cloneFrom)
+ : ViewProps(props)
+ {
+ if (cloneFrom) {
+ auto cloneFromProps = cloneFrom.as();
+
+ }
+ }
+
+ void SetProp(uint32_t hash, winrt::hstring propName, winrt::Microsoft::ReactNative::IJSValueReader value) noexcept {
+ winrt::Microsoft::ReactNative::ReadProp(hash, propName, value, *this);
+ }
+
+ const winrt::Microsoft::ReactNative::ViewProps ViewProps;
+};
+
+struct CustomAccessibilityEventEmitter {
+ CustomAccessibilityEventEmitter(const winrt::Microsoft::ReactNative::EventEmitter &eventEmitter)
+ : m_eventEmitter(eventEmitter) {}
+
+ private:
+ winrt::Microsoft::ReactNative::EventEmitter m_eventEmitter{nullptr};
+};
+
+template
+struct BaseCustomAccessibility {
+
+ virtual void UpdateProps(
+ const winrt::Microsoft::ReactNative::ComponentView &/*view*/,
+ const winrt::com_ptr &newProps,
+ const winrt::com_ptr &/*oldProps*/) noexcept {
+ m_props = newProps;
+ }
+
+ // UpdateLayoutMetrics will only be called if this method is overridden
+ virtual void UpdateLayoutMetrics(
+ const winrt::Microsoft::ReactNative::ComponentView &/*view*/,
+ const winrt::Microsoft::ReactNative::LayoutMetrics &/*newLayoutMetrics*/,
+ const winrt::Microsoft::ReactNative::LayoutMetrics &/*oldLayoutMetrics*/) noexcept {
+ }
+
+ // UpdateState will only be called if this method is overridden
+ virtual void UpdateState(
+ const winrt::Microsoft::ReactNative::ComponentView &/*view*/,
+ const winrt::Microsoft::ReactNative::IComponentState &/*newState*/) noexcept {
+ }
+
+ virtual void UpdateEventEmitter(const std::shared_ptr &eventEmitter) noexcept {
+ m_eventEmitter = eventEmitter;
+ }
+
+ // MountChildComponentView will only be called if this method is overridden
+ virtual void MountChildComponentView(const winrt::Microsoft::ReactNative::ComponentView &/*view*/,
+ const winrt::Microsoft::ReactNative::MountChildComponentViewArgs &/*args*/) noexcept {
+ }
+
+ // UnmountChildComponentView will only be called if this method is overridden
+ virtual void UnmountChildComponentView(const winrt::Microsoft::ReactNative::ComponentView &/*view*/,
+ const winrt::Microsoft::ReactNative::UnmountChildComponentViewArgs &/*args*/) noexcept {
+ }
+
+ // Initialize will only be called if this method is overridden
+ virtual void Initialize(const winrt::Microsoft::ReactNative::ComponentView &/*view*/) noexcept {
+ }
+
+ // CreateVisual will only be called if this method is overridden
+ virtual winrt::Microsoft::UI::Composition::Visual CreateVisual(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
+ return view.as().Compositor().CreateSpriteVisual();
+ }
+
+ // FinalizeUpdate will only be called if this method is overridden
+ virtual void FinalizeUpdate(const winrt::Microsoft::ReactNative::ComponentView &/*view*/,
+ winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept {
+ }
+
+ // CreateAutomationPeer will only be called if this method is overridden
+ virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/,
+ const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept {
+ return nullptr;
+ }
+
+
+
+ const std::shared_ptr& EventEmitter() const { return m_eventEmitter; }
+ const winrt::com_ptr& Props() const { return m_props; }
+
+private:
+ winrt::com_ptr m_props;
+ std::shared_ptr m_eventEmitter;
+};
+
+template
+void RegisterCustomAccessibilityNativeComponent(
+ winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder,
+ std::function builderCallback) noexcept {
+ packageBuilder.as().AddViewComponent(
+ L"CustomAccessibility", [builderCallback](winrt::Microsoft::ReactNative::IReactViewComponentBuilder const &builder) noexcept {
+ auto compBuilder = builder.as();
+
+ builder.SetCreateProps([](winrt::Microsoft::ReactNative::ViewProps props,
+ const winrt::Microsoft::ReactNative::IComponentProps& cloneFrom) noexcept {
+ return winrt::make(props, cloneFrom);
+ });
+
+ builder.SetUpdatePropsHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
+ const winrt::Microsoft::ReactNative::IComponentProps &newProps,
+ const winrt::Microsoft::ReactNative::IComponentProps &oldProps) noexcept {
+ auto userData = view.UserData().as();
+ userData->UpdateProps(view, newProps ? newProps.as() : nullptr, oldProps ? oldProps.as() : nullptr);
+ });
+
+ compBuilder.SetUpdateLayoutMetricsHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
+ const winrt::Microsoft::ReactNative::LayoutMetrics &newLayoutMetrics,
+ const winrt::Microsoft::ReactNative::LayoutMetrics &oldLayoutMetrics) noexcept {
+ auto userData = view.UserData().as();
+ userData->UpdateLayoutMetrics(view, newLayoutMetrics, oldLayoutMetrics);
+ });
+
+ builder.SetUpdateEventEmitterHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
+ const winrt::Microsoft::ReactNative::EventEmitter &eventEmitter) noexcept {
+ auto userData = view.UserData().as();
+ userData->UpdateEventEmitter(std::make_shared(eventEmitter));
+ });
+
+ if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::FinalizeUpdate != &BaseCustomAccessibility::FinalizeUpdate) {
+ builder.SetFinalizeUpdateHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
+ winrt::Microsoft::ReactNative::ComponentViewUpdateMask mask) noexcept {
+ auto userData = view.UserData().as();
+ userData->FinalizeUpdate(view, mask);
+ });
+ }
+
+ if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::UpdateState != &BaseCustomAccessibility::UpdateState) {
+ builder.SetUpdateStateHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
+ const winrt::Microsoft::ReactNative::IComponentState &newState) noexcept {
+ auto userData = view.UserData().as();
+ userData->UpdateState(view, newState);
+ });
+ }
+
+ if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::MountChildComponentView != &BaseCustomAccessibility::MountChildComponentView) {
+ builder.SetMountChildComponentViewHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
+ const winrt::Microsoft::ReactNative::MountChildComponentViewArgs &args) noexcept {
+ auto userData = view.UserData().as();
+ return userData->MountChildComponentView(view, args);
+ });
+ }
+
+ if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::UnmountChildComponentView != &BaseCustomAccessibility::UnmountChildComponentView) {
+ builder.SetUnmountChildComponentViewHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
+ const winrt::Microsoft::ReactNative::UnmountChildComponentViewArgs &args) noexcept {
+ auto userData = view.UserData().as();
+ return userData->UnmountChildComponentView(view, args);
+ });
+ }
+
+ if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseCustomAccessibility::CreateAutomationPeer) {
+ builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
+ const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept {
+ auto userData = view.UserData().as();
+ return userData->CreateAutomationPeer(view, args);
+ });
+ }
+
+ compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
+ auto userData = winrt::make_self();
+ if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseCustomAccessibility::Initialize) {
+ userData->Initialize(view);
+ }
+ view.UserData(*userData);
+ });
+
+ if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateVisual != &BaseCustomAccessibility::CreateVisual) {
+ compBuilder.SetCreateVisualHandler([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
+ auto userData = view.UserData().as();
+ return userData->CreateVisual(view);
+ });
+ }
+
+ // Allow app to further customize the builder
+ if (builderCallback) {
+ builderCallback(compBuilder);
+ }
+ });
+}
+
+} // namespace winrt::SampleCustomComponent::Codegen
+
+#endif // #ifdef RNW_NEW_ARCH
diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/DrawingIsland.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/DrawingIsland.g.h
index 32f9101b02e..acb9244cef6 100644
--- a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/DrawingIsland.g.h
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/DrawingIsland.g.h
@@ -95,6 +95,12 @@ struct BaseDrawingIsland {
winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept {
}
+ // CreateAutomationPeer will only be called if this method is overridden
+ virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/,
+ const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept {
+ return nullptr;
+ }
+
const std::shared_ptr& EventEmitter() const { return m_eventEmitter; }
@@ -170,6 +176,14 @@ void RegisterDrawingIslandNativeComponent(
});
}
+ if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseDrawingIsland::CreateAutomationPeer) {
+ builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
+ const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept {
+ auto userData = view.UserData().as();
+ return userData->CreateAutomationPeer(view, args);
+ });
+ }
+
compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
auto userData = winrt::make_self();
if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseDrawingIsland::Initialize) {
diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/MovingLight.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/MovingLight.g.h
index f7eb7736288..4fbfa7f0d99 100644
--- a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/MovingLight.g.h
+++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/MovingLight.g.h
@@ -136,6 +136,12 @@ struct BaseMovingLight {
winrt::Microsoft::ReactNative::ComponentViewUpdateMask /*mask*/) noexcept {
}
+ // CreateAutomationPeer will only be called if this method is overridden
+ virtual winrt::Windows::Foundation::IInspectable CreateAutomationPeer(const winrt::Microsoft::ReactNative::ComponentView & /*view*/,
+ const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& /*args*/) noexcept {
+ return nullptr;
+ }
+
// You must provide an implementation of this method to handle the "setLightOn" command
virtual void HandleSetLightOnCommand(bool value) noexcept = 0;
@@ -229,6 +235,14 @@ void RegisterMovingLightNativeComponent(
});
}
+ if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::CreateAutomationPeer != &BaseMovingLight::CreateAutomationPeer) {
+ builder.SetCreateAutomationPeerHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
+ const winrt::Microsoft::ReactNative::CreateAutomationPeerArgs& args) noexcept {
+ auto userData = view.UserData().as();
+ return userData->CreateAutomationPeer(view, args);
+ });
+ }
+
compBuilder.SetViewComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
auto userData = winrt::make_self();
if CONSTEXPR_SUPPORTED_ON_VIRTUAL_FN_ADDRESS (&TUserData::Initialize != &BaseMovingLight::Initialize) {
diff --git a/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj b/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj
index b8ea2a82103..b3c58d57a35 100644
--- a/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj
+++ b/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj
@@ -107,6 +107,7 @@
Version.lib;
Dwmapi.lib;
WindowsApp_downlevel.lib;
+ icu.lib;
%(AdditionalDependencies)
diff --git a/vnext/Microsoft.ReactNative.Cxx/StructInfo.h b/vnext/Microsoft.ReactNative.Cxx/StructInfo.h
index c92d2d40cfe..9594f13c72c 100644
--- a/vnext/Microsoft.ReactNative.Cxx/StructInfo.h
+++ b/vnext/Microsoft.ReactNative.Cxx/StructInfo.h
@@ -81,7 +81,7 @@ struct FieldInfo {
FieldInfo(TValue TClass::*fieldPtr) noexcept
: m_fieldReader{FieldReader},
m_fieldWriter{FieldWriter},
- m_fieldPtrStore{*reinterpret_cast(&fieldPtr)} {
+ m_fieldPtrStore{StoreFieldPtr(fieldPtr)} {
static_assert(sizeof(m_fieldPtrStore) >= sizeof(fieldPtr));
}
@@ -94,6 +94,13 @@ struct FieldInfo {
}
private:
+ template
+ static uintptr_t StoreFieldPtr(TValue TClass::*fieldPtr) noexcept {
+ uintptr_t result{};
+ std::memcpy(&result, &fieldPtr, sizeof(fieldPtr));
+ return result;
+ }
+
FieldReaderType m_fieldReader;
FieldWriterType m_fieldWriter;
const uintptr_t m_fieldPtrStore;
diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp
index b1d0593b3c9..6c2aee7ba13 100644
--- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp
@@ -8,6 +8,7 @@
#include "DynamicReader.h"
#include "ComponentView.g.cpp"
+#include "CreateAutomationPeerArgs.g.h"
#include "LayoutMetricsChangedArgs.g.cpp"
#include "MountChildComponentViewArgs.g.cpp"
#include "UnmountChildComponentViewArgs.g.cpp"
@@ -641,7 +642,32 @@ facebook::react::Tag ComponentView::hitTest(
return -1;
}
+struct CreateAutomationPeerArgs
+ : public winrt::Microsoft::ReactNative::implementation::CreateAutomationPeerArgsT {
+ CreateAutomationPeerArgs(winrt::Windows::Foundation::IInspectable defaultAutomationPeer)
+ : m_defaultAutomationPeer(defaultAutomationPeer) {}
+
+ winrt::Windows::Foundation::IInspectable DefaultAutomationPeer() const noexcept {
+ return m_defaultAutomationPeer;
+ }
+
+ private:
+ winrt::Windows::Foundation::IInspectable m_defaultAutomationPeer;
+};
+
winrt::IInspectable ComponentView::EnsureUiaProvider() noexcept {
+ if (m_uiaProvider == nullptr) {
+ if (m_builder && m_builder->CreateAutomationPeerHandler()) {
+ m_uiaProvider = m_builder->CreateAutomationPeerHandler()(
+ *this, winrt::make(CreateAutomationProvider()));
+ } else {
+ m_uiaProvider = CreateAutomationProvider();
+ }
+ }
+ return m_uiaProvider;
+}
+
+winrt::Windows::Foundation::IInspectable ComponentView::CreateAutomationProvider() noexcept {
return nullptr;
}
diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h
index 66a1de581fd..7700ac9e3fa 100644
--- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h
@@ -208,6 +208,7 @@ struct ComponentView
virtual facebook::react::Tag
hitTest(facebook::react::Point pt, facebook::react::Point &localPt, bool ignorePointerEvents = false) const noexcept;
virtual winrt::Windows::Foundation::IInspectable EnsureUiaProvider() noexcept;
+ virtual winrt::Windows::Foundation::IInspectable CreateAutomationProvider() noexcept;
virtual std::optional getAccessiblityValue() noexcept;
virtual void setAcccessiblityValue(std::string &&value) noexcept;
virtual bool getAcccessiblityIsReadOnly() noexcept;
@@ -265,6 +266,7 @@ struct ComponentView
facebook::react::LayoutMetrics m_layoutMetrics;
winrt::Windows::Foundation::Collections::IVector m_children{
winrt::single_threaded_vector()};
+ winrt::Windows::Foundation::IInspectable m_uiaProvider{nullptr};
winrt::event<
winrt::Windows::Foundation::EventHandler>
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ActivityIndicatorComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ActivityIndicatorComponentView.cpp
index 7cd4a65d052..8d98ef9840d 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ActivityIndicatorComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ActivityIndicatorComponentView.cpp
@@ -4,7 +4,6 @@
#pragma once
#include "ActivityIndicatorComponentView.h"
-#include "CompositionDynamicAutomationProvider.h"
#include
#include
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.cpp
index 9e4a2963678..859973dd75f 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.cpp
@@ -8,11 +8,8 @@
namespace winrt::Microsoft::ReactNative::implementation {
CompositionAnnotationProvider::CompositionAnnotationProvider(
- const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView,
- CompositionDynamicAutomationProvider *parentProvider) noexcept
- : m_view{componentView} {
- m_parentProvider.copy_from(parentProvider);
-}
+ const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView) noexcept
+ : m_view{componentView} {}
HRESULT __stdcall CompositionAnnotationProvider::get_AnnotationTypeId(int *retVal) {
if (retVal == nullptr)
return E_POINTER;
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.h
index d82af128808..4b682f59646 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.h
@@ -1,6 +1,5 @@
#pragma once
-#include
#include
#include
#include
@@ -12,8 +11,7 @@ namespace winrt::Microsoft::ReactNative::implementation {
class CompositionAnnotationProvider : public winrt::implements {
public:
CompositionAnnotationProvider(
- const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView,
- CompositionDynamicAutomationProvider *parentProvider) noexcept;
+ const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView) noexcept;
// inherited via IAnnotationProvider
virtual HRESULT __stdcall get_AnnotationTypeId(int *retVal) override;
@@ -25,7 +23,6 @@ class CompositionAnnotationProvider : public winrt::implements m_annotationProvider;
- winrt::com_ptr m_parentProvider;
};
} // namespace winrt::Microsoft::ReactNative::implementation
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp
index 6ce1074a63c..30fbc50895b 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp
@@ -2,6 +2,7 @@
#include "CompositionDynamicAutomationProvider.h"
#include
#include
+#include
#include
#include
#include
@@ -45,19 +46,6 @@ CompositionDynamicAutomationProvider::CompositionDynamicAutomationProvider(
if (props->accessibilityState.has_value() && props->accessibilityState->selected.has_value()) {
AddSelectionItemsToContainer(this);
}
-
- if (strongView.try_as() ||
- strongView.try_as()) {
- m_textProvider = winrt::make(
- strongView.as(), this)
- .try_as();
- }
-
- if (strongView.try_as()) {
- m_annotationProvider = winrt::make(
- strongView.as(), this)
- .try_as();
- }
}
CompositionDynamicAutomationProvider::CompositionDynamicAutomationProvider(
@@ -161,6 +149,13 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::SetFocus(void) {
return UiaSetFocusHelper(m_view);
}
+winrt::IUnknown CompositionDynamicAutomationProvider::TryGetChildSiteLinkAutomationProvider() {
+ if (m_childSiteLink) {
+ return m_childSiteLink.AutomationProvider().as();
+ }
+ return nullptr;
+}
+
HRESULT __stdcall CompositionDynamicAutomationProvider::get_FragmentRoot(IRawElementProviderFragmentRoot **pRetVal) {
if (pRetVal == nullptr)
return E_POINTER;
@@ -297,16 +292,31 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTE
if (patternId == UIA_TextPatternId &&
(strongView.try_as() ||
strongView.try_as())) {
+ if (!m_textProvider) {
+ m_textProvider = winrt::make(
+ strongView.as())
+ .as();
+ }
m_textProvider.as().copy_to(pRetVal);
}
if (patternId == UIA_TextPattern2Id &&
strongView.try_as()) {
+ if (!m_textProvider) {
+ m_textProvider = winrt::make(
+ strongView.as())
+ .as();
+ }
m_textProvider.as().copy_to(pRetVal);
}
if (patternId == UIA_AnnotationPatternId &&
strongView.try_as() &&
accessibilityAnnotationHasValue(props->accessibilityAnnotation)) {
+ if (!m_annotationProvider) {
+ m_annotationProvider = winrt::make(
+ strongView.as())
+ .as();
+ }
m_annotationProvider.as().copy_to(pRetVal);
}
@@ -949,9 +959,18 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetSelection(SAFEARRAY *
std::vector selectedItems;
for (size_t i = 0; i < m_selectionItems.size(); i++) {
auto selectionItem = m_selectionItems.at(i);
- auto provider = selectionItem.as();
+
+ winrt::com_ptr unkSelectionItemProvider;
+ auto hr = selectionItem->GetPatternProvider(UIA_SelectionItemPatternId, unkSelectionItemProvider.put());
+ if (FAILED(hr))
+ return hr;
+
+ auto selectionItemProvider = unkSelectionItemProvider.try_as();
+ if (!selectionItemProvider)
+ return E_FAIL;
+
BOOL selected;
- auto hr = provider->get_IsSelected(&selected);
+ hr = selectionItemProvider->get_IsSelected(&selected);
if (hr == S_OK && selected) {
selectedItems.push_back(int(i));
}
@@ -1010,27 +1029,28 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::get_IsSelected(BOOL *pRe
return S_OK;
}
-IRawElementProviderSimple *findSelectionContainer(winrt::Microsoft::ReactNative::ComponentView current) {
+winrt::Microsoft::ReactNative::ComponentView findSelectionContainer(
+ winrt::Microsoft::ReactNative::ComponentView current) noexcept {
if (!current)
return nullptr;
- auto props = std::static_pointer_cast(
- winrt::get_self(current)->props());
- if (props->accessibilityState.has_value() && props->accessibilityState->multiselectable.has_value() &&
- props->accessibilityState->required.has_value()) {
- auto uiaProvider =
- current.as()->EnsureUiaProvider();
- if (uiaProvider != nullptr) {
- auto spProviderSimple = uiaProvider.try_as();
- if (spProviderSimple != nullptr) {
- spProviderSimple->AddRef();
- return spProviderSimple.get();
- }
+ if (auto viewbase = current.try_as()) {
+ auto props = viewbase->viewProps();
+ if (props->accessibilityState.has_value() && props->accessibilityState->multiselectable.has_value() &&
+ props->accessibilityState->required.has_value()) {
+ return current;
}
- } else {
- return findSelectionContainer(current.Parent());
}
- return nullptr;
+ return findSelectionContainer(current.Parent());
+}
+
+winrt::Microsoft::ReactNative::ComponentView CompositionDynamicAutomationProvider::GetSelectionContainer() noexcept {
+ auto strongView = m_view.view();
+
+ if (!strongView)
+ return nullptr;
+
+ return findSelectionContainer(strongView.Parent());
}
HRESULT __stdcall CompositionDynamicAutomationProvider::get_SelectionContainer(IRawElementProviderSimple **pRetVal) {
@@ -1041,7 +1061,20 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::get_SelectionContainer(I
if (!strongView)
return UIA_E_ELEMENTNOTAVAILABLE;
- *pRetVal = findSelectionContainer(strongView.Parent());
+ *pRetVal = nullptr;
+
+ auto selectionContainerView = GetSelectionContainer();
+ auto uiaProvider =
+ winrt::get_self(selectionContainerView)
+ ->EnsureUiaProvider();
+ if (uiaProvider != nullptr) {
+ auto spProviderSimple = uiaProvider.try_as();
+ if (spProviderSimple != nullptr) {
+ spProviderSimple->AddRef();
+ *pRetVal = spProviderSimple.get();
+ }
+ }
+
return S_OK;
}
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h
index 7009a6e21b8..7a5b8d686ac 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h
@@ -97,6 +97,15 @@ class CompositionDynamicAutomationProvider : public winrt::implements<
void AddToSelectionItems(winrt::com_ptr &item);
void RemoveFromSelectionItems(winrt::com_ptr &item);
+ winrt::Microsoft::ReactNative::ComponentView GetSelectionContainer() noexcept;
+
+ void SetChildSiteLink(winrt::Microsoft::UI::Content::ChildSiteLink childSiteLink) {
+ m_childSiteLink = childSiteLink;
+ }
+
+ // If this object is for a ChildSiteLink, returns the ChildSiteLink's automation provider.
+ // This will be a provider object from the hosted framework (for example, WinUI).
+ winrt::IUnknown TryGetChildSiteLinkAutomationProvider();
private:
::Microsoft::ReactNative::ReactTaggedView m_view;
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp
index 9bb904ec04a..d60b545b49e 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp
@@ -15,6 +15,7 @@
#include
#include "Composition.Input.h"
#include "CompositionViewComponentView.h"
+#include "ParagraphComponentView.h"
#include "ReactNativeIsland.h"
#include "RootComponentView.h"
@@ -1101,6 +1102,13 @@ void CompositionEventHandler::onPointerExited(
void CompositionEventHandler::onPointerPressed(
const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept {
+ namespace Composition = winrt::Microsoft::ReactNative::Composition;
+
+ // Clears any active text selection when left pointer is pressed
+ if (pointerPoint.Properties().PointerUpdateKind() != Composition::Input::PointerUpdateKind::RightButtonPressed) {
+ RootComponentView().ClearCurrentTextSelection();
+ }
+
PointerId pointerId = pointerPoint.PointerId();
auto staleTouch = std::find_if(m_activeTouches.begin(), m_activeTouches.end(), [pointerId](const auto &pair) {
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp
index 6ead642c857..e254eb4c367 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp
@@ -219,7 +219,8 @@ HRESULT __stdcall CompositionRootAutomationProvider::ElementProviderFromPoint(
auto local = rootView->ConvertScreenToLocal({static_cast(x), static_cast(y)});
auto provider = rootView->UiaProviderFromPoint(
{static_cast(local.X * rootView->LayoutMetrics().PointScaleFactor),
- static_cast(local.Y * rootView->LayoutMetrics().PointScaleFactor)});
+ static_cast(local.Y * rootView->LayoutMetrics().PointScaleFactor)},
+ {static_cast(x), static_cast(y)});
auto spFragment = provider.try_as();
if (spFragment) {
*pRetVal = spFragment.detach();
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.cpp
index ef997fe0ae4..cdc309090c2 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.cpp
@@ -10,10 +10,8 @@
namespace winrt::Microsoft::ReactNative::implementation {
CompositionTextProvider::CompositionTextProvider(
- const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView,
- CompositionDynamicAutomationProvider *parentProvider) noexcept
+ const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView) noexcept
: m_view{componentView} {
- m_parentProvider.copy_from(parentProvider);
EnsureTextRangeProvider();
}
@@ -24,10 +22,9 @@ void CompositionTextProvider::EnsureTextRangeProvider() {
return;
if (!m_textRangeProvider) {
- m_textRangeProvider =
- winrt::make(
- strongView.as(), m_parentProvider.get())
- .try_as();
+ m_textRangeProvider = winrt::make(
+ strongView.as())
+ .as();
}
}
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.h
index cb68ad7bfe9..28195b2fbd8 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.h
@@ -1,6 +1,5 @@
#pragma once
-#include
#include
#include
#include
@@ -11,9 +10,7 @@ namespace winrt::Microsoft::ReactNative::implementation {
class CompositionTextProvider : public winrt::implements {
public:
- CompositionTextProvider(
- const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView,
- CompositionDynamicAutomationProvider *parentProvider) noexcept;
+ CompositionTextProvider(const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView) noexcept;
// inherited via ITextProvider
virtual HRESULT __stdcall get_DocumentRange(ITextRangeProvider **pRetVal) override;
@@ -35,7 +32,6 @@ class CompositionTextProvider : public winrt::implements m_textRangeProvider;
- winrt::com_ptr m_parentProvider;
};
} // namespace winrt::Microsoft::ReactNative::implementation
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.cpp
index e2260913e15..eef088606e0 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.cpp
@@ -11,18 +11,14 @@
namespace winrt::Microsoft::ReactNative::implementation {
CompositionTextRangeProvider::CompositionTextRangeProvider(
- const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView,
- CompositionDynamicAutomationProvider *parentProvider) noexcept
- : m_view{componentView} {
- m_parentProvider.copy_from(parentProvider);
-}
+ const winrt::Microsoft::ReactNative::ComponentView &componentView) noexcept
+ : m_view{componentView} {}
HRESULT __stdcall CompositionTextRangeProvider::Clone(ITextRangeProvider **pRetVal) {
if (pRetVal == nullptr)
return E_POINTER;
- auto clone = winrt::make(
- m_view.view().as(), m_parentProvider.get());
+ auto clone = winrt::make(m_view.view());
*pRetVal = clone.detach();
return S_OK;
}
@@ -91,13 +87,13 @@ HRESULT __stdcall CompositionTextRangeProvider::GetAttributeValue(TEXTATTRIBUTEI
if (!strongView)
return UIA_E_ELEMENTNOTAVAILABLE;
- auto props = std::static_pointer_cast(
+ auto props = std::static_pointer_cast(
winrt::get_self(strongView)->props());
- auto textinputProps = std::static_pointer_cast(
- winrt::get_self(strongView)->props());
+ auto asParagraph =
+ strongView.try_as();
- auto isTextInput =
+ auto asTextInput =
strongView.try_as();
if (props == nullptr)
@@ -106,15 +102,16 @@ HRESULT __stdcall CompositionTextRangeProvider::GetAttributeValue(TEXTATTRIBUTEI
if (attributeId == UIA_BackgroundColorAttributeId) {
pRetVal->vt = VT_I4;
pRetVal->lVal = (*props->backgroundColor).AsColorRefWithAlpha();
- } else if (attributeId == UIA_CapStyleAttributeId) {
+ } else if (attributeId == UIA_CapStyleAttributeId && asParagraph) {
pRetVal->vt = VT_I4;
auto fontVariant = facebook::react::FontVariant::Default;
auto textTransform = facebook::react::TextTransform::None;
- if (props->textAttributes.fontVariant.has_value()) {
- fontVariant = props->textAttributes.fontVariant.value();
+
+ if (asParagraph->paragraphProps().textAttributes.fontVariant.has_value()) {
+ fontVariant = asParagraph->paragraphProps().textAttributes.fontVariant.value();
}
- if (props->textAttributes.textTransform.has_value()) {
- textTransform = props->textAttributes.textTransform.value();
+ if (asParagraph->paragraphProps().textAttributes.textTransform.has_value()) {
+ textTransform = asParagraph->paragraphProps().textAttributes.textTransform.value();
}
if (fontVariant == facebook::react::FontVariant::SmallCaps) {
pRetVal->lVal = CapStyle_SmallCap;
@@ -125,39 +122,44 @@ HRESULT __stdcall CompositionTextRangeProvider::GetAttributeValue(TEXTATTRIBUTEI
} else if (textTransform == facebook::react::TextTransform::Uppercase) {
pRetVal->lVal = CapStyle_AllCap;
}
- } else if (attributeId == UIA_FontNameAttributeId) {
+ } else if (attributeId == UIA_FontNameAttributeId && asParagraph) {
pRetVal->vt = VT_BSTR;
- auto fontName = props->textAttributes.fontFamily;
+ auto fontName = asParagraph->paragraphProps().textAttributes.fontFamily;
if (fontName.empty()) {
fontName = "Segoe UI";
}
std::wstring wfontName(fontName.begin(), fontName.end());
pRetVal->bstrVal = SysAllocString(wfontName.c_str());
- } else if (attributeId == UIA_FontSizeAttributeId) {
+ } else if (attributeId == UIA_FontSizeAttributeId && asParagraph) {
pRetVal->vt = VT_R8;
- pRetVal->dblVal = props->textAttributes.fontSize;
- } else if (attributeId == UIA_FontWeightAttributeId) {
- if (props->textAttributes.fontWeight.has_value()) {
+ pRetVal->dblVal = asParagraph->paragraphProps().textAttributes.fontSize;
+ } else if (attributeId == UIA_FontWeightAttributeId && asParagraph) {
+ if (asParagraph->paragraphProps().textAttributes.fontWeight.has_value()) {
pRetVal->vt = VT_I4;
- pRetVal->lVal = static_cast(props->textAttributes.fontWeight.value());
+ pRetVal->lVal = static_cast(asParagraph->paragraphProps().textAttributes.fontWeight.value());
}
- } else if (attributeId == UIA_ForegroundColorAttributeId) {
+ } else if (attributeId == UIA_ForegroundColorAttributeId && asParagraph) {
pRetVal->vt = VT_I4;
- pRetVal->lVal = (*props->textAttributes.foregroundColor).AsColorRefWithAlpha();
- } else if (attributeId == UIA_IsItalicAttributeId) {
+ pRetVal->lVal = (*asParagraph->paragraphProps().textAttributes.foregroundColor).AsColorRefWithAlpha();
+ } else if (attributeId == UIA_IsItalicAttributeId && asParagraph) {
pRetVal->vt = VT_BOOL;
- pRetVal->boolVal = (props->textAttributes.fontStyle.has_value() &&
- props->textAttributes.fontStyle.value() == facebook::react::FontStyle::Italic)
+ pRetVal->boolVal =
+ (asParagraph->paragraphProps().textAttributes.fontStyle.has_value() &&
+ asParagraph->paragraphProps().textAttributes.fontStyle.value() == facebook::react::FontStyle::Italic)
? VARIANT_TRUE
: VARIANT_FALSE;
} else if (attributeId == UIA_IsReadOnlyAttributeId) {
pRetVal->vt = VT_BOOL;
- pRetVal->boolVal = isTextInput ? textinputProps->editable ? VARIANT_FALSE : VARIANT_TRUE : VARIANT_TRUE;
- } else if (attributeId == UIA_HorizontalTextAlignmentAttributeId) {
+ if (asTextInput) {
+ pRetVal->boolVal = asTextInput->windowsTextInputProps().editable ? VARIANT_FALSE : VARIANT_TRUE;
+ } else {
+ pRetVal->boolVal = VARIANT_TRUE;
+ }
+ } else if (attributeId == UIA_HorizontalTextAlignmentAttributeId && asParagraph) {
pRetVal->vt = VT_I4;
auto textAlign = facebook::react::TextAlignment::Center;
- if (props->textAttributes.alignment.has_value()) {
- textAlign = props->textAttributes.alignment.value();
+ if (asParagraph->paragraphProps().textAttributes.alignment.has_value()) {
+ textAlign = asParagraph->paragraphProps().textAttributes.alignment.value();
}
if (textAlign == facebook::react::TextAlignment::Left) {
pRetVal->lVal = HorizontalTextAlignment_Left;
@@ -170,40 +172,42 @@ HRESULT __stdcall CompositionTextRangeProvider::GetAttributeValue(TEXTATTRIBUTEI
} else if (textAlign == facebook::react::TextAlignment::Natural) {
pRetVal->lVal = HorizontalTextAlignment_Left;
}
- } else if (attributeId == UIA_StrikethroughColorAttributeId) {
- if (props->textAttributes.textDecorationLineType.has_value() &&
- (props->textAttributes.textDecorationLineType.value() ==
+ } else if (attributeId == UIA_StrikethroughColorAttributeId && asParagraph) {
+ if (asParagraph->paragraphProps().textAttributes.textDecorationLineType.has_value() &&
+ (asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() ==
facebook::react::TextDecorationLineType::Strikethrough ||
- props->textAttributes.textDecorationLineType.value() ==
+ asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() ==
facebook::react::TextDecorationLineType::UnderlineStrikethrough)) {
pRetVal->vt = VT_I4;
- pRetVal->lVal = (*props->textAttributes.textDecorationColor).AsColorRefWithAlpha();
+ pRetVal->lVal = (*asParagraph->paragraphProps().textAttributes.textDecorationColor).AsColorRefWithAlpha();
}
- } else if (attributeId == UIA_StrikethroughStyleAttributeId) {
- if (props->textAttributes.textDecorationLineType.has_value() &&
- (props->textAttributes.textDecorationLineType.value() ==
+ } else if (attributeId == UIA_StrikethroughStyleAttributeId && asParagraph) {
+ if (asParagraph->paragraphProps().textAttributes.textDecorationLineType.has_value() &&
+ (asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() ==
facebook::react::TextDecorationLineType::Strikethrough ||
- props->textAttributes.textDecorationLineType.value() ==
+ asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() ==
facebook::react::TextDecorationLineType::UnderlineStrikethrough)) {
pRetVal->vt = VT_I4;
- auto style = props->textAttributes.textDecorationStyle.value();
+ auto style = asParagraph->paragraphProps().textAttributes.textDecorationStyle.value();
pRetVal->lVal = GetTextDecorationLineStyle(style);
}
- } else if (attributeId == UIA_UnderlineColorAttributeId) {
- if (props->textAttributes.textDecorationLineType.has_value() &&
- (props->textAttributes.textDecorationLineType.value() == facebook::react::TextDecorationLineType::Underline ||
- props->textAttributes.textDecorationLineType.value() ==
+ } else if (attributeId == UIA_UnderlineColorAttributeId && asParagraph) {
+ if (asParagraph->paragraphProps().textAttributes.textDecorationLineType.has_value() &&
+ (asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() ==
+ facebook::react::TextDecorationLineType::Underline ||
+ asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() ==
facebook::react::TextDecorationLineType::UnderlineStrikethrough)) {
pRetVal->vt = VT_I4;
- pRetVal->lVal = (*props->textAttributes.textDecorationColor).AsColorRefWithAlpha();
+ pRetVal->lVal = (*asParagraph->paragraphProps().textAttributes.textDecorationColor).AsColorRefWithAlpha();
}
- } else if (attributeId == UIA_UnderlineStyleAttributeId) {
- if (props->textAttributes.textDecorationLineType.has_value() &&
- (props->textAttributes.textDecorationLineType.value() == facebook::react::TextDecorationLineType::Underline ||
- props->textAttributes.textDecorationLineType.value() ==
+ } else if (attributeId == UIA_UnderlineStyleAttributeId && asParagraph) {
+ if (asParagraph->paragraphProps().textAttributes.textDecorationLineType.has_value() &&
+ (asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() ==
+ facebook::react::TextDecorationLineType::Underline ||
+ asParagraph->paragraphProps().textAttributes.textDecorationLineType.value() ==
facebook::react::TextDecorationLineType::UnderlineStrikethrough)) {
pRetVal->vt = VT_I4;
- auto style = props->textAttributes.textDecorationStyle.value();
+ auto style = asParagraph->paragraphProps().textAttributes.textDecorationStyle.value();
pRetVal->lVal = GetTextDecorationLineStyle(style);
}
}
@@ -214,7 +218,18 @@ HRESULT __stdcall CompositionTextRangeProvider::GetBoundingRectangles(SAFEARRAY
if (pRetVal == nullptr)
return E_POINTER;
UiaRect rect;
- auto hr = m_parentProvider->get_BoundingRectangle(&rect);
+
+ auto strongView = m_view.view();
+ if (!strongView)
+ return UIA_E_ELEMENTNOTAVAILABLE;
+
+ auto componentView = strongView.as();
+ auto provider = componentView->EnsureUiaProvider();
+ auto repf = provider.try_as();
+ if (!repf)
+ return E_FAIL;
+
+ auto hr = repf->get_BoundingRectangle(&rect);
if (FAILED(hr))
return hr;
*pRetVal = SafeArrayCreateVector(VT_R8, 0, 4);
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.h
index 18ec13688bf..8fb3308dbc4 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.h
@@ -1,6 +1,5 @@
#pragma once
-#include
#include
#include
#include
@@ -12,9 +11,7 @@ namespace winrt::Microsoft::ReactNative::implementation {
class CompositionTextRangeProvider : public winrt::implements {
public:
- CompositionTextRangeProvider(
- const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView,
- CompositionDynamicAutomationProvider *parentProvider) noexcept;
+ CompositionTextRangeProvider(const winrt::Microsoft::ReactNative::ComponentView &componentView) noexcept;
// inherited via ITextRangeProvider
virtual HRESULT __stdcall Clone(ITextRangeProvider **pRetVal) override;
@@ -53,7 +50,6 @@ class CompositionTextRangeProvider : public winrt::implements m_parentProvider;
};
} // namespace winrt::Microsoft::ReactNative::implementation
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp
index b53b7057344..95a2e752c7c 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp
@@ -801,8 +801,8 @@ void ComponentView::updateAccessibilityProps(
winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty(
EnsureUiaProvider(),
UIA_LiveSettingPropertyId,
- oldViewProps.accessibilityLiveRegion,
- newViewProps.accessibilityLiveRegion);
+ winrt::Microsoft::ReactNative::implementation::GetLiveSetting(oldViewProps.accessibilityLiveRegion),
+ winrt::Microsoft::ReactNative::implementation::GetLiveSetting(newViewProps.accessibilityLiveRegion));
winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty(
EnsureUiaProvider(), UIA_LevelPropertyId, oldViewProps.accessibilityLevel, newViewProps.accessibilityLevel);
@@ -857,14 +857,13 @@ void ComponentView::updateAccessibilityProps(
if ((oldViewProps.accessibilityState.has_value() && oldViewProps.accessibilityState->selected.has_value()) !=
((newViewProps.accessibilityState.has_value() && newViewProps.accessibilityState->selected.has_value()))) {
- auto compProvider =
- EnsureUiaProvider()
- .try_as();
- if (compProvider) {
+ EnsureUiaProvider();
+ if (m_innerAutomationProvider) {
if ((newViewProps.accessibilityState.has_value() && newViewProps.accessibilityState->selected.has_value())) {
- winrt::Microsoft::ReactNative::implementation::AddSelectionItemsToContainer(compProvider.get());
+ winrt::Microsoft::ReactNative::implementation::AddSelectionItemsToContainer(m_innerAutomationProvider.get());
} else {
- winrt::Microsoft::ReactNative::implementation::RemoveSelectionItemsFromContainer(compProvider.get());
+ winrt::Microsoft::ReactNative::implementation::RemoveSelectionItemsFromContainer(
+ m_innerAutomationProvider.get());
}
}
}
@@ -1354,12 +1353,17 @@ std::string ViewComponentView::DefaultControlType() const noexcept {
return "group";
}
-winrt::IInspectable ComponentView::EnsureUiaProvider() noexcept {
- if (m_uiaProvider == nullptr) {
- m_uiaProvider =
- winrt::make(*get_strong());
- }
- return m_uiaProvider;
+winrt::Windows::Foundation::IInspectable ComponentView::CreateAutomationProvider() noexcept {
+ Assert(!m_innerAutomationProvider);
+ m_innerAutomationProvider =
+ winrt::make_self(
+ *get_strong());
+ return *m_innerAutomationProvider;
+}
+
+const winrt::com_ptr
+ &ComponentView::InnerAutomationProvider() const noexcept {
+ return m_innerAutomationProvider;
}
bool IntersectRect(RECT *prcDst, const RECT &prcSrc1, const RECT &prcSrc2) {
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h
index 588af7ad463..fed4797026c 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h
@@ -19,8 +19,11 @@ namespace Microsoft::ReactNative {
struct CompContext;
} // namespace Microsoft::ReactNative
-namespace winrt::Microsoft::ReactNative::Composition::implementation {
+namespace winrt::Microsoft::ReactNative::implementation {
+class CompositionDynamicAutomationProvider;
+}
+namespace winrt::Microsoft::ReactNative::Composition::implementation {
struct FocusPrimitive {
std::shared_ptr m_focusInnerPrimitive;
std::shared_ptr m_focusOuterPrimitive;
@@ -100,7 +103,9 @@ struct ComponentView : public ComponentViewT<
comp::CompositionPropertySet EnsureCenterPointPropertySet() noexcept;
void EnsureTransformMatrixFacade() noexcept;
- winrt::IInspectable EnsureUiaProvider() noexcept override;
+ winrt::Windows::Foundation::IInspectable CreateAutomationProvider() noexcept override;
+ const winrt::com_ptr
+ &InnerAutomationProvider() const noexcept;
std::optional getAccessiblityValue() noexcept override;
void setAcccessiblityValue(std::string &&value) noexcept override;
bool getAcccessiblityIsReadOnly() noexcept override;
@@ -130,7 +135,9 @@ struct ComponentView : public ComponentViewT<
facebook::react::Point &ptContent,
facebook::react::Point &localPt) const noexcept;
- winrt::IInspectable m_uiaProvider{nullptr};
+ // Most access should be through EnsureUIAProvider, instead of direct access to this.
+ winrt::com_ptr
+ m_innerAutomationProvider;
winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext m_compContext;
comp::CompositionPropertySet m_centerPropSet{nullptr};
facebook::react::SharedViewEventEmitter m_eventEmitter;
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp
index 57aa7d14656..327c0028600 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp
@@ -111,12 +111,11 @@ void ContentIslandComponentView::ParentLayoutChanged() noexcept {
});
}
-winrt::IInspectable ContentIslandComponentView::EnsureUiaProvider() noexcept {
- if (m_uiaProvider == nullptr) {
- m_uiaProvider = winrt::make(
- *get_strong(), m_childSiteLink);
- }
- return m_uiaProvider;
+winrt::Windows::Foundation::IInspectable ContentIslandComponentView::CreateAutomationProvider() noexcept {
+ m_innerAutomationProvider =
+ winrt::make_self(
+ *get_strong(), m_childSiteLink);
+ return *m_innerAutomationProvider;
}
bool ContentIslandComponentView::focusable() const noexcept {
@@ -126,6 +125,27 @@ bool ContentIslandComponentView::focusable() const noexcept {
return true;
}
+facebook::react::Tag ContentIslandComponentView::hitTest(
+ facebook::react::Point pt,
+ facebook::react::Point &localPt,
+ bool ignorePointerEvents) const noexcept {
+ facebook::react::Point ptLocal{pt.x - m_layoutMetrics.frame.origin.x, pt.y - m_layoutMetrics.frame.origin.y};
+
+ // Check if the point is within the bounds of this ContentIslandComponentView.
+ // This ensures that hit tests correctly return this view's tag for UIA purposes,
+ // even when the actual content (XAML buttons, etc.) is hosted in the ContentIsland.
+ auto props = viewProps();
+ if ((ignorePointerEvents || props->pointerEvents == facebook::react::PointerEventsMode::Auto ||
+ props->pointerEvents == facebook::react::PointerEventsMode::BoxOnly) &&
+ ptLocal.x >= 0 && ptLocal.x <= m_layoutMetrics.frame.size.width && ptLocal.y >= 0 &&
+ ptLocal.y <= m_layoutMetrics.frame.size.height) {
+ localPt = ptLocal;
+ return Tag();
+ }
+
+ return -1;
+}
+
// Helper to convert a FocusNavigationDirection to a FocusNavigationReason.
winrt::Microsoft::UI::Input::FocusNavigationReason GetFocusNavigationReason(
winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept {
@@ -178,14 +198,12 @@ ContentIslandComponentView::~ContentIslandComponentView() noexcept {
void ContentIslandComponentView::MountChildComponentView(
const winrt::Microsoft::ReactNative::ComponentView &childComponentView,
uint32_t index) noexcept {
- assert(false);
base_type::MountChildComponentView(childComponentView, index);
}
void ContentIslandComponentView::UnmountChildComponentView(
const winrt::Microsoft::ReactNative::ComponentView &childComponentView,
uint32_t index) noexcept {
- assert(false);
base_type::UnmountChildComponentView(childComponentView, index);
}
@@ -262,6 +280,10 @@ void ContentIslandComponentView::ConfigureChildSiteLinkAutomation() noexcept {
args.AutomationProvider(nullptr);
args.Handled(true);
});
+
+ if (m_innerAutomationProvider) {
+ m_innerAutomationProvider->SetChildSiteLink(m_childSiteLink);
+ }
}
} // namespace winrt::Microsoft::ReactNative::Composition::implementation
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h
index f530baa3400..e6f5fe8808a 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h
@@ -40,7 +40,10 @@ struct ContentIslandComponentView : ContentIslandComponentViewTOuterVisual();
auto brush = m_compContext.CreateColorBrush({204, 200, 230, 255});
+ float scaleFactor = m_layoutMetrics.pointScaleFactor;
for (auto &element : elements) {
auto overlayVisual = m_compContext.CreateSpriteVisual();
- overlayVisual.Size({element.width, element.height});
- overlayVisual.Offset({element.x, element.y, 0.0f});
+ overlayVisual.Size({element.width * scaleFactor, element.height * scaleFactor});
+ overlayVisual.Offset({element.x * scaleFactor, element.y * scaleFactor, 0.0f});
overlayVisual.Brush(brush);
rootVisual.InsertAt(overlayVisual, root->overlayIndex() + m_activeOverlays);
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp
index 782f17fcf62..bc3e5c8c6cf 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp
@@ -22,7 +22,6 @@
#include
#include
#include
-#include "CompositionDynamicAutomationProvider.h"
#include "CompositionHelpers.h"
#include "RootComponentView.h"
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp
index 407022de250..acd3d69fa89 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp
@@ -8,17 +8,44 @@
#include
#include
+#include
+#include
#include
#include
#include
#include
#include
-#include "CompositionDynamicAutomationProvider.h"
+#include
+#include
#include "CompositionHelpers.h"
+#include "RootComponentView.h"
#include "TextDrawing.h"
namespace winrt::Microsoft::ReactNative::Composition::implementation {
+// Automatically restores the original DPI of a render target
+struct DpiRestorer {
+ ID2D1RenderTarget *renderTarget = nullptr;
+ float originalDpiX = 0.0f;
+ float originalDpiY = 0.0f;
+
+ void operator()(ID2D1RenderTarget *) const noexcept {
+ if (renderTarget) {
+ renderTarget->SetDpi(originalDpiX, originalDpiY);
+ }
+ }
+};
+
+inline std::unique_ptr
+MakeDpiGuard(ID2D1RenderTarget &renderTarget, float newDpiX, float newDpiY) noexcept {
+ float originalDpiX, originalDpiY;
+ renderTarget.GetDpi(&originalDpiX, &originalDpiY);
+ renderTarget.SetDpi(newDpiX, newDpiY);
+
+ return std::unique_ptr(
+ &renderTarget, DpiRestorer{&renderTarget, originalDpiX, originalDpiY});
+}
+
ParagraphComponentView::ParagraphComponentView(
const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext,
facebook::react::Tag tag,
@@ -28,7 +55,8 @@ ParagraphComponentView::ParagraphComponentView(
compContext,
tag,
reactContext,
- ComponentViewFeatures::Default & ~ComponentViewFeatures::Background) {}
+ // Disable Background (text draws its own) and FocusVisual (selection highlight is the focus indicator)
+ ComponentViewFeatures::Default & ~ComponentViewFeatures::Background & ~ComponentViewFeatures::FocusVisual) {}
void ParagraphComponentView::MountChildComponentView(
const winrt::Microsoft::ReactNative::ComponentView &childComponentView,
@@ -71,6 +99,14 @@ void ParagraphComponentView::updateProps(
m_textLayout = nullptr;
}
+ // Clear selection if text becomes non-selectable
+ if (oldViewProps.isSelectable != newViewProps.isSelectable) {
+ if (!newViewProps.isSelectable) {
+ ClearSelection();
+ }
+ m_requireRedraw = true;
+ }
+
Super::updateProps(props, oldProps);
}
@@ -131,6 +167,108 @@ void ParagraphComponentView::updateTextAlignment(
m_textLayout = nullptr;
}
+bool ParagraphComponentView::IsTextSelectableAtPoint(facebook::react::Point pt) noexcept {
+ // paragraph-level selectable prop is enabled
+ const auto &props = paragraphProps();
+ if (!props.isSelectable) {
+ return false;
+ }
+
+ // Finds which text fragment was hit
+ if (m_attributedStringBox.getValue().getFragments().size() && m_textLayout) {
+ BOOL isTrailingHit = false;
+ BOOL isInside = false;
+ DWRITE_HIT_TEST_METRICS metrics;
+ winrt::check_hresult(m_textLayout->HitTestPoint(pt.x, pt.y, &isTrailingHit, &isInside, &metrics));
+
+ if (isInside) {
+ uint32_t textPosition = metrics.textPosition;
+
+ // Finds which fragment contains this text position
+ for (auto fragment : m_attributedStringBox.getValue().getFragments()) {
+ if (textPosition < fragment.string.length()) {
+ return true;
+ }
+ textPosition -= static_cast(fragment.string.length());
+ }
+ }
+ }
+
+ return false;
+}
+
+std::optional ParagraphComponentView::GetTextPositionAtPoint(facebook::react::Point pt) noexcept {
+ if (!m_textLayout) {
+ return std::nullopt;
+ }
+
+ BOOL isTrailingHit = FALSE;
+ BOOL isInside = FALSE;
+ DWRITE_HIT_TEST_METRICS metrics = {};
+
+ // Convert screen coordinates to character position
+ HRESULT hr = m_textLayout->HitTestPoint(pt.x, pt.y, &isTrailingHit, &isInside, &metrics);
+ if (FAILED(hr) || !isInside) {
+ return std::nullopt;
+ }
+
+ // Calculates the actual character position
+ // If isTrailingHit is true, the point is closer to the trailing edge of the character,
+ // so we should return the next character position (for cursor positioning)
+ return static_cast(metrics.textPosition + isTrailingHit);
+}
+
+std::optional ParagraphComponentView::GetClampedTextPosition(facebook::react::Point pt) noexcept {
+ if (!m_textLayout) {
+ return std::nullopt;
+ }
+
+ const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)};
+ if (utf16Text.empty()) {
+ return std::nullopt;
+ }
+
+ DWRITE_TEXT_METRICS textMetrics;
+ if (FAILED(m_textLayout->GetMetrics(&textMetrics))) {
+ return std::nullopt;
+ }
+
+ // Clamp the point to the text bounds for hit testing
+ const float clampedX = std::max(0.0f, std::min(pt.x, textMetrics.width));
+ const float clampedY = std::max(0.0f, std::min(pt.y, textMetrics.height));
+
+ BOOL isTrailingHit = FALSE;
+ BOOL isInside = FALSE;
+ DWRITE_HIT_TEST_METRICS metrics = {};
+
+ HRESULT hr = m_textLayout->HitTestPoint(clampedX, clampedY, &isTrailingHit, &isInside, &metrics);
+ if (FAILED(hr)) {
+ return std::nullopt;
+ }
+
+ int32_t result = static_cast(metrics.textPosition);
+ if (pt.x > textMetrics.width) {
+ // Dragging right - go to end of character
+ result = static_cast(metrics.textPosition + metrics.length);
+ } else if (pt.x < 0) {
+ // Dragging left - go to start of character
+ result = static_cast(metrics.textPosition);
+ } else if (isTrailingHit) {
+ // Inside bounds, trailing hit
+ result += 1;
+ }
+
+ if (pt.y > textMetrics.height) {
+ // Dragging below - select to end of text
+ result = static_cast(utf16Text.length());
+ } else if (pt.y < 0) {
+ // Dragging above - select to start of text
+ result = 0;
+ }
+
+ return result;
+}
+
void ParagraphComponentView::OnRenderingDeviceLost() noexcept {
DrawText();
}
@@ -263,6 +401,76 @@ void ParagraphComponentView::onThemeChanged() noexcept {
}
// Renders the text into our composition surface
+void ParagraphComponentView::DrawSelectionHighlight(
+ ID2D1RenderTarget &renderTarget,
+ float offsetX,
+ float offsetY,
+ float pointScaleFactor) noexcept {
+ if (!m_selectionStart || !m_selectionEnd || !m_textLayout) {
+ return;
+ }
+
+ // During drag, selection may not be normalized yet, using min/max for rendering
+ const int32_t selStart = std::min(*m_selectionStart, *m_selectionEnd);
+ const int32_t selEnd = std::max(*m_selectionStart, *m_selectionEnd);
+ if (selEnd <= selStart) {
+ return;
+ }
+
+ // Scale offset to match text layout coordinates (same as RenderText)
+ const float scaledOffsetX = offsetX / pointScaleFactor;
+ const float scaledOffsetY = offsetY / pointScaleFactor;
+
+ // Set DPI to match text rendering
+ const float dpi = pointScaleFactor * 96.0f;
+ std::unique_ptr dpiGuard = MakeDpiGuard(renderTarget, dpi, dpi);
+
+ // Get the hit test metrics for the selected text range
+ UINT32 actualCount = 0;
+ HRESULT hr = m_textLayout->HitTestTextRange(
+ static_cast(selStart),
+ static_cast(selEnd - selStart),
+ scaledOffsetX,
+ scaledOffsetY,
+ nullptr,
+ 0,
+ &actualCount);
+
+ if (actualCount == 0) {
+ return;
+ }
+
+ std::vector hitTestMetrics(actualCount);
+ hr = m_textLayout->HitTestTextRange(
+ static_cast(selStart),
+ static_cast(selEnd - selStart),
+ scaledOffsetX,
+ scaledOffsetY,
+ hitTestMetrics.data(),
+ actualCount,
+ &actualCount);
+
+ if (FAILED(hr)) {
+ return;
+ }
+
+ // TODO: use prop selectionColor if provided
+ winrt::com_ptr selectionBrush;
+ const D2D1_COLOR_F selectionColor = theme()->D2DPlatformColor("Highlight@40");
+ hr = renderTarget.CreateSolidColorBrush(selectionColor, selectionBrush.put());
+
+ if (FAILED(hr)) {
+ return;
+ }
+
+ // Draw rectangles for each hit test metric
+ for (UINT32 i = 0; i < actualCount; i++) {
+ const auto &metric = hitTestMetrics[i];
+ const D2D1_RECT_F rect = {metric.left, metric.top, metric.left + metric.width, metric.top + metric.height};
+ renderTarget.FillRectangle(&rect, selectionBrush.get());
+ }
+}
+
void ParagraphComponentView::DrawText() noexcept {
if (!m_drawingSurface || theme()->IsEmpty())
return;
@@ -281,13 +489,20 @@ void ParagraphComponentView::DrawText() noexcept {
viewProps()->backgroundColor ? theme()->D2DColor(*viewProps()->backgroundColor)
: D2D1::ColorF(D2D1::ColorF::Black, 0.0f));
const auto &props = paragraphProps();
+
+ // Calculate text offset
+ const float textOffsetX = static_cast(offset.x) + m_layoutMetrics.contentInsets.left;
+ const float textOffsetY = static_cast(offset.y) + m_layoutMetrics.contentInsets.top;
+
+ // Draw selection highlight behind text
+ DrawSelectionHighlight(*d2dDeviceContext, textOffsetX, textOffsetY, m_layoutMetrics.pointScaleFactor);
+
RenderText(
*d2dDeviceContext,
*m_textLayout,
m_attributedStringBox.getValue(),
props.textAttributes,
- {static_cast(offset.x) + m_layoutMetrics.contentInsets.left,
- static_cast(offset.y) + m_layoutMetrics.contentInsets.top},
+ {textOffsetX, textOffsetY},
m_layoutMetrics.pointScaleFactor,
*theme());
@@ -299,6 +514,324 @@ void ParagraphComponentView::DrawText() noexcept {
}
}
+void ParagraphComponentView::ClearSelection() noexcept {
+ const bool hadSelection = (m_selectionStart || m_selectionEnd || m_isSelecting);
+ m_selectionStart = std::nullopt;
+ m_selectionEnd = std::nullopt;
+ m_isSelecting = false;
+ if (hadSelection) {
+ // Clears selection highlight
+ DrawText();
+ }
+}
+
+void ParagraphComponentView::OnPointerPressed(
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept {
+ // Only handle selection if text is selectable
+ const auto &props = paragraphProps();
+ if (!props.isSelectable) {
+ Super::OnPointerPressed(args);
+ return;
+ }
+
+ auto pp = args.GetCurrentPoint(-1);
+
+ // Ignores right-click
+ if (pp.Properties().PointerUpdateKind() ==
+ winrt::Microsoft::ReactNative::Composition::Input::PointerUpdateKind::RightButtonPressed) {
+ args.Handled(true);
+ return;
+ }
+
+ auto position = pp.Position();
+
+ facebook::react::Point localPt{
+ position.X - m_layoutMetrics.frame.origin.x, position.Y - m_layoutMetrics.frame.origin.y};
+
+ std::optional charPosition = GetTextPositionAtPoint(localPt);
+
+ if (charPosition) {
+ if (auto root = rootComponentView()) {
+ root->ClearCurrentTextSelection();
+ }
+
+ // Check for double-click
+ auto now = std::chrono::steady_clock::now();
+ auto timeSinceLastClick = std::chrono::duration_cast(now - m_lastClickTime);
+ const UINT doubleClickTime = GetDoubleClickTime();
+ const bool isDoubleClick = (timeSinceLastClick.count() < static_cast(doubleClickTime)) &&
+ m_lastClickPosition && (std::abs(*charPosition - *m_lastClickPosition) <= 1);
+
+ // Update last click tracking
+ m_lastClickTime = now;
+ m_lastClickPosition = charPosition;
+
+ if (isDoubleClick) {
+ SelectWordAtPosition(*charPosition);
+ m_isSelecting = false;
+ } else {
+ // Single-click: start drag selection
+ m_selectionStart = charPosition;
+ m_selectionEnd = charPosition;
+ m_isSelecting = true;
+
+ // Tracks selection even when the mouse moves outside the component bounds
+ CapturePointer(args.Pointer());
+ }
+
+ if (auto root = rootComponentView()) {
+ root->SetViewWithTextSelection(*get_strong());
+ }
+
+ // Focuses so we receive onLostFocus when clicking elsewhere
+ if (auto root = rootComponentView()) {
+ root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
+ }
+
+ args.Handled(true);
+ } else {
+ ClearSelection();
+ m_lastClickPosition = std::nullopt;
+ Super::OnPointerPressed(args);
+ }
+}
+
+void ParagraphComponentView::OnPointerMoved(
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept {
+ // Only track movement if we're actively selecting
+ if (!m_isSelecting) {
+ Super::OnPointerMoved(args);
+ return;
+ }
+
+ auto pp = args.GetCurrentPoint(static_cast(Tag()));
+ auto position = pp.Position();
+
+ facebook::react::Point localPt{position.X, position.Y};
+ std::optional charPosition = GetClampedTextPosition(localPt);
+
+ if (charPosition && charPosition != m_selectionEnd) {
+ m_selectionEnd = charPosition;
+ DrawText();
+ args.Handled(true);
+ }
+}
+
+void ParagraphComponentView::OnPointerReleased(
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept {
+ // Check for right-click to show context menu
+ auto pp = args.GetCurrentPoint(-1);
+ if (pp.Properties().PointerUpdateKind() ==
+ winrt::Microsoft::ReactNative::Composition::Input::PointerUpdateKind::RightButtonReleased) {
+ const auto &props = paragraphProps();
+ if (props.isSelectable) {
+ ShowContextMenu();
+ args.Handled(true);
+ return;
+ }
+ }
+
+ if (!m_isSelecting) {
+ Super::OnPointerReleased(args);
+ return;
+ }
+
+ m_isSelecting = false;
+
+ ReleasePointerCapture(args.Pointer());
+
+ if (!m_selectionStart || !m_selectionEnd || *m_selectionStart == *m_selectionEnd) {
+ m_selectionStart = std::nullopt;
+ m_selectionEnd = std::nullopt;
+ } else {
+ SetSelection(*m_selectionStart, *m_selectionEnd);
+ }
+
+ args.Handled(true);
+}
+
+void ParagraphComponentView::onLostFocus(
+ const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept {
+ ClearSelection();
+
+ Super::onLostFocus(args);
+}
+
+void ParagraphComponentView::OnPointerCaptureLost() noexcept {
+ // Pointer capture was lost stop any active selection drag
+ if (m_isSelecting) {
+ m_isSelecting = false;
+
+ if (!m_selectionStart || !m_selectionEnd || *m_selectionStart == *m_selectionEnd) {
+ m_selectionStart = std::nullopt;
+ m_selectionEnd = std::nullopt;
+ } else {
+ SetSelection(*m_selectionStart, *m_selectionEnd);
+ }
+ }
+
+ Super::OnPointerCaptureLost();
+}
+
+std::string ParagraphComponentView::GetSelectedText() const noexcept {
+ if (!m_selectionStart || !m_selectionEnd) {
+ return "";
+ }
+
+ const int32_t selStart = std::min(*m_selectionStart, *m_selectionEnd);
+ const int32_t selEnd = std::max(*m_selectionStart, *m_selectionEnd);
+
+ if (selEnd <= selStart) {
+ return "";
+ }
+
+ const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)};
+
+ if (selStart >= static_cast(utf16Text.length())) {
+ return "";
+ }
+
+ const int32_t clampedEnd = std::min(selEnd, static_cast(utf16Text.length()));
+ const std::wstring selectedUtf16 =
+ utf16Text.substr(static_cast(selStart), static_cast(clampedEnd - selStart));
+ return ::Microsoft::Common::Unicode::Utf16ToUtf8(selectedUtf16);
+}
+
+void ParagraphComponentView::CopySelectionToClipboard() noexcept {
+ const std::string selectedText = GetSelectedText();
+ if (selectedText.empty()) {
+ return;
+ }
+
+ // Convert UTF-8 to wide string for Windows clipboard
+ const std::wstring wideText = ::Microsoft::Common::Unicode::Utf8ToUtf16(selectedText);
+
+ winrt::Windows::ApplicationModel::DataTransfer::DataPackage dataPackage;
+ dataPackage.SetText(wideText);
+ winrt::Windows::ApplicationModel::DataTransfer::Clipboard::SetContent(dataPackage);
+}
+
+void ParagraphComponentView::SelectWordAtPosition(int32_t charPosition) noexcept {
+ const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)};
+ const int32_t textLength = static_cast(utf16Text.length());
+
+ if (utf16Text.empty() || charPosition < 0 || charPosition >= textLength) {
+ return;
+ }
+
+ int32_t wordStart = charPosition;
+ int32_t wordEnd = charPosition;
+
+ ::Microsoft::ReactNative::IcuUtils::WordBreakIterator wordBreaker(utf16Text.c_str(), textLength);
+ const bool icuSuccess = wordBreaker.IsValid() && wordBreaker.GetWordBoundaries(charPosition, wordStart, wordEnd);
+
+ if (!icuSuccess) {
+ wordStart = charPosition;
+ wordEnd = charPosition;
+
+ while (wordStart > 0) {
+ int32_t prevPos = ::Microsoft::ReactNative::IcuUtils::MoveToPreviousCodePoint(utf16Text.c_str(), wordStart);
+ ::Microsoft::ReactNative::IcuUtils::UChar32 prevCp =
+ ::Microsoft::ReactNative::IcuUtils::GetCodePointAt(utf16Text.c_str(), textLength, prevPos);
+ if (!::Microsoft::ReactNative::IcuUtils::IsAlphanumeric(prevCp)) {
+ break;
+ }
+ wordStart = prevPos;
+ }
+
+ while (wordEnd < textLength) {
+ ::Microsoft::ReactNative::IcuUtils::UChar32 cp =
+ ::Microsoft::ReactNative::IcuUtils::GetCodePointAt(utf16Text.c_str(), textLength, wordEnd);
+ if (!::Microsoft::ReactNative::IcuUtils::IsAlphanumeric(cp)) {
+ break;
+ }
+ wordEnd = ::Microsoft::ReactNative::IcuUtils::MoveToNextCodePoint(utf16Text.c_str(), textLength, wordEnd);
+ }
+ }
+
+ if (wordEnd > wordStart) {
+ SetSelection(wordStart, wordEnd);
+ DrawText();
+ }
+}
+
+void ParagraphComponentView::SetSelection(int32_t start, int32_t end) noexcept {
+ m_selectionStart = std::min(start, end);
+ m_selectionEnd = std::max(start, end);
+}
+
+void ParagraphComponentView::ShowContextMenu() noexcept {
+ HMENU menu = CreatePopupMenu();
+ if (!menu) {
+ return;
+ }
+
+ const bool hasSelection = (m_selectionStart && m_selectionEnd && *m_selectionStart != *m_selectionEnd);
+ const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)};
+ const bool hasText = !utf16Text.empty();
+
+ // Add menu items (1 = Copy, 2 = Select All)
+ AppendMenuW(menu, MF_STRING | (hasSelection ? 0 : MF_GRAYED), 1, L"Copy");
+ AppendMenuW(menu, MF_STRING | (hasText ? 0 : MF_GRAYED), 2, L"Select All");
+
+ // Get cursor position for menu placement
+ POINT cursorPos;
+ GetCursorPos(&cursorPos);
+
+ const HWND hwnd = GetActiveWindow();
+
+ const int cmd = TrackPopupMenu(
+ menu, TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RETURNCMD | TPM_NONOTIFY, cursorPos.x, cursorPos.y, 0, hwnd, NULL);
+
+ if (cmd == 1) {
+ // Copy
+ CopySelectionToClipboard();
+ } else if (cmd == 2) {
+ SetSelection(0, static_cast(utf16Text.length()));
+ DrawText();
+ }
+
+ DestroyMenu(menu);
+}
+
+void ParagraphComponentView::OnKeyDown(
+ const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept {
+ const bool isCtrlDown =
+ (args.KeyboardSource().GetKeyState(winrt::Windows::System::VirtualKey::Control) &
+ winrt::Microsoft::UI::Input::VirtualKeyStates::Down) == winrt::Microsoft::UI::Input::VirtualKeyStates::Down;
+
+ // Handle Ctrl+C for copy
+ if (isCtrlDown && args.Key() == winrt::Windows::System::VirtualKey::C) {
+ if (m_selectionStart && m_selectionEnd && *m_selectionStart != *m_selectionEnd) {
+ CopySelectionToClipboard();
+ args.Handled(true);
+ return;
+ }
+ }
+
+ // Handle Ctrl+A for select all
+ if (isCtrlDown && args.Key() == winrt::Windows::System::VirtualKey::A) {
+ const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)};
+ if (!utf16Text.empty()) {
+ if (auto root = rootComponentView()) {
+ root->ClearCurrentTextSelection();
+ }
+
+ SetSelection(0, static_cast(utf16Text.length()));
+
+ if (auto root = rootComponentView()) {
+ root->SetViewWithTextSelection(*get_strong());
+ }
+
+ DrawText();
+ args.Handled(true);
+ return;
+ }
+ }
+
+ Super::OnKeyDown(args);
+}
+
std::string ParagraphComponentView::DefaultControlType() const noexcept {
return "text";
}
@@ -307,6 +840,19 @@ std::string ParagraphComponentView::DefaultAccessibleName() const noexcept {
return m_attributedStringBox.getValue().getString();
}
+bool ParagraphComponentView::focusable() const noexcept {
+ // Text is focusable when it's selectable or when explicitly marked as focusable via props
+ return paragraphProps().isSelectable || viewProps()->focusable;
+}
+
+std::pair ParagraphComponentView::cursor() const noexcept {
+ // Returns I-beam cursor for selectable text
+ if (paragraphProps().isSelectable) {
+ return {facebook::react::Cursor::Text, nullptr};
+ }
+ return Super::cursor();
+}
+
winrt::Microsoft::ReactNative::ComponentView ParagraphComponentView::Create(
const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext,
facebook::react::Tag tag,
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.h
index 648b6e41a04..91290a80c12 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.h
@@ -12,6 +12,7 @@
#include
#include