feat(10-10): implement OsDragOverlay.vue — OS file drag detection with depth counter
- Options API component; emits files-dropped with file list
- Depth-counter pattern (D-16) prevents flicker across child element boundaries
- Guards against in-app drags via dataTransfer.types.includes('Files') check
- window addEventListener/removeEventListener in mounted/beforeUnmount
- Teleport to body; z-[9998] keeps overlay below ToastContainer z-[9999]
- Fade transition via scoped CSS (.fade-enter-active/.fade-leave-active)
- 8/8 tests GREEN
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="showOverlay"
|
||||
class="fixed inset-0 z-[9998] bg-indigo-900/40 flex items-center justify-center pointer-events-none"
|
||||
data-test="os-drag-overlay"
|
||||
>
|
||||
<div class="bg-white rounded-2xl px-10 py-8 text-center shadow-xl pointer-events-none">
|
||||
<AppIcon name="upload" class="w-10 h-10 text-indigo-400 mx-auto mb-3" />
|
||||
<p class="text-base font-semibold text-gray-800">Drop files to upload</p>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppIcon from '../ui/AppIcon.vue'
|
||||
|
||||
export default {
|
||||
name: 'OsDragOverlay',
|
||||
components: { AppIcon },
|
||||
emits: ['files-dropped'],
|
||||
data() {
|
||||
return {
|
||||
dragDepth: 0,
|
||||
showOverlay: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onDragEnter(e) {
|
||||
if (!e.dataTransfer?.types.includes('Files')) return
|
||||
this.dragDepth++
|
||||
this.showOverlay = true
|
||||
},
|
||||
onDragLeave() {
|
||||
this.dragDepth = Math.max(0, this.dragDepth - 1)
|
||||
if (this.dragDepth === 0) this.showOverlay = false
|
||||
},
|
||||
onDragOver(e) {
|
||||
e.preventDefault()
|
||||
},
|
||||
onDrop(e) {
|
||||
e.preventDefault()
|
||||
this.dragDepth = 0
|
||||
this.showOverlay = false
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
if (files.length) this.$emit('files-dropped', files)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('dragenter', this.onDragEnter)
|
||||
window.addEventListener('dragleave', this.onDragLeave)
|
||||
window.addEventListener('dragover', this.onDragOver)
|
||||
window.addEventListener('drop', this.onDrop)
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('dragenter', this.onDragEnter)
|
||||
window.removeEventListener('dragleave', this.onDragLeave)
|
||||
window.removeEventListener('dragover', this.onDragOver)
|
||||
window.removeEventListener('drop', this.onDrop)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user