diff --git a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap index d64f709fa54e..a3748ebef495 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap +++ b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap @@ -5,6 +5,8 @@ exports[`common initial render should be successfull 1`] = ` class="dx-widget dx-cardview" > - This is cardView +
+ +
`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx index 18a673b2714a..90378facc2a4 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx @@ -2,31 +2,46 @@ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ import { combined } from '@ts/core/reactive/index'; import { View } from '@ts/grids/new/grid_core/core/view'; +import { PagerView } from '@ts/grids/new/grid_core/pager/view'; import { ToolbarView } from '@ts/grids/new/grid_core/toolbar/view'; import type { ComponentType } from 'inferno'; // eslint-disable-next-line @typescript-eslint/no-empty-interface interface MainViewProps { Toolbar: ComponentType; + Pager: ComponentType; } -// eslint-disable-next-line no-empty-pattern function MainViewComponent({ - Toolbar, + Toolbar, Pager, }: MainViewProps): JSX.Element { return (<> {/* @ts-expect-error */} - This is cardView +
+ {/* + TODO: + Pager, as renovated component, has strange disposing. + See `inferno_renderer.remove` method. + It somehow mutates $V prop of parent element. + Without this div, CardView would be parent of Pager. + In this case all `componentWillUnmount`s aren't called + */} + {/* @ts-expect-error */} + +
); } export class MainView extends View { protected override component = MainViewComponent; - public static dependencies = [ToolbarView] as const; + public static dependencies = [ + PagerView, ToolbarView, + ] as const; constructor( + private readonly pager: PagerView, private readonly toolbar: ToolbarView, ) { super(); @@ -37,6 +52,7 @@ export class MainView extends View { protected override getProps() { return combined({ Toolbar: this.toolbar.asInferno(), + Pager: this.pager.asInferno(), }); } } diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/inferno_wrappers/pager.ts b/packages/devextreme/js/__internal/grids/new/grid_core/inferno_wrappers/pager.ts new file mode 100644 index 000000000000..589e4a33dbf7 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/inferno_wrappers/pager.ts @@ -0,0 +1,10 @@ +import type { Properties as PaginationProperties } from '@js/ui/pagination'; +import dxPagination from '@js/ui/pagination'; + +import { InfernoWrapper } from './widget_wrapper'; + +export class Pager extends InfernoWrapper { + protected getComponentFabric(): typeof dxPagination { + return dxPagination; + } +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/options.ts index b2d6bfc693a1..569f83aece83 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/options.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/options.ts @@ -4,7 +4,8 @@ import type { WidgetOptions } from '@js/ui/widget/ui.widget'; import * as columnsController from './columns_controller/index'; import * as dataController from './data_controller/index'; -import type * as toolbar from './toolbar'; +import * as pager from './pager/index'; +import type * as toolbar from './toolbar/index'; import type { GridCoreNew } from './widget'; /** @@ -13,12 +14,14 @@ import type { GridCoreNew } from './widget'; export type Options = & WidgetOptions & dataController.Options + & pager.Options & columnsController.Options & toolbar.Options; export const defaultOptions = { ...dataController.defaultOptions, ...columnsController.defaultOptions, + ...pager.defaultOptions, } satisfies Options; // TODO: separate by modules diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/pager/__snapshots__/view.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/pager/__snapshots__/view.test.ts.snap new file mode 100644 index 000000000000..fc8eb1ebb701 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/pager/__snapshots__/view.test.ts.snap @@ -0,0 +1,537 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Applying options when allowedPageSizes = 'auto' calculates pageSizes by pageSize 1`] = ` +
+ +
+`; + +exports[`Applying options when allowedPageSizes with custom values displays custom values 1`] = ` +
+ +
+`; + +exports[`Applying options when changing a visible to 'false' at runtime Pager should be hidden 1`] = ` +
+ +
+`; + +exports[`Applying options when changing a visible to 'true' at runtime Pager should be visible 1`] = ` +
+ +
+`; + +exports[`Applying options when changing an allowedPageSizes to custom values at runtime applies custom values 1`] = ` +
+ +
+`; + +exports[`Applying options when visible = 'auto' and pageCount <= 1 Pager should be hidden 1`] = ` +
+ +
+`; + +exports[`Applying options when visible = 'auto' and pageCount > 1 Pager should be visible 1`] = ` +
+ +
+`; + +exports[`Applying options when visible = 'false' Pager should be hidden 1`] = ` +
+ +
+`; + +exports[`Applying options when visible = 'true' Pager should be visible 1`] = ` +
+ +
+`; + +exports[`render PagerView with options 1`] = ` +
+ +
+`; + +exports[`render empty PagerView 1`] = ` +
+ +`; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/pager/index.ts b/packages/devextreme/js/__internal/grids/new/grid_core/pager/index.ts new file mode 100644 index 000000000000..38129b24f9e3 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/pager/index.ts @@ -0,0 +1,2 @@ +export { defaultOptions, type Options } from './options'; +export { PagerView as View } from './view'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/pager/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/pager/options.ts new file mode 100644 index 000000000000..2c6a75b63dd4 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/pager/options.ts @@ -0,0 +1,27 @@ +import type { Mode } from '@js/common'; +import messageLocalization from '@js/localization/message'; +import type { PagerBase } from '@js/ui/pagination'; + +export type PageSize = number | 'all'; + +export type PageSizes = PageSize[] | Mode; + +export type PagerVisible = boolean | Mode; + +export interface PagerOptions extends PagerBase { + allowedPageSizes?: PageSizes; + visible?: PagerVisible; +} + +export interface Options { + pager?: PagerOptions; +} + +export const defaultOptions = { + pager: { + visible: 'auto', + showPageSizeSelector: false, + allowedPageSizes: 'auto', + label: messageLocalization.format('dxPager-ariaLabel'), + }, +} satisfies Options; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/pager/pager.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/pager/pager.tsx new file mode 100644 index 000000000000..9cdda688637d --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/pager/pager.tsx @@ -0,0 +1,12 @@ +import type { PagerBase } from '@js/ui/pagination'; +import type { InfernoNode } from 'inferno'; + +import { Pager } from '../inferno_wrappers/pager'; + +export type PagerProps = PagerBase & { visible: boolean }; + +export function PagerView(props: PagerProps): InfernoNode { + return ( + props.visible && + ); +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/pager/utils.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/pager/utils.test.ts new file mode 100644 index 000000000000..4073777fed42 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/pager/utils.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from '@jest/globals'; + +import { calculatePageSizes, isVisible } from './utils'; + +describe('calculatePageSizes', () => { + describe('when pageSizesConfig = \'auto\'', () => { + it('calculates pageSizes by pageSize', () => { + expect(calculatePageSizes(undefined, 'auto', 6)).toEqual([3, 6, 12]); + }); + }); + + describe('when pageSizesConfig with custom values', () => { + it('return custom values', () => { + expect(calculatePageSizes(undefined, [4, 10, 20], 6)).toEqual([4, 10, 20]); + }); + }); + + describe('when there is an initial value of pageSizes and pageSizesConfig = \'auto\'', () => { + it('return initial values', () => { + expect(calculatePageSizes([3, 6, 12], 'auto', 12)).toEqual([3, 6, 12]); + }); + }); +}); + +describe('isVisible', () => { + describe('when visibleConfig = true', () => { + it('visible should be equal to true', () => { + expect(isVisible(true, 1)).toBe(true); + }); + }); + + describe('when visibleConfig = false', () => { + it('visible should be equal to false', () => { + expect(isVisible(false, 2)).toBe(false); + }); + }); + + describe('when visibleConfig = \'auto\' and pageCount = 1', () => { + it('visible should be equal to false', () => { + expect(isVisible('auto', 1)).toBe(false); + }); + }); + + describe('when visibleConfig = \'auto\' and pageCount > 1', () => { + it('visible should be equal to true', () => { + expect(isVisible('auto', 2)).toBe(true); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/pager/utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/pager/utils.ts new file mode 100644 index 000000000000..51c877e18d02 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/pager/utils.ts @@ -0,0 +1,30 @@ +import type { PagerVisible, PageSize, PageSizes } from './options'; + +// TODO: Need to fix case with runtime changes the allowedPageSizes property to 'auto' +export function calculatePageSizes( + allowedPageSizes: PageSize[] | undefined, + pageSizesConfig: PageSizes, + pageSize: number, +): PageSizes { + if (Array.isArray(pageSizesConfig)) { + return pageSizesConfig; + } + if (Array.isArray(allowedPageSizes) && allowedPageSizes.includes(pageSize)) { + return allowedPageSizes; + } + if (pageSizesConfig && pageSize > 1) { + return [Math.floor(pageSize / 2), pageSize, pageSize * 2]; + } + + return []; +} + +export function isVisible( + visibleConfig: PagerVisible, + pageCount: number, +): boolean { + if (visibleConfig === 'auto') { + return pageCount > 1; + } + return visibleConfig; +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.test.ts new file mode 100644 index 000000000000..d4977f2f12c4 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.test.ts @@ -0,0 +1,213 @@ +/* eslint-disable spellcheck/spell-checker */ +/* eslint-disable @typescript-eslint/dot-notation */ +import { describe, expect, it } from '@jest/globals'; + +import { DataController } from '../data_controller/data_controller'; +import type { Options } from '../options'; +import { OptionsControllerMock } from '../options_controller/options_controller.mock'; +import { PagerView } from './view'; + +const createPagerView = (options?: Options) => { + const rootElement = document.createElement('div'); + const optionsController = new OptionsControllerMock(options ?? { + dataSource: [], + pager: { + visible: true, + }, + }); + + const dataController = new DataController(optionsController); + const pager = new PagerView(dataController, optionsController); + + pager.render(rootElement); + + return { + rootElement, + optionsController, + }; +}; + +describe('render', () => { + it('empty PagerView', () => { + const { rootElement } = createPagerView(); + + expect(rootElement).toMatchSnapshot(); + }); + + it('PagerView with options', () => { + const { rootElement } = createPagerView({ + dataSource: [...new Array(20)].map((_, index) => ({ field: `test_${index}` })), + paging: { + pageIndex: 2, + }, + pager: { + showPageSizeSelector: true, + }, + }); + + expect(rootElement).toMatchSnapshot(); + }); +}); + +describe('Applying options', () => { + describe('when visible = \'auto\' and pageCount <= 1', () => { + it('Pager should be hidden', () => { + const { rootElement } = createPagerView({ + dataSource: [...new Array(4)].map((_, index) => ({ field: `test_${index}` })), + paging: { + pageIndex: 6, + }, + pager: { + visible: 'auto', + showPageSizeSelector: true, + }, + }); + + expect(rootElement).toMatchSnapshot(); + }); + }); + + describe('when visible = \'auto\' and pageCount > 1', () => { + it('Pager should be visible', () => { + const { rootElement } = createPagerView({ + dataSource: [...new Array(20)].map((_, index) => ({ field: `test_${index}` })), + paging: { + pageIndex: 6, + }, + pager: { + visible: 'auto', + showPageSizeSelector: true, + }, + }); + + expect(rootElement).toMatchSnapshot(); + }); + }); + + describe('when visible = \'true\'', () => { + it('Pager should be visible', () => { + const { rootElement } = createPagerView({ + dataSource: [...new Array(20)].map((_, index) => ({ field: `test_${index}` })), + paging: { + pageIndex: 6, + }, + pager: { + visible: true, + showPageSizeSelector: true, + }, + }); + + expect(rootElement).toMatchSnapshot(); + }); + }); + + describe('when visible = \'false\'', () => { + it('Pager should be hidden', () => { + const { rootElement } = createPagerView({ + dataSource: [...new Array(20)].map((_, index) => ({ field: `test_${index}` })), + paging: { + pageIndex: 6, + }, + pager: { + visible: false, + showPageSizeSelector: true, + }, + }); + + expect(rootElement).toMatchSnapshot(); + }); + }); + + describe('when changing a visible to \'false\' at runtime', () => { + it('Pager should be hidden', () => { + const { rootElement, optionsController } = createPagerView({ + dataSource: [...new Array(4)].map((_, index) => ({ field: `test_${index}` })), + paging: { + pageIndex: 6, + }, + pager: { + visible: true, + showPageSizeSelector: true, + }, + }); + + optionsController.option('pager.visible', false); + + expect(rootElement).toMatchSnapshot(); + }); + }); + + describe('when changing a visible to \'true\' at runtime', () => { + it('Pager should be visible', () => { + const { rootElement, optionsController } = createPagerView({ + dataSource: [...new Array(4)].map((_, index) => ({ field: `test_${index}` })), + paging: { + pageIndex: 6, + }, + pager: { + visible: false, + showPageSizeSelector: true, + }, + }); + + optionsController.option('pager.visible', true); + + expect(rootElement).toMatchSnapshot(); + }); + }); + + describe('when allowedPageSizes = \'auto\'', () => { + it('calculates pageSizes by pageSize', () => { + const { rootElement } = createPagerView({ + dataSource: [...new Array(4)].map((_, index) => ({ field: `test_${index}` })), + paging: { + pageIndex: 6, + }, + pager: { + visible: true, + allowedPageSizes: 'auto', + showPageSizeSelector: true, + }, + }); + + expect(rootElement).toMatchSnapshot(); + }); + }); + + describe('when allowedPageSizes with custom values', () => { + it('displays custom values', () => { + const { rootElement } = createPagerView({ + dataSource: [...new Array(20)].map((_, index) => ({ field: `test_${index}` })), + paging: { + pageIndex: 6, + }, + pager: { + visible: 'auto', + allowedPageSizes: [4, 10, 20], + showPageSizeSelector: true, + }, + }); + + expect(rootElement).toMatchSnapshot(); + }); + }); + + describe('when changing an allowedPageSizes to custom values at runtime', () => { + it('applies custom values', () => { + const { rootElement, optionsController } = createPagerView({ + dataSource: [...new Array(20)].map((_, index) => ({ field: `test_${index}` })), + paging: { + pageIndex: 6, + }, + pager: { + allowedPageSizes: 'auto', + showPageSizeSelector: true, + }, + }); + + optionsController.option('pager.allowedPageSizes', [4, 10, 20]); + + expect(rootElement).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.tsx new file mode 100644 index 000000000000..28f1ec5568da --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.tsx @@ -0,0 +1,63 @@ +/* eslint-disable spellcheck/spell-checker */ +import type { SubsGets } from '@ts/core/reactive/index'; +import { combined, computed } from '@ts/core/reactive/index'; + +import { View } from '../core/view'; +import { DataController } from '../data_controller/index'; +import { OptionsController } from '../options_controller/options_controller'; +import type { PagerProps } from './pager'; +import { PagerView as Pager } from './pager'; +import { calculatePageSizes, isVisible } from './utils'; + +export class PagerView extends View { + protected override component = Pager; + + public static dependencies = [DataController, OptionsController] as const; + + private readonly pageSizesConfig = this.options.oneWay('pager.allowedPageSizes'); + + private readonly allowedPageSizes = computed( + (pageSizesConfig, pageSize) => calculatePageSizes( + this.allowedPageSizes?.unreactive_get(), + pageSizesConfig, + pageSize, + ), + [this.pageSizesConfig, this.dataController.pageSize], + ); + + private readonly visibleConfig = this.options.oneWay('pager.visible'); + + private readonly visible = computed( + (visibleConfig, pageCount) => isVisible(visibleConfig, pageCount), + [this.visibleConfig, this.dataController.pageCount], + ); + + constructor( + private readonly dataController: DataController, + private readonly options: OptionsController, + ) { + super(); + } + + protected override getProps(): SubsGets { + return combined({ + itemCount: this.dataController.totalCount, + allowedPageSizes: this.allowedPageSizes, + visible: this.visible, + pageIndex: computed( + (pageIndex) => pageIndex + 1, + [this.dataController.pageIndex], + ), + pageIndexChanged: (value): void => this.dataController.pageIndex.update(value - 1), + pageSize: this.dataController.pageSize, + pageSizeChanged: (value): void => this.dataController.pageSize.update(value), + isGridCompatibility: false, + pageCount: this.dataController.pageCount, + showPageSizeSelector: this.options.oneWay('pager.showPageSizeSelector'), + _skipValidation: true, + tabIndex: 0, + showInfo: this.options.oneWay('pager.showInfo'), + showNavigationButtons: this.options.oneWay('pager.showNavigationButtons'), + }); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts index 3864b725aa1d..5f68ef5c9b92 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts @@ -13,6 +13,7 @@ import * as ColumnsControllerModule from './columns_controller/index'; import * as DataControllerModule from './data_controller/index'; import { MainView } from './main_view'; import { defaultOptions, defaultOptionsRules, type Options } from './options'; +import { PagerView } from './pager/view'; import { ToolbarController } from './toolbar/controller'; import { ToolbarView } from './toolbar/view'; @@ -27,6 +28,8 @@ export class GridCoreNewBase< protected columnsController!: ColumnsControllerModule.ColumnsController; + private pagerView!: PagerView; + private toolbarController!: ToolbarController; private toolbarView!: ToolbarView; @@ -37,6 +40,7 @@ export class GridCoreNewBase< this.diContext.register(ColumnsControllerModule.ColumnsController); this.diContext.register(ToolbarController); this.diContext.register(ToolbarView); + this.diContext.register(PagerView); } protected _initDIContext(): void { @@ -44,6 +48,7 @@ export class GridCoreNewBase< this.columnsController = this.diContext.get(ColumnsControllerModule.ColumnsController); this.toolbarController = this.diContext.get(ToolbarController); this.toolbarView = this.diContext.get(ToolbarView); + this.pagerView = this.diContext.get(PagerView); } protected _init(): void {