Bug 1909020 - Make createDragEventObject() in EventUtils.js aware of HiDPI environments r=smaug
`synthesizePlainDragAndDrop()` sends `drop` event [1] via `PresShell` [2]. Then, the `screenX` and `screenY` values are set to `WidgetEvent::mRefPoint` [3]. Then, the `drop` event's position is recorded before dispatch [4]. So, `drop` event is fired on the target but the `mRefPoint` may be outside the target. Finally, synthesized `eMouseMove` after `eDrop` will be fired on the wrong element which is different from `eDrop`'s target. This caused the failures of `test_synthmousemove_after_dnd.html` and `test_dragdrop.html` on Android. Perhaps, we should improve `nsDOMWindowUtils` or something lower layer later. Instead, this patch fixes in `EventUtils.js` level. This makes the `createDragEventObject()`. 1. https://searchfox.org/mozilla-central/rev/6a72a6d20eeb1b20b93862a79166938d6ce794a0/testing/mochitest/tests/SimpleTest/EventUtils.js#3893-3900 2. https://searchfox.org/mozilla-central/rev/6a72a6d20eeb1b20b93862a79166938d6ce794a0/testing/mochitest/tests/SimpleTest/EventUtils.js#376-393,400 3. https://searchfox.org/mozilla-central/rev/6a72a6d20eeb1b20b93862a79166938d6ce794a0/dom/events/MouseEvent.cpp#81-84 4. https://searchfox.org/mozilla-central/rev/6a72a6d20eeb1b20b93862a79166938d6ce794a0/layout/base/PresShell.cpp#8987 Differential Revision: https://phabricator.services.mozilla.com/D251091
This commit is contained in:
committed by
masayuki@d-toybox.com
parent
842f61a7ac
commit
735e6414f0
@@ -939,9 +939,6 @@ async function doTest() {
|
||||
})();
|
||||
|
||||
// -------- Test dragging contenteditable to same contenteditable
|
||||
// Bug 1904272: Android Non-XOrigin incorrectly inserts after the 3rd M
|
||||
// instead of after the 2nd M in some of the following tests.
|
||||
const isAndroidException = AppConstants.platform === "android" && !isXOrigin;
|
||||
|
||||
await (async function test_dragging_from_contenteditable_to_itself() {
|
||||
const description = "dragging text in contenteditable to same contenteditable";
|
||||
@@ -972,16 +969,8 @@ async function doTest() {
|
||||
}
|
||||
)
|
||||
) {
|
||||
const kExpectedOffsets = isAndroidException ? [3,3] : [2,2];
|
||||
if (isAndroidException) {
|
||||
todo_is(contenteditable.innerHTML, "<b>bd</b> <span>MM</span><b>ol</b><span>MM</span>",
|
||||
`${description}: dragged range should be removed from contenteditable`);
|
||||
isnot(contenteditable.innerHTML, "<b>bd</b> <span>MMMM</span><b>ol</b>",
|
||||
`${description}: dragged range should be removed from contenteditable`);
|
||||
} else {
|
||||
is(contenteditable.innerHTML, "<b>bd</b> <span>MM</span><b>ol</b><span>MM</span>",
|
||||
`${description}: dragged range should be removed from contenteditable`);
|
||||
}
|
||||
is(contenteditable.innerHTML, "<b>bd</b> <span>MM</span><b>ol</b><span>MM</span>",
|
||||
`${description}: dragged range should be removed from contenteditable`);
|
||||
is(beforeinputEvents.length, 2,
|
||||
`${description}: 2 "beforeinput" events should be fired on contenteditable`);
|
||||
checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null,
|
||||
@@ -991,8 +980,8 @@ async function doTest() {
|
||||
checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null,
|
||||
[{type: "text/html", data: "<b>ol</b>"},
|
||||
{type: "text/plain", data: "ol"}],
|
||||
[{startContainer: lastTextNode, startOffset: kExpectedOffsets[0],
|
||||
endContainer: lastTextNode, endOffset: kExpectedOffsets[1]}],
|
||||
[{startContainer: lastTextNode, startOffset: 2,
|
||||
endContainer: lastTextNode, endOffset: 2}],
|
||||
description);
|
||||
is(inputEvents.length, 2,
|
||||
`${description}: 2 "input" events should be fired on contenteditable`);
|
||||
@@ -1039,16 +1028,8 @@ async function doTest() {
|
||||
}
|
||||
)
|
||||
) {
|
||||
const kExpectedOffsets = isAndroidException ? [3,3] : [2,2];
|
||||
if (isAndroidException) {
|
||||
todo_is(contenteditable.innerHTML, "<b>bold</b> <span>MM</span><b>ol</b><span>MM</span>",
|
||||
`${description}: dragged range shouldn't be removed from contenteditable`);
|
||||
isnot(contenteditable.innerHTML, "<b>bold</b> <span>MMMM</span><b>ol</b>",
|
||||
`${description}: dragged range shouldn't be removed from contenteditable`);
|
||||
} else {
|
||||
is(contenteditable.innerHTML, "<b>bold</b> <span>MM</span><b>ol</b><span>MM</span>",
|
||||
`${description}: dragged range shouldn't be removed from contenteditable`);
|
||||
}
|
||||
is(contenteditable.innerHTML, "<b>bold</b> <span>MM</span><b>ol</b><span>MM</span>",
|
||||
`${description}: dragged range shouldn't be removed from contenteditable`);
|
||||
is(beforeinputEvents.length, 2,
|
||||
`${description}: 2 "beforeinput" events should be fired on contenteditable`);
|
||||
checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null,
|
||||
@@ -1058,8 +1039,8 @@ async function doTest() {
|
||||
checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null,
|
||||
[{type: "text/html", data: "<b>ol</b>"},
|
||||
{type: "text/plain", data: "ol"}],
|
||||
[{startContainer: lastTextNode, startOffset: kExpectedOffsets[0],
|
||||
endContainer: lastTextNode, endOffset: kExpectedOffsets[1]}],
|
||||
[{startContainer: lastTextNode, startOffset: 2,
|
||||
endContainer: lastTextNode, endOffset: 2}],
|
||||
description);
|
||||
is(inputEvents.length, 1,
|
||||
`${description}: only one "input" event should be fired on contenteditable`);
|
||||
@@ -1114,12 +1095,11 @@ async function doTest() {
|
||||
[{startContainer: selectionContainers[0], startOffset: 1,
|
||||
endContainer: selectionContainers[1], endOffset: 3}],
|
||||
description);
|
||||
const kExpectedOffsets = isAndroidException ? [3,3] : [2,2];
|
||||
checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null,
|
||||
[{type: "text/html", data: "<b>ol</b>"},
|
||||
{type: "text/plain", data: "ol"}],
|
||||
[{startContainer: lastTextNode, startOffset: kExpectedOffsets[0],
|
||||
endContainer: lastTextNode, endOffset: kExpectedOffsets[1]}],
|
||||
[{startContainer: lastTextNode, startOffset: 2,
|
||||
endContainer: lastTextNode, endOffset: 2}],
|
||||
description);
|
||||
is(inputEvents.length, 1,
|
||||
`${description}: only one "input" event should be fired on contenteditable`);
|
||||
@@ -1162,23 +1142,15 @@ async function doTest() {
|
||||
}
|
||||
)
|
||||
) {
|
||||
const kExpectedOffsets = isAndroidException ? [3,3] : [2,2];
|
||||
if (isAndroidException) {
|
||||
todo_is(contenteditable.innerHTML, "<b>bold</b> <span>MM</span><b>ol</b><span>MM</span>",
|
||||
`${description}: dragged range shouldn't be removed from contenteditable`);
|
||||
isnot(contenteditable.innerHTML, "<b>bold</b> <span>MMMM</span><b>ol</b>",
|
||||
`${description}: dragged range shouldn't be removed from contenteditable`);
|
||||
} else {
|
||||
is(contenteditable.innerHTML, "<b>bold</b> <span>MM</span><b>ol</b><span>MM</span>",
|
||||
`${description}: dragged range shouldn't be removed from contenteditable`);
|
||||
}
|
||||
is(contenteditable.innerHTML, "<b>bold</b> <span>MM</span><b>ol</b><span>MM</span>",
|
||||
`${description}: dragged range shouldn't be removed from contenteditable`);
|
||||
is(beforeinputEvents.length, 1,
|
||||
`${description}: only 1 "beforeinput" events should be fired on contenteditable`);
|
||||
checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null,
|
||||
[{type: "text/html", data: "<b>ol</b>"},
|
||||
{type: "text/plain", data: "ol"}],
|
||||
[{startContainer: lastTextNode, startOffset: kExpectedOffsets[0],
|
||||
endContainer: lastTextNode, endOffset: kExpectedOffsets[1]}],
|
||||
[{startContainer: lastTextNode, startOffset: 2,
|
||||
endContainer: lastTextNode, endOffset: 2}],
|
||||
description);
|
||||
is(inputEvents.length, 1,
|
||||
`${description}: only 1 "input" events should be fired on contenteditable`);
|
||||
@@ -3548,16 +3520,8 @@ async function doTest() {
|
||||
}
|
||||
)
|
||||
) {
|
||||
const kExpectedOffsets = isAndroidException ? [4,4] : [2,2];
|
||||
if (isAndroidException) {
|
||||
todo_is(contenteditable.innerHTML, "!!<span>MM</span>dragme<span>MM</span>",
|
||||
`${description}: dragged range should be moved in inline contenteditable`);
|
||||
is(contenteditable.innerHTML, "!!<span>MMMM</span>dragme",
|
||||
`${description}: dragged range should be moved in inline contenteditable`);
|
||||
} else {
|
||||
is(contenteditable.innerHTML, "!!<span>MM</span>dragme<span>MM</span>",
|
||||
`${description}: dragged range should be moved in inline contenteditable`);
|
||||
}
|
||||
is(contenteditable.innerHTML, "!!<span>MM</span>dragme<span>MM</span>",
|
||||
`${description}: dragged range should be moved in inline contenteditable`);
|
||||
is(beforeinputEvents.length, 2,
|
||||
`${description}: 2 "beforeinput" events should be fired on inline contenteditable`);
|
||||
checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null,
|
||||
@@ -3567,8 +3531,8 @@ async function doTest() {
|
||||
checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null,
|
||||
[{type: "text/html", data: "dragme"},
|
||||
{type: "text/plain", data: "dragme"}],
|
||||
[{startContainer: span.firstChild, startOffset: kExpectedOffsets[0],
|
||||
endContainer: span.firstChild, endOffset: kExpectedOffsets[1]}],
|
||||
[{startContainer: span.firstChild, startOffset: 2,
|
||||
endContainer: span.firstChild, endOffset: 2}],
|
||||
description);
|
||||
is(inputEvents.length, 2,
|
||||
`${description}: 2 "input" events should be fired on inline contenteditable`);
|
||||
@@ -3611,18 +3575,10 @@ async function doTest() {
|
||||
}
|
||||
)
|
||||
) {
|
||||
const kExpectedOffsets = isAndroidException ? [2,2] : [1,1];
|
||||
is(contenteditable.innerHTML, "!!",
|
||||
`${description}: dragged range should be removed from inline contenteditable`);
|
||||
if (isAndroidException) {
|
||||
todo_is(otherContenteditable.innerHTML, "MdragmeM",
|
||||
`${description}: dragged content should be inserted into other inline contenteditable`);
|
||||
is(otherContenteditable.innerHTML, "MMdragme",
|
||||
`${description}: dragged content should be inserted into other inline contenteditable`);
|
||||
} else {
|
||||
is(otherContenteditable.innerHTML, "MdragmeM",
|
||||
`${description}: dragged content should be inserted into other inline contenteditable`);
|
||||
}
|
||||
is(otherContenteditable.innerHTML, "MdragmeM",
|
||||
`${description}: dragged content should be inserted into other inline contenteditable`);
|
||||
is(beforeinputEvents.length, 2,
|
||||
`${description}: 2 "beforeinput" events should be fired on inline contenteditable`);
|
||||
checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null,
|
||||
@@ -3632,8 +3588,8 @@ async function doTest() {
|
||||
checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null,
|
||||
[{type: "text/html", data: "dragme"},
|
||||
{type: "text/plain", data: "dragme"}],
|
||||
[{startContainer: otherContenteditable.firstChild, startOffset: kExpectedOffsets[0],
|
||||
endContainer: otherContenteditable.firstChild, endOffset: kExpectedOffsets[1]}],
|
||||
[{startContainer: otherContenteditable.firstChild, startOffset: 1,
|
||||
endContainer: otherContenteditable.firstChild, endOffset: 1}],
|
||||
description);
|
||||
is(inputEvents.length, 2,
|
||||
`${description}: 2 "input" events should be fired on inline contenteditable`);
|
||||
|
||||
@@ -597,7 +597,6 @@ support-files = [
|
||||
]
|
||||
|
||||
["test_synthmousemove_after_dnd.html"]
|
||||
skip-if = ["os == 'android'"] # Bug 1909020
|
||||
|
||||
["test_transformed_scrolling_repaints.html"]
|
||||
|
||||
|
||||
@@ -32,9 +32,19 @@ SimpleTest.waitForFocus(async () => {
|
||||
const promiseDrop = new Promise(resolve => {
|
||||
target.addEventListener("drop", event => {
|
||||
event.preventDefault();
|
||||
target.addEventListener("mouseover", resolve, {once: true});
|
||||
isnot(
|
||||
document.querySelector("span:hover"),
|
||||
target,
|
||||
"The target should not have hover state during the drop event propagation"
|
||||
);
|
||||
info("Waiting for mouseover event after drop event...");
|
||||
target.addEventListener("mouseover", () => {
|
||||
info("Got mouseover event");
|
||||
resolve();
|
||||
}, {once: true});
|
||||
}, { once: true });
|
||||
});
|
||||
info("Dragging selection is the source element and drop it to the target...");
|
||||
getSelection().selectAllChildren(source);
|
||||
synthesizePlainDragAndDrop({
|
||||
srcSelection: getSelection(),
|
||||
|
||||
@@ -1465,13 +1465,7 @@ function synthesizeNativeMouseEvent(aParams, aCallback = null) {
|
||||
}
|
||||
|
||||
const rect = target?.getBoundingClientRect();
|
||||
let resolution = 1.0;
|
||||
try {
|
||||
resolution = _getDOMWindowUtils(win.top).getResolution();
|
||||
} catch (e) {
|
||||
// XXX How to get mobile viewport scale on Fission+xorigin since
|
||||
// window.top access isn't allowed due to cross-origin?
|
||||
}
|
||||
const resolution = _getTopWindowResolution(win);
|
||||
const scaleValue = (() => {
|
||||
if (scale === "inScreenPixels") {
|
||||
return 1.0;
|
||||
@@ -1490,15 +1484,7 @@ function synthesizeNativeMouseEvent(aParams, aCallback = null) {
|
||||
if (screenX != undefined) {
|
||||
return screenX * scaleValue;
|
||||
}
|
||||
let winInnerOffsetX = win.mozInnerScreenX;
|
||||
try {
|
||||
winInnerOffsetX =
|
||||
win.top.mozInnerScreenX +
|
||||
(win.mozInnerScreenX - win.top.mozInnerScreenX) * resolution;
|
||||
} catch (e) {
|
||||
// XXX fission+xorigin test throws permission denied since win.top is
|
||||
// cross-origin.
|
||||
}
|
||||
const winInnerOffsetX = _getScreenXInUnscaledCSSPixels(win);
|
||||
return (
|
||||
(((atCenter ? rect.width / 2 : offsetX) + rect.left) * resolution +
|
||||
winInnerOffsetX) *
|
||||
@@ -1511,15 +1497,7 @@ function synthesizeNativeMouseEvent(aParams, aCallback = null) {
|
||||
if (screenY != undefined) {
|
||||
return screenY * scaleValue;
|
||||
}
|
||||
let winInnerOffsetY = win.mozInnerScreenY;
|
||||
try {
|
||||
winInnerOffsetY =
|
||||
win.top.mozInnerScreenY +
|
||||
(win.mozInnerScreenY - win.top.mozInnerScreenY) * resolution;
|
||||
} catch (e) {
|
||||
// XXX fission+xorigin test throws permission denied since win.top is
|
||||
// cross-origin.
|
||||
}
|
||||
const winInnerOffsetY = _getScreenYInUnscaledCSSPixels(win);
|
||||
return (
|
||||
(((atCenter ? rect.height / 2 : offsetY) + rect.top) * resolution +
|
||||
winInnerOffsetY) *
|
||||
@@ -2249,6 +2227,67 @@ function _getDOMWindowUtils(aWindow = window) {
|
||||
return aWindow.windowUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Window} aWindow The window.
|
||||
* @returns The scaling value applied to the top window.
|
||||
*/
|
||||
function _getTopWindowResolution(aWindow) {
|
||||
let resolution = 1.0;
|
||||
try {
|
||||
resolution = _getDOMWindowUtils(aWindow.top).getResolution();
|
||||
} catch (e) {
|
||||
// XXX How to get mobile viewport scale on Fission+xorigin since
|
||||
// window.top access isn't allowed due to cross-origin?
|
||||
}
|
||||
return resolution;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Window} aWindow The window which you want to get its x-offset in the
|
||||
* screen.
|
||||
* @returns The screenX of aWindow in the unscaled CSS pixels.
|
||||
*/
|
||||
function _getScreenXInUnscaledCSSPixels(aWindow) {
|
||||
// XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546),
|
||||
// so use window.top's mozInnerScreen. But this won't work fission+xorigin
|
||||
// with mobile viewport until mozInnerScreen returns valid value with
|
||||
// scale.
|
||||
let winInnerOffsetX = aWindow.mozInnerScreenX;
|
||||
try {
|
||||
winInnerOffsetX =
|
||||
aWindow.top.mozInnerScreenX +
|
||||
(aWindow.mozInnerScreenX - aWindow.top.mozInnerScreenX) *
|
||||
_getTopWindowResolution(aWindow);
|
||||
} catch (e) {
|
||||
// XXX fission+xorigin test throws permission denied since win.top is
|
||||
// cross-origin.
|
||||
}
|
||||
return winInnerOffsetX;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Window} aWindow The window which you want to get its y-offset in the
|
||||
* screen.
|
||||
* @returns The screenY of aWindow in the unscaled CSS pixels.
|
||||
*/
|
||||
function _getScreenYInUnscaledCSSPixels(aWindow) {
|
||||
// XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546),
|
||||
// so use window.top's mozInnerScreen. But this won't work fission+xorigin
|
||||
// with mobile viewport until mozInnerScreen returns valid value with
|
||||
// scale.
|
||||
let winInnerOffsetY = aWindow.mozInnerScreenY;
|
||||
try {
|
||||
winInnerOffsetY =
|
||||
aWindow.top.mozInnerScreenY +
|
||||
(aWindow.mozInnerScreenY - aWindow.top.mozInnerScreenY) *
|
||||
_getTopWindowResolution(aWindow);
|
||||
} catch (e) {
|
||||
// XXX fission+xorigin test throws permission denied since win.top is
|
||||
// cross-origin.
|
||||
}
|
||||
return winInnerOffsetY;
|
||||
}
|
||||
|
||||
function _defineConstant(name, value) {
|
||||
Object.defineProperty(this, name, {
|
||||
value,
|
||||
@@ -3127,17 +3166,28 @@ function createDragEventObject(
|
||||
aDataTransfer,
|
||||
aDragEvent
|
||||
) {
|
||||
var destRect = aDestElement.getBoundingClientRect();
|
||||
var destClientX = destRect.left + destRect.width / 2;
|
||||
var destClientY = destRect.top + destRect.height / 2;
|
||||
var destScreenX = aDestWindow.mozInnerScreenX + destClientX;
|
||||
var destScreenY = aDestWindow.mozInnerScreenY + destClientY;
|
||||
if ("clientX" in aDragEvent && !("screenX" in aDragEvent)) {
|
||||
destScreenX = aDestWindow.mozInnerScreenX + aDragEvent.clientX;
|
||||
}
|
||||
if ("clientY" in aDragEvent && !("screenY" in aDragEvent)) {
|
||||
destScreenY = aDestWindow.mozInnerScreenY + aDragEvent.clientY;
|
||||
}
|
||||
const resolution = _getTopWindowResolution(aDestWindow.top);
|
||||
const destRect = aDestElement.getBoundingClientRect();
|
||||
// If clientX and/or clientY are specified, we should use them. Otherwise,
|
||||
// use the center of the dest element.
|
||||
const destClientXInCSSPixels =
|
||||
"clientX" in aDragEvent && !("screenX" in aDragEvent)
|
||||
? aDragEvent.clientX
|
||||
: destRect.left + destRect.width / 2;
|
||||
const destClientYInCSSPixels =
|
||||
"clientY" in aDragEvent && !("screenY" in aDragEvent)
|
||||
? aDragEvent.clientY
|
||||
: destRect.top + destRect.height / 2;
|
||||
|
||||
const devicePixelRatio = aDestWindow.devicePixelRatio;
|
||||
const destScreenXInDevicePixels =
|
||||
(_getScreenXInUnscaledCSSPixels(aDestWindow) +
|
||||
destClientXInCSSPixels * resolution) *
|
||||
devicePixelRatio;
|
||||
const destScreenYInDevicePixels =
|
||||
(_getScreenYInUnscaledCSSPixels(aDestWindow) +
|
||||
destClientYInCSSPixels * resolution) *
|
||||
devicePixelRatio;
|
||||
|
||||
// Wrap only in plain mochitests
|
||||
let dataTransfer;
|
||||
@@ -3151,14 +3201,13 @@ function createDragEventObject(
|
||||
// nsContentUtils::SetDataTransferInEvent for actual impl).
|
||||
dataTransfer.dropEffect = aDataTransfer.dropEffect;
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
{
|
||||
type: aType,
|
||||
screenX: destScreenX,
|
||||
screenY: destScreenY,
|
||||
clientX: destClientX,
|
||||
clientY: destClientY,
|
||||
screenX: _EU_roundDevicePixels(destScreenXInDevicePixels),
|
||||
screenY: _EU_roundDevicePixels(destScreenYInDevicePixels),
|
||||
clientX: _EU_roundDevicePixels(destClientXInCSSPixels),
|
||||
clientY: _EU_roundDevicePixels(destClientYInCSSPixels),
|
||||
dataTransfer,
|
||||
_domDispatchOnly: aDragEvent._domDispatchOnly,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user