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

View File

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

View File

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

View File

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