Skip to content

Commit

Permalink
Bug 1695574 - Make `PresShell::EventHandler::HandleEventUsingCoordina…
Browse files Browse the repository at this point in the history
…tes` keep handling touch events after preceding pointer event target is removed r=smaug,edgar,dom-core

As far as investigating the behavior of the other browsers, touch events are
dispatched the same target as the preceding `pointerdown` even after the target
is removed from the DOM tree.  However, the following pointer events are
dispatched on the target as usual (i.e., the element under the pointer except
when the pointer is captured).  However, our code stops handling touch events
if the preceding `pointerdown` removes the target and not dispatching
`touchstart` causes not dispatching the following touch events so that no
`click` event is fired.

This patch makes the touch event dispatching path work without frame and
keep handling even with an orphan event target.

Differential Revision: https://phabricator.services.mozilla.com/D202811
  • Loading branch information
masayuki-nakano committed Mar 12, 2024
1 parent 495637f commit 9f0669c
Show file tree
Hide file tree
Showing 6 changed files with 437 additions and 79 deletions.
51 changes: 28 additions & 23 deletions dom/events/PointerEventHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -610,15 +610,16 @@ EventMessage PointerEventHandler::ToPointerEventMessage(

/* static */
void PointerEventHandler::DispatchPointerFromMouseOrTouch(
PresShell* aShell, nsIFrame* aFrame, nsIContent* aContent,
WidgetGUIEvent* aEvent, bool aDontRetargetEvents, nsEventStatus* aStatus,
nsIContent** aTargetContent) {
MOZ_ASSERT(aFrame || aContent);
MOZ_ASSERT(aEvent);
PresShell* aShell, nsIFrame* aEventTargetFrame,
nsIContent* aEventTargetContent, WidgetGUIEvent* aMouseOrTouchEvent,
bool aDontRetargetEvents, nsEventStatus* aStatus,
nsIContent** aMouseOrTouchEventTarget /* = nullptr */) {
MOZ_ASSERT(aEventTargetFrame || aEventTargetContent);
MOZ_ASSERT(aMouseOrTouchEvent);

EventMessage pointerMessage = eVoidEvent;
if (aEvent->mClass == eMouseEventClass) {
WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent();
if (aMouseOrTouchEvent->mClass == eMouseEventClass) {
WidgetMouseEvent* mouseEvent = aMouseOrTouchEvent->AsMouseEvent();
// Don't dispatch pointer events caused by a mouse when simulating touch
// devices in RDM.
Document* doc = aShell->GetDocument();
Expand All @@ -636,7 +637,7 @@ void PointerEventHandler::DispatchPointerFromMouseOrTouch(
// 2. We don't synthesize pointer events for those events that are not
// dispatched to DOM.
if (!mouseEvent->convertToPointer ||
!aEvent->IsAllowedToDispatchDOMEvent()) {
!aMouseOrTouchEvent->IsAllowedToDispatchDOMEvent()) {
return;
}

Expand All @@ -648,20 +649,20 @@ void PointerEventHandler::DispatchPointerFromMouseOrTouch(
InitPointerEventFromMouse(&event, mouseEvent, pointerMessage);
event.convertToPointer = mouseEvent->convertToPointer = false;
RefPtr<PresShell> shell(aShell);
if (!aFrame) {
shell = PresShell::GetShellForEventTarget(nullptr, aContent);
if (!aEventTargetFrame) {
shell = PresShell::GetShellForEventTarget(nullptr, aEventTargetContent);
if (!shell) {
return;
}
}
PreHandlePointerEventsPreventDefault(&event, aEvent);
PreHandlePointerEventsPreventDefault(&event, aMouseOrTouchEvent);
// Dispatch pointer event to the same target which is found by the
// corresponding mouse event.
shell->HandleEventWithTarget(&event, aFrame, aContent, aStatus, true,
aTargetContent);
PostHandlePointerEventsPreventDefault(&event, aEvent);
} else if (aEvent->mClass == eTouchEventClass) {
WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent();
shell->HandleEventWithTarget(&event, aEventTargetFrame, aEventTargetContent,
aStatus, true, aMouseOrTouchEventTarget);
PostHandlePointerEventsPreventDefault(&event, aMouseOrTouchEvent);
} else if (aMouseOrTouchEvent->mClass == eTouchEventClass) {
WidgetTouchEvent* touchEvent = aMouseOrTouchEvent->AsTouchEvent();
// loop over all touches and dispatch pointer events on each touch
// copy the event
pointerMessage = PointerEventHandler::ToPointerEventMessage(touchEvent);
Expand All @@ -681,7 +682,7 @@ void PointerEventHandler::DispatchPointerFromMouseOrTouch(
InitPointerEventFromTouch(event, *touchEvent, *touch, i == 0);
event.convertToPointer = touch->convertToPointer = false;
event.mCoalescedWidgetEvents = touch->mCoalescedWidgetEvents;
if (aEvent->mMessage == eTouchStart) {
if (aMouseOrTouchEvent->mMessage == eTouchStart) {
// We already did hit test for touchstart in PresShell. We should
// dispatch pointerdown to the same target as touchstart.
nsCOMPtr<nsIContent> content =
Expand All @@ -696,18 +697,22 @@ void PointerEventHandler::DispatchPointerFromMouseOrTouch(
continue;
}

PreHandlePointerEventsPreventDefault(&event, aEvent);
PreHandlePointerEventsPreventDefault(&event, aMouseOrTouchEvent);
shell->HandleEventWithTarget(&event, frame, content, aStatus, true,
nullptr);
PostHandlePointerEventsPreventDefault(&event, aEvent);
aMouseOrTouchEventTarget);
PostHandlePointerEventsPreventDefault(&event, aMouseOrTouchEvent);
} else {
// We didn't hit test for other touch events. Spec doesn't mention that
// all pointer events should be dispatched to the same target as their
// corresponding touch events. Call PresShell::HandleEvent so that we do
// hit test for pointer events.
PreHandlePointerEventsPreventDefault(&event, aEvent);
shell->HandleEvent(aFrame, &event, aDontRetargetEvents, aStatus);
PostHandlePointerEventsPreventDefault(&event, aEvent);
// FIXME: If aDontRetargetEvents is true and the event is fired on
// different document, we cannot track the pointer event target when
// it's removed from the tree.
PreHandlePointerEventsPreventDefault(&event, aMouseOrTouchEvent);
shell->HandleEvent(aEventTargetFrame, &event, aDontRetargetEvents,
aStatus);
PostHandlePointerEventsPreventDefault(&event, aMouseOrTouchEvent);
}
}
}
Expand Down
38 changes: 33 additions & 5 deletions dom/events/PointerEventHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,39 @@ class PointerEventHandler final {
static void PostHandlePointerEventsPreventDefault(
WidgetPointerEvent* aPointerEvent, WidgetGUIEvent* aMouseOrTouchEvent);

MOZ_CAN_RUN_SCRIPT
static void DispatchPointerFromMouseOrTouch(
PresShell* aShell, nsIFrame* aFrame, nsIContent* aContent,
WidgetGUIEvent* aEvent, bool aDontRetargetEvents, nsEventStatus* aStatus,
nsIContent** aTargetContent);
/**
* Dispatch a pointer event for aMouseOrTouchEvent to aEventTargetContent.
*
* @param aShell The PresShell which is handling the event.
* @param aEventTargetFrame The frame for aEventTargetContent.
* @param aEventTargetContent The event target node.
* @param aMouseOrTouchEvent A mouse or touch event.
* @param aDontRetargetEvents If true, this won't dispatch event with
* different PresShell from aShell. Otherwise,
* pointer events may be fired on different
* document if and only if aMouseOrTOuchEvent is a
* touch event except eTouchStart.
* @param aState [out] The result of the pointer event.
* @param aMouseOrTouchEventTarget
* [out] The event target for the following mouse
* or touch event. If aEventTargetContent has not
* been removed from the tree, this is always set
* to it. If aEventTargetContent is removed from
* the tree and aMouseOrTouchEvent is a mouse
* event, this is set to inclusive ancestor of
* aEventTargetContent which is still connected.
* If aEventTargetContent is removed from the tree
* and aMouseOrTouchEvent is a touch event, this is
* set to aEventTargetContent because touch event
* should be dispatched even on disconnected node.
* FIXME: If the event is a touch event but the
* message is not eTouchStart, this won't be set.
*/
MOZ_CAN_RUN_SCRIPT static void DispatchPointerFromMouseOrTouch(
PresShell* aShell, nsIFrame* aEventTargetFrame,
nsIContent* aEventTargetContent, WidgetGUIEvent* aMouseOrTouchEvent,
bool aDontRetargetEvents, nsEventStatus* aStatus,
nsIContent** aMouseOrTouchEventTarget = nullptr);

static void InitPointerEventFromMouse(WidgetPointerEvent* aPointerEvent,
WidgetMouseEvent* aMouseEvent,
Expand Down
97 changes: 59 additions & 38 deletions layout/base/PresShell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -580,20 +580,35 @@ class MOZ_STACK_CLASS AutoPointerEventTargetUpdater final {
mShell = aShell;
mWeakFrame = aFrame;
mTargetContent = aTargetContent;
aShell->mPointerEventTarget = aFrame->GetContent();
mFromTouch = aEvent->AsPointerEvent()->mFromTouchEvent;
mOriginalPointerEventTarget = aShell->mPointerEventTarget =
aFrame->GetContent();
}

~AutoPointerEventTargetUpdater() {
if (!mTargetContent || !mShell || mWeakFrame.IsAlive()) {
return;
}
mShell->mPointerEventTarget.swap(*mTargetContent);
if (mFromTouch) {
// If the source event is a touch event, the touch event target should
// always be same target as preceding ePointerDown. Therefore, we should
// always set it back to the original event target.
mOriginalPointerEventTarget.swap(*mTargetContent);
} else {
// If the source event is not a touch event (must be a mouse event in
// this case), the event should be fired on the closest inclusive ancestor
// of the pointer event target which is still connected. The mutations
// are tracked by PresShell::ContentRemoved. Therefore, we should set it.
mShell->mPointerEventTarget.swap(*mTargetContent);
}
}

private:
RefPtr<PresShell> mShell;
nsCOMPtr<nsIContent> mOriginalPointerEventTarget;
AutoWeakFrame mWeakFrame;
nsIContent** mTargetContent;
bool mFromTouch = false;
};

bool PresShell::sDisableNonTestMouseEvents = false;
Expand Down Expand Up @@ -6890,18 +6905,13 @@ PresShell* PresShell::GetShellForTouchEvent(WidgetGUIEvent* aEvent) {
return nullptr;
}

nsCOMPtr<nsIContent> content = do_QueryInterface(oldTouch->GetTarget());
nsIContent* const content =
nsIContent::FromEventTargetOrNull(oldTouch->GetTarget());
if (!content) {
return nullptr;
}

nsIFrame* contentFrame = content->GetPrimaryFrame();
if (!contentFrame) {
return nullptr;
}

PresShell* presShell = contentFrame->PresContext()->GetPresShell();
if (presShell) {
if (PresShell* const presShell = content->OwnerDoc()->GetPresShell()) {
return presShell;
}
}
Expand Down Expand Up @@ -7251,12 +7261,6 @@ nsresult PresShell::EventHandler::HandleEventUsingCoordinates(
return NS_OK;
}

// frame could be null after dispatching pointer events.
// XXX Despite of this comment, we update the event target data outside
// DispatchPrecedingPointerEvent(). Can we make it call
// UpdateTouchEventTarget()?
eventTargetData.UpdateTouchEventTarget(aGUIEvent);

// Handle the event in the correct shell.
// We pass the subshell's root frame as the frame to start from. This is
// the only correct alternative; if the event was captured then it
Expand Down Expand Up @@ -7448,41 +7452,58 @@ bool PresShell::EventHandler::DispatchPrecedingPointerEvent(

AutoWeakFrame weakTargetFrame(targetFrame);
AutoWeakFrame weakFrame(aEventTargetData->GetFrame());
nsCOMPtr<nsIContent> content(aEventTargetData->GetContent());
nsCOMPtr<nsIContent> pointerEventTargetContent(
aEventTargetData->GetContent());
RefPtr<PresShell> presShell(aEventTargetData->mPresShell);
nsCOMPtr<nsIContent> targetContent;
nsCOMPtr<nsIContent> mouseOrTouchEventTargetContent;
PointerEventHandler::DispatchPointerFromMouseOrTouch(
presShell, aEventTargetData->GetFrame(), content, aGUIEvent,
aDontRetargetEvents, aEventStatus, getter_AddRefs(targetContent));
presShell, aEventTargetData->GetFrame(), pointerEventTargetContent,
aGUIEvent, aDontRetargetEvents, aEventStatus,
getter_AddRefs(mouseOrTouchEventTargetContent));

// If the target frame is alive, the caller should keep handling the event
// unless event target frame is destroyed.
if (weakTargetFrame.IsAlive()) {
return weakFrame.IsAlive();
if (weakTargetFrame.IsAlive() && weakFrame.IsAlive()) {
aEventTargetData->UpdateTouchEventTarget(aGUIEvent);
return true;
}

// If the event is not a mouse event, the caller should keep handling the
// event unless event target frame is destroyed. Note that this case is
// not defined by the spec.
if (aGUIEvent->mClass != eMouseEventClass) {
return weakFrame.IsAlive();
presShell->FlushPendingNotifications(FlushType::Layout);
if (MOZ_UNLIKELY(mPresShell->IsDestroying())) {
return false;
}

// Spec defines that mouse events must be dispatched to the same target as
// the pointer event. If the target is no longer participating in its
// ownerDocument's tree, fire the event at the original target's nearest
// ancestor node
if (!targetContent) {
// The spec defines that mouse events must be dispatched to the same target as
// the pointer event.
// The Touch Events spec defines that touch events must be dispatched to the
// same target as touch start and the other browsers dispatch touch events
// even if the touch event target is not connected to the document.
// Retargetting the event is handled by AutoPointerEventTargetUpdater and
// mouseOrTouchEventTargetContent stores the result.

// If the target is no longer participating in its ownerDocument's tree,
// fire the event at the original target's nearest ancestor node.
if (!mouseOrTouchEventTargetContent) {
MOZ_ASSERT(aGUIEvent->mClass == eMouseEventClass);
return false;
}

aEventTargetData->SetFrameAndContent(targetContent->GetPrimaryFrame(),
targetContent);
aEventTargetData->mPresShell = PresShell::GetShellForEventTarget(
aEventTargetData->GetFrame(), aEventTargetData->GetContent());
aEventTargetData->SetFrameAndContent(
mouseOrTouchEventTargetContent->GetPrimaryFrame(),
mouseOrTouchEventTargetContent);
aEventTargetData->mPresShell =
mouseOrTouchEventTargetContent->IsInComposedDoc()
? PresShell::GetShellForEventTarget(aEventTargetData->GetFrame(),
aEventTargetData->GetContent())
: mouseOrTouchEventTargetContent->OwnerDoc()->GetPresShell();

// If new target PresShel is not found, we cannot keep handling the event.
return !!aEventTargetData->mPresShell;
if (!aEventTargetData->mPresShell) {
return false;
}

aEventTargetData->UpdateTouchEventTarget(aGUIEvent);
return true;
}

/**
Expand Down Expand Up @@ -12032,7 +12053,7 @@ void PresShell::EventHandler::EventTargetData::UpdateTouchEventTarget(
nsIFrame* newFrame =
TouchManager::SuppressInvalidPointsAndGetTargetedFrame(touchEvent);
if (!newFrame) {
return; // XXX Why don't we stop handling the event in this case?
return;
}
SetFrameAndComputePresShellAndContent(newFrame, aGUIEvent);
return;
Expand Down
44 changes: 36 additions & 8 deletions layout/base/TouchManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -174,17 +174,43 @@ nsIFrame* TouchManager::SuppressInvalidPointsAndGetTargetedFrame(
}

nsIFrame* frame = nullptr;
for (int32_t i = aEvent->mTouches.Length(); i;) {
for (uint32_t i = aEvent->mTouches.Length(); i;) {
--i;
dom::Touch* touch = aEvent->mTouches[i];
if (TouchManager::HasCapturedTouch(touch->Identifier())) {
continue;
}

MOZ_ASSERT(touch->mOriginalTarget);
nsCOMPtr<nsIContent> targetContent = do_QueryInterface(touch->GetTarget());
nsIFrame* targetFrame =
targetContent ? targetContent->GetPrimaryFrame() : nullptr;
nsIContent* const targetContent =
nsIContent::FromEventTargetOrNull(touch->GetTarget());
if (MOZ_UNLIKELY(!targetContent)) {
touch->mIsTouchEventSuppressed = true;
continue;
}

// Even if the target content is not connected, we should dispatch the touch
// start event except when the target content is owned by different
// document.
if (MOZ_UNLIKELY(!targetContent->IsInComposedDoc())) {
if (anyTarget && anyTarget->OwnerDoc() != targetContent->OwnerDoc()) {
touch->mIsTouchEventSuppressed = true;
continue;
}
if (!anyTarget) {
anyTarget = targetContent;
}
touch->SetTouchTarget(targetContent->GetAsElementOrParentElement());
if (PresShell* const presShell =
targetContent->OwnerDoc()->GetPresShell()) {
if (nsIFrame* rootFrame = presShell->GetRootFrame()) {
frame = rootFrame;
}
}
continue;
}

nsIFrame* targetFrame = targetContent->GetPrimaryFrame();
if (targetFrame && !anyTarget) {
anyTarget = targetContent;
} else {
Expand All @@ -208,10 +234,12 @@ nsIFrame* TouchManager::SuppressInvalidPointsAndGetTargetedFrame(
touch->mIsTouchEventSuppressed = true;
} else {
targetFrame = newTargetFrame;
targetFrame->GetContentForEvent(aEvent, getter_AddRefs(targetContent));
touch->SetTouchTarget(targetContent
? targetContent->GetAsElementOrParentElement()
: nullptr);
nsCOMPtr<nsIContent> newTargetContent;
targetFrame->GetContentForEvent(aEvent,
getter_AddRefs(newTargetContent));
touch->SetTouchTarget(
newTargetContent ? newTargetContent->GetAsElementOrParentElement()
: nullptr);
}
}
if (targetFrame) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
[pointerup_after_pointerdown_target_removed.html?pen]
[pointerup event from pen fired after pointerdown target is removed]
expected: FAIL


[pointerup_after_pointerdown_target_removed.html?touch]
[pointerup event from touch fired after pointerdown target is removed]
expected: FAIL
Loading

0 comments on commit 9f0669c

Please sign in to comment.