Reimplement keyboard shortcuts more generically

This commit is contained in:
James Lyne 2021-05-29 00:08:43 +01:00
parent 0ca26beb8e
commit 789505e2d9
3 changed files with 95 additions and 76 deletions

View File

@ -31,7 +31,7 @@ import {ActionTypes} from "@/store/action-types";
import {parseUrl} from '@/util'; import {parseUrl} from '@/util';
import {hideSplash, showSplash, showSplashError} from '@/util/splash'; import {hideSplash, showSplash, showSplashError} from '@/util/splash';
import {MutationTypes} from "@/store/mutation-types"; import {MutationTypes} from "@/store/mutation-types";
import {LiveAtlasServerDefinition} from "@/index"; import {LiveAtlasServerDefinition, LiveAtlasUIElement} from "@/index";
export default defineComponent({ export default defineComponent({
name: 'App', name: 'App',
@ -132,6 +132,34 @@ export default defineComponent({
onResize = () => { onResize = () => {
store.commit(MutationTypes.SET_SMALL_SCREEN, window.innerWidth < 480 || window.innerHeight < 500); store.commit(MutationTypes.SET_SMALL_SCREEN, window.innerWidth < 480 || window.innerHeight < 500);
},
onKeydown = (e: KeyboardEvent) => {
if(!e.ctrlKey || !e.shiftKey) {
return;
}
let element: LiveAtlasUIElement;
switch(e.key) {
case 'M':
element = 'maps';
break;
case 'C':
element = 'chat';
break;
case 'P':
element = 'players';
break;
case 'L':
element = 'layers';
break;
default:
return;
}
e.preventDefault();
store.commit(MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY, element);
}; };
watch(title, (title) => document.title = title); watch(title, (title) => document.title = title);
@ -169,8 +197,14 @@ export default defineComponent({
handleUrl(); handleUrl();
onResize(); onResize();
onMounted(() => window.addEventListener('resize', onResize)); onMounted(() => {
onUnmounted(() => window.addEventListener('resize', onResize)); window.addEventListener('resize', onResize);
window.addEventListener('keydown', onKeydown);
});
onUnmounted(() => {
window.addEventListener('resize', onResize);
window.addEventListener('keydown', onKeydown);
});
return { return {
chatBoxEnabled, chatBoxEnabled,

View File

@ -15,25 +15,22 @@
--> -->
<template> <template>
<section class="sidebar" role="none"> <section class="sidebar" role="none" ref="sidebar">
<header class="sidebar__buttons"> <header class="sidebar__buttons">
<button v-if="mapCount > 1" :class="{'button--maps': true}" <button v-if="mapCount > 1" :class="{'button--maps': true}" @click="toggleMaps" :title="messageWorlds"
@click="toggleMaps" :title="messageWorlds" :aria-label="messageWorlds" :aria-expanded="mapsVisible">
:aria-label="messageWorlds" :aria-expanded="currentlyVisible.has('maps')">
<SvgIcon name="maps"></SvgIcon> <SvgIcon name="maps"></SvgIcon>
</button> </button>
<button :class="{'button--players': true}" <button :class="{'button--players': true}" @click="togglePlayers" :title="messagePlayers"
@click="togglePlayers" :title="messagePlayers" :aria-label="messagePlayers" :aria-expanded="playersVisible">
:aria-label="messagePlayers" :aria-expanded="currentlyVisible.has('players')">
<SvgIcon name="players"></SvgIcon> <SvgIcon name="players"></SvgIcon>
</button> </button>
</header> </header>
<div class="sidebar__content" @keydown="handleSidebarKeydown"> <div class="sidebar__content" @keydown="handleSidebarKeydown">
<ServerList v-if="serverCount > 1" v-show="currentlyVisible.has('maps')"></ServerList> <ServerList v-if="serverCount > 1" v-show="mapsVisible"></ServerList>
<WorldList v-if="mapCount > 1" v-show="currentlyVisible.has('maps')"></WorldList> <WorldList v-if="mapCount > 1" v-show="mapsVisible"></WorldList>
<PlayerList id="players" v-if="previouslyVisible.has('players')" <PlayerList id="players" v-if="previouslyVisible.has('players')" v-show="playersVisible"></PlayerList>
v-show="currentlyVisible.has('players')"></PlayerList> <FollowTarget v-if="following" v-show="followVisible" :target="following"></FollowTarget>
<FollowTarget v-if="following" v-show="followActive" :target="following"></FollowTarget>
</div> </div>
</section> </section>
</template> </template>
@ -49,8 +46,9 @@ import SvgIcon from "@/components/SvgIcon.vue";
import {MutationTypes} from "@/store/mutation-types"; import {MutationTypes} from "@/store/mutation-types";
import "@/assets/icons/players.svg"; import "@/assets/icons/players.svg";
import "@/assets/icons/maps.svg"; import "@/assets/icons/maps.svg";
import {nextTick} from "vue"; import {nextTick, ref, watch} from "vue";
import {handleKeyboardEvent} from "@/util/events"; import {handleKeyboardEvent} from "@/util/events";
import {focus} from "@/util";
export default defineComponent({ export default defineComponent({
components: { components: {
@ -63,6 +61,8 @@ export default defineComponent({
setup() { setup() {
const store = useStore(), const store = useStore(),
sidebar = ref<HTMLElement | null>(null),
currentlyVisible = computed(() => store.state.ui.visibleElements), currentlyVisible = computed(() => store.state.ui.visibleElements),
previouslyVisible = computed(() => store.state.ui.previouslyVisibleElements), previouslyVisible = computed(() => store.state.ui.previouslyVisibleElements),
smallScreen = computed(() => store.state.ui.smallScreen), smallScreen = computed(() => store.state.ui.smallScreen),
@ -73,78 +73,55 @@ export default defineComponent({
messageWorlds = computed(() => store.state.messages.worldsHeading), messageWorlds = computed(() => store.state.messages.worldsHeading),
messagePlayers = computed(() => store.state.messages.playersHeading), messagePlayers = computed(() => store.state.messages.playersHeading),
followActive = computed(() => { playersVisible = computed(() => currentlyVisible.value.has('players')),
mapsVisible = computed(() => currentlyVisible.value.has('maps')),
followVisible = computed(() => {
//Show following alongside playerlist on small screens //Show following alongside playerlist on small screens
return (!smallScreen.value && following.value) return (!smallScreen.value && following.value)
|| (smallScreen.value && currentlyVisible.value.has('players')); || (smallScreen.value && playersVisible.value);
}); });
return { //Arrow key section navigation
mapCount, const handleSidebarKeydown = (e: KeyboardEvent) => {
serverCount,
currentlyVisible,
previouslyVisible,
followActive,
following,
messageWorlds,
messagePlayers,
}
},
mounted() {
window.addEventListener('keydown', this.handleButtonKeydown);
},
unmounted() {
window.removeEventListener('keydown', this.handleButtonKeydown);
},
methods: {
handleButtonKeydown(e: KeyboardEvent) {
if(e.key === '!' && e.ctrlKey && e.shiftKey) {
e.preventDefault();
this.toggleMaps();
} else if(e.key === '"' && e.ctrlKey && e.shiftKey) {
e.preventDefault();
this.togglePlayers();
}
},
handleSidebarKeydown(e: KeyboardEvent) {
if(!e.target || !(e.target as HTMLElement).matches('.section__heading button')) { if(!e.target || !(e.target as HTMLElement).matches('.section__heading button')) {
return; return;
} }
const sectionHeadings: HTMLElement[] = Array.from(this.$el.querySelectorAll('.section__heading button')); const sectionHeadings: HTMLElement[] = Array.from(sidebar.value!.querySelectorAll('.section__heading button'));
handleKeyboardEvent(e, sectionHeadings); handleKeyboardEvent(e, sectionHeadings);
}, };
togglePlayers() {
useStore().commit(MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY, 'players');
nextTick(this.focusPlayers);
},
toggleMaps() {
useStore().commit(MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY, 'maps');
nextTick(this.focusMaps);
},
focusPlayers() {
if (this.currentlyVisible.has('players')) {
const heading = document.getElementById('players-heading');
if (heading) { const togglePlayers = () => store.commit(MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY, 'players');
heading.focus(); const toggleMaps = () => store.commit(MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY, 'maps');
}
//Move focus when sidebar sections become visible
const focusMaps = () => focus('.section__heading button');
const focusPlayers = () => focus('#players-heading');
watch(playersVisible, newValue => newValue && nextTick(() => focusPlayers()));
watch(mapsVisible, newValue => newValue && nextTick(() => focusMaps()));
return {
sidebar,
mapCount,
serverCount,
following,
messageWorlds,
messagePlayers,
previouslyVisible,
playersVisible,
mapsVisible,
followVisible,
handleSidebarKeydown,
togglePlayers,
toggleMaps
} }
}, },
focusMaps() { });
if(this.currentlyVisible.has('maps')) {
const heading = document.querySelector('.section__heading button');
if(heading) {
(heading as HTMLElement).focus();
}
}
}
}
})
</script> </script>

View File

@ -229,3 +229,11 @@ export const getUrlForLocation = (world: DynmapWorld, map: DynmapWorldMap, locat
return `#${world.name};${map.name};${locationString};${zoom}`; return `#${world.name};${map.name};${locationString};${zoom}`;
} }
export const focus = (selector: string) => {
const element = document.querySelector(selector);
if(element) {
(element as HTMLElement).focus();
}
}