Context menu keyboard event handling and general improvements

This commit is contained in:
James Lyne 2021-05-28 22:32:04 +01:00
parent 2e1426e945
commit dd1a1b3c97
2 changed files with 51 additions and 51 deletions

View File

@ -1,6 +1,5 @@
<template> <template>
<nav id="map-context-menu" v-show="menuVisible" ref="menuElement" :style="style"> <nav role="none" id="map-context-menu" ref="menuElement" :style="style" @keydown="handleKeydown">
<div tabindex="0" ref="focusMover" class="focus-mover" aria-label="Context menu"></div>
<ul class="menu" role="menu"> <ul class="menu" role="menu">
<li role="none"> <li role="none">
<!--suppress HtmlUnknownAttribute --> <!--suppress HtmlUnknownAttribute -->
@ -31,10 +30,11 @@ import {computed, defineComponent, onMounted, onUnmounted, watch} from "@vue/run
import {LeafletMouseEvent} from "leaflet"; import {LeafletMouseEvent} from "leaflet";
import {useStore} from "@/store"; import {useStore} from "@/store";
import WorldListItem from "@/components/sidebar/WorldListItem.vue"; import WorldListItem from "@/components/sidebar/WorldListItem.vue";
import {ref} from "vue"; import {CSSProperties, ref} from "vue";
import {getUrlForLocation} from "@/util"; import {getUrlForLocation} from "@/util";
import {notify} from "@kyvg/vue3-notification"; import {notify} from "@kyvg/vue3-notification";
import {nextTick} from 'vue'; import {nextTick} from 'vue';
import {handleKeyboardEvent} from "@/util/events";
export default defineComponent({ export default defineComponent({
name: "MapContextMenu", name: "MapContextMenu",
@ -55,7 +55,6 @@ export default defineComponent({
messageCenterHere = computed(() => store.state.messages.contextMenuCenterHere), messageCenterHere = computed(() => store.state.messages.contextMenuCenterHere),
menuElement = ref<HTMLInputElement | null>(null), menuElement = ref<HTMLInputElement | null>(null),
focusMover = ref<HTMLInputElement | null>(null),
menuVisible = computed(() => !!event.value), menuVisible = computed(() => !!event.value),
currentProjection = computed(() => store.state.currentProjection), currentProjection = computed(() => store.state.currentProjection),
@ -94,17 +93,20 @@ export default defineComponent({
}), }),
style = computed(() => { style = computed(() => {
if (!menuElement.value || !event.value) { if (!event.value) {
return {}; return {
'visibility': 'hidden',
'left': '-1000px',
} as CSSProperties;
} }
//Don't position offscreen //Don't position offscreen
const x = Math.min( const x = Math.min(
window.innerWidth - menuElement.value.offsetWidth - 10, window.innerWidth - menuElement.value!.offsetWidth - 10,
event.value.originalEvent.clientX event.value.originalEvent.clientX
), ),
y = Math.min( y = Math.min(
window.innerHeight - menuElement.value.offsetHeight - 10, window.innerHeight - menuElement.value!.offsetHeight - 10,
event.value.originalEvent.clientY event.value.originalEvent.clientY
); );
@ -114,30 +116,47 @@ export default defineComponent({
}); });
const handleEsc = (e: KeyboardEvent) => { const handleEsc = (e: KeyboardEvent) => {
if (e.key === "Escape" && menuVisible.value) { if (e.key === "Escape" && menuVisible.value) {
closeContextMenu(); closeContextMenu();
}
},
closeContextMenu = () => {
event.value = null;
},
pan = () => {
if (event.value) {
props.leaflet.panTo(event.value.latlng);
props.leaflet.getContainer().focus();
}
},
copySuccess = () => notify('Copied to clipboard'),
copyError = (e: Error) => {
notify({ type: 'error', text:'Unable to copy to clipboard'});
console.error('Error copying to clipboard', e);
};
watch(menuVisible, value => {
if(value) {
nextTick(() => focusMover.value && focusMover.value.focus());
} }
}) };
const handleKeydown = (e: KeyboardEvent) => {
handleKeyboardEvent(e, Array.from(menuElement.value!.querySelectorAll('button, input')));
}
const focusFirstItem = () => {
if(menuElement.value) {
const firstItem = menuElement.value.querySelector('button');
if(firstItem) {
firstItem.focus();
}
}
};
const closeContextMenu = () => event.value = null;
const pan = () => {
if (event.value) {
props.leaflet.panTo(event.value.latlng);
props.leaflet.getContainer().focus();
}
}
const copySuccess = () => notify('Copied to clipboard');
const copyError = (e: Error) => {
notify({ type: 'error', text:'Unable to copy to clipboard'});
console.error('Error copying to clipboard', e);
};
watch(event, value => {
if(value) {
props.leaflet.closePopup();
props.leaflet.closeTooltip();
nextTick(() => menuElement.value && focusFirstItem());
}
});
onMounted(() => { onMounted(() => {
window.addEventListener('click', closeContextMenu); window.addEventListener('click', closeContextMenu);
@ -180,13 +199,6 @@ export default defineComponent({
} }
}); });
watch(event, value => {
if(value) {
props.leaflet.closePopup();
props.leaflet.closeTooltip();
}
});
return { return {
messageCopyLink, messageCopyLink,
messageCenterHere, messageCenterHere,
@ -196,7 +208,6 @@ export default defineComponent({
menuVisible, menuVisible,
menuElement, menuElement,
focusMover,
url, url,
locationLabel, locationLabel,
@ -205,6 +216,7 @@ export default defineComponent({
style, style,
pan, pan,
handleKeydown,
} }
}, },
}) })

View File

@ -89,18 +89,6 @@ input {
outline: none; outline: none;
} }
.focus-mover {
clip: rect(1px, 1px, 1px, 1px);
position: absolute;
height: 1px;
width: 1px;
@include focus {
outline: none;
border: none;
}
}
.checkbox { .checkbox {
display: flex; display: flex;
position: relative; position: relative;