Improve context menus

This commit is contained in:
Pavel Djundik 2019-11-18 22:05:47 +02:00
parent 9147772cb2
commit 85907f54ba
4 changed files with 85 additions and 56 deletions

View File

@ -1,19 +1,34 @@
<template> <template>
<div v-if="isOpen" id="context-menu-container" @click="close" @contextmenu.prevent="close"> <div
<ul id="context-menu" ref="contextMenu" role="menu" :style="style"> v-if="isOpen"
<li id="context-menu-container"
v-for="item of items" @click="containerClick"
:key="item.name" @contextmenu.prevent="containerClick"
:class="[ >
'context-menu-' + item.type, <ul
item.class ? 'context-menu-' + item.class : null, id="context-menu"
]" ref="contextMenu"
tabindex="0" role="menu"
role="menuitem" :style="style"
@click="clickItem(item)" tabindex="-1"
> @mouseleave="activeItem = -1"
{{ item.label }} @keydown.enter.prevent="clickActiveItem"
</li> >
<template v-for="(item, id) of items">
<li
:key="item.name"
:class="[
'context-menu-' + item.type,
item.class ? 'context-menu-' + item.class : null,
{active: id === activeItem},
]"
role="menuitem"
@mouseover="hoverItem(id)"
@click="clickItem(item)"
>
{{ item.label }}
</li>
</template>
</ul> </ul>
</div> </div>
</template> </template>
@ -31,6 +46,7 @@ export default {
isOpen: false, isOpen: false,
previousActiveElement: null, previousActiveElement: null,
items: [], items: [],
activeItem: -1,
style: { style: {
left: 0, left: 0,
top: 0, top: 0,
@ -39,57 +55,27 @@ export default {
}, },
mounted() { mounted() {
Mousetrap.bind("esc", this.close); Mousetrap.bind("esc", this.close);
Mousetrap.bind(["up", "down", "tab", "shift+tab"], this.navigateMenu);
const trap = Mousetrap(this.$refs.contextMenu);
trap.bind(["up", "down"], (e, key) => {
if (!this.isOpen) {
return;
}
const items = this.$refs.contextMenu.querySelectorAll(".context-menu-item");
let index = Array.from(items).findIndex((item) => item === document.activeElement);
if (key === "down") {
index = (index + 1) % items.length;
} else {
index = Math.max(index, 0) - 1;
if (index < 0) {
index = items.length + index;
}
}
items[index].focus();
});
trap.bind("enter", () => {
if (!this.isOpen) {
return;
}
const item = this.$refs.contextMenu.querySelector(":focus");
item.click();
return false;
});
}, },
destroyed() { destroyed() {
Mousetrap.unbind("esc", this.close); Mousetrap.unbind("esc", this.close);
Mousetrap.unbind(["up", "down", "tab", "shift+tab"], this.navigateMenu);
}, },
methods: { methods: {
open(event, items) { open(event, items) {
this.items = items; event.preventDefault();
this.isOpen = true;
this.previousActiveElement = document.activeElement; this.previousActiveElement = document.activeElement;
this.items = items;
this.activeItem = 0;
this.isOpen = true;
// Position the menu and set the focus on the first item after it's size has updated // Position the menu and set the focus on the first item after it's size has updated
this.$nextTick(() => { this.$nextTick(() => {
const pos = this.positionContextMenu(event); const pos = this.positionContextMenu(event);
this.style.left = pos.left + "px"; this.style.left = pos.left + "px";
this.style.top = pos.top + "px"; this.style.top = pos.top + "px";
this.$refs.contextMenu.focus();
this.$refs.contextMenu.querySelector(".context-menu-item:first-child").focus();
}); });
}, },
close() { close() {
@ -98,17 +84,59 @@ export default {
} }
this.isOpen = false; this.isOpen = false;
this.items = [];
if (this.previousActiveElement) { if (this.previousActiveElement) {
this.previousActiveElement.focus(); this.previousActiveElement.focus();
this.previousActiveElement = null; this.previousActiveElement = null;
} }
}, },
hoverItem(id) {
this.activeItem = id;
},
clickItem(item) { clickItem(item) {
if (item.action) { if (item.action) {
item.action(); item.action();
this.close();
} else if (item.link) { } else if (item.link) {
this.$router.push(item.link); this.$router.push(item.link);
this.close();
}
},
clickActiveItem() {
if (this.items[this.activeItem]) {
this.clickItem(this.items[this.activeItem]);
}
},
navigateMenu(event, key) {
event.preventDefault();
const direction = key === "down" || key === "tab" ? 1 : -1;
let currentIndex = this.activeItem;
currentIndex += direction;
const nextItem = this.items[currentIndex];
// If the next item we would select is a divider, skip over it
if (nextItem && !nextItem.action && !nextItem.link) {
currentIndex += direction;
}
if (currentIndex < 0) {
currentIndex += this.items.length;
}
if (currentIndex > this.items.length - 1) {
currentIndex -= this.items.length;
}
this.activeItem = currentIndex;
},
containerClick(event) {
if (event.currentTarget === event.target) {
this.close();
} }
}, },
positionContextMenu(event) { positionContextMenu(event) {

View File

@ -4,6 +4,7 @@
:data-name="user.nick" :data-name="user.nick"
role="button" role="button"
v-on="onHover ? {mouseover: hover} : {}" v-on="onHover ? {mouseover: hover} : {}"
@click.prevent="rightClick($event)"
@contextmenu.prevent="rightClick($event)" @contextmenu.prevent="rightClick($event)"
>{{ user.mode }}{{ user.nick }}</span >{{ user.mode }}{{ user.nick }}</span
> >

View File

@ -4,6 +4,7 @@
:data-name="user.original.nick" :data-name="user.original.nick"
role="button" role="button"
@mouseover="hover" @mouseover="hover"
@click.prevent="rightClick($event)"
@contextmenu.prevent="rightClick($event)" @contextmenu.prevent="rightClick($event)"
v-html="user.original.mode + user.string" v-html="user.original.mode + user.string"
/> />

View File

@ -2088,6 +2088,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15);
border: 1px solid rgba(0, 0, 0, 0.15); border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 5px; border-radius: 5px;
outline: 0;
} }
.context-menu-divider { .context-menu-divider {
@ -2109,15 +2110,13 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
border-radius: 3px; border-radius: 3px;
} }
.context-menu-item:focus, .context-menu-item.active,
.textcomplete-item:focus, .textcomplete-item:focus,
.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: rgba(0, 0, 0, 0.1); background-color: rgba(0, 0, 0, 0.1);
transition: none; transition: none;
outline: 0;
} }
.context-menu-item::before, .context-menu-item::before,