Add useFocusTrap
This commit is contained in:
parent
7e53e9a511
commit
cbaa58d215
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
|
ref="queueModal"
|
||||||
class="main with-background component-queue"
|
class="main with-background component-queue"
|
||||||
:aria-label="labels.queue"
|
:aria-label="labels.queue"
|
||||||
>
|
>
|
||||||
|
@ -221,7 +222,7 @@
|
||||||
<div>
|
<div>
|
||||||
<translate
|
<translate
|
||||||
translate-context="Sidebar/Queue/Text"
|
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 }
|
Track %{ index } of %{ length }
|
||||||
</translate><template v-if="!$store.state.radios.running">
|
</translate><template v-if="!$store.state.radios.running">
|
||||||
|
@ -240,13 +241,13 @@
|
||||||
v-model:list="tracks"
|
v-model:list="tracks"
|
||||||
tag="tbody"
|
tag="tbody"
|
||||||
handle=".handle"
|
handle=".handle"
|
||||||
@update="reorder"
|
|
||||||
item-key="id"
|
item-key="id"
|
||||||
|
@update="reorder"
|
||||||
>
|
>
|
||||||
<template #item="{ element: track, index }">
|
<template #item="{ element: track, index }">
|
||||||
<tr
|
<tr
|
||||||
:key="track.id"
|
:key="track.id"
|
||||||
:class="['queue-item', {'active': index === queue.currentIndex}]"
|
:class="['queue-item', {'active': index === currentIndex}]"
|
||||||
>
|
>
|
||||||
<td class="handle">
|
<td class="handle">
|
||||||
<i class="grip lines icon" />
|
<i class="grip lines icon" />
|
||||||
|
@ -344,15 +345,16 @@
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters, mapActions } from 'vuex'
|
import { mapState, mapGetters, mapActions, useStore } from 'vuex'
|
||||||
import $ from 'jquery'
|
import { nextTick, onMounted, ref, computed } from 'vue'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { sum } from 'lodash-es'
|
import { sum } from 'lodash-es'
|
||||||
import time from '~/utils/time'
|
import time from '~/utils/time'
|
||||||
import { createFocusTrap } from 'focus-trap'
|
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
||||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||||
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
|
import { useTimeoutFn, useWindowScroll, useWindowSize } from '@vueuse/core'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -360,18 +362,48 @@ export default {
|
||||||
TrackPlaylistIcon,
|
TrackPlaylistIcon,
|
||||||
draggable
|
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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
showVolume: false,
|
showVolume: false,
|
||||||
isShuffling: false,
|
isShuffling: false,
|
||||||
tracksChangeBuffer: null,
|
tracksChangeBuffer: null,
|
||||||
focusTrap: null,
|
|
||||||
time
|
time
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
currentIndex: state => state.queue.currentIndex,
|
|
||||||
playing: state => state.player.playing,
|
playing: state => state.player.playing,
|
||||||
isLoadingAudio: state => state.player.isLoadingAudio,
|
isLoadingAudio: state => state.player.isLoadingAudio,
|
||||||
volume: state => state.player.volume,
|
volume: state => state.player.volume,
|
||||||
|
@ -379,8 +411,7 @@ export default {
|
||||||
duration: state => state.player.duration,
|
duration: state => state.player.duration,
|
||||||
bufferProgress: state => state.player.bufferProgress,
|
bufferProgress: state => state.player.bufferProgress,
|
||||||
errored: state => state.player.errored,
|
errored: state => state.player.errored,
|
||||||
currentTime: state => state.player.currentTime,
|
currentTime: state => state.player.currentTime
|
||||||
queue: state => state.queue
|
|
||||||
}),
|
}),
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentTrack: 'queue/currentTrack',
|
currentTrack: 'queue/currentTrack',
|
||||||
|
@ -459,16 +490,6 @@ export default {
|
||||||
this.$store.commit('ui/queueFocused', null)
|
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: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
cleanTrack: 'queue/cleanTrack',
|
cleanTrack: 'queue/cleanTrack',
|
||||||
|
@ -486,16 +507,6 @@ export default {
|
||||||
newIndex: event.newIndex
|
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) {
|
touchProgress (e) {
|
||||||
const target = this.$refs.progress
|
const target = this.$refs.progress
|
||||||
const time = (e.layerX / target.offsetWidth) * this.duration
|
const time = (e.layerX / target.offsetWidth) * this.duration
|
||||||
|
|
|
@ -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>
|
<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
|
<i
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="close inside icon"
|
class="close inside icon"
|
||||||
|
@ -7,83 +87,3 @@
|
||||||
<slot v-if="show" />
|
<slot v-if="show" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
|
||||||
|
|
Loading…
Reference in New Issue