diff --git a/dom/base/test/useractivation/browser.toml b/dom/base/test/useractivation/browser.toml
new file mode 100644
index 000000000000..a924733da94b
--- /dev/null
+++ b/dom/base/test/useractivation/browser.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files = [
+ "../empty.html",
+]
+
+["browser_useractivation_key_events.js"]
diff --git a/dom/base/test/useractivation/browser_useractivation_key_events.js b/dom/base/test/useractivation/browser_useractivation_key_events.js
new file mode 100644
index 000000000000..d6d180a1e606
--- /dev/null
+++ b/dom/base/test/useractivation/browser_useractivation_key_events.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BASE = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const TEST_URL = BASE + "empty.html";
+
+async function synthesizeKeyAndTest(aBrowser, aKey, aEvent, aIsActive) {
+ let promise = SpecialPowers.spawn(
+ aBrowser,
+ [aKey, aEvent, aIsActive],
+ async (key, event, isActive) => {
+ return new Promise(aResolve => {
+ content.document.clearUserGestureActivation();
+ content.document.addEventListener(
+ "keydown",
+ function (e) {
+ e.preventDefault();
+ is(
+ content.document.hasBeenUserGestureActivated,
+ isActive,
+ `check has-been-user-activated for ${key} with ${JSON.stringify(event)}`
+ );
+ is(
+ content.document.hasValidTransientUserGestureActivation,
+ isActive,
+ `check has-valid-transient-user-activation for ${key} with ${JSON.stringify(event)}`
+ );
+ aResolve();
+ },
+ { once: true }
+ );
+ });
+ }
+ );
+ // Ensure the event listener has registered on the remote.
+ await SpecialPowers.spawn(aBrowser, [], () => {
+ return new Promise(resolve => {
+ SpecialPowers.executeSoon(resolve);
+ });
+ });
+ EventUtils.synthesizeKey(aKey, aEvent);
+ return promise;
+}
+
+let browser;
+add_setup(async function setup() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ browser = tab.linkedBrowser;
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+add_task(async function TestPrintableKey() {
+ let tests = ["a", "b", "c", "A", "B", "1", "2", "3"];
+
+ for (let key of tests) {
+ await synthesizeKeyAndTest(browser, key, {}, true);
+ }
+});
+
+add_task(async function TestNonPrintableKey() {
+ let tests = [
+ ["KEY_Backspace", false],
+ ["KEY_Control", false],
+ ["KEY_Shift", false],
+ ["KEY_Escape", false],
+ // Treat as user input
+ ["KEY_Tab", true],
+ ["KEY_Enter", true],
+ [" ", true],
+ ];
+
+ for (let [key, expectedResult] of tests) {
+ await synthesizeKeyAndTest(browser, key, {}, expectedResult);
+ }
+});
+
+add_task(async function TestModifier() {
+ let tests = [
+ ["a", { accelKey: true }, false],
+ ["z", { accelKey: true }, false],
+ ["a", { metaKey: true }, !navigator.platform.includes("Mac")],
+ // Treat as user input
+ ["a", { altGraphKey: true }, true],
+ ["a", { fnKey: true }, true],
+ ["a", { altKey: true }, true],
+ ["a", { shiftKey: true }, true],
+ ["c", { altKey: true }, true],
+ ["c", { accelKey: true }, true],
+ ["v", { altKey: true }, true],
+ ["v", { accelKey: true }, true],
+ ["x", { altKey: true }, true],
+ ["x", { accelKey: true }, true],
+ ];
+
+ for (let [key, event, expectedResult] of tests) {
+ await synthesizeKeyAndTest(browser, key, event, expectedResult);
+ }
+});
diff --git a/dom/base/test/useractivation/mochitest.toml b/dom/base/test/useractivation/mochitest.toml
index 1e7645a725a2..c959f3bf5dd5 100644
--- a/dom/base/test/useractivation/mochitest.toml
+++ b/dom/base/test/useractivation/mochitest.toml
@@ -30,8 +30,6 @@ support-files = ["file_self_close.html"]
["test_useractivation_has_been_activated.html"]
-["test_useractivation_key_events.html"]
-
["test_useractivation_open_new_window.html"]
support-files = ["file_self_close.html"]
diff --git a/dom/base/test/useractivation/moz.build b/dom/base/test/useractivation/moz.build
index d8b7f8909498..3ac1d85dffb0 100644
--- a/dom/base/test/useractivation/moz.build
+++ b/dom/base/test/useractivation/moz.build
@@ -11,3 +11,7 @@ MOCHITEST_MANIFESTS += [
MOCHITEST_CHROME_MANIFESTS += [
"chrome.toml",
]
+
+BROWSER_CHROME_MANIFESTS += [
+ "browser.toml",
+]
diff --git a/dom/base/test/useractivation/test_useractivation_key_events.html b/dom/base/test/useractivation/test_useractivation_key_events.html
deleted file mode 100644
index 53517fe685ca..000000000000
--- a/dom/base/test/useractivation/test_useractivation_key_events.html
+++ /dev/null
@@ -1,87 +0,0 @@
-
-
-
- User activation test: key events
-
-
-
-
-
-
-
diff --git a/dom/events/GlobalKeyListener.cpp b/dom/events/GlobalKeyListener.cpp
index 001ca4eedfa4..1a51a8ad9495 100644
--- a/dom/events/GlobalKeyListener.cpp
+++ b/dom/events/GlobalKeyListener.cpp
@@ -223,7 +223,9 @@ void GlobalKeyListener::HandleEventOnCaptureInSystemEventGroup(
return;
}
- if (!HasHandlerForEvent(aEvent).mMeaningfulHandlerFound) {
+ WalkHandlersResult result = HasHandlerForEvent(aEvent);
+ widgetEvent->mFlags.mIsShortcutKey |= result.mRelevantHandlerFound;
+ if (!result.mMeaningfulHandlerFound) {
return;
}
@@ -261,6 +263,7 @@ GlobalKeyListener::WalkHandlersResult GlobalKeyListener::WalkHandlersInternal(
}
bool foundDisabledHandler = false;
+ bool foundRelevantHandler = false;
for (const ShortcutKeyCandidate& key : shortcutKeys) {
const bool skipIfEarlierHandlerDisabled =
key.mSkipIfEarlierHandlerDisabled ==
@@ -276,6 +279,7 @@ GlobalKeyListener::WalkHandlersResult GlobalKeyListener::WalkHandlersInternal(
if (result.mMeaningfulHandlerFound) {
return result;
}
+ foundRelevantHandler |= result.mRelevantHandlerFound;
// Note that if the candidate should not match if an earlier handler is
// disabled, the char code of the candidate is a char which may be
// introduced with different shift state. In this case, we do NOT find a
@@ -286,7 +290,9 @@ GlobalKeyListener::WalkHandlersResult GlobalKeyListener::WalkHandlersInternal(
foundDisabledHandler = result.mDisabledHandlerFound;
}
}
- return {};
+ WalkHandlersResult result;
+ result.mRelevantHandlerFound = foundRelevantHandler;
+ return result;
}
GlobalKeyListener::WalkHandlersResult GlobalKeyListener::WalkHandlersAndExecute(
@@ -303,6 +309,7 @@ GlobalKeyListener::WalkHandlersResult GlobalKeyListener::WalkHandlersAndExecute(
// Try all of the handlers until we find one that matches the event.
bool foundDisabledHandler = false;
+ bool foundRelevantHandler = false;
for (KeyEventHandler* handler = mHandler; handler;
handler = handler->GetNextHandler()) {
bool stopped = aKeyEvent->IsDispatchStopped();
@@ -352,6 +359,7 @@ GlobalKeyListener::WalkHandlersResult GlobalKeyListener::WalkHandlersAndExecute(
result.mMeaningfulHandlerFound = true;
result.mReservedHandlerForChromeFound =
IsReservedKey(widgetKeyboardEvent, handler);
+ result.mRelevantHandlerFound = true;
return result;
}
@@ -364,8 +372,10 @@ GlobalKeyListener::WalkHandlersResult GlobalKeyListener::WalkHandlersAndExecute(
WalkHandlersResult result;
result.mMeaningfulHandlerFound = true;
result.mReservedHandlerForChromeFound = true;
+ result.mRelevantHandlerFound = true;
return result;
}
+ foundRelevantHandler = true;
}
// Otherwise, we've not found a handler for the event yet.
continue;
@@ -382,6 +392,7 @@ GlobalKeyListener::WalkHandlersResult GlobalKeyListener::WalkHandlersAndExecute(
result.mReservedHandlerForChromeFound =
IsReservedKey(widgetKeyboardEvent, handler);
result.mDisabledHandlerFound = (rv == NS_SUCCESS_DOM_NO_OPERATION);
+ result.mRelevantHandlerFound = true;
return result;
}
}
@@ -401,6 +412,7 @@ GlobalKeyListener::WalkHandlersResult GlobalKeyListener::WalkHandlersAndExecute(
WalkHandlersResult result;
result.mDisabledHandlerFound = foundDisabledHandler;
+ result.mRelevantHandlerFound = foundRelevantHandler;
return result;
}
diff --git a/dom/events/GlobalKeyListener.h b/dom/events/GlobalKeyListener.h
index d3048e60a8e1..babb1ed818e0 100644
--- a/dom/events/GlobalKeyListener.h
+++ b/dom/events/GlobalKeyListener.h
@@ -69,6 +69,9 @@ class GlobalKeyListener : public nsIDOMEventListener {
bool mReservedHandlerForChromeFound = false;
// Set to true if found handler is disabled.
bool mDisabledHandlerFound = false;
+ // Set to true if a command is found but may correspond to a different type
+ // of keyboard event.
+ bool mRelevantHandlerFound = false;
};
// walk the handlers, looking for one to handle the event
diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html b/dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html
index fc8c47065e48..aa58866a9857 100644
--- a/dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html
+++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html
@@ -31,15 +31,11 @@
// Keys that are expected to be not considered interaction with the page, and
// so not gesture activate the document.
let blacklistKeyPresses = [
- "Tab",
"CapsLock",
"NumLock",
"ScrollLock",
"FnLock",
"Meta",
- "Hyper",
- "Super",
- "ContextMenu",
"ArrowUp",
"ArrowDown",
"ArrowLeft",
@@ -48,7 +44,6 @@
"PageDown",
"Home",
"End",
- "Backspace",
"Fn",
"Alt",
"AltGraph",
@@ -57,14 +52,58 @@
"Escape",
];
+ // XXX not sure why Android behave different on handling Backspace key.
+ if (!navigator.appVersion.includes("Android")) {
+ blacklistKeyPresses.push("Backspace");
+ }
+
let modifiedKeys = [
- { key: "V", modifiers: { altKey: true, shiftKey: true } },
- { key: "a", modifiers: { altKey: true } },
- { key: "a", modifiers: { ctrlKey: true } },
- { key: "KEY_ArrowRight", modifiers: { metaKey: true } },
- { key: "KEY_ArrowRight", modifiers: { altKey: true } },
+ { key: "a", modifiers: { accelKey: true } },
+ { key: "z", modifiers: { accelKey: true } },
];
+ // Browser shortcut on non-Linux platform.
+ if (navigator.platform.indexOf("Linux") !== 0) {
+ modifiedKeys.push(
+ { key: "KEY_ArrowRight", modifiers: { metaKey: true } },
+ { key: "KEY_ArrowRight", modifiers: { altKey: true } });
+ }
+
+ let script = SpecialPowers.loadChromeScript(function() {
+ /* eslint-env mozilla/chrome-script */
+ var EventUtils = {};
+ EventUtils.window = {};
+ EventUtils._EU_Ci = Ci;
+ EventUtils._EU_Cc = Cc;
+ Services.scriptloader
+ .loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils);
+
+ addMessageListener("synthesizeKey", function(data) {
+ let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!browserWin) {
+ browserWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ EventUtils.synthesizeKey(data.key, data.event, browserWin);
+ });
+ });
+
+ async function synthesizeKeyAndWait(key, event = {}) {
+ let promise = new Promise(aResolve => {
+ document.addEventListener(
+ "keydown",
+ function (e) {
+ e.preventDefault();
+ info("Received keydown event: " + e.key);
+ aResolve();
+ },
+ { once: true }
+ );
+ });
+ script.sendAsyncMessage("synthesizeKey", { key, event });
+ await promise;
+ }
+
async function sendInput(element, name, input) {
synthesizeMouseAtCenter(input, {});
let played = await element.play().then(() => true, () => false);
@@ -107,7 +146,7 @@
for (let key of blacklistKeyPresses) {
document.body.focus();
- synthesizeKey("KEY_" + key);
+ await synthesizeKeyAndWait("KEY_" + key);
played = await element.play().then(() => true, () => false);
is(played, false, "Key " + key + " should not activate document and should not unblock play");
}
@@ -116,14 +155,14 @@
let keyNames = (m) => Object.keys(m).join("+");
for (let x of modifiedKeys) {
document.body.focus();
- synthesizeKey(x.key, x.modifiers);
+ await synthesizeKeyAndWait(x.key, x.modifiers);
played = await element.play().then(() => true, () => false);
is(played, false, "Key (" + x.key + "+" + keyNames(x.modifiers) + ") should not activate document and should not unblock play");
}
// Try pressing a key not in the blacklist, then playing.
// Document should be activated, and media should play.
- synthesizeKey(" ");
+ await synthesizeKeyAndWait(" ");
played = await element.play().then(() => true, () => false);
is(played, true, "Space key should activate document and should unblock play");
diff --git a/widget/BasicEvents.h b/widget/BasicEvents.h
index 22d9486b7d13..c9e43d5f7797 100644
--- a/widget/BasicEvents.h
+++ b/widget/BasicEvents.h
@@ -162,6 +162,9 @@ struct BaseEventFlags {
// Certain mouse events can be marked as positionless to return 0 from
// coordinate related getters.
bool mIsPositionless : 1;
+ // Indicates if a key handler is registered to execute a command for the key
+ // combination.
+ bool mIsShortcutKey : 1;
// Flags managing state of propagation between processes.
// Note the the following flags shouldn't be referred directly. Use utility
diff --git a/widget/TextEvents.h b/widget/TextEvents.h
index 41bb624c9d0d..7490e6884136 100644
--- a/widget/TextEvents.h
+++ b/widget/TextEvents.h
@@ -310,28 +310,29 @@ class WidgetKeyboardEvent final : public WidgetInputEvent {
}
bool CanUserGestureActivateTarget() const {
- // Printable keys, 'carriage return' and 'space' are supported user gestures
- // for activating the document. However, if supported key is being pressed
- // combining with other operation keys, such like alt, control ..etc., we
- // won't activate the target for them because at that time user might
- // interact with browser or window manager which doesn't necessarily
- // demonstrate user's intent to play media.
- const bool isCombiningWithOperationKeys = (IsControl() && !IsAltGraph()) ||
- (IsAlt() && !IsAltGraph()) ||
- IsMeta();
- const bool isEnterOrSpaceKey =
- mKeyNameIndex == KEY_NAME_INDEX_Enter || mKeyCode == NS_VK_SPACE;
- return (PseudoCharCode() || isEnterOrSpaceKey) &&
- (!isCombiningWithOperationKeys ||
- // ctrl-c/ctrl-x/ctrl-v is quite common shortcut for clipboard
- // operation.
- // XXXedgar, we have to find a better way to handle browser keyboard
- // shortcut for user activation, instead of just ignoring all
- // combinations, see bug 1641171.
- ((mKeyCode == dom::KeyboardEvent_Binding::DOM_VK_C ||
- mKeyCode == dom::KeyboardEvent_Binding::DOM_VK_V ||
- mKeyCode == dom::KeyboardEvent_Binding::DOM_VK_X) &&
- IsAccel()));
+ if (IsModifierKeyEvent()) {
+ return false;
+ }
+
+ if (mFlags.mIsShortcutKey) {
+ // Space is quite common shortcut for playing media.
+ return mKeyCode == NS_VK_SPACE ||
+ // ctrl-c/ctrl-x/ctrl-v is quite common shortcut for clipboard
+ // operation.
+ // XXXedgar, we probably could improve this by referring to
+ // EditCommandsConstRef() if we're sure the event target on Linux
+ // and macOS is active with any edit commands.
+ ((mKeyCode == dom::KeyboardEvent_Binding::DOM_VK_C ||
+ mKeyCode == dom::KeyboardEvent_Binding::DOM_VK_V ||
+ mKeyCode == dom::KeyboardEvent_Binding::DOM_VK_X) &&
+ IsAccel());
+ }
+
+ // ESC key is ususally used to exit some state, it should not be considered
+ // as a user activation key to avoid page requests to enter again the same
+ // state to trap the user.
+ // https://html.spec.whatwg.org/multipage/interaction.html#activation-triggering-input-event
+ return mKeyNameIndex != KEY_NAME_INDEX_Escape;
}
// Returns true if this event is likely an user activation for a link or