Files
sousa-gecko/browser/components/customkeys/CustomKeysParent.sys.mjs
T
Anna Yeddi ayeddi@mozilla.com 7e22a78276 Bug 2041506: Pt.2 - Replacing table structure with moz-card accordion components in about:keyboard. r=Jamie,fluent-reviewers,flod
Replacing the `<table>` structure in `about:keyboard` with `moz-card` accordion and other reusable components to align with a new design.

This patch:
1. Changes each shortcut category to become a `moz-card` accordion and each shortcut row uses `moz-box-item`
1. Ensures manually added categories, shortcuts, and their inputs (that are not available in menus) are rendered, localized, and labeled as expected
1. Provides responsive layout support for `moz-box-item` elements allowing the rows to reflow to two lines on smaller screens
1. Combines the plain `<input>` with `.new` and `.newLabel` classes into a single `moz-input-text.newKey` component so the unified editing input is consistently referenced by one selector across JS and CSS
1. Updates the conflict confirmation dialog to use `nsIPrompt.MODAL_TYPE_CONTENT` for consistency with the Reset All dialog and to align with the updated design language
1. Adds a placeholder to shortcut inputs to communicate the unassigned/cleared states to align with updated design and ensure an on-screen functionality of text inputs is indicated
1. Updates search the search filtering logic to work with the redesigned `moz-card` accordion structure and adding visual text highlighting for matched search terms. The search is now operates on the new `moz-card.category` and `.key` selectors rather than the previously used `tbody` and `tr` selectors. With the new DOM structure relying on reusable components that include more shadow DOM communication, the filtering and highlighting logic was updated to work across shadow DOM boundaries:
  1. Updates design so categories with at least one matching shortcut are shown and their accordion is expanded and categories without any match are hidden
  1. Adds `<mark>` element highlighting to the matched portion of shortcut names (while custom highlight API is not yet penetrating shadow DOM)
  1. Uses the Settings UI search highlight color tokens for visual consistency, with an addition of `forced-colors` media query variants
1. Cleans up table-related code from `customkeys.js` and `customkeys.css`.
1. Creates a Fluent migration recipe for all the renamed strings in `customkeys.ftl`

Notes:
Tests are added in the part 3 of this stack to cover all parts of the redesigned changes.

Differential Revision: https://phabricator.services.mozilla.com/D302641
2026-06-29 17:12:29 +00:00

298 lines
11 KiB
JavaScript

/* 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { CustomKeys } from "moz-src:///browser/components/customkeys/CustomKeys.sys.mjs";
import { ShortcutUtils } from "resource://gre/modules/ShortcutUtils.sys.mjs";
const KEY_NAMES_TO_CODES = {
ArrowDown: "VK_DOWN",
ArrowLeft: "VK_LEFT",
ArrowRight: "VK_RIGHT",
ArrowUp: "VK_UP",
};
/**
* Actor implementation for about:keyboard.
*/
export class CustomKeysParent extends JSWindowActorParent {
getKeyData(id) {
const keyEl =
this.browsingContext.topChromeWindow.document.getElementById(id);
if (!keyEl) {
return null;
}
return {
// Do not prettify a shortcut that is cleared
shortcut:
keyEl.hasAttribute("key") || keyEl.hasAttribute("keycode")
? ShortcutUtils.prettifyShortcut(keyEl)
: "",
isCustomized: !!CustomKeys.getDefaultKey(id),
};
}
// Get keyboard shortcuts in the form:
// { <categoryTitle>: { <keyId>: { title: <title>, shortcut: <String>, isCustomized: <Boolean> } }
// If categoryTitle or keyTitle begins with "customkeys-", it is a Fluent id.
getKeys() {
// Make Dev Tools populate the Browser Tools menu so we can gather those
// shortcuts here.
const topWin = this.browsingContext.topChromeWindow;
Services.obs.notifyObservers(topWin, "customkeys-ui-showing");
const add = (category, id, title) => {
const data = this.getKeyData(id);
if (data) {
data.title = title;
category[id] = data;
}
};
const keys = {};
// Gather as many keys as we can from the menu bar menus.
const mainMenuBar = topWin.document.getElementById("main-menubar");
for (const item of mainMenuBar.querySelectorAll("menuitem[key]")) {
const menu = item.closest("menu");
if (menu.id == "historyUndoMenu" || menu.id == "historyUndoWindowMenu") {
// The reopen last tab/window commands have the label of the actual
// tab/window they will reopen. We handle those specially later.
continue;
}
if (!keys[menu.label]) {
keys[menu.label] = {};
}
add(keys[menu.label], item.getAttribute("key"), item.label);
}
// Add some shortcuts that aren't available in menus.
const fileCat = topWin.document.getElementById("file-menu").label;
let cat = keys[fileCat];
add(cat, "key_duplicateTab", "customkeys-file-duplicate-tab");
const isMac = AppConstants.platform === "macosx";
if (!isMac) {
add(
cat,
"focusURLBar2",
topWin.document.getElementById("menu_openLocation").label
);
}
add(cat, "key_search", "customkeys-file-focus-search");
add(cat, "key_search2", "customkeys-file-focus-search");
const historyCat = topWin.document.getElementById("history-menu").label;
cat = keys[historyCat];
add(
cat,
"key_restoreLastClosedTabOrWindowOrSession",
"customkeys-history-reopen-tab"
);
add(cat, "key_undoCloseWindow", "customkeys-history-reopen-window");
const sidebarCat = topWin.document.getElementById(
"viewSidebarMenuMenu"
).label;
cat = keys[sidebarCat];
add(cat, "toggleSidebarKb", "customkeys-sidebar-toggle");
const viewCat = topWin.document.getElementById("view-menu").label;
cat = keys[viewCat];
add(cat, "viewBookmarksToolbarKb", "customkeys-view-bookmarks-toolbar");
add(
cat,
"key_togglePictureInPicture",
"customkeys-view-picture-in-picture"
);
add(cat, "key_addTabSplitView", "customkeys-view-add-split-view");
add(cat, "key_separateTabSplitView", "customkeys-view-separate-split-view");
const editCat = topWin.document.getElementById("edit-menu").label;
cat = keys[editCat];
add(
cat,
"key_findAgain2",
topWin.document.getElementById("menu_findAgain").label
);
add(cat, "key_findPrevious", "customkeys-edit-find-previous");
add(cat, "key_findPrevious2", "customkeys-edit-find-previous");
const toolsCat = topWin.document.getElementById("tools-menu").label;
cat = keys[toolsCat];
add(cat, "key_screenshot", "customkeys-tools-screenshot");
const browserToolsCat =
topWin.document.getElementById("browserToolsMenu").label;
cat = keys[browserToolsCat];
add(cat, "key_toggleToolboxF12", "customkeys-dev-tools");
add(cat, "key_inspector", "customkeys-dev-inspector");
add(cat, "key_webconsole", "customkeys-dev-webconsole");
add(cat, "key_jsdebugger", "customkeys-dev-debugger");
add(cat, "key_netmonitor", "customkeys-dev-network");
add(cat, "key_styleeditor", "customkeys-dev-style");
add(cat, "key_performance", "customkeys-dev-performance");
add(cat, "key_storage", "customkeys-dev-storage");
add(cat, "key_dom", "customkeys-dev-dom");
add(cat, "key_accessibility", "customkeys-dev-accessibility");
add(cat, "key_profilerStartStop", "customkeys-dev-profiler-toggle");
add(
cat,
"key_profilerStartStopAlternate",
"customkeys-dev-profiler-toggle"
);
add(cat, "key_profilerCapture", "customkeys-dev-profiler-capture");
add(cat, "key_profilerCaptureAlternate", "customkeys-dev-profiler-capture");
cat = keys["customkeys-category-navigation-2"] = {};
add(cat, "goBackKb", "customkeys-nav-back");
add(cat, "goForwardKb", "customkeys-nav-forward");
add(cat, "goHome", "customkeys-nav-home");
add(cat, "key_reload", "customkeys-nav-reload");
add(cat, "key_reload2", "customkeys-nav-reload");
add(cat, "key_reload_skip_cache", "customkeys-nav-reload-skip-cache");
add(cat, "key_reload_skip_cache2", "customkeys-nav-reload-skip-cache");
add(cat, "key_stop", "customkeys-nav-stop");
if (AppConstants.platform !== "win") {
add(cat, "goBackKb2", "customkeys-nav-back");
add(cat, "goForwardKb2", "customkeys-nav-forward");
}
if (isMac) {
add(cat, "key_stop_mac", "customkeys-nav-stop");
}
for (let i = 1; i <= 8; i++) {
add(cat, `key_selectTab${i}`, `customkeys-nav-select-tab-${i}`);
}
add(cat, "key_selectLastTab", "customkeys-nav-select-last-tab");
add(cat, "key_toggleMute", "customkeys-nav-toggle-mute");
return keys;
}
prettifyShortcut({ modifiers, key, keycode }) {
// ShortcutUtils.prettifyShortcut needs a key element, but we don't have
// that here. It simply concatenates the prettified modifiers and key
// anyway.
const prettyMods = ShortcutUtils.getModifierString(modifiers);
const prettyKey = ShortcutUtils.getKeyString(keycode, key);
return prettyMods + prettyKey;
}
async receiveMessage(message) {
switch (message.name) {
case "CustomKeys:CaptureKey": {
if (message.data) {
this.browsingContext.embedderElement.addEventListener(
"keydown",
this
);
} else {
this.browsingContext.embedderElement.removeEventListener(
"keydown",
this
);
}
return null;
}
case "CustomKeys:ChangeKey": {
CustomKeys.changeKey(message.data.id, message.data);
return this.getKeyData(message.data.id);
}
case "CustomKeys:ClearKey": {
const id = message.data;
CustomKeys.clearKey(id);
return this.getKeyData(id);
}
case "CustomKeys:Confirm": {
const result = await Services.prompt.asyncConfirmEx(
this.browsingContext,
Ci.nsIPrompt.MODAL_TYPE_CONTENT,
message.data.title,
message.data.body,
Ci.nsIPromptService.BUTTON_POS_0_DEFAULT |
(Ci.nsIPromptService.BUTTON_TITLE_IS_STRING *
Ci.nsIPromptService.BUTTON_POS_0) |
(Ci.nsIPromptService.BUTTON_TITLE_IS_STRING *
Ci.nsIPromptService.BUTTON_POS_1),
message.data.buttonConfirm,
message.data.buttonCancel,
null,
null,
false,
{ useTitle: true }
);
// Return true for confirm, false for cancel.
return result.get("buttonNumClicked") == 0;
}
case "CustomKeys:GetDefaultKey": {
const data = { id: message.data };
Object.assign(data, CustomKeys.getDefaultKey(data.id));
data.shortcut = this.prettifyShortcut(data);
return data;
}
case "CustomKeys:GetKeys": {
return this.getKeys();
}
case "CustomKeys:ResetAll": {
return CustomKeys.resetAll();
}
case "CustomKeys:ResetKey": {
CustomKeys.resetKey(message.data);
return this.getKeyData(message.data);
}
}
return null;
}
handleEvent(event) {
if (event.type == "keydown") {
event.preventDefault();
event.stopPropagation();
const data = {};
let modifiers = [];
const isMac = AppConstants.platform === "macosx";
if (event.altKey) {
modifiers.push("alt");
}
if (event.ctrlKey) {
modifiers.push(isMac ? "control" : "accel");
}
// We can't support the Windows key (metaKey) on Windows because
// GlobalKeyListener ignores the Windows key state if no shortcut keys
// match the event. See bug 1100862. Unfortunately, there are multiple
// keysets and the absence of a match in one keyset doesn't mean there
// won't be a match in another.
if (event.metaKey && AppConstants.platform !== "win") {
modifiers.push(isMac ? "accel" : "meta");
}
if (event.shiftKey) {
modifiers.push("shift");
}
data.modifiers = modifiers.sort().join(",");
if (
event.key == "Alt" ||
event.key == "Control" ||
event.key == "Meta" ||
event.key == "Shift"
) {
data.isModifier = true;
data.modifierString = ShortcutUtils.getModifierString(data.modifiers);
this.sendAsyncMessage("CustomKeys:CapturedKey", data);
return;
}
data.isValid = true;
if (event.key.length == 1) {
data.key = event.key.toUpperCase();
if (!modifiers.length || (event.shiftKey && modifiers.length == 1)) {
// This is a printable character; e.g. a letter, number or punctuation
// mark. That's not a valid shortcut key.
data.isValid = false;
}
} else {
data.keycode =
KEY_NAMES_TO_CODES[event.key] ??
ShortcutUtils.getKeycodeAttribute(event.key);
if (event.key == "Backspace") {
data.isValid = false;
}
}
if (data.isValid) {
data.shortcut = this.prettifyShortcut(data);
}
this.sendAsyncMessage("CustomKeys:CapturedKey", data);
}
}
}