Make context menus accessible with keyboard
This commit is contained in:
parent
a81cef397c
commit
d178ac9749
@ -1981,12 +1981,15 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
|||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-menu-item:focus,
|
||||||
|
.textcomplete-item:focus,
|
||||||
.context-menu-item:hover,
|
.context-menu-item:hover,
|
||||||
.textcomplete-item:hover,
|
.textcomplete-item:hover,
|
||||||
.textcomplete-menu .active,
|
.textcomplete-menu .active,
|
||||||
#chat .userlist .user.active {
|
#chat .userlist .user.active {
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
transition: none;
|
transition: none;
|
||||||
|
outline: 0; /* TODO: Handle focus outlines in PR #1873 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-item::before,
|
.context-menu-item::before,
|
||||||
|
@ -96,10 +96,7 @@
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="context-menu-container">
|
<div id="context-menu-container"></div>
|
||||||
<ul id="context-menu"></ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="image-viewer"></div>
|
<div id="image-viewer"></div>
|
||||||
|
|
||||||
<script src="js/bundle.vendor.js"></script>
|
<script src="js/bundle.vendor.js"></script>
|
||||||
|
@ -1,65 +1,134 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const $ = require("jquery");
|
const $ = require("jquery");
|
||||||
|
const Mousetrap = require("mousetrap");
|
||||||
const templates = require("../views");
|
const templates = require("../views");
|
||||||
let contextMenu, contextMenuContainer;
|
|
||||||
|
const contextMenuContainer = $("#context-menu-container");
|
||||||
|
|
||||||
module.exports = class ContextMenu {
|
module.exports = class ContextMenu {
|
||||||
constructor(contextMenuItems, contextMenuActions, selectedElement, event) {
|
constructor(contextMenuItems, contextMenuActions, selectedElement, event) {
|
||||||
|
this.previousActiveElement = document.activeElement;
|
||||||
this.contextMenuItems = contextMenuItems;
|
this.contextMenuItems = contextMenuItems;
|
||||||
this.contextMenuActions = contextMenuActions;
|
this.contextMenuActions = contextMenuActions;
|
||||||
this.selectedElement = selectedElement;
|
this.selectedElement = selectedElement;
|
||||||
this.event = event;
|
this.event = event;
|
||||||
|
|
||||||
contextMenuContainer = $("#context-menu-container");
|
|
||||||
contextMenu = $("#context-menu");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
showContextMenu(this.contextMenuItems, this.selectedElement, this.event);
|
const contextMenu = showContextMenu(this.contextMenuItems, this.selectedElement, this.event);
|
||||||
this.bindEvents();
|
this.bindEvents(contextMenu);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bindEvents() {
|
hide() {
|
||||||
|
contextMenuContainer
|
||||||
|
.hide()
|
||||||
|
.empty()
|
||||||
|
.off(".contextMenu");
|
||||||
|
|
||||||
|
Mousetrap.unbind("escape");
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents(contextMenu) {
|
||||||
const contextMenuActions = this.contextMenuActions;
|
const contextMenuActions = this.contextMenuActions;
|
||||||
|
|
||||||
contextMenuActions.execute = (id, ...args) => contextMenuActions[id] && contextMenuActions[id](...args);
|
contextMenuActions.execute = (id, ...args) => contextMenuActions[id] && contextMenuActions[id](...args);
|
||||||
|
|
||||||
contextMenu.find(".context-menu-item").on("click", function() {
|
const clickItem = (item) => {
|
||||||
const $this = $(this);
|
const itemData = item.attr("data-data");
|
||||||
const itemData = $this.attr("data-data");
|
const contextAction = item.attr("data-action");
|
||||||
const contextAction = $this.attr("data-action");
|
|
||||||
|
this.hide();
|
||||||
|
|
||||||
contextMenuActions.execute(contextAction, itemData);
|
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) {
|
function showContextMenu(contextMenuItems, selectedElement, event) {
|
||||||
const target = $(event.currentTarget);
|
const target = $(event.currentTarget);
|
||||||
let output = "";
|
const contextMenu = $("<ul>", {id: "context-menu"});
|
||||||
|
|
||||||
for (const item of contextMenuItems) {
|
for (const item of contextMenuItems) {
|
||||||
if (item.check(target)) {
|
if (item.check(target)) {
|
||||||
if (item.divider) {
|
if (item.divider) {
|
||||||
output += templates.contextmenu_divider();
|
contextMenu.append(templates.contextmenu_divider());
|
||||||
} else {
|
} else {
|
||||||
output += templates.contextmenu_item({
|
contextMenu.append(templates.contextmenu_item({
|
||||||
class: typeof item.className === "function" ? item.className(target) : item.className,
|
class: typeof item.className === "function" ? item.className(target) : item.className,
|
||||||
action: item.actionId,
|
action: item.actionId,
|
||||||
text: typeof item.displayName === "function" ? item.displayName(target) : item.displayName,
|
text: typeof item.displayName === "function" ? item.displayName(target) : item.displayName,
|
||||||
data: typeof item.data === "function" ? item.data(target) : item.data,
|
data: typeof item.data === "function" ? item.data(target) : item.data,
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contextMenuContainer.show();
|
contextMenuContainer
|
||||||
|
.html(contextMenu)
|
||||||
|
.show();
|
||||||
|
|
||||||
contextMenu
|
contextMenu
|
||||||
.html(output)
|
.css(positionContextMenu(contextMenu, selectedElement, event))
|
||||||
.css(positionContextMenu(selectedElement, event));
|
.find(".context-menu-item:first-child")
|
||||||
|
.trigger("focus");
|
||||||
|
|
||||||
|
return contextMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
function positionContextMenu(selectedElement, e) {
|
function positionContextMenu(contextMenu, selectedElement, e) {
|
||||||
let offset;
|
let offset;
|
||||||
const menuWidth = contextMenu.outerWidth();
|
const menuWidth = contextMenu.outerWidth();
|
||||||
const menuHeight = contextMenu.outerHeight();
|
const menuHeight = contextMenu.outerHeight();
|
||||||
|
@ -8,7 +8,6 @@ const form = $("#form");
|
|||||||
const input = $("#input");
|
const input = $("#input");
|
||||||
const sidebar = $("#sidebar");
|
const sidebar = $("#sidebar");
|
||||||
const windows = $("#windows");
|
const windows = $("#windows");
|
||||||
const contextMenuContainer = $("#context-menu-container");
|
|
||||||
|
|
||||||
Mousetrap.bind([
|
Mousetrap.bind([
|
||||||
"pageup",
|
"pageup",
|
||||||
@ -97,12 +96,6 @@ Mousetrap.bind([
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
Mousetrap.bind([
|
|
||||||
"escape",
|
|
||||||
], function() {
|
|
||||||
contextMenuContainer.hide();
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputTrap = Mousetrap(input.get(0));
|
const inputTrap = Mousetrap(input.get(0));
|
||||||
|
|
||||||
function enableHistory() {
|
function enableHistory() {
|
||||||
|
@ -18,7 +18,6 @@ require("./webpush");
|
|||||||
require("./keybinds");
|
require("./keybinds");
|
||||||
require("./clipboard");
|
require("./clipboard");
|
||||||
const contextMenuFactory = require("./contextMenuFactory");
|
const contextMenuFactory = require("./contextMenuFactory");
|
||||||
const contextMenuContainer = $("#context-menu-container");
|
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
const sidebar = $("#sidebar, #footer");
|
const sidebar = $("#sidebar, #footer");
|
||||||
@ -80,11 +79,6 @@ $(function() {
|
|||||||
return contextMenuFactory.createContextMenu($(this), e).show();
|
return contextMenuFactory.createContextMenu($(this), e).show();
|
||||||
});
|
});
|
||||||
|
|
||||||
contextMenuContainer.on("click contextmenu", function() {
|
|
||||||
contextMenuContainer.hide();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
function resetInputHeight(input) {
|
function resetInputHeight(input) {
|
||||||
input.style.height = input.style.minHeight;
|
input.style.height = input.style.minHeight;
|
||||||
}
|
}
|
||||||
|
@ -57,9 +57,9 @@ chat.on("mouseleave", ".userlist .user", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
exports.handleKeybinds = function(input) {
|
exports.handleKeybinds = function(input) {
|
||||||
Mousetrap(input.get(0)).bind(["up", "down"], (e, key) => {
|
const trap = Mousetrap(input.get(0));
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
|
trap.bind(["up", "down"], (e, key) => {
|
||||||
const userlists = input.closest(".userlist");
|
const userlists = input.closest(".userlist");
|
||||||
let userlist;
|
let userlist;
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ exports.handleKeybinds = function(input) {
|
|||||||
const users = userlist.find(".user");
|
const users = userlist.find(".user");
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find which item in the array of users is currently selected, if any.
|
// 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
|
// Adjust scroll when active item is outside of the visible area
|
||||||
utils.scrollIntoViewNicely(userlist.find(".user.active")[0]);
|
utils.scrollIntoViewNicely(userlist.find(".user.active")[0]);
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// When pressing Enter, open the context menu (emit a click) on the active
|
// When pressing Enter, open the context menu (emit a click) on the active
|
||||||
// user
|
// user
|
||||||
Mousetrap(input.get(0)).bind("enter", () => {
|
trap.bind("enter", () => {
|
||||||
const user = input.closest(".userlist").find(".user.active");
|
const user = input.closest(".userlist").find(".user.active");
|
||||||
|
|
||||||
if (user.length) {
|
if (user.length) {
|
||||||
@ -109,5 +111,7 @@ exports.handleKeybinds = function(input) {
|
|||||||
clickEvent.pageY = userOffset.top + user.height();
|
clickEvent.pageY = userOffset.top + user.height();
|
||||||
user.trigger(clickEvent);
|
user.trigger(clickEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
<li class="context-menu-item context-menu-{{class}}" data-action="{{action}}"{{#if data}} data-data="{{data}}"{{/if}}>
|
<li class="context-menu-item context-menu-{{class}}" data-action="{{action}}"{{#if data}} data-data="{{data}}"{{/if}} tabindex="0">
|
||||||
{{text}}
|
{{text}}
|
||||||
</li>
|
</li>
|
||||||
|
Loading…
Reference in New Issue
Block a user