forked from mirrors/gecko-dev
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:
parent
7e9c59f478
commit
236646d127
4 changed files with 192 additions and 37 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue