fix(nextcloud-files): IN-07 Ablegen auf offene Dialoge und Menues wird ignoriert

- bei offenem Dialog oder Menue (oder einem Ziel darin) zeigt die Ansicht keine
  Ablage-Markierung und laedt nichts hoch; das Ereignis wird trotzdem abgefangen, damit der
  Browser die Datei nicht selbst oeffnet
- ein bereits behandeltes Ablegen (defaultPrevented) wird nicht hochgeladen

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-08 22:51:53 +02:00
parent f0f3718b37
commit 49eebde215
2 changed files with 50 additions and 3 deletions
@@ -1,6 +1,7 @@
import { import {
act, act,
cleanup, cleanup,
createEvent,
fireEvent, fireEvent,
render as rtlRender, render as rtlRender,
screen, screen,
@@ -549,6 +550,35 @@ describe('FileBrowser — Ziehen und Ablegen', () => {
expect(screen.queryByTestId('nc-files-drop-overlay')).toBeNull(); expect(screen.queryByTestId('nc-files-drop-overlay')).toBeNull();
}); });
it('Ablegen bei offenem Dialog wird ignoriert, ohne dass der Browser die Datei oeffnet (IN-07)', async () => {
await mount();
fireEvent.click(screen.getByRole('button', { name: 'Neuer Ordner' }));
const input = await screen.findByLabelText('Name des Ordners');
const dt = files([new File(['x'], 'neu.txt')]);
fireEvent.dragEnter(input, { dataTransfer: dt });
expect(screen.queryByTestId('nc-files-drop-overlay')).toBeNull();
const over = createEvent.dragOver(input, { dataTransfer: dt });
fireEvent(input, over);
expect(over.defaultPrevented).toBe(true);
const drop = createEvent.drop(input, { dataTransfer: dt });
fireEvent(input, drop);
expect(drop.defaultPrevented).toBe(true);
await act(async () => {});
expect(mockUpload).not.toHaveBeenCalled();
expect(screen.queryByTestId('nc-files-drop-overlay')).toBeNull();
});
it('ein schon behandeltes Ablegen (defaultPrevented) wird nicht hochgeladen', async () => {
await mount();
const dt = files([new File(['x'], 'neu.txt')]);
const row = screen.getByRole('row', { name: 'Projekte' });
const drop = createEvent.drop(row, { dataTransfer: dt });
drop.preventDefault();
fireEvent(row, drop);
await act(async () => {});
expect(mockUpload).not.toHaveBeenCalled();
});
it('ein Ziehen ohne Dateien (markierter Text) wird ignoriert', async () => { it('ein Ziehen ohne Dateien (markierter Text) wird ignoriert', async () => {
await mount(); await mount();
const dt = { types: ['text/plain'], files: [], items: [] }; const dt = { types: ['text/plain'], files: [], items: [] };
@@ -417,9 +417,18 @@ export function FileBrowser({
return () => window.removeEventListener('beforeunload', warn); return () => window.removeEventListener('beforeunload', warn);
}, [anyActive]); }, [anyActive]);
// Ziehen und Ablegen: am Fenster, mit Tiefenzaehler; nur Ziehen mit Dateien zaehlt. // Ziehen und Ablegen: am Fenster, mit Tiefenzaehler; nur Ziehen mit Dateien zaehlt. Ist ein
// Dialog oder Menue offen (oder liegt das Ziel darin), wird nichts abgelegt (IN-07): der
// Browser darf die Datei dann aber auch nicht selbst oeffnen.
const dialogOpenRef = useRef(false);
dialogOpenRef.current = dialog !== null || menu !== null;
useEffect(() => { useEffect(() => {
let depth = 0; let depth = 0;
const blocked = (e: DragEvent): boolean => {
if (dialogOpenRef.current) return true;
const el = e.target instanceof Element ? e.target : null;
return !!el?.closest('[role="dialog"], [role="alertdialog"], [role="menu"]');
};
const targetOf = (e: DragEvent): string => { const targetOf = (e: DragEvent): string => {
const el = e.target instanceof Element ? e.target.closest('[data-drop-folder]') : null; const el = e.target instanceof Element ? e.target.closest('[data-drop-folder]') : null;
return el?.getAttribute('data-drop-folder') ?? pathRef.current; return el?.getAttribute('data-drop-folder') ?? pathRef.current;
@@ -427,11 +436,16 @@ export function FileBrowser({
const onEnter = (e: DragEvent) => { const onEnter = (e: DragEvent) => {
if (!hasFiles(e.dataTransfer)) return; if (!hasFiles(e.dataTransfer)) return;
depth += 1; depth += 1;
setDropTarget(targetOf(e)); setDropTarget(blocked(e) ? null : targetOf(e));
}; };
const onOver = (e: DragEvent) => { const onOver = (e: DragEvent) => {
if (!hasFiles(e.dataTransfer)) return; if (!hasFiles(e.dataTransfer)) return;
e.preventDefault(); e.preventDefault();
if (blocked(e)) {
if (e.dataTransfer) e.dataTransfer.dropEffect = 'none';
setDropTarget(null);
return;
}
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
const target = targetOf(e); const target = targetOf(e);
setDropTarget((prev) => (prev === target ? prev : target)); setDropTarget((prev) => (prev === target ? prev : target));
@@ -443,10 +457,13 @@ export function FileBrowser({
}; };
const onDrop = (e: DragEvent) => { const onDrop = (e: DragEvent) => {
if (!hasFiles(e.dataTransfer) || !e.dataTransfer) return; if (!hasFiles(e.dataTransfer) || !e.dataTransfer) return;
// Ein Bestandteil der Seite hat das Ablegen schon selbst behandelt.
const handled = e.defaultPrevented;
e.preventDefault(); e.preventDefault();
depth = 0; depth = 0;
const target = targetOf(e);
setDropTarget(null); setDropTarget(null);
if (handled || blocked(e)) return;
const target = targetOf(e);
const snapshot = snapshotDrop(e.dataTransfer); const snapshot = snapshotDrop(e.dataTransfer);
void collectDrop(snapshot).then((result) => { void collectDrop(snapshot).then((result) => {
if (result.problem) { if (result.problem) {