Bug 1806591: Implement scroll handoff for tree widgets r=mstriemer,masayuki

Differential Revision: https://phabricator.services.mozilla.com/D174051
This commit is contained in:
Daisuke Akatsuka 2023-04-24 06:27:13 +00:00
parent 90c4ec5697
commit 37bf49cf78
6 changed files with 329 additions and 18 deletions

View File

@ -132,6 +132,7 @@
#include "mozilla/dom/BrowsingContextGroup.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/IMEContentObserver.h"
#include "mozilla/WheelHandlingHelper.h"
#ifdef XP_WIN
# include <direct.h>
@ -4904,6 +4905,15 @@ nsDOMWindowUtils::GetOrientationLock(uint32_t* aOrientationLock) {
return NS_OK;
}
NS_IMETHODIMP
nsDOMWindowUtils::GetWheelScrollTarget(Element** aResult) {
*aResult = nullptr;
if (nsIFrame* targetFrame = WheelTransaction::GetScrollTargetFrame()) {
NS_IF_ADDREF(*aResult = Element::FromNodeOrNull(targetFrame->GetContent()));
}
return NS_OK;
}
NS_IMETHODIMP
nsDOMWindowUtils::SetHiDPIMode(bool aHiDPI) {
#ifdef DEBUG

View File

@ -2306,6 +2306,9 @@ interface nsIDOMWindowUtils : nsISupports {
// Returns the current orientation lock value in browsing context.
// This value is defined in hal/HalScreenConfiguration.h
readonly attribute uint32_t orientationLock;
// Returns an element currently scrolling by wheel.
Element getWheelScrollTarget();
};
[scriptable, uuid(c694e359-7227-4392-a138-33c0cc1f15a6)]

View File

@ -195,6 +195,10 @@ skip-if = (os == 'mac' || os == 'win') # Bug 1141245, frequent timeouts on OSX 1
[test_tooltip_noautohide.xhtml]
[test_tree.xhtml]
[test_tree_hier.xhtml]
[test_tree_scroll.xhtml]
support-files =
!/gfx/layers/apz/test/mochitest/apz_test_utils.js
!/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js
[test_tree_single.xhtml]
[test_tree_view.xhtml]
[test_window_intrinsic_size.xhtml]

View File

@ -0,0 +1,93 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
<!--
XUL Widget Test for scrolling behavior of tree
-->
<window title="Tree" width="500" height="600"
onload="setTimeout(testtag_tree_scroll);"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js"></script>
<script src="chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script>
<script src="chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script>
<script src="tree_shared.js"/>
<html:div style="height: 200px; overflow-y: scroll;">
<html:div id="top" style="height: 50px; background: cyan;"></html:div>
<tree rows="3">
<treecols>
<treecol id="name" label="label" sort="label" flex="1"/>
</treecols>
<treechildren>
<treeitem>
<treerow>
<treecell label="0"/>
</treerow>
</treeitem>
<treeitem>
<treerow>
<treecell label="1"/>
</treerow>
</treeitem>
<treeitem>
<treerow>
<treecell label="2"/>
</treerow>
</treeitem>
<treeitem>
<treerow>
<treecell label="3"/>
</treerow>
</treeitem>
<treeitem>
<treerow>
<treecell label="4"/>
</treerow>
</treeitem>
<treeitem>
<treerow>
<treecell label="5"/>
</treerow>
</treeitem>
<treeitem>
<treerow>
<treecell label="6"/>
</treerow>
</treeitem>
<treeitem>
<treerow>
<treecell label="7"/>
</treerow>
</treeitem>
<treeitem>
<treerow>
<treecell label="8"/>
</treerow>
</treeitem>
<treeitem>
<treerow>
<treecell label="9"/>
</treerow>
</treeitem>
</treechildren>
</tree>
<html:div id="bottom" style="height: 150px; background: orange;"></html:div>
</html:div>
<!-- test results are displayed in the html:body -->
<body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/>
<!-- test code goes here -->
<script type="application/javascript"><![CDATA[
SimpleTest.waitForExplicitFinish();
]]>
</script>
</window>

View File

@ -1753,6 +1753,194 @@ function testtag_tree_wheel(aTree) {
is(defaultPrevented, 48, "wheel event default prevented");
}
async function testtag_tree_scroll() {
const tree = document.querySelector("tree");
info("Scroll down with the content scrollbar at the top");
await doScrollTest({
tree,
initialTreeScrollRow: 0,
initialContainerScrollTop: 0,
scrollDelta: 10,
isTreeScrollExpected: true,
});
info("Scroll down with the content scrollbar at the middle");
await doScrollTest({
tree,
initialTreeScrollRow: 3,
initialContainerScrollTop: 0,
scrollDelta: 10,
isTreeScrollExpected: true,
});
info("Scroll down with the content scrollbar at the bottom");
await doScrollTest({
tree,
initialTreeScrollRow: 9,
initialContainerScrollTop: 0,
scrollDelta: 10,
isTreeScrollExpected: false,
});
info("Scroll up with the content scrollbar at the bottom");
await doScrollTest({
tree,
initialTreeScrollRow: 9,
initialContainerScrollTop: 50,
scrollDelta: -10,
isTreeScrollExpected: true,
});
info("Scroll up with the content scrollbar at the middle");
await doScrollTest({
tree,
initialTreeScrollRow: 5,
initialContainerScrollTop: 50,
scrollDelta: -10,
isTreeScrollExpected: true,
});
info("Scroll up with the content scrollbar at the top");
await doScrollTest({
tree,
initialTreeScrollRow: 0,
initialContainerScrollTop: 50,
scrollDelta: -10,
isTreeScrollExpected: false,
});
info("Check whether the tree is not scrolled when the parent is scrolling");
await doScrollWhileScrollingParent(tree);
SimpleTest.finish();
}
async function doScrollWhileScrollingParent(tree) {
const scrollbar = tree.shadowRoot.querySelector(
"scrollbar[orient='vertical']"
);
const parent = tree.parentElement;
// Set initial scroll amount.
tree.scrollToRow(0);
parent.scrollTop = 0;
const scrollAmount = scrollbar.getAttribute("curpos");
// Scroll parent from top to bottom.
const utils = SpecialPowers.getDOMWindowUtils(window);
let isScrollSeriesTimeout = false;
await SimpleTest.promiseWaitForCondition(async () => {
await nativeScroll(parent, 10, 10, 10);
if (!utils.getWheelScrollTarget()) {
// Dependent on the environment, it might be over the timeout
// (ScrollAnimationPhysics::kScrollSeriesTimeoutMs) that handles wheel
// events as one series. If wheel event happened on the parent couldn't
// be handled as one series of events, the tree component consumes the
// event, this test will be invalid.
isScrollSeriesTimeout = true;
return true;
}
return parent.scrollTop === parent.scrollTopMax;
});
if (!isScrollSeriesTimeout) {
is(
scrollAmount,
scrollbar.getAttribute("curpos"),
"The tree should not be scrolled"
);
}
}
async function doScrollTest({
tree,
initialTreeScrollRow,
initialContainerScrollTop,
scrollDelta,
isTreeScrollExpected,
}) {
const scrollbar = tree.shadowRoot.querySelector(
"scrollbar[orient='vertical']"
);
const container = tree.parentElement;
// Set initial scroll amount.
tree.scrollToRow(initialTreeScrollRow);
container.scrollTop = initialContainerScrollTop;
const treeScrollAmount = scrollbar.getAttribute("curpos");
const containerScrollAmount = container.scrollTop;
// Wait until changing either scroll.
await SimpleTest.promiseWaitForCondition(async () => {
await nativeScroll(tree, 10, 10, scrollDelta);
return (
treeScrollAmount !== scrollbar.getAttribute("curpos") ||
containerScrollAmount !== container.scrollTop
);
});
is(
treeScrollAmount !== scrollbar.getAttribute("curpos"),
isTreeScrollExpected,
"Scroll of tree is expected"
);
is(
containerScrollAmount !== container.scrollTop,
!isTreeScrollExpected,
"Scroll of container is expected"
);
// Wait until finishing wheel scroll transaction.
const utils = SpecialPowers.getDOMWindowUtils(window);
await SimpleTest.promiseWaitForCondition(() => !utils.getWheelScrollTarget());
}
async function nativeScroll(component, offsetX, offsetY, scrollDelta) {
const utils = SpecialPowers.getDOMWindowUtils(window);
const x = component.screenX + offsetX;
const y = component.screenY + offsetX;
// Mouse move event.
await new Promise(resolve => {
window.addEventListener("mousemove", resolve, { once: true });
utils.sendNativeMouseEvent(
x,
y,
utils.NATIVE_MOUSE_MESSAGE_MOVE,
0,
{},
component
);
});
// Wheel event.
await new Promise(resolve => {
window.addEventListener("wheel", resolve, { once: true });
utils.sendNativeMouseScrollEvent(
x,
y,
// nativeVerticalWheelEventMsg is defined in apz_test_native_event_utils.js
// eslint-disable-next-line no-undef
nativeVerticalWheelEventMsg(),
0,
// nativeScrollUnits is defined in apz_test_native_event_utils.js
// eslint-disable-next-line no-undef
-nativeScrollUnits(component, scrollDelta),
0,
0,
0,
component
);
});
// promiseApzFlushedRepaints is defined in apz_test_utils.js
// eslint-disable-next-line no-undef
await promiseApzFlushedRepaints();
}
function synthesizeColumnDrag(
aTree,
aMouseDownColumnNumber,

View File

@ -672,6 +672,10 @@
el.addEventListener("command", stopProp);
}
this.shadowRoot.appendChild(this.constructor.fragment);
this.#verticalScrollbar = this.shadowRoot.querySelector(
"scrollbar[orient='vertical']"
);
}
static get inheritedAttributes() {
@ -787,31 +791,20 @@
// This event doesn't retarget, so listen on the shadow DOM directly
this.shadowRoot.addEventListener("MozMousePixelScroll", event => {
if (
!(
this.getAttribute("allowunderflowscroll") == "true" &&
this.getAttribute("hidevscroll") == "true"
)
) {
if (this.#canScroll(event)) {
event.preventDefault();
}
});
// This event doesn't retarget, so listen on the shadow DOM directly
this.shadowRoot.addEventListener("DOMMouseScroll", event => {
if (
!(
this.getAttribute("allowunderflowscroll") == "true" &&
this.getAttribute("hidevscroll") == "true"
)
) {
event.preventDefault();
}
if (this._editingColumn) {
if (!this.#canScroll(event)) {
return;
}
if (event.axis == event.HORIZONTAL_AXIS) {
event.preventDefault();
if (this._editingColumn) {
return;
}
@ -1377,7 +1370,7 @@
// in LTR mode, and left side of the cell in RTL mode.
let left = style.direction == "rtl" ? cellRect.x : textRect.x;
let scrollbarWidth = window.windowUtils.getBoundsWithoutFlushing(
this.shadowRoot.querySelector("scrollbar[orient='vertical']")
this.#verticalScrollbar
).width;
// Note: this won't be quite right in RTL for trees using twisties
// or indentation. bug 1708159 tracks fixing the implementation
@ -1664,6 +1657,26 @@
return this.changeOpenState(this.currentIndex);
}
#verticalScrollbar = null;
#canScroll(event) {
if (
window.windowUtils.getWheelScrollTarget() ||
event.axis == event.HORIZONTAL_AXIS ||
(this.getAttribute("allowunderflowscroll") == "true" &&
this.getAttribute("hidevscroll") == "true")
) {
return false;
}
const curpos = Number(this.#verticalScrollbar.getAttribute("curpos"));
return (
(event.detail < 0 && 0 < curpos) ||
(event.detail > 0 &&
curpos < Number(this.#verticalScrollbar.getAttribute("maxpos")))
);
}
}
MozXULElement.implementCustomInterface(MozTree, [