Skip to content

Commit 4dba273

Browse files
authored
T1328904 - TreeList — Auto-width columns are incorrectly resized in Firefox (DevExpress#34190)
1 parent 81cce53 commit 4dba273

8 files changed

Lines changed: 222 additions & 19 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { ClientFunction } from 'testcafe';
2+
import ExpandableCell from 'devextreme-testcafe-models/treeList/expandableCell';
3+
import TreeList from 'devextreme-testcafe-models/treeList';
4+
import url from '../../../helpers/getPageUrl';
5+
import { createWidget } from '../../../helpers/createWidget';
6+
7+
fixture.disablePageReloads`Columns Auto Width`
8+
.page(url(__dirname, '../../container.html'));
9+
10+
const treeListData = [
11+
{
12+
id: 1, parentId: 0, name: 'Root item with a long name', size: 1024, date: '2024-01-01',
13+
},
14+
{
15+
id: 2, parentId: 1, name: 'Child 1', size: 512, date: '2024-02-01',
16+
},
17+
{
18+
id: 3, parentId: 1, name: 'Child 2 with a longer name value', size: 256, date: '2024-03-01',
19+
},
20+
{
21+
id: 4, parentId: 0, name: 'Second root', size: 2048, date: '2024-04-01',
22+
},
23+
];
24+
25+
const treeListConfig = {
26+
dataSource: treeListData,
27+
keyExpr: 'id',
28+
parentIdExpr: 'parentId',
29+
columnAutoWidth: true,
30+
width: 500,
31+
repaintChangesOnly: true,
32+
columns: [
33+
{ dataField: 'name' },
34+
{ dataField: 'size', width: 100 },
35+
{ dataField: 'date', width: 150 },
36+
],
37+
scrolling: {
38+
mode: 'standard',
39+
useNative: false,
40+
},
41+
};
42+
43+
// T1328904
44+
test('columns should update auto width when expanding row', async (t) => {
45+
const treeList = new TreeList('#container');
46+
await t.expect(treeList.isReady()).ok();
47+
48+
const widthsBefore = await treeList.getHeaderCellWidths();
49+
const [nameWidthBefore] = widthsBefore;
50+
await t.expect(widthsBefore).eql([250, 100, 150]);
51+
52+
const expandableCell = new ExpandableCell(treeList.getDataRow(0).getDataCell(0));
53+
await t.click(expandableCell.getExpandButton());
54+
55+
const widthsAfter = await treeList.getHeaderCellWidths();
56+
const [nameWidthAfter, sizeWidthAfter, dateWidthAfter] = widthsAfter;
57+
await t.expect(nameWidthAfter).gt(nameWidthBefore);
58+
await t.expect(sizeWidthAfter).eql(100);
59+
await t.expect(dateWidthAfter).eql(150);
60+
}).before(async () => createWidget('dxTreeList', treeListConfig));
61+
62+
// T1328904
63+
test('columns should update auto width when collapsing row', async (t) => {
64+
const treeList = new TreeList('#container');
65+
await t.expect(treeList.isReady()).ok();
66+
67+
const widthsBefore = await treeList.getHeaderCellWidths();
68+
const [nameWidthBefore] = widthsBefore;
69+
70+
const expandableCell = new ExpandableCell(treeList.getDataRow(0).getDataCell(0));
71+
await t.click(expandableCell.getCollapseButton());
72+
73+
const widthsAfter = await treeList.getHeaderCellWidths();
74+
const [nameWidthAfter, sizeWidthAfter, dateWidthAfter] = widthsAfter;
75+
await t.expect(nameWidthAfter).lt(nameWidthBefore);
76+
await t.expect(sizeWidthAfter).eql(100);
77+
await t.expect(dateWidthAfter).eql(150);
78+
}).before(async () => createWidget('dxTreeList', {
79+
...treeListConfig,
80+
expandedRowKeys: [1],
81+
}));
82+
83+
// T1328904
84+
test('columns should update auto width when expanded row keys are updated using API', async (t) => {
85+
const treeList = new TreeList('#container');
86+
await t.expect(treeList.isReady()).ok();
87+
88+
const widthsBefore = await treeList.getHeaderCellWidths();
89+
const [nameWidthBefore] = widthsBefore;
90+
await t.expect(widthsBefore).eql([250, 100, 150]);
91+
92+
await treeList.apiOption('expandedRowKeys', [1]);
93+
94+
const widthsAfter = await treeList.getHeaderCellWidths();
95+
const [nameWidthAfter, sizeWidthAfter, dateWidthAfter] = widthsAfter;
96+
await t.expect(nameWidthAfter).gt(nameWidthBefore);
97+
await t.expect(sizeWidthAfter).eql(100);
98+
await t.expect(dateWidthAfter).eql(150);
99+
}).before(async () => createWidget('dxTreeList', treeListConfig));
100+
101+
test('columns should update auto width after loadDescendants call', async (t) => {
102+
const treeList = new TreeList('#container');
103+
await t.expect(treeList.isReady()).ok();
104+
105+
await treeList.apiLoadDescendants(1);
106+
107+
const widthsBefore = await treeList.getHeaderCellWidths();
108+
await t.expect(widthsBefore).eql([250, 100, 150]);
109+
110+
const expandableCell = new ExpandableCell(treeList.getDataRow(0).getDataCell(0));
111+
await t.click(expandableCell.getExpandButton());
112+
113+
const widthsAfter = await treeList.getHeaderCellWidths();
114+
const [nameWidthAfter, sizeWidthAfter, dateWidthAfter] = widthsAfter;
115+
await t.expect(nameWidthAfter).gt(250);
116+
await t.expect(sizeWidthAfter).eql(100);
117+
await t.expect(dateWidthAfter).eql(150);
118+
}).before(async () => createWidget('dxTreeList', {
119+
...treeListConfig,
120+
dataSource: {
121+
key: 'id',
122+
load: ClientFunction((loadOptions: any) => {
123+
let result = treeListData;
124+
125+
if (loadOptions.filter) {
126+
const parentIds = loadOptions.filter[0] === 'parentId'
127+
? [loadOptions.filter[2]]
128+
: loadOptions.filter
129+
.filter((f: any) => Array.isArray(f) && f[0] === 'parentId')
130+
.map((f: any) => f[2]);
131+
132+
if (parentIds.length) {
133+
result = treeListData.filter((item) => parentIds.includes(item.parentId));
134+
}
135+
}
136+
return Promise.resolve(result);
137+
}, { dependencies: { treeListData } }),
138+
},
139+
remoteOperations: {
140+
filtering: true,
141+
},
142+
}));

packages/devextreme/js/__internal/grids/grid_core/data_controller/m_data_controller.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1244,7 +1244,7 @@ export class DataController extends DataHelperMixin(modules.Controller) {
12441244
change.isDataChanged = true;
12451245
change.repaintChangesOnly = operationTypes && !operationTypes.grouping && !operationTypes.filtering && this.option('repaintChangesOnly');
12461246

1247-
if (operationTypes && (operationTypes.reload || operationTypes.paging || operationTypes.groupExpanding)) {
1247+
if (this.needUpdateDimensions(operationTypes)) {
12481248
change.needUpdateDimensions = true;
12491249
}
12501250
}
@@ -1261,6 +1261,12 @@ export class DataController extends DataHelperMixin(modules.Controller) {
12611261
this._fireChanged(change);
12621262
}
12631263

1264+
protected needUpdateDimensions(operationTypes) {
1265+
return operationTypes && (
1266+
operationTypes.reload || operationTypes.paging || operationTypes.groupExpanding
1267+
);
1268+
}
1269+
12641270
public loadingOperationTypes() {
12651271
const dataSource = this.dataSource();
12661272

packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export default class DataSourceAdapter extends modules.Controller {
198198

199199
private _needClearStoreDataCache() {
200200
const remoteOperations = this.remoteOperations();
201-
const operationTypes = calculateOperationTypes(this._lastLoadOptions || {}, {});
201+
const operationTypes = this._calculateOperationTypes(this._lastLoadOptions || {}, {});
202202
const isLocalOperations = Object.keys(remoteOperations).every((operationName) => !operationTypes[operationName] || !remoteOperations[operationName]);
203203

204204
return !isLocalOperations;
@@ -333,6 +333,10 @@ export default class DataSourceAdapter extends modules.Controller {
333333
return currentOperationTypes.some((operationType) => remoteOperations[operationType]);
334334
}
335335

336+
protected _calculateOperationTypes(loadOptions, lastLoadOptions, isFullReload?: boolean) {
337+
return calculateOperationTypes(loadOptions, lastLoadOptions, isFullReload);
338+
}
339+
336340
/**
337341
* @extended: virtual_scrolling, TreeLists's data_source_adapter, DataGrid's m_grouping
338342
*/
@@ -412,7 +416,7 @@ export default class DataSourceAdapter extends modules.Controller {
412416

413417
const loadOptions = extend({ pageIndex: this.pageIndex(), pageSize: this.pageSize() }, options.storeLoadOptions);
414418

415-
const operationTypes = calculateOperationTypes(loadOptions, lastLoadOptions, isFullReload);
419+
const operationTypes = this._calculateOperationTypes(loadOptions, lastLoadOptions, isFullReload);
416420

417421
this._customizeRemoteOperations(options, operationTypes);
418422

packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -172,38 +172,45 @@ export class ResizingController extends modules.ViewController {
172172
}
173173

174174
private _refreshSizes(e) {
175-
// @ts-expect-error
176-
let resizeDeferred = new Deferred<null>().resolve(null);
177175
const changeType = e?.changeType;
178176
const isDelayed = e?.isDelayed;
179-
const items = this._dataController.items();
180177

181-
if (!e || changeType === 'refresh' || changeType === 'prepend' || changeType === 'append') {
178+
if (!e || ['refresh', 'prepend', 'append'].includes(changeType)) {
182179
if (!isDelayed) {
183-
resizeDeferred = this.resize();
180+
return this.resize();
184181
}
185-
} else if (changeType === 'update') {
186-
if (e.changeTypes?.length === 0) {
187-
return resizeDeferred;
182+
}
183+
184+
if (changeType === 'update') {
185+
if (!e.changeTypes?.length) {
186+
// @ts-expect-error
187+
return new Deferred<null>().resolve(null);
188188
}
189-
if ((items.length > 1 || e.changeTypes[0] !== 'insert')
190-
&& !(items.length === 0 && e.changeTypes[0] === 'remove') && !e.needUpdateDimensions) {
189+
190+
const items = this._dataController.items();
191+
const isHidingNoDataPanel = items.length <= 1 && e.changeTypes[0] === 'insert';
192+
const isShowingNoDataPanel = items.length === 0 && e.changeTypes[0] === 'remove';
193+
194+
if (!isHidingNoDataPanel && !isShowingNoDataPanel && !e.needUpdateDimensions) {
191195
// @ts-expect-error
192-
resizeDeferred = new Deferred();
196+
const deferred = new Deferred();
193197

194198
this._waitAsyncTemplates().done(() => {
195199
deferUpdate(() => deferRender(() => deferUpdate(() => {
196200
this._setScrollerSpacing();
197201
this._rowsView.resize();
198-
resizeDeferred.resolve();
202+
deferred.resolve();
199203
})));
200-
}).fail(resizeDeferred.reject);
201-
} else {
202-
resizeDeferred = this.resize();
204+
}).fail(deferred.reject);
205+
206+
return deferred;
203207
}
208+
209+
return this.resize();
204210
}
205211

206-
return resizeDeferred;
212+
// @ts-expect-error
213+
return new Deferred<null>().resolve(null);
207214
}
208215

209216
/**

packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ export class TreeListDataController extends DataController {
9595
return super.publicMethods().concat(['expandRow', 'collapseRow', 'isRowExpanded', 'getRootNode', 'getNodeByKey', 'loadDescendants', 'forEachNode']);
9696
}
9797

98+
protected override needUpdateDimensions(operationTypes) {
99+
return super.needUpdateDimensions(operationTypes) || (
100+
operationTypes && operationTypes.nodeExpanding
101+
);
102+
}
103+
98104
private changeRowExpand(key) {
99105
if (this._dataSource) {
100106
const args: any = {

packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter {
7272

7373
private _totalItemsCount: any;
7474

75+
private _lastExpandedRowKeys: any;
76+
7577
private _createKeyGetter() {
7678
const keyExpr = this.getKeyExpr();
7779

@@ -262,6 +264,15 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter {
262264
return gridCoreUtils.combineFilters(parentIdFilters, 'or');
263265
}
264266

267+
protected override _calculateOperationTypes(loadOptions, lastLoadOptions, isFullReload?: boolean) {
268+
const currentExpandedKeys = this.option('expandedRowKeys');
269+
270+
return {
271+
...super._calculateOperationTypes(loadOptions, lastLoadOptions, isFullReload),
272+
nodeExpanding: !equalByValue(this._lastExpandedRowKeys, currentExpandedKeys),
273+
};
274+
}
275+
265276
protected _customizeRemoteOperations(options, operationTypes) {
266277
super._customizeRemoteOperations.apply(this, arguments as any);
267278

@@ -607,6 +618,10 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter {
607618
this._updateHasItemsMap(options);
608619
super._handleDataLoaded(options);
609620

621+
if (!options.isCustomLoading) {
622+
this._lastExpandedRowKeys = this.option('expandedRowKeys')?.slice();
623+
}
624+
610625
if (data.isConverted && this._cachedStoreData) {
611626
this._cachedStoreData.isConverted = true;
612627
}

packages/testcafe-models/dataGrid/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,19 @@ export default class DataGrid extends GridCore {
177177
return this.getHeadersContainer().find(`.${CLASS.scrollContainer}`);
178178
}
179179

180+
async getHeaderCellWidths(): Promise<number[]> {
181+
const cells = this.getHeaders().getHeaderRow(0).getHeaderCells();
182+
const count = await cells.count;
183+
const widths: number[] = [];
184+
185+
for (let i = 0; i < count; i += 1) {
186+
const { width } = await cells.nth(i).boundingClientRect;
187+
widths.push(Math.round(width));
188+
}
189+
190+
return widths;
191+
}
192+
180193
getRowsView(): Selector {
181194
return this.element.find(`.${this.addWidgetPrefix(CLASS.rowsView)}`);
182195
}

packages/testcafe-models/treeList/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ClientFunction } from 'testcafe';
12
import type { WidgetName } from '../types';
23
import DataGrid from '../dataGrid';
34

@@ -17,4 +18,13 @@ export default class TreeList extends DataGrid {
1718
getAdaptiveButtonSelector(): string {
1819
return `.${CLASS.adaptiveColumnButton}`;
1920
}
21+
22+
apiLoadDescendants(key?: unknown): Promise<void> {
23+
const { getInstance } = this;
24+
25+
return ClientFunction(
26+
() => (getInstance() as any).loadDescendants(key),
27+
{ dependencies: { getInstance, key } },
28+
)();
29+
}
2030
}

0 commit comments

Comments
 (0)