Client - improve keyboard navigation
This commit is contained in:
		| @@ -7,7 +7,7 @@ | ||||
|         <div class="admin-menu description-list"> | ||||
|           <dl> | ||||
|             <dt> | ||||
|               <router-link to="/admin/application"> | ||||
|               <router-link id="adminLink" to="/admin/application"> | ||||
|                 {{ $t('admin.APPLICATION') }} | ||||
|               </router-link> | ||||
|             </dt> | ||||
| @@ -54,7 +54,7 @@ | ||||
| </template> | ||||
|  | ||||
| <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 Card from '@/components/Common/Card.vue' | ||||
| @@ -69,6 +69,13 @@ | ||||
|   }) | ||||
|  | ||||
|   const { appConfig, appStatistics } = toRefs(props) | ||||
|  | ||||
|   onMounted(() => { | ||||
|     const applicationLink = document.getElementById('adminLink') | ||||
|     if (applicationLink) { | ||||
|       applicationLink.focus() | ||||
|     } | ||||
|   }) | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|   | ||||
| @@ -22,8 +22,8 @@ | ||||
|               {{ $t('buttons.YES') }} | ||||
|             </button> | ||||
|             <button | ||||
|               tabindex="0" | ||||
|               id="cancel-button" | ||||
|               :tabindex="0" | ||||
|               :id="`${name}-cancel-button`" | ||||
|               class="cancel" | ||||
|               @click="emit('cancelAction')" | ||||
|             > | ||||
| @@ -46,9 +46,11 @@ | ||||
|     title: string | ||||
|     message: string | ||||
|     strongMessage?: string | null | ||||
|     name?: string | null | ||||
|   } | ||||
|   const props = withDefaults(defineProps<Props>(), { | ||||
|     strongMessage: () => null, | ||||
|     name: 'modal', | ||||
|   }) | ||||
|  | ||||
|   const emit = defineEmits(['cancelAction', 'confirmAction']) | ||||
|   | ||||
| @@ -138,7 +138,7 @@ | ||||
|   function updateDisplayModal(display: boolean) { | ||||
|     displayModal.value = display | ||||
|     if (display) { | ||||
|       const button = document.getElementById('cancel-button') | ||||
|       const button = document.getElementById('modal-cancel-button') | ||||
|       if (button) { | ||||
|         button.focus() | ||||
|       } | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| <template> | ||||
|   <div class="chart-menu"> | ||||
|     <div class="chart-arrow"> | ||||
|       <i | ||||
|         class="fa fa-chevron-left" | ||||
|         aria-hidden="true" | ||||
|     <button | ||||
|       class="chart-arrow transparent" | ||||
|       @click="emit('arrowClick', true)" | ||||
|       /> | ||||
|     </div> | ||||
|       @keydown.enter="emit('arrowClick', true)" | ||||
|     > | ||||
|       <i class="fa fa-chevron-left" aria-hidden="true" /> | ||||
|     </button> | ||||
|     <div class="time-frames custom-checkboxes-group"> | ||||
|       <div class="time-frames-checkboxes custom-checkboxes"> | ||||
|         <div | ||||
| @@ -22,23 +22,30 @@ | ||||
|               :checked="selectedTimeFrame === 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> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="chart-arrow"> | ||||
|       <i | ||||
|         class="fa fa-chevron-right" | ||||
|         aria-hidden="true" | ||||
|     <button | ||||
|       class="chart-arrow transparent" | ||||
|       @click="emit('arrowClick', false)" | ||||
|       /> | ||||
|     </div> | ||||
|       @keydown.enter="emit('arrowClick', false)" | ||||
|     > | ||||
|       <i class="fa fa-chevron-right" aria-hidden="true" /> | ||||
|     </button> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
|   import { ref } from 'vue' | ||||
|   import { onMounted, ref } from 'vue' | ||||
|  | ||||
|   const emit = defineEmits(['arrowClick', 'timeFrameUpdate']) | ||||
|  | ||||
| @@ -49,11 +56,19 @@ | ||||
|     selectedTimeFrame.value = timeFrame | ||||
|     emit('timeFrameUpdate', timeFrame) | ||||
|   } | ||||
|  | ||||
|   onMounted(() => { | ||||
|     const input = document.getElementById('frame-month') | ||||
|     if (input) { | ||||
|       input.focus() | ||||
|     } | ||||
|   }) | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|   .chart-menu { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|  | ||||
|     .chart-arrow, | ||||
|     .time-frames { | ||||
|   | ||||
| @@ -11,7 +11,14 @@ | ||||
|             :disabled="disabled" | ||||
|             @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> | ||||
|       </div> | ||||
|     </div> | ||||
| @@ -19,7 +26,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
|   import { toRefs, withDefaults } from 'vue' | ||||
|   import { onMounted, toRefs, withDefaults } from 'vue' | ||||
|  | ||||
|   interface Props { | ||||
|     tabs: string[] | ||||
| @@ -33,6 +40,13 @@ | ||||
|  | ||||
|   const { tabs, selectedTab, disabled } = toRefs(props) | ||||
|  | ||||
|   onMounted(() => { | ||||
|     const input = document.getElementById(`tab-${tabs.value[0]}`) | ||||
|     if (input) { | ||||
|       input.focus() | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   function getPath(tab: string) { | ||||
|     switch (tab) { | ||||
|       case 'ACCOUNT': | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| <template> | ||||
|   <div id="workout-card-title"> | ||||
|     <div | ||||
|       class="workout-previous workout-arrow" | ||||
|     <button | ||||
|       class="workout-previous workout-arrow transparent" | ||||
|       :class="{ inactive: !workoutObject.previousUrl }" | ||||
|       :disabled="!workoutObject.previousUrl" | ||||
|       :title=" | ||||
|         workoutObject.previousUrl | ||||
|           ? $t(`workouts.PREVIOUS_${workoutObject.type}`) | ||||
| @@ -15,33 +16,37 @@ | ||||
|       " | ||||
|     > | ||||
|       <i class="fa fa-chevron-left" aria-hidden="true" /> | ||||
|     </div> | ||||
|     </button> | ||||
|     <div class="workout-card-title"> | ||||
|       <SportImage :sport-label="sport.label" :color="sport.color" /> | ||||
|       <div class="workout-title-date"> | ||||
|         <div class="workout-title" v-if="workoutObject.type === 'WORKOUT'"> | ||||
|           <span>{{ workoutObject.title }}</span> | ||||
|           <i | ||||
|             class="fa fa-edit" | ||||
|             aria-hidden="true" | ||||
|           <button | ||||
|             class="transparent icon-button" | ||||
|             @click=" | ||||
|               $router.push({ | ||||
|                 name: 'EditWorkout', | ||||
|                 params: { workoutId: workoutObject.workoutId }, | ||||
|               }) | ||||
|             " | ||||
|           /> | ||||
|           <i | ||||
|           > | ||||
|             <i class="fa fa-edit" aria-hidden="true" /> | ||||
|           </button> | ||||
|           <button | ||||
|             v-if="workoutObject.with_gpx" | ||||
|             class="fa fa-download" | ||||
|             aria-hidden="true" | ||||
|             class="transparent icon-button" | ||||
|             @click.prevent="downloadGpx(workoutObject.workoutId)" | ||||
|           /> | ||||
|           <i | ||||
|             class="fa fa-trash" | ||||
|             aria-hidden="true" | ||||
|             @click="emit('displayModal', true)" | ||||
|           /> | ||||
|           > | ||||
|             <i class="fa fa-download" aria-hidden="true" /> | ||||
|           </button> | ||||
|           <button | ||||
|             id="delete-workout-button" | ||||
|             class="transparent icon-button" | ||||
|             @click="displayDeleteModal" | ||||
|           > | ||||
|             <i class="fa fa-trash" aria-hidden="true" /> | ||||
|           </button> | ||||
|         </div> | ||||
|         <div class="workout-title" v-else> | ||||
|           {{ workoutObject.title }} | ||||
| @@ -69,9 +74,10 @@ | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div | ||||
|       class="workout-next workout-arrow" | ||||
|     <button | ||||
|       class="workout-next workout-arrow transparent" | ||||
|       :class="{ inactive: !workoutObject.nextUrl }" | ||||
|       :disabled="!workoutObject.nextUrl" | ||||
|       :title=" | ||||
|         workoutObject.nextUrl | ||||
|           ? $t(`workouts.NEXT_${workoutObject.type}`) | ||||
| @@ -82,7 +88,7 @@ | ||||
|       " | ||||
|     > | ||||
|       <i class="fa fa-chevron-right" aria-hidden="true" /> | ||||
|     </div> | ||||
|     </button> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| @@ -119,6 +125,10 @@ | ||||
|         gpxLink.click() | ||||
|       }) | ||||
|   } | ||||
|   function displayDeleteModal(event: Event & { target: HTMLInputElement }) { | ||||
|     event.target.blur() | ||||
|     emit('displayModal', true) | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @@ -166,9 +176,13 @@ | ||||
|       } | ||||
|  | ||||
|       .fa { | ||||
|         cursor: pointer; | ||||
|         padding: 0 $default-padding * 0.3; | ||||
|       } | ||||
|       .icon-button { | ||||
|         cursor: pointer; | ||||
|         padding: 0; | ||||
|         margin-left: 2px; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     @media screen and (max-width: $small-limit) { | ||||
|   | ||||
| @@ -18,12 +18,20 @@ | ||||
|             @ready="fitBounds(bounds)" | ||||
|           > | ||||
|             <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" /> | ||||
|             </LControl> | ||||
|             <LControl | ||||
|               position="topleft" | ||||
|               class="map-control" | ||||
|               tabindex="0" | ||||
|               role="button" | ||||
|               @click="toggleFullscreen" | ||||
|             > | ||||
|               <i | ||||
|   | ||||
| @@ -2,10 +2,12 @@ | ||||
|   <div class="workout-detail"> | ||||
|     <Modal | ||||
|       v-if="displayModal" | ||||
|       name="workout" | ||||
|       :title="$t('common.CONFIRMATION')" | ||||
|       :message="$t('workouts.WORKOUT_DELETION_CONFIRMATION')" | ||||
|       @confirmAction="deleteWorkout(workoutObject.workoutId)" | ||||
|       @cancelAction="updateDisplayModal(false)" | ||||
|       @cancelAction="cancelDelete" | ||||
|       @keydown.esc="cancelDelete" | ||||
|     /> | ||||
|     <Card> | ||||
|       <template #title> | ||||
| @@ -35,6 +37,7 @@ | ||||
|     ComputedRef, | ||||
|     Ref, | ||||
|     computed, | ||||
|     nextTick, | ||||
|     ref, | ||||
|     toRefs, | ||||
|     watch, | ||||
| @@ -161,18 +164,50 @@ | ||||
|   } | ||||
|   function updateDisplayModal(value: boolean) { | ||||
|     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) { | ||||
|     updateDisplayModal(false) | ||||
|     store.dispatch(WORKOUTS_STORE.ACTIONS.DELETE_WORKOUT, { | ||||
|       workoutId: workoutId, | ||||
|     }) | ||||
|   } | ||||
|   function scrollToTop() { | ||||
|     window.scrollTo({ | ||||
|       top: 0, | ||||
|       behavior: 'smooth', | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   watch( | ||||
|     () => route.params.segmentId, | ||||
|     async (newSegmentId) => { | ||||
|       if (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([]) | ||||
|  | ||||
|   onMounted(() => { | ||||
|     let element | ||||
|     if (props.workout.id) { | ||||
|       formatWorkoutForm(props.workout) | ||||
|       element = document.getElementById('sport') | ||||
|     } else { | ||||
|       element = document.getElementById('withGpx') | ||||
|     } | ||||
|     if (element) { | ||||
|       element.focus() | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
|             <div class="form-item"> | ||||
|               <label> {{ $t('workouts.FROM') }}: </label> | ||||
|               <input | ||||
|                 id="from" | ||||
|                 name="from" | ||||
|                 type="date" | ||||
|                 :value="$route.query.from" | ||||
| @@ -185,7 +186,7 @@ | ||||
| </template> | ||||
|  | ||||
| <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 { LocationQuery, useRoute, useRouter } from 'vue-router' | ||||
|  | ||||
| @@ -216,6 +217,13 @@ | ||||
|   ) | ||||
|   let params: LocationQuery = Object.assign({}, route.query) | ||||
|  | ||||
|   onMounted(() => { | ||||
|     const filter = document.getElementById('from') | ||||
|     if (filter) { | ||||
|       filter.focus() | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   function handleFilterChange(event: Event & { target: HTMLInputElement }) { | ||||
|     if (event.target.value === '') { | ||||
|       delete params[event.target.name] | ||||
|   | ||||
| @@ -85,14 +85,22 @@ button { | ||||
|     border-color: transparent; | ||||
|     box-shadow: none; | ||||
|  | ||||
|     &:hover { | ||||
|     &:hover, &:disabled { | ||||
|       background: transparent; | ||||
|     } | ||||
|  | ||||
|     &:hover { | ||||
|       color: var(--app-color); | ||||
|     } | ||||
|  | ||||
|     &:enabled:active { | ||||
|       box-shadow: none; | ||||
|     } | ||||
|  | ||||
|     &:disabled, &.confirm:disabled { | ||||
|       border-color: transparent; | ||||
|       color: var(--disabled-color); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &:hover { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user