diff --git a/client/css/style.css b/client/css/style.css
index 72961c8c..7feb14a0 100644
--- a/client/css/style.css
+++ b/client/css/style.css
@@ -1981,12 +1981,15 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
transition: background-color 0.2s;
}
+.context-menu-item:focus,
+.textcomplete-item:focus,
.context-menu-item:hover,
.textcomplete-item:hover,
.textcomplete-menu .active,
#chat .userlist .user.active {
background-color: #f6f6f6;
transition: none;
+ outline: 0; /* TODO: Handle focus outlines in PR #1873 */
}
.context-menu-item::before,
diff --git a/client/index.html.tpl b/client/index.html.tpl
index 7ecf4569..9035dc87 100644
--- a/client/index.html.tpl
+++ b/client/index.html.tpl
@@ -96,10 +96,7 @@
-
-
+
diff --git a/client/js/contextMenu.js b/client/js/contextMenu.js
index b5008f75..c620eac0 100644
--- a/client/js/contextMenu.js
+++ b/client/js/contextMenu.js
@@ -1,65 +1,137 @@
"use strict";
+
const $ = require("jquery");
+const Mousetrap = require("mousetrap");
const templates = require("../views");
-let contextMenu, contextMenuContainer;
+
+const contextMenuContainer = $("#context-menu-container");
module.exports = class ContextMenu {
constructor(contextMenuItems, contextMenuActions, selectedElement, event) {
+ this.previousActiveElement = document.activeElement;
this.contextMenuItems = contextMenuItems;
this.contextMenuActions = contextMenuActions;
this.selectedElement = selectedElement;
this.event = event;
-
- contextMenuContainer = $("#context-menu-container");
- contextMenu = $("#context-menu");
}
show() {
- showContextMenu(this.contextMenuItems, this.selectedElement, this.event);
- this.bindEvents();
+ const contextMenu = showContextMenu(this.contextMenuItems, this.selectedElement, this.event);
+ this.bindEvents(contextMenu);
return false;
}
- bindEvents() {
+ hide() {
+ contextMenuContainer
+ .hide()
+ .empty()
+ .off(".contextMenu");
+
+ Mousetrap.unbind("escape");
+ }
+
+ bindEvents(contextMenu) {
const contextMenuActions = this.contextMenuActions;
contextMenuActions.execute = (id, ...args) => contextMenuActions[id] && contextMenuActions[id](...args);
- contextMenu.find(".context-menu-item").on("click", function() {
- const $this = $(this);
- const itemData = $this.attr("data-data");
- const contextAction = $this.attr("data-action");
+ const clickItem = (item) => {
+ const itemData = item.attr("data-data");
+ const contextAction = item.attr("data-action");
+
+ this.hide();
+
contextMenuActions.execute(contextAction, itemData);
+ };
+
+ contextMenu.on("click", ".context-menu-item", function() {
+ clickItem($(this));
+ });
+
+ const trap = Mousetrap(contextMenu.get(0));
+
+ trap.bind(["up", "down"], (e, key) => {
+ const items = contextMenu.find(".context-menu-item");
+
+ let index = items.toArray().findIndex((item) => $(item).is(":focus"));
+
+ if (key === "down") {
+ index = (index + 1) % items.length;
+ } else {
+ index = Math.max(index, 0) - 1;
+ }
+
+ items.eq(index).trigger("focus");
+ });
+
+ trap.bind("enter", () => {
+ const item = contextMenu.find(".context-menu-item:focus");
+
+ if (item.length) {
+ clickItem(item);
+ }
+
+ return false;
+ });
+
+ // Hide context menu when clicking or right clicking outside of it
+ contextMenuContainer.on("click.contextMenu contextmenu.contextMenu", (e) => {
+ // Do not close the menu when clicking inside of the context menu (e.g. on a divider)
+ if ($(e.target).prop("id") === "context-menu") {
+ return;
+ }
+
+ this.hide();
+ return false;
+ });
+
+ // Hide the context menu when pressing escape within the context menu container
+ Mousetrap.bind("escape", () => {
+ this.hide();
+
+ // Return focus to the previously focused element
+ $(this.previousActiveElement).trigger("focus");
+
+ return false;
});
}
};
function showContextMenu(contextMenuItems, selectedElement, event) {
const target = $(event.currentTarget);
- let output = "";
+ const contextMenu = $("", {
+ id: "context-menu",
+ role: "menu",
+ });
for (const item of contextMenuItems) {
if (item.check(target)) {
if (item.divider) {
- output += templates.contextmenu_divider();
+ contextMenu.append(templates.contextmenu_divider());
} else {
- output += templates.contextmenu_item({
+ contextMenu.append(templates.contextmenu_item({
class: typeof item.className === "function" ? item.className(target) : item.className,
action: item.actionId,
text: typeof item.displayName === "function" ? item.displayName(target) : item.displayName,
data: typeof item.data === "function" ? item.data(target) : item.data,
- });
+ }));
}
}
}
- contextMenuContainer.show();
+ contextMenuContainer
+ .html(contextMenu)
+ .show();
+
contextMenu
- .html(output)
- .css(positionContextMenu(selectedElement, event));
+ .css(positionContextMenu(contextMenu, selectedElement, event))
+ .find(".context-menu-item:first-child")
+ .trigger("focus");
+
+ return contextMenu;
}
-function positionContextMenu(selectedElement, e) {
+function positionContextMenu(contextMenu, selectedElement, e) {
let offset;
const menuWidth = contextMenu.outerWidth();
const menuHeight = contextMenu.outerHeight();
diff --git a/client/js/contextMenuFactory.js b/client/js/contextMenuFactory.js
index 8d793604..951fd028 100644
--- a/client/js/contextMenuFactory.js
+++ b/client/js/contextMenuFactory.js
@@ -60,8 +60,6 @@ function addWhoisItem() {
target: $("#chat").data("id"),
text: "/whois " + itemData,
});
-
- $(`.channel.active .userlist .user[data-name="${itemData}"]`).trigger("click");
}
addContextMenuItem({
diff --git a/client/js/keybinds.js b/client/js/keybinds.js
index bb27af31..c5196f18 100644
--- a/client/js/keybinds.js
+++ b/client/js/keybinds.js
@@ -8,7 +8,6 @@ const form = $("#form");
const input = $("#input");
const sidebar = $("#sidebar");
const windows = $("#windows");
-const contextMenuContainer = $("#context-menu-container");
Mousetrap.bind([
"pageup",
@@ -97,12 +96,6 @@ Mousetrap.bind([
return false;
});
-Mousetrap.bind([
- "escape",
-], function() {
- contextMenuContainer.hide();
-});
-
const inputTrap = Mousetrap(input.get(0));
function enableHistory() {
diff --git a/client/js/lounge.js b/client/js/lounge.js
index 24bb09da..03328b14 100644
--- a/client/js/lounge.js
+++ b/client/js/lounge.js
@@ -18,7 +18,6 @@ require("./webpush");
require("./keybinds");
require("./clipboard");
const contextMenuFactory = require("./contextMenuFactory");
-const contextMenuContainer = $("#context-menu-container");
$(function() {
const sidebar = $("#sidebar, #footer");
@@ -80,11 +79,6 @@ $(function() {
return contextMenuFactory.createContextMenu($(this), e).show();
});
- contextMenuContainer.on("click contextmenu", function() {
- contextMenuContainer.hide();
- return false;
- });
-
function resetInputHeight(input) {
input.style.height = input.style.minHeight;
}
diff --git a/client/js/userlist.js b/client/js/userlist.js
index 4c6ff81f..47f4b47b 100644
--- a/client/js/userlist.js
+++ b/client/js/userlist.js
@@ -57,9 +57,9 @@ chat.on("mouseleave", ".userlist .user", function() {
});
exports.handleKeybinds = function(input) {
- Mousetrap(input.get(0)).bind(["up", "down"], (e, key) => {
- e.preventDefault();
+ const trap = Mousetrap(input.get(0));
+ trap.bind(["up", "down"], (e, key) => {
const userlists = input.closest(".userlist");
let userlist;
@@ -73,7 +73,7 @@ exports.handleKeybinds = function(input) {
const users = userlist.find(".user");
if (users.length === 0) {
- return;
+ return false;
}
// Find which item in the array of users is currently selected, if any.
@@ -95,11 +95,13 @@ exports.handleKeybinds = function(input) {
// Adjust scroll when active item is outside of the visible area
utils.scrollIntoViewNicely(userlist.find(".user.active")[0]);
+
+ return false;
});
// When pressing Enter, open the context menu (emit a click) on the active
// user
- Mousetrap(input.get(0)).bind("enter", () => {
+ trap.bind("enter", () => {
const user = input.closest(".userlist").find(".user.active");
if (user.length) {
@@ -109,5 +111,7 @@ exports.handleKeybinds = function(input) {
clickEvent.pageY = userOffset.top + user.height();
user.trigger(clickEvent);
}
+
+ return false;
});
};
diff --git a/client/views/contextmenu_divider.tpl b/client/views/contextmenu_divider.tpl
index 19316d55..c341b5c6 100644
--- a/client/views/contextmenu_divider.tpl
+++ b/client/views/contextmenu_divider.tpl
@@ -1 +1 @@
-
+
diff --git a/client/views/contextmenu_item.tpl b/client/views/contextmenu_item.tpl
index fe281d49..fbbb3f2b 100644
--- a/client/views/contextmenu_item.tpl
+++ b/client/views/contextmenu_item.tpl
@@ -1,3 +1,3 @@
-