Skip to content

Commit 803fbee

Browse files
authored
feat(core): add new preventDragFromKeys grid option, fixes #1537 (#1538)
* feat(core): add `preventDragFromKeys` grid option, fixes #1537 - the issue was brought in discussion #1537, the root cause was the mouse drag sometime kicks in when the user selects a few row by using the Ctrl+click combo, however in rare occasion the user might move by even a single pixel and that sends an onDrag event which the SlickCellRangeSelector picks up when then sends a new event `onCellRangeSelecting` and then the SlickRowSelectionModel assumes it's a new range from a mouse drag and override the previous range. However we should really prevent this mouse drag from happening when the user is pressing the Ctrl/Meta keys to avoid having the issue brought in #1537
1 parent 2a4ff8f commit 803fbee

File tree

5 files changed

+113
-40
lines changed

5 files changed

+113
-40
lines changed

packages/common/src/core/__tests__/slickInteractions.spec.ts

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('Draggable class', () => {
3030

3131
dg = Draggable({ containerElement, allowDragFrom: 'div.slick-cell', onDrag: dragInitSpy });
3232

33-
containerElement.dispatchEvent(new Event('mousedown'));
33+
containerElement.dispatchEvent(new MouseEvent('mousedown'));
3434

3535
expect(dg).toBeTruthy();
3636
expect(dragInitSpy).not.toHaveBeenCalled();
@@ -43,7 +43,7 @@ describe('Draggable class', () => {
4343

4444
dg = Draggable({ containerElement, allowDragFrom: 'div.slick-cell', onDrag: dragSpy, onDragInit: dragInitSpy });
4545

46-
containerElement.dispatchEvent(new Event('mousedown'));
46+
containerElement.dispatchEvent(new MouseEvent('mousedown'));
4747

4848
expect(dg).toBeTruthy();
4949
expect(dragInitSpy).toHaveBeenCalled();
@@ -52,6 +52,22 @@ describe('Draggable class', () => {
5252
dg.destroy();
5353
});
5454

55+
it('should NOT trigger dragInit event when user is pressing mousedown and mousemove + Ctrl key combo that we considered as forbidden via "preventDragFromKeys"', () => {
56+
const dragInitSpy = jest.fn();
57+
const dragSpy = jest.fn();
58+
containerElement.className = 'slick-cell';
59+
60+
dg = Draggable({ containerElement, allowDragFrom: 'div.slick-cell', preventDragFromKeys: ['ctrlKey'], onDrag: dragSpy, onDragInit: dragInitSpy });
61+
62+
containerElement.dispatchEvent(new MouseEvent('mousedown', { ctrlKey: true }));
63+
64+
expect(dg).toBeTruthy();
65+
expect(dragInitSpy).not.toHaveBeenCalled();
66+
expect(dragSpy).not.toHaveBeenCalled();
67+
68+
dg.destroy();
69+
});
70+
5571
it('should trigger mousedown and expect a dragInit and a dragStart and drag to all happen since it was triggered by an allowed element and we did move afterward', () => {
5672
const removeListenerSpy = jest.spyOn(document.body, 'removeEventListener');
5773
const dragInitSpy = jest.fn();
@@ -62,13 +78,13 @@ describe('Draggable class', () => {
6278

6379
dg = Draggable({ containerElement, allowDragFrom: 'div.slick-cell', onDrag: dragSpy, onDragInit: dragInitSpy, onDragStart: dragStartSpy, onDragEnd: dragEndSpy });
6480

65-
const mdEvt = new Event('mousedown');
81+
const mdEvt = new MouseEvent('mousedown');
6682
Object.defineProperty(mdEvt, 'clientX', { writable: true, configurable: true, value: 10 });
6783
Object.defineProperty(mdEvt, 'clientY', { writable: true, configurable: true, value: 10 });
6884
containerElement.dispatchEvent(mdEvt);
6985

70-
const mmEvt = new Event('mousemove');
71-
const muEvt = new Event('mouseup');
86+
const mmEvt = new MouseEvent('mousemove');
87+
const muEvt = new MouseEvent('mouseup');
7288
Object.defineProperty(mmEvt, 'clientX', { writable: true, configurable: true, value: 12 });
7389
Object.defineProperty(mmEvt, 'clientY', { writable: true, configurable: true, value: 10 });
7490
Object.defineProperty(muEvt, 'clientX', { writable: true, configurable: true, value: 12 });
@@ -83,6 +99,38 @@ describe('Draggable class', () => {
8399
expect(dragEndSpy).toHaveBeenCalled();
84100
expect(removeListenerSpy).toHaveBeenCalledTimes(5 * 2);
85101
});
102+
103+
it('should NOT trigger dragInit,dragStart events when user is pressing mousedown and mousemove + Meta key combo that we considered as forbidden via "preventDragFromKeys"', () => {
104+
const removeListenerSpy = jest.spyOn(document.body, 'removeEventListener');
105+
const dragInitSpy = jest.fn();
106+
const dragSpy = jest.fn();
107+
const dragStartSpy = jest.fn();
108+
const dragEndSpy = jest.fn();
109+
containerElement.className = 'slick-cell';
110+
111+
dg = Draggable({ containerElement, allowDragFrom: 'div.slick-cell', preventDragFromKeys: ['metaKey'], onDrag: dragSpy, onDragInit: dragInitSpy, onDragStart: dragStartSpy, onDragEnd: dragEndSpy });
112+
113+
const mdEvt = new MouseEvent('mousedown', { metaKey: true });
114+
Object.defineProperty(mdEvt, 'clientX', { writable: true, configurable: true, value: 10 });
115+
Object.defineProperty(mdEvt, 'clientY', { writable: true, configurable: true, value: 10 });
116+
containerElement.dispatchEvent(mdEvt);
117+
118+
const mmEvt = new MouseEvent('mousemove', { metaKey: true });
119+
const muEvt = new MouseEvent('mouseup', { metaKey: true });
120+
Object.defineProperty(mmEvt, 'clientX', { writable: true, configurable: true, value: 12 });
121+
Object.defineProperty(mmEvt, 'clientY', { writable: true, configurable: true, value: 10 });
122+
Object.defineProperty(muEvt, 'clientX', { writable: true, configurable: true, value: 12 });
123+
Object.defineProperty(muEvt, 'clientY', { writable: true, configurable: true, value: 10 });
124+
document.body.dispatchEvent(mmEvt);
125+
document.body.dispatchEvent(muEvt);
126+
127+
expect(dg).toBeTruthy();
128+
expect(dragInitSpy).not.toHaveBeenCalledWith(mdEvt, { startX: 10, startY: 10, deltaX: 2, deltaY: 0, dragHandle: containerElement, dragSource: containerElement, target: document.body });
129+
expect(dragStartSpy).not.toHaveBeenCalled();
130+
expect(dragSpy).not.toHaveBeenCalled();
131+
expect(dragEndSpy).not.toHaveBeenCalled();
132+
expect(removeListenerSpy).toHaveBeenCalledTimes(5 * 2);
133+
});
86134
});
87135

88136
describe('MouseWheel class', () => {
@@ -188,13 +236,13 @@ describe('Resizable class', () => {
188236

189237
rsz = Resizable({ resizeableElement: containerElement, resizeableHandleElement: containerElement, onResize: resizeSpy, onResizeStart: resizeStartSpy, onResizeEnd: resizeEndSpy });
190238

191-
const mdEvt = new Event('mousedown');
239+
const mdEvt = new MouseEvent('mousedown');
192240
Object.defineProperty(mdEvt, 'clientX', { writable: true, configurable: true, value: 10 });
193241
Object.defineProperty(mdEvt, 'clientY', { writable: true, configurable: true, value: 10 });
194242
containerElement.dispatchEvent(mdEvt);
195243

196-
const mmEvt = new Event('mousemove');
197-
const muEvt = new Event('mouseup');
244+
const mmEvt = new MouseEvent('mousemove');
245+
const muEvt = new MouseEvent('mouseup');
198246
Object.defineProperty(mmEvt, 'clientX', { writable: true, configurable: true, value: 12 });
199247
Object.defineProperty(mmEvt, 'clientY', { writable: true, configurable: true, value: 10 });
200248
Object.defineProperty(muEvt, 'clientX', { writable: true, configurable: true, value: 12 });

packages/common/src/core/slickGrid.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
256256
forceSyncScrolling: false,
257257
addNewRowCssClass: 'new-row',
258258
preserveCopiedSelectionOnPaste: false,
259+
preventDragFromKeys: ['ctrlKey', 'metaKey'],
259260
showCellSelection: true,
260261
viewportClass: undefined,
261262
minRowBuffer: 3,
@@ -904,6 +905,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
904905
allowDragFrom: 'div.slick-cell',
905906
// the slick cell parent must always contain `.dnd` and/or `.cell-reorder` class to be identified as draggable
906907
allowDragFromClosest: 'div.slick-cell.dnd, div.slick-cell.cell-reorder',
908+
preventDragFromKeys: this._options.preventDragFromKeys,
907909
onDragInit: this.handleDragInit.bind(this),
908910
onDragStart: this.handleDragStart.bind(this),
909911
onDrag: this.handleDrag.bind(this),

packages/common/src/core/slickInteractions.ts

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import type { DragItem, DragPosition, DraggableOption, MouseWheelOption, Resizab
2828
*/
2929
export function Draggable(options: DraggableOption) {
3030
let { containerElement } = options;
31-
const { onDragInit, onDragStart, onDrag, onDragEnd } = options;
31+
const { onDragInit, onDragStart, onDrag, onDragEnd, preventDragFromKeys } = options;
3232
let element: HTMLElement | null;
3333
let startX: number;
3434
let startY: number;
@@ -52,7 +52,7 @@ export function Draggable(options: DraggableOption) {
5252
}
5353
}
5454

55-
function executeDragCallbackWhenDefined(callback?: (e: DragEvent, dd: DragPosition) => boolean | void, evt?: MouseEvent | Touch | TouchEvent, dd?: DragItem) {
55+
function executeDragCallbackWhenDefined(callback?: (e: DragEvent, dd: DragPosition) => boolean | void, evt?: MouseEvent | Touch | TouchEvent | KeyboardEvent, dd?: DragItem) {
5656
if (typeof callback === 'function') {
5757
return callback(evt as DragEvent, dd as DragItem);
5858
}
@@ -65,45 +65,62 @@ export function Draggable(options: DraggableOption) {
6565
}
6666
}
6767

68-
function userPressed(event: MouseEvent | TouchEvent) {
68+
/** Do we want to prevent Drag events from happening (for example prevent onDrag when Ctrl key is pressed while dragging) */
69+
function preventDrag(event: MouseEvent | TouchEvent | KeyboardEvent) {
70+
let eventPrevented = false;
71+
if (preventDragFromKeys) {
72+
preventDragFromKeys.forEach(key => {
73+
if ((event as KeyboardEvent)[key]) {
74+
eventPrevented = true;
75+
}
76+
});
77+
}
78+
return eventPrevented;
79+
}
80+
81+
function userPressed(event: MouseEvent | TouchEvent | KeyboardEvent) {
6982
element = event.target as HTMLElement;
70-
const targetEvent: MouseEvent | Touch = (event as TouchEvent)?.touches?.[0] ?? event;
71-
const { target } = targetEvent;
72-
73-
if (!options.allowDragFrom || (options.allowDragFrom && (element.matches(options.allowDragFrom)) || (options.allowDragFromClosest && element.closest(options.allowDragFromClosest)))) {
74-
originaldd.dragHandle = element as HTMLElement;
75-
const winScrollPos = windowScrollPosition();
76-
startX = winScrollPos.left + targetEvent.clientX;
77-
startY = winScrollPos.top + targetEvent.clientY;
78-
deltaX = targetEvent.clientX - targetEvent.clientX;
79-
deltaY = targetEvent.clientY - targetEvent.clientY;
80-
originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target });
81-
const result = executeDragCallbackWhenDefined(onDragInit as (e: DragEvent, dd: DragPosition) => boolean | void, event, originaldd as DragItem);
82-
83-
if (result !== false) {
84-
document.body.addEventListener('mousemove', userMoved);
85-
document.body.addEventListener('touchmove', userMoved);
86-
document.body.addEventListener('mouseup', userReleased);
87-
document.body.addEventListener('touchend', userReleased);
88-
document.body.addEventListener('touchcancel', userReleased);
83+
if (!preventDrag(event)) {
84+
const targetEvent: MouseEvent | Touch = (event as TouchEvent)?.touches?.[0] ?? event;
85+
const { target } = targetEvent;
86+
87+
if (!options.allowDragFrom || (options.allowDragFrom && (element.matches(options.allowDragFrom)) || (options.allowDragFromClosest && element.closest(options.allowDragFromClosest)))) {
88+
originaldd.dragHandle = element as HTMLElement;
89+
const winScrollPos = windowScrollPosition();
90+
startX = winScrollPos.left + targetEvent.clientX;
91+
startY = winScrollPos.top + targetEvent.clientY;
92+
deltaX = targetEvent.clientX - targetEvent.clientX;
93+
deltaY = targetEvent.clientY - targetEvent.clientY;
94+
originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target });
95+
const result = executeDragCallbackWhenDefined(onDragInit as (e: DragEvent, dd: DragPosition) => boolean | void, event, originaldd as DragItem);
96+
97+
if (result !== false) {
98+
document.body.addEventListener('mousemove', userMoved);
99+
document.body.addEventListener('touchmove', userMoved);
100+
document.body.addEventListener('mouseup', userReleased);
101+
document.body.addEventListener('touchend', userReleased);
102+
document.body.addEventListener('touchcancel', userReleased);
103+
}
89104
}
90105
}
91106
}
92107

93-
function userMoved(event: MouseEvent | TouchEvent) {
108+
function userMoved(event: MouseEvent | TouchEvent | KeyboardEvent) {
94109
const targetEvent: MouseEvent | Touch = (event as TouchEvent)?.touches?.[0] ?? event;
95-
deltaX = targetEvent.clientX - startX;
96-
deltaY = targetEvent.clientY - startY;
97-
const { target } = targetEvent;
110+
if (!preventDrag(event)) {
111+
deltaX = targetEvent.clientX - startX;
112+
deltaY = targetEvent.clientY - startY;
113+
const { target } = targetEvent;
114+
115+
if (!dragStarted) {
116+
originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target });
117+
executeDragCallbackWhenDefined(onDragStart, event, originaldd as DragItem);
118+
dragStarted = true;
119+
}
98120

99-
if (!dragStarted) {
100121
originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target });
101-
executeDragCallbackWhenDefined(onDragStart, event, originaldd as DragItem);
102-
dragStarted = true;
122+
executeDragCallbackWhenDefined(onDrag, event, originaldd as DragItem);
103123
}
104-
105-
originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target });
106-
executeDragCallbackWhenDefined(onDrag, event, originaldd as DragItem);
107124
}
108125

109126
function userReleased(event: MouseEvent | TouchEvent) {

packages/common/src/interfaces/gridOption.interface.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,9 @@ export interface GridOption<C extends Column = Column> {
668668
/** Defaults to false, do we want prevent the usage of DocumentFragment by the library (might not be supported by all environments, e.g. not supported by Salesforce) */
669669
preventDocumentFragmentUsage?: boolean;
670670

671+
/** Defaults to `['ctrlKey', 'metaKey']`, list of keys that when pressed will prevent Draggable events from triggering (e.g. prevent onDrag when Ctrl key is pressed while dragging) */
672+
preventDragFromKeys?: Array<'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey'>;
673+
671674
/** Preselect certain rows by their row index ("enableCheckboxSelector" must be enabled) */
672675
preselectedRows?: number[];
673676

packages/common/src/interfaces/interactions.interface.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export interface DraggableOption {
1717
/** when defined, will allow dragging from a specific element or its closest parent by using the .closest() query selector. */
1818
allowDragFromClosest?: string;
1919

20+
/** Defaults to `['ctrlKey', 'metaKey']`, list of keys that when pressed will prevent Draggable events from triggering (e.g. prevent onDrag when Ctrl key is pressed while dragging) */
21+
preventDragFromKeys?: Array<'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey'>;
22+
2023
/** drag initialized callback */
2124
onDragInit?: (e: DragEvent, dd: DragPosition) => boolean | void;
2225

0 commit comments

Comments
 (0)