Client - handle keyboard navigation on workouts displayed on calendar
This commit is contained in:
		| @@ -15,6 +15,7 @@ | |||||||
|           :workouts="filterWorkouts(day, workouts)" |           :workouts="filterWorkouts(day, workouts)" | ||||||
|           :sports="sports" |           :sports="sports" | ||||||
|           :displayHARecord="displayHARecord" |           :displayHARecord="displayHARecord" | ||||||
|  |           :index="i" | ||||||
|         /> |         /> | ||||||
|         <div class="calendar-cell-day"> |         <div class="calendar-cell-day"> | ||||||
|           {{ format(day, 'd') }} |           {{ format(day, 'd') }} | ||||||
|   | |||||||
| @@ -1,9 +1,7 @@ | |||||||
| <template> | <template> | ||||||
|   <div |   <router-link | ||||||
|     class="calendar-workout" |     class="calendar-workout" | ||||||
|     @click=" |     :to="{ name: 'Workout', params: { workoutId: workout.id } }" | ||||||
|       $router.push({ name: 'Workout', params: { workoutId: workout.id } }) |  | ||||||
|     " |  | ||||||
|   > |   > | ||||||
|     <SportImage |     <SportImage | ||||||
|       :sport-label="sportLabel" |       :sport-label="sportLabel" | ||||||
| @@ -26,7 +24,7 @@ | |||||||
|         " |         " | ||||||
|       /> |       /> | ||||||
|     </sup> |     </sup> | ||||||
|   </div> |   </router-link> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ | |||||||
|           :datasets="chartDatasets" |           :datasets="chartDatasets" | ||||||
|           :colors="colors" |           :colors="colors" | ||||||
|           :displayHARecord="displayHARecord" |           :displayHARecord="displayHARecord" | ||||||
|  |           :index="index" | ||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| @@ -32,6 +33,7 @@ | |||||||
|           :datasets="chartDatasets" |           :datasets="chartDatasets" | ||||||
|           :colors="colors" |           :colors="colors" | ||||||
|           :displayHARecord="displayHARecord" |           :displayHARecord="displayHARecord" | ||||||
|  |           :index="index" | ||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| @@ -52,10 +54,11 @@ | |||||||
|     displayHARecord: boolean |     displayHARecord: boolean | ||||||
|     workouts: IWorkout[] |     workouts: IWorkout[] | ||||||
|     sports: ISport[] |     sports: ISport[] | ||||||
|  |     index: number | ||||||
|   } |   } | ||||||
|   const props = defineProps<Props>() |   const props = defineProps<Props>() | ||||||
|  |  | ||||||
|   const { displayHARecord, workouts, sports } = toRefs(props) |   const { displayHARecord, index, sports, workouts } = toRefs(props) | ||||||
|   const chartDatasets = computed(() => getDonutDatasets(props.workouts)) |   const chartDatasets = computed(() => getDonutDatasets(props.workouts)) | ||||||
|   const colors = computed(() => sportIdColors(props.sports)) |   const colors = computed(() => sportIdColors(props.sports)) | ||||||
|   const displayedWorkoutCount = 6 |   const displayedWorkoutCount = 6 | ||||||
|   | |||||||
| @@ -1,16 +1,22 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="calendar-workouts-chart"> |   <div class="calendar-workouts-chart"> | ||||||
|     <div class="workouts-chart" @click="togglePane"> |     <button | ||||||
|  |       class="workouts-chart transparent" | ||||||
|  |       :id="`workouts-donut-${index}`" | ||||||
|  |       @click="togglePane" | ||||||
|  |     > | ||||||
|       <div class="workouts-count">{{ workouts.length }}</div> |       <div class="workouts-count">{{ workouts.length }}</div> | ||||||
|       <DonutChart :datasets="datasets" :colors="colors" /> |       <DonutChart :datasets="datasets" :colors="colors" /> | ||||||
|     </div> |     </button> | ||||||
|     <div class="workouts-pane" v-if="!isHidden"> |     <div class="workouts-pane" v-if="!isHidden"> | ||||||
|       <div class="more-workouts" v-click-outside="togglePane"> |       <div | ||||||
|         <i |         class="more-workouts" | ||||||
|           class="fa fa-times calendar-more" |         :id="`workouts-pane-${index}`" | ||||||
|           aria-hidden="true" |         v-click-outside="togglePane" | ||||||
|           @click="togglePane" |       > | ||||||
|         /> |         <button class="calendar-more-close transparent" @click="togglePane"> | ||||||
|  |           <i class="fa fa-times" aria-hidden="true" /> | ||||||
|  |         </button> | ||||||
|         <CalendarWorkout |         <CalendarWorkout | ||||||
|           v-for="(workout, index) in workouts" |           v-for="(workout, index) in workouts" | ||||||
|           :key="index" |           :key="index" | ||||||
| @@ -25,7 +31,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|   import { ref, toRefs } from 'vue' |   import { nextTick, onMounted, onUnmounted, ref, toRefs } from 'vue' | ||||||
|  |  | ||||||
|   import CalendarWorkout from '@/components/Dashboard/UserCalendar/CalendarWorkout.vue' |   import CalendarWorkout from '@/components/Dashboard/UserCalendar/CalendarWorkout.vue' | ||||||
|   import DonutChart from '@/components/Dashboard/UserCalendar/DonutChart.vue' |   import DonutChart from '@/components/Dashboard/UserCalendar/DonutChart.vue' | ||||||
| @@ -39,16 +45,71 @@ | |||||||
|     sports: ISport[] |     sports: ISport[] | ||||||
|     workouts: IWorkout[] |     workouts: IWorkout[] | ||||||
|     displayHARecord: boolean |     displayHARecord: boolean | ||||||
|  |     index: number | ||||||
|   } |   } | ||||||
|   const props = defineProps<Props>() |   const props = defineProps<Props>() | ||||||
|  |   let tabbableElementIndex = 0 | ||||||
|  |  | ||||||
|   const { colors, datasets, sports, workouts } = toRefs(props) |   const { colors, datasets, index, sports, workouts } = toRefs(props) | ||||||
|   const isHidden = ref(true) |   const isHidden = ref(true) | ||||||
|  |  | ||||||
|   function togglePane(event: Event) { |   function isWorkoutsMorePaneDisplayed() { | ||||||
|  |     const pane = document.getElementById(`workouts-pane-${index.value}`) | ||||||
|  |     return pane?.children && pane?.children.length > 0 ? pane : null | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async function togglePane(event: Event) { | ||||||
|  |     event.preventDefault() | ||||||
|     event.stopPropagation() |     event.stopPropagation() | ||||||
|     isHidden.value = !isHidden.value |     isHidden.value = !isHidden.value | ||||||
|  |     await nextTick() | ||||||
|  |     const pane = isWorkoutsMorePaneDisplayed() | ||||||
|  |     if (!isHidden.value) { | ||||||
|  |       ;(pane?.children[0] as HTMLButtonElement).focus() | ||||||
|  |     } else { | ||||||
|  |       document.getElementById(`workouts-donut-${index.value}`)?.focus() | ||||||
|     } |     } | ||||||
|  |   } | ||||||
|  |   function handleKey(e: KeyboardEvent) { | ||||||
|  |     if (!isHidden.value) { | ||||||
|  |       // focusTrap | ||||||
|  |       if (!isHidden.value && (e.key === 'Tab' || e.keyCode === 9)) { | ||||||
|  |         e.preventDefault() | ||||||
|  |         e.stopPropagation() | ||||||
|  |         const pane = isWorkoutsMorePaneDisplayed() | ||||||
|  |         if (pane) { | ||||||
|  |           if (e.shiftKey) { | ||||||
|  |             tabbableElementIndex -= 1 | ||||||
|  |             if (tabbableElementIndex < 0) { | ||||||
|  |               tabbableElementIndex = pane.children.length - 1 | ||||||
|  |             } | ||||||
|  |           } else { | ||||||
|  |             tabbableElementIndex += 1 | ||||||
|  |             if (tabbableElementIndex >= pane.children.length) { | ||||||
|  |               tabbableElementIndex = 0 | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |           // children are only links or buttons | ||||||
|  |           ;( | ||||||
|  |             pane.children[tabbableElementIndex] as | ||||||
|  |               | HTMLButtonElement | ||||||
|  |               | HTMLLinkElement | ||||||
|  |           ).focus() | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       // close pane on Escape | ||||||
|  |       if (e.key === 'Escape') { | ||||||
|  |         togglePane(e) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   onMounted(() => { | ||||||
|  |     document.addEventListener('keydown', handleKey) | ||||||
|  |   }) | ||||||
|  |   onUnmounted(() => { | ||||||
|  |     document.removeEventListener('keydown', handleKey) | ||||||
|  |   }) | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @@ -58,6 +119,7 @@ | |||||||
|  |  | ||||||
|     .workouts-chart { |     .workouts-chart { | ||||||
|       position: relative; |       position: relative; | ||||||
|  |       padding: 0; | ||||||
|       .workouts-count { |       .workouts-count { | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: center; |         justify-content: center; | ||||||
| @@ -108,11 +170,12 @@ | |||||||
|         flex-wrap: wrap; |         flex-wrap: wrap; | ||||||
|         z-index: 1000; |         z-index: 1000; | ||||||
|  |  | ||||||
|         .calendar-more { |         .calendar-more-close { | ||||||
|           position: absolute; |           position: absolute; | ||||||
|           font-size: 0.9em; |           font-size: 0.9em; | ||||||
|           top: 5px; |           top: 5px; | ||||||
|           right: 5px; |           right: 5px; | ||||||
|  |           padding: 0; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -95,6 +95,7 @@ button { | |||||||
|   &.transparent { |   &.transparent { | ||||||
|     font-family: 'PT Sans', Helvetica, Arial, sans-serif; |     font-family: 'PT Sans', Helvetica, Arial, sans-serif; | ||||||
|     font-size: 1em; |     font-size: 1em; | ||||||
|  |     background: transparent; | ||||||
|     border-color: transparent; |     border-color: transparent; | ||||||
|     box-shadow: none; |     box-shadow: none; | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user