Skip to content

Commit 8fe961f

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

File tree

11 files changed

+430
-41
lines changed

11 files changed

+430
-41
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Column } from '@js/ui/data_grid';
2+
import DataGrid from '@js/ui/data_grid';
3+
4+
import { GridCoreModel } from '../../../../grid_core/__tests__/__mock__/model/grid_core';
5+
6+
export class DataGridModel extends GridCoreModel<DataGrid> {
7+
public getInstance(): DataGrid {
8+
return DataGrid.getInstance(this.root) as DataGrid;
9+
}
10+
11+
public apiGetVisibleColumns(headerLevel?: number): Column[] {
12+
if (headerLevel === undefined) {
13+
return this.getInstance().getVisibleColumns();
14+
}
15+
16+
return this.getInstance().getVisibleColumns(headerLevel);
17+
}
18+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2+
/* eslint-disable @typescript-eslint/no-explicit-any */
3+
import type { GridBase } from '@js/common/grids';
4+
import type { dxElementWrapper } from '@js/core/renderer';
5+
import $ from '@js/core/renderer';
6+
7+
const SELECTORS = {
8+
headerRowClass: 'dx-header-row',
9+
dataRowClass: 'dx-data-row',
10+
groupRowClass: 'dx-group-row',
11+
};
12+
13+
export abstract class GridCoreModel<TInstance extends GridBase = GridBase> {
14+
constructor(protected readonly root: HTMLElement) {}
15+
16+
public getHeaderCells(): NodeListOf<HTMLElement> {
17+
return this.root.querySelectorAll(`.${SELECTORS.headerRowClass} > td`);
18+
}
19+
20+
public getHeaderCell(columnIndex: number): HTMLElement {
21+
return this.getHeaderCells()[columnIndex];
22+
}
23+
24+
public getCellElement(rowIndex: number, columnIndex: number): HTMLElement {
25+
return this.root.querySelectorAll(`.${SELECTORS.dataRowClass}`)[rowIndex]?.querySelectorAll('td')[columnIndex] as HTMLElement;
26+
}
27+
28+
public getGroupRows(): NodeListOf<HTMLElement> {
29+
return this.root.querySelectorAll(`.${SELECTORS.groupRowClass}`);
30+
}
31+
32+
public apiColumnOption(id: string, name?: string, value?: any): any {
33+
switch (arguments.length) {
34+
case 1:
35+
return this.getInstance().columnOption(id);
36+
case 2:
37+
return this.getInstance().columnOption(id, name);
38+
default:
39+
this.getInstance().columnOption(id, name as string, value);
40+
return undefined;
41+
}
42+
}
43+
44+
public getHeaderByText(text: string): dxElementWrapper {
45+
return $(Array.from(this.getHeaderCells()).find((el) => $(el).text().includes(text)));
46+
}
47+
48+
public abstract getInstance(): TInstance;
49+
}

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
@@ -993,8 +993,8 @@ const exportExtender = (
993993
const columnsResizer = (
994994
Base: ModuleType<ColumnsResizerViewController>,
995995
) => class AdaptivityColumnsResizerExtender extends Base {
996-
protected _pointCreated(point, cellsLength, columns) {
997-
const result = super._pointCreated(point, cellsLength, columns);
996+
protected _pointCreated(point, columns, cells?: dxElementWrapper) {
997+
const result = super._pointCreated(point, columns, cells);
998998
const currentColumn = columns[point.columnIndex] || {};
999999
const nextColumnIndex = this._getNextColumnIndex(point.columnIndex);
10001000
const nextColumn = columns[nextColumnIndex] || {};
@@ -1019,8 +1019,12 @@ const columnsResizer = (
10191019
const draggingHeader = (
10201020
Base: ModuleType<DraggingHeaderViewController>,
10211021
) => class AdaptivityDraggingHeaderExtender extends Base {
1022-
protected _pointCreated(point, columns, location, sourceColumn) {
1023-
const result = super._pointCreated(point, columns, location, sourceColumn);
1022+
protected _pointCreated({
1023+
point, columns, location, sourceColumn, cells,
1024+
}) {
1025+
const result = super._pointCreated({
1026+
point, columns, location, sourceColumn, cells,
1027+
});
10241028
const column = columns[point.columnIndex - 1] || {};
10251029
const hasAdaptiveHiddenWidth = column.visibleWidth === HIDDEN_COLUMNS_WIDTH;
10261030

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+
});

0 commit comments

Comments
 (0)