From aea7d981d6ee7d5c0286eaf9afaaefd29cdafbe2 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:37:39 -0700 Subject: [PATCH 1/9] overflow: hidden should prevent hittesting --- .../Fabric/Composition/CompositionViewComponentView.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index 8948a5bb4e1..71d6f2a2f89 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -1174,15 +1174,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(); } From f1e98059d777a5cde6feb23047bbb8bab4c8583c Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:46:14 -0700 Subject: [PATCH 2/9] Change files --- ...ative-windows-52161979-b14c-4bbe-b376-b292bbd03a4a.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-52161979-b14c-4bbe-b376-b292bbd03a4a.json 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..e7ea0fd6e89 --- /dev/null +++ b/change/react-native-windows-52161979-b14c-4bbe-b376-b292bbd03a4a.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "overflow: hidden should prevent hittesting", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} From 2436058442f998c79e2111735e18e547116e83c3 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:35:14 -0700 Subject: [PATCH 3/9] Add tests --- .../js/examples-win/HitTest/HitTestExample.js | 101 ++++++++++++++++++ .../src/js/utils/RNTesterList.windows.js | 5 + .../e2e-test-app-fabric/test/HitTest.test.ts | 69 ++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 packages/@react-native-windows/tester/src/js/examples-win/HitTest/HitTestExample.js create mode 100644 packages/e2e-test-app-fabric/test/HitTest.test.ts 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..3e37880ec51 --- /dev/null +++ b/packages/@react-native-windows/tester/src/js/examples-win/HitTest/HitTestExample.js @@ -0,0 +1,101 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * @format + */ + +'use strict'; + +import React from 'react'; + +import {View, Text, Pressable} from 'react-native'; +import {useState} from 'react'; + +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 (): React.Node { + 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 + + + + + ); + }, + }, + { + title: 'overflow hidden affects hit testing\n', + render: function (): React.Node { + 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 + + + + + ); + }, + }, +]; 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 c9c15047b6c..c3df1a9fdef 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 @@ -207,6 +207,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..d71450d11f0 --- /dev/null +++ b/packages/e2e-test-app-fabric/test/HitTest.test.ts @@ -0,0 +1,69 @@ +/** + * 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'); + }); + +}); From ec32e6d60bd23da6e8d8acb4da876143608f9a0c Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:04:29 -0700 Subject: [PATCH 4/9] snapshots + lint --- .../e2e-test-app-fabric/test/HitTest.test.ts | 20 +- .../__snapshots__/HomeUIADump.test.ts.snap | 301 ++++++++++------ .../PointerButtonComponentTest.test.ts.snap | 53 +++ .../PressableComponentTest.test.ts.snap | 4 +- .../TextComponentTest.test.ts.snap | 54 +-- .../TouchableComponentTest.test.ts.snap | 8 +- .../__snapshots__/snapshotPages.test.js.snap | 321 +++++++++++++++++- 7 files changed, 602 insertions(+), 159 deletions(-) create mode 100644 packages/e2e-test-app-fabric/test/__snapshots__/PointerButtonComponentTest.test.ts.snap diff --git a/packages/e2e-test-app-fabric/test/HitTest.test.ts b/packages/e2e-test-app-fabric/test/HitTest.test.ts index d71450d11f0..042943091be 100644 --- a/packages/e2e-test-app-fabric/test/HitTest.test.ts +++ b/packages/e2e-test-app-fabric/test/HitTest.test.ts @@ -5,10 +5,10 @@ * @format */ -import { app } from '@react-native-windows/automation'; -import { dumpVisualTree } from '@react-native-windows/automation-commands'; -import { goToComponentExample } from './RNTesterNavigation'; -import { verifyNoErrorLogs } from './Helpers'; +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 @@ -23,11 +23,10 @@ afterEach(async () => { async function verifyElementAccessibiltyValue(element: string, value: string) { const dump = await dumpVisualTree(element); - expect(dump!["Automation Tree"]["ValuePattern.Value"]).toBe(value); + 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'); @@ -36,13 +35,13 @@ describe('Hit Testing', () => { // 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 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, }); + 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'); @@ -55,15 +54,14 @@ describe('Hit Testing', () => { 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 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, }); + 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 83cd2b99f8a..6060ab66a55 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 @@ -1469,87 +1469,6 @@ exports[`Home UIA Tree Dump Custom Native Accessibility Example 1`] = ` } `; -exports[`Home UIA Tree Dump Cxx TurboModule 1`] = ` -{ - "Automation Tree": { - "AutomationId": "Cxx TurboModule", - "ControlType": 50026, - "IsKeyboardFocusable": true, - "LocalizedControlType": "group", - "Name": "Cxx TurboModule Usage of Cxx TurboModule", - "__Children": [ - { - "AutomationId": "", - "ControlType": 50020, - "LocalizedControlType": "text", - "Name": "Cxx TurboModule", - "TextRangePattern.GetText": "Cxx TurboModule", - }, - { - "AutomationId": "", - "ControlType": 50020, - "LocalizedControlType": "text", - "Name": "Usage of Cxx TurboModule", - "TextRangePattern.GetText": "Usage of Cxx TurboModule", - }, - ], - }, - "Component Tree": { - "Type": "Microsoft.ReactNative.Composition.ViewComponentView", - "_Props": { - "AccessibilityLabel": "Cxx TurboModule Usage of Cxx TurboModule", - "TestId": "Cxx TurboModule", - }, - "__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": "Cxx TurboModule", - "Offset": "0, 0, 0", - "Size": "966, 78", - "Visual Type": "SpriteVisual", - "__Children": [ - { - "Offset": "16, 16, 0", - "Size": "141, 25", - "Visual Type": "SpriteVisual", - "__Children": [ - { - "Offset": "0, 0, 0", - "Size": "141, 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 DevSettings 1`] = ` { "Automation Tree": { @@ -2264,12 +2183,12 @@ exports[`Home UIA Tree Dump Filter 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", }, ], @@ -2522,6 +2441,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 +2571,7 @@ exports[`Home UIA Tree Dump Image 1`] = ` }, "Comment": "Image", "Offset": "0, 0, 0", - "Size": "966, 77", + "Size": "966, 78", "Visual Type": "SpriteVisual", "__Children": [ { @@ -2669,12 +2669,12 @@ exports[`Home UIA Tree Dump Keyboard 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", }, ], @@ -2750,12 +2750,12 @@ exports[`Home UIA Tree Dump Keyboard 2`] = ` }, { "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", }, ], @@ -3057,7 +3057,7 @@ exports[`Home UIA Tree Dump Layout Events 1`] = ` }, "Comment": "Layout Events", "Offset": "0, 0, 0", - "Size": "966, 77", + "Size": "966, 78", "Visual Type": "SpriteVisual", "__Children": [ { @@ -3138,7 +3138,7 @@ exports[`Home UIA Tree Dump Legacy Native Module 1`] = ` }, "Comment": "Legacy Native Module", "Offset": "0, 0, 0", - "Size": "966, 78", + "Size": "966, 77", "Visual Type": "SpriteVisual", "__Children": [ { @@ -3398,12 +3398,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 +3479,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 +4029,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": [ { @@ -4127,12 +4127,12 @@ exports[`Home UIA Tree Dump Native Animated Example 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", }, ], @@ -4191,7 +4191,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": [ { @@ -4289,12 +4289,12 @@ exports[`Home UIA Tree Dump PanResponder Sample 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", }, ], @@ -4547,6 +4547,87 @@ exports[`Home UIA Tree Dump PlatformColor 1`] = ` } `; +exports[`Home UIA Tree Dump Pointer Button 1`] = ` +{ + "Automation Tree": { + "AutomationId": "Pointer Button", + "ControlType": 50026, + "IsKeyboardFocusable": true, + "LocalizedControlType": "group", + "Name": "Pointer Button Tests that PointerEvent.button and PointerEvent.buttons are correctly populated.", + "__Children": [ + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "Pointer Button", + "TextRangePattern.GetText": "Pointer Button", + }, + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "Tests that PointerEvent.button and PointerEvent.buttons are correctly populated.", + "TextRangePattern.GetText": "Tests that PointerEvent.button and PointerEvent.buttons are correctly populated.", + }, + ], + }, + "Component Tree": { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "AccessibilityLabel": "Pointer Button Tests that PointerEvent.button and PointerEvent.buttons are correctly populated.", + "TestId": "Pointer Button", + }, + "__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": "Pointer Button", + "Offset": "0, 0, 0", + "Size": "966, 78", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "16, 16, 0", + "Size": "115, 25", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "115, 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 Pointer Events 1`] = ` { "Automation Tree": { @@ -4937,12 +5018,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", }, ], @@ -5018,12 +5099,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", }, ], @@ -5597,7 +5678,7 @@ exports[`Home UIA Tree Dump Text 1`] = ` }, "Comment": "Text", "Offset": "0, 0, 0", - "Size": "966, 78", + "Size": "966, 77", "Visual Type": "SpriteVisual", "__Children": [ { @@ -5678,7 +5759,7 @@ exports[`Home UIA Tree Dump TextInput 1`] = ` }, "Comment": "TextInput", "Offset": "0, 0, 0", - "Size": "966, 77", + "Size": "966, 78", "Visual Type": "SpriteVisual", "__Children": [ { @@ -6169,12 +6250,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", }, ], @@ -6412,12 +6493,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__/PointerButtonComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/PointerButtonComponentTest.test.ts.snap new file mode 100644 index 00000000000..9ea6e8adb43 --- /dev/null +++ b/packages/e2e-test-app-fabric/test/__snapshots__/PointerButtonComponentTest.test.ts.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pointer Button Tests onPointerDown reports correct button property on left click 1`] = ` +{ + "Automation Tree": { + "AutomationId": "pointer-button-target", + "ControlType": 50026, + "LocalizedControlType": "group", + }, + "Component Tree": { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "TestId": "pointer-button-target", + }, + }, + "Visual Tree": { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 0, 255, 255)", + }, + "Comment": "pointer-button-target", + "Offset": "0, 0, 0", + "Size": "200, 200", + "Visual Type": "SpriteVisual", + }, +} +`; + +exports[`Pointer Button Tests onPointerUp reports correct button property on left click 1`] = ` +{ + "Automation Tree": { + "AutomationId": "pointer-up-button-target", + "ControlType": 50026, + "LocalizedControlType": "group", + }, + "Component Tree": { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "TestId": "pointer-up-button-target", + }, + }, + "Visual Tree": { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 0, 255, 255)", + }, + "Comment": "pointer-up-button-target", + "Offset": "0, 0, 0", + "Size": "200, 200", + "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 5e8c7116993..4d9b837e37b 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 @@ -1961,8 +1961,8 @@ exports[`Pressable Tests Text can have pressable behavior 1`] = ` { "Automation Tree": { "AutomationId": "tappable_text", - "ControlType": 50020, - "LocalizedControlType": "text", + "ControlType": 50005, + "LocalizedControlType": "link", "Name": "Text has built-in onPress handling", "TextRangePattern.GetText": "Text has built-in onPress handling", }, diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/TextComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/TextComponentTest.test.ts.snap index be1ba4318a3..9a870819a63 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/TextComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/TextComponentTest.test.ts.snap @@ -234,7 +234,7 @@ exports[`Text Tests Text can be restricted to one line 1`] = ` "Visual Tree": { "Comment": "text-one-line", "Offset": "0, 0, 0", - "Size": "300, 19", + "Size": "300, 20", "Visual Type": "SpriteVisual", }, } @@ -258,7 +258,7 @@ exports[`Text Tests Text can be selectable 1`] = ` "Visual Tree": { "Comment": "text-selectable", "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", }, } @@ -396,12 +396,12 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 39", + "Size": "916, 40", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 39", + "Size": "916, 40", "Visual Type": "SpriteVisual", "__Children": [ { @@ -437,7 +437,7 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "Color": "rgba(255, 0, 0, 255)", }, "Offset": "-10, 20, 0", - "Size": "10, 13", + "Size": "10, 14", "Visual Type": "SpriteVisual", }, { @@ -473,7 +473,7 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "Color": "rgba(255, 0, 0, 255)", }, "Offset": "0, 22, 0", - "Size": "20, 9", + "Size": "20, 10", "Visual Type": "SpriteVisual", }, ], @@ -482,12 +482,12 @@ exports[`Text Tests Text can have advanced borders 1`] = ` }, { "Offset": "0, 38, 0", - "Size": "916, 40", + "Size": "916, 39", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 40", + "Size": "916, 39", "Visual Type": "SpriteVisual", "__Children": [ { @@ -523,7 +523,7 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "Color": "rgba(0, 0, 255, 255)", }, "Offset": "-10, 20, 0", - "Size": "10, 14", + "Size": "10, 13", "Visual Type": "SpriteVisual", }, { @@ -559,7 +559,7 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "Color": "rgba(0, 0, 255, 255)", }, "Offset": "0, 22, 0", - "Size": "20, 10", + "Size": "20, 9", "Visual Type": "SpriteVisual", }, ], @@ -661,8 +661,8 @@ exports[`Text Tests Text can have an outer color 1`] = ` { "Automation Tree": { "AutomationId": "text-outer-color", - "ControlType": 50020, - "LocalizedControlType": "text", + "ControlType": 50005, + "LocalizedControlType": "link", "Name": "(Normal text,(R)red(G)green(B)blue(C)cyan(M)magenta(Y)yellow(K)black(and bold(and tiny bold italic blue(and tiny normal blue))))", "TextRangePattern.GetText": "(Normal text,(R)red(G)green(B)blue(C)cyan(M)magenta(Y)yellow(K)black(and bold(and tiny bold italic blue(and tiny normal blue))))", }, @@ -675,7 +675,7 @@ exports[`Text Tests Text can have an outer color 1`] = ` "Visual Tree": { "Comment": "text-outer-color", "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", }, } @@ -740,7 +740,7 @@ exports[`Text Tests Text can have borders 1`] = ` "Visual Tree": { "Comment": "text-border", "Offset": "0, 0, 0", - "Size": "916, 384", + "Size": "916, 385", "Visual Type": "SpriteVisual", "__Children": [ { @@ -853,12 +853,12 @@ exports[`Text Tests Text can have borders 1`] = ` }, { "Offset": "0, 365, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", }, ], @@ -886,7 +886,7 @@ exports[`Text Tests Text can have decoration lines: Solid Line Through 1`] = ` "Visual Tree": { "Comment": "text-decoration-solid-linethru", "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", }, } @@ -910,7 +910,7 @@ exports[`Text Tests Text can have decoration lines: Underline 1`] = ` "Visual Tree": { "Comment": "text-decoration-underline", "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", }, } @@ -953,7 +953,7 @@ exports[`Text Tests Text can have inline views/images 1`] = ` "Visual Tree": { "Comment": "text-view", "Offset": "0, 0, 0", - "Size": "916, 27", + "Size": "916, 26", "Visual Type": "SpriteVisual", "__Children": [ { @@ -1044,17 +1044,17 @@ exports[`Text Tests Text can have nested views 1`] = ` "Visual Tree": { "Comment": "text-nested-view", "Offset": "0, 0, 0", - "Size": "916, 41", + "Size": "916, 40", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", }, ], @@ -1230,17 +1230,17 @@ exports[`Text Tests Texts can clip inline View/Images 1`] = ` "Visual Tree": { "Comment": "text-view-images-clipped", "Offset": "0, 0, 0", - "Size": "916, 222", + "Size": "916, 223", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", }, ], @@ -1300,12 +1300,12 @@ exports[`Text Tests Texts can clip inline View/Images 1`] = ` }, { "Offset": "0, 103, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", }, ], diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/TouchableComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/TouchableComponentTest.test.ts.snap index e5670892632..dacfdc72efa 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/TouchableComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/TouchableComponentTest.test.ts.snap @@ -4,8 +4,8 @@ exports[`Touchable Tests Text components can be tappable 1`] = ` { "Automation Tree": { "AutomationId": "tappable_text", - "ControlType": 50020, - "LocalizedControlType": "text", + "ControlType": 50005, + "LocalizedControlType": "link", "Name": "Text has built-in onPress handling", "TextRangePattern.GetText": "Text has built-in onPress handling", }, @@ -120,10 +120,10 @@ exports[`Touchable Tests Touchables can be defined in a set using accessibilityP }, { "AutomationId": "", - "ControlType": 50020, + "ControlType": 50005, "IsKeyboardFocusable": true, "LiveSetting": "Assertive", - "LocalizedControlType": "text", + "LocalizedControlType": "link", "Name": "TouchableWithoutFeedback (Control 3 in Set of 3)", "TextRangePattern.GetText": "TouchableWithoutFeedback (Control 3 in Set of 3)", }, 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 d9fdfc834f3..2371b90f71a 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 @@ -26040,6 +26040,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`] = ` `; +exports[`snapshotAllPages Pointer Button 1`] = ` + + + Click the box to test pointer events + + + +`; + +exports[`snapshotAllPages Pointer Button 2`] = ` + + + Click the box to test pointer up events + + + +`; + exports[`snapshotAllPages Pointer Events 1`] = ` Move fast and be normal , + + Move fast and be italic, but just be longer so that you don't fit on a single line and make sure text is not truncated. + , ] `; @@ -75303,6 +75539,81 @@ exports[`snapshotAllPages Text 46`] = ` `; exports[`snapshotAllPages Text 47`] = ` + + + Link Text + + + + Nested Link + + + + Before + + + Nested Link + + After + + + + Nested Link 1 + + - + + Nested Link 2 + + + +`; + +exports[`snapshotAllPages Text 48`] = ` `; -exports[`snapshotAllPages Text 48`] = ` +exports[`snapshotAllPages Text 49`] = ` `; -exports[`snapshotAllPages Text 49`] = ` +exports[`snapshotAllPages Text 50`] = ` `; -exports[`snapshotAllPages Text 50`] = ` +exports[`snapshotAllPages Text 51`] = ` @@ -75560,7 +75871,7 @@ exports[`snapshotAllPages Text 50`] = ` `; -exports[`snapshotAllPages Text 51`] = ` +exports[`snapshotAllPages Text 52`] = ` `; -exports[`snapshotAllPages Text 52`] = ` +exports[`snapshotAllPages Text 53`] = ` Date: Wed, 25 Mar 2026 13:37:39 -0700 Subject: [PATCH 5/9] overflow: hidden should prevent hittesting --- .../Fabric/Composition/CompositionViewComponentView.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index 8948a5bb4e1..71d6f2a2f89 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -1174,15 +1174,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(); } From 98b7b2cd6e9064e6d2b46e85fdda2dd388207323 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:46:14 -0700 Subject: [PATCH 6/9] Change files --- ...ative-windows-52161979-b14c-4bbe-b376-b292bbd03a4a.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-52161979-b14c-4bbe-b376-b292bbd03a4a.json 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..e7ea0fd6e89 --- /dev/null +++ b/change/react-native-windows-52161979-b14c-4bbe-b376-b292bbd03a4a.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "overflow: hidden should prevent hittesting", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} From 9c2fb746373cb22a418a0787d2998b111c2f0c6c Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:35:14 -0700 Subject: [PATCH 7/9] Add tests --- .../js/examples-win/HitTest/HitTestExample.js | 101 ++++++++++++++++++ .../src/js/utils/RNTesterList.windows.js | 5 + .../e2e-test-app-fabric/test/HitTest.test.ts | 69 ++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 packages/@react-native-windows/tester/src/js/examples-win/HitTest/HitTestExample.js create mode 100644 packages/e2e-test-app-fabric/test/HitTest.test.ts 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..3e37880ec51 --- /dev/null +++ b/packages/@react-native-windows/tester/src/js/examples-win/HitTest/HitTestExample.js @@ -0,0 +1,101 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * @format + */ + +'use strict'; + +import React from 'react'; + +import {View, Text, Pressable} from 'react-native'; +import {useState} from 'react'; + +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 (): React.Node { + 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 + + + + + ); + }, + }, + { + title: 'overflow hidden affects hit testing\n', + render: function (): React.Node { + 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 + + + + + ); + }, + }, +]; 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 30164d87def..9fb20e81174 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 @@ -212,6 +212,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..d71450d11f0 --- /dev/null +++ b/packages/e2e-test-app-fabric/test/HitTest.test.ts @@ -0,0 +1,69 @@ +/** + * 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'); + }); + +}); From fdc6d2a1e3d281ff7b6ace55d32933762235a72e Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:04:29 -0700 Subject: [PATCH 8/9] snapshots + lint --- .../e2e-test-app-fabric/test/HitTest.test.ts | 20 +-- .../__snapshots__/HomeUIADump.test.ts.snap | 115 ++++++++++-- .../__snapshots__/snapshotPages.test.js.snap | 168 ++++++++++++++++++ 3 files changed, 275 insertions(+), 28 deletions(-) diff --git a/packages/e2e-test-app-fabric/test/HitTest.test.ts b/packages/e2e-test-app-fabric/test/HitTest.test.ts index d71450d11f0..042943091be 100644 --- a/packages/e2e-test-app-fabric/test/HitTest.test.ts +++ b/packages/e2e-test-app-fabric/test/HitTest.test.ts @@ -5,10 +5,10 @@ * @format */ -import { app } from '@react-native-windows/automation'; -import { dumpVisualTree } from '@react-native-windows/automation-commands'; -import { goToComponentExample } from './RNTesterNavigation'; -import { verifyNoErrorLogs } from './Helpers'; +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 @@ -23,11 +23,10 @@ afterEach(async () => { async function verifyElementAccessibiltyValue(element: string, value: string) { const dump = await dumpVisualTree(element); - expect(dump!["Automation Tree"]["ValuePattern.Value"]).toBe(value); + 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'); @@ -36,13 +35,13 @@ describe('Hit Testing', () => { // 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 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, }); + 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'); @@ -55,15 +54,14 @@ describe('Hit Testing', () => { 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 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, }); + 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 16457c7c5cf..6060ab66a55 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 @@ -2441,6 +2441,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": { @@ -2490,7 +2571,7 @@ exports[`Home UIA Tree Dump Image 1`] = ` }, "Comment": "Image", "Offset": "0, 0, 0", - "Size": "966, 77", + "Size": "966, 78", "Visual Type": "SpriteVisual", "__Children": [ { @@ -3317,12 +3398,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", }, ], @@ -3398,12 +3479,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", }, ], @@ -3948,7 +4029,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": [ { @@ -4110,7 +4191,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": [ { @@ -4937,12 +5018,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", }, ], @@ -5018,12 +5099,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", }, ], @@ -5597,7 +5678,7 @@ exports[`Home UIA Tree Dump Text 1`] = ` }, "Comment": "Text", "Offset": "0, 0, 0", - "Size": "966, 78", + "Size": "966, 77", "Visual Type": "SpriteVisual", "__Children": [ { @@ -5678,7 +5759,7 @@ exports[`Home UIA Tree Dump TextInput 1`] = ` }, "Comment": "TextInput", "Offset": "0, 0, 0", - "Size": "966, 77", + "Size": "966, 78", "Visual Type": "SpriteVisual", "__Children": [ { @@ -6169,12 +6250,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", }, ], @@ -6412,12 +6493,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 731a03eea1e..2371b90f71a 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 @@ -26040,6 +26040,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`] = ` Date: Fri, 27 Mar 2026 11:21:26 -0700 Subject: [PATCH 9/9] lint fix --- .../js/examples-win/HitTest/HitTestExample.js | 157 +++++++++--------- 1 file changed, 81 insertions(+), 76 deletions(-) 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 index 3e37880ec51..68558983a4e 100644 --- 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 @@ -7,9 +7,84 @@ 'use strict'; import React from 'react'; +import { View, Text, Pressable } from 'react-native'; -import {View, Text, Pressable} from 'react-native'; -import {useState} from 'react'; +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'; @@ -18,84 +93,14 @@ exports.description = 'Test that overflow hidden affect hit testing'; exports.examples = [ { title: 'overflow visible affects hit testing\n', - render: function (): React.Node { - 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 - - - - - ); + render: function () { + return ; }, }, { title: 'overflow hidden affects hit testing\n', - render: function (): React.Node { - 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 - - - - - ); + render: function () { + return ; }, }, ];