feat(10-11): add click-after-drag guard + teleport folder picker to StorageBrowser
- Guard file-row click: draggingFile ? null : emit('file-open', file)
- Add onFileDragEnd() with nextTick reset to prevent click-after-drag navigation
- Update onDropDocOnFolder to defer draggingFile reset via await nextTick()
- Replace absolute folder picker with Teleport to body + getBoundingClientRect positioning
- Add scroll/resize listeners to reposition picker on window scroll
- All 6 UX-11 dragmove tests now pass
This commit is contained in:
@@ -141,9 +141,9 @@
|
|||||||
:draggable="mode === 'local'"
|
:draggable="mode === 'local'"
|
||||||
class="px-4 py-2.5 grid grid-cols-[2rem_1fr_6rem_8rem_6rem] gap-3 items-center hover:bg-gray-50 group cursor-pointer transition-colors select-none"
|
class="px-4 py-2.5 grid grid-cols-[2rem_1fr_6rem_8rem_6rem] gap-3 items-center hover:bg-gray-50 group cursor-pointer transition-colors select-none"
|
||||||
:class="{ 'opacity-50': draggingFile?.id === file.id }"
|
:class="{ 'opacity-50': draggingFile?.id === file.id }"
|
||||||
@click="$emit('file-open', file)"
|
@click="draggingFile ? null : $emit('file-open', file)"
|
||||||
@dragstart="mode === 'local' ? onFileDragStart(file, $event) : null"
|
@dragstart="mode === 'local' ? onFileDragStart(file, $event) : null"
|
||||||
@dragend="draggingFile = null; dragOverFolderId = null"
|
@dragend="onFileDragEnd"
|
||||||
>
|
>
|
||||||
<div class="w-7 h-7 bg-indigo-50 rounded-lg flex items-center justify-center shrink-0">
|
<div class="w-7 h-7 bg-indigo-50 rounded-lg flex items-center justify-center shrink-0">
|
||||||
<svg class="w-4 h-4 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
<!-- Move -->
|
<!-- Move -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button
|
<button
|
||||||
@click.stop="folderPickerFileId = folderPickerFileId === file.id ? null : file.id"
|
@click.stop="openFolderPicker(file.id, $event)"
|
||||||
title="Move to folder"
|
title="Move to folder"
|
||||||
class="p-1.5 rounded hover:bg-gray-200 text-gray-400 hover:text-gray-700 transition-colors"
|
class="p-1.5 rounded hover:bg-gray-200 text-gray-400 hover:text-gray-700 transition-colors"
|
||||||
>
|
>
|
||||||
@@ -194,23 +194,6 @@
|
|||||||
d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
|
d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div
|
|
||||||
v-if="folderPickerFileId === file.id"
|
|
||||||
class="absolute right-0 top-full mt-1 w-48 bg-white border border-gray-200 rounded-xl shadow-lg z-20 py-1"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<button class="w-full text-left px-3 py-2 text-sm text-gray-600 hover:bg-gray-50"
|
|
||||||
@click.stop="$emit('file-move', { fileId: file.id, folderId: null }); folderPickerFileId = null">
|
|
||||||
Root (no folder)
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-for="f in rootFolders"
|
|
||||||
:key="f.id"
|
|
||||||
class="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-700 truncate"
|
|
||||||
@click.stop="$emit('file-move', { fileId: file.id, folderId: f.id }); folderPickerFileId = null"
|
|
||||||
>{{ f.name }}</button>
|
|
||||||
<p v-if="!rootFolders.length" class="px-3 py-2 text-xs text-gray-400">No folders yet</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Delete -->
|
<!-- Delete -->
|
||||||
<button @click.stop="$emit('file-delete', file.id)" title="Delete"
|
<button @click.stop="$emit('file-delete', file.id)" title="Delete"
|
||||||
@@ -267,6 +250,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Teleported folder picker — avoids overflow:hidden clipping -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="folderPickerFileId"
|
||||||
|
:style="pickerStyle"
|
||||||
|
data-test="folder-picker"
|
||||||
|
class="fixed z-[9999] bg-white border border-gray-200 rounded-xl shadow-lg overflow-y-auto py-1"
|
||||||
|
style="max-height: 240px;"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full text-left px-3 py-2 text-sm text-gray-600 hover:bg-gray-50"
|
||||||
|
@click.stop="$emit('file-move', { fileId: folderPickerFileId, folderId: null }); folderPickerFileId = null"
|
||||||
|
>
|
||||||
|
Root (no folder)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-for="f in rootFolders"
|
||||||
|
:key="f.id"
|
||||||
|
class="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-700 truncate"
|
||||||
|
@click.stop="$emit('file-move', { fileId: folderPickerFileId, folderId: f.id }); folderPickerFileId = null"
|
||||||
|
>{{ f.name }}</button>
|
||||||
|
<p v-if="!rootFolders.length" class="px-3 py-2 text-xs text-gray-400">No folders yet</p>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -385,22 +394,80 @@ function onFolderDragOver(folderId, e) {
|
|||||||
dragOverFolderId.value = folderId
|
dragOverFolderId.value = folderId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onFileDragEnd() {
|
||||||
|
dragOverFolderId.value = null
|
||||||
|
nextTick(() => { draggingFile.value = null })
|
||||||
|
}
|
||||||
|
|
||||||
async function onDropDocOnFolder(folderId) {
|
async function onDropDocOnFolder(folderId) {
|
||||||
if (!draggingFile.value) return
|
if (!draggingFile.value) return
|
||||||
const fileId = draggingFile.value.id
|
const fileId = draggingFile.value.id
|
||||||
draggingFile.value = null
|
|
||||||
dragOverFolderId.value = null
|
dragOverFolderId.value = null
|
||||||
emit('file-move', { fileId, folderId })
|
emit('file-move', { fileId, folderId })
|
||||||
|
await nextTick()
|
||||||
|
draggingFile.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Move folder picker ────────────────────────────────────────────────────────
|
// ── Move folder picker (Teleport + getBoundingClientRect) ─────────────────────
|
||||||
|
|
||||||
const folderPickerFileId = ref(null)
|
const folderPickerFileId = ref(null)
|
||||||
|
const pickerStyle = ref({})
|
||||||
|
// Map fileId → trigger button element, so scroll listener can reposition
|
||||||
|
const pickerTriggerMap = new Map()
|
||||||
|
|
||||||
function onOutsideClick(e) {
|
function updatePickerPosition(rect) {
|
||||||
if (!e.target.closest('.relative')) folderPickerFileId.value = null
|
const spaceBelow = window.innerHeight - rect.bottom
|
||||||
|
const dropH = 240
|
||||||
|
if (spaceBelow >= dropH || spaceBelow > 120) {
|
||||||
|
pickerStyle.value = {
|
||||||
|
top: `${rect.bottom + 4}px`,
|
||||||
|
left: `${rect.left}px`,
|
||||||
|
width: '192px',
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pickerStyle.value = {
|
||||||
|
bottom: `${window.innerHeight - rect.top + 4}px`,
|
||||||
|
left: `${rect.left}px`,
|
||||||
|
width: '192px',
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => document.addEventListener('click', onOutsideClick))
|
function openFolderPicker(fileId, ev) {
|
||||||
onUnmounted(() => document.removeEventListener('click', onOutsideClick))
|
// Toggle off if same file
|
||||||
|
if (folderPickerFileId.value === fileId) {
|
||||||
|
folderPickerFileId.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
folderPickerFileId.value = fileId
|
||||||
|
const trigger = ev.currentTarget
|
||||||
|
pickerTriggerMap.set(fileId, trigger)
|
||||||
|
updatePickerPosition(trigger.getBoundingClientRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWindowScroll() {
|
||||||
|
if (!folderPickerFileId.value) return
|
||||||
|
const trig = pickerTriggerMap.get(folderPickerFileId.value)
|
||||||
|
if (trig) updatePickerPosition(trig.getBoundingClientRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOutsideClick(e) {
|
||||||
|
if (
|
||||||
|
!e.target.closest('[data-test="folder-picker"]') &&
|
||||||
|
!e.target.closest('.relative')
|
||||||
|
) {
|
||||||
|
folderPickerFileId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', onOutsideClick)
|
||||||
|
window.addEventListener('scroll', onWindowScroll, true)
|
||||||
|
window.addEventListener('resize', onWindowScroll)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', onOutsideClick)
|
||||||
|
window.removeEventListener('scroll', onWindowScroll, true)
|
||||||
|
window.removeEventListener('resize', onWindowScroll)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user