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] |}>;