diff --git a/change/react-native-windows-52161979-b14c-4bbe-b376-b292bbd03a4a.json b/change/react-native-windows-52161979-b14c-4bbe-b376-b292bbd03a4a.json
new file mode 100644
index 00000000000..0fcfae8520b
--- /dev/null
+++ b/change/react-native-windows-52161979-b14c-4bbe-b376-b292bbd03a4a.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "overflow: hidden should prevent hittesting",
+ "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-c4c264df-7619-4559-b43f-9761d5d68d63.json b/change/react-native-windows-c4c264df-7619-4559-b43f-9761d5d68d63.json
index a8131090345..c5921ba4196 100644
--- a/change/react-native-windows-c4c264df-7619-4559-b43f-9761d5d68d63.json
+++ b/change/react-native-windows-c4c264df-7619-4559-b43f-9761d5d68d63.json
@@ -1,7 +1,7 @@
{
- "type": "none",
+ "type": "patch",
"comment": "Implement button property",
"packageName": "react-native-windows",
"email": "hmalothu@microsoft.com",
"dependentChangeType": "none"
-}
+}
\ No newline at end of file
diff --git a/change/react-native-windows-edabe4dc-3296-42b9-952c-b3e3f7dc08d0.json b/change/react-native-windows-edabe4dc-3296-42b9-952c-b3e3f7dc08d0.json
new file mode 100644
index 00000000000..d6ba87986e9
--- /dev/null
+++ b/change/react-native-windows-edabe4dc-3296-42b9-952c-b3e3f7dc08d0.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Only show focus visuals when using keyboard to move focus",
+ "packageName": "react-native-windows",
+ "email": "30809111+acoates-ms@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
\ No newline at end of file
diff --git a/packages/@react-native-windows/tester/src/js/examples-win/HitTest/HitTestExample.js b/packages/@react-native-windows/tester/src/js/examples-win/HitTest/HitTestExample.js
new file mode 100644
index 00000000000..c4550934b32
--- /dev/null
+++ b/packages/@react-native-windows/tester/src/js/examples-win/HitTest/HitTestExample.js
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT License.
+ * @format
+ */
+
+'use strict';
+
+import React from 'react';
+import {View, Text, Pressable} from 'react-native';
+
+function HitTestWithOverflowVisibile() {
+ const [bgColor, setBgColor] = React.useState('red');
+
+ return (
+
+
+ Clicking the pressable should work even if it is outside the bounds of
+ its parent.
+
+
+
+ {
+ setBgColor(bgColor === 'red' ? 'green' : 'red');
+ }}>
+ Press me
+
+
+
+
+ );
+}
+
+function HitTestWithOverflowHidden() {
+ const [bgColor, setBgColor] = React.useState('red');
+ return (
+
+
+ Clicking within the visible view will trigger the pressable. Clicking
+ outside the bounds, where the pressable extends but is clipped by its
+ parent overflow:hidden, should not trigger the pressable.
+
+
+
+ {
+ setBgColor(bgColor === 'red' ? 'green' : 'red');
+ }}>
+ Press me
+
+
+
+
+ );
+}
+
+exports.displayName = 'HitTestExample';
+exports.title = 'Hit Testing';
+exports.category = 'Basic';
+exports.description = 'Test that overflow hidden affect hit testing';
+exports.examples = [
+ {
+ title: 'overflow visible affects hit testing\n',
+ render: function () {
+ return ;
+ },
+ },
+ {
+ title: 'overflow hidden affects hit testing\n',
+ render: function () {
+ 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 f581f42e88a..a0c915a3887 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
@@ -213,6 +213,11 @@ const Components: Array = [
key: 'LegacyTextHitTestTest',
module: require('../examples-win/LegacyTests/TextHitTestPage'),
},
+ {
+ key: 'HitTestExample',
+ category: 'UI',
+ module: require('../examples-win/HitTest/HitTestExample'),
+ },
{
key: 'PerformanceComparisonExample',
category: 'Basic',
diff --git a/packages/e2e-test-app-fabric/test/HitTest.test.ts b/packages/e2e-test-app-fabric/test/HitTest.test.ts
new file mode 100644
index 00000000000..042943091be
--- /dev/null
+++ b/packages/e2e-test-app-fabric/test/HitTest.test.ts
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT License.
+ *
+ * @format
+ */
+
+import {app} from '@react-native-windows/automation';
+import {dumpVisualTree} from '@react-native-windows/automation-commands';
+import {goToComponentExample} from './RNTesterNavigation';
+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('Hit Testing');
+});
+
+afterEach(async () => {
+ await verifyNoErrorLogs();
+});
+
+async function verifyElementAccessibiltyValue(element: string, value: string) {
+ const dump = await dumpVisualTree(element);
+ expect(dump!['Automation Tree']['ValuePattern.Value']).toBe(value);
+}
+
+describe('Hit Testing', () => {
+ test('Hit testing child outside the bounds of parents', async () => {
+ const target = await app.findElementByTestID('visible-overflow-element');
+
+ // View starts in red state
+ await verifyElementAccessibiltyValue('visible-overflow-element', 'red');
+
+ // The webdriverio package computes the offsets from the center point of the target.
+ // This is within the bounds of the child and the parent, so should hitTest even with overflow:visible
+ await target.click({x: -50, y: -50});
+
+ await verifyElementAccessibiltyValue('visible-overflow-element', 'green');
+
+ // The webdriverio package computes the offsets from the center point of the target.
+ // This is within the bounds of the child, but outside the parents bounds
+ await target.click({x: 0, y: 0});
+
+ // View should still be red, since the click should hit the pressable
+ await verifyElementAccessibiltyValue('visible-overflow-element', 'red');
+ });
+
+ test('Overflow hidden prevents hittesting child', async () => {
+ const target = await app.findElementByTestID('hidden-overflow-element');
+
+ // View starts in red state
+ await verifyElementAccessibiltyValue('hidden-overflow-element', 'red');
+
+ // This is within the bounds of the child and the parent, so should hitTest even with overflow:hidden
+ await target.click({x: -50, y: -50});
+
+ await verifyElementAccessibiltyValue('hidden-overflow-element', 'green');
+
+ // This is within the bounds of the child, but shouldn't hit test, since the parent is overflow:hidden
+ await target.click({x: 0, y: 0});
+
+ // View should still be green, since the click shouldn't hit the pressable
+ await verifyElementAccessibiltyValue('hidden-overflow-element', 'green');
+ });
+});
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 9b7af269a2c..8215378d4ca 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
@@ -2522,6 +2522,87 @@ exports[`Home UIA Tree Dump Glyph UWP 1`] = `
}
`;
+exports[`Home UIA Tree Dump Hit Testing 1`] = `
+{
+ "Automation Tree": {
+ "AutomationId": "Hit Testing",
+ "ControlType": 50026,
+ "IsKeyboardFocusable": true,
+ "LocalizedControlType": "group",
+ "Name": "Hit Testing Test that overflow hidden affect hit testing",
+ "__Children": [
+ {
+ "AutomationId": "",
+ "ControlType": 50020,
+ "LocalizedControlType": "text",
+ "Name": "Hit Testing",
+ "TextRangePattern.GetText": "Hit Testing",
+ },
+ {
+ "AutomationId": "",
+ "ControlType": 50020,
+ "LocalizedControlType": "text",
+ "Name": "Test that overflow hidden affect hit testing",
+ "TextRangePattern.GetText": "Test that overflow hidden affect hit testing",
+ },
+ ],
+ },
+ "Component Tree": {
+ "Type": "Microsoft.ReactNative.Composition.ViewComponentView",
+ "_Props": {
+ "AccessibilityLabel": "Hit Testing Test that overflow hidden affect hit testing",
+ "TestId": "Hit Testing",
+ },
+ "__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": "Hit Testing",
+ "Offset": "0, 0, 0",
+ "Size": "966, 77",
+ "Visual Type": "SpriteVisual",
+ "__Children": [
+ {
+ "Offset": "16, 16, 0",
+ "Size": "85, 25",
+ "Visual Type": "SpriteVisual",
+ "__Children": [
+ {
+ "Offset": "0, 0, 0",
+ "Size": "85, 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 Image 1`] = `
{
"Automation Tree": {
@@ -2571,7 +2652,7 @@ exports[`Home UIA Tree Dump Image 1`] = `
},
"Comment": "Image",
"Offset": "0, 0, 0",
- "Size": "966, 77",
+ "Size": "966, 78",
"Visual Type": "SpriteVisual",
"__Children": [
{
@@ -3398,12 +3479,12 @@ exports[`Home UIA Tree Dump LegacyLoginTest 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",
},
],
@@ -3479,12 +3560,12 @@ exports[`Home UIA Tree Dump LegacySelectableTextTest 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",
},
],
@@ -4029,7 +4110,7 @@ exports[`Home UIA Tree Dump Moving Light Example 1`] = `
},
"Comment": "Moving Light Example",
"Offset": "0, 0, 0",
- "Size": "966, 78",
+ "Size": "966, 77",
"Visual Type": "SpriteVisual",
"__Children": [
{
@@ -4191,7 +4272,7 @@ exports[`Home UIA Tree Dump New App Screen 1`] = `
},
"Comment": "New App Screen",
"Offset": "0, 0, 0",
- "Size": "966, 77",
+ "Size": "966, 78",
"Visual Type": "SpriteVisual",
"__Children": [
{
@@ -5018,12 +5099,12 @@ exports[`Home UIA Tree Dump ScrollView 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",
},
],
@@ -5099,12 +5180,12 @@ exports[`Home UIA Tree Dump ScrollViewAnimated 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",
},
],
@@ -5678,7 +5759,7 @@ exports[`Home UIA Tree Dump Text 1`] = `
},
"Comment": "Text",
"Offset": "0, 0, 0",
- "Size": "966, 78",
+ "Size": "966, 77",
"Visual Type": "SpriteVisual",
"__Children": [
{
@@ -5759,7 +5840,7 @@ exports[`Home UIA Tree Dump TextInput 1`] = `
},
"Comment": "TextInput",
"Offset": "0, 0, 0",
- "Size": "966, 77",
+ "Size": "966, 78",
"Visual Type": "SpriteVisual",
"__Children": [
{
@@ -6250,12 +6331,12 @@ exports[`Home UIA Tree Dump TransparentHitTestExample 1`] = `
"__Children": [
{
"Offset": "16, 16, 0",
- "Size": "214, 25",
+ "Size": "214, 24",
"Visual Type": "SpriteVisual",
"__Children": [
{
"Offset": "0, 0, 0",
- "Size": "214, 25",
+ "Size": "214, 24",
"Visual Type": "SpriteVisual",
},
],
@@ -6493,12 +6574,12 @@ exports[`Home UIA Tree Dump View 1`] = `
"__Children": [
{
"Offset": "16, 16, 0",
- "Size": "38, 24",
+ "Size": "38, 25",
"Visual Type": "SpriteVisual",
"__Children": [
{
"Offset": "0, 0, 0",
- "Size": "38, 24",
+ "Size": "38, 25",
"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 f26b6db5f4e..e1c08116c4c 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
@@ -25908,6 +25908,174 @@ exports[`snapshotAllPages Glyph UWP 1`] = `
`;
+exports[`snapshotAllPages Hit Testing 1`] = `
+
+
+ Clicking the pressable should work even if it is outside the bounds of its parent.
+
+
+
+
+
+ Press me
+
+
+
+
+
+`;
+
+exports[`snapshotAllPages Hit Testing 2`] = `
+
+
+ Clicking within the visible view will trigger the pressable. Clicking outside the bounds, where the pressable extends but is clipped by its parent overflow:hidden, should not trigger the pressable.
+
+
+
+
+
+ Press me
+
+
+
+
+
+`;
+
exports[`snapshotAllPages Keyboard 1`] = `
KeyDown;
diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp
index 4ac8beda5d5..f47bc05e67e 100644
--- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp
@@ -283,6 +283,7 @@ void ComponentView::parent(const winrt::Microsoft::ReactNative::ComponentView &p
oldRootView->TrySetFocusedComponent(
oldParent,
winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
+ winrt::Microsoft::ReactNative::FocusState::Programmatic,
true /*forceNoSelectionIfCannotMove*/);
}
}
@@ -431,9 +432,10 @@ void ComponentView::GotFocus(winrt::event_token const &token) noexcept {
m_gotFocusEvent.remove(token);
}
-bool ComponentView::TryFocus() noexcept {
+bool ComponentView::TryFocus(winrt::Microsoft::ReactNative::FocusState focusState) noexcept {
if (auto root = rootComponentView()) {
- return root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
+ return root->TrySetFocusedComponent(
+ *get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None, focusState);
}
return false;
diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h
index 13eb1f8f322..25c62c81e8c 100644
--- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h
@@ -201,7 +201,7 @@ struct ComponentView
LayoutMetrics LayoutMetrics() const noexcept;
- bool TryFocus() noexcept;
+ bool TryFocus(winrt::Microsoft::ReactNative::FocusState focusState) noexcept;
virtual bool focusable() const noexcept;
virtual facebook::react::SharedViewEventEmitter eventEmitterAtPoint(facebook::react::Point pt) noexcept;
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp
index 96f640af06f..80b5362c3e5 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp
@@ -614,6 +614,8 @@ int64_t CompositionEventHandler::SendMessage(HWND hwnd, uint32_t msg, uint64_t w
void CompositionEventHandler::onKeyDown(
const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept {
+ RootComponentView().UseKeyboardForProgrammaticFocus(true);
+
if (auto focusedComponent = RootComponentView().GetFocusedComponent()) {
winrt::get_self(focusedComponent)->OnKeyDown(args);
@@ -637,7 +639,7 @@ void CompositionEventHandler::onKeyDown(
}
if (!fCtrl && args.Key() == winrt::Windows::System::VirtualKey::Tab) {
- if (RootComponentView().TryMoveFocus(!fShift)) {
+ if (RootComponentView().TryMoveFocus(!fShift, winrt::Microsoft::ReactNative::FocusState::Keyboard)) {
args.Handled(true);
}
@@ -647,6 +649,8 @@ void CompositionEventHandler::onKeyDown(
void CompositionEventHandler::onKeyUp(
const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept {
+ RootComponentView().UseKeyboardForProgrammaticFocus(true);
+
if (auto focusedComponent = RootComponentView().GetFocusedComponent()) {
winrt::get_self(focusedComponent)->OnKeyUp(args);
@@ -1179,6 +1183,8 @@ void CompositionEventHandler::onPointerPressed(
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept {
namespace Composition = winrt::Microsoft::ReactNative::Composition;
+ RootComponentView().UseKeyboardForProgrammaticFocus(false);
+
// Clears any active text selection when left pointer is pressed
if (pointerPoint.Properties().PointerUpdateKind() != Composition::Input::PointerUpdateKind::RightButtonPressed) {
RootComponentView().ClearCurrentTextSelection();
@@ -1275,6 +1281,8 @@ void CompositionEventHandler::onPointerReleased(
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept {
int pointerId = pointerPoint.PointerId();
+ RootComponentView().UseKeyboardForProgrammaticFocus(false);
+
auto activeTouch = std::find_if(m_activeTouches.begin(), m_activeTouches.end(), [pointerId](const auto &pair) {
return pair.second.touch.identifier == pointerId;
});
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp
index 16169eac4ac..9b2ddbfbd22 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp
@@ -375,7 +375,8 @@ void ComponentView::onGotFocus(
const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept {
if (args.OriginalSource() == Tag()) {
m_eventEmitter->onFocus();
- if (viewProps()->enableFocusRing) {
+ if (viewProps()->enableFocusRing &&
+ rootComponentView()->focusState() == winrt::Microsoft::ReactNative::FocusState::Keyboard) {
facebook::react::Rect focusRect = m_layoutMetrics.frame;
focusRect.origin.x -= (FOCUS_VISUAL_WIDTH * 2);
focusRect.origin.y -= (FOCUS_VISUAL_WIDTH * 2);
@@ -428,15 +429,20 @@ void ComponentView::HandleCommand(const winrt::Microsoft::ReactNative::HandleCom
auto commandName = args.CommandName();
if (commandName == L"focus") {
if (auto root = rootComponentView()) {
- root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
+ root->TrySetFocusedComponent(
+ *get_strong(),
+ winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
+ winrt::Microsoft::ReactNative::FocusState::Programmatic);
}
return;
}
if (commandName == L"blur") {
if (auto root = rootComponentView()) {
root->TrySetFocusedComponent(
- nullptr, winrt::Microsoft::ReactNative::FocusNavigationDirection::None); // Todo store this component as
- // previously focused element
+ nullptr,
+ winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
+ winrt::Microsoft::ReactNative::FocusState::Programmatic); // Todo store this component as
+ // previously focused element
}
return;
}
@@ -1176,15 +1182,17 @@ facebook::react::Tag ViewComponentView::hitTest(
facebook::react::Tag targetTag = -1;
+ bool isPointInside = ptLocal.x >= 0 && ptLocal.x <= m_layoutMetrics.frame.size.width && ptLocal.y >= 0 &&
+ ptLocal.y <= m_layoutMetrics.frame.size.height;
+
if ((ignorePointerEvents || m_props->pointerEvents == facebook::react::PointerEventsMode::Auto ||
m_props->pointerEvents == facebook::react::PointerEventsMode::BoxNone) &&
- anyHitTestHelper(targetTag, ptLocal, localPt))
+ (isPointInside || !viewProps()->getClipsContentToBounds()) && anyHitTestHelper(targetTag, ptLocal, localPt))
return targetTag;
if ((ignorePointerEvents || m_props->pointerEvents == facebook::react::PointerEventsMode::Auto ||
m_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) {
+ isPointInside) {
localPt = ptLocal;
return Tag();
}
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp
index 47a4f1e43c7..5ff37988bce 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp
@@ -83,7 +83,7 @@ void ContentIslandComponentView::ConnectInternal() noexcept {
m_navigationHost.DepartFocusRequested([wkThis = get_weak()](const auto &, const auto &args) {
if (auto strongThis = wkThis.get()) {
const bool next = (args.Request().Reason() != winrt::Microsoft::UI::Input::FocusNavigationReason::Last);
- strongThis->rootComponentView()->TryMoveFocus(next);
+ strongThis->rootComponentView()->TryMoveFocus(next, winrt::Microsoft::ReactNative::FocusState::Programmatic);
args.Result(winrt::Microsoft::UI::Input::FocusNavigationResult::Moved);
}
});
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp
index 625ef17e26c..e20ff718107 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp
@@ -613,7 +613,10 @@ void ParagraphComponentView::OnPointerPressed(
// Focuses so we receive onLostFocus when clicking elsewhere
if (auto root = rootComponentView()) {
- root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
+ root->TrySetFocusedComponent(
+ *get_strong(),
+ winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
+ winrt::Microsoft::ReactNative::FocusState::Pointer);
}
args.Handled(true);
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp
index d666530456b..b2a6b887ee3 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp
@@ -82,7 +82,8 @@ winrt::Microsoft::ReactNative::ComponentView RootComponentView::GetFocusedCompon
void RootComponentView::SetFocusedComponent(
const winrt::Microsoft::ReactNative::ComponentView &value,
- winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept {
+ winrt::Microsoft::ReactNative::FocusNavigationDirection direction,
+ winrt::Microsoft::ReactNative::FocusState focusState) noexcept {
if (m_focusedComponent == value)
return;
@@ -97,11 +98,26 @@ void RootComponentView::SetFocusedComponent(
winrt::get_self(rootView)->TrySetFocus();
}
m_focusedComponent = value;
+ if (focusState == winrt::Microsoft::ReactNative::FocusState::Programmatic) {
+ focusState =
+ (!m_useKeyboardForProgrammaticFocus || m_focusState == winrt::Microsoft::ReactNative::FocusState::Pointer)
+ ? winrt::Microsoft::ReactNative::FocusState::Pointer
+ : winrt::Microsoft::ReactNative::FocusState::Keyboard;
+ }
+ m_focusState = focusState;
auto args = winrt::make(value, direction);
winrt::get_self(value)->onGotFocus(args);
}
}
+winrt::Microsoft::ReactNative::FocusState RootComponentView::focusState() const noexcept {
+ return m_focusState;
+}
+
+void RootComponentView::UseKeyboardForProgrammaticFocus(bool value) noexcept {
+ m_useKeyboardForProgrammaticFocus = value;
+}
+
bool RootComponentView::NavigateFocus(const winrt::Microsoft::ReactNative::FocusNavigationRequest &request) noexcept {
if (request.Reason() == winrt::Microsoft::ReactNative::FocusNavigationReason::Restore) {
if (m_focusedComponent)
@@ -116,7 +132,8 @@ bool RootComponentView::NavigateFocus(const winrt::Microsoft::ReactNative::Focus
view,
request.Reason() == winrt::Microsoft::ReactNative::FocusNavigationReason::First
? winrt::Microsoft::ReactNative::FocusNavigationDirection::First
- : winrt::Microsoft::ReactNative::FocusNavigationDirection::Last);
+ : winrt::Microsoft::ReactNative::FocusNavigationDirection::Last,
+ winrt::Microsoft::ReactNative::FocusState::Programmatic);
}
return view != nullptr;
}
@@ -124,6 +141,7 @@ bool RootComponentView::NavigateFocus(const winrt::Microsoft::ReactNative::Focus
bool RootComponentView::TrySetFocusedComponent(
const winrt::Microsoft::ReactNative::ComponentView &view,
winrt::Microsoft::ReactNative::FocusNavigationDirection direction,
+ winrt::Microsoft::ReactNative::FocusState focusState,
bool forceNoSelectionIfCannotMove /*= false*/) noexcept {
auto target = view;
auto selfView = winrt::get_self(target);
@@ -157,15 +175,15 @@ bool RootComponentView::TrySetFocusedComponent(
winrt::get_self(losingFocusArgs.NewFocusedComponent())
->rootComponentView()
- ->SetFocusedComponent(gettingFocusArgs.NewFocusedComponent(), direction);
+ ->SetFocusedComponent(gettingFocusArgs.NewFocusedComponent(), direction, focusState);
} else {
- SetFocusedComponent(nullptr, direction);
+ SetFocusedComponent(nullptr, direction, focusState);
}
return true;
}
-bool RootComponentView::TryMoveFocus(bool next) noexcept {
+bool RootComponentView::TryMoveFocus(bool next, winrt::Microsoft::ReactNative::FocusState focusState) noexcept {
if (!m_focusedComponent) {
return NavigateFocus(winrt::Microsoft::ReactNative::FocusNavigationRequest(
next ? winrt::Microsoft::ReactNative::FocusNavigationReason::First
@@ -173,7 +191,8 @@ bool RootComponentView::TryMoveFocus(bool next) noexcept {
}
Mso::Functor fn =
- [currentlyFocused = m_focusedComponent, next](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
+ [currentlyFocused = m_focusedComponent, next, focusState](
+ const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
if (view == currentlyFocused)
return false;
auto selfView = winrt::get_self(view);
@@ -185,7 +204,8 @@ bool RootComponentView::TryMoveFocus(bool next) noexcept {
->TrySetFocusedComponent(
view,
next ? winrt::Microsoft::ReactNative::FocusNavigationDirection::Next
- : winrt::Microsoft::ReactNative::FocusNavigationDirection::Previous);
+ : winrt::Microsoft::ReactNative::FocusNavigationDirection::Previous,
+ focusState);
};
if (winrt::Microsoft::ReactNative::implementation::walkTree(m_focusedComponent, next, fn)) {
@@ -249,7 +269,10 @@ void RootComponentView::start(const winrt::Microsoft::ReactNative::ReactNativeIs
}
void RootComponentView::stop() noexcept {
- SetFocusedComponent(nullptr, winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
+ SetFocusedComponent(
+ nullptr,
+ winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
+ winrt::Microsoft::ReactNative::FocusState::Programmatic);
if (m_visualAddedToIsland) {
if (auto rootView = m_wkRootView.get()) {
winrt::get_self(rootView)->RemoveRenderedVisual(
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h
index 58b6f1ae2d4..977eb8394b1 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h
@@ -30,15 +30,17 @@ struct RootComponentView : RootComponentViewT m_wkRootView{nullptr};
winrt::weak_ref m_wkPortal{nullptr};
bool m_visualAddedToIsland{false};
+ bool m_useKeyboardForProgrammaticFocus{true};
::Microsoft::ReactNative::ReactTaggedView m_viewWithTextSelection{
winrt::Microsoft::ReactNative::ComponentView{nullptr}};
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/SwitchComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/SwitchComponentView.cpp
index 6adffd46183..78412a4cb2d 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/SwitchComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/SwitchComponentView.cpp
@@ -271,7 +271,10 @@ void SwitchComponentView::OnPointerPressed(
m_supressAnimationForNextFrame = true;
if (auto root = rootComponentView()) {
- root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
+ root->TrySetFocusedComponent(
+ *get_strong(),
+ winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
+ winrt::Microsoft::ReactNative::FocusState::Pointer);
}
updateVisuals();
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp
index 843c42e8d2f..59cbe03aa61 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp
@@ -233,7 +233,9 @@ struct CompTextHost : public winrt::implements {
winrt::check_hresult(
m_outer->QueryInterface(winrt::guid_of(), winrt::put_abi(view)));
m_outer->rootComponentView()->TrySetFocusedComponent(
- view, winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
+ view,
+ winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
+ winrt::Microsoft::ReactNative::FocusState::Programmatic);
// assert(false);
// TODO focus
}
@@ -1469,7 +1471,10 @@ void WindowsTextInputComponentView::onMounted() noexcept {
// Handle autoFocus property - focus the component when mounted if autoFocus is true
if (windowsTextInputProps().autoFocus) {
if (auto root = rootComponentView()) {
- root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
+ root->TrySetFocusedComponent(
+ *get_strong(),
+ winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
+ winrt::Microsoft::ReactNative::FocusState::Programmatic);
}
}
}
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp
index 6cbb13c4443..cd2084a18ed 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp
@@ -141,7 +141,10 @@ HRESULT UiaSetFocusHelper(::Microsoft::ReactNative::ReactTaggedView &view) noexc
if (rootCV == nullptr)
return UIA_E_ELEMENTNOTAVAILABLE;
- return rootCV->TrySetFocusedComponent(strongView, winrt::Microsoft::ReactNative::FocusNavigationDirection::None)
+ return rootCV->TrySetFocusedComponent(
+ strongView,
+ winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
+ winrt::Microsoft::ReactNative::FocusState::Programmatic)
? S_OK
: E_FAIL;
}