Skip to content

Commit b5a9487

Browse files
authored
DataGrid: fix interaction blocking while hovering over the grid header and dragging (T1291988) (#31489)
1 parent aa6282b commit b5a9487

File tree

9 files changed

+363
-41
lines changed

9 files changed

+363
-41
lines changed

packages/devextreme/js/__internal/grids/grid_core/adaptivity/m_adaptivity.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -996,8 +996,8 @@ const exportExtender = (
996996
const columnsResizer = (
997997
Base: ModuleType<ColumnsResizerViewController>,
998998
) => class AdaptivityColumnsResizerExtender extends Base {
999-
protected _pointCreated(point, cellsLength, columns) {
1000-
const result = super._pointCreated(point, cellsLength, columns);
999+
protected _pointCreated(point, columns, cells?: dxElementWrapper) {
1000+
const result = super._pointCreated(point, columns, cells);
10011001
const currentColumn = columns[point.columnIndex] || {};
10021002
const nextColumnIndex = this._getNextColumnIndex(point.columnIndex);
10031003
const nextColumn = columns[nextColumnIndex] || {};
@@ -1022,8 +1022,12 @@ const columnsResizer = (
10221022
const draggingHeader = (
10231023
Base: ModuleType<DraggingHeaderViewController>,
10241024
) => class AdaptivityDraggingHeaderExtender extends Base {
1025-
protected _pointCreated(point, columns, location, sourceColumn) {
1026-
const result = super._pointCreated(point, columns, location, sourceColumn);
1025+
protected _pointCreated({
1026+
point, columns, location, sourceColumn, cells,
1027+
}) {
1028+
const result = super._pointCreated({
1029+
point, columns, location, sourceColumn, cells,
1030+
});
10271031
const column = columns[point.columnIndex - 1] || {};
10281032
const hasAdaptiveHiddenWidth = column.visibleWidth === HIDDEN_COLUMNS_WIDTH;
10291033

packages/devextreme/js/__internal/grids/grid_core/column_fixing/m_column_fixing.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,6 @@ const baseFixedColumns = <T extends ModuleType<ColumnsView>>(Base: T) => class B
408408
return cellElements;
409409
}
410410

411-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
412411
public getColumnWidths(fixedTableElement?: any, rowIndex?: number) {
413412
const result = super.getColumnWidths(fixedTableElement, rowIndex);
414413
const fixedColumns = this.getFixedColumns();
@@ -1077,8 +1076,12 @@ const draggingHeader = (Base: ModuleType<DraggingHeaderViewController>) => class
10771076
return super._generatePointsByColumns(options, needToCheckPrevPoint);
10781077
}
10791078

1080-
protected _pointCreated(point, columns, location, sourceColumn) {
1081-
const result = super._pointCreated.apply(this, arguments as any);
1079+
protected _pointCreated({
1080+
point, columns, location, sourceColumn, cells,
1081+
}) {
1082+
const result = super._pointCreated({
1083+
point, columns, location, sourceColumn, cells,
1084+
});
10821085
const targetColumn = columns[point.columnIndex];
10831086
// @ts-expect-error
10841087
const $transparentColumn = this._columnHeadersView.getTransparentColumnElement();
@@ -1122,7 +1125,7 @@ const columnsResizer = (Base: ModuleType<ColumnsResizerViewController>) => class
11221125
point.index += correctIndex;
11231126
}
11241127

1125-
return that._pointCreated(point, columns.length, columns);
1128+
return that._pointCreated(point, columns);
11261129
});
11271130
}
11281131
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import {
2+
afterEach, beforeEach, describe, expect, it, jest,
3+
} from '@jest/globals';
4+
import {
5+
end as dragEventEnd,
6+
move as dragEventMove,
7+
start as dragEventStart,
8+
} from '@js/common/core/events/drag';
9+
import type { dxElementWrapper } from '@js/core/renderer';
10+
import $ from '@js/core/renderer';
11+
import type { Properties as DataGridProperties } from '@js/ui/data_grid';
12+
import DataGrid from '@js/ui/data_grid';
13+
import errors from '@js/ui/widget/ui.errors';
14+
import { DataGridModel } from '@ts/grids/data_grid/__tests__/__mock__/model/data_grid';
15+
16+
const SELECTORS = {
17+
gridContainer: '#gridContainer',
18+
};
19+
20+
const GRID_CONTAINER_ID = 'gridContainer';
21+
22+
const createDataGrid = async (
23+
options: DataGridProperties = {},
24+
): Promise<{
25+
$container: dxElementWrapper;
26+
component: DataGridModel;
27+
instance: DataGrid;
28+
}> => new Promise((resolve) => {
29+
const $container = $('<div>')
30+
.attr('id', GRID_CONTAINER_ID)
31+
.appendTo(document.body);
32+
33+
const instance = new DataGrid($container.get(0) as HTMLDivElement, options);
34+
const component = new DataGridModel($container.get(0) as HTMLElement);
35+
36+
jest.runAllTimers();
37+
38+
resolve({
39+
$container,
40+
component,
41+
instance,
42+
});
43+
});
44+
45+
const beforeTest = (): void => {
46+
jest.useFakeTimers();
47+
jest.spyOn(errors, 'log').mockImplementation(jest.fn());
48+
};
49+
50+
const afterTest = (): void => {
51+
const $container = $(SELECTORS.gridContainer);
52+
const dataGrid = ($container as any).dxDataGrid('instance') as DataGrid;
53+
54+
dataGrid.dispose();
55+
$container.remove();
56+
jest.clearAllMocks();
57+
jest.useRealTimers();
58+
};
59+
60+
describe('Performance optimization', () => {
61+
beforeEach(beforeTest);
62+
afterEach(afterTest);
63+
64+
const createGridWith200Columns = async (): Promise<{
65+
$container: dxElementWrapper;
66+
component: DataGridModel;
67+
instance: DataGrid;
68+
}> => {
69+
const columns = [
70+
{
71+
dataField: 'id', caption: 'ID', width: '100px', fixed: true,
72+
},
73+
{
74+
caption: 'Name',
75+
columns: [
76+
{ dataField: 'name.first', caption: 'First name', width: '150px' },
77+
{ dataField: 'name.last', caption: 'Last name', width: '150px' },
78+
],
79+
},
80+
...Array.from({ length: 198 }, (_, index) => ({
81+
dataField: `values.${index}`,
82+
caption: `Value ${index + 1}`,
83+
width: '100px',
84+
})),
85+
];
86+
87+
const dataSource = [
88+
{
89+
id: 1,
90+
name: { first: 'John', last: 'Doe' },
91+
values: Array.from({ length: 198 }, (_, index) => index + 1),
92+
},
93+
];
94+
95+
return createDataGrid({
96+
dataSource,
97+
columns,
98+
width: '100%',
99+
showBorders: true,
100+
showColumnLines: true,
101+
allowColumnResizing: true,
102+
allowColumnReordering: true,
103+
});
104+
};
105+
106+
describe('ColumnsResizerViewController', () => {
107+
it('should call "_pointCreated" 202 times when generating points by columns (1 fixed + 1 group + 2 group children + 198 regular)', async () => {
108+
const { instance } = await createGridWith200Columns();
109+
const columnsResizerController = (instance as any).getController('columnsResizer');
110+
111+
const pointCreatedSpy = jest.spyOn(columnsResizerController, '_pointCreated');
112+
113+
columnsResizerController.pointsByColumns();
114+
115+
expect(pointCreatedSpy).toHaveBeenCalledTimes(202);
116+
});
117+
118+
it('should call "getColumnElements" as many times as there are head rows', async () => {
119+
const { instance } = await createGridWith200Columns();
120+
const columnsResizerController = (instance as any).getController('columnsResizer');
121+
const columnHeadersView = (instance as any).getView('columnHeadersView');
122+
123+
const columnHeadersViewSpy = jest.spyOn(columnHeadersView, 'getColumnElements');
124+
125+
columnsResizerController.pointsByColumns();
126+
127+
expect(columnHeadersViewSpy).toHaveBeenCalledTimes(2);
128+
});
129+
});
130+
131+
describe('DraggingHeaderViewController', () => {
132+
const getDragEvent = (
133+
eventName: string,
134+
headerOffset: { left: number; top: number },
135+
dragOffset: { left: number; top: number },
136+
) => {
137+
const dragEndEvent = document.createEvent('CustomEvent') as any;
138+
139+
dragEndEvent.initCustomEvent(eventName, true, true);
140+
dragEndEvent.pageX = headerOffset.left + dragOffset.left;
141+
dragEndEvent.pageY = headerOffset.top + dragOffset.top;
142+
dragEndEvent.pointerType = 'mouse';
143+
144+
return dragEndEvent;
145+
};
146+
147+
it('should call "getBoundingRect" once for each dragging panel view', async () => {
148+
const { instance } = await createGridWith200Columns();
149+
const columnHeadersView = (instance as any).getView('columnHeadersView');
150+
const columnChooserView = (instance as any).getView('columnChooserView');
151+
const headerPanelView = (instance as any).getView('headerPanel');
152+
153+
const getBoundingViewMocks = [
154+
jest.spyOn(columnHeadersView, 'getBoundingRect'),
155+
jest.spyOn(columnChooserView, 'getBoundingRect'),
156+
jest.spyOn(headerPanelView, 'getBoundingRect'),
157+
];
158+
159+
const $headerCell = $(columnHeadersView.element()).find('.dx-header-row td').eq(5);
160+
const headerOffset = $headerCell.offset();
161+
162+
if (!headerOffset) {
163+
throw new Error('Header cell not found');
164+
}
165+
166+
const dragStartOffset = { left: 10, top: 10 };
167+
const dragStartEvent = getDragEvent(dragEventStart, headerOffset, dragStartOffset);
168+
$headerCell.get(0)?.dispatchEvent(dragStartEvent);
169+
170+
const dragMoveOffset = { left: 500, top: 10 };
171+
const dragMoveEvent = getDragEvent(dragEventMove, headerOffset, dragMoveOffset);
172+
$headerCell.get(0)?.dispatchEvent(dragMoveEvent);
173+
174+
const dragEndOffset = { left: 500, top: 10 };
175+
const dragEndEvent = getDragEvent(dragEventEnd, headerOffset, dragEndOffset);
176+
$headerCell.get(0)?.dispatchEvent(dragEndEvent);
177+
178+
getBoundingViewMocks.forEach((getBoundingViewMock) => {
179+
expect(getBoundingViewMock).toHaveBeenCalledTimes(1);
180+
});
181+
});
182+
});
183+
});

packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering.ts

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from '@js/core/utils/size';
2222
import { isDefined, isObject, isString } from '@js/core/utils/type';
2323
import swatchContainer from '@ts/core/utils/swatch_container';
24+
import { getDraggingPanelBoundingRects } from '@ts/grids/grid_core/columns_resizing_reordering/utils';
2425
import type { EditorFactory } from '@ts/grids/grid_core/editor_factory/m_editor_factory';
2526
import type { ColumnPoint, ModuleType } from '@ts/grids/grid_core/m_types';
2627
import type { RowsView } from '@ts/grids/grid_core/views/m_rows_view';
@@ -467,14 +468,15 @@ export class DraggingHeaderView extends modules.View {
467468
const that = this;
468469
let result;
469470

470-
each(that._dragOptions.draggingPanels, (index, draggingPanel) => {
471-
if (draggingPanel) {
472-
const boundingRect = draggingPanel.getBoundingRect();
473-
if (boundingRect && (boundingRect.bottom === undefined || pos.y < boundingRect.bottom) && (boundingRect.top === undefined || pos.y > boundingRect.top)
474-
&& (boundingRect.left === undefined || pos.x > boundingRect.left) && (boundingRect.right === undefined || pos.x < boundingRect.right)) {
475-
result = draggingPanel;
476-
return false;
477-
}
471+
each(that._dragOptions.draggingPanelBoundingRects, (_, { draggingPanel, boundingRect }) => {
472+
if (boundingRect
473+
&& (boundingRect.bottom === undefined || pos.y < boundingRect.bottom)
474+
&& (boundingRect.top === undefined || pos.y > boundingRect.top)
475+
&& (boundingRect.left === undefined || pos.x > boundingRect.left)
476+
&& (boundingRect.right === undefined || pos.x < boundingRect.right)
477+
) {
478+
result = draggingPanel;
479+
return false;
478480
}
479481

480482
return undefined;
@@ -525,9 +527,13 @@ export class DraggingHeaderView extends modules.View {
525527

526528
public dragHeader(options) {
527529
const { columnElement } = options;
530+
const dragOptions = {
531+
...options,
532+
draggingPanelBoundingRects: getDraggingPanelBoundingRects(options.draggingPanels),
533+
};
528534

529535
this._isDragging = true;
530-
this._dragOptions = options;
536+
this._dragOptions = dragOptions;
531537
this._dropOptions = {
532538
sourceIndex: options.index,
533539
sourceColumnIndex: this._getVisibleIndexObject(options.rowIndex, options.columnIndex),
@@ -748,11 +754,12 @@ export class ColumnsResizerViewController extends modules.ViewController {
748754
/**
749755
* @extended: adaptivity
750756
*/
751-
protected _pointCreated(point, cellsLength, columns) {
757+
protected _pointCreated(point, columns, cells?: dxElementWrapper) {
752758
const isNextColumnMode = isNextColumnResizingMode(this);
753759
const rtlEnabled = this.option('rtlEnabled');
754760
const isRtlParentStyle = this._isRtlParentStyle();
755761
const firstPointColumnIndex = !isNextColumnMode && rtlEnabled && !isRtlParentStyle ? 0 : 1;
762+
const cellsLength = cells?.length ?? columns.length;
756763

757764
if (point.index >= firstPointColumnIndex && point.index < cellsLength + (!isNextColumnMode && (!rtlEnabled || isRtlParentStyle) ? 1 : 0)) {
758765
this._correctColumnIndexForPoint(point, firstPointColumnIndex, columns);
@@ -989,7 +996,7 @@ export class ColumnsResizerViewController extends modules.ViewController {
989996
if (cells && cells.length > 0) {
990997
that._pointsByColumns = gridCoreUtils.getPointsByColumns(
991998
cells,
992-
(point) => that._pointCreated(correctColumnY(point), cells.length, columns),
999+
(point) => that._pointCreated(correctColumnY(point), columns, cells),
9931000
false,
9941001
0,
9951002
needToCheckPrevPoint,
@@ -1385,10 +1392,18 @@ export class DraggingHeaderViewController extends modules.ViewController {
13851392
* @extended: column_fixing
13861393
*/
13871394
public _generatePointsByColumns(options, needToCheckPrevPoint = false) {
1395+
const cells = this._columnHeadersView.getColumnElements();
13881396
this.isCustomGroupColumnPosition = this.checkIsCustomGroupColumnPosition(options);
1397+
13891398
const points = gridCoreUtils.getPointsByColumns(
13901399
options.columnElements,
1391-
(point) => this._pointCreated(point, options.columns, options.targetDraggingPanel.getName(), options.sourceColumn),
1400+
(point) => this._pointCreated({
1401+
point,
1402+
columns: options.columns,
1403+
location: options.targetDraggingPanel.getName(),
1404+
sourceColumn: options.sourceColumn,
1405+
cells,
1406+
}),
13921407
options.isVerticalOrientation,
13931408
options.startColumnIndex,
13941409
needToCheckPrevPoint,
@@ -1416,14 +1431,24 @@ export class DraggingHeaderViewController extends modules.ViewController {
14161431

14171432
/**
14181433
* @extended: adaptivity, column_fixing
1419-
* Function that is used to filter column points, it's called for each point
1434+
* @description Function used to filter column points, it's called for each point
14201435
* @param point Point that we are checking
14211436
* @param columns All columns in the given location
1422-
* @param location Location where we move column (headers, group, column chooser etc)
1437+
* @param location Location where we move column (headers, group, column chooser, etc.)
14231438
* @param sourceColumn Column that is dragging
1439+
* @param cells JQuery-wrapped collection of header cell elements
14241440
* @returns whether to filter current point (true - remove point, false - keep it)
14251441
*/
1426-
protected _pointCreated(point, columns, location, sourceColumn): boolean {
1442+
1443+
protected _pointCreated({
1444+
point, columns, location, sourceColumn,
1445+
}: {
1446+
point: ColumnPoint;
1447+
columns: any[];
1448+
location?: string;
1449+
sourceColumn?: any;
1450+
cells?: dxElementWrapper;
1451+
}): boolean {
14271452
const targetColumn = columns[point.columnIndex];
14281453
const prevColumn = columns[point.columnIndex - 1];
14291454

0 commit comments

Comments
 (0)