diff --git a/change/react-native-windows-2dd3b1fe-b27b-4762-9038-1d1e1d3a4ab0.json b/change/react-native-windows-2dd3b1fe-b27b-4762-9038-1d1e1d3a4ab0.json
new file mode 100644
index 00000000000..2b7577436b5
--- /dev/null
+++ b/change/react-native-windows-2dd3b1fe-b27b-4762-9038-1d1e1d3a4ab0.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "Add hideTitleBar and hideBorder to Modal",
+ "packageName": "react-native-windows",
+ "email": "30809111+acoates-ms@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/react-native-windows-4ad3ddaf-1358-4ff0-9971-38a7db68266c.json b/change/react-native-windows-4ad3ddaf-1358-4ff0-9971-38a7db68266c.json
new file mode 100644
index 00000000000..db40a35b9e0
--- /dev/null
+++ b/change/react-native-windows-4ad3ddaf-1358-4ff0-9971-38a7db68266c.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "fix: use IsPrimary() for touch pointer isPrimary instead of hardcoded pointer ID check",
+ "packageName": "react-native-windows",
+ "email": "gordomacmaster@gmail.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/react-native-windows-c470c550-399c-4439-80d3-4f8f1761cccb.json b/change/react-native-windows-c470c550-399c-4439-80d3-4f8f1761cccb.json
new file mode 100644
index 00000000000..97c3f806f53
--- /dev/null
+++ b/change/react-native-windows-c470c550-399c-4439-80d3-4f8f1761cccb.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "fix: cancel zombie touch state when ScrollView redirects pointer for manipulation, scope per-pointer events to the changed pointer, and remove always-true IsPointerWithinInitialTree fallback",
+ "packageName": "react-native-windows",
+ "email": "gordomacmaster@gmail.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/react-native-windows-f88fd0c5-a799-4c62-9e2d-edce4c369727.json b/change/react-native-windows-f88fd0c5-a799-4c62-9e2d-edce4c369727.json
new file mode 100644
index 00000000000..543a90787e7
--- /dev/null
+++ b/change/react-native-windows-f88fd0c5-a799-4c62-9e2d-edce4c369727.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "Image does not consistently rerender on image source change",
+ "packageName": "react-native-windows",
+ "email": "30809111+acoates-ms@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/@react-native-windows/tester/src/js/examples/Modal/ModalPresentation.windows.js b/packages/@react-native-windows/tester/src/js/examples/Modal/ModalPresentation.windows.js
index fc5840ecc51..791df342683 100644
--- a/packages/@react-native-windows/tester/src/js/examples/Modal/ModalPresentation.windows.js
+++ b/packages/@react-native-windows/tester/src/js/examples/Modal/ModalPresentation.windows.js
@@ -19,7 +19,15 @@ import {RNTesterThemeContext} from '../../components/RNTesterTheme';
import RNTOption from '../../components/RNTOption';
import * as React from 'react';
import {useCallback, useContext, useState} from 'react';
-import {Modal, Platform, StyleSheet, Switch, Text, View} from 'react-native';
+import {
+ Modal,
+ Platform,
+ StyleSheet,
+ Switch,
+ Text,
+ TextInput,
+ View,
+} from 'react-native';
const animationTypes = ['slide', 'none', 'fade'] as const;
const presentationStyles = [
@@ -69,6 +77,9 @@ function ModalPresentation() {
}),
onDismiss: undefined,
onShow: undefined,
+ hideBorder: false, // Windows
+ hideTitleBar: false, // Windows
+ title: 'Modal Presentation', // Windows
visible: false,
backdropColor: undefined,
});
@@ -190,6 +201,35 @@ function ModalPresentation() {
}
/>
+ {/* [Windows] - HideTitleBar is a Windows only prop. It is not supported on iOS or Android. */}
+
+ HideTitleBar
+
+ setProps(prev => ({...prev, hideTitleBar: enabled}))
+ }
+ />
+
+ {/* [Windows] - HideBorder is a Windows only prop. It is not supported on iOS or Android. */}
+
+ HideBorder
+
+ setProps(prev => ({...prev, hideBorder: enabled}))
+ }
+ />
+
+ {/* [Windows] - Title is a Windows only prop. It is not supported on iOS or Android. */}
+
+ Title
+ setProps(prev => ({...prev, title: text}))}
+ />
+
{Platform.OS === 'ios' && presentationStyle !== 'overFullScreen' ? (
iOS Modal can only be transparent with 'overFullScreen' Presentation
diff --git a/packages/e2e-test-app-fabric/test/TextInputComponentTest.test.ts b/packages/e2e-test-app-fabric/test/TextInputComponentTest.test.ts
index b065057aab0..090faa78ecb 100644
--- a/packages/e2e-test-app-fabric/test/TextInputComponentTest.test.ts
+++ b/packages/e2e-test-app-fabric/test/TextInputComponentTest.test.ts
@@ -30,6 +30,10 @@ const searchBox = async (input: string) => {
// than replace. Without the clear, a retry produces "onPressInonPressIn"
// and the comparison never converges.
await searchBox.clearValue();
+
+ // Do an extra wait here, since the autofocus textinput will move focus to itself and steal focus from the search box, causing the setValue to fail.
+ await new Promise(resolve => setTimeout(resolve, 500));
+
await searchBox.setValue(input);
return (await searchBox.getText()) === input;
},
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 1ee473d97bf..74d7cfa8844 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
@@ -31524,6 +31524,119 @@ exports[`snapshotAllPages Modal 1`] = `
value={false}
/>
+
+
+ HideTitleBar
+
+
+
+
+
+ HideBorder
+
+
+
+
+
+ Title
+
+
+
second);
+ m_activeTouches.erase(activeTouch);
+ if (cancelledTouchCopy.eventEmitter) {
+ DispatchSynthesizedTouchCancelForActiveTouch(cancelledTouchCopy, pointerPoint, keyModifiers);
+ }
+ }
}
void CompositionEventHandler::onPointerMoved(
@@ -1372,7 +1391,7 @@ void CompositionEventHandler::onPointerPressed(
UpdateActiveTouch(activeTouch, ptScaled, ptLocal);
- activeTouch.isPrimary = pointerId == 1;
+ activeTouch.isPrimary = pointerPoint.Properties().IsPrimary();
// Map the Windows pointer ID to a small identifier (0–19) safe for use as a JS array index.
// Windows touch IDs can be arbitrarily large (e.g. 2233), which causes React Native to warn
// and corrupts touch state, leaving Pressables stuck after a scroll.
@@ -1620,16 +1639,6 @@ bool CompositionEventHandler::IsPointerWithinInitialTree(const ActiveTouch &acti
currentView = currentView.Parent();
}
- // Fallback: if the pointer drifted spatially but the original target
- // is still structurally within the initial tree, honor the tap.
- // This provides touch-device tolerance for finger drift.
- auto targetView = viewRegistry.componentViewDescriptorWithTag(activeTouch.touch.target).view;
- while (targetView) {
- if (targetView.Tag() == initialTag)
- return true;
- targetView = targetView.Parent();
- }
-
return false;
}
@@ -1691,7 +1700,15 @@ void CompositionEventHandler::DispatchTouchEvent(
facebook::react::TouchEvent event;
- size_t index = 0;
+ // First pass: build changedTouches and the set of unique emitters from every active
+ // touch. The per-pointer PointerEvent dispatch (onPointerDown/Move/Up/Cancel/Click) is
+ // fired only for the touch whose state actually changed — non-changed touches contribute
+ // to the W3C TouchEvent's touches/targetTouches sets in the loops below but must not
+ // re-fire pointer events of their own. Previously we dispatched the per-pointer event
+ // for every entry in m_activeTouches, which produced duplicated onPointerMove on
+ // non-moving fingers and replayed onPointerUp/onClick on stale targets after the OS
+ // reclaimed a pointer (e.g. ScrollView manipulation redirect leaving a zombie touch).
+ const ActiveTouch *changedTouch = nullptr;
for (const auto &pair : m_activeTouches) {
const auto &activeTouch = pair.second;
@@ -1700,14 +1717,17 @@ void CompositionEventHandler::DispatchTouchEvent(
}
if (pair.first == pointerId) {
+ changedTouch = &activeTouch;
event.changedTouches.insert(activeTouch.touch);
}
uniqueEventEmitters.insert(activeTouch.eventEmitter);
+ }
- facebook::react::PointerEvent pointerEvent = CreatePointerEventFromActiveTouch(activeTouch, eventType);
+ if (changedTouch) {
+ facebook::react::PointerEvent pointerEvent = CreatePointerEventFromActiveTouch(*changedTouch, eventType);
winrt::Microsoft::ReactNative::ComponentView targetView{nullptr};
- bool shouldLeave = (eventType == TouchEventType::End && activeTouch.shouldLeaveWhenReleased) ||
+ bool shouldLeave = (eventType == TouchEventType::End && changedTouch->shouldLeaveWhenReleased) ||
eventType == TouchEventType::Cancel;
if (!shouldLeave) {
auto *rootViewForHit = RootComponentView();
@@ -1722,29 +1742,29 @@ void CompositionEventHandler::DispatchTouchEvent(
}
}
- auto handler = [this, &activeTouch, eventType, &pointerEvent](
+ auto handler = [this, changedTouch, eventType, &pointerEvent](
std::vector &eventPathViews) {
switch (eventType) {
case TouchEventType::Start:
- activeTouch.eventEmitter->onPointerDown(pointerEvent);
+ changedTouch->eventEmitter->onPointerDown(pointerEvent);
break;
case TouchEventType::Move: {
- activeTouch.eventEmitter->onPointerMove(pointerEvent);
+ changedTouch->eventEmitter->onPointerMove(pointerEvent);
break;
}
case TouchEventType::End:
- activeTouch.eventEmitter->onPointerUp(pointerEvent);
+ changedTouch->eventEmitter->onPointerUp(pointerEvent);
if (pointerEvent.isPrimary && pointerEvent.button == 0) {
- if (IsPointerWithinInitialTree(activeTouch)) {
- activeTouch.eventEmitter->onClick(pointerEvent);
+ if (IsPointerWithinInitialTree(*changedTouch)) {
+ changedTouch->eventEmitter->onClick(pointerEvent);
}
- } else if (IsPointerWithinInitialTree(activeTouch)) {
- activeTouch.eventEmitter->onAuxClick(pointerEvent);
+ } else if (IsPointerWithinInitialTree(*changedTouch)) {
+ changedTouch->eventEmitter->onAuxClick(pointerEvent);
}
break;
case TouchEventType::Cancel:
case TouchEventType::CaptureLost:
- activeTouch.eventEmitter->onPointerCancel(pointerEvent);
+ changedTouch->eventEmitter->onPointerCancel(pointerEvent);
break;
}
};
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp
index 1b21d3cf2d3..31d6c0e269f 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp
@@ -131,6 +131,7 @@ void ImageComponentView::didReceiveImage(const std::shared_ptrm_brushFactory) {
Visual().as().Brush(
m_imageResponseImage->m_brushFactory(m_reactContext.Handle(), m_compContext));
+ } else if (m_requiresImageRedraw) {
+ m_requiresImageRedraw = false;
+ DrawImage();
}
}
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h
index a63f8bc1443..0423a6c2447 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h
@@ -99,6 +99,7 @@ struct ImageComponentView : ImageComponentViewT m_imageResponseImage;
std::shared_ptr m_imageResponseObserver;
+ bool m_requiresImageRedraw{true};
facebook::react::ImageShadowNode::ConcreteState::Shared m_state;
};
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp
index 04f26782c47..5770ab3422d 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp
@@ -7,6 +7,7 @@
#include "../../../codegen/react/components/rnwcore/ModalHostView.g.h"
#include
+#include
#include
#include
#include
@@ -112,6 +113,12 @@ struct ModalHostView : public winrt::implements::UpdateProps(view, newProps, oldProps);
}
@@ -258,6 +265,61 @@ struct ModalHostView : public winrt::implementshideTitleBar.value_or(false) && m_localProps->hideBorder.value_or(false);
+
+ auto titleBar = m_rnWindow.AppWindow().TitleBar();
+ titleBar.ResetToDefault();
+ overlappedPresenter.IsResizable(false);
+
+ overlappedPresenter.SetBorderAndTitleBar(!hideBorders, !m_localProps->hideTitleBar.value_or(false));
+
+ auto hwnd = GetWindowFromWindowId(m_rnWindow.AppWindow().Id());
+
+ if (hideBorders) {
+ const DWMNCRENDERINGPOLICY ncPolicy = DWMNCRP_DISABLED;
+ ::DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &ncPolicy, sizeof(ncPolicy));
+
+ const DWM_WINDOW_CORNER_PREFERENCE cornerPref = DWMWCP_DEFAULT;
+ ::DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPref, sizeof(cornerPref));
+
+ const COLORREF zeroColor = 0;
+ ::DwmSetWindowAttribute(hwnd, DWMWA_BORDER_COLOR, &zeroColor, sizeof(zeroColor));
+ ::DwmSetWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, &zeroColor, sizeof(zeroColor));
+ ::DwmSetWindowAttribute(hwnd, DWMWA_TEXT_COLOR, &zeroColor, sizeof(zeroColor));
+ } else {
+ const DWMNCRENDERINGPOLICY ncPolicy = DWMNCRP_USEWINDOWSTYLE;
+ ::DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &ncPolicy, sizeof(ncPolicy));
+
+ const DWM_WINDOW_CORNER_PREFERENCE cornerPref = DWMWCP_DEFAULT;
+ ::DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPref, sizeof(cornerPref));
+
+ const COLORREF zeroColor = DWMWA_COLOR_DEFAULT;
+ ::DwmSetWindowAttribute(hwnd, DWMWA_BORDER_COLOR, &zeroColor, sizeof(zeroColor));
+ ::DwmSetWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, &zeroColor, sizeof(zeroColor));
+ ::DwmSetWindowAttribute(hwnd, DWMWA_TEXT_COLOR, &zeroColor, sizeof(zeroColor));
+
+ titleBar.IconShowOptions(winrt::Microsoft::UI::Windowing::IconShowOptions::HideIconAndSystemMenu);
+ }
+ }
+
// creates a new modal window
void EnsureModalCreated(const winrt::Microsoft::ReactNative::ComponentView &view) {
if (m_popUp) {
@@ -282,22 +344,8 @@ struct ModalHostView : public winrt::implementstitle.has_value()) {
diff --git a/vnext/codegen/react/components/rnwcore/ModalHostView.g.h b/vnext/codegen/react/components/rnwcore/ModalHostView.g.h
index d90331edf10..d3050db3d15 100644
--- a/vnext/codegen/react/components/rnwcore/ModalHostView.g.h
+++ b/vnext/codegen/react/components/rnwcore/ModalHostView.g.h
@@ -37,6 +37,8 @@ struct ModalHostViewProps : winrt::implementssupportedOrientations;
identifier = cloneFromProps->identifier;
title = cloneFromProps->title;
+ hideTitleBar = cloneFromProps->hideTitleBar;
+ hideBorder = cloneFromProps->hideBorder;
onRequestClose = cloneFromProps->onRequestClose;
onShow = cloneFromProps->onShow;
onDismiss = cloneFromProps->onDismiss;
@@ -84,6 +86,12 @@ struct ModalHostViewProps : winrt::implements title;
+ REACT_FIELD(hideTitleBar)
+ std::optional hideTitleBar{};
+
+ REACT_FIELD(hideBorder)
+ std::optional hideBorder{};
+
// These fields can be used to determine if JS has registered for this event
REACT_FIELD(onRequestClose)
bool onRequestClose{false};
diff --git a/vnext/codegen/react/components/rnwcore/Props.cpp b/vnext/codegen/react/components/rnwcore/Props.cpp
index b5f355391cb..8c975cac38d 100644
--- a/vnext/codegen/react/components/rnwcore/Props.cpp
+++ b/vnext/codegen/react/components/rnwcore/Props.cpp
@@ -477,7 +477,9 @@ ModalHostViewProps::ModalHostViewProps(
allowSwipeDismissal(convertRawProp(context, rawProps, "allowSwipeDismissal", sourceProps.allowSwipeDismissal, {false})),
supportedOrientations(convertRawProp(context, rawProps, "supportedOrientations", ModalHostViewSupportedOrientationsMaskWrapped{ .value = sourceProps.supportedOrientations }, {static_cast(ModalHostViewSupportedOrientations::Portrait)}).value),
identifier(convertRawProp(context, rawProps, "identifier", sourceProps.identifier, {0})),
- title(convertRawProp(context, rawProps, "title", sourceProps.title, {})) {}
+ title(convertRawProp(context, rawProps, "title", sourceProps.title, {})),
+ hideTitleBar(convertRawProp(context, rawProps, "hideTitleBar", sourceProps.hideTitleBar, {false})),
+ hideBorder(convertRawProp(context, rawProps, "hideBorder", sourceProps.hideBorder, {false})) {}
#ifdef RN_SERIALIZABLE_STATE
ComponentName ModalHostViewProps::getDiffPropsImplementationTarget() const {
@@ -542,6 +544,14 @@ folly::dynamic ModalHostViewProps::getDiffProps(
if (title != oldProps->title) {
result["title"] = title;
}
+
+ if (hideTitleBar != oldProps->hideTitleBar) {
+ result["hideTitleBar"] = hideTitleBar;
+ }
+
+ if (hideBorder != oldProps->hideBorder) {
+ result["hideBorder"] = hideBorder;
+ }
return result;
}
#endif
diff --git a/vnext/codegen/react/components/rnwcore/Props.h b/vnext/codegen/react/components/rnwcore/Props.h
index 726ded89f25..07de2c4cdbe 100644
--- a/vnext/codegen/react/components/rnwcore/Props.h
+++ b/vnext/codegen/react/components/rnwcore/Props.h
@@ -470,6 +470,8 @@ class ModalHostViewProps final : public ViewProps {
ModalHostViewSupportedOrientationsMask supportedOrientations{static_cast(ModalHostViewSupportedOrientations::Portrait)};
int identifier{0};
std::string title{};
+ bool hideTitleBar{false};
+ bool hideBorder{false};
#ifdef RN_SERIALIZABLE_STATE
ComponentName getDiffPropsImplementationTarget() const override;
diff --git a/vnext/src-win/Libraries/Modal/Modal.d.ts b/vnext/src-win/Libraries/Modal/Modal.d.ts
index 66dc9abe8a0..f345348bae9 100644
--- a/vnext/src-win/Libraries/Modal/Modal.d.ts
+++ b/vnext/src-win/Libraries/Modal/Modal.d.ts
@@ -117,6 +117,10 @@ export interface ModalWindowsProps {
/* title for the modal, shown in the title bar */
// [Windows
title?: string | undefined;
+
+ hideTitleBar?: boolean | undefined;
+
+ hideBorder?: boolean | undefined;
// Windows]
}
diff --git a/vnext/src-win/Libraries/Modal/Modal.windows.js b/vnext/src-win/Libraries/Modal/Modal.windows.js
index fcf82354ac7..aecbc1ad010 100644
--- a/vnext/src-win/Libraries/Modal/Modal.windows.js
+++ b/vnext/src-win/Libraries/Modal/Modal.windows.js
@@ -174,6 +174,8 @@ export type ModalPropsWindows = {
* [Windows] The `title` prop sets the title of the modal window.
*/
title?: ?string,
+ hideTitleBar?: ?boolean,
+ hideBorder?: ?boolean,
};
export type ModalProps = {
@@ -352,6 +354,8 @@ class Modal extends React.Component {
onOrientationChange={this.props.onOrientationChange}
allowSwipeDismissal={this.props.allowSwipeDismissal}
testID={this.props.testID}
+ hideTitleBar={this.props.hideTitleBar} // [Windows]
+ hideBorder={this.props.hideBorder} // [Windows]
title={this.props.title}>
diff --git a/vnext/src-win/src/private/specs_DEPRECATED/components/RCTModalHostViewNativeComponent.js b/vnext/src-win/src/private/specs_DEPRECATED/components/RCTModalHostViewNativeComponent.js
index 651fc3257e9..8808f4aa337 100644
--- a/vnext/src-win/src/private/specs_DEPRECATED/components/RCTModalHostViewNativeComponent.js
+++ b/vnext/src-win/src/private/specs_DEPRECATED/components/RCTModalHostViewNativeComponent.js
@@ -152,6 +152,10 @@ type RCTModalHostViewNativeProps = $ReadOnly<{|
*/
// [Windows
title?: WithDefault,
+
+ hideTitleBar?: WithDefault,
+
+ hideBorder?: WithDefault,
// Windows]
|}>;