зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1815713 - Allow pull to refresh from subframes. r=botond,geckoview-reviewers,owlish
When a subframe and all of its scroll handoff parents have no room to scroll up, provide a mechanism for the frontend to implement pull-to-refresh. When input targets a subframe that meets the above conditions: For eager results and content that has a related event handler that does not preventDefault, return input "unhandled". This allows pull to refresh to work on mobile when the upwards scroll occurs over a subframe. Differential Revision: https://phabricator.services.mozilla.com/D176559
This commit is contained in:
Родитель
f6546a4099
Коммит
1e2edc0ebb
|
@ -54,7 +54,8 @@ struct APZHandledResult {
|
|||
// an event handler using preventDefault() in the callback, so call sites of
|
||||
// this function should be responsible to set a proper |aPlace|.
|
||||
APZHandledResult(APZHandledPlace aPlace,
|
||||
const AsyncPanZoomController* aTarget);
|
||||
const AsyncPanZoomController* aTarget,
|
||||
bool aPopulateDirectionsForUnhandled = false);
|
||||
APZHandledResult(APZHandledPlace aPlace, SideBits aScrollableDirections,
|
||||
ScrollDirections aOverscrollDirections)
|
||||
: mPlace(aPlace),
|
||||
|
|
|
@ -108,10 +108,27 @@ void APZEventResult::UpdateHandledResult(
|
|||
}
|
||||
|
||||
if (aTarget && !aTarget->IsRootContent()) {
|
||||
auto [result, rootApzc] =
|
||||
// If the event targets a subframe but the subframe and its ancestors
|
||||
// are all scrolled to the top, we want an upward swipe to allow
|
||||
// triggering pull-to-refresh.
|
||||
bool mayTriggerPullToRefresh =
|
||||
aBlock.GetOverscrollHandoffChain()->ScrollingUpWillTriggerPullToRefresh(
|
||||
aTarget);
|
||||
if (mayTriggerPullToRefresh) {
|
||||
// Similar to what is done for the dynamic toolbar, we need to ensure
|
||||
// that if the input has the dispatch to content flag, we need to change
|
||||
// the handled result to Nothing(), so that GeckoView can wait for the
|
||||
// result.
|
||||
mHandledResult = (aDispatchToContent)
|
||||
? Nothing()
|
||||
: Some(APZHandledResult{APZHandledPlace::Unhandled,
|
||||
aTarget, true});
|
||||
}
|
||||
|
||||
auto [mayMoveDynamicToolbar, rootApzc] =
|
||||
aBlock.GetOverscrollHandoffChain()->ScrollingDownWillMoveDynamicToolbar(
|
||||
aTarget);
|
||||
if (result) {
|
||||
if (mayMoveDynamicToolbar) {
|
||||
MOZ_ASSERT(rootApzc && rootApzc->IsRootContent());
|
||||
// The event is actually consumed by a non-root APZC but scroll
|
||||
// positions in all relevant APZCs are at the bottom edge, so if there's
|
||||
|
@ -339,11 +356,16 @@ APZEventResult APZInputBridge::ReceiveInputEvent(
|
|||
}
|
||||
|
||||
APZHandledResult::APZHandledResult(APZHandledPlace aPlace,
|
||||
const AsyncPanZoomController* aTarget)
|
||||
const AsyncPanZoomController* aTarget,
|
||||
bool aPopulateDirectionsForUnhandled)
|
||||
: mPlace(aPlace) {
|
||||
MOZ_ASSERT(aTarget);
|
||||
switch (aPlace) {
|
||||
case APZHandledPlace::Unhandled:
|
||||
if (aPopulateDirectionsForUnhandled) {
|
||||
mScrollableDirections = aTarget->ScrollableDirections();
|
||||
mOverscrollDirections = aTarget->GetAllowedHandoffDirections();
|
||||
}
|
||||
break;
|
||||
case APZHandledPlace::HandledByContent:
|
||||
if (aTarget) {
|
||||
|
|
|
@ -2339,6 +2339,11 @@ bool AsyncPanZoomController::CanVerticalScrollWithDynamicToolbar() const {
|
|||
return mY.CanVerticalScrollWithDynamicToolbar();
|
||||
}
|
||||
|
||||
bool AsyncPanZoomController::CanOverscrollUpwards() const {
|
||||
RecursiveMutexAutoLock lock(mRecursiveMutex);
|
||||
return !mY.CanScrollTo(eSideTop) && mY.OverscrollBehaviorAllowsHandoff();
|
||||
}
|
||||
|
||||
bool AsyncPanZoomController::CanScrollDownwards() const {
|
||||
RecursiveMutexAutoLock lock(mRecursiveMutex);
|
||||
return mY.CanScrollTo(eSideBottom);
|
||||
|
|
|
@ -564,6 +564,9 @@ class AsyncPanZoomController {
|
|||
// Return true if there is room to scroll downwards.
|
||||
bool CanScrollDownwards() const;
|
||||
|
||||
// Return true if there is room to scroll upwards.
|
||||
bool CanOverscrollUpwards() const;
|
||||
|
||||
/**
|
||||
* Convert a point on the scrollbar from this APZC's ParentLayer coordinates
|
||||
* to OuterCSS coordinates relative to the beginning of the scroll track.
|
||||
|
|
|
@ -952,9 +952,17 @@ static APZHandledResult GetHandledResultFor(
|
|||
: APZHandledResult{APZHandledPlace::Unhandled, aApzc};
|
||||
}
|
||||
|
||||
auto [result, rootApzc] = aCurrentInputBlock.GetOverscrollHandoffChain()
|
||||
->ScrollingDownWillMoveDynamicToolbar(aApzc);
|
||||
if (!result) {
|
||||
bool mayTriggerPullToRefresh =
|
||||
aCurrentInputBlock.GetOverscrollHandoffChain()
|
||||
->ScrollingUpWillTriggerPullToRefresh(aApzc);
|
||||
if (mayTriggerPullToRefresh) {
|
||||
return APZHandledResult{APZHandledPlace::Unhandled, aApzc, true};
|
||||
}
|
||||
|
||||
auto [willMoveDynamicToolbar, rootApzc] =
|
||||
aCurrentInputBlock.GetOverscrollHandoffChain()
|
||||
->ScrollingDownWillMoveDynamicToolbar(aApzc);
|
||||
if (!willMoveDynamicToolbar) {
|
||||
return APZHandledResult{APZHandledPlace::HandledByContent, aApzc};
|
||||
}
|
||||
|
||||
|
|
|
@ -224,5 +224,22 @@ OverscrollHandoffChain::ScrollingDownWillMoveDynamicToolbar(
|
|||
return {false, nullptr};
|
||||
}
|
||||
|
||||
bool OverscrollHandoffChain::ScrollingUpWillTriggerPullToRefresh(
|
||||
const AsyncPanZoomController* aApzc) const {
|
||||
MOZ_ASSERT(aApzc && !aApzc->IsRootContent(),
|
||||
"Should be used for non-root APZC");
|
||||
|
||||
for (uint32_t i = IndexOf(aApzc); i < Length(); i++) {
|
||||
if (mChain[i]->IsRootContent()) {
|
||||
return mChain[i]->CanOverscrollUpwards();
|
||||
}
|
||||
|
||||
if (!mChain[i]->CanOverscrollUpwards()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace layers
|
||||
} // namespace mozilla
|
||||
|
|
|
@ -121,6 +121,9 @@ class OverscrollHandoffChain {
|
|||
ScrollingDownWillMoveDynamicToolbar(
|
||||
const AsyncPanZoomController* aApzc) const;
|
||||
|
||||
bool ScrollingUpWillTriggerPullToRefresh(
|
||||
const AsyncPanZoomController* aApzc) const;
|
||||
|
||||
private:
|
||||
std::vector<RefPtr<AsyncPanZoomController>> mChain;
|
||||
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="height=device-height,width=device-width,initial-scale=1.0"
|
||||
/>
|
||||
<style type="text/css">
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
/* background contains one extra transparent.gif because we want trick the
|
||||
contentful paint detection; We want to make sure the background is loaded
|
||||
before the test starts so we always wait for the contentful paint timestamp
|
||||
to exist, however, gradient isn't considered as contentful per spec, so Gecko
|
||||
wouldn't generate a timestamp for it. Hence, we added a transparent gif
|
||||
to the image list to trick the detection. */
|
||||
background: url("/assets/www/transparent.gif"),
|
||||
linear-gradient(135deg, red, white);
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.subframe {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#one > .subframe {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
#two > .subframe {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
#three > .subframe {
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
#four > .subframe {
|
||||
background-color: yellow;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="one" class="container">
|
||||
<div class="subframe"></div>
|
||||
</div>
|
||||
<div id="two" class="container">
|
||||
<div class="subframe"></div>
|
||||
</div>
|
||||
<div id="three" class="container">
|
||||
<div class="subframe"></div>
|
||||
</div>
|
||||
<div id="four" class="container">
|
||||
<div class="subframe"></div>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
document
|
||||
.getElementById("three")
|
||||
.scrollTo({ top: 200, behavior: "instant" });
|
||||
|
||||
document.getElementById("four").addEventListener("touchstart", e => {
|
||||
console.log("not preventing default");
|
||||
});
|
||||
|
||||
document.getElementById("two").addEventListener("touchstart", e => {
|
||||
console.log("preventing default");
|
||||
e.preventDefault();
|
||||
});
|
||||
</script>
|
||||
</html>
|
|
@ -29,7 +29,12 @@
|
|||
</style>
|
||||
<body>
|
||||
<div id="scroll" style="width: 100%; height: 100vh; overflow-y: scroll">
|
||||
<div style="height: 200vh"></div>
|
||||
<div style="height: 300vh"></div>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
document
|
||||
.getElementById("scroll")
|
||||
.scrollTo({ top: 50, behavior: "instant" });
|
||||
</script>
|
||||
</html>
|
||||
|
|
|
@ -92,6 +92,7 @@ open class BaseSessionTest(noErrorCollector: Boolean = false) {
|
|||
const val IFRAME_UNKNOWN_PROTOCOL = "/assets/www/iframe_unknown_protocol.html"
|
||||
const val MEDIA_SESSION_DOM1_PATH = "/assets/www/media_session_dom1.html"
|
||||
const val MEDIA_SESSION_DEFAULT1_PATH = "/assets/www/media_session_default1.html"
|
||||
const val PULL_TO_REFRESH_SUBFRAME_PATH = "/assets/www/pull-to-refresh-subframe.html"
|
||||
const val TOUCH_HTML_PATH = "/assets/www/touch.html"
|
||||
const val TOUCH_XORIGIN_HTML_PATH = "/assets/www/touch_xorigin.html"
|
||||
const val GETUSERMEDIA_XORIGIN_CONTAINER_HTML_PATH = "/assets/www/getusermedia_xorigin_container.html"
|
||||
|
|
|
@ -107,9 +107,9 @@ class InputResultDetailTest : BaseSessionTest() {
|
|||
// Since sendDownEvent() just sends a touch-down, APZ doesn't
|
||||
// yet know the direction, hence it allows scrolling in both
|
||||
// the pan-x and pan-y cases.
|
||||
var expectedPlace = if (touchAction == "none" || (subframe && scrollable)) {
|
||||
var expectedPlace = if (touchAction == "none") {
|
||||
PanZoomController.INPUT_RESULT_HANDLED_CONTENT
|
||||
} else if (scrollable) {
|
||||
} else if (scrollable && !subframe) {
|
||||
PanZoomController.INPUT_RESULT_HANDLED
|
||||
} else {
|
||||
PanZoomController.INPUT_RESULT_UNHANDLED
|
||||
|
@ -283,7 +283,7 @@ class InputResultDetailTest : BaseSessionTest() {
|
|||
"handoff",
|
||||
value,
|
||||
PanZoomController.INPUT_RESULT_HANDLED_CONTENT,
|
||||
PanZoomController.SCROLLABLE_FLAG_BOTTOM,
|
||||
(PanZoomController.SCROLLABLE_FLAG_BOTTOM or PanZoomController.SCROLLABLE_FLAG_TOP),
|
||||
PanZoomController.OVERSCROLL_FLAG_VERTICAL,
|
||||
)
|
||||
|
||||
|
|
|
@ -270,6 +270,44 @@ class PanZoomControllerTest : BaseSessionTest() {
|
|||
return result
|
||||
}
|
||||
|
||||
@WithDisplay(width = 100, height = 100)
|
||||
@Test
|
||||
fun pullToRefreshSubframe() {
|
||||
setupDocument(PULL_TO_REFRESH_SUBFRAME_PATH)
|
||||
|
||||
// No touch handler and no room to scroll up
|
||||
var value = sessionRule.waitForResult(sendDownEvent(50f, 10f))
|
||||
assertThat(
|
||||
"Touch when subframe has no room to scroll up should be unhandled",
|
||||
value,
|
||||
equalTo(PanZoomController.INPUT_RESULT_UNHANDLED),
|
||||
)
|
||||
|
||||
// Touch handler with preventDefault
|
||||
value = sessionRule.waitForResult(sendDownEvent(50f, 35f))
|
||||
assertThat(
|
||||
"Touch when content handles the input should indicate so",
|
||||
value,
|
||||
equalTo(PanZoomController.INPUT_RESULT_HANDLED_CONTENT),
|
||||
)
|
||||
|
||||
// Content with room to scroll up
|
||||
value = sessionRule.waitForResult(sendDownEvent(50f, 60f))
|
||||
assertThat(
|
||||
"Touch when subframe has room to scroll up should be handled by content",
|
||||
value,
|
||||
equalTo(PanZoomController.INPUT_RESULT_HANDLED_CONTENT),
|
||||
)
|
||||
|
||||
// Touch handler without preventDefault and no room to scroll up
|
||||
value = sessionRule.waitForResult(sendDownEvent(50f, 85f))
|
||||
assertThat(
|
||||
"Touch no room up and not handled by content should be unhandled",
|
||||
value,
|
||||
equalTo(PanZoomController.INPUT_RESULT_UNHANDLED),
|
||||
)
|
||||
}
|
||||
|
||||
@WithDisplay(width = 100, height = 100)
|
||||
@Test
|
||||
fun touchEventForResultWithStaticToolbar() {
|
||||
|
@ -359,6 +397,22 @@ class PanZoomControllerTest : BaseSessionTest() {
|
|||
|
||||
// There is a 100% height iframe which is scrollable.
|
||||
setupTouchEventDocument(IFRAME_100_PERCENT_HEIGHT_SCROLLABLE_HTML_PATH, withEventHandler)
|
||||
|
||||
// Scroll down a bit to ensure the original tap cannot be the start of a
|
||||
// pull to refresh gesture.
|
||||
mainSession.evaluateJS(
|
||||
"""
|
||||
const iframe = document.querySelector('iframe');
|
||||
iframe.contentWindow.scrollTo({
|
||||
left: 0,
|
||||
top: 50,
|
||||
behavior: 'instant',
|
||||
});
|
||||
""".trimIndent(),
|
||||
)
|
||||
waitForScroll(scrollWaitTimeout)
|
||||
mainSession.flushApzRepaints()
|
||||
|
||||
value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
|
||||
// The input result should be handled in the iframe content.
|
||||
assertThat(
|
||||
|
@ -419,6 +473,22 @@ class PanZoomControllerTest : BaseSessionTest() {
|
|||
|
||||
// There is a 98vh iframe which is scrollable.
|
||||
setupTouchEventDocument(IFRAME_98VH_SCROLLABLE_HTML_PATH, withEventHandler)
|
||||
|
||||
// Scroll down a bit to ensure the original tap cannot be the start of a
|
||||
// pull to refresh gesture.
|
||||
mainSession.evaluateJS(
|
||||
"""
|
||||
const iframe = document.querySelector('iframe');
|
||||
iframe.contentWindow.scrollTo({
|
||||
left: 0,
|
||||
top: 50,
|
||||
behavior: 'instant',
|
||||
});
|
||||
""".trimIndent(),
|
||||
)
|
||||
waitForScroll(scrollWaitTimeout)
|
||||
mainSession.flushApzRepaints()
|
||||
|
||||
value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
|
||||
// The input result should be handled in the iframe content initially.
|
||||
assertThat(
|
||||
|
|
Загрузка…
Ссылка в новой задаче