Skip to content

Commit

Permalink
Allow pointer events on disabled form elements on Nightly
Browse files Browse the repository at this point in the history
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 #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

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1799565
gecko-commit: 0f30447455dcbf2c99259f763a443f79fa521757
gecko-reviewers: smaug
  • Loading branch information
saschanaz authored and moz-wptsync-bot committed Nov 14, 2022
1 parent d1d9e0a commit d3f49b1
Show file tree
Hide file tree
Showing 2 changed files with 250 additions and 2 deletions.
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:[email protected]">
<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>
9 changes: 7 additions & 2 deletions pointerevents/pointerevent_disabled_form_control.html
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 d3f49b1

Please sign in to comment.