Client - improve keyboard navigation
This commit is contained in:
parent
6653648422
commit
3f672b5e90
@ -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>
|
||||||
|
@ -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'])
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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"
|
@click="emit('arrowClick', true)"
|
||||||
aria-hidden="true"
|
@keydown.enter="emit('arrowClick', true)"
|
||||||
@click="emit('arrowClick', true)"
|
>
|
||||||
/>
|
<i class="fa fa-chevron-left" aria-hidden="true" />
|
||||||
</div>
|
</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"
|
@click="emit('arrowClick', false)"
|
||||||
aria-hidden="true"
|
@keydown.enter="emit('arrowClick', false)"
|
||||||
@click="emit('arrowClick', false)"
|
>
|
||||||
/>
|
<i class="fa fa-chevron-right" aria-hidden="true" />
|
||||||
</div>
|
</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 {
|
||||||
|
@ -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':
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user