Merge pull request #999 from mathuo/fix/github-issue-995-chrome-drag-prevention

Fix Chrome drag event prevention when disableDnd=true
This commit is contained in:
mathuo 2025-08-25 22:27:21 +01:00 committed by GitHub
commit 4615f4d984
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 334 additions and 13 deletions

View File

@ -176,4 +176,120 @@ describe('abstractDragHandler', () => {
handler.dispose(); handler.dispose();
}); });
test('that disabled handler calls preventDefault on dragstart', () => {
const element = document.createElement('div');
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement, disabled?: boolean) {
super(el, disabled);
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element, true);
const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toBeCalledTimes(1);
handler.dispose();
});
test('that non-disabled handler does not call preventDefault on dragstart', () => {
const element = document.createElement('div');
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement, disabled?: boolean) {
super(el, disabled);
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element, false);
const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toHaveBeenCalledTimes(0);
handler.dispose();
});
test('that setDisabled method updates disabled state', () => {
const element = document.createElement('div');
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement, disabled?: boolean) {
super(el, disabled);
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element, false);
// Initially not disabled
let event = new Event('dragstart');
let spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toHaveBeenCalledTimes(0);
// Disable and test
handler.setDisabled(true);
event = new Event('dragstart');
spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toBeCalledTimes(1);
// Re-enable and test
handler.setDisabled(false);
event = new Event('dragstart');
spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toHaveBeenCalledTimes(0);
handler.dispose();
});
test('that disabled handler does not fire onDragStart event', () => {
const element = document.createElement('div');
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement, disabled?: boolean) {
super(el, disabled);
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element, true);
const spy = jest.fn();
handler.onDragStart(spy);
fireEvent.dragStart(element);
expect(spy).toHaveBeenCalledTimes(0);
handler.dispose();
});
}); });

View File

@ -338,5 +338,104 @@ describe('tab', () => {
cut.updateDragAndDropState(); cut.updateDragAndDropState();
expect(cut.element.draggable).toBe(true); expect(cut.element.draggable).toBe(true);
}); });
test('that dragstart is prevented when disableDnd is true', () => {
const accessor = fromPartial<DockviewComponent>({
options: { disableDnd: true }
});
const groupMock = jest.fn();
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
accessor,
new groupMock()
);
const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(cut.element, event);
expect(spy).toHaveBeenCalledTimes(1);
cut.dispose();
});
test('that dragstart is not prevented when disableDnd is false', () => {
const accessor = fromPartial<DockviewComponent>({
options: { disableDnd: false }
});
const groupMock = jest.fn();
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
accessor,
new groupMock()
);
const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(cut.element, event);
expect(spy).toHaveBeenCalledTimes(0);
cut.dispose();
});
test('that updateDragAndDropState updates drag handler disabled state', () => {
const options = { disableDnd: false };
const accessor = fromPartial<DockviewComponent>({
options
});
const groupMock = jest.fn();
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
accessor,
new groupMock()
);
// Initially not disabled
let event = new Event('dragstart');
let spy = jest.spyOn(event, 'preventDefault');
fireEvent(cut.element, event);
expect(spy).toHaveBeenCalledTimes(0);
// Simulate option change to disabled
options.disableDnd = true;
cut.updateDragAndDropState();
event = new Event('dragstart');
spy = jest.spyOn(event, 'preventDefault');
fireEvent(cut.element, event);
expect(spy).toHaveBeenCalledTimes(1);
// Change back to enabled
options.disableDnd = false;
cut.updateDragAndDropState();
event = new Event('dragstart');
spy = jest.spyOn(event, 'preventDefault');
fireEvent(cut.element, event);
expect(spy).toHaveBeenCalledTimes(0);
cut.dispose();
});
test('that onDragStart is not fired when disableDnd is true', () => {
const accessor = fromPartial<DockviewComponent>({
options: { disableDnd: true }
});
const groupMock = jest.fn();
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
accessor,
new groupMock()
);
const spy = jest.fn();
cut.onDragStart(spy);
fireEvent.dragStart(cut.element);
expect(spy).toHaveBeenCalledTimes(0);
cut.dispose();
});
}); });
}); });

View File

@ -111,5 +111,100 @@ describe('voidContainer', () => {
cut.updateDragAndDropState(); cut.updateDragAndDropState();
expect(cut.element.classList.contains('dv-draggable')).toBe(true); expect(cut.element.classList.contains('dv-draggable')).toBe(true);
}); });
test('that dragstart is prevented when disableDnd is true', () => {
const accessor = fromPartial<DockviewComponent>({
options: { disableDnd: true }
});
const group = fromPartial<DockviewGroupPanel>({
api: {
location: { type: 'grid' }
}
});
const cut = new VoidContainer(accessor, group);
const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(cut.element, event);
expect(spy).toHaveBeenCalledTimes(1);
cut.dispose();
});
test('that dragstart is not prevented when disableDnd is false', () => {
const accessor = fromPartial<DockviewComponent>({
options: { disableDnd: false }
});
const group = fromPartial<DockviewGroupPanel>({
api: {
location: { type: 'grid' }
}
});
const cut = new VoidContainer(accessor, group);
const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(cut.element, event);
expect(spy).toHaveBeenCalledTimes(0);
cut.dispose();
});
test('that updateDragAndDropState updates drag handler disabled state', () => {
const options = { disableDnd: false };
const accessor = fromPartial<DockviewComponent>({
options
});
const group = fromPartial<DockviewGroupPanel>({
api: {
location: { type: 'grid' }
}
});
const cut = new VoidContainer(accessor, group);
// Initially not disabled
let event = new Event('dragstart');
let spy = jest.spyOn(event, 'preventDefault');
fireEvent(cut.element, event);
expect(spy).toHaveBeenCalledTimes(0);
// Simulate option change to disabled
options.disableDnd = true;
cut.updateDragAndDropState();
event = new Event('dragstart');
spy = jest.spyOn(event, 'preventDefault');
fireEvent(cut.element, event);
expect(spy).toHaveBeenCalledTimes(1);
// Change back to enabled
options.disableDnd = false;
cut.updateDragAndDropState();
event = new Event('dragstart');
spy = jest.spyOn(event, 'preventDefault');
fireEvent(cut.element, event);
expect(spy).toHaveBeenCalledTimes(0);
cut.dispose();
});
test('that onDragStart is not fired when disableDnd is true', () => {
const accessor = fromPartial<DockviewComponent>({
options: { disableDnd: true }
});
const group = fromPartial<DockviewGroupPanel>({
api: {
location: { type: 'grid' }
}
});
const cut = new VoidContainer(accessor, group);
const spy = jest.fn();
cut.onDragStart(spy);
fireEvent.dragStart(cut.element);
expect(spy).toHaveBeenCalledTimes(0);
cut.dispose();
});
}); });
}); });

View File

@ -13,7 +13,7 @@ export abstract class DragHandler extends CompositeDisposable {
private readonly _onDragStart = new Emitter<DragEvent>(); private readonly _onDragStart = new Emitter<DragEvent>();
readonly onDragStart = this._onDragStart.event; readonly onDragStart = this._onDragStart.event;
constructor(protected readonly el: HTMLElement) { constructor(protected readonly el: HTMLElement, private disabled?: boolean) {
super(); super();
this.addDisposables( this.addDisposables(
@ -25,6 +25,10 @@ export abstract class DragHandler extends CompositeDisposable {
this.configure(); this.configure();
} }
public setDisabled(disabled: boolean): void {
this.disabled = disabled;
}
abstract getData(event: DragEvent): IDisposable; abstract getData(event: DragEvent): IDisposable;
protected isCancelled(_event: DragEvent): boolean { protected isCancelled(_event: DragEvent): boolean {
@ -35,7 +39,7 @@ export abstract class DragHandler extends CompositeDisposable {
this.addDisposables( this.addDisposables(
this._onDragStart, this._onDragStart,
addDisposableListener(this.el, 'dragstart', (event) => { addDisposableListener(this.el, 'dragstart', (event) => {
if (event.defaultPrevented || this.isCancelled(event)) { if (event.defaultPrevented || this.isCancelled(event) || this.disabled) {
event.preventDefault(); event.preventDefault();
return; return;
} }

View File

@ -14,9 +14,10 @@ export class GroupDragHandler extends DragHandler {
constructor( constructor(
element: HTMLElement, element: HTMLElement,
private readonly accessor: DockviewComponent, private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel private readonly group: DockviewGroupPanel,
disabled?: boolean
) { ) {
super(element); super(element, disabled);
this.addDisposables( this.addDisposables(
addDisposableListener( addDisposableListener(

View File

@ -26,9 +26,10 @@ class TabDragHandler extends DragHandler {
element: HTMLElement, element: HTMLElement,
private readonly accessor: DockviewComponent, private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel, private readonly group: DockviewGroupPanel,
private readonly panel: IDockviewPanel private readonly panel: IDockviewPanel,
disabled?: boolean
) { ) {
super(element); super(element, disabled);
} }
getData(event: DragEvent): IDisposable { getData(event: DragEvent): IDisposable {
@ -49,6 +50,7 @@ export class Tab extends CompositeDisposable {
private readonly _element: HTMLElement; private readonly _element: HTMLElement;
private readonly dropTarget: Droptarget; private readonly dropTarget: Droptarget;
private content: ITabRenderer | undefined = undefined; private content: ITabRenderer | undefined = undefined;
private readonly dragHandler: TabDragHandler;
private readonly _onPointDown = new Emitter<MouseEvent>(); private readonly _onPointDown = new Emitter<MouseEvent>();
readonly onPointerDown: Event<MouseEvent> = this._onPointDown.event; readonly onPointerDown: Event<MouseEvent> = this._onPointDown.event;
@ -79,11 +81,12 @@ export class Tab extends CompositeDisposable {
toggleClass(this.element, 'dv-inactive-tab', true); toggleClass(this.element, 'dv-inactive-tab', true);
const dragHandler = new TabDragHandler( this.dragHandler = new TabDragHandler(
this._element, this._element,
this.accessor, this.accessor,
this.group, this.group,
this.panel this.panel,
!!this.accessor.options.disableDnd
); );
this.dropTarget = new Droptarget(this._element, { this.dropTarget = new Droptarget(this._element, {
@ -115,7 +118,7 @@ export class Tab extends CompositeDisposable {
this._onPointDown, this._onPointDown,
this._onDropped, this._onDropped,
this._onDragStart, this._onDragStart,
dragHandler.onDragStart((event) => { this.dragHandler.onDragStart((event) => {
if (event.dataTransfer) { if (event.dataTransfer) {
const style = getComputedStyle(this.element); const style = getComputedStyle(this.element);
const newNode = this.element.cloneNode(true) as HTMLElement; const newNode = this.element.cloneNode(true) as HTMLElement;
@ -135,7 +138,7 @@ export class Tab extends CompositeDisposable {
} }
this._onDragStart.fire(event); this._onDragStart.fire(event);
}), }),
dragHandler, this.dragHandler,
addDisposableListener(this._element, 'pointerdown', (event) => { addDisposableListener(this._element, 'pointerdown', (event) => {
this._onPointDown.fire(event); this._onPointDown.fire(event);
}), }),
@ -161,6 +164,7 @@ export class Tab extends CompositeDisposable {
public updateDragAndDropState(): void { public updateDragAndDropState(): void {
this._element.draggable = !this.accessor.options.disableDnd; this._element.draggable = !this.accessor.options.disableDnd;
this.dragHandler.setDisabled(!!this.accessor.options.disableDnd);
} }
public dispose(): void { public dispose(): void {

View File

@ -15,6 +15,7 @@ import { toggleClass } from '../../../dom';
export class VoidContainer extends CompositeDisposable { export class VoidContainer extends CompositeDisposable {
private readonly _element: HTMLElement; private readonly _element: HTMLElement;
private readonly dropTarget: Droptarget; private readonly dropTarget: Droptarget;
private readonly handler: GroupDragHandler;
private readonly _onDrop = new Emitter<DroptargetEvent>(); private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event; readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
@ -49,7 +50,7 @@ export class VoidContainer extends CompositeDisposable {
}) })
); );
const handler = new GroupDragHandler(this._element, accessor, group); this.handler = new GroupDragHandler(this._element, accessor, group, !!this.accessor.options.disableDnd);
this.dropTarget = new Droptarget(this._element, { this.dropTarget = new Droptarget(this._element, {
acceptedTargetZones: ['center'], acceptedTargetZones: ['center'],
@ -72,8 +73,8 @@ export class VoidContainer extends CompositeDisposable {
this.onWillShowOverlay = this.dropTarget.onWillShowOverlay; this.onWillShowOverlay = this.dropTarget.onWillShowOverlay;
this.addDisposables( this.addDisposables(
handler, this.handler,
handler.onDragStart((event) => { this.handler.onDragStart((event) => {
this._onDragStart.fire(event); this._onDragStart.fire(event);
}), }),
this.dropTarget.onDrop((event) => { this.dropTarget.onDrop((event) => {
@ -86,5 +87,6 @@ export class VoidContainer extends CompositeDisposable {
updateDragAndDropState(): void { updateDragAndDropState(): void {
this._element.draggable = !this.accessor.options.disableDnd; this._element.draggable = !this.accessor.options.disableDnd;
toggleClass(this._element, 'dv-draggable', !this.accessor.options.disableDnd); toggleClass(this._element, 'dv-draggable', !this.accessor.options.disableDnd);
this.handler.setDisabled(!!this.accessor.options.disableDnd);
} }
} }