Client - improve keyboard navigation
This commit is contained in:
		| @@ -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" |  | ||||||
|         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 { | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user