Commit 77e042ca authored by Ehsan Karamad's avatar Ehsan Karamad Committed by Commit Bot

Ignore preventDefault on wheel (vertical-scroll)

This CL implements the logic required for enforcing 'vertical-scroll'
(through feature policy) on frames where a preventing mouse wheel
handler may block vertical scrolling.

The CL essentially overwrites the value of dispatch type from mouse
wheel handlers iff the suggested direction of scrolling is vertical.

Bug: 611982
Change-Id: I15230f11f616d093b21984fe0b526d94dc62dada
Reviewed-on: https://chromium-review.googlesource.com/1073729
Commit-Queue: Ehsan Karamad <ekaramad@chromium.org>
Reviewed-by: default avatarRobert Flack <flackr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#567773}
parent 2a2ef301
<!DOCTYPE html>
<style>
body, html {
height: 100%;
width: 100%;
overflow: hidden;
}
</style>
<body>
<p>This page blocks all 'mouse-wheel'.</p>
<script>
function defaultScroll() {
window.scrollTo(0, 0);
}
document.body.addEventListener(
"wheel",
(e) => { e.preventDefault(); defaultScroll(); }, {passive: false});
defaultScroll();
</script>
</body>
......@@ -39,12 +39,11 @@ iframe {
// Wait for the helper scripts to load.
promise_test(async() => {
if (window.touch_scroll_api_ready)
if (window.input_api_ready)
return Promise.resolve();
await new Promise( (r) => {
window.resolve_on_touch_scroll_api_ready = r;
window.resolve_on_input_api_ready = r;
});
}, "Make sure input injection API is ready.");
// Sanity-check: Verify we can scroll using the test-API (empty <iframe>).
......
......@@ -39,10 +39,10 @@ iframe {
// Wait for the helper scripts to load.
promise_test(async() => {
if (window.touch_scroll_api_ready)
if (window.input_api_ready)
return Promise.resolve();
await new Promise( (r) => {
window.resolve_on_touch_scroll_api_ready = r;
window.resolve_on_input_api_ready = r;
});
}, "Make sure input injection API is ready.");
......@@ -231,7 +231,7 @@ iframe {
assert_equals(
new_scale_factor,
current_scale_factor,
"Pinch zoom must be blocked.")
"Pinch zoom must be blocked.");
}, "Verify that pinch zoom is blocked in frames with 'vertical-scroll' none'" +
" where 'touchstart' is prevent defaulted.");
</script>
<!doctype html>
<title>vertical-scroll test for 'mousewheel'</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/feature-policy/experimental-features/resources/common.js"></script>
<script src="/feature-policy/experimental-features/resources/vertical-scroll.js"></script>
<style>
html, body {
height: 100%;
width: 100%;
}
iframe {
width: 90%;
height: 90%;
margin: 0;
padding: 0;
}
.spacer {
width: 100%;
height: 100%;
margin: 100%;
}
</style>
<iframe></iframe>
<br/>
<p>Spacers below to make page scrollable</p>
<br/>
<div class="spacer"></div>
<div class="spacer"></div>
<p> EOF </p>
<script>
"use strict";
let url = url_base + "vertical-scroll-wheel-block.html";
function iframeElement() {
return document.querySelector("iframe");
}
// Used as the polling interval when waiting for a specific condition.
let verify_scroll_offset_delay = 5;
let no_scroll_timout = 50;
function waitUntilSatisfied(testInstance, predicate) {
return new Promise((r) => {
function testPredicate() {
if (predicate()) {
r();
} else {
testInstance.step_timeout(testPredicate, verify_scroll_offset_delay);
}
}
testPredicate();
});
}
function resetScroll(testInstance) {
window.scrollTo({top: 0, left: 0, behavior: "instant"});
return waitUntilSatisfied(testInstance, () => {
return window.scrollX === 0 && window.scrollY === 0;
});
}
function waitForMinimumScrollOffset(testInstance, minX, minY) {
return waitUntilSatisfied(testInstance, () => {
return window.scrollX >= minX && window.scrollY >= minY;
});
}
function waitFor(testInstance, delay) {
let checked_once = false;
return waitUntilSatisfied(testInstance, () => {
if (checked_once)
return true;
checked_once = true;
return false;
});
}
// Wait for the helper scripts to load.
promise_test(async() => {
if (window.input_api_ready)
return Promise.resolve();
await new Promise((r) => {
window.resolve_on_input_api_ready = r;
});
}, "Make sure input injection API is ready.");
// Sanity-check: Test API for scrolling along y-axis works as expected.
promise_test(async(testInstance) => {
await resetScroll(testInstance);
await inject_wheel_scroll("down");
await waitForMinimumScrollOffset(testInstance, 0, 1);
assert_greater_than(window.scrollY, 0, "Expected vertical scroll.");
}, "Sanity-check: the page scrolls vertically in response to vertical wheel.");
// Sanity-check: Test API for scrolling along x-axis works as expected.
promise_test(async(testInstance) => {
await resetScroll(testInstance);
await inject_wheel_scroll("right");
await waitForMinimumScrollOffset(testInstance, 1, 0);
assert_greater_than(window.scrollX, 0, "Expected horizontal scroll.");
}, "Sanity-check: the page scrolls horizontally in response to horizontal wheel.");
// Test that when 'vertical-scroll' is enabled, vertical scroll can be
// blocked by canceling 'wheel' event.
promise_test(async(testInstance) => {
setFeatureState(iframeElement(), "vertical-scroll", "*");
await loadUrlInIframe(iframeElement(), url);
await resetScroll(testInstance);
await inject_wheel_scroll("down")
await waitFor(testInstance, no_scroll_timout);
assert_equals(window.scrollY, 0, "Did not expected vertical scroll.");
}, "When 'vertical-scroll' is enabled canceling vertical 'wheel' " +
"blocks vertical scrolling.");
// Test that when 'vertical-scroll' is disabled, vertical scroll cannot
// be blocked by canceling 'wheel' event.
promise_test(async(testInstance) => {
setFeatureState(iframeElement(), "vertical-scroll", "'none'");
await loadUrlInIframe(iframeElement(), url);
await resetScroll(testInstance);
await inject_wheel_scroll("down");
await waitForMinimumScrollOffset(testInstance, 0, 1);
assert_greater_than(window.scrollY, 0, "Expected vertical scroll.");
}, "When 'vertical-scroll' is disabled canceling vertical 'wheel' " +
"does not block vertical scrolling.");
// Test that when 'vertical-scroll' is disabled, horizontal scroll can be
// blocked by canceling 'wheel' event.
promise_test(async(testInstance) => {
setFeatureState(iframeElement(), "vertical-scroll", "'none'");
await loadUrlInIframe(iframeElement(), url);
await resetScroll(testInstance);
await inject_wheel_scroll("right");
await waitFor(testInstance, no_scroll_timout);
assert_equals(window.scrollX, 0, "Did not expect horizontal scroll.");
}, "When 'vertical-scroll' is disabled canceling horizontal 'wheel' " +
"blocks horizontal scrolling.");
</script>
importAutomationScript('/feature-policy/experimental-features/vertical-scroll.js');
function inject_wheel_scroll(direction) {
return wheelScroll(direction, window.innerWidth / 2, window.innerHeight / 2);
}
const delta_to_scroll = 100;
const delta_for_scroll = 100;
function platformAPIExists() {
return window.chrome && window.chrome.gpuBenchmarking;
function ensurePlatformAPIExists(input_src) {
if (input_src === "touch" &&
(!window.chrome || !window.chrome.gpuBenchmarking)) {
throw "'gpuBenchmarking' needed for this test.";
} else if (input_src === "wheel" && !window.eventSender) {
throw "'eventSender' is needed for this test.";
}
}
function getScrollDeltaFromDirection(direction) {
let delta_x = (direction === "left") ? delta_for_scroll :
(direction === "right") ? -delta_for_scroll : 0;
let delta_y = (delta_x !== 0) ? 0 :
(direction === "up") ? delta_for_scroll :
(direction === "down") ? -delta_for_scroll : 0;
if (delta_x === delta_y)
throw `Invlaid direction ${direction}.`;
return {x: delta_x, y: delta_y};
}
function waitForCompositorCommit() {
......@@ -14,74 +30,87 @@ function waitForCompositorCommit() {
});
}
function touchScroll(direction, start_x, start_y) {
if (!platformAPIExists())
return Promise.reject();
let delta_x = (direction === "left") ? delta_to_scroll :
(direction === "right") ? -delta_to_scroll : 0;
let delta_y = (delta_x !== 0) ? 0 :
(direction === "up") ? delta_to_scroll :
(direction === "down") ? -delta_to_scroll : 0;
if (delta_x === delta_y)
return Promise.reject("Invalid touch direction.");
function touchScrollGesture(touch_point, delta) {
return new Promise((resolve) => {
chrome.gpuBenchmarking.pointerActionSequence( [
{source: "touch",
actions: [
{ name: "pointerDown", x: touch_point.x, y: touch_point.y},
{ name: "pointerMove",
x: (touch_point.x + delta.x),
y: (touch_point.y + delta.y)
},
{ name: "pause", duration: 0.1 },
{ name: "pointerUp" }
]}], resolve);
});
}
return waitForCompositorCommit().then(() => {
return new Promise((resolve) => {
chrome.gpuBenchmarking.pointerActionSequence( [
{source: "touch",
actions: [
{ name: "pointerDown", x: start_x, y: start_y},
{ name: "pointerMove",
x: (start_x + delta_x),
y: (start_y + delta_y)
},
{ name: "pause", duration: 0.1 },
{ name: "pointerUp" }
]}], resolve);
});
}).then(waitForCompositorCommit);
async function touchScroll(direction, start_x, start_y) {
ensurePlatformAPIExists("touch");
let delta = getScrollDeltaFromDirection(direction);
await waitForCompositorCommit();
await touchScrollGesture({x: start_x, y: start_y}, delta);
await waitForCompositorCommit();
}
function pinchZoom(direction, start_x_1, start_y_1, start_x_2, start_y_2) {
if (!platformAPIExists())
return Promise.reject();
function pinchZoomGesture(
touch_point_1, touch_point_2, move_offset, offset_upper_bound) {
return new Promise((resolve) => {
var pointerActions = [{'source': 'touch'}, {'source': 'touch'}];
var pointerAction1 = pointerActions[0];
var pointerAction2 = pointerActions[1];
pointerAction1.actions = [];
pointerAction2.actions = [];
pointerAction1.actions.push(
{name: 'pointerDown', x: touch_point_1.x, y: touch_point_1.y});
pointerAction2.actions.push(
{name: 'pointerDown', x: touch_point_2.x, y: touch_point_2.y});
for (var offset = move_offset; offset < offset_upper_bound; offset += move_offset) {
pointerAction1.actions.push({
name: 'pointerMove',
x: (touch_point_1.x - offset),
y: touch_point_1.y,
});
pointerAction2.actions.push({
name: 'pointerMove',
x: (touch_point_2.x + offset),
y: touch_point_2.y,
});
}
pointerAction1.actions.push({name: 'pointerUp'});
pointerAction2.actions.push({name: 'pointerUp'});
chrome.gpuBenchmarking.pointerActionSequence(pointerActions, resolve);
})
}
async function pinchZoom(direction, start_x_1, start_y_1, start_x_2, start_y_2) {
ensurePlatformAPIExists("touch");
let zoom_in = direction === "in";
let delta = zoom_in ? -delta_to_scroll : delta_to_scroll;
let delta = zoom_in ? -delta_for_scroll : delta_for_scroll;
let move_offset = 10;
let offset_upper_bound = 80;
await waitForCompositorCommit();
await pinchZoomGesture(
{x: start_x_1, y: start_y_1},
{x: start_x_2, y: start_y_2},
move_offset,
offset_upper_bound);
await waitForCompositorCommit();
}
return waitForCompositorCommit().then(() => {
return new Promise((resolve) => {
var pointerActions = [{'source': 'touch'}, {'source': 'touch'}];
var pointerAction1 = pointerActions[0];
var pointerAction2 = pointerActions[1];
pointerAction1.actions = [];
pointerAction2.actions = [];
pointerAction1.actions.push(
{name: 'pointerDown', x: start_x_1, y: start_y_1});
pointerAction2.actions.push(
{name: 'pointerDown', x: start_x_2, y: start_y_2});
for (var offset = move_offset; offset < offset_upper_bound; offset += move_offset) {
pointerAction1.actions.push({
name: 'pointerMove',
x: (start_x_1 - offset),
y: start_y_1,
});
pointerAction2.actions.push({
name: 'pointerMove',
x: start_x_2 + offset,
y: start_y_2,
});
}
pointerAction1.actions.push({name: 'pointerUp'});
pointerAction2.actions.push({name: 'pointerUp'});
chrome.gpuBenchmarking.pointerActionSequence(pointerActions, resolve);
});
}).then(waitForCompositorCommit);
function wheelScroll(direction, start_x, start_y) {
ensurePlatformAPIExists("wheel");
let delta = getScrollDeltaFromDirection(direction);
return new Promise((resolve) => {
eventSender.mouseMoveTo(start_x, start_y);
eventSender.mouseDown(0);
eventSender.mouseUp(0);
eventSender.mouseScrollBy(delta.x , delta.y);
resolve();
});
}
window.touch_scroll_api_ready = true;
if (window.resolve_on_touch_scroll_api_ready)
window.resolve_on_touch_scroll_api_ready();
window.input_api_ready = true;
if (window.resolve_on_input_api_ready)
window.resolve_on_input_api_ready();
\ No newline at end of file
......@@ -14,6 +14,7 @@
#include "third_party/blink/renderer/core/layout/hit_test_request.h"
#include "third_party/blink/renderer/core/layout/hit_test_result.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/platform/feature_policy/feature_policy.h"
namespace blink {
MouseWheelEventManager::MouseWheelEventManager(LocalFrame& frame)
......@@ -104,8 +105,30 @@ WebInputEventResult MouseWheelEventManager::HandleWheelEvent(
WheelEvent::Create(event, wheel_target_->GetDocument().domWindow());
DispatchEventResult dom_event_result =
wheel_target_->DispatchEvent(dom_event);
if (dom_event_result != DispatchEventResult::kNotCanceled)
return EventHandlingUtil::ToWebInputEventResult(dom_event_result);
if (dom_event_result != DispatchEventResult::kNotCanceled) {
bool should_enforce_vertical_scroll =
dom_event_result == DispatchEventResult::kCanceledByEventHandler &&
RuntimeEnabledFeatures::ExperimentalProductivityFeaturesEnabled() &&
!wheel_target_->GetDocument().GetFrame()->IsFeatureEnabled(
mojom::FeaturePolicyFeature::kVerticalScroll);
// TODO(ekaramad): This does not seem correct. The behavior of shift +
// scrolling seems different on Mac vs Linux/Windows. We need this done
// properly and perhaps even tag WebMouseWheelEvent with a scrolling
// direction (https://crbug.com/853292).
// When using shift + mouse scroll (to horizontally scroll), the expected
// value of |delta_x| is exactly zero.
bool is_vertical =
(std::abs(dom_event->deltaX()) < std::abs(dom_event->deltaY())) &&
(!dom_event->shiftKey() || dom_event->deltaX() != 0);
// TODO(ekaramad): If the only wheel handlers on the page are from such
// disabled frames we should simply start scrolling on CC and the events
// must get here as passive (https://crbug.com/853059).
// Overwriting the dispatch results ensures that vertical scroll cannot be
// blocked by disabled frames.
return (should_enforce_vertical_scroll && is_vertical)
? WebInputEventResult::kNotHandled
: EventHandlingUtil::ToWebInputEventResult(dom_event_result);
}
}
return WebInputEventResult::kNotHandled;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment