commit 6f4719b83cd84eda4fb6d88f6dcd778381766331
parent 85c86acf14255b234b99e60ea295708012e92fb1
Author: James Teh <jteh@mozilla.com>
Date: Wed, 12 Nov 2025 05:14:26 +0000
Bug 1635774 part 2: Functionality to support customisation of keyboard shortcuts. r=mossop
This adds a CustomKeys module which provides the functionality to customise keyboard shortcuts defined by <key> elements.
Customisations are saved to and loaded from a JSON configuration file.
This is hooked into browser-init so that it can apply to all browser windows.
Differential Revision: https://phabricator.services.mozilla.com/D266791
Diffstat:
7 files changed, 697 insertions(+), 0 deletions(-)
diff --git a/browser/base/content/browser-init.js b/browser/base/content/browser-init.js
@@ -3,6 +3,10 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+const { CustomKeys } = ChromeUtils.importESModule(
+ "resource:///modules/CustomKeys.sys.mjs"
+);
+
let _resolveDelayedStartup;
var delayedStartupPromise = new Promise(resolve => {
_resolveDelayedStartup = resolve;
@@ -265,6 +269,7 @@ var gBrowserInit = {
if (gToolbarKeyNavEnabled) {
ToolbarKeyboardNavigator.init();
}
+ CustomKeys.initWindow(window);
// Update UI if browser is under remote control.
gRemoteControl.updateVisualCue();
@@ -1083,6 +1088,7 @@ var gBrowserInit = {
if (gToolbarKeyNavEnabled) {
ToolbarKeyboardNavigator.uninit();
}
+ CustomKeys.uninitWindow(window);
// Bug 1952900 to allow switching to unload category without leaking
ChromeUtils.importESModule(
diff --git a/browser/components/customkeys/CustomKeys.sys.mjs b/browser/components/customkeys/CustomKeys.sys.mjs
@@ -0,0 +1,273 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { JSONFile } from "resource://gre/modules/JSONFile.sys.mjs";
+
+// All the attributes for a <key> element that might specify the key to press.
+// We include data-l10n-id because keys are often specified using Fluent. Note
+// that we deliberately remove data-l10n-id when a key is customized because
+// otherwise, Fluent would overwrite the customization.
+const ATTRS = ["data-l10n-id", "key", "keycode", "modifiers"];
+
+// The original keys for any shortcuts that have been customized. This is used
+// to identify whether a shortcut has been customized, as well as to reset a
+// shortcut.
+const original = {};
+// All the browser windows we are handling. Maps from a window to our
+// MutationObserver for that window.
+const windows = new Map();
+
+// The configuration is an Object with the form:
+// { "someKeyId": {
+// modifiers: "mod1,mod2",
+// key: "someKey",
+// keycode: "someKeycode"
+// } }
+// Only one of `key` or `keycode` should be specified for each entry. An entry
+// may be empty to indicate that the default key has been cleared; i.e.
+// { "someKeyId": {} }
+const config = new JSONFile({
+ path: PathUtils.join(PathUtils.profileDir, "customKeys.json"),
+});
+
+function applyToKeyEl(window, keyId, keysets) {
+ const keyEl = window.document.getElementById(keyId);
+ if (!keyEl || keyEl.tagName != "key") {
+ return;
+ }
+ if (!original[keyId]) {
+ // Save the original key in case the user wants to reset.
+ const orig = (original[keyId] = {});
+ for (const attr of ATTRS) {
+ const val = keyEl.getAttribute(attr);
+ if (val) {
+ orig[attr] = val;
+ }
+ }
+ }
+ for (const attr of ATTRS) {
+ keyEl.removeAttribute(attr);
+ }
+ const data = config.data[keyId];
+ for (const attr of ["modifiers", "key", "keycode"]) {
+ const val = data[attr];
+ if (val) {
+ keyEl.setAttribute(attr, val);
+ }
+ }
+ keysets.add(keyEl.parentElement);
+}
+
+function resetKeyEl(window, keyId, keysets) {
+ const keyEl = window.document.getElementById(keyId);
+ if (!keyEl) {
+ return;
+ }
+ const orig = original[keyId];
+ for (const attr of ATTRS) {
+ keyEl.removeAttribute(attr);
+ const val = orig[attr];
+ if (val !== undefined) {
+ keyEl.setAttribute(attr, val);
+ }
+ }
+ keysets.add(keyEl.parentElement);
+}
+
+async function applyToNewWindow(window) {
+ await config.load();
+ const keysets = new Set();
+ for (const keyId in config.data) {
+ applyToKeyEl(window, keyId, keysets);
+ }
+ refreshKeysets(window, keysets);
+ observe(window);
+}
+
+function refreshKeysets(window, keysets) {
+ if (keysets.size == 0) {
+ return;
+ }
+ const observer = windows.get(window);
+ if (observer) {
+ // We don't want our MutationObserver to process this.
+ observer.disconnect();
+ }
+ // Gecko doesn't watch for changes to key elements. It only sets up a key
+ // element when its keyset is bound to the tree. See:
+ // https://searchfox.org/mozilla-central/rev/7857ea04d142f2abc0d777085f9e54526c7cedf9/dom/xul/nsXULElement.cpp#587
+ // Therefore, remove and re-add any modified keysets to apply the changes.
+ for (const keyset of keysets) {
+ const parent = keyset.parentElement;
+ keyset.remove();
+ parent.append(keyset);
+ }
+ if (observer) {
+ observe(window);
+ }
+}
+
+function observe(window) {
+ let observer = windows.get(window);
+ if (!observer) {
+ // A keyset can be added dynamically. For example, DevTools does this during
+ // delayed startup. Note that key elements cannot be added dynamically. We
+ // know this because Gecko doesn't watch for changes to key elements. It
+ // only sets up a key element when its keyset is bound to the tree. See:
+ // https://searchfox.org/mozilla-central/rev/7857ea04d142f2abc0d777085f9e54526c7cedf9/dom/xul/nsXULElement.cpp#587
+ observer = new window.MutationObserver(mutations => {
+ const keysets = new Set();
+ for (const mutation of mutations) {
+ for (const node of mutation.addedNodes) {
+ if (node.tagName != "keyset") {
+ continue;
+ }
+ for (const key of node.children) {
+ if (key.tagName != "key" || !config.data[key.id]) {
+ continue;
+ }
+ applyToKeyEl(window, key.id, keysets);
+ }
+ }
+ }
+ refreshKeysets(window, keysets);
+ });
+ windows.set(window, observer);
+ }
+ for (const node of [window.document, window.document.body]) {
+ observer.observe(node, { childList: true });
+ }
+ return observer;
+}
+
+export const CustomKeys = {
+ /**
+ * Customize a keyboard shortcut.
+ *
+ * @param {string} id The id of the key element.
+ * @param {object} data
+ * @param {string} data.modifiers The modifiers for the key; e.g. "accel,shift".
+ * @param {string} data.key The key itself; e.g. "Y". Mutually exclusive with data.keycode.
+ * @param {string} data.keycode The key code; e.g. VK_F1. Mutually exclusive with data.key.
+ */
+ changeKey(id, { modifiers, key, keycode }) {
+ const existing = config.data[id];
+ if (
+ existing &&
+ modifiers == existing.modifiers &&
+ key == existing.key &&
+ keycode == existing.keycode
+ ) {
+ return; // No change.
+ }
+ const defaultKey = this.getDefaultKey(id);
+ if (
+ defaultKey &&
+ modifiers == defaultKey.modifiers &&
+ key == defaultKey.key &&
+ keycode == defaultKey.keycode
+ ) {
+ this.resetKey(id);
+ return;
+ }
+ const data = (config.data[id] = {});
+ if (modifiers) {
+ data.modifiers = modifiers;
+ }
+ if (key) {
+ data.key = key;
+ }
+ if (keycode) {
+ data.keycode = keycode;
+ }
+ for (const window of windows.keys()) {
+ const keysets = new Set();
+ applyToKeyEl(window, id, keysets);
+ refreshKeysets(window, keysets);
+ }
+ config.saveSoon();
+ },
+
+ /**
+ * Reset a keyboard shortcut to its defalt.
+ *
+ * @param {string} id The id of the key element.
+ */
+ resetKey(id) {
+ if (!config.data[id]) {
+ return; // No change.
+ }
+ delete config.data[id];
+ for (const window of windows.keys()) {
+ const keysets = new Set();
+ resetKeyEl(window, id, keysets);
+ refreshKeysets(window, keysets);
+ }
+ delete original[id];
+ config.saveSoon();
+ },
+
+ /**
+ * Clear a keyboard shortcut; i.e. so it does nothing.
+ *
+ * @param {string} id The id of the key element.
+ */
+ clearKey(id) {
+ this.changeKey(id, {});
+ },
+
+ /**
+ * Reset all keyboard shortcuts to their defaults.
+ */
+ resetAll() {
+ config.data = {};
+ for (const window of windows.keys()) {
+ const keysets = new Set();
+ for (const id in original) {
+ resetKeyEl(window, id, keysets);
+ }
+ refreshKeysets(window, keysets);
+ }
+ for (const id of Object.keys(original)) {
+ delete original[id];
+ }
+ config.saveSoon();
+ },
+
+ initWindow(window) {
+ applyToNewWindow(window);
+ },
+
+ uninitWindow(window) {
+ windows.get(window).disconnect();
+ windows.delete(window);
+ },
+
+ /**
+ * Return the default key for this shortcut in the form:
+ * { modifiers: "mod1,mod2", key: "someKey", keycode: "someKeycode" }
+ * If this shortcut isn't customized, return null.
+ *
+ * @param {string} keyId The id of the key element.
+ */
+ getDefaultKey(keyId) {
+ const origKey = original[keyId];
+ if (!origKey) {
+ return null;
+ }
+ const data = {};
+ if (origKey.modifiers) {
+ data.modifiers = origKey.modifiers;
+ }
+ if (origKey.key) {
+ data.key = origKey.key;
+ }
+ if (origKey.keycode) {
+ data.keycode = origKey.keycode;
+ }
+ return data;
+ },
+};
+
+Object.freeze(CustomKeys);
diff --git a/browser/components/customkeys/moz.build b/browser/components/customkeys/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES += [
+ "CustomKeys.sys.mjs",
+]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Keyboard Navigation")
diff --git a/browser/components/customkeys/tests/browser/browser.toml b/browser/components/customkeys/tests/browser/browser.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+support-files = ["head.js"]
+
+["browser_CustomKeys.js"]
diff --git a/browser/components/customkeys/tests/browser/browser_CustomKeys.js b/browser/components/customkeys/tests/browser/browser_CustomKeys.js
@@ -0,0 +1,374 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test the behavior of assigning keys, resetting keys, etc. via the CustomKeys module. The UI is tested separately.
+ */
+
+add_setup(async function () {
+ // We need to be sure that the foreground tab has fully loaded before running
+ // any tests. Otherwise, when keyboard events are sent to the remote process,
+ // there is a race condition where they sometimes won't be handled again in
+ // the parent process afterward.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ registerCleanupFunction(function () {
+ CustomKeys.resetAll();
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Test changing a key.
+add_task(async function testChangeKey() {
+ is(
+ document.activeElement,
+ gBrowser.selectedBrowser,
+ "Tab document browser is focused"
+ );
+
+ is(
+ CustomKeys.getDefaultKey("key_gotoHistory"),
+ null,
+ "key_gotoHistory is not customized"
+ );
+ info(`Changing key_gotoHistory to ${consts.unusedDisplay}`);
+ CustomKeys.changeKey("key_gotoHistory", {
+ modifiers: consts.unusedModifiers,
+ key: consts.unusedKey,
+ });
+ Assert.deepEqual(
+ CustomKeys.getDefaultKey("key_gotoHistory"),
+ { modifiers: consts.historyModifiers, key: "H" },
+ "key_gotoHistory is customized"
+ );
+ info(`Pressing ${consts.unusedDisplay}`);
+ let focused = BrowserTestUtils.waitForEvent(window, "SidebarFocused");
+ EventUtils.synthesizeKey(consts.unusedKey, consts.unusedOptions, window);
+ await focused;
+ ok(true, "Sidebar got focus");
+ is(
+ SidebarController.currentID,
+ "viewHistorySidebar",
+ "History sidebar is open"
+ );
+ info(`Pressing ${consts.unusedDisplay}`);
+ focused = BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "focus");
+ EventUtils.synthesizeKey(consts.unusedKey, consts.unusedOptions, window);
+ await focused;
+ ok(true, "Tab document browser got focus");
+
+ is(
+ CustomKeys.getDefaultKey("viewBookmarksSidebarKb"),
+ null,
+ "viewBookmarksSidebarKb is not customized"
+ );
+ info(`Changing viewBookmarksSidebarKb to ${consts.historyDisplay}`);
+ CustomKeys.changeKey("viewBookmarksSidebarKb", {
+ modifiers: consts.historyModifiers,
+ key: "H",
+ });
+ Assert.deepEqual(
+ CustomKeys.getDefaultKey("viewBookmarksSidebarKb"),
+ { modifiers: "accel", key: "B" },
+ "viewBookmarksSidebarKb is customized"
+ );
+ info(`Pressing ${consts.historyDisplay}`);
+ focused = BrowserTestUtils.waitForEvent(window, "SidebarFocused");
+ EventUtils.synthesizeKey("H", consts.historyOptions, window);
+ await focused;
+ ok(true, "Sidebar got focus");
+ is(
+ SidebarController.currentID,
+ "viewBookmarksSidebar",
+ "Bookmarks sidebar is open"
+ );
+ info(`Pressing ${consts.historyDisplay}`);
+ focused = BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "focus");
+ EventUtils.synthesizeKey("H", consts.historyOptions, window);
+ await focused;
+ ok(true, "Tab document browser got focus");
+
+ info("Resetting all keys");
+ CustomKeys.resetAll();
+});
+
+// Test clearing a key.
+add_task(async function testClearKey() {
+ is(
+ document.activeElement,
+ gBrowser.selectedBrowser,
+ "Tab document browser is focused"
+ );
+ // Move focus into chrome so that accel+l to focus the URL bar reliably occurs
+ // immediately. We need this guarantee because we want to test when pressing
+ // accel+l does nothing, so we can't rely on an event for that test.
+ info("Focusing selected tab");
+ let focused = BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "focus");
+ gBrowser.selectedTab.focus();
+ await focused;
+ ok(true, "Selected tab got focus");
+
+ is(
+ CustomKeys.getDefaultKey("focusURLBar"),
+ null,
+ "focusURLBar is not customized"
+ );
+ info("Pressing accel+L");
+ EventUtils.synthesizeKey("L", { accelKey: true }, window);
+ is(document.activeElement, gURLBar.inputField, "URL bar is focused");
+ info("Focusing selected tab");
+ focused = BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "focus");
+ gBrowser.selectedTab.focus();
+ await focused;
+ ok(true, "Selected tab got focus");
+
+ info("Clearing focusURLBar");
+ CustomKeys.clearKey("focusURLBar");
+ Assert.deepEqual(
+ CustomKeys.getDefaultKey("focusURLBar"),
+ { modifiers: "accel", key: "L" },
+ "focusURLBar is customized"
+ );
+ info("Pressing accel+L");
+ EventUtils.synthesizeKey("L", { accelKey: true }, window);
+ is(
+ document.activeElement,
+ gBrowser.selectedTab,
+ "Selected tab still focused"
+ );
+
+ // The tab bar has focus now. We need to move focus back to the document
+ // because otherwise, the focus will remain on the tab bar for the next test.
+ info("Focusing tab document browser");
+ focused = BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "focus");
+ gBrowser.selectedBrowser.focus();
+ await focused;
+ ok(true, "Tab document browser got focus");
+ info("Resetting all keys");
+ CustomKeys.resetAll();
+});
+
+// Test resetting a key.
+add_task(async function testResetKey() {
+ is(
+ document.activeElement,
+ gBrowser.selectedBrowser,
+ "Tab document browser is focused"
+ );
+
+ is(
+ CustomKeys.getDefaultKey("focusURLBar"),
+ null,
+ "focusURLBar is not customized"
+ );
+ info("Clearing focusURLBar");
+ CustomKeys.clearKey("focusURLBar");
+ Assert.deepEqual(
+ CustomKeys.getDefaultKey("focusURLBar"),
+ { modifiers: "accel", key: "L" },
+ "focusURLBar is customized"
+ );
+
+ info("Resetting focusURLBar");
+ CustomKeys.resetKey("focusURLBar");
+ is(
+ CustomKeys.getDefaultKey("focusURLBar"),
+ null,
+ "focusURLBar is not customized"
+ );
+ info("Pressing accel+L");
+ let focused = BrowserTestUtils.waitForEvent(gURLBar.inputField, "focus");
+ EventUtils.synthesizeKey("L", { accelKey: true }, window);
+ await focused;
+ ok(true, "URL bar got focus");
+ info("Focusing tab document browser");
+ focused = BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "focus");
+ gBrowser.selectedBrowser.focus();
+ await focused;
+ ok(true, "Tab document browser got focus");
+
+ info("Resetting all keys");
+ CustomKeys.resetAll();
+});
+
+// Test resetting all keys.
+add_task(async function testResetAll() {
+ is(
+ document.activeElement,
+ gBrowser.selectedBrowser,
+ "Tab document browser is focused"
+ );
+
+ is(
+ CustomKeys.getDefaultKey("key_gotoHistory"),
+ null,
+ "key_gotoHistory is not customized"
+ );
+ info(`Changing key_gotoHistory to ${consts.unusedDisplay}`);
+ CustomKeys.changeKey("key_gotoHistory", {
+ modifiers: consts.unusedModifiers,
+ key: consts.unusedKey,
+ });
+ Assert.deepEqual(
+ CustomKeys.getDefaultKey("key_gotoHistory"),
+ { modifiers: consts.historyModifiers, key: "H" },
+ "key_gotoHistory is customized"
+ );
+
+ is(
+ CustomKeys.getDefaultKey("focusURLBar"),
+ null,
+ "focusURLBar is not customized"
+ );
+ info("Clearing focusURLBar");
+ CustomKeys.clearKey("focusURLBar");
+ Assert.deepEqual(
+ CustomKeys.getDefaultKey("focusURLBar"),
+ { modifiers: "accel", key: "L" },
+ "focusURLBar is customized"
+ );
+
+ info("Resetting all keys");
+ CustomKeys.resetAll();
+
+ info(`Pressing ${consts.historyDisplay}`);
+ let focused = BrowserTestUtils.waitForEvent(window, "SidebarFocused");
+ EventUtils.synthesizeKey("H", consts.historyOptions, window);
+ await focused;
+ ok(true, "Sidebar got focus");
+ is(
+ SidebarController.currentID,
+ "viewHistorySidebar",
+ "History sidebar is open"
+ );
+ info(`Pressing ${consts.historyDisplay}`);
+ focused = BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "focus");
+ EventUtils.synthesizeKey("H", consts.historyOptions, window);
+ await focused;
+ ok(true, "Tab document browser got focus");
+
+ info("Pressing accel+L");
+ focused = BrowserTestUtils.waitForEvent(gURLBar.inputField, "focus");
+ EventUtils.synthesizeKey("L", { accelKey: true }, window);
+ await focused;
+ ok(true, "URL bar got focus");
+ info("Focusing tab document browser");
+ focused = BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "focus");
+ gBrowser.selectedBrowser.focus();
+ await focused;
+ ok(true, "Tab document browser got focus");
+});
+
+// Test that changes apply to other windows.
+add_task(async function testOtherWindow() {
+ // Test changing a key before a new window is opened.
+ is(
+ CustomKeys.getDefaultKey("key_gotoHistory"),
+ null,
+ "key_gotoHistory is not customized"
+ );
+ info(`Changing key_gotoHistory to ${consts.unusedDisplay}`);
+ CustomKeys.changeKey("key_gotoHistory", {
+ modifiers: consts.unusedModifiers,
+ key: consts.unusedKey,
+ });
+ Assert.deepEqual(
+ CustomKeys.getDefaultKey("key_gotoHistory"),
+ { modifiers: consts.historyModifiers, key: "H" },
+ "key_gotoHistory is customized"
+ );
+
+ info("Opening new window");
+ const newWin = await BrowserTestUtils.openNewBrowserWindow();
+ is(
+ newWin.document.activeElement,
+ newWin.gURLBar.inputField,
+ "URL bar is focused in new window"
+ );
+
+ info(`Pressing ${consts.unusedDisplay} in new window`);
+ let focused = BrowserTestUtils.waitForEvent(newWin, "SidebarFocused");
+ EventUtils.synthesizeKey(consts.unusedKey, consts.unusedOptions, newWin);
+ await focused;
+ ok(true, "Sidebar got focus in new window");
+ is(
+ newWin.SidebarController.currentID,
+ "viewHistorySidebar",
+ "History sidebar is open in new window"
+ );
+ info(`Pressing ${consts.unusedDisplay} in new window`);
+ focused = BrowserTestUtils.waitForEvent(
+ newWin.gBrowser.selectedBrowser,
+ "focus"
+ );
+ EventUtils.synthesizeKey(consts.unusedKey, consts.unusedOptions, newWin);
+ await focused;
+ ok(true, "Tab document browser got focus in new window");
+
+ // Test changing a key after a new window is opened.
+ is(
+ CustomKeys.getDefaultKey("viewBookmarksSidebarKb"),
+ null,
+ "viewBookmarksSidebarKb is not customized"
+ );
+ info(`Changing viewBookmarksSidebarKb to ${consts.historyDisplay}`);
+ CustomKeys.changeKey("viewBookmarksSidebarKb", {
+ modifiers: consts.historyModifiers,
+ key: "H",
+ });
+ Assert.deepEqual(
+ CustomKeys.getDefaultKey("viewBookmarksSidebarKb"),
+ { modifiers: "accel", key: "B" },
+ "viewBookmarksSidebarKb is customized"
+ );
+ info(`Pressing ${consts.historyDisplay} in new window`);
+ focused = BrowserTestUtils.waitForEvent(newWin, "SidebarFocused");
+ EventUtils.synthesizeKey("H", consts.historyOptions, newWin);
+ await focused;
+ ok(true, "Sidebar got focus in new window");
+ is(
+ newWin.SidebarController.currentID,
+ "viewBookmarksSidebar",
+ "Bookmarks sidebar is open"
+ );
+ info(`Pressing ${consts.historyDisplay} in new window`);
+ focused = BrowserTestUtils.waitForEvent(
+ newWin.gBrowser.selectedBrowser,
+ "focus"
+ );
+ EventUtils.synthesizeKey("H", consts.historyOptions, newWin);
+ await focused;
+ ok(true, "Tab document browser got focus in new window");
+
+ // Test resetting keys after a new window is opened.
+ info("Resetting all keys");
+ CustomKeys.resetAll();
+
+ is(
+ CustomKeys.getDefaultKey("key_gotoHistory"),
+ null,
+ "key_gotoHistory is not customized"
+ );
+ info(`Pressing ${consts.historyDisplay} in new window`);
+ focused = BrowserTestUtils.waitForEvent(newWin, "SidebarFocused");
+ EventUtils.synthesizeKey("H", consts.historyOptions, newWin);
+ await focused;
+ ok(true, "Sidebar got focus in new window");
+ is(
+ newWin.SidebarController.currentID,
+ "viewHistorySidebar",
+ "History sidebar is open in new window"
+ );
+ info(`Pressing ${consts.historyDisplay} in new window`);
+ focused = BrowserTestUtils.waitForEvent(
+ newWin.gBrowser.selectedBrowser,
+ "focus"
+ );
+ EventUtils.synthesizeKey("H", consts.historyOptions, newWin);
+ await focused;
+ ok(true, "Tab document browser got focus in new window");
+
+ info("Closing new window");
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/browser/components/customkeys/tests/browser/head.js b/browser/components/customkeys/tests/browser/head.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const isMac = AppConstants.platform == "macosx";
+const isLinux = AppConstants.platform == "linux";
+
+const consts = {
+ // The following constants specify the default key combinations for various
+ // commands. They must be updated if these change in future.
+ // key_gotoHistory
+ historyDisplay: isMac ? "⇧⌘H" : "Ctrl+H",
+ historyModifiers: isMac ? "accel,shift" : "accel",
+ historyOptions: { accelKey: true, shiftKey: isMac },
+
+ // The following unused* constants specify a key combination which is unused by
+ // default. This will need to be updated if this key combination is assigned to
+ // something by default in future.
+ unusedModifiers: "accel,shift",
+ unusedOptions: { accelKey: true, shiftKey: true },
+ unusedKey: isLinux ? "Q" : "Y",
+ unusedModifiersDisplay: isMac ? "⇧⌘" : "Ctrl+Shift+",
+};
+consts.unusedDisplay = `${consts.unusedModifiersDisplay}${consts.unusedKey}`;
diff --git a/browser/components/moz.build b/browser/components/moz.build
@@ -36,6 +36,7 @@ DIRS += [
"contentanalysis",
"contextualidentity",
"customizableui",
+ "customkeys",
"downloads",
"enterprisepolicies",
"extensions",