-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow pointer events on disabled form elements on Nightly
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
1 parent
d1d9e0a
commit d3f49b1
Showing
2 changed files
with
250 additions
and
2 deletions.
There are no files selected for viewing
243 changes: 243 additions & 0 deletions
243
html/semantics/disabled-elements/event-propagate-disabled.tentative.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters