From 95d61234b6de0ad8958b46e086bdc5cce9272f59 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 29 May 2025 18:43:20 -0700 Subject: [PATCH 01/36] Pointer events (#14713) * PointerEvent fixes * Change files * format --- ...-1ff7a358-a5fe-45d6-8df4-ecd913ae9509.json | 7 + .../Composition/CompositionEventHandler.cpp | 175 ++++++++---------- .../Composition/CompositionEventHandler.h | 2 +- .../view/HostPlatformViewEventEmitter.cpp | 36 ++-- .../view/HostPlatformViewEventEmitter.h | 19 +- .../renderer/components/view/MouseEvent.h | 20 ++ 6 files changed, 146 insertions(+), 113 deletions(-) create mode 100644 change/react-native-windows-1ff7a358-a5fe-45d6-8df4-ecd913ae9509.json create mode 100644 vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/MouseEvent.h diff --git a/change/react-native-windows-1ff7a358-a5fe-45d6-8df4-ecd913ae9509.json b/change/react-native-windows-1ff7a358-a5fe-45d6-8df4-ecd913ae9509.json new file mode 100644 index 00000000000..0bc83ccc34c --- /dev/null +++ b/change/react-native-windows-1ff7a358-a5fe-45d6-8df4-ecd913ae9509.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "PointerEvent fixes", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp index 8b5f12b04df..6e662f618d8 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -637,17 +636,6 @@ void CompositionEventHandler::HandleIncomingPointerEvent( auto eventPathViews = GetTouchableViewsInPathToRoot(targetView); - // Over - if (targetView != nullptr && previousTargetTag != targetView.Tag()) { - bool shouldEmitOverEvent = - IsAnyViewInPathListeningToEvent(eventPathViews, facebook::react::ViewEvents::Offset::PointerOver); - const auto eventEmitter = winrt::get_self(targetView) - ->eventEmitterAtPoint(event.offsetPoint); - if (shouldEmitOverEvent && eventEmitter != nullptr) { - eventEmitter->onPointerOver(event); - } - } - // Entering // We only want to emit events to JS if there is a view that is currently listening to said event @@ -664,7 +652,6 @@ void CompositionEventHandler::HandleIncomingPointerEvent( auto componentView = *itComponentView; bool shouldEmitEvent = componentView != nullptr && (hasParentEnterListener || - IsViewListeningToEvent(componentView, facebook::react::ViewEvents::Offset::PointerEnter) || IsViewListeningToEvent(componentView, facebook::react::WindowsViewEvents::Offset::MouseEnter)); if (std::find(currentlyHoveredViews.begin(), currentlyHoveredViews.end(), componentView) == @@ -674,16 +661,12 @@ void CompositionEventHandler::HandleIncomingPointerEvent( m_context, componentView.Tag(), pointerPoint, keyModifiers); winrt::get_self(componentView) ->OnPointerEntered(args); - if (shouldEmitEvent) { const auto eventEmitter = winrt::get_self(componentView) ->eventEmitter(); - if (eventEmitter) { - eventEmitter->onPointerEnter(event); - if (IsMousePointerEvent(event)) { - eventEmitter->onMouseEnter(event); - } + if (eventEmitter && IsMousePointerEvent(event)) { + eventEmitter->onMouseEnter(event); } } } @@ -696,26 +679,13 @@ void CompositionEventHandler::HandleIncomingPointerEvent( // Call the underlaying pointer handler handler(eventPathViews); - // Out - if (previousTargetTag != -1 && previousTargetTag != (targetView ? targetView.Tag() : -1)) { - bool shouldEmitOutEvent = - IsAnyViewInPathListeningToEvent(currentlyHoveredViews, facebook::react::ViewEvents::Offset::PointerOut); - const auto eventEmitter = - winrt::get_self(prevTargetView)->eventEmitter(); - if (shouldEmitOutEvent && eventEmitter != nullptr) { - eventEmitter->onPointerOut(event); - } - } - // Leaving // pointerleave events need to be emitted from the deepest target to the root but // we also need to efficiently keep track of if a view has a parent which is listening to the leave events, // so we first iterate from the root to the target, collecting the views which need events fired for, of which // we reverse iterate (now from target to root), actually emitting the events. - std::vector - viewsToEmitJSLeaveEventsTo; // NSMutableOrderedSet *viewsToEmitLeaveEventsTo = - // [NSMutableOrderedSet orderedSet]; + std::vector viewsToEmitJSLeaveEventsTo; std::vector viewsToEmitLeaveEventsTo; @@ -723,14 +693,11 @@ void CompositionEventHandler::HandleIncomingPointerEvent( bool hasParentLeaveListener = false; for (auto itComponentView = currentlyHoveredViews.rbegin(); itComponentView != currentlyHoveredViews.rend(); - itComponentView++) { // for (RCTReactTaggedView *taggedView in [currentlyHoveredViews - // reverseObjectEnumerator]) - // { + itComponentView++) { auto componentView = *itComponentView; bool shouldEmitJSEvent = componentView != nullptr && (hasParentLeaveListener || - IsViewListeningToEvent(componentView, facebook::react::ViewEvents::Offset::PointerLeave) || IsViewListeningToEvent(componentView, facebook::react::WindowsViewEvents::Offset::MouseLeave)); if (std::find(eventPathViews.begin(), eventPathViews.end(), componentView) == eventPathViews.end()) { @@ -755,17 +722,13 @@ void CompositionEventHandler::HandleIncomingPointerEvent( } for (auto itComponentView = viewsToEmitJSLeaveEventsTo.rbegin(); itComponentView != viewsToEmitJSLeaveEventsTo.rend(); - itComponentView++) { // for (UIView *componentView in [viewsToEmitJSLeaveEventsTo - // reverseObjectEnumerator]) { + itComponentView++) { auto componentView = *itComponentView; const auto eventEmitter = winrt::get_self(componentView)->eventEmitter(); - if (eventEmitter) { - eventEmitter->onPointerLeave(event); - if (IsMousePointerEvent(event)) { - eventEmitter->onMouseLeave(event); - } + if (eventEmitter && IsMousePointerEvent(event)) { + eventEmitter->onMouseLeave(event); } } @@ -815,41 +778,39 @@ void CompositionEventHandler::SetCursor(facebook::react::Cursor cursor, HCURSOR case facebook::react::Cursor::Pointer: type = winrt::Windows::UI::Core::CoreCursorType::Hand; break; - /* -- Additional cursors not added in core until later version - case facebook::react::Cursor::Help: - type = winrt::Windows::UI::Core::CoreCursorType::Help; - break; - case facebook::react::Cursor::NotAllowed: - type = winrt::Windows::UI::Core::CoreCursorType::UniversalNo; - break; - case facebook::react::Cursor::Wait: - type = winrt::Windows::UI::Core::CoreCursorType::Wait; - break; - case facebook::react::Cursor::Move: - type = winrt::Windows::UI::Core::CoreCursorType::SizeAll; - break; - case facebook::react::Cursor::NESWResize: - type = winrt::Windows::UI::Core::CoreCursorType::SizeNortheastSouthwest; - break; - case facebook::react::Cursor::NSResize: - type = winrt::Windows::UI::Core::CoreCursorType::SizeNorthSouth; - break; - case facebook::react::Cursor::NWSEResize: - type = winrt::Windows::UI::Core::CoreCursorType::SizeNorthwestSoutheast; - break; - case facebook::react::Cursor::EWResize: - type = winrt::Windows::UI::Core::CoreCursorType::SizeWestEast; - break; - case facebook::react::Cursor::Text: - type = winrt::Windows::UI::Core::CoreCursorType::IBeam; - break; - case facebook::react::Cursor::Progress: - type = winrt::Windows::UI::Core::CoreCursorType::Wait; // IDC_APPSTARTING not mapped to CoreCursor? - break; - case facebook::react::Cursor::Crosshair: - type = winrt::Windows::UI::Core::CoreCursorType::Cross; - break; - */ + case facebook::react::Cursor::Help: + type = winrt::Windows::UI::Core::CoreCursorType::Help; + break; + case facebook::react::Cursor::NotAllowed: + type = winrt::Windows::UI::Core::CoreCursorType::UniversalNo; + break; + case facebook::react::Cursor::Wait: + type = winrt::Windows::UI::Core::CoreCursorType::Wait; + break; + case facebook::react::Cursor::Move: + type = winrt::Windows::UI::Core::CoreCursorType::SizeAll; + break; + case facebook::react::Cursor::NESWResize: + type = winrt::Windows::UI::Core::CoreCursorType::SizeNortheastSouthwest; + break; + case facebook::react::Cursor::NSResize: + type = winrt::Windows::UI::Core::CoreCursorType::SizeNorthSouth; + break; + case facebook::react::Cursor::NWSEResize: + type = winrt::Windows::UI::Core::CoreCursorType::SizeNorthwestSoutheast; + break; + case facebook::react::Cursor::EWResize: + type = winrt::Windows::UI::Core::CoreCursorType::SizeWestEast; + break; + case facebook::react::Cursor::Text: + type = winrt::Windows::UI::Core::CoreCursorType::IBeam; + break; + case facebook::react::Cursor::Progress: + type = winrt::Windows::UI::Core::CoreCursorType::Wait; // IDC_APPSTARTING not mapped to CoreCursor? + break; + case facebook::react::Cursor::Crosshair: + type = winrt::Windows::UI::Core::CoreCursorType::Cross; + break; default: break; } @@ -880,7 +841,6 @@ void CompositionEventHandler::SetCursor(facebook::react::Cursor cursor, HCURSOR case facebook::react::Cursor::Pointer: idc = IDC_HAND; break; - /* -- Additional cursors not added in core until later version case facebook::react::Cursor::Help: idc = IDC_HELP; break; @@ -914,7 +874,6 @@ void CompositionEventHandler::SetCursor(facebook::react::Cursor cursor, HCURSOR case facebook::react::Cursor::Crosshair: idc = IDC_CROSS; break; - */ default: break; } @@ -1064,21 +1023,48 @@ void CompositionEventHandler::onPointerMoved( facebook::react::PointerEvent pointerEvent = CreatePointerEventFromIncompleteHoverData(ptScaled, ptLocal); - auto handler = [&targetView, - &pointerEvent](std::vector &eventPathViews) { + // check if this pointer corresponds to active touch that has a responder + auto activeTouch = m_activeTouches.find(pointerId); + bool isActiveTouch = activeTouch != m_activeTouches.end() && activeTouch->second.eventEmitter != nullptr; + + auto handler = [&, targetView, pointerEvent, isActiveTouch]( + std::vector &eventPathViews) { const auto eventEmitter = targetView ? winrt::get_self(targetView) ->eventEmitterAtPoint(pointerEvent.offsetPoint) - : nullptr; - bool hasMoveEventListeners = - IsAnyViewInPathListeningToEvent(eventPathViews, facebook::react::ViewEvents::Offset::PointerMove) || - IsAnyViewInPathListeningToEvent(eventPathViews, facebook::react::ViewEvents::Offset::PointerMoveCapture); - if (eventEmitter != nullptr && hasMoveEventListeners) { + : RootComponentView().eventEmitterAtPoint(pointerEvent.offsetPoint); + + if (eventEmitter != nullptr) { eventEmitter->onPointerMove(pointerEvent); + } else { + ClearAllHoveredForPointer(pointerEvent); } }; HandleIncomingPointerEvent(pointerEvent, targetView, pointerPoint, keyModifiers, handler); + + if (isActiveTouch) { + // For active touches with responders, also dispatch through touch event system + UpdateActiveTouch(activeTouch->second, ptScaled, ptLocal); + DispatchTouchEvent(TouchEventType::Move, pointerId, pointerPoint, keyModifiers); + } + } +} + +void CompositionEventHandler::ClearAllHoveredForPointer(const facebook::react::PointerEvent &pointerEvent) noexcept { + // special case if we have no target + // PointerEventsProcessor requires move events to keep track of the hovered components in core. + // It also treats a onPointerLeave event as a special case that removes the hover state of all currently hovered + // events. If we get null for the targetView, that means that the mouse is no over any components, so we have no + // element to send the move event to. However we need to send something so that any previously hovered elements + // are no longer hovered. + auto children = RootComponentView().Children(); + if (auto size = children.Size()) { + auto firstChild = children.GetAt(0); + if (auto childEventEmitter = + winrt::get_self(firstChild)->eventEmitter()) { + childEventEmitter->onPointerLeave(pointerEvent); + } } } @@ -1104,7 +1090,9 @@ void CompositionEventHandler::onPointerExited( facebook::react::PointerEvent pointerEvent = CreatePointerEventFromIncompleteHoverData(ptScaled, ptLocal); - auto handler = [](std::vector &eventPathViews) {}; + auto handler = [&](std::vector &eventPathViews) { + ClearAllHoveredForPointer(pointerEvent); + }; HandleIncomingPointerEvent(pointerEvent, nullptr, pointerPoint, keyModifiers, handler); } @@ -1392,12 +1380,7 @@ void CompositionEventHandler::DispatchTouchEvent( activeTouch.eventEmitter->onPointerDown(pointerEvent); break; case TouchEventType::Move: { - bool hasMoveEventListeners = - IsAnyViewInPathListeningToEvent(eventPathViews, facebook::react::ViewEvents::Offset::PointerMove) || - IsAnyViewInPathListeningToEvent(eventPathViews, facebook::react::ViewEvents::Offset::PointerMoveCapture); - if (hasMoveEventListeners) { - activeTouch.eventEmitter->onPointerMove(pointerEvent); - } + activeTouch.eventEmitter->onPointerMove(pointerEvent); break; } case TouchEventType::End: @@ -1452,4 +1435,4 @@ void CompositionEventHandler::DispatchTouchEvent( } } -} // namespace Microsoft::ReactNative +} // namespace Microsoft::ReactNative \ No newline at end of file diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h index be9cfa44b79..acddeac866f 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h @@ -14,7 +14,6 @@ #include #include #include -#include "Utils/BatchingEventEmitter.h" namespace winrt { using namespace Windows::UI; @@ -99,6 +98,7 @@ class CompositionEventHandler : public std::enable_shared_from_this &)> handler); + void ClearAllHoveredForPointer(const facebook::react::PointerEvent &pointerEvent) noexcept; struct ActiveTouch { facebook::react::Touch touch; diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewEventEmitter.cpp b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewEventEmitter.cpp index 091e0c52f48..fc7a3ca6663 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewEventEmitter.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewEventEmitter.cpp @@ -38,20 +38,30 @@ void HostPlatformViewEventEmitter::onBlur() const { #pragma mark - Mouse Events -void HostPlatformViewEventEmitter::onMouseEnter(PointerEvent const &pointerEvent) const { - dispatchEvent( - "mouseEnter", - std::make_shared(pointerEvent), - EventPriority::AsynchronousBatched, - RawEvent::Category::ContinuousStart); +void HostPlatformViewEventEmitter::onMouseEnter(MouseEvent const &pointerEvent) const { + dispatchEvent("mouseEnter", std::make_shared(pointerEvent), RawEvent::Category::ContinuousStart); } -void HostPlatformViewEventEmitter::onMouseLeave(PointerEvent const &pointerEvent) const { - dispatchEvent( - "mouseLeave", - std::make_shared(pointerEvent), - EventPriority::AsynchronousBatched, - RawEvent::Category::ContinuousStart); +void HostPlatformViewEventEmitter::onMouseLeave(MouseEvent const &pointerEvent) const { + dispatchEvent("mouseLeave", std::make_shared(pointerEvent), RawEvent::Category::ContinuousStart); } -} // namespace facebook::react +#pragma mark - Touch Events + +void HostPlatformViewEventEmitter::onPressIn(GestureResponderEvent event) const { + dispatchEvent("pressIn", [event](jsi::Runtime &runtime) { + auto payload = jsi::Object(runtime); + auto nativeEvent = jsi::Object(runtime); + nativeEvent.setProperty(runtime, "target", static_cast(event.target)); + nativeEvent.setProperty(runtime, "pageX", event.pagePoint.x); + nativeEvent.setProperty(runtime, "pageY", event.pagePoint.y); + nativeEvent.setProperty(runtime, "locationX", event.offsetPoint.x); + nativeEvent.setProperty(runtime, "locationY", event.offsetPoint.y); + nativeEvent.setProperty(runtime, "timestamp", event.timestamp); + nativeEvent.setProperty(runtime, "identifier", event.identifier); + payload.setProperty(runtime, "nativeEvent", nativeEvent); + return payload; + }); +} + +} // namespace facebook::react \ No newline at end of file diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewEventEmitter.h b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewEventEmitter.h index 95913ef2eb0..4c2d132ec57 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewEventEmitter.h +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewEventEmitter.h @@ -5,9 +5,18 @@ #include #include "KeyEvent.h" +#include "MouseEvent.h" namespace facebook::react { +struct GestureResponderEvent { + Tag target; + Point pagePoint; + Point offsetPoint; + double timestamp; + int identifier; +}; + class HostPlatformViewEventEmitter : public BaseViewEventEmitter { public: using BaseViewEventEmitter::BaseViewEventEmitter; @@ -24,8 +33,12 @@ class HostPlatformViewEventEmitter : public BaseViewEventEmitter { #pragma mark - Mouse Events - void onMouseEnter(PointerEvent const &pointerEvent) const; - void onMouseLeave(PointerEvent const &pointerEvent) const; + void onMouseEnter(MouseEvent const &pointerEvent) const; + void onMouseLeave(MouseEvent const &pointerEvent) const; + +#pragma mark - Touch Events + + virtual void onPressIn(GestureResponderEvent event) const; }; -} // namespace facebook::react +} // namespace facebook::react \ No newline at end of file diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/MouseEvent.h b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/MouseEvent.h new file mode 100644 index 00000000000..68ebfe073d0 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/MouseEvent.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include + +namespace facebook::react { + +struct MouseEvent : public PointerEvent { + MouseEvent(PointerEvent &event) : PointerEvent(event){}; + + // We override the type so that it is not recorded as a PointerType, + // otherwise PointerEventsProcessor gets confused by the Windows specific MouseEvents + EventPayloadType getType() const override { + return static_cast(-1); + }; +}; + +} // namespace facebook::react \ No newline at end of file From 2e77f223bb2e328032ea4855a164283323489ca2 Mon Sep 17 00:00:00 2001 From: Anupriya Verma <54227869+anupriya13@users.noreply.github.com> Date: Fri, 11 Apr 2025 06:50:32 +0530 Subject: [PATCH 02/36] [Fabric] Implement SpellCheck and AutoCorrect for TextInput (#14509) --- ...-99f88013-45df-4134-99df-b0fb86dc5e88.json | 7 + .../LegacyTests/TextInputTestPage.tsx | 2 + .../__snapshots__/snapshotPages.test.js.snap | 2 + packages/playground/Samples/textinput.tsx | 344 ++++++++++-------- .../WindowsTextInputComponentView.cpp | 56 +++ .../TextInput/WindowsTextInputComponentView.h | 4 + .../TextInput/WindowsTextInputProps.cpp | 3 +- .../TextInput/WindowsTextInputProps.h | 3 +- 8 files changed, 272 insertions(+), 149 deletions(-) create mode 100644 change/react-native-windows-99f88013-45df-4134-99df-b0fb86dc5e88.json diff --git a/change/react-native-windows-99f88013-45df-4134-99df-b0fb86dc5e88.json b/change/react-native-windows-99f88013-45df-4134-99df-b0fb86dc5e88.json new file mode 100644 index 00000000000..88a4d392a1f --- /dev/null +++ b/change/react-native-windows-99f88013-45df-4134-99df-b0fb86dc5e88.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement SpellCheck and AutoCorrect for TextInput", + "packageName": "react-native-windows", + "email": "54227869+anupriya13@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/@react-native-windows/tester/src/js/examples-win/LegacyTests/TextInputTestPage.tsx b/packages/@react-native-windows/tester/src/js/examples-win/LegacyTests/TextInputTestPage.tsx index 739f3f49458..552718b6331 100644 --- a/packages/@react-native-windows/tester/src/js/examples-win/LegacyTests/TextInputTestPage.tsx +++ b/packages/@react-native-windows/tester/src/js/examples-win/LegacyTests/TextInputTestPage.tsx @@ -62,6 +62,8 @@ export class TextInputTestPage extends React.Component< style={{height: 80}} placeholder="MultiLine" multiline={true} + spellCheck={false} + autoCorrect={false} /> { state = { passwordHidden: true, text: '', + endEditingText: '', }; onPressShowPassword = () => { @@ -56,155 +58,203 @@ export default class Bootstrap extends React.Component<{}, any> { this.setState({passwordHidden: !previousState}); }; + handleEndEditing = (event: any) => { + const text = event.nativeEvent.text; + this.setState({endEditingText: text}); + console.log('Text input focus lost:', text); + }; + render() { let textInputRef: TextInput | null; return ( - - - - - - - - - - - - - - (textInputRef = ref)} - onFocus={() => setTimeout(() => textInputRef?.blur(), 5000)} - placeholder={'blurs after 5 seconds'} - style={styles.input} - /> - { - this.setState({text}); - }} - value={this.state.text} - selectionColor="red" - maxLength={10} - keyboardType="numeric" - /> -