Bug 1877019 - Reflect user activation modifiers for keyboard event only if it likely activates a link or a link-like button. r=smaug

Differential Revision: https://phabricator.services.mozilla.com/D200202
This commit is contained in:
Tooru Fujisawa 2024-02-03 04:50:41 +00:00
parent 7e9c59f478
commit 236646d127
4 changed files with 192 additions and 37 deletions

View file

@ -15,29 +15,66 @@ add_task(async function () {
const metaShiftEvent = { [metaKey]: true, shiftKey: true };
const tests = [
// id, options, result
["#instant", normalEvent, "tab"],
["#instant", shiftEvent, "window"],
["#instant", metaEvent, "tab-bg"],
["#instant", metaShiftEvent, "tab"],
// type, id, options, result
["mouse", "#instant", normalEvent, "tab"],
["mouse", "#instant", shiftEvent, "window"],
["mouse", "#instant", metaEvent, "tab-bg"],
["mouse", "#instant", metaShiftEvent, "tab"],
["#instant-popup", normalEvent, "popup"],
["#instant-popup", shiftEvent, "window"],
["#instant-popup", metaEvent, "tab-bg"],
["#instant-popup", metaShiftEvent, "tab"],
["mouse", "#instant-popup", normalEvent, "popup"],
["mouse", "#instant-popup", shiftEvent, "window"],
["mouse", "#instant-popup", metaEvent, "tab-bg"],
["mouse", "#instant-popup", metaShiftEvent, "tab"],
["#delayed", normalEvent, "tab"],
["#delayed", shiftEvent, "window"],
["#delayed", metaEvent, "tab-bg"],
["#delayed", metaShiftEvent, "tab"],
["mouse", "#delayed", normalEvent, "tab"],
["mouse", "#delayed", shiftEvent, "window"],
["mouse", "#delayed", metaEvent, "tab-bg"],
["mouse", "#delayed", metaShiftEvent, "tab"],
["#delayed-popup", normalEvent, "popup"],
["#delayed-popup", shiftEvent, "window"],
["#delayed-popup", metaEvent, "tab-bg"],
["#delayed-popup", metaShiftEvent, "tab"],
["mouse", "#delayed-popup", normalEvent, "popup"],
["mouse", "#delayed-popup", shiftEvent, "window"],
["mouse", "#delayed-popup", metaEvent, "tab-bg"],
["mouse", "#delayed-popup", metaShiftEvent, "tab"],
// NOTE: meta+keyboard doesn't activate.
["VK_SPACE", "#instant", normalEvent, "tab"],
["VK_SPACE", "#instant", shiftEvent, "window"],
["VK_SPACE", "#instant-popup", normalEvent, "popup"],
["VK_SPACE", "#instant-popup", shiftEvent, "window"],
["VK_SPACE", "#delayed", normalEvent, "tab"],
["VK_SPACE", "#delayed", shiftEvent, "window"],
["VK_SPACE", "#delayed-popup", normalEvent, "popup"],
["VK_SPACE", "#delayed-popup", shiftEvent, "window"],
["KEY_Enter", "#link-instant", normalEvent, "tab"],
["KEY_Enter", "#link-instant", shiftEvent, "window"],
["KEY_Enter", "#link-instant-popup", normalEvent, "popup"],
["KEY_Enter", "#link-instant-popup", shiftEvent, "window"],
["KEY_Enter", "#link-delayed", normalEvent, "tab"],
["KEY_Enter", "#link-delayed", shiftEvent, "window"],
["KEY_Enter", "#link-delayed-popup", normalEvent, "popup"],
["KEY_Enter", "#link-delayed-popup", shiftEvent, "window"],
// Trigger user-defined shortcut key, where modifiers shouldn't affect.
["x", "#instant", normalEvent, "tab"],
["x", "#instant", shiftEvent, "tab"],
["x", "#instant", metaEvent, "tab"],
["x", "#instant", metaShiftEvent, "tab"],
["y", "#instant", normalEvent, "popup"],
["y", "#instant", shiftEvent, "popup"],
["y", "#instant", metaEvent, "popup"],
["y", "#instant", metaShiftEvent, "popup"],
];
for (const [id, event, result] of tests) {
for (const [type, id, event, result] of tests) {
const eventStr = JSON.stringify(event);
let openPromise;
@ -53,7 +90,29 @@ add_task(async function () {
});
}
BrowserTestUtils.synthesizeMouseAtCenter(id, { ...event }, browser);
if (type == "mouse") {
BrowserTestUtils.synthesizeMouseAtCenter(id, { ...event }, browser);
} else {
// Make sure the keyboard activates a simple button on the page.
await ContentTask.spawn(browser, id, elementId => {
content.document.querySelector("#focus-result").value = "";
content.document.querySelector("#focus-check").focus();
});
BrowserTestUtils.synthesizeKey("VK_SPACE", {}, browser);
await ContentTask.spawn(browser, {}, async () => {
await ContentTaskUtils.waitForCondition(
() =>
content.document.querySelector("#focus-result").value === "ok"
);
});
// Once confirmed the keyboard event works, send the actual event
// that calls window.open.
await ContentTask.spawn(browser, id, elementId => {
content.document.querySelector(elementId).focus();
});
BrowserTestUtils.synthesizeKey(type, { ...event }, browser);
}
const openedThing = await openPromise;
@ -64,13 +123,13 @@ add_task(async function () {
Assert.equal(
gBrowser.selectedTab,
newTab,
`${id} with ${eventStr} opened a foreground tab`
`${id} with ${type} and ${eventStr} opened a foreground tab`
);
} else {
Assert.notEqual(
gBrowser.selectedTab,
newTab,
`${id} with ${eventStr} opened a background tab`
`${id} with ${type} and ${eventStr} opened a background tab`
);
}
@ -82,15 +141,30 @@ add_task(async function () {
if (result == "window") {
ok(
!tabs.collapsed,
`${id} with ${eventStr} opened a regular window`
`${id} with ${type} and ${eventStr} opened a regular window`
);
} else {
ok(tabs.collapsed, `${id} with ${eventStr} opened a popup window`);
ok(
tabs.collapsed,
`${id} with ${type} and ${eventStr} opened a popup window`
);
}
const closedPopupPromise = BrowserTestUtils.windowClosed(newWindow);
newWindow.close();
await closedPopupPromise;
// Make sure the focus comes back to this window before proceeding
// to the next test.
if (Services.focus.focusedWindow != window) {
const focusBack = BrowserTestUtils.waitForEvent(
window,
"focus",
true
);
window.focus();
await focusBack;
}
}
}
}

View file

@ -19,11 +19,52 @@ div {
</div>
<div>
<input id="delayed" type="button" value="delayed no features"
onclick="setTimeout(() => window.open('about:blank', '_blank'), 500);">
onclick="setTimeout(() => window.open('about:blank', '_blank'), 100);">
</div>
<div>
<input id="delayed-popup" type="button" value="delayed popup"
onclick="setTimeout(() => window.open('about:blank', '_blank', 'popup=true'), 500);">
onclick="setTimeout(() => window.open('about:blank', '_blank', 'popup=true'), 100);">
<div>
<div>
<a id="link-instant" href=""
onclick="window.open('about:blank', '_blank'); event.preventDefault()">
instant no features
</a>
</div>
<div>
<a id="link-instant-popup" href=""
onclick="window.open('about:blank', '_blank', 'popup=true'); event.preventDefault()">
instant popup
</a>
</div>
<div>
<a id="link-delayed" href=""
onclick="setTimeout(() => window.open('about:blank', '_blank'), 100); event.preventDefault()">
delayed no features
</a>
</div>
<div>
<a id="link-delayed-popup" href=""
onclick="setTimeout(() => window.open('about:blank', '_blank', 'popup=true'), 100); event.preventDefault()">
delayed popup
</a>
<div>
<div>
<input id="focus-check" type="button" value="check focus"
onclick="document.getElementById('focus-result').value = 'ok';">
</div>
<div>
<input id="focus-result" type="text" value="">
<script type="text/javascript">
document.addEventListener("keydown", event => {
if (event.key == "x") {
window.open('about:blank', '_blank');
}
if (event.key == "y") {
window.open('about:blank', '_blank', 'popup=true');
}
});
</script>
</body>
</html>

View file

@ -1041,6 +1041,24 @@ nsresult EventStateManager::PreHandleEvent(nsPresContext* aPresContext,
return NS_OK;
}
// Returns true if this event is likely an user activation for a link or
// a link-like button, where modifier keys are likely be used for controlling
// where the link is opened.
//
// The modifiers associated with the user activation is used for controlling
// where the `window.open` is opened into.
static bool CanReflectModifiersToUserActivation(WidgetInputEvent* aEvent) {
MOZ_ASSERT(aEvent->mMessage == eKeyDown || aEvent->mMessage == eMouseDown ||
aEvent->mMessage == ePointerDown || aEvent->mMessage == eTouchEnd);
WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent();
if (keyEvent) {
return keyEvent->CanReflectModifiersToUserActivation();
}
return true;
}
void EventStateManager::NotifyTargetUserActivation(WidgetEvent* aEvent,
nsIContent* aTargetContent) {
if (!aEvent->IsTrusted()) {
@ -1100,17 +1118,19 @@ void EventStateManager::NotifyTargetUserActivation(WidgetEvent* aEvent,
aEvent->mMessage == ePointerDown || aEvent->mMessage == eTouchEnd);
UserActivation::Modifiers modifiers;
if (WidgetInputEvent* inputEvent = aEvent->AsInputEvent()) {
if (inputEvent->IsShift()) {
modifiers.SetShift();
}
if (inputEvent->IsMeta()) {
modifiers.SetMeta();
}
if (inputEvent->IsControl()) {
modifiers.SetControl();
}
if (inputEvent->IsAlt()) {
modifiers.SetAlt();
if (CanReflectModifiersToUserActivation(inputEvent)) {
if (inputEvent->IsShift()) {
modifiers.SetShift();
}
if (inputEvent->IsMeta()) {
modifiers.SetMeta();
}
if (inputEvent->IsControl()) {
modifiers.SetControl();
}
if (inputEvent->IsAlt()) {
modifiers.SetAlt();
}
}
}
doc->NotifyUserGestureActivation(modifiers);

View file

@ -333,6 +333,26 @@ class WidgetKeyboardEvent final : public WidgetInputEvent {
IsAccel()));
}
// Returns true if this event is likely an user activation for a link or
// a link-like button, where modifier keys are likely be used for controlling
// where the link is opened.
//
// This returns false if the keyboard event is more likely an user-defined
// shortcut key.
bool CanReflectModifiersToUserActivation() const {
MOZ_ASSERT(CanUserGestureActivateTarget(),
"Consumer should check CanUserGestureActivateTarget first");
// 'carriage return' and 'space' are supported user gestures for activating
// a link or a button.
// A button often behaves like a link, by calling window.open inside its
// event handler.
//
// Access keys can also activate links/buttons, but access keys have their
// own modifiers, and those modifiers are not appropriate for reflecting to
// the user activation nor controlling where the link is opened.
return mKeyNameIndex == KEY_NAME_INDEX_Enter || mKeyCode == NS_VK_SPACE;
}
[[nodiscard]] bool ShouldWorkAsSpaceKey() const {
if (mKeyCode == NS_VK_SPACE) {
return true;