Client - improve keyboard navigation

This commit is contained in:
Sam 2023-07-13 12:54:45 +02:00
parent 6653648422
commit 3f672b5e90
11 changed files with 165 additions and 47 deletions

View File

@ -7,7 +7,7 @@
<div class="admin-menu description-list"> <div class="admin-menu description-list">
<dl> <dl>
<dt> <dt>
<router-link to="/admin/application"> <router-link id="adminLink" to="/admin/application">
{{ $t('admin.APPLICATION') }} {{ $t('admin.APPLICATION') }}
</router-link> </router-link>
</dt> </dt>
@ -54,7 +54,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { capitalize, toRefs, withDefaults } from 'vue' import { capitalize, onMounted, toRefs, withDefaults } from 'vue'
import AppStatsCards from '@/components/Administration/AppStatsCards.vue' import AppStatsCards from '@/components/Administration/AppStatsCards.vue'
import Card from '@/components/Common/Card.vue' import Card from '@/components/Common/Card.vue'
@ -69,6 +69,13 @@
}) })
const { appConfig, appStatistics } = toRefs(props) const { appConfig, appStatistics } = toRefs(props)
onMounted(() => {
const applicationLink = document.getElementById('adminLink')
if (applicationLink) {
applicationLink.focus()
}
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -22,8 +22,8 @@
{{ $t('buttons.YES') }} {{ $t('buttons.YES') }}
</button> </button>
<button <button
tabindex="0" :tabindex="0"
id="cancel-button" :id="`${name}-cancel-button`"
class="cancel" class="cancel"
@click="emit('cancelAction')" @click="emit('cancelAction')"
> >
@ -46,9 +46,11 @@
title: string title: string
message: string message: string
strongMessage?: string | null strongMessage?: string | null
name?: string | null
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
strongMessage: () => null, strongMessage: () => null,
name: 'modal',
}) })
const emit = defineEmits(['cancelAction', 'confirmAction']) const emit = defineEmits(['cancelAction', 'confirmAction'])

View File

@ -138,7 +138,7 @@
function updateDisplayModal(display: boolean) { function updateDisplayModal(display: boolean) {
displayModal.value = display displayModal.value = display
if (display) { if (display) {
const button = document.getElementById('cancel-button') const button = document.getElementById('modal-cancel-button')
if (button) { if (button) {
button.focus() button.focus()
} }

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="chart-menu"> <div class="chart-menu">
<div class="chart-arrow"> <button
<i class="chart-arrow transparent"
class="fa fa-chevron-left"
aria-hidden="true"
@click="emit('arrowClick', true)" @click="emit('arrowClick', true)"
/> @keydown.enter="emit('arrowClick', true)"
</div> >
<i class="fa fa-chevron-left" aria-hidden="true" />
</button>
<div class="time-frames custom-checkboxes-group"> <div class="time-frames custom-checkboxes-group">
<div class="time-frames-checkboxes custom-checkboxes"> <div class="time-frames-checkboxes custom-checkboxes">
<div <div
@ -22,23 +22,30 @@
:checked="selectedTimeFrame === frame" :checked="selectedTimeFrame === frame"
@input="onUpdateTimeFrame(frame)" @input="onUpdateTimeFrame(frame)"
/> />
<span>{{ $t(`statistics.TIME_FRAMES.${frame}`) }}</span> <span
:id="`frame-${frame}`"
:tabindex="0"
role="button"
@keydown.enter="onUpdateTimeFrame(frame)"
>
{{ $t(`statistics.TIME_FRAMES.${frame}`) }}
</span>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<div class="chart-arrow"> <button
<i class="chart-arrow transparent"
class="fa fa-chevron-right"
aria-hidden="true"
@click="emit('arrowClick', false)" @click="emit('arrowClick', false)"
/> @keydown.enter="emit('arrowClick', false)"
</div> >
<i class="fa fa-chevron-right" aria-hidden="true" />
</button>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { onMounted, ref } from 'vue'
const emit = defineEmits(['arrowClick', 'timeFrameUpdate']) const emit = defineEmits(['arrowClick', 'timeFrameUpdate'])
@ -49,11 +56,19 @@
selectedTimeFrame.value = timeFrame selectedTimeFrame.value = timeFrame
emit('timeFrameUpdate', timeFrame) emit('timeFrameUpdate', timeFrame)
} }
onMounted(() => {
const input = document.getElementById('frame-month')
if (input) {
input.focus()
}
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chart-menu { .chart-menu {
display: flex; display: flex;
align-items: center;
.chart-arrow, .chart-arrow,
.time-frames { .time-frames {

View File

@ -11,7 +11,14 @@
:disabled="disabled" :disabled="disabled"
@input="$router.push(getPath(tab))" @input="$router.push(getPath(tab))"
/> />
<span>{{ $t(`user.PROFILE.TABS.${tab}`) }}</span> <span
:id="`tab-${tab}`"
:tabindex="0"
role="button"
@keydown.enter="$router.push(getPath(tab))"
>
{{ $t(`user.PROFILE.TABS.${tab}`) }}
</span>
</label> </label>
</div> </div>
</div> </div>
@ -19,7 +26,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { toRefs, withDefaults } from 'vue' import { onMounted, toRefs, withDefaults } from 'vue'
interface Props { interface Props {
tabs: string[] tabs: string[]
@ -33,6 +40,13 @@
const { tabs, selectedTab, disabled } = toRefs(props) const { tabs, selectedTab, disabled } = toRefs(props)
onMounted(() => {
const input = document.getElementById(`tab-${tabs.value[0]}`)
if (input) {
input.focus()
}
})
function getPath(tab: string) { function getPath(tab: string) {
switch (tab) { switch (tab) {
case 'ACCOUNT': case 'ACCOUNT':

View File

@ -1,8 +1,9 @@
<template> <template>
<div id="workout-card-title"> <div id="workout-card-title">
<div <button
class="workout-previous workout-arrow" class="workout-previous workout-arrow transparent"
:class="{ inactive: !workoutObject.previousUrl }" :class="{ inactive: !workoutObject.previousUrl }"
:disabled="!workoutObject.previousUrl"
:title=" :title="
workoutObject.previousUrl workoutObject.previousUrl
? $t(`workouts.PREVIOUS_${workoutObject.type}`) ? $t(`workouts.PREVIOUS_${workoutObject.type}`)
@ -15,33 +16,37 @@
" "
> >
<i class="fa fa-chevron-left" aria-hidden="true" /> <i class="fa fa-chevron-left" aria-hidden="true" />
</div> </button>
<div class="workout-card-title"> <div class="workout-card-title">
<SportImage :sport-label="sport.label" :color="sport.color" /> <SportImage :sport-label="sport.label" :color="sport.color" />
<div class="workout-title-date"> <div class="workout-title-date">
<div class="workout-title" v-if="workoutObject.type === 'WORKOUT'"> <div class="workout-title" v-if="workoutObject.type === 'WORKOUT'">
<span>{{ workoutObject.title }}</span> <span>{{ workoutObject.title }}</span>
<i <button
class="fa fa-edit" class="transparent icon-button"
aria-hidden="true"
@click=" @click="
$router.push({ $router.push({
name: 'EditWorkout', name: 'EditWorkout',
params: { workoutId: workoutObject.workoutId }, params: { workoutId: workoutObject.workoutId },
}) })
" "
/> >
<i <i class="fa fa-edit" aria-hidden="true" />
</button>
<button
v-if="workoutObject.with_gpx" v-if="workoutObject.with_gpx"
class="fa fa-download" class="transparent icon-button"
aria-hidden="true"
@click.prevent="downloadGpx(workoutObject.workoutId)" @click.prevent="downloadGpx(workoutObject.workoutId)"
/> >
<i <i class="fa fa-download" aria-hidden="true" />
class="fa fa-trash" </button>
aria-hidden="true" <button
@click="emit('displayModal', true)" id="delete-workout-button"
/> class="transparent icon-button"
@click="displayDeleteModal"
>
<i class="fa fa-trash" aria-hidden="true" />
</button>
</div> </div>
<div class="workout-title" v-else> <div class="workout-title" v-else>
{{ workoutObject.title }} {{ workoutObject.title }}
@ -69,9 +74,10 @@
</div> </div>
</div> </div>
</div> </div>
<div <button
class="workout-next workout-arrow" class="workout-next workout-arrow transparent"
:class="{ inactive: !workoutObject.nextUrl }" :class="{ inactive: !workoutObject.nextUrl }"
:disabled="!workoutObject.nextUrl"
:title=" :title="
workoutObject.nextUrl workoutObject.nextUrl
? $t(`workouts.NEXT_${workoutObject.type}`) ? $t(`workouts.NEXT_${workoutObject.type}`)
@ -82,7 +88,7 @@
" "
> >
<i class="fa fa-chevron-right" aria-hidden="true" /> <i class="fa fa-chevron-right" aria-hidden="true" />
</div> </button>
</div> </div>
</template> </template>
@ -119,6 +125,10 @@
gpxLink.click() gpxLink.click()
}) })
} }
function displayDeleteModal(event: Event & { target: HTMLInputElement }) {
event.target.blur()
emit('displayModal', true)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -166,9 +176,13 @@
} }
.fa { .fa {
cursor: pointer;
padding: 0 $default-padding * 0.3; padding: 0 $default-padding * 0.3;
} }
.icon-button {
cursor: pointer;
padding: 0;
margin-left: 2px;
}
} }
@media screen and (max-width: $small-limit) { @media screen and (max-width: $small-limit) {

View File

@ -18,12 +18,20 @@
@ready="fitBounds(bounds)" @ready="fitBounds(bounds)"
> >
<LControlLayers /> <LControlLayers />
<LControl position="topleft" class="map-control" @click="resetZoom"> <LControl
position="topleft"
class="map-control"
tabindex="0"
role="button"
@click="resetZoom"
>
<i class="fa fa-refresh" aria-hidden="true" /> <i class="fa fa-refresh" aria-hidden="true" />
</LControl> </LControl>
<LControl <LControl
position="topleft" position="topleft"
class="map-control" class="map-control"
tabindex="0"
role="button"
@click="toggleFullscreen" @click="toggleFullscreen"
> >
<i <i

View File

@ -2,10 +2,12 @@
<div class="workout-detail"> <div class="workout-detail">
<Modal <Modal
v-if="displayModal" v-if="displayModal"
name="workout"
:title="$t('common.CONFIRMATION')" :title="$t('common.CONFIRMATION')"
:message="$t('workouts.WORKOUT_DELETION_CONFIRMATION')" :message="$t('workouts.WORKOUT_DELETION_CONFIRMATION')"
@confirmAction="deleteWorkout(workoutObject.workoutId)" @confirmAction="deleteWorkout(workoutObject.workoutId)"
@cancelAction="updateDisplayModal(false)" @cancelAction="cancelDelete"
@keydown.esc="cancelDelete"
/> />
<Card> <Card>
<template #title> <template #title>
@ -35,6 +37,7 @@
ComputedRef, ComputedRef,
Ref, Ref,
computed, computed,
nextTick,
ref, ref,
toRefs, toRefs,
watch, watch,
@ -161,18 +164,50 @@
} }
function updateDisplayModal(value: boolean) { function updateDisplayModal(value: boolean) {
displayModal.value = value displayModal.value = value
if (displayModal.value) {
nextTick(() => {
const button = document.getElementById('workout-cancel-button')
if (button) {
button.focus()
}
})
}
}
function cancelDelete() {
updateDisplayModal(false)
const button = document.getElementById('delete-workout-button')
if (button) {
button.focus()
}
} }
function deleteWorkout(workoutId: string) { function deleteWorkout(workoutId: string) {
updateDisplayModal(false)
store.dispatch(WORKOUTS_STORE.ACTIONS.DELETE_WORKOUT, { store.dispatch(WORKOUTS_STORE.ACTIONS.DELETE_WORKOUT, {
workoutId: workoutId, workoutId: workoutId,
}) })
} }
function scrollToTop() {
window.scrollTo({
top: 0,
behavior: 'smooth',
})
}
watch( watch(
() => route.params.segmentId, () => route.params.segmentId,
async (newSegmentId) => { async (newSegmentId) => {
if (newSegmentId) { if (newSegmentId) {
segmentId.value = +newSegmentId segmentId.value = +newSegmentId
scrollToTop()
}
}
)
watch(
() => route.params.workoutId,
async (workoutId) => {
if (workoutId) {
displayModal.value = false
scrollToTop()
} }
} }
) )

View File

@ -352,8 +352,15 @@
const payloadErrorMessages: Ref<string[]> = ref([]) const payloadErrorMessages: Ref<string[]> = ref([])
onMounted(() => { onMounted(() => {
let element
if (props.workout.id) { if (props.workout.id) {
formatWorkoutForm(props.workout) formatWorkoutForm(props.workout)
element = document.getElementById('sport')
} else {
element = document.getElementById('withGpx')
}
if (element) {
element.focus()
} }
}) })

View File

@ -7,6 +7,7 @@
<div class="form-item"> <div class="form-item">
<label> {{ $t('workouts.FROM') }}: </label> <label> {{ $t('workouts.FROM') }}: </label>
<input <input
id="from"
name="from" name="from"
type="date" type="date"
:value="$route.query.from" :value="$route.query.from"
@ -185,7 +186,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ComputedRef, computed, toRefs, watch } from 'vue' import { ComputedRef, computed, toRefs, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { LocationQuery, useRoute, useRouter } from 'vue-router' import { LocationQuery, useRoute, useRouter } from 'vue-router'
@ -216,6 +217,13 @@
) )
let params: LocationQuery = Object.assign({}, route.query) let params: LocationQuery = Object.assign({}, route.query)
onMounted(() => {
const filter = document.getElementById('from')
if (filter) {
filter.focus()
}
})
function handleFilterChange(event: Event & { target: HTMLInputElement }) { function handleFilterChange(event: Event & { target: HTMLInputElement }) {
if (event.target.value === '') { if (event.target.value === '') {
delete params[event.target.name] delete params[event.target.name]

View File

@ -85,14 +85,22 @@ button {
border-color: transparent; border-color: transparent;
box-shadow: none; box-shadow: none;
&:hover { &:hover, &:disabled {
background: transparent; background: transparent;
}
&:hover {
color: var(--app-color); color: var(--app-color);
} }
&:enabled:active { &:enabled:active {
box-shadow: none; box-shadow: none;
} }
&:disabled, &.confirm:disabled {
border-color: transparent;
color: var(--disabled-color);
}
} }
&:hover { &:hover {