Bug 1481923 - Fire a touchcancel event on contextmenu events only if our own context menu is opened on Android. r=botond

And don't fire the touchcancel event in the case where the content calls
preventDefault() for contextmenu events. This is going to be a behavior
change, but Chrome actually does fire a touchcancel event only if the Chrome's
context menu is opened, so this change will make our behavior match Chrome.

To tell whether our own context menu is opened or not, we use
mDefaultPreventedByChrome flag. Unfortunately this approach is applicable only
for Android since Android is the only one platform we call preventDefault() [1]
when opening context menu.

[1] https://searchfox.org/mozilla-central/rev/95c41d54c3fd65d51976d5188842a69b459a7589/mobile/android/actors/ContentDelegateChild.jsm#100

Differential Revision: https://phabricator.services.mozilla.com/D115964
This commit is contained in:
Hiroyuki Ikezoe 2021-06-01 05:47:29 +00:00
parent aafeee2434
commit ed2b8e554f
5 changed files with 115 additions and 37 deletions

View File

@ -36,7 +36,8 @@ function longPressLink() {
var eventsFired = 0;
function recordEvent(e) {
let target = document.getElementById("b");
if (getPlatform() == "windows") {
const platform = getPlatform();
if (platform == "windows") {
// On Windows we get a mouselongtap event once the long-tap has been detected
// by APZ, and that's what we use as the trigger to lift the finger. That then
// triggers the contextmenu. This matches the platform convention.
@ -63,10 +64,10 @@ function recordEvent(e) {
subtestDone();
});
}
} else {
// On non-Windows platforms we get a contextmenu event once the long-tap has
// been detected. Since we prevent-default that, we don't get a mouselongtap
// event at all, and instead get a touchcancel.
} else if (platform != "android") {
// On non-Windows desktop platforms we get a contextmenu event once the
// long-tap has been detected. Since we prevent-default that, we don't get
// a mouselongtap event at all, and instead get a touchcancel.
switch (eventsFired) {
case 0: is(e.type, "touchstart", "Got a touchstart"); break;
case 1: is(e.type, "mouseover", "Got a mouseover"); break;
@ -88,6 +89,53 @@ function recordEvent(e) {
});
});
}
} else {
// On Android we get a contextmenu event once the long-tap has been
// detected. If contextmenu opens we get a touchcancel event, and if
// contextmenu didn't open because of preventDefault() in the content,
// we will not get the touchcancel event.
switch (eventsFired) {
case 0: is(e.type, "touchstart", "Got a touchstart"); break;
case 1: is(e.type, "mouseover", "Got a mouseover"); break;
case 2: is(e.type, "mouseenter", "Got a mouseenter"); break;
case 3: is(e.type, "mousemove", "Got a mousemove"); break;
case 4: is(e.type, "contextmenu", "Got a contextmenu");
// Do preventDefault() in this content, thus we will not get any
// touchcancel event.
e.preventDefault();
synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
dump("Finished synthesizing touch-end, waiting for a touchend event...\n");
});
break;
case 5: is(e.type, "touchend", "Got a touchend");
// Send another long press.
synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
dump("Finished synthesizing touch-start, waiting for events...\n");
});
break;
case 6: is(e.type, "touchstart", "Got another touchstart"); break;
// NOTE: In this another event case, we don't get mouseover or moveenter
// event either since the target element hasn't been changed.
case 7: is(e.type, "mousemove", "Got another mousemove"); break;
case 8: is(e.type, "contextmenu", "Got another contextmenu");
// DON'T DO preventDefault() this time, thus we should get a touchcancel
// event.
break;
case 9: is(e.type, "touchcancel", "Got a touchcancel"); break;
default: ok(false, "Got an unexpected event of type " + e.type); break;
}
eventsFired++;
if (eventsFired == 10) {
removeMouseEventListeners(target);
synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
dump("Finished synthesizing touch-end, doing an APZ flush to see if any more unexpected events come through...\n");
promiseOnlyApzControllerFlushed().then(function() {
dump("Done APZ flush, ending test...\n");
subtestDone();
});
});
}
}
}

View File

@ -545,11 +545,11 @@ nsEventStatus APZCCallbackHelper::DispatchSynthesizedMouseEvent(
return DispatchWidgetEvent(event);
}
bool APZCCallbackHelper::DispatchMouseEvent(
PreventDefaultResult APZCCallbackHelper::DispatchMouseEvent(
PresShell* aPresShell, const nsString& aType, const CSSPoint& aPoint,
int32_t aButton, int32_t aClickCount, int32_t aModifiers,
unsigned short aInputSourceArg, uint32_t aPointerId) {
NS_ENSURE_TRUE(aPresShell, true);
NS_ENSURE_TRUE(aPresShell, PreventDefaultResult::ByContent);
PreventDefaultResult preventDefaultResult;
nsContentUtils::SendMouseEvent(
@ -558,7 +558,7 @@ bool APZCCallbackHelper::DispatchMouseEvent(
/* aIgnoreRootScrollFrame = */ false, 0, aInputSourceArg, aPointerId,
false, &preventDefaultResult, false,
/* aIsWidgetEventSynthesized = */ false);
return preventDefaultResult != PreventDefaultResult::No;
return preventDefaultResult;
}
void APZCCallbackHelper::FireSingleTapEvent(const LayoutDevicePoint& aPoint,

View File

@ -26,6 +26,7 @@ class nsCOMPtr;
namespace mozilla {
class PresShell;
enum class PreventDefaultResult : uint8_t;
namespace layers {
@ -115,11 +116,10 @@ class APZCCallbackHelper {
* This is a lightweight wrapper around nsContentUtils::SendMouseEvent()
* and as such expects |aPoint| to be in layout coordinates. */
MOZ_CAN_RUN_SCRIPT
static bool DispatchMouseEvent(PresShell* aPresShell, const nsString& aType,
const CSSPoint& aPoint, int32_t aButton,
int32_t aClickCount, int32_t aModifiers,
unsigned short aInputSourceArg,
uint32_t aPointerId);
static PreventDefaultResult DispatchMouseEvent(
PresShell* aPresShell, const nsString& aType, const CSSPoint& aPoint,
int32_t aButton, int32_t aClickCount, int32_t aModifiers,
unsigned short aInputSourceArg, uint32_t aPointerId);
/* Fire a single-tap event at the given point. The event is dispatched
* via the given widget. */

View File

@ -198,11 +198,10 @@ void APZEventState::ProcessSingleTap(const CSSPoint& aPoint,
}
}
bool APZEventState::FireContextmenuEvents(PresShell* aPresShell,
const CSSPoint& aPoint,
const CSSToLayoutDeviceScale& aScale,
Modifiers aModifiers,
const nsCOMPtr<nsIWidget>& aWidget) {
PreventDefaultResult APZEventState::FireContextmenuEvents(
PresShell* aPresShell, const CSSPoint& aPoint,
const CSSToLayoutDeviceScale& aScale, Modifiers aModifiers,
const nsCOMPtr<nsIWidget>& aWidget) {
// Suppress retargeting for mouse events generated by a long-press
EventRetargetSuppression suppression;
@ -223,14 +222,15 @@ bool APZEventState::FireContextmenuEvents(PresShell* aPresShell,
// including in JS code, so it's not trivial to change.
CSSPoint point = CSSPoint::FromAppUnits(
ViewportUtils::VisualToLayout(CSSPoint::ToAppUnits(aPoint), aPresShell));
bool eventHandled = APZCCallbackHelper::DispatchMouseEvent(
aPresShell, u"contextmenu"_ns, point, 2, 1,
WidgetModifiersToDOMModifiers(aModifiers),
dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH,
0 /* Use the default value here. */);
PreventDefaultResult preventDefaultResult =
APZCCallbackHelper::DispatchMouseEvent(
aPresShell, u"contextmenu"_ns, point, 2, 1,
WidgetModifiersToDOMModifiers(aModifiers),
dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH,
0 /* Use the default value here. */);
APZES_LOG("Contextmenu event handled: %d\n", eventHandled);
if (eventHandled) {
APZES_LOG("Contextmenu event %s\n", ToString(preventDefaultResult).c_str());
if (preventDefaultResult != PreventDefaultResult::No) {
// If the contextmenu event was handled then we're showing a contextmenu,
// and so we should remove any activation
mActiveElementManager->ClearActivation();
@ -240,12 +240,18 @@ bool APZEventState::FireContextmenuEvents(PresShell* aPresShell,
nsEventStatus status = APZCCallbackHelper::DispatchSynthesizedMouseEvent(
eMouseLongTap, /*time*/ 0, aPoint * aScale, aModifiers,
/*clickCount*/ 1, aWidget);
eventHandled = (status == nsEventStatus_eConsumeNoDefault);
APZES_LOG("eMouseLongTap event handled: %d\n", eventHandled);
if (status == nsEventStatus_eConsumeNoDefault) {
// Assuming no JS actor listens eMouseLongTap events.
preventDefaultResult = PreventDefaultResult::ByContent;
} else {
preventDefaultResult = PreventDefaultResult::No;
}
APZES_LOG("eMouseLongTap event %s\n",
ToString(preventDefaultResult).c_str());
#endif
}
return eventHandled;
return preventDefaultResult;
}
void APZEventState::ProcessLongTap(PresShell* aPresShell,
@ -272,16 +278,39 @@ void APZEventState::ProcessLongTap(PresShell* aPresShell,
eMouseLongTap, /*time*/ 0, aPoint * aScale, aModifiers, /*clickCount*/ 1,
widget);
bool eventHandled = (status == nsEventStatus_eConsumeNoDefault);
PreventDefaultResult preventDefaultResult =
(status == nsEventStatus_eConsumeNoDefault)
? PreventDefaultResult::ByContent
: PreventDefaultResult::No;
#else
bool eventHandled =
PreventDefaultResult preventDefaultResult =
FireContextmenuEvents(aPresShell, aPoint, aScale, aModifiers, widget);
#endif
mContentReceivedInputBlockCallback(aInputBlockId, eventHandled);
mContentReceivedInputBlockCallback(
aInputBlockId, preventDefaultResult != PreventDefaultResult::No);
const bool eventHandled =
#ifdef MOZ_WIDGET_ANDROID
// On Android, GeckoView calls preventDefault() in a JSActor
// (ContentDelegateChild.jsm) when opening context menu so that we can
// tell whether contextmenu opens in response to the contextmenu event by
// checking where preventDefault() got called.
preventDefaultResult == PreventDefaultResult::ByChrome;
#else
// Unfortunately on desktop platforms other than Windows we can't use
// the same approach for Android since we no longer call preventDefault()
// since bug 1558506. So for now, we keep the current behavior that is
// sending a touchcancel event if the contextmenu event was
// preventDefault-ed in an event handler in the content itself.
preventDefaultResult == PreventDefaultResult::ByContent;
#endif
if (eventHandled) {
// Also send a touchcancel to content, so that listeners that might be
// waiting for a touchend don't trigger.
// Also send a touchcancel to content
// a) on Android if browser's contextmenu is open
// b) on Windows if the long tap event was consumed
// c) on other platforms if preventDefault() was called for the contextmenu
// event
// so that listeners that might be waiting for a touchend don't trigger.
WidgetTouchEvent cancelTouchEvent(true, eTouchCancel, widget.get());
cancelTouchEvent.mModifiers = aModifiers;
auto ldPoint = LayoutDeviceIntPoint::Round(aPoint * aScale);

View File

@ -29,6 +29,7 @@ class nsIWidget;
namespace mozilla {
class PresShell;
enum class PreventDefaultResult : uint8_t;
namespace layers {
@ -79,10 +80,10 @@ class APZEventState final {
~APZEventState();
bool SendPendingTouchPreventedResponse(bool aPreventDefault);
MOZ_CAN_RUN_SCRIPT
bool FireContextmenuEvents(PresShell* aPresShell, const CSSPoint& aPoint,
const CSSToLayoutDeviceScale& aScale,
Modifiers aModifiers,
const nsCOMPtr<nsIWidget>& aWidget);
PreventDefaultResult FireContextmenuEvents(
PresShell* aPresShell, const CSSPoint& aPoint,
const CSSToLayoutDeviceScale& aScale, Modifiers aModifiers,
const nsCOMPtr<nsIWidget>& aWidget);
already_AddRefed<nsIWidget> GetWidget() const;
already_AddRefed<nsIContent> GetTouchRollup() const;
bool MainThreadAgreesEventsAreConsumableByAPZ() const;