Add useFocusTrap
This commit is contained in:
parent
7e53e9a511
commit
cbaa58d215
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue