fix(explorer): 非文件拖拽不再触发文件树上传遮罩 (#317)
拖动 Tab(重排/跨 pane split)经过资源管理器文件树时,FileTree 的 dragenter/dragover/drop 此前对任何拖拽都响应:显示上传遮罩并吞掉 事件,导致 pane 根的 Tab-drop 处理收不到、Tab 无法落下。 统一按 dataTransfer.types 含 'Files' 门控(与 Sidebar.tsx 面板宿主 shield 一致):非文件拖拽原样冒泡给 pane 根,OS 文件拖拽行为不变。 回归测试:应用内 Tab 拖拽不出现 uploadDropZone、不改 dropTarget、 不 preventDefault、不触发 onUploadRequest。
This commit is contained in:
@@ -52,6 +52,13 @@ function parentOf(path: string): string {
|
||||
return at <= 0 ? path : path.slice(0, at)
|
||||
}
|
||||
|
||||
/** Only OS file drags belong to the upload surface; in-app drags (tab reorder,
|
||||
* split zones) must pass through untouched to the pane's tab-drop handling
|
||||
* (mirror of Sidebar.tsx's panel-host shield gate). */
|
||||
function isFileDrag(event: DragEvent): boolean {
|
||||
return event.dataTransfer?.types.includes('Files') ?? false
|
||||
}
|
||||
|
||||
/** How long the row's "copied" label stays after a successful write. */
|
||||
const COPIED_MS = 1200
|
||||
|
||||
@@ -160,12 +167,14 @@ export function FileTree(props: {
|
||||
})
|
||||
}
|
||||
const handleBodyDrop = (event: DragEvent): void => {
|
||||
if (!isFileDrag(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
resetDrop()
|
||||
if (cwd !== undefined) reportDrop(cwd, event.dataTransfer)
|
||||
}
|
||||
const handleDirDrop = (event: DragEvent, dir: string): void => {
|
||||
if (!isFileDrag(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
resetDrop()
|
||||
@@ -176,6 +185,7 @@ export function FileTree(props: {
|
||||
handleDirDrop(event, parentOf(path))
|
||||
}
|
||||
const handleBodyDragEnter = (event: DragEvent): void => {
|
||||
if (!isFileDrag(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dropDepth.current += 1
|
||||
@@ -195,6 +205,7 @@ export function FileTree(props: {
|
||||
setDropRect(null)
|
||||
}
|
||||
const handleBodyDragOver = (event: DragEvent): void => {
|
||||
if (!isFileDrag(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.dataTransfer.dropEffect = busy ? 'none' : 'copy'
|
||||
@@ -205,6 +216,7 @@ export function FileTree(props: {
|
||||
setDropTarget(null)
|
||||
}
|
||||
const handleRowDragOver = (event: DragEvent, dir: string): void => {
|
||||
if (!isFileDrag(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.dataTransfer.dropEffect = busy ? 'none' : 'copy'
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createElement } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { FileTree } from '../src/client/FileTree.tsx'
|
||||
import { TAB_DRAG_TYPE } from '../src/client/TabBar.tsx'
|
||||
import type { UploadItem } from '../src/client/upload.ts'
|
||||
|
||||
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||
@@ -72,12 +73,12 @@ async function mountTree(busy = false): Promise<Harness> {
|
||||
}
|
||||
|
||||
/** A bubbling drag event carrying a stub dataTransfer (jsdom has no DragEvent). */
|
||||
function dragEvent(type: string): Event {
|
||||
function dragEvent(type: string, dataTypes: string[] = ['Files']): Event {
|
||||
const event = new Event(type, { bubbles: true, cancelable: true })
|
||||
Object.defineProperty(event, 'dataTransfer', {
|
||||
// No entry API on the items, so drops fall back to the flat file list.
|
||||
value: {
|
||||
types: ['Files'],
|
||||
types: dataTypes,
|
||||
items: [],
|
||||
files: [new File(['x'], 'dropped.txt')],
|
||||
dropEffect: '',
|
||||
@@ -87,8 +88,8 @@ function dragEvent(type: string): Event {
|
||||
}
|
||||
|
||||
/** Dispatch inside act() so React flushes the state update. */
|
||||
function fire(target: Element, type: string): Event {
|
||||
const event = dragEvent(type)
|
||||
function fire(target: Element, type: string, dataTypes?: string[]): Event {
|
||||
const event = dragEvent(type, dataTypes)
|
||||
act(() => { target.dispatchEvent(event) })
|
||||
return event
|
||||
}
|
||||
@@ -179,4 +180,26 @@ describe('FileTree drag-drop surface', () => {
|
||||
expect(busyHarness.uploads).toHaveLength(0)
|
||||
busyHarness.unmount()
|
||||
})
|
||||
|
||||
it('ignores an in-app tab drag: no upload drop zone and events pass through', () => {
|
||||
const enter = fire(harness.body, 'dragenter', [TAB_DRAG_TYPE])
|
||||
expect(enter.defaultPrevented).toBe(false)
|
||||
expect(dropZone()).toBeNull()
|
||||
const over = fire(harness.body, 'dragover', [TAB_DRAG_TYPE])
|
||||
expect(over.defaultPrevented).toBe(false)
|
||||
expect(dropZone()).toBeNull()
|
||||
// A row hover must not retarget the drop directory either.
|
||||
const dir = rowByName(harness.container, 'src')
|
||||
const rowOver = fire(dir, 'dragover', [TAB_DRAG_TYPE])
|
||||
expect(rowOver.defaultPrevented).toBe(false)
|
||||
expect(dir.className).not.toContain('explorerRowDropTarget')
|
||||
})
|
||||
|
||||
it('passes an in-app tab drop through without reporting an upload', async () => {
|
||||
const drop = fire(harness.body, 'drop', [TAB_DRAG_TYPE])
|
||||
await act(async () => {})
|
||||
expect(drop.defaultPrevented).toBe(false)
|
||||
expect(harness.uploads).toHaveLength(0)
|
||||
expect(dropZone()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user