Add useFocusTrap

This commit is contained in:
Kasper Seweryn 2022-05-01 11:19:34 +02:00 committed by Georg Krause
parent 7e53e9a511
commit cbaa58d215
2 changed files with 124 additions and 113 deletions

View File

@ -1,5 +1,6 @@
<template>
<section
ref="queueModal"
class="main with-background component-queue"
:aria-label="labels.queue"
>
@ -221,7 +222,7 @@
<div>
<translate
translate-context="Sidebar/Queue/Text"
:translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}"
:translate-params="{index: currentIndex + 1, length: queue.tracks.length}"
>
Track %{ index } of %{ length }
</translate><template v-if="!$store.state.radios.running">
@ -240,13 +241,13 @@
v-model:list="tracks"
tag="tbody"
handle=".handle"
@update="reorder"
item-key="id"
@update="reorder"
>
<template #item="{ element: track, index }">
<tr
:key="track.id"
:class="['queue-item', {'active': index === queue.currentIndex}]"
:class="['queue-item', {'active': index === currentIndex}]"
>
<td class="handle">
<i class="grip lines icon" />
@ -285,8 +286,8 @@
>
<strong>{{ track.title }}</strong><br>
<span>
{{ track.artist.name }}
</span>
{{ track.artist.name }}
</span>
</button>
</td>
<td class="duration-cell">
@ -344,15 +345,16 @@
</section>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex'
import $ from 'jquery'
import { mapState, mapGetters, mapActions, useStore } from 'vuex'
import { nextTick, onMounted, ref, computed } from 'vue'
import moment from 'moment'
import { sum } from 'lodash-es'
import time from '~/utils/time'
import { createFocusTrap } from 'focus-trap'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
import draggable from 'vuedraggable'
import { useTimeoutFn, useWindowScroll, useWindowSize } from '@vueuse/core'
export default {
components: {
@ -360,18 +362,48 @@ export default {
TrackPlaylistIcon,
draggable
},
setup () {
const queueModal = ref()
const { activate } = useFocusTrap(queueModal, { allowOutsideClick: true })
activate()
const store = useStore()
const queue = store.state.queue
const currentIndex = computed(() => store.state.queue.currentIndex)
const { y: pageYOffset } = useWindowScroll()
const { height: windowHeight } = useWindowSize()
const scrollToCurrent = async () => {
await nextTick()
const item = queueModal.value?.querySelector('.queue-item.active')
const { top } = item?.getBoundingClientRect() ?? { top: 0 }
window.scrollTo({
top: top + pageYOffset.value - windowHeight.value / 2,
behavior: 'smooth'
})
}
onMounted(async () => {
await nextTick()
// delay is to let transition work
useTimeoutFn(scrollToCurrent, 400)
})
// TODO (wvffle): Add useVirtualList to speed up the queue rendering and potentially resolve #1471
// Each item has 49px height on desktop and 50.666px on tablet(?) and down
return { queueModal, scrollToCurrent, queue, currentIndex }
},
data () {
return {
showVolume: false,
isShuffling: false,
tracksChangeBuffer: null,
focusTrap: null,
time
}
},
computed: {
...mapState({
currentIndex: state => state.queue.currentIndex,
playing: state => state.player.playing,
isLoadingAudio: state => state.player.isLoadingAudio,
volume: state => state.player.volume,
@ -379,8 +411,7 @@ export default {
duration: state => state.player.duration,
bufferProgress: state => state.player.bufferProgress,
errored: state => state.player.errored,
currentTime: state => state.player.currentTime,
queue: state => state.queue
currentTime: state => state.player.currentTime
}),
...mapGetters({
currentTrack: 'queue/currentTrack',
@ -459,16 +490,6 @@ export default {
this.$store.commit('ui/queueFocused', null)
}
},
mounted () {
this.focusTrap = createFocusTrap(this.$el, { allowOutsideClick: () => { return true } })
this.focusTrap.activate()
this.$nextTick(() => {
setTimeout(() => {
this.scrollToCurrent()
// delay is to let transition work
}, 400)
})
},
methods: {
...mapActions({
cleanTrack: 'queue/cleanTrack',
@ -486,16 +507,6 @@ export default {
newIndex: event.newIndex
})
},
scrollToCurrent () {
const current = $(this.$el).find('.queue-item.active')[0]
if (!current) {
return
}
const elementRect = current.getBoundingClientRect()
const absoluteElementTop = elementRect.top + window.pageYOffset
const middle = absoluteElementTop - (window.innerHeight / 2)
window.scrollTo({ top: middle, behaviour: 'smooth' })
},
touchProgress (e) {
const target = this.$refs.progress
const time = (e.layerX / target.offsetWidth) * this.duration

View File

@ -1,5 +1,85 @@
<script setup lang="ts">
import $ from 'jquery'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import { computed, onBeforeUnmount, ref, watchEffect } from 'vue'
import { useVModel } from '@vueuse/core'
import { useStore } from 'vuex'
interface Props {
show: boolean
fullscreen?: boolean
scrolling?: boolean
additionalClasses?: string[]
}
const props = withDefaults(
defineProps<Props>(),
{
fullscreen: true,
scrolling: false,
additionalClasses: () => []
}
)
const emit = defineEmits(['approved', 'deny', 'update:show', 'show', 'hide'])
const modal = ref()
const { activate, deactivate, pause, unpause } = useFocusTrap(modal)
const show = useVModel(props, 'show', emit)
const control = ref()
const initModal = () => {
control.value = $(modal.value).modal({
duration: 100,
onApprove: () => emit('approved'),
onDeny: () => emit('deny'),
onHidden: () => (show.value = false)
})
}
watchEffect(() => {
if (show.value) {
initModal()
emit('show')
control.value?.modal('show')
activate()
unpause()
document.body.classList.add('scrolling')
return
}
if (control.value) {
emit('hide')
control.value.modal('hide')
control.value.remove()
deactivate()
pause()
document.body.classList.remove('scrolling')
}
})
onBeforeUnmount(() => {
control.value?.modal('hide')
})
const store = useStore()
const classes = computed(() => [
...props.additionalClasses,
'ui', 'modal',
{
active: show.value,
scrolling: props.scrolling,
'overlay fullscreen': props.fullscreen && ['phone', 'tablet'].includes(store.getters['ui/windowSize'])
}
])
</script>
<template>
<div :class="additionalClasses.concat(['ui', {'active': show}, {'scrolling': scrolling} ,{'overlay fullscreen': fullscreen && ['phone', 'tablet'].indexOf($store.getters['ui/windowSize']) > -1},'modal'])">
<div
ref="modal"
:class="classes"
>
<i
tabindex="0"
class="close inside icon"
@ -7,83 +87,3 @@
<slot v-if="show" />
</div>
</template>
<script>
import $ from 'jquery'
import { createFocusTrap } from 'focus-trap'
export default {
props: {
show: { type: Boolean, required: true },
fullscreen: { type: Boolean, default: true },
scrolling: { type: Boolean, required: false, default: false },
additionalClasses: { type: Array, required: false, default: () => [] }
},
data () {
return {
control: null,
focusTrap: null
}
},
watch: {
show: {
handler (newValue) {
if (newValue) {
this.initModal()
this.$emit('show')
this.control.modal('show')
this.focusTrap.activate()
this.focusTrap.unpause()
document.body.classList.add('scrolling')
} else {
if (this.control) {
this.$emit('hide')
this.control.modal('hide')
this.control.remove()
this.focusTrap.deactivate()
this.focusTrap.pause()
document.body.classList.remove('scrolling')
}
}
}
},
$route (to, from) {
this.closeModal()
}
},
mounted () {
this.focusTrap = createFocusTrap(this.$el)
},
beforeUnmount () {
if (this.control) {
$(this.$el).modal('hide')
}
this.focusTrap.deactivate()
$(this.$el).remove()
},
methods: {
initModal () {
this.control = $(this.$el).modal({
duration: 100,
onApprove: function () {
this.$emit('approved')
}.bind(this),
onDeny: function () {
this.$emit('deny')
}.bind(this),
onHidden: function () {
this.$emit('update:show', false)
}.bind(this),
onVisible: function () {
this.focusTrap.activate()
this.focusTrap.unpause()
}.bind(this)
})
},
closeModal () {
this.$emit('update:show', false)
}
}
}
</script>