Bug 1520263 - Make tabbrowser wait reply event for F7 key before toggling caret browsing mode r=smaug

Unfortunately, it seems that `F7` key handling in `tabbrowser.js` cannot
replaced with using XUL `<key>` element because it works with
`ShortcutUtils.getSystemActionForEvent` for mapping `F7` is a toggle key of
caret browsing mode.

Therefore, this patch exposes some raw information of `WidgetEvent` related
to cross process event propagation and makes `tabbrowser.js` check it and
request reply event if it's required.

So, when a remote content has focus, `tabbrowser.js` will do nothing for both
`keydown` and `keypress` of `F7` key with original events, then, will request
a reply event if its default is not prevented.  Finally, reply `F7` keypress
event will toggle caret browsing mode if the event is fired and not consumed.

Differential Revision: https://phabricator.services.mozilla.com/D106591
This commit is contained in:
Masayuki Nakano 2021-02-27 03:02:23 +00:00
parent bf87c0093b
commit e2b7c9c6b0
6 changed files with 163 additions and 2 deletions

View File

@ -4966,6 +4966,34 @@
}
},
/**
* _maybeRequestReplyFromRemoteContent may call
* aEvent.requestReplyFromRemoteContent if necessary.
*
* @param aEvent The handling event.
* @return true if the handler should wait a reply event.
* false if the handle can handle the immediately.
*/
_maybeRequestReplyFromRemoteContent(aEvent) {
if (aEvent.defaultPrevented) {
return false;
}
// If the event target is a remote browser, and the event has not been
// handled by the remote content yet, we should wait a reply event
// from the content.
if (aEvent.isWaitingReplyFromRemoteContent) {
return true; // Somebody called requestReplyFromRemoteContent already.
}
if (
!aEvent.isReplyEventFromRemoteContent &&
aEvent.target?.isRemoteBrowser === true
) {
aEvent.requestReplyFromRemoteContent();
return true;
}
return false;
},
_handleKeyDownEvent(aEvent) {
if (!aEvent.isTrusted) {
// Don't let untrusted events mess with tabs.
@ -4981,6 +5009,9 @@
// navigation should always work for better user experience.
switch (ShortcutUtils.getSystemActionForEvent(aEvent)) {
case ShortcutUtils.TOGGLE_CARET_BROWSING:
this._maybeRequestReplyFromRemoteContent(aEvent);
return;
case ShortcutUtils.MOVE_TAB_BACKWARD:
this.moveTabBackward();
aEvent.preventDefault();
@ -5068,9 +5099,13 @@
switch (ShortcutUtils.getSystemActionForEvent(aEvent, { rtl: RTL_UI })) {
case ShortcutUtils.TOGGLE_CARET_BROWSING:
if (!aEvent.defaultPrevented) {
this.toggleCaretBrowsing();
if (
aEvent.defaultPrevented ||
this._maybeRequestReplyFromRemoteContent(aEvent)
) {
break;
}
this.toggleCaretBrowsing();
break;
case ShortcutUtils.NEXT_TAB:

View File

@ -2,6 +2,8 @@
support-files = head.js
[browser_bookmarks_shortcut.js]
[browser_cancel_caret_browsing_in_content.js]
support-files = file_empty.html
[browser_popup_keyNav.js]
support-files = focusableContent.html
[browser_toolbarButtonKeyPress.js]

View File

@ -0,0 +1,91 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function() {
const kPrefName_CaretBrowsingOn = "accessibility.browsewithcaret";
await SpecialPowers.pushPrefEnv({
set: [
["accessibility.browsewithcaret_shortcut.enabled", true],
["accessibility.warn_on_browsewithcaret", false],
["test.events.async.enabled", true],
[kPrefName_CaretBrowsingOn, false],
],
});
await BrowserTestUtils.withNewTab(
"https://example.com/browser/browser/base/content/test/keyboard/file_empty.html",
async function(browser) {
await SpecialPowers.spawn(browser, [], () => {
content.document.documentElement.scrollTop; // Flush layout.
});
function promiseFirstAndReplyKeyEvents(aExpectedConsume) {
return new Promise(resolve => {
const eventType = aExpectedConsume ? "keydown" : "keypress";
let eventCount = 0;
let listener = () => {
if (++eventCount === 2) {
window.removeEventListener(eventType, listener, {
capture: true,
mozSystemGroup: true,
});
resolve();
}
};
window.addEventListener(eventType, listener, {
capture: true,
mozSystemGroup: true,
});
registerCleanupFunction(() => {
window.removeEventListener(eventType, listener, {
capture: true,
mozSystemGroup: true,
});
});
});
}
let promiseReplyF7KeyEvents = promiseFirstAndReplyKeyEvents(false);
EventUtils.synthesizeKey("KEY_F7");
info("Waiting reply F7 keypress event...");
await promiseReplyF7KeyEvents;
await TestUtils.waitForTick();
is(
Services.prefs.getBoolPref(kPrefName_CaretBrowsingOn),
true,
"F7 key should enable caret browsing mode"
);
await SpecialPowers.pushPrefEnv({
set: [[kPrefName_CaretBrowsingOn, false]],
});
await SpecialPowers.spawn(browser, [], () => {
content.document.documentElement.scrollTop; // Flush layout.
content.window.addEventListener(
"keydown",
event => event.preventDefault(),
{ capture: true }
);
});
promiseReplyF7KeyEvents = promiseFirstAndReplyKeyEvents(true);
EventUtils.synthesizeKey("KEY_F7");
info("Waiting for reply F7 keydown event...");
await promiseReplyF7KeyEvents;
try {
info(`Checking reply keypress event is not fired...`);
await TestUtils.waitForCondition(
() => Services.prefs.getBoolPref(kPrefName_CaretBrowsingOn),
"",
100, // interval
5 // maxTries
);
} catch (e) {}
is(
Services.prefs.getBoolPref(kPrefName_CaretBrowsingOn),
false,
"F7 key shouldn't enable caret browsing mode because F7 keydown event is consumed by web content"
);
}
);
});

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<title>Page left intentionally blank...</title>
</head>
<body>
</body>
</html>

View File

@ -290,6 +290,18 @@ class Event : public nsISupports, public nsWrapperCache {
*/
static void GetWidgetEventType(WidgetEvent* aEvent, nsAString& aType);
void RequestReplyFromRemoteContent() {
mEvent->MarkAsWaitingReplyFromRemoteProcess();
}
bool IsWaitingReplyFromRemoteContent() const {
return mEvent->IsWaitingReplyFromRemoteProcess();
}
bool IsReplyEventFromRemoteContent() const {
return mEvent->IsHandledInRemoteProcess();
}
protected:
// Internal helper functions
void SetEventType(const nsAString& aEventTypeArg);

View File

@ -85,6 +85,19 @@ partial interface Event {
[ChromeOnly] void preventMultipleActions();
[ChromeOnly] readonly attribute boolean multipleActionsPrevented;
[ChromeOnly] readonly attribute boolean isSynthesized;
/**
* When the event target is a remote browser, calling this will fire an
* reply event in the chrome process.
*/
[ChromeOnly] void requestReplyFromRemoteContent();
/**
* Returns true when the event shouldn't be handled by chrome.
*/
[ChromeOnly] readonly attribute boolean isWaitingReplyFromRemoteContent;
/**
* Returns true when the event is a reply event from a remote process.
*/
[ChromeOnly] readonly attribute boolean isReplyEventFromRemoteContent;
};
dictionary EventInit {