Skip to content

Commit

Permalink
Bug 1799565 - Allow pointer events on disabled form elements on Night…
Browse files Browse the repository at this point in the history
…ly r=smaug

Corresponds to the latest consensus and also matches what Chrome shipped behind `--enable-blink-features=SendMouseEventsDisabledFormControls`.

Imported the portion of tests that is directly impacted here from web-platform-tests/wpt#32381. Others are not directly impacted and thus I'd like to land them separately since there are still some mismatching behavior around `button` element.

Differential Revision: https://phabricator.services.mozilla.com/D161537
  • Loading branch information
saschanaz committed Nov 14, 2022
1 parent 017c3ab commit 8b667f2
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 25 deletions.
6 changes: 6 additions & 0 deletions dom/html/nsGenericHTMLElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1968,6 +1968,12 @@ bool nsGenericHTMLFormElement::IsElementDisabledForEvents(WidgetEvent* aEvent,
case eLegacyMouseLineOrPageScroll:
case eLegacyMousePixelScroll:
return false;
case ePointerDown:
case ePointerUp:
case ePointerCancel:
case ePointerGotCapture:
case ePointerLostCapture:
return !StaticPrefs::dom_forms_always_allow_pointer_events_enabled();
default:
break;
}
Expand Down
5 changes: 5 additions & 0 deletions modules/libpref/init/StaticPrefList.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2894,6 +2894,11 @@
value: true
mirror: always

- name: dom.forms.always_allow_pointer_events.enabled
type: bool
value: @IS_NIGHTLY_BUILD@
mirror: always

# Is support for HTMLInputElement.showPicker enabled?
- name: dom.input.showPicker
type: bool
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
[pointerevent_disabled_form_control.html?mouse]
expected: TIMEOUT
[mouse pointerevent attributes]
expected: NOTRUN


prefs: [dom.forms.always_allow_pointer_events.enabled:true]
[pointerevent_disabled_form_control.html?pen]
expected:
if (os == "android") and fission: [ERROR, TIMEOUT]
ERROR

[pointerevent_disabled_form_control.html?touch]
expected: TIMEOUT
[touch pointerevent attributes]
expected: NOTRUN
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
<!DOCTYPE html>
<meta charset="utf8">
<meta name="timeout" content="long">
<title>Event propagation on disabled form elements</title>
<link rel="author" href="mailto:krosylight@mozilla.com">
<link rel="help" href="https://github.com/whatwg/html/issues/2368">
<link rel="help" href="https://github.com/whatwg/html/issues/5886">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>

<div id="cases">
<input> <!-- Sanity check with non-disabled control -->
<select disabled></select>
<select disabled>
<!-- <option> can't be clicked as it doesn't have its own painting area -->
<option>foo</option>
</select>
<fieldset disabled>Text</fieldset>
<fieldset disabled><span class="target">Span</span></fieldset>
<button disabled>Text</button>
<button disabled><span class="target">Span</span></button>
<textarea disabled></textarea>
<input disabled type="button">
<input disabled type="checkbox">
<input disabled type="color" value="#000000">
<input disabled type="date">
<input disabled type="datetime-local">
<input disabled type="email">
<input disabled type="file">
<input disabled type="image">
<input disabled type="month">
<input disabled type="number">
<input disabled type="password">
<input disabled type="radio">
<!-- Native click will click the bar -->
<input disabled type="range" value="0">
<!-- Native click will click the slider -->
<input disabled type="range" value="50">
<input disabled type="reset">
<input disabled type="search">
<input disabled type="submit">
<input disabled type="tel">
<input disabled type="text">
<input disabled type="time">
<input disabled type="url">
<input disabled type="week">
</div>

<script>
/**
* @param {Element} element
*/
function getEventFiringTarget(element) {
return element.querySelector(".target") || element;
}

const allEvents = ["pointermove", "mousemove", "pointerdown", "mousedown", "pointerup", "mouseup", "click"];

/**
* @param {*} t
* @param {Element} element
* @param {Element} observingElement
*/
function setupTest(t, element, observingElement) {
/** @type {{type: string, composedPath: Node[]}[]} */
const observedEvents = [];
const controller = new AbortController();
const { signal } = controller;
const listenerFn = t.step_func(event => {
observedEvents.push({
type: event.type,
target: event.target,
isTrusted: event.isTrusted,
composedPath: event.composedPath().map(n => n.constructor.name),
});
});
for (const event of allEvents) {
observingElement.addEventListener(event, listenerFn, { signal });
}
t.add_cleanup(() => controller.abort());

const target = getEventFiringTarget(element);
return { target, observedEvents };
}

/**
* @param {Element} target
* @param {*} observedEvent
*/
function shouldNotBubble(target, observedEvent) {
return (
target.disabled &&
observedEvent.isTrusted &&
["mousedown", "mouseup", "click"].includes(observedEvent.type)
);
}

/**
* @param {Event} event
*/
function getExpectedComposedPath(event) {
let target = event.target;
const result = [];
while (target) {
if (shouldNotBubble(target, event)) {
return result;
}
result.push(target.constructor.name);
target = target.parentNode;
}
result.push("Window");
return result;
}

/**
* @param {object} options
* @param {Element & { disabled: boolean }} options.element
* @param {Element} options.observingElement
* @param {string[]} options.expectedEvents
* @param {(target: Element) => (Promise<void> | void)} options.clickerFn
* @param {string} options.title
*/
function promise_event_test({ element, observingElement, expectedEvents, nonDisabledExpectedEvents, clickerFn, title }) {
promise_test(async t => {
const { target, observedEvents } = setupTest(t, element, observingElement);

await t.step_func(clickerFn)(target);
await new Promise(resolve => t.step_timeout(resolve, 0));

const expected = element.disabled ? expectedEvents : nonDisabledExpectedEvents;
assert_array_equals(observedEvents.map(e => e.type), expected, "Observed events");

for (const observed of observedEvents) {
assert_equals(observed.target, target, `${observed.type}.target`)
assert_array_equals(
observed.composedPath,
getExpectedComposedPath(observed),
`${observed.type}.composedPath`
);
}

}, `${title} on ${element.outerHTML}, observed from <${observingElement.localName}>`);
}

/**
* @param {object} options
* @param {Element & { disabled: boolean }} options.element
* @param {string[]} options.expectedEvents
* @param {(target: Element) => (Promise<void> | void)} options.clickerFn
* @param {string} options.title
*/
function promise_event_test_hierarchy({ element, expectedEvents, nonDisabledExpectedEvents, clickerFn, title }) {
const targets = [element, document.body];
if (element.querySelector(".target")) {
targets.unshift(element.querySelector(".target"));
}
for (const observingElement of targets) {
promise_event_test({ element, observingElement, expectedEvents, nonDisabledExpectedEvents, clickerFn, title });
}
}

function trusted_click(target) {
// To workaround type=file clicking issue
// https://github.com/w3c/webdriver/issues/1666
return new test_driver.Actions()
.pointerMove(0, 0, { origin: target })
.pointerDown()
.pointerUp()
.send();
}

const mouseEvents = ["mousemove", "mousedown", "mouseup", "click"];
const pointerEvents = ["pointermove", "pointerdown", "pointerup"];

// Events except mousedown/up/click
const allowedEvents = ["pointermove", "mousemove", "pointerdown", "pointerup"];

const elements = document.getElementById("cases").children;
for (const element of elements) {
// Observe on a child element of the control, if exists
const target = element.querySelector(".target");
if (target) {
promise_event_test({
element,
observingElement: target,
expectedEvents: allEvents,
nonDisabledExpectedEvents: allEvents,
clickerFn: trusted_click,
title: "Trusted click"
});
}

// Observe on the control itself
promise_event_test({
element,
observingElement: element,
expectedEvents: allowedEvents,
nonDisabledExpectedEvents: allEvents,
clickerFn: trusted_click,
title: "Trusted click"
});

// Observe on document.body
promise_event_test({
element,
observingElement: document.body,
expectedEvents: allowedEvents,
nonDisabledExpectedEvents: allEvents,
clickerFn: trusted_click,
title: "Trusted click"
});

const eventFirePair = [
[MouseEvent, mouseEvents],
[PointerEvent, pointerEvents]
];

for (const [eventInterface, events] of eventFirePair) {
promise_event_test_hierarchy({
element,
expectedEvents: events,
nonDisabledExpectedEvents: events,
clickerFn: target => {
for (const event of events) {
target.dispatchEvent(new eventInterface(event, { bubbles: true }))
}
},
title: `Dispatch new ${eventInterface.name}()`
})
}

promise_event_test_hierarchy({
element,
expectedEvents: getEventFiringTarget(element) === element ? [] : ["click"],
nonDisabledExpectedEvents: ["click"],
clickerFn: target => target.click(),
title: `click()`
})
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
var inputSource = location.search.substring(1);
var detected_pointertypes = {};
var detected_eventTypes = {};
var eventList = ['pointerout', 'pointerover', 'pointerenter', 'pointermove', 'pointerdown', 'pointerup', 'pointerleave'];
var eventList = ['pointerout', 'pointerover', 'pointerenter', 'pointermove', 'pointerdown', 'gotpointercapture', 'pointerup', 'lostpointercapture', 'pointerleave'];

function resetTestState() {
detected_eventTypes = {};
Expand All @@ -29,10 +29,14 @@
var actions_promise;

eventList.forEach(function(eventName) {
on_event(target, eventName, function (event) {
on_event(target, eventName, function (event) {
detected_eventTypes[event.type] = true;
detected_pointertypes[event.pointerType] = true;

if (event.type === "pointerdown") {
target.setPointerCapture(event.pointerId);
}

if (Object.keys(detected_eventTypes).length == eventList.length) {
// Make sure the test finishes after all the input actions are completed.
actions_promise.then( () => {
Expand All @@ -55,6 +59,7 @@
actions_promise = clickInTarget(inputSource, target).then(function() {
return new test_driver.Actions()
.addPointer(inputSource + "Pointer1", inputSource)
.pointerMove(0, 0, {origin: target})
.pointerMove(0, 0)
.send();
});
Expand Down

0 comments on commit 8b667f2

Please sign in to comment.