From 9a243bc9ce91a08d909b28c1dcaddb441b029039 Mon Sep 17 00:00:00 2001 From: Maksim Zakharov <251575087+bit-byte0@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:12:46 +0400 Subject: [PATCH 1/2] refactor(grids): extract scrolling knowledge to extenders --- .../data_controller/data_controller.ts | 25 +++++-- .../utils/__tests__/paging.test.ts | 27 +------ .../grid_core/data_controller/utils/paging.ts | 11 --- ...l_scrolling_data_controller.paging.test.ts | 73 +++++++++++++++++++ .../virtual_scrolling_data_controller.ts | 24 +++++- 5 files changed, 115 insertions(+), 45 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/__tests__/virtual_scrolling_data_controller.paging.test.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/data_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/data_controller.ts index ee2403af6f00..a43de2430c30 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/data_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/data_controller.ts @@ -47,7 +47,7 @@ import type { UpdateItemChange, UserState, } from './types'; -import { resolvePaginate, syncPaging } from './utils/paging'; +import { syncPaging } from './utils/paging'; import { getRefreshOptions } from './utils/refresh'; import { attachChangedItems, @@ -275,7 +275,6 @@ export class DataController extends modules.Controller { case 'remoteOperations': case 'keyExpr': case 'dataSource': - case 'scrolling': args.handled = true; this.reset(); break; @@ -617,19 +616,31 @@ export class DataController extends modules.Controller { } private applyPagingOptions(dataSource: PagingDataSource): PagingChanges { - const { scrolling, paging } = this.option(); + const { paging } = this.option(); - // Not paging state to reconcile, but a per-load request flag: infinite - // scrolling detects the last page locally and needs no grand total. - dataSource.requireTotalCount(scrolling?.mode !== 'infinite'); + dataSource.requireTotalCount(this.requiresTotalCount()); return syncPaging(dataSource, { - paginate: resolvePaginate(paging?.enabled, scrolling?.mode), + paginate: this.resolvePaginate(paging?.enabled), pageSize: paging?.pageSize, pageIndex: paging?.pageIndex, }); } + /** + * @extended: virtual_scrolling + */ + protected resolvePaginate(enabled: boolean | undefined): boolean | undefined { + return enabled; + } + + /** + * @extended: virtual_scrolling + */ + protected requiresTotalCount(): boolean { + return true; + } + /** * @extended: state_storing, virtual_scrolling */ diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/__tests__/paging.test.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/__tests__/paging.test.ts index 185dc4ae86a5..0a58ae260560 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/__tests__/paging.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/__tests__/paging.test.ts @@ -1,32 +1,7 @@ import { describe, expect, it } from '@jest/globals'; import type { PagingDataSource } from '../../types'; -import { resolvePaginate, syncPaging } from '../paging'; - -describe('resolvePaginate', () => { - it.each([ - { enabled: true, scrollingMode: 'standard', expected: true }, - { enabled: false, scrollingMode: 'standard', expected: false }, - // Virtual and infinite scrolling paginate even with paging disabled. - { enabled: false, scrollingMode: 'virtual', expected: true }, - { enabled: false, scrollingMode: 'infinite', expected: true }, - { enabled: true, scrollingMode: 'virtual', expected: true }, - { enabled: true, scrollingMode: 'infinite', expected: true }, - ])('should be $expected for enabled=$enabled, scrolling.mode=$scrollingMode', ({ - enabled, scrollingMode, expected, - }) => { - expect(resolvePaginate(enabled, scrollingMode)).toBe(expected); - }); - - // An undefined `paging.enabled` leaves the data source's paginate alone, - // even in a mode that would otherwise force it on. - it.each(['standard', 'virtual', 'infinite'])( - 'should be undefined when enabled is undefined in %s mode', - (scrollingMode) => { - expect(resolvePaginate(undefined, scrollingMode)).toBeUndefined(); - }, - ); -}); +import { syncPaging } from '../paging'; const createDataSourceMock = ( state: { paginate: boolean; pageSize: number; pageIndex: number }, diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/paging.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/paging.ts index 0d4ba2ec690a..dc8f50219e70 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/paging.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/paging.ts @@ -1,16 +1,5 @@ import type { PagingChanges, PagingDataSource, SyncPagingOptions } from '../types'; -export function resolvePaginate( - enabled: boolean | undefined, - scrollingMode: string | undefined, -): boolean | undefined { - if (enabled === undefined) { - return undefined; - } - - return enabled || scrollingMode === 'virtual' || scrollingMode === 'infinite'; -} - export function syncPaging( dataSource: PagingDataSource, options: SyncPagingOptions, diff --git a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/__tests__/virtual_scrolling_data_controller.paging.test.ts b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/__tests__/virtual_scrolling_data_controller.paging.test.ts new file mode 100644 index 000000000000..74c28fa4b410 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/__tests__/virtual_scrolling_data_controller.paging.test.ts @@ -0,0 +1,73 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import type { DataGridScrollMode } from '@js/ui/data_grid'; +import { + afterTest, + beforeTest, + createDataGrid, +} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; +import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; + +declare class ExposedDataController extends DataController { + public resolvePaginate(enabled: boolean | undefined): boolean | undefined; + + public requiresTotalCount(): boolean; +} + +const withScrollingMode = async (mode: DataGridScrollMode): Promise => { + const { instance } = await createDataGrid({ dataSource: [], scrolling: { mode } }); + + return instance.getController('data') as unknown as ExposedDataController; +}; + +describe('Virtual scrolling data controller paging', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('resolvePaginate', () => { + it.each<{ mode: DataGridScrollMode; enabled: boolean; expected: boolean }>([ + { mode: 'standard', enabled: true, expected: true }, + { mode: 'standard', enabled: false, expected: false }, + { mode: 'virtual', enabled: false, expected: true }, + { mode: 'infinite', enabled: false, expected: true }, + ])('should be $expected in $mode mode with enabled=$enabled', async ({ + mode, enabled, expected, + }) => { + const dataController = await withScrollingMode(mode); + + expect(dataController.resolvePaginate(enabled)).toBe(expected); + }); + + it.each(['standard', 'virtual', 'infinite'])( + 'should leave paginate untouched when enabled is undefined in %s mode', + async (mode) => { + const dataController = await withScrollingMode(mode); + + expect(dataController.resolvePaginate(undefined)).toBeUndefined(); + }, + ); + }); + + describe('requiresTotalCount', () => { + it.each<{ mode: DataGridScrollMode; expected: boolean }>([ + { mode: 'standard', expected: true }, + { mode: 'virtual', expected: true }, + { mode: 'infinite', expected: false }, + ])('should be $expected in $mode mode', async ({ mode, expected }) => { + const dataController = await withScrollingMode(mode); + + expect(dataController.requiresTotalCount()).toBe(expected); + }); + }); + + it('should reset the data controller when the scrolling option changes', async () => { + const { instance } = await createDataGrid({ dataSource: [] }); + const dataController = instance.getController('data'); + const resetSpy = jest.spyOn(dataController, 'reset'); + + instance.option('scrolling.mode', 'virtual'); + + expect(resetSpy).toHaveBeenCalled(); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/virtual_scrolling_data_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/virtual_scrolling_data_controller.ts index d85f9fa8473a..5fc7324010ca 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/virtual_scrolling_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/virtual_scrolling_data_controller.ts @@ -25,7 +25,7 @@ import type { DataChange, PagingOptionName, PagingResult, ProcessedItem, RefreshOptions, } from '@ts/grids/grid_core/data_controller/types'; import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types'; -import type { ModuleType } from '@ts/grids/grid_core/m_types'; +import type { ModuleType, OptionChanged } from '@ts/grids/grid_core/m_types'; import type { VirtualItemsCount } from '@ts/grids/grid_core/virtual_data_loader/types'; import gridCoreUtils from '../../m_utils'; @@ -933,6 +933,28 @@ export const virtualScrollingDataControllerExtender = ( return result; } + protected resolvePaginate(enabled: boolean | undefined): boolean | undefined { + if (enabled === undefined) { + return undefined; + } + + return enabled || isVirtualPaging(this); + } + + protected requiresTotalCount(): boolean { + return !isInfiniteMode(this); + } + + public optionChanged(args: OptionChanged): void { + if (args.name === 'scrolling') { + args.handled = true; + this.reset(); + return; + } + + super.optionChanged(args); + } + public isEmpty(): boolean { return this.option(LEGACY_SCROLLING_MODE) === false ? !this.items(true).length : super.isEmpty.apply(this, arguments as any); } From 3ffb5309276cb185aa60f71bf4b174485da489fa Mon Sep 17 00:00:00 2001 From: Maksim Zakharov <251575087+bit-byte0@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:13:37 +0400 Subject: [PATCH 2/2] test(grids): keep paging enabled in treeList stateStoring mocks --- .../tests/DevExpress.ui.widgets.treeList/stateStoring.tests.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/stateStoring.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/stateStoring.tests.js index e8889da63876..b22518b54933 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/stateStoring.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/stateStoring.tests.js @@ -34,6 +34,9 @@ QUnit.module('State Storing', { keyExpr: 'id', parentIdExpr: 'parentId', loadingTimeout: null, + paging: { + enabled: true + }, scrolling: { mode: 'virtual' }