Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,135 @@ test.meta({ browserSize: [800, 800] })('The page should not be changed when hidi
delete (window as any).testLoadCount;
})());

test('New virtual mode. Virtual rows should not be rendered after the height is decreased and restored (T1336247)', async (t) => {
const dataGrid = new DataGrid('#container');
const getViewportSize = ClientFunction(
() => (window as any).widget.getController('data').viewportSize() as number,
);

await t.expect(dataGrid.isReady()).ok();

const initialViewportSize = await getViewportSize();

await dataGrid.option('height', 150);
await t.expect(getViewportSize()).lt(initialViewportSize);

await dataGrid.option('height', 600);
await t.expect(getViewportSize()).eql(initialViewportSize);

const visibleRows = await dataGrid.apiGetVisibleRows();

await t
.expect(visibleRows.length)
.eql(10)
.expect(dataGrid.getRowsView().find(`.${ClassNames.virtualRow}`).count)
.eql(0);
}).before(async () => createWidget('dxDataGrid', {
dataSource: getData(10, 2),
height: 600,
scrolling: {
mode: 'virtual',
},
}));

test('New virtual mode. Virtual rows should not be rendered after the container height is decreased and restored (T1336247)', async (t) => {
const dataGrid = new DataGrid('#container');
const setContainerHeight = ClientFunction((height: number) => {
$('#container').css('height', height);
});

await t.expect(dataGrid.isReady()).ok();

await setContainerHeight(150);
await dataGrid.apiUpdateDimensions();
await setContainerHeight(600);
await dataGrid.apiUpdateDimensions();

await t
.expect(dataGrid.getRowsView().find(`.${ClassNames.virtualRow}`).count)
.eql(0);
}).before(async () => {
await ClientFunction(() => {
$('#container').css('height', 600);
})();

return createWidget('dxDataGrid', {
dataSource: getData(10, 2),
scrolling: {
mode: 'virtual',
},
});
});

test('New virtual mode. Virtual rows should not be rendered after a row is selected while the height is decreased (T1336247)', async (t) => {
const dataGrid = new DataGrid('#container');
const getViewportSize = ClientFunction(
() => (window as any).widget.getController('data').viewportSize() as number,
);

await t.expect(dataGrid.isReady()).ok();

const initialViewportSize = await getViewportSize();

await dataGrid.option('height', 150);
await t.expect(getViewportSize()).lt(initialViewportSize);

await t
.click(dataGrid.getDataRow(0).element)
.expect(dataGrid.getDataRow(0).isSelected)
.ok();

await dataGrid.option('height', 600);
await t.expect(getViewportSize()).eql(initialViewportSize);

await t
.expect(dataGrid.getRowsView().find(`.${ClassNames.virtualRow}`).count)
.eql(0);
}).before(async () => createWidget('dxDataGrid', {
dataSource: getData(10, 2),
height: 600,
selection: {
mode: 'single',
},
scrolling: {
mode: 'virtual',
},
}));

test('New virtual mode. Virtual rows should not be rendered after a row is focused while the height is decreased (T1336247)', async (t) => {
const dataGrid = new DataGrid('#container');
const getViewportSize = ClientFunction(
() => (window as any).widget.getController('data').viewportSize() as number,
);

await t.expect(dataGrid.isReady()).ok();

const initialViewportSize = await getViewportSize();

await dataGrid.option('height', 150);
await t.expect(getViewportSize()).lt(initialViewportSize);

await t
.click(dataGrid.getDataRow(0).element)
.expect(dataGrid.getDataRow(0).isFocusedRow)
.ok();

await dataGrid.option('height', 600);
await t.expect(getViewportSize()).eql(initialViewportSize);

await t
.expect(dataGrid.getRowsView().find(`.${ClassNames.virtualRow}`).count)
.eql(0);
}).before(async () => createWidget('dxDataGrid', {
dataSource: getData(10, 2).map((item, index) => ({ ...item, id: index })),
keyExpr: 'id',
height: 600,
focusedRowEnabled: true,
scrolling: {
mode: 'virtual',
},
}));

fixture.disablePageReloads`Remote Scrolling`
.page(url(__dirname, '../../container.html'));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import {
afterEach, beforeEach, describe, expect, it, jest,
} from '@jest/globals';
import eventsEngine from '@js/common/core/events/core/events_engine';
import { removeEvent } from '@js/common/core/events/remove';
import type { dxElementWrapper } from '@js/core/renderer';
import type { Properties as DataGridProperties } from '@js/ui/data_grid';
import type { Properties as DataGridProperties, RowPreparedEvent } from '@js/ui/data_grid';

import {
afterTest, beforeTest, createDataGrid,
afterTest,
beforeTest,
createDataGrid,
flushAsync,
} from '../../__tests__/__mock__/helpers/utils';

interface TemplateArgs {
Expand Down Expand Up @@ -96,3 +101,65 @@ describe('RowsView renderCompleted with async templates', () => {
expect(rowKeysOnRenderCompleted).toEqual([FIRST_PAGE_ROW_KEYS]);
});
});

describe('RowsView row changes', () => {
beforeEach(beforeTest);
afterEach(afterTest);

const CONTENT_HEIGHT = 500;

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const createGrid = async () => {
const dataSource = ROWS.map((row) => ({ ...row }));
const contentMinHeightsOnRowRemove: string[] = [];

const grid = await createDataGrid({
dataSource,
repaintChangesOnly: true,
onRowPrepared: ({ rowElement }: RowPreparedEvent) => {
eventsEngine.on(rowElement, removeEvent, () => {
const content = rowElement.closest<HTMLElement>('.dx-datagrid-rowsview .dx-datagrid-content');

if (content) {
contentMinHeightsOnRowRemove.push(content.style.minHeight);
}
});
},
});
const content = (grid.$container.get(0) as HTMLElement)
.querySelector<HTMLElement>('.dx-datagrid-rowsview .dx-datagrid-content') as HTMLElement;

content.style.height = `${CONTENT_HEIGHT}px`;
contentMinHeightsOnRowRemove.length = 0;

const removeFirstRow = async (): Promise<void> => {
dataSource.shift();
const refreshing = grid.instance.refresh(true);
await flushAsync();
await refreshing;
};

return {
...grid, content, contentMinHeightsOnRowRemove, removeFirstRow,
};
};

describe('when a rendered row is removed', () => {
it('should keep the content height while the row is removed', async () => {
const { contentMinHeightsOnRowRemove, removeFirstRow } = await createGrid();

await removeFirstRow();

expect(contentMinHeightsOnRowRemove).toEqual([`${CONTENT_HEIGHT}px`]);
});

it('should restore the content min-height after the row is removed', async () => {
const { content, removeFirstRow } = await createGrid();
content.style.minHeight = '100px';

await removeFirstRow();

expect(content.style.minHeight).toBe('100px');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { ColumnsView } from './m_columns_view';
import type { RowsViewScrollEvent } from './types';
import {
getCellText, getMaxHorizontalScrollOffset, isRowElementVisible, isSameColumnLayout,
keepContentHeight,
} from './utils';

const ROWS_VIEW_CLASS = 'rowsview';
Expand Down Expand Up @@ -459,77 +460,82 @@ export class RowsView extends ColumnsView {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return this.waitAsyncTemplates().done(() => {
const contentChanges = this._contentChanges;
const hasRowChanges = contentChanges.some(
({ change: contentChange }) => contentChange?.changeType === 'update',
);

this._contentChanges = [];

contentChanges.forEach(({ newTableElement, change, isFixedTableRendering }) => {
const tableElement = this.getTableElement(isFixedTableRendering);
const contentElement = this._findContentElement(isFixedTableRendering);
const changeType = change?.changeType;
const executors: any[] = [];
const highlightChanges = this.option('highlightChanges');
const rowInsertedClass = this.addWidgetPrefix(ROW_INSERTED_ANIMATION_CLASS);

switch (changeType) {
case 'update':
each(change.rowIndices, (index, rowIndex) => {
const $newRowElement = this._getRowElements(newTableElement).eq(index);
const dataChangeType = change.changeTypes?.[index];
const item = change.items && change.items[index];

executors.push(() => {
const $rowElements = this._getRowElements(tableElement);
const $rowElement = $rowElements.eq(rowIndex);

// eslint-disable-next-line default-case
switch (dataChangeType) {
case 'update':
if (item) {
const columnIndices = change.columnIndices?.[index];

if (isDefined(item.visible) && item.visible !== $rowElement.is(':visible')) {
$rowElement.toggle(item.visible);
} else if (columnIndices) {
this._updateCells($rowElement, $newRowElement, columnIndices, item);
keepContentHeight(hasRowChanges ? this._findContentElement() : undefined, () => {
contentChanges.forEach(({ newTableElement, change, isFixedTableRendering }) => {
const tableElement = this.getTableElement(isFixedTableRendering);
const contentElement = this._findContentElement(isFixedTableRendering);
const changeType = change?.changeType;
const executors: any[] = [];
const highlightChanges = this.option('highlightChanges');
const rowInsertedClass = this.addWidgetPrefix(ROW_INSERTED_ANIMATION_CLASS);

switch (changeType) {
case 'update':
each(change.rowIndices, (index, rowIndex) => {
const $newRowElement = this._getRowElements(newTableElement).eq(index);
const dataChangeType = change.changeTypes?.[index];
const item = change.items && change.items[index];

executors.push(() => {
const $rowElements = this._getRowElements(tableElement);
const $rowElement = $rowElements.eq(rowIndex);

// eslint-disable-next-line default-case
switch (dataChangeType) {
case 'update':
if (item) {
const columnIndices = change.columnIndices?.[index];

if (isDefined(item.visible) && item.visible !== $rowElement.is(':visible')) {
$rowElement.toggle(item.visible);
} else if (columnIndices) {
this._updateCells($rowElement, $newRowElement, columnIndices, item);
} else {
$rowElement.replaceWith($newRowElement);
}
}
break;
case 'insert':
if (!$rowElements.length) {
if (tableElement) {
const target = $newRowElement.is('tbody') ? tableElement : tableElement.children('tbody');
$newRowElement.prependTo(target);
}
} else if ($rowElement.length) {
$newRowElement.insertBefore($rowElement);
} else {
$rowElement.replaceWith($newRowElement);
$newRowElement.insertAfter($rowElements.last());
}
}
break;
case 'insert':
if (!$rowElements.length) {
if (tableElement) {
const target = $newRowElement.is('tbody') ? tableElement : tableElement.children('tbody');
$newRowElement.prependTo(target);
if (highlightChanges && change.isLiveUpdate) {
$newRowElement.addClass(rowInsertedClass);
}
} else if ($rowElement.length) {
$newRowElement.insertBefore($rowElement);
} else {
$newRowElement.insertAfter($rowElements.last());
}
if (highlightChanges && change.isLiveUpdate) {
$newRowElement.addClass(rowInsertedClass);
}
break;
case 'remove':
$rowElement.remove();
break;
}
break;
case 'remove':
$rowElement.remove();
break;
}
});
});
each(executors, function () {
this();
});
});
each(executors, function () {
this();
});

newTableElement.remove();
break;
default:
this.setTableElement(newTableElement, isFixedTableRendering);
contentElement.addClass(this.addWidgetPrefix(CONTENT_CLASS));
this._setGridRole(contentElement);
this._renderContent(contentElement, newTableElement, isFixedTableRendering);
break;
}
newTableElement.remove();
break;
default:
this.setTableElement(newTableElement, isFixedTableRendering);
contentElement.addClass(this.addWidgetPrefix(CONTENT_CLASS));
this._setGridRole(contentElement);
this._renderContent(contentElement, newTableElement, isFixedTableRendering);
break;
}
});
});
}).fail(() => {
this._contentChanges = [];
Expand Down
Loading
Loading