Skip to content

Commit

Permalink
Make context menus accessible with keyboard
Browse files Browse the repository at this point in the history
  • Loading branch information
xPaw committed Apr 22, 2018
1 parent 8e2cce5 commit 9fe0d24
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 42 deletions.
2 changes: 2 additions & 0 deletions client/css/style.css
Expand Up @@ -1916,6 +1916,8 @@ 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,
Expand Down
5 changes: 1 addition & 4 deletions client/index.html.tpl
Expand Up @@ -78,10 +78,7 @@
</article>
</div>

<div id="context-menu-container">
<ul id="context-menu"></ul>
</div>

<div id="context-menu-container"></div>
<div id="image-viewer"></div>

<script src="js/bundle.vendor.js"></script>
Expand Down
106 changes: 87 additions & 19 deletions client/js/contextMenu.js
@@ -1,65 +1,133 @@
"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.data("data");
const contextAction = $this.data("action");
const clickItem = (item) => {
const itemData = item.data("data");
const contextAction = item.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 = $("<ul>", {id: "context-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();
Expand Down
9 changes: 1 addition & 8 deletions client/js/keybinds.js
Expand Up @@ -8,7 +8,6 @@ const form = $("#form");
const input = $("#input");
const sidebar = $("#sidebar");
const windows = $("#windows");
const contextMenuContainer = $("#context-menu-container");

Mousetrap.bind([
"pageup",
Expand Down Expand Up @@ -97,12 +96,6 @@ Mousetrap.bind([
return false;
});

Mousetrap.bind([
"escape",
], function() {
contextMenuContainer.hide();
});

const inputTrap = Mousetrap(input.get(0));

function enableHistory() {
Expand Down Expand Up @@ -248,7 +241,7 @@ const ignoredKeys = {
};

if (!("ontouchstart" in window || navigator.maxTouchPoints > 0)) {
$(document.body).on("keydown", (e) => {
$(document).on("keydown", (e) => {
// Ignore if target isn't body (e.g. focused into input)
// Ignore any key that uses alt modifier
// Ignore keys defined above
Expand Down
6 changes: 0 additions & 6 deletions client/js/lounge.js
Expand Up @@ -19,7 +19,6 @@ require("./webpush");
require("./keybinds");
require("./clipboard");
const contextMenuFactory = require("./contextMenuFactory");
const contextMenuContainer = $("#context-menu-container");

$(function() {
const sidebar = $("#sidebar, #footer");
Expand Down Expand Up @@ -81,11 +80,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;
}
Expand Down
12 changes: 8 additions & 4 deletions client/js/userlist.js
Expand Up @@ -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;

Expand All @@ -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.
Expand All @@ -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) {
Expand All @@ -109,5 +111,7 @@ exports.handleKeybinds = function(input) {
clickEvent.pageY = userOffset.top + user.height();
user.trigger(clickEvent);
}

return false;
});
};
2 changes: 1 addition & 1 deletion client/views/contextmenu_item.tpl
@@ -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}}
</li>

0 comments on commit 9fe0d24

Please sign in to comment.