Issue #2030 - Dispatch click on common interactive ancestor if mousdown/up are not on the same element

For example, if the mouse is dragged from one element to another while
staying inside the same button, dispatch from their common ancestor
This commit is contained in:
Martok 2022-11-27 16:35:56 +01:00 • committed by roytam1
commit b4dd106262
6 changed files with 103 additions and 56 deletions

View file

@ -2377,6 +2377,56 @@ nsContentUtils::GetCommonAncestor(nsINode* aNode1,
return parent; return parent;
} }
// static
nsINode*
nsContentUtils::GetCommonAncestorUnderInteractiveContent(nsINode* aNode1,
nsINode* aNode2)
{
if (!aNode1 || !aNode2) {
return nullptr;
}
if (aNode1 == aNode2) {
return aNode1;
}
// Build the chain of parents
AutoTArray<nsINode*, 30> parents1;
do {
parents1.AppendElement(aNode1);
if (aNode1->IsElement() &&
aNode1->AsElement()->IsInteractiveHTMLContent(true)) {
break;
}
aNode1 = aNode1->GetFlattenedTreeParentNode();
} while (aNode1);
AutoTArray<nsINode*, 30> parents2;
do {
parents2.AppendElement(aNode2);
if (aNode2->IsElement() &&
aNode2->AsElement()->IsInteractiveHTMLContent(true)) {
break;
}
aNode2 = aNode2->GetFlattenedTreeParentNode();
} while (aNode2);
// Find where the parent chain differs
uint32_t pos1 = parents1.Length();
uint32_t pos2 = parents2.Length();
nsINode* parent = nullptr;
for (uint32_t len = std::min(pos1, pos2); len > 0; --len) {
nsINode* child1 = parents1.ElementAt(--pos1);
nsINode* child2 = parents2.ElementAt(--pos2);
if (child1 != child2) {
break;
}
parent = child1;
}
return parent;
}
/* static */ /* static */
bool bool
nsContentUtils::PositionIsBefore(nsINode* aNode1, nsINode* aNode2) nsContentUtils::PositionIsBefore(nsINode* aNode1, nsINode* aNode2)

View file

@ -324,6 +324,15 @@ public:
static nsINode* GetCommonAncestor(nsINode* aNode1, static nsINode* GetCommonAncestor(nsINode* aNode1,
nsINode* aNode2); nsINode* aNode2);
/**
* Returns the common ancestor under interactive content, if any.
* If neither one has interactive content as ancestor, common ancestor will be
* returned. If only one has interactive content as ancestor, null will be
* returned. If the nodes are the same, that node is returned.
*/
static nsINode* GetCommonAncestorUnderInteractiveContent(nsINode* aNode1,
nsINode* aNode2);
/** /**
* Returns true if aNode1 is before aNode2 in the same connected * Returns true if aNode1 is before aNode2 in the same connected
* tree. * tree.

View file

@ -194,6 +194,11 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Event)
default: default:
break; break;
} }
if (WidgetMouseEvent* mouseEvent = tmp->mEvent->AsMouseEvent()) {
NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mEvent->mClickTarget");
cb.NoteXPCOMChild(mouseEvent->mClickTarget);
}
} }
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPresContext) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPresContext)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExplicitOriginalTarget) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExplicitOriginalTarget)

View file

@ -434,11 +434,8 @@ NS_IMPL_CYCLE_COLLECTION(EventStateManager,
mGestureDownContent, mGestureDownContent,
mGestureDownFrameOwner, mGestureDownFrameOwner,
mLastLeftMouseDownContent, mLastLeftMouseDownContent,
mLastLeftMouseDownContentParent,
mLastMiddleMouseDownContent, mLastMiddleMouseDownContent,
mLastMiddleMouseDownContentParent,
mLastRightMouseDownContent, mLastRightMouseDownContent,
mLastRightMouseDownContentParent,
mActiveContent, mActiveContent,
mHoverContent, mHoverContent,
mURLTargetContent, mURLTargetContent,
@ -4576,16 +4573,13 @@ EventStateManager::SetClickCount(WidgetMouseEvent* aEvent,
nsEventStatus* aStatus) nsEventStatus* aStatus)
{ {
nsCOMPtr<nsIContent> mouseContent; nsCOMPtr<nsIContent> mouseContent;
nsIContent* mouseContentParent = nullptr;
if (mCurrentTarget) { if (mCurrentTarget) {
mCurrentTarget->GetContentForEvent(aEvent, getter_AddRefs(mouseContent)); mCurrentTarget->GetContentForEvent(aEvent, getter_AddRefs(mouseContent));
} }
if (mouseContent) { if (mouseContent && mouseContent->IsNodeOfType(nsINode::eTEXT)) {
if (mouseContent->IsNodeOfType(nsINode::eTEXT)) { nsINode* parent = mouseContent->GetFlattenedTreeParentNode();
mouseContent = mouseContent->GetParent(); if (parent && parent->IsContent()) {
} mouseContent = parent->AsContent();
if (mouseContent && mouseContent->IsRootOfNativeAnonymousSubtree()) {
mouseContentParent = mouseContent->GetParent();
} }
} }
@ -4593,54 +4587,51 @@ EventStateManager::SetClickCount(WidgetMouseEvent* aEvent,
case WidgetMouseEvent::eLeftButton: case WidgetMouseEvent::eLeftButton:
if (aEvent->mMessage == eMouseDown) { if (aEvent->mMessage == eMouseDown) {
mLastLeftMouseDownContent = mouseContent; mLastLeftMouseDownContent = mouseContent;
mLastLeftMouseDownContentParent = mouseContentParent;
} else if (aEvent->mMessage == eMouseUp) { } else if (aEvent->mMessage == eMouseUp) {
if (mLastLeftMouseDownContent == mouseContent || aEvent->mClickTarget =
mLastLeftMouseDownContentParent == mouseContent || nsContentUtils::GetCommonAncestorUnderInteractiveContent(
mLastLeftMouseDownContent == mouseContentParent) { mouseContent, mLastLeftMouseDownContent);
if (aEvent->mClickTarget) {
aEvent->mClickCount = mLClickCount; aEvent->mClickCount = mLClickCount;
mLClickCount = 0; mLClickCount = 0;
} else { } else {
aEvent->mClickCount = 0; aEvent->mClickCount = 0;
} }
mLastLeftMouseDownContent = nullptr; mLastLeftMouseDownContent = nullptr;
mLastLeftMouseDownContentParent = nullptr;
} }
break; break;
case WidgetMouseEvent::eMiddleButton: case WidgetMouseEvent::eMiddleButton:
if (aEvent->mMessage == eMouseDown) { if (aEvent->mMessage == eMouseDown) {
mLastMiddleMouseDownContent = mouseContent; mLastMiddleMouseDownContent = mouseContent;
mLastMiddleMouseDownContentParent = mouseContentParent;
} else if (aEvent->mMessage == eMouseUp) { } else if (aEvent->mMessage == eMouseUp) {
if (mLastMiddleMouseDownContent == mouseContent || aEvent->mClickTarget =
mLastMiddleMouseDownContentParent == mouseContent || nsContentUtils::GetCommonAncestorUnderInteractiveContent(
mLastMiddleMouseDownContent == mouseContentParent) { mouseContent, mLastMiddleMouseDownContent);
if (aEvent->mClickTarget) {
aEvent->mClickCount = mMClickCount; aEvent->mClickCount = mMClickCount;
mMClickCount = 0; mMClickCount = 0;
} else { } else {
aEvent->mClickCount = 0; aEvent->mClickCount = 0;
} }
mLastMiddleMouseDownContent = nullptr; mLastMiddleMouseDownContent = nullptr;
mLastMiddleMouseDownContentParent = nullptr;
} }
break; break;
case WidgetMouseEvent::eRightButton: case WidgetMouseEvent::eRightButton:
if (aEvent->mMessage == eMouseDown) { if (aEvent->mMessage == eMouseDown) {
mLastRightMouseDownContent = mouseContent; mLastRightMouseDownContent = mouseContent;
mLastRightMouseDownContentParent = mouseContentParent;
} else if (aEvent->mMessage == eMouseUp) { } else if (aEvent->mMessage == eMouseUp) {
if (mLastRightMouseDownContent == mouseContent || aEvent->mClickTarget =
mLastRightMouseDownContentParent == mouseContent || nsContentUtils::GetCommonAncestorUnderInteractiveContent(
mLastRightMouseDownContent == mouseContentParent) { mouseContent, mLastRightMouseDownContent);
if (aEvent->mClickTarget) {
aEvent->mClickCount = mRClickCount; aEvent->mClickCount = mRClickCount;
mRClickCount = 0; mRClickCount = 0;
} else { } else {
aEvent->mClickCount = 0; aEvent->mClickCount = 0;
} }
mLastRightMouseDownContent = nullptr; mLastRightMouseDownContent = nullptr;
mLastRightMouseDownContentParent = nullptr;
} }
break; break;
} }
@ -4662,7 +4653,7 @@ EventStateManager::EventCausesClickEvents(const WidgetMouseEvent& aMouseEvent)
} }
// If mouse is still over same element, clickcount will be > 1. // If mouse is still over same element, clickcount will be > 1.
// If it has moved it will be zero, so no click. // If it has moved it will be zero, so no click.
if (!aMouseEvent.mClickCount) { if (!aMouseEvent.mClickCount || !aMouseEvent.mClickTarget) {
return false; return false;
} }
// Check that the window isn't disabled before firing a click // Check that the window isn't disabled before firing a click
@ -4681,7 +4672,7 @@ EventStateManager::InitAndDispatchClickEvent(WidgetMouseEvent* aMouseUpEvent,
{ {
MOZ_ASSERT(aMouseUpEvent); MOZ_ASSERT(aMouseUpEvent);
MOZ_ASSERT(EventCausesClickEvents(*aMouseUpEvent)); MOZ_ASSERT(EventCausesClickEvents(*aMouseUpEvent));
MOZ_ASSERT(aMouseUpContent || aCurrentTarget || aOverrideClickTarget); MOZ_ASSERT(aMouseUpContent || aCurrentTarget);
WidgetMouseEvent event(aMouseUpEvent->IsTrusted(), aMessage, WidgetMouseEvent event(aMouseUpEvent->IsTrusted(), aMessage,
aMouseUpEvent->mWidget, WidgetMouseEvent::eReal); aMouseUpEvent->mWidget, WidgetMouseEvent::eReal);
@ -4695,6 +4686,10 @@ EventStateManager::InitAndDispatchClickEvent(WidgetMouseEvent* aMouseUpEvent,
event.button = aMouseUpEvent->button; event.button = aMouseUpEvent->button;
event.inputSource = aMouseUpEvent->inputSource; event.inputSource = aMouseUpEvent->inputSource;
if (!aMouseUpContent->IsInComposedDoc()) {
return NS_OK;
}
// Use local event status for each click event dispatching since it'll be // Use local event status for each click event dispatching since it'll be
// cleared by EventStateManager::PreHandleEvent(). Therefore, dispatching // cleared by EventStateManager::PreHandleEvent(). Therefore, dispatching
// an event means that previous event status will be ignored. // an event means that previous event status will be ignored.
@ -4730,22 +4725,12 @@ EventStateManager::PostHandleMouseUp(WidgetMouseEvent* aMouseUpEvent,
return NS_OK; return NS_OK;
} }
nsCOMPtr<nsIContent> mouseUpContent = GetEventTargetContent(aMouseUpEvent); nsCOMPtr<nsIContent> clickTarget = do_QueryInterface(aMouseUpEvent->mClickTarget);
// Click events apply to *elements* not nodes. At this point the target NS_ENSURE_STATE(clickTarget);
// content may have been reset to some non-element content, and so we need
// to walk up the closest ancestor element, just like we do in
// nsPresShell::HandlePositionedEvent.
while (mouseUpContent && !mouseUpContent->IsElement()) {
mouseUpContent = mouseUpContent->GetFlattenedTreeParent();
}
if (!mouseUpContent && !mCurrentTarget) {
return NS_OK;
}
// Fire click events if the event target is still available. // Fire click events if the event target is still available.
nsresult rv = DispatchClickEvents(presShell, aMouseUpEvent, aStatus, nsresult rv = DispatchClickEvents(presShell, aMouseUpEvent, aStatus,
mouseUpContent); clickTarget);
if (NS_WARN_IF(NS_FAILED(rv))) { if (NS_WARN_IF(NS_FAILED(rv))) {
return rv; return rv;
} }
@ -4756,13 +4741,13 @@ nsresult
EventStateManager::DispatchClickEvents(nsIPresShell* aPresShell, EventStateManager::DispatchClickEvents(nsIPresShell* aPresShell,
WidgetMouseEvent* aMouseUpEvent, WidgetMouseEvent* aMouseUpEvent,
nsEventStatus* aStatus, nsEventStatus* aStatus,
nsIContent* aMouseUpContent) nsIContent* aClickTarget)
{ {
MOZ_ASSERT(aPresShell); MOZ_ASSERT(aPresShell);
MOZ_ASSERT(aMouseUpEvent); MOZ_ASSERT(aMouseUpEvent);
MOZ_ASSERT(EventCausesClickEvents(*aMouseUpEvent)); MOZ_ASSERT(EventCausesClickEvents(*aMouseUpEvent));
MOZ_ASSERT(aStatus); MOZ_ASSERT(aStatus);
MOZ_ASSERT(aMouseUpContent || mCurrentTarget || aOverrideClickTarget); MOZ_ASSERT(aClickTarget);
bool notDispatchToContents = bool notDispatchToContents =
(aMouseUpEvent->button == WidgetMouseEvent::eMiddleButton || (aMouseUpEvent->button == WidgetMouseEvent::eMiddleButton ||
@ -4770,12 +4755,10 @@ EventStateManager::DispatchClickEvents(nsIPresShell* aPresShell,
bool fireAuxClick = notDispatchToContents; bool fireAuxClick = notDispatchToContents;
nsWeakFrame currentTarget = aClickTarget->GetPrimaryFrame();
// HandleEvent clears out mCurrentTarget which we might need again
nsWeakFrame currentTarget = mCurrentTarget;
nsresult ret = nsresult ret =
InitAndDispatchClickEvent(aMouseUpEvent, aStatus, eMouseClick, InitAndDispatchClickEvent(aMouseUpEvent, aStatus, eMouseClick,
aPresShell, aMouseUpContent, currentTarget, aPresShell, aClickTarget, currentTarget,
notDispatchToContents); notDispatchToContents);
if (NS_WARN_IF(NS_FAILED(ret))) { if (NS_WARN_IF(NS_FAILED(ret))) {
return ret; return ret;
@ -4783,21 +4766,20 @@ EventStateManager::DispatchClickEvents(nsIPresShell* aPresShell,
// Fire double click event if click count is 2. // Fire double click event if click count is 2.
if (aMouseUpEvent->mClickCount == 2 && if (aMouseUpEvent->mClickCount == 2 &&
aMouseUpContent && aMouseUpContent->IsInComposedDoc()) { aClickTarget && aClickTarget->IsInComposedDoc()) {
ret = InitAndDispatchClickEvent(aMouseUpEvent, aStatus, eMouseDoubleClick, ret = InitAndDispatchClickEvent(aMouseUpEvent, aStatus, eMouseDoubleClick,
aPresShell, aMouseUpContent, currentTarget, aPresShell, aClickTarget, currentTarget,
notDispatchToContents); notDispatchToContents);
if (NS_WARN_IF(NS_FAILED(ret))) { if (NS_WARN_IF(NS_FAILED(ret))) {
return ret; return ret;
} }
} }
// Fire auxclick even if necessary. // Fire auxclick even if necessary.
if (fireAuxClick && if (fireAuxClick &&
aMouseUpContent && aMouseUpContent->IsInComposedDoc()) { aClickTarget && aClickTarget->IsInComposedDoc()) {
ret = InitAndDispatchClickEvent(aMouseUpEvent, aStatus, eMouseAuxClick, ret = InitAndDispatchClickEvent(aMouseUpEvent, aStatus, eMouseAuxClick,
aPresShell, aMouseUpContent, currentTarget, aPresShell, aClickTarget, currentTarget,
false); false);
NS_WARNING_ASSERTION(NS_SUCCEEDED(ret), "Failed to dispatch eMouseAuxClick"); NS_WARNING_ASSERTION(NS_SUCCEEDED(ret), "Failed to dispatch eMouseAuxClick");
} }

View file

@ -1014,11 +1014,8 @@ private:
uint16_t mGestureDownButtons; uint16_t mGestureDownButtons;
nsCOMPtr<nsIContent> mLastLeftMouseDownContent; nsCOMPtr<nsIContent> mLastLeftMouseDownContent;
nsCOMPtr<nsIContent> mLastLeftMouseDownContentParent;
nsCOMPtr<nsIContent> mLastMiddleMouseDownContent; nsCOMPtr<nsIContent> mLastMiddleMouseDownContent;
nsCOMPtr<nsIContent> mLastMiddleMouseDownContentParent;
nsCOMPtr<nsIContent> mLastRightMouseDownContent; nsCOMPtr<nsIContent> mLastRightMouseDownContent;
nsCOMPtr<nsIContent> mLastRightMouseDownContentParent;
nsCOMPtr<nsIContent> mActiveContent; nsCOMPtr<nsIContent> mActiveContent;
nsCOMPtr<nsIContent> mHoverContent; nsCOMPtr<nsIContent> mHoverContent;

View file

@ -275,6 +275,10 @@ public:
return result; return result;
} }
// If during mouseup handling we detect that click event might need to be
// dispatched, this is setup to be the target of the click event.
nsCOMPtr<dom::EventTarget> mClickTarget;
// mReason indicates the reason why the event is fired: // mReason indicates the reason why the event is fired:
// - Representing mouse operation. // - Representing mouse operation.
// - Synthesized for emulating mousemove event when the content under the // - Synthesized for emulating mousemove event when the content under the