diff --git a/client/components/ContextMenu.vue b/client/components/ContextMenu.vue index 89e434e6..ff2cd94c 100644 --- a/client/components/ContextMenu.vue +++ b/client/components/ContextMenu.vue @@ -1,19 +1,34 @@ @@ -31,6 +46,7 @@ export default { isOpen: false, previousActiveElement: null, items: [], + activeItem: -1, style: { left: 0, top: 0, @@ -39,57 +55,27 @@ export default { }, mounted() { Mousetrap.bind("esc", this.close); - - 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; - }); + Mousetrap.bind(["up", "down", "tab", "shift+tab"], this.navigateMenu); }, destroyed() { Mousetrap.unbind("esc", this.close); + Mousetrap.unbind(["up", "down", "tab", "shift+tab"], this.navigateMenu); }, methods: { open(event, items) { - this.items = items; - this.isOpen = true; + event.preventDefault(); + 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 this.$nextTick(() => { const pos = this.positionContextMenu(event); this.style.left = pos.left + "px"; this.style.top = pos.top + "px"; - - this.$refs.contextMenu.querySelector(".context-menu-item:first-child").focus(); + this.$refs.contextMenu.focus(); }); }, close() { @@ -98,17 +84,59 @@ export default { } this.isOpen = false; + this.items = []; if (this.previousActiveElement) { this.previousActiveElement.focus(); this.previousActiveElement = null; } }, + hoverItem(id) { + this.activeItem = id; + }, clickItem(item) { if (item.action) { item.action(); + this.close(); } else if (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) { diff --git a/client/components/Username.vue b/client/components/Username.vue index 20629fca..64444f21 100644 --- a/client/components/Username.vue +++ b/client/components/Username.vue @@ -4,6 +4,7 @@ :data-name="user.nick" role="button" v-on="onHover ? {mouseover: hover} : {}" + @click.prevent="rightClick($event)" @contextmenu.prevent="rightClick($event)" >{{ user.mode }}{{ user.nick }} diff --git a/client/components/UsernameFiltered.vue b/client/components/UsernameFiltered.vue index b1588831..62881a17 100644 --- a/client/components/UsernameFiltered.vue +++ b/client/components/UsernameFiltered.vue @@ -4,6 +4,7 @@ :data-name="user.original.nick" role="button" @mouseover="hover" + @click.prevent="rightClick($event)" @contextmenu.prevent="rightClick($event)" v-html="user.original.mode + user.string" /> diff --git a/client/css/style.css b/client/css/style.css index 2ec817c8..c7150a98 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -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); border: 1px solid rgba(0, 0, 0, 0.15); border-radius: 5px; + outline: 0; } .context-menu-divider { @@ -2109,15 +2110,13 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */ border-radius: 3px; } -.context-menu-item:focus, +.context-menu-item.active, .textcomplete-item:focus, -.context-menu-item:hover, .textcomplete-item:hover, .textcomplete-menu .active, #chat .userlist .user.active { background-color: rgba(0, 0, 0, 0.1); transition: none; - outline: 0; } .context-menu-item::before,