{
render(): JSX.Element {
this.cardRefs = new Array(this.props.items.length).fill(undefined).map(() => createRef());
+ const className = combineClasses({
+ [CLASSES.content]: true,
+ [CLASSES.grid]: true,
+ [CLASSES.selectCheckBoxesHidden]: !!this.props.needToHiddenCheckBoxes,
+ });
return (
this.keyboardController.onKeyDown(e)}
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/types.ts b/packages/devextreme/js/__internal/grids/new/card_view/content_view/types.ts
new file mode 100644
index 000000000000..a871461422f9
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/types.ts
@@ -0,0 +1,12 @@
+import type { DataRow } from '@ts/grids/new/grid_core/columns_controller/types';
+
+export interface SelectCardOptions {
+ control?: boolean;
+ shift?: boolean;
+ needToUpdateCheckboxes?: boolean;
+}
+
+export interface CardHoldEvent {
+ event?: MouseEvent;
+ row: DataRow;
+}
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx
index 8829c4a6619c..e88dacf2ee08 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx
+++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx
@@ -4,11 +4,13 @@ import { compileGetter } from '@js/core/utils/data';
import { isDefined } from '@js/core/utils/type';
import { combined, computed, state } from '@ts/core/reactive/index';
import type { OptionsController } from '@ts/grids/new/card_view/options_controller';
+import type { DataRow } from '@ts/grids/new/grid_core/columns_controller/types';
import { ContentView as ContentViewBase } from '../../grid_core/content_view/view';
import type { DataObject } from '../../grid_core/data_controller/types';
import type { ContentViewProps } from './content_view';
import { ContentView as ContentViewComponent } from './content_view';
+import type { CardHoldEvent, SelectCardOptions } from './types';
import { factors } from './utils';
export class ContentView extends ContentViewBase {
@@ -75,6 +77,7 @@ export class ContentView extends ContentViewBase {
...this.getBaseProps(),
contentProps: combined({
items: this.itemsController.items,
+ needToHiddenCheckBoxes: this.selectionController.needToHiddenCheckBoxes,
// items: computed((virtualState) => virtualState.virtualItems, [this.virtualState]),
fieldTemplate: this.options.template('fieldTemplate'),
cardsPerRow: this.cardsPerRow,
@@ -82,6 +85,9 @@ export class ContentView extends ContentViewBase {
cardProps: combined({
minWidth: this.cardMinWidth,
maxWidth: this.options.oneWay('cardMaxWidth'),
+ isCheckBoxesRendered: this.selectionController.isCheckBoxesRendered,
+ allowSelectOnClick: this.selectionController.allowSelectOnClick,
+ onHold: this.onCardHold.bind(this),
onClick: this.options.action('onCardClick'),
onDblClick: this.options.action('onCardDblClick'),
onHoverChanged: this.options.action('onCardHoverChanged'),
@@ -107,6 +113,7 @@ export class ContentView extends ContentViewBase {
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
toolbar: this.options.oneWay('cardHeader.items') as any,
+ selectCard: this.selectCard.bind(this),
}),
}),
virtualScrollingProps: combined({
@@ -127,4 +134,16 @@ export class ContentView extends ContentViewBase {
// @ts-expect-error
return compileGetter(expr);
}
+
+ private selectCard(row: DataRow, options: SelectCardOptions) {
+ if (options.needToUpdateCheckboxes) {
+ this.selectionController.updateSelectionCheckBoxesVisible(true);
+ }
+
+ this.selectionController.changeCardSelection(row.index, options);
+ }
+
+ private onCardHold(e: CardHoldEvent) {
+ this.selectionController.processLongTap(e.row);
+ }
}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx
index 388830a55fea..2c353d7f1944 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx
@@ -7,6 +7,7 @@ import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/co
import { View } from '@ts/grids/new/grid_core/core/view';
import { DataController } from '@ts/grids/new/grid_core/data_controller/index';
import { ErrorController } from '@ts/grids/new/grid_core/error_controller/error_controller';
+import { SelectionController } from '@ts/grids/new/grid_core/selection/controller';
import { createRef } from 'inferno';
import { ItemsController } from '../items_controller/items_controller';
@@ -33,6 +34,7 @@ export abstract class ContentView extends View {
OptionsController,
ErrorController,
ColumnsController,
+ SelectionController,
ItemsController,
] as const;
@@ -41,6 +43,7 @@ export abstract class ContentView extends View {
protected readonly options: OptionsController,
protected readonly errorController: ErrorController,
protected readonly columnsController: ColumnsController,
+ protected readonly selectionController: SelectionController,
protected readonly itemsController: ItemsController,
) {
super();
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts
index 7a578bb7438a..b14e85fefd25 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts
@@ -79,6 +79,8 @@ export class DataController {
[this.totalCount, this.pageSize],
);
+ public readonly isLoaded = state(false);
+
private readonly normalizedRemoteOptions = computed(
(remoteOperations, dataSource) => {
const store = dataSource.store();
@@ -102,6 +104,7 @@ export class DataController {
effect(
(dataSource) => {
const changedCallback = (e?): void => {
+ this.isLoaded.update(true);
this.onChanged(dataSource, e);
};
const loadingChangedCallback = (): void => {
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap
index c21034559638..e37de819196e 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap
@@ -82,6 +82,7 @@ exports[`ItemsController createDataRow should process data object to data row us
"id": 1,
},
"index": 0,
+ "isSelected": false,
"key": 1,
}
`;
@@ -168,6 +169,65 @@ exports[`ItemsController createDataRow should process data object to data row us
"id": 1,
},
"index": 0,
+ "isSelected": true,
"key": 1,
}
`;
+
+exports[`ItemsController setSelectionState should update the select state of the item 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "a": "my a value",
+ "id": 1,
+ },
+ ],
+ [],
+ [
+ 1,
+ ],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "a": "my a value",
+ "id": 1,
+ },
+ "index": 0,
+ "isSelected": true,
+ "key": 1,
+ },
+ ],
+}
+`;
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.test.ts
index d1c56d77af3b..34c6a954dcc5 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.test.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.test.ts
@@ -12,7 +12,6 @@ import { ItemsController } from './items_controller';
const setup = (config: Options = {}) => {
const options = new OptionsControllerMock(config);
-
const columnsController = new ColumnsController(options);
const filterController = new FilterController(options);
const sortingController = new SortingController(options, columnsController);
@@ -62,8 +61,21 @@ describe('ItemsController', () => {
});
const columns = columnsController.columns.unreactive_get();
- const dataRow = itemsController.createDataRow(dataObject, columns, 0);
+ const dataRow = itemsController.createDataRow(dataObject, columns, 0, [1]);
expect(dataRow).toMatchSnapshot();
});
});
+
+ describe('setSelectionState', () => {
+ it('should update the select state of the item', () => {
+ const { itemsController } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, a: 'my a value' }],
+ });
+
+ itemsController.setSelectionState([1]);
+
+ expect(itemsController.items).toMatchSnapshot();
+ });
+ });
});
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts
index 49b75d741bf7..fd8dea5687de 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts
@@ -1,13 +1,16 @@
+import { equalByValue } from '@js/core/utils/common';
import formatHelper from '@js/format_helper';
-import { computed } from '@ts/core/reactive/index';
+import { computed, state } from '@ts/core/reactive';
import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/columns_controller';
import { DataController } from '@ts/grids/new/grid_core/data_controller/data_controller';
import { SearchController } from '@ts/grids/new/grid_core/search';
import type { Column, DataRow } from '../columns_controller/types';
-import type { DataObject } from '../data_controller/types';
+import type { DataObject, Key } from '../data_controller/types';
export class ItemsController {
+ private readonly selectedCardKeys = state([]);
+
public static dependencies = [
DataController,
ColumnsController,
@@ -18,6 +21,7 @@ export class ItemsController {
(
dataItems,
columns: Column[],
+ selectedCardKeys,
// NOTE: We should trigger computed by search options change
// But all work with these options encapsulated in SearchHighlightTextProcessor
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -27,11 +31,13 @@ export class ItemsController {
item,
columns,
itemIndex,
+ selectedCardKeys,
),
),
[
this.dataController.items,
this.columnsController.visibleColumns,
+ this.selectedCardKeys,
this.searchController.highlightTextOptions,
],
);
@@ -42,10 +48,15 @@ export class ItemsController {
private readonly searchController: SearchController,
) {}
+ public setSelectionState(keys: Key[]): void {
+ this.selectedCardKeys.update(keys);
+ }
+
public createDataRow(
data: DataObject,
columns: Column[],
itemIndex: number,
+ selectedCardKeys?: Key[],
): DataRow {
const itemKey = this.dataController.getDataKey(data);
@@ -72,7 +83,15 @@ export class ItemsController {
}),
key: itemKey,
index: itemIndex,
+ isSelected: !!selectedCardKeys?.includes(itemKey),
data,
};
}
+
+ public getRowByKey(key: Key): DataRow | undefined {
+ // eslint-disable-next-line spellcheck/spell-checker
+ const items = this.items.unreactive_get();
+
+ return items.find((item) => equalByValue(item.key, key));
+ }
}
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 2c6c89a440e6..494f64990f9d 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/options.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/options.ts
@@ -13,6 +13,7 @@ import { filterPanel } from './filtering/index';
import * as pager from './pager/index';
import * as searchPanel from './search/index';
import type { SearchProperties } from './search/types';
+import * as selection from './selection/index';
import * as sortingController from './sorting_controller/index';
import type * as toolbar from './toolbar/index';
import type { GridCoreNew } from './widget';
@@ -33,6 +34,7 @@ export type Options =
& contentView.Options
& editing.Options
& searchPanel.Options
+ & selection.Options
// TODO: Remove this mock search options during search implementation
& SearchProperties
& columnChooser.Options
@@ -51,6 +53,7 @@ export const defaultOptions = {
...editing.defaultOptions,
...searchPanel.defaultOptions,
...columnChooser.defaultOptions,
+ ...selection.defaultOptions,
searchText: '',
} satisfies Options;
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/controller.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/controller.test.ts.snap
new file mode 100644
index 000000000000..875389acbdd4
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/controller.test.ts.snap
@@ -0,0 +1,571 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SelectionController changeCardSelection when the control arg equal to false should update the select state of the item 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [],
+ [
+ 1,
+ ],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": true,
+ "key": 1,
+ },
+ ],
+}
+`;
+
+exports[`SelectionController changeCardSelection when the control arg equal to true should update the select state of the item 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [],
+ [],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": false,
+ "key": 1,
+ },
+ ],
+}
+`;
+
+exports[`SelectionController deselectCards should deselect item 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [],
+ [],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": false,
+ "key": 1,
+ },
+ ],
+}
+`;
+
+exports[`SelectionController deselectCardsByIndexes should deselect item 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [],
+ [],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": false,
+ "key": 1,
+ },
+ ],
+}
+`;
+
+exports[`SelectionController public methods changeCardSelection when the control arg equal to false should update the select state of the item 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [],
+ [
+ 1,
+ ],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": true,
+ "key": 1,
+ },
+ ],
+}
+`;
+
+exports[`SelectionController public methods changeCardSelection when the control arg equal to true should update the select state of the item 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [],
+ [],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": false,
+ "key": 1,
+ },
+ ],
+}
+`;
+
+exports[`SelectionController public methods deselectCards should deselect item 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [],
+ [],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": false,
+ "key": 1,
+ },
+ ],
+}
+`;
+
+exports[`SelectionController public methods selectCards should select item 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [],
+ [
+ 1,
+ ],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": true,
+ "key": 1,
+ },
+ ],
+}
+`;
+
+exports[`SelectionController selectCards should select item 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [],
+ [
+ 1,
+ ],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": true,
+ "key": 1,
+ },
+ ],
+}
+`;
+
+exports[`SelectionController selectCardsByIndexes should select item 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [],
+ [
+ 1,
+ ],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": true,
+ "key": 1,
+ },
+ ],
+}
+`;
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/options.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/options.test.ts.snap
new file mode 100644
index 000000000000..450dc1044be7
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/options.test.ts.snap
@@ -0,0 +1,204 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Options selectedCardKeys when given should set the select state of the item 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [],
+ [
+ 1,
+ ],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": true,
+ "key": 1,
+ },
+ ],
+}
+`;
+
+exports[`Options selection allowSelectAll when it is false and selection mode is 'multiple' selection should not work 1`] = `[]`;
+
+exports[`Options selection allowSelectAll when it is true and selection mode is 'multiple' selection should not work 1`] = `
+[
+ {
+ "locateInMenu": "auto",
+ "location": "before",
+ "name": "selectAllButton",
+ "options": {
+ "disabled": false,
+ "icon": "selectall",
+ "onClick": [Function],
+ "text": "Select All",
+ },
+ "widget": "dxButton",
+ },
+ {
+ "locateInMenu": "auto",
+ "location": "before",
+ "name": "clearSelectionButton",
+ "options": {
+ "disabled": true,
+ "icon": "close",
+ "onClick": [Function],
+ "text": "Clear selection",
+ },
+ "widget": "dxButton",
+ },
+]
+`;
+
+exports[`Options selection allowSelectAll when it is true and selection mode isn't 'multiple' selection should not work 1`] = `[]`;
+
+exports[`Options selection mode when it is 'none' and the selectedCardKeys is specified selection should not apply 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [],
+ [],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": false,
+ "key": 1,
+ },
+ ],
+}
+`;
+
+exports[`Options selection mode when it is 'none' selection should not work 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [],
+ [],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": false,
+ "key": 1,
+ },
+ ],
+}
+`;
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/const.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/const.ts
new file mode 100644
index 000000000000..0664d40bec41
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/const.ts
@@ -0,0 +1,17 @@
+export enum SelectionMode {
+ Multiple = 'multiple',
+
+ Single = 'single',
+
+ None = 'none',
+}
+
+export enum ShowCheckBoxesMode {
+ Always = 'always',
+
+ OnClick = 'onClick',
+
+ OnLongTap = 'onLongTap',
+
+ None = 'none',
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.test.ts
new file mode 100644
index 000000000000..fbed51d7649f
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.test.ts
@@ -0,0 +1,847 @@
+/* eslint-disable spellcheck/spell-checker */
+import {
+ describe, expect, it, jest,
+} from '@jest/globals';
+
+import { ColumnsController } from '../columns_controller/columns_controller';
+import { DataController } from '../data_controller';
+import { FilterController } from '../filtering/filter_controller';
+import { ItemsController } from '../items_controller/items_controller';
+import type { Options } from '../options';
+import { OptionsControllerMock } from '../options_controller/options_controller.mock';
+import { SearchController } from '../search/controller';
+import { SortingController } from '../sorting_controller/sorting_controller';
+import { ToolbarController } from '../toolbar/controller';
+import { SelectionController } from './controller';
+
+const setup = (config: Options = {}) => {
+ const optionsController = new OptionsControllerMock({
+ selection: {
+ mode: 'single',
+ },
+ selectedCardKeys: [],
+ ...config,
+ });
+
+ const filterController = new FilterController(optionsController);
+ const columnsController = new ColumnsController(optionsController);
+ const sortingController = new SortingController(optionsController, columnsController);
+
+ const dataController = new DataController(optionsController, sortingController, filterController);
+
+ const searchController = new SearchController(optionsController);
+ const itemsController = new ItemsController(dataController, columnsController, searchController);
+ const toolbarController = new ToolbarController(optionsController);
+
+ const selectionController = new SelectionController(
+ optionsController,
+ dataController,
+ itemsController,
+ toolbarController,
+ );
+
+ return {
+ optionsController,
+ selectionController,
+ itemsController,
+ };
+};
+
+describe('SelectionController', () => {
+ // Public methods
+
+ describe('selectCards', () => {
+ it('should select item', () => {
+ const {
+ selectionController,
+ itemsController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ });
+
+ selectionController.selectCards([1]);
+ expect(itemsController.items).toMatchSnapshot();
+ });
+ });
+
+ describe('deselectCards', () => {
+ it('should deselect item', () => {
+ const {
+ selectionController,
+ itemsController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selectedCardKeys: [1],
+ });
+
+ selectionController.deselectCards([1]);
+ expect(itemsController.items).toMatchSnapshot();
+ });
+ });
+
+ describe('selectCardsByIndexes', () => {
+ it('should select item', () => {
+ const {
+ selectionController,
+ itemsController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ });
+
+ selectionController.selectCardsByIndexes([0]);
+ expect(itemsController.items).toMatchSnapshot();
+ });
+ });
+
+ describe('deselectCardsByIndexes', () => {
+ it('should deselect item', () => {
+ const {
+ selectionController,
+ itemsController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selectedCardKeys: [1],
+ });
+
+ selectionController.deselectCardsByIndexes([0]);
+ expect(itemsController.items).toMatchSnapshot();
+ });
+ });
+
+ describe('changeCardSelection', () => {
+ describe('when the control arg equal to false', () => {
+ it('should update the select state of the item', () => {
+ const {
+ selectionController,
+ itemsController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ });
+
+ selectionController.changeCardSelection(0, { control: false });
+ expect(itemsController.items).toMatchSnapshot();
+ });
+ });
+
+ describe('when the control arg equal to true', () => {
+ it('should update the select state of the item', () => {
+ const {
+ selectionController,
+ itemsController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selectedCardKeys: [1],
+ });
+
+ selectionController.changeCardSelection(0, { control: true });
+ expect(itemsController.items).toMatchSnapshot();
+ });
+ });
+
+ describe('when item is selected and multiple selection enabled', () => {
+ it('should update the select state of the item', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selectedCardKeys: [1],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ },
+ });
+
+ selectionController.changeCardSelection(0);
+ expect(selectionController.getSelectedCardKeys()).toEqual([]);
+ });
+ });
+ });
+
+ describe('isCardSelected', () => {
+ describe('when the selectedCardKeys is specified', () => {
+ it('should return true', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selectedCardKeys: [1],
+ });
+
+ expect(selectionController.isCardSelected(1)).toBe(true);
+ });
+ });
+
+ describe('when the selectedCardKeys isn\'t specified', () => {
+ it('should return false', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ });
+
+ expect(selectionController.isCardSelected(1)).toBe(false);
+ });
+ });
+ });
+
+ describe('getSelectedCardKeys', () => {
+ it('should return the selected card keys', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selectedCardKeys: [1],
+ });
+
+ expect(selectionController.getSelectedCardKeys()).toEqual([1]);
+ });
+ });
+
+ describe('getSelectedCards', () => {
+ it('should return the selected card keys', () => {
+ const {
+ selectionController,
+ itemsController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selectedCardKeys: [1],
+ });
+
+ expect(selectionController.getSelectedCards())
+ .toEqual(itemsController.items.unreactive_get());
+ });
+ });
+
+ describe('clearSelection', () => {
+ it('should clear the selection', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selectedCardKeys: [1],
+ });
+
+ selectionController.clearSelection();
+ expect(selectionController.getSelectedCardKeys().length).toBe(0);
+ });
+ });
+
+ describe('updateSelectionCheckBoxesVisible', () => {
+ describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onClick\'', () => {
+ it('should show the selection checkboxes', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ },
+ });
+
+ selectionController.updateSelectionCheckBoxesVisible(true);
+ expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(true);
+ });
+
+ it('should hide the selection checkboxes', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ },
+ });
+
+ selectionController.updateSelectionCheckBoxesVisible(false);
+ expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(false);
+ });
+ });
+ });
+
+ describe('processLongTap', () => {
+ describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onLongTap\'', () => {
+ it('should render the selection checkbox', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onLongTap',
+ },
+ });
+
+ // @ts-expect-error
+ selectionController.processLongTap({ index: 0 });
+ expect(selectionController.isCheckBoxesRendered.unreactive_get()).toBe(true);
+ });
+ });
+
+ describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onClick\'', () => {
+ it('should show the selection checkbox', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ },
+ });
+
+ // @ts-expect-error
+ selectionController.processLongTap({ index: 0 });
+ expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(true);
+ });
+ });
+
+ describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'none\'', () => {
+ it('should select a first item', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'none',
+ },
+ });
+
+ // @ts-expect-error
+ selectionController.processLongTap({ index: 0 });
+ expect(selectionController.getSelectedCardKeys()).toEqual([1]);
+ });
+ });
+
+ describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'none\'', () => {
+ it('should not select a first item', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ },
+ });
+
+ // @ts-expect-error
+ selectionController.processLongTap({ index: 0 });
+ expect(selectionController.getSelectedCardKeys()).toEqual([]);
+ });
+ });
+ });
+
+ // Public properties
+ describe('isCheckBoxesRendered', () => {
+ describe('when the selection mode is equal to \'none\'', () => {
+ it('should return false', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'none',
+ },
+ });
+
+ expect(selectionController.isCheckBoxesRendered.unreactive_get()).toBe(false);
+ });
+ });
+
+ describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'always\'', () => {
+ it('should return true', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ },
+ });
+
+ expect(selectionController.isCheckBoxesRendered.unreactive_get()).toBe(true);
+ });
+ });
+
+ describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onClick\'', () => {
+ it('should return true', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ },
+ });
+
+ expect(selectionController.isCheckBoxesRendered.unreactive_get()).toBe(true);
+ });
+ });
+
+ describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onLongTap\'', () => {
+ it('should return false', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onLongTap',
+ },
+ });
+
+ expect(selectionController.isCheckBoxesRendered.unreactive_get()).toBe(false);
+ });
+ });
+ });
+
+ describe('isCheckBoxesVisible', () => {
+ describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onClick\'', () => {
+ it('should return false', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ },
+ });
+
+ expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(false);
+ });
+ });
+
+ describe('when selecting one card', () => {
+ it('should return false', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }, { id: 3, value: 'test3' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ },
+ });
+
+ selectionController.selectCards([1]);
+ expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(false);
+ });
+ });
+
+ describe('when selecting two cards', () => {
+ it('should return true', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }, { id: 3, value: 'test3' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ },
+ });
+
+ selectionController.selectCards([1, 2]);
+ expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(true);
+ });
+ });
+
+ describe('when deselecting all cards', () => {
+ it('should return false', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }, { id: 3, value: 'test3' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ },
+ selectedCardKeys: [1, 2],
+ });
+
+ selectionController.deselectCards([1, 2]);
+ expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(false);
+ });
+ });
+ });
+
+ describe('needToHiddenCheckBoxes', () => {
+ describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onClick\'', () => {
+ it('should return true', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ },
+ });
+
+ expect(selectionController.needToHiddenCheckBoxes.unreactive_get()).toBe(true);
+ });
+ });
+
+ describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'always\'', () => {
+ it('should return false', () => {
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ },
+ });
+
+ expect(selectionController.needToHiddenCheckBoxes.unreactive_get()).toBe(false);
+ });
+ });
+ });
+
+ // Events
+
+ describe('onSelectionChanging', () => {
+ describe('when selecting a card', () => {
+ it('should be called', () => {
+ const selectionChangingMockFn = jest.fn();
+ const cardData = { id: 1, value: 'test' };
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [cardData],
+ selection: {
+ mode: 'multiple',
+ },
+ onSelectionChanging: selectionChangingMockFn,
+ });
+
+ selectionController.selectCards([1]);
+
+ expect(selectionChangingMockFn.mock.calls).toHaveLength(1);
+ expect(selectionChangingMockFn.mock.lastCall).toMatchObject([{
+ cancel: false,
+ currentDeselectedCardKeys: [],
+ currentSelectedCardKeys: [1],
+ isDeselectAll: false,
+ isSelectAll: false,
+ selectedCardKeys: [1],
+ selectedCardsData: [cardData],
+ }]);
+ });
+ });
+
+ describe('when deselecting a card', () => {
+ it('should be called', () => {
+ const selectionChangingMockFn = jest.fn();
+ const cardData = { id: 1, value: 'test' };
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [cardData],
+ selection: {
+ mode: 'multiple',
+ },
+ selectedCardKeys: [1],
+ onSelectionChanging: selectionChangingMockFn,
+ });
+
+ selectionController.deselectCards([1]);
+
+ expect(selectionChangingMockFn.mock.calls).toHaveLength(1);
+ expect(selectionChangingMockFn.mock.lastCall).toMatchObject([{
+ cancel: false,
+ currentDeselectedCardKeys: [1],
+ currentSelectedCardKeys: [],
+ isDeselectAll: false,
+ isSelectAll: false,
+ selectedCardKeys: [],
+ selectedCardsData: [],
+ }]);
+ });
+ });
+
+ describe('when selecting all cards', () => {
+ it('should be called', () => {
+ const selectionChangingMockFn = jest.fn();
+ const data = [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }];
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: data,
+ selection: {
+ mode: 'multiple',
+ allowSelectAll: true,
+ },
+ onSelectionChanging: selectionChangingMockFn,
+ });
+
+ selectionController.selectAll();
+
+ expect(selectionChangingMockFn.mock.calls).toHaveLength(1);
+ expect(selectionChangingMockFn.mock.lastCall).toMatchObject([{
+ cancel: false,
+ currentDeselectedCardKeys: [],
+ currentSelectedCardKeys: [1, 2],
+ isDeselectAll: false,
+ isSelectAll: false,
+ selectedCardKeys: [1, 2],
+ selectedCardsData: data,
+ }]);
+ });
+ });
+
+ describe('when deselecting all cards', () => {
+ it('should be called', () => {
+ const selectionChangingMockFn = jest.fn();
+ const data = [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }];
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: data,
+ selection: {
+ mode: 'multiple',
+ allowSelectAll: true,
+ },
+ selectedCardKeys: [1, 2],
+ onSelectionChanging: selectionChangingMockFn,
+ });
+
+ selectionController.deselectAll();
+
+ expect(selectionChangingMockFn.mock.calls).toHaveLength(1);
+ expect(selectionChangingMockFn.mock.lastCall).toMatchObject([{
+ cancel: false,
+ currentDeselectedCardKeys: [1, 2],
+ currentSelectedCardKeys: [],
+ isDeselectAll: false,
+ isSelectAll: false,
+ selectedCardKeys: [],
+ selectedCardsData: [],
+ }]);
+ });
+ });
+
+ describe('when a cancel arg is specified as true', () => {
+ it('should be called', () => {
+ const selectionChangingMockFn = jest.fn((e: any) => { e.cancel = true; });
+ const cardData = { id: 1, value: 'test' };
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [cardData],
+ selection: {
+ mode: 'multiple',
+ },
+ onSelectionChanging: selectionChangingMockFn,
+ });
+
+ selectionController.selectCards([1]);
+
+ expect(selectionChangingMockFn.mock.calls).toHaveLength(1);
+ expect(selectionChangingMockFn.mock.lastCall).toMatchObject([{
+ cancel: true,
+ currentDeselectedCardKeys: [],
+ currentSelectedCardKeys: [1],
+ isDeselectAll: false,
+ isSelectAll: false,
+ selectedCardKeys: [1],
+ selectedCardsData: [cardData],
+ }]);
+ expect(selectionController.getSelectedCardKeys()).toEqual([]);
+ });
+ });
+
+ describe('when a cancel arg is specified as Promise', () => {
+ it('should be called', () => {
+ const cancel = Promise.resolve(true);
+ const selectionChangingMockFn = jest.fn((e: any) => { e.cancel = cancel; });
+ const cardData = { id: 1, value: 'test' };
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [cardData],
+ selection: {
+ mode: 'multiple',
+ },
+ onSelectionChanging: selectionChangingMockFn,
+ });
+
+ selectionController.selectCards([1]);
+
+ expect(selectionChangingMockFn.mock.calls).toHaveLength(1);
+ expect(selectionChangingMockFn.mock.lastCall).toMatchObject([{
+ cancel,
+ currentDeselectedCardKeys: [],
+ currentSelectedCardKeys: [1],
+ isDeselectAll: false,
+ isSelectAll: false,
+ selectedCardKeys: [1],
+ selectedCardsData: [cardData],
+ }]);
+ expect(selectionController.getSelectedCardKeys()).toEqual([]);
+ });
+ });
+ });
+
+ describe('onSelectionChanged', () => {
+ describe('when selecting a card', () => {
+ it('should be called', () => {
+ const selectionChangedMockFn = jest.fn();
+ const cardData = { id: 1, value: 'test' };
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [cardData],
+ selection: {
+ mode: 'multiple',
+ },
+ onSelectionChanged: selectionChangedMockFn,
+ });
+
+ selectionController.selectCards([1]);
+
+ expect(selectionChangedMockFn.mock.calls).toHaveLength(1);
+ expect(selectionChangedMockFn.mock.lastCall).toMatchObject([{
+ currentDeselectedCardKeys: [],
+ currentSelectedCardKeys: [1],
+ isDeselectAll: false,
+ isSelectAll: false,
+ selectedCardKeys: [1],
+ selectedCardsData: [cardData],
+ }]);
+ });
+ });
+
+ describe('when deselecting a card', () => {
+ it('should be called', () => {
+ const selectionChangedMockFn = jest.fn();
+ const cardData = { id: 1, value: 'test' };
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [cardData],
+ selection: {
+ mode: 'multiple',
+ },
+ selectedCardKeys: [1],
+ onSelectionChanged: selectionChangedMockFn,
+ });
+
+ selectionController.deselectCards([1]);
+
+ expect(selectionChangedMockFn.mock.calls).toHaveLength(1);
+ expect(selectionChangedMockFn.mock.lastCall).toMatchObject([{
+ currentDeselectedCardKeys: [1],
+ currentSelectedCardKeys: [],
+ isDeselectAll: false,
+ isSelectAll: false,
+ selectedCardKeys: [],
+ selectedCardsData: [],
+ }]);
+ });
+ });
+
+ describe('when selecting all cards', () => {
+ it('should be called', () => {
+ const selectionChangedMockFn = jest.fn();
+ const data = [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }];
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: data,
+ selection: {
+ mode: 'multiple',
+ allowSelectAll: true,
+ },
+ onSelectionChanged: selectionChangedMockFn,
+ });
+
+ selectionController.selectAll();
+
+ expect(selectionChangedMockFn.mock.calls).toHaveLength(1);
+ expect(selectionChangedMockFn.mock.lastCall).toMatchObject([{
+ currentDeselectedCardKeys: [],
+ currentSelectedCardKeys: [1, 2],
+ isDeselectAll: false,
+ isSelectAll: false,
+ selectedCardKeys: [1, 2],
+ selectedCardsData: data,
+ }]);
+ });
+ });
+
+ describe('when deselecting all cards', () => {
+ it('should be called', () => {
+ const selectionChangedMockFn = jest.fn();
+ const data = [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }];
+ const {
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: data,
+ selection: {
+ mode: 'multiple',
+ allowSelectAll: true,
+ },
+ selectedCardKeys: [1, 2],
+ onSelectionChanged: selectionChangedMockFn,
+ });
+
+ selectionController.deselectAll();
+
+ expect(selectionChangedMockFn.mock.calls).toHaveLength(1);
+ expect(selectionChangedMockFn.mock.lastCall).toMatchObject([{
+ currentDeselectedCardKeys: [1, 2],
+ currentSelectedCardKeys: [],
+ isDeselectAll: false,
+ isSelectAll: false,
+ selectedCardKeys: [],
+ selectedCardsData: [],
+ }]);
+ });
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts
new file mode 100644
index 000000000000..77521ef65c84
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts
@@ -0,0 +1,395 @@
+/* eslint-disable @typescript-eslint/no-unsafe-return */
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable spellcheck/spell-checker */
+import type { DeferredObj } from '@js/core/utils/deferred';
+import messageLocalization from '@js/localization/message';
+import type { SubsGets } from '@ts/core/reactive/index';
+import { computed, effect, state } from '@ts/core/reactive/index';
+import { DataController } from '@ts/grids/new/grid_core/data_controller';
+import { ShowCheckBoxesMode } from '@ts/grids/new/grid_core/selection/const';
+import Selection from '@ts/ui/selection/m_selection';
+
+import type { DataRow } from '../columns_controller/types';
+import type { Key } from '../data_controller/types';
+import { ItemsController } from '../items_controller/items_controller';
+import { OptionsController } from '../options_controller/options_controller';
+import { ToolbarController } from '../toolbar/controller';
+import { SelectionMode } from './const';
+import type {
+ SelectedCardKeys, SelectionEventInfo, SelectionOptions,
+} from './types';
+
+export class SelectionController {
+ public static dependencies = [
+ OptionsController,
+ DataController,
+ ItemsController,
+ ToolbarController,
+ ] as const;
+
+ private readonly selectedCardKeys = this.options.twoWay('selectedCardKeys');
+
+ private readonly selectionOption: SubsGets = this.options.oneWay('selection');
+
+ private readonly selectionHelper: SubsGets;
+
+ private readonly _isCheckBoxesRendered = state(false);
+
+ private readonly onSelectionChanging = this.options.action('onSelectionChanging');
+
+ private readonly onSelectionChanged = this.options.action('onSelectionChanged');
+
+ public readonly isCheckBoxesRendered = computed(
+ (selectionMode, showCheckBoxesMode, _isCheckBoxesRendered) => {
+ if (selectionMode === SelectionMode.Multiple) {
+ switch (showCheckBoxesMode) {
+ case ShowCheckBoxesMode.Always:
+ case ShowCheckBoxesMode.OnClick:
+ return true;
+ case ShowCheckBoxesMode.OnLongTap:
+ return _isCheckBoxesRendered;
+ default:
+ return false;
+ }
+ }
+
+ return false;
+ },
+ [
+ this.options.oneWay('selection.mode'),
+ this.options.oneWay('selection.showCheckBoxesMode'),
+ this._isCheckBoxesRendered,
+ ],
+ );
+
+ public readonly _isCheckBoxesVisible = state(false);
+
+ public readonly isCheckBoxesVisible = computed(
+ (selectionOption, _isCheckBoxesVisible) => {
+ const { mode, showCheckBoxesMode } = selectionOption;
+
+ if (mode === SelectionMode.Multiple) {
+ return showCheckBoxesMode !== ShowCheckBoxesMode.OnClick || _isCheckBoxesVisible;
+ }
+
+ return false;
+ },
+ [
+ this.selectionOption,
+ this._isCheckBoxesVisible,
+ ],
+ );
+
+ public readonly needToHiddenCheckBoxes = computed(
+ (isCheckBoxesVisible, selectionOption) => {
+ const { mode, showCheckBoxesMode } = selectionOption;
+
+ if (mode === SelectionMode.Multiple && showCheckBoxesMode === ShowCheckBoxesMode.OnClick) {
+ return !isCheckBoxesVisible;
+ }
+
+ return false;
+ },
+ [
+ this.isCheckBoxesVisible,
+ this.selectionOption,
+ ],
+ );
+
+ public readonly allowSelectOnClick = computed(
+ (selectionOption) => {
+ const { mode, showCheckBoxesMode } = selectionOption;
+
+ return mode !== SelectionMode.Multiple || showCheckBoxesMode !== ShowCheckBoxesMode.Always;
+ },
+ [this.selectionOption],
+ );
+
+ public readonly needToAddSelectionButtons = computed(
+ (selectionMode, allowSelectAll) => selectionMode === SelectionMode.Multiple && allowSelectAll,
+ [
+ this.options.oneWay('selection.mode'),
+ this.options.oneWay('selection.allowSelectAll'),
+ ],
+ );
+
+ constructor(
+ private readonly options: OptionsController,
+ private readonly dataController: DataController,
+ private readonly itemsController: ItemsController,
+ private readonly toolbarController: ToolbarController,
+ ) {
+ this.selectionHelper = computed(
+ (
+ dataSource,
+ selectionOption,
+ ) => {
+ if (selectionOption.mode === SelectionMode.None) {
+ return undefined;
+ }
+
+ const selectionConfig = this.getSelectionConfig(
+ dataSource,
+ selectionOption,
+ );
+
+ return new Selection(selectionConfig);
+ },
+ [
+ this.dataController.dataSource,
+ this.selectionOption,
+ ],
+ );
+
+ effect((selectedCardKeys, selectionOption) => {
+ if (selectionOption.mode !== SelectionMode.None) {
+ this.itemsController.setSelectionState(selectedCardKeys);
+
+ if (selectedCardKeys.length > 1) {
+ this._isCheckBoxesVisible.update(true);
+ } else if (selectedCardKeys.length === 0) {
+ this._isCheckBoxesVisible.update(false);
+ }
+ }
+ }, [this.selectedCardKeys, this.selectionOption]);
+
+ effect((isLoaded) => {
+ if (isLoaded) {
+ const selectedCardKeys = this.selectedCardKeys.unreactive_get();
+
+ this.selectCards(selectedCardKeys);
+ }
+ }, [this.dataController.isLoaded]);
+
+ effect((selectedCardKeys) => {
+ this.updateSelectionToolbarButtons(selectedCardKeys);
+ }, [this.selectedCardKeys, this.dataController.items]);
+ }
+
+ private getSelectionConfig(dataSource, selectionOption): object {
+ const selectedCardKeys = this.selectedCardKeys.unreactive_get();
+
+ return {
+ selectedKeys: selectedCardKeys,
+ mode: selectionOption.mode,
+ maxFilterLengthInRequest: selectionOption.maxFilterLengthInRequest,
+ ignoreDisabledItems: true,
+ key() {
+ return dataSource.key();
+ },
+ keyOf(item) {
+ return dataSource.store().keyOf(item);
+ },
+ dataFields() {
+ return dataSource.select();
+ },
+ load(options) {
+ return dataSource.store().load(options);
+ },
+ plainItems() {
+ return dataSource.items();
+ },
+ filter() {
+ // TODO Salimov: Need to take combined filter
+ return dataSource.filter();
+ },
+ totalCount: () => dataSource.totalCount(),
+ onSelectionChanging: this.selectionChanging.bind(this),
+ onSelectionChanged: this.selectionChanged.bind(this),
+ };
+ }
+
+ private getSelectionEventArgs(e): SelectionEventInfo {
+ return {
+ currentSelectedCardKeys: [...e.addedItemKeys],
+ currentDeselectedCardKeys: [...e.removedItemKeys],
+ selectedCardKeys: [...e.selectedItemKeys],
+ selectedCardsData: [...e.selectedItems],
+ isSelectAll: false,
+ isDeselectAll: false,
+ };
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private selectionChanging(e: any): void {
+ if (e.addedItemKeys.length || e.removedItemKeys.length) {
+ const onSelectionChanging = this.onSelectionChanging.unreactive_get();
+ const eventArgs = {
+ ...this.getSelectionEventArgs(e),
+ cancel: false,
+ };
+
+ onSelectionChanging?.(eventArgs);
+ e.cancel = eventArgs.cancel;
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private selectionChanged(e: any): void {
+ if (e.addedItemKeys.length || e.removedItemKeys.length) {
+ const onSelectionChanged = this.onSelectionChanged.unreactive_get();
+ const eventArgs = this.getSelectionEventArgs(e);
+
+ this.selectedCardKeys.update([...e.selectedItemKeys]);
+ onSelectionChanged?.(eventArgs);
+ }
+ }
+
+ private isOnePageSelectAll(): boolean {
+ const selectionOption = this.selectionOption.unreactive_get();
+
+ return selectionOption?.selectAllMode === 'page';
+ }
+
+ private isSelectAll(): boolean | undefined {
+ const selectionHelper = this.selectionHelper.unreactive_get();
+
+ return selectionHelper?.getSelectAllState(this.isOnePageSelectAll());
+ }
+
+ private updateSelectionToolbarButtons(
+ selectedCardKeys: SelectedCardKeys,
+ ) {
+ const isSelectAll = this.isSelectAll();
+ const isOnePageSelectAll = this.isOnePageSelectAll();
+
+ this.toolbarController.addDefaultItem({
+ name: 'selectAllButton',
+ widget: 'dxButton',
+ options: {
+ icon: 'selectall',
+ onClick: () => {
+ this.selectAll();
+ },
+ disabled: !!isSelectAll,
+ text: messageLocalization.format('dxCardView-selectAll'),
+ },
+ location: 'before',
+ locateInMenu: 'auto',
+ }, this.needToAddSelectionButtons);
+ this.toolbarController.addDefaultItem({
+ name: 'clearSelectionButton',
+ widget: 'dxButton',
+ options: {
+ icon: 'close',
+ onClick: () => {
+ this.deselectAll();
+ },
+ disabled: isOnePageSelectAll ? isSelectAll === false : selectedCardKeys.length === 0,
+ text: messageLocalization.format('dxCardView-clearSelection'),
+ },
+ location: 'before',
+ locateInMenu: 'auto',
+ }, this.needToAddSelectionButtons);
+ }
+
+ private getItemKeysByIndexes(indexes: number[]): Key[] {
+ const items = this.itemsController.items.unreactive_get();
+
+ return indexes
+ .map((index) => items[index]?.key)
+ .filter((key) => key !== undefined);
+ }
+
+ public changeCardSelection(
+ cardIndex: number,
+ options?: { control?: boolean; shift?: boolean },
+ ): void {
+ const selectionHelper = this.selectionHelper?.unreactive_get();
+ const isCheckBoxesVisible = this.isCheckBoxesVisible.unreactive_get();
+ const keys = options ?? {};
+
+ if (isCheckBoxesVisible) {
+ keys.control = isCheckBoxesVisible;
+ }
+
+ selectionHelper?.changeItemSelection(cardIndex, keys, false);
+ }
+
+ public selectCards(keys: Key[], preserve = false): DeferredObj | undefined {
+ const selectionHelper = this.selectionHelper?.unreactive_get();
+
+ return selectionHelper?.selectedItemKeys(keys, preserve);
+ }
+
+ public selectCardsByIndexes(indexes: number[]): DeferredObj | undefined {
+ const keys = this.getItemKeysByIndexes(indexes);
+
+ return this.selectCards(keys);
+ }
+
+ public deselectCards(keys: Key[]): DeferredObj | undefined {
+ const selectionHelper = this.selectionHelper?.unreactive_get();
+
+ return selectionHelper?.selectedItemKeys(keys, true, true);
+ }
+
+ public deselectCardsByIndexes(indexes: number[]): DeferredObj | undefined {
+ const keys = this.getItemKeysByIndexes(indexes);
+
+ return this.deselectCards(keys);
+ }
+
+ public isCardSelected(key: Key): boolean {
+ const selectedCardKeys = this.selectedCardKeys.unreactive_get();
+
+ return selectedCardKeys.includes(key);
+ }
+
+ public selectAll(): DeferredObj | undefined {
+ const selectionHelper = this.selectionHelper.unreactive_get();
+
+ return selectionHelper?.selectAll(this.isOnePageSelectAll());
+ }
+
+ public deselectAll(): DeferredObj | undefined {
+ const selectionHelper = this.selectionHelper.unreactive_get();
+
+ return selectionHelper?.deselectAll(this.isOnePageSelectAll());
+ }
+
+ public clearSelection(): DeferredObj | undefined {
+ const selectionHelper = this.selectionHelper.unreactive_get();
+
+ return selectionHelper?.clearSelection();
+ }
+
+ public getSelectedCards(): DataRow[] {
+ const selectedCardKey = this.getSelectedCardKeys();
+
+ return selectedCardKey
+ .map((key) => this.itemsController.getRowByKey(key))
+ .filter((item): item is DataRow => !!item);
+ }
+
+ public getSelectedCardKeys(): Key[] {
+ return this.selectedCardKeys.unreactive_get();
+ }
+
+ private toggleSelectionCheckBoxes(): void {
+ const isCheckBoxesRendered = this._isCheckBoxesRendered.unreactive_get();
+
+ this._isCheckBoxesRendered.update(!isCheckBoxesRendered);
+ }
+
+ public updateSelectionCheckBoxesVisible(value: boolean): void {
+ this._isCheckBoxesVisible.update(value);
+ }
+
+ public processLongTap(row: DataRow): void {
+ const { mode, showCheckBoxesMode } = this.selectionOption.unreactive_get();
+
+ if (mode !== SelectionMode.None) {
+ if (showCheckBoxesMode === ShowCheckBoxesMode.OnLongTap) {
+ this.toggleSelectionCheckBoxes();
+ } else {
+ if (showCheckBoxesMode === ShowCheckBoxesMode.OnClick) {
+ this._isCheckBoxesVisible.update(true);
+ }
+ if (showCheckBoxesMode !== ShowCheckBoxesMode.Always) {
+ this.changeCardSelection(row.index, { control: true });
+ }
+ }
+ }
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/index.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/index.ts
new file mode 100644
index 000000000000..afd0cbb3be7d
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/index.ts
@@ -0,0 +1,3 @@
+export { SelectionController as Controller } from './controller';
+export { defaultOptions, type Options } from './options';
+export { PublicMethods } from './public_methods';
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.test.ts
new file mode 100644
index 000000000000..2b763c0f86b7
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.test.ts
@@ -0,0 +1,154 @@
+/* eslint-disable spellcheck/spell-checker */
+import { describe, expect, it } from '@jest/globals';
+
+import { ColumnsController } from '../columns_controller/columns_controller';
+import { DataController } from '../data_controller';
+import { FilterController } from '../filtering/filter_controller';
+import { ItemsController } from '../items_controller/items_controller';
+import type { Options } from '../options';
+import { OptionsControllerMock } from '../options_controller/options_controller.mock';
+import { SearchController } from '../search/controller';
+import { SortingController } from '../sorting_controller/sorting_controller';
+import { ToolbarController } from '../toolbar/controller';
+import { SelectionController } from './controller';
+
+const setup = (config: Options = {}) => {
+ const optionsController = new OptionsControllerMock({
+ selection: {
+ mode: 'single',
+ },
+ ...config,
+ });
+
+ const filterController = new FilterController(optionsController);
+ const columnsController = new ColumnsController(optionsController);
+ const sortingController = new SortingController(optionsController, columnsController);
+
+ const dataController = new DataController(optionsController, sortingController, filterController);
+
+ const searchController = new SearchController(optionsController);
+ const itemsController = new ItemsController(dataController, columnsController, searchController);
+ const toolbarController = new ToolbarController(optionsController);
+
+ const selectionController = new SelectionController(
+ optionsController,
+ dataController,
+ itemsController,
+ toolbarController,
+ );
+
+ return {
+ selectionController,
+ itemsController,
+ toolbarController,
+ };
+};
+
+describe('Options', () => {
+ describe('selectedCardKeys', () => {
+ describe('when given', () => {
+ it('should set the select state of the item', () => {
+ const {
+ itemsController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selectedCardKeys: [1],
+ });
+
+ expect(itemsController.items).toMatchSnapshot();
+ });
+ });
+ });
+
+ describe('selection', () => {
+ describe('mode', () => {
+ describe('when it is \'none\'', () => {
+ it('selection should not work', () => {
+ const {
+ itemsController,
+ selectionController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'none',
+ },
+ });
+
+ selectionController.selectCards([1]);
+ expect(itemsController.items).toMatchSnapshot();
+ });
+ });
+ describe('when it is \'none\' and the selectedCardKeys is specified', () => {
+ it('selection should not apply', () => {
+ const {
+ itemsController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selectedCardKeys: [1],
+ selection: {
+ mode: 'none',
+ },
+ });
+
+ expect(itemsController.items).toMatchSnapshot();
+ });
+ });
+ });
+
+ describe('allowSelectAll', () => {
+ describe('when it is true and selection mode is \'multiple\'', () => {
+ it('selection should not work', () => {
+ const {
+ toolbarController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ allowSelectAll: true,
+ },
+ });
+
+ expect(toolbarController.items.unreactive_get()).toMatchSnapshot();
+ });
+ });
+
+ describe('when it is false and selection mode is \'multiple\'', () => {
+ it('selection should not work', () => {
+ const {
+ toolbarController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'multiple',
+ allowSelectAll: false,
+ },
+ });
+
+ expect(toolbarController.items.unreactive_get()).toMatchSnapshot();
+ });
+ });
+
+ describe('when it is true and selection mode isn\'t \'multiple\'', () => {
+ it('selection should not work', () => {
+ const {
+ toolbarController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selection: {
+ mode: 'single',
+ allowSelectAll: true,
+ },
+ });
+
+ expect(toolbarController.items.unreactive_get()).toMatchSnapshot();
+ });
+ });
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.ts
new file mode 100644
index 000000000000..f8378f15251e
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.ts
@@ -0,0 +1,23 @@
+import type {
+ SelectedCardKeys,
+ SelectionChangedEvent,
+ SelectionChangingEvent,
+ SelectionOptions,
+} from './types';
+
+export interface Options {
+ selectedCardKeys?: SelectedCardKeys;
+ selection?: SelectionOptions;
+ onSelectionChanging?: ((e: SelectionChangingEvent) => void);
+ onSelectionChanged?: ((e: SelectionChangedEvent) => void);
+}
+
+export const defaultOptions: Options = {
+ selectedCardKeys: [],
+ selection: {
+ mode: 'none',
+ showCheckBoxesMode: 'always',
+ allowSelectAll: true,
+ selectAllMode: 'allPages',
+ },
+};
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/public_methods.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/public_methods.ts
new file mode 100644
index 000000000000..8d1ac090b53f
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/public_methods.ts
@@ -0,0 +1,52 @@
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import type { DeferredObj } from '@js/core/utils/deferred';
+
+import type { DataRow } from '../columns_controller/types';
+import type { Key } from '../data_controller/types';
+import type { Constructor } from '../types';
+import type { GridCoreNewBase } from '../widget';
+
+export function PublicMethods>(GridCore: TBase) {
+ return class GridCoreWithSelectionController extends GridCore {
+ public isCardSelected(key: Key): boolean {
+ return this.selectionController.isCardSelected(key);
+ }
+
+ public getSelectedCardKeys(): Key[] {
+ return this.selectionController.getSelectedCardKeys();
+ }
+
+ public getSelectedCards(): DataRow[] {
+ return this.selectionController.getSelectedCards();
+ }
+
+ public selectCards(keys: Key[], preserve = false): DeferredObj | undefined {
+ return this.selectionController.selectCards(keys, preserve);
+ }
+
+ public deselectCards(keys: Key[]): DeferredObj | undefined {
+ return this.selectionController.deselectCards(keys);
+ }
+
+ public selectCardsByIndexes(indexes: number[]): DeferredObj | undefined {
+ return this.selectionController.selectCardsByIndexes(indexes);
+ }
+
+ public deselectCardsByIndexes(indexes: number[]): DeferredObj | undefined {
+ return this.selectionController.deselectCardsByIndexes(indexes);
+ }
+
+ public selectAll(): DeferredObj | undefined {
+ return this.selectionController.selectAll();
+ }
+
+ public deselectAll(): DeferredObj | undefined {
+ return this.selectionController.deselectAll();
+ }
+
+ public clearSelection(): void {
+ this.selectionController.clearSelection();
+ }
+ };
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/types.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/types.ts
new file mode 100644
index 000000000000..bc6f533c538d
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/types.ts
@@ -0,0 +1,42 @@
+import type { SelectAllMode, SingleMultipleOrNone } from '@js/common';
+import type { EventInfo } from '@js/common/core/events';
+import type { SelectionColumnDisplayMode } from '@js/common/grids';
+import type dxCardView from '@js/ui/card_view';
+
+import type { Key } from '../data_controller/types';
+
+export type SelectedCardKeys = Key[];
+
+export interface SelectionEventInfo {
+ readonly currentSelectedCardKeys: TKey[];
+
+ readonly currentDeselectedCardKeys: TKey[];
+
+ readonly selectedCardKeys: TKey[];
+
+ readonly selectedCardsData: TCardData[];
+
+ readonly isSelectAll: boolean;
+
+ readonly isDeselectAll: boolean;
+}
+
+export type SelectionChangingEvent =
+ EventInfo> & SelectionEventInfo & {
+ cancel: boolean | PromiseLike | PromiseLike;
+ };
+
+export type SelectionChangedEvent =
+ EventInfo> & SelectionEventInfo;
+
+export type { SelectionColumnDisplayMode as ShowCheckBoxesMode };
+
+export interface SelectionOptions {
+ mode: SingleMultipleOrNone;
+
+ showCheckBoxesMode?: SelectionColumnDisplayMode;
+
+ allowSelectAll?: boolean;
+
+ selectAllMode?: SelectAllMode;
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/defaults.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/defaults.tsx
index 44e079e6dabf..b757d432e3ff 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/defaults.tsx
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/defaults.tsx
@@ -5,4 +5,6 @@ export const DEFAULT_TOOLBAR_ITEMS = [
'columnChooserButton',
'searchPanel',
'addCardButton',
+ 'selectAllButton',
+ 'clearSelectionButton',
] as const;
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 e686ec75df56..fb1f742f00aa 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts
@@ -27,6 +27,7 @@ import { MainView } from './main_view';
import { defaultOptions, defaultOptionsRules, type Options } from './options';
import { PagerView } from './pager/view';
import { SearchController } from './search/controller';
+import * as SelectionControllerModule from './selection';
import * as SortingControllerModule from './sorting_controller/index';
import type { SortingController } from './sorting_controller/sorting_controller';
import { ToolbarController } from './toolbar/controller';
@@ -48,6 +49,8 @@ export class GridCoreNewBase<
protected sortingController!: SortingController;
+ protected selectionController!: SelectionControllerModule.Controller;
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly
private editingController!: EditingController;
@@ -77,6 +80,7 @@ export class GridCoreNewBase<
this.diContext.register(DataControllerModule.CompatibilityDataController);
this.diContext.register(ItemsController);
this.diContext.register(ColumnsControllerModule.ColumnsController);
+ this.diContext.register(SelectionControllerModule.Controller);
this.diContext.register(ColumnsControllerModule.CompatibilityColumnsController);
this.diContext.register(SortingControllerModule.SortingController);
this.diContext.register(ToolbarController);
@@ -107,6 +111,7 @@ export class GridCoreNewBase<
this.dataController = this.diContext.get(DataControllerModule.DataController);
this.columnsController = this.diContext.get(ColumnsControllerModule.ColumnsController);
this.sortingController = this.diContext.get(SortingControllerModule.SortingController);
+ this.selectionController = this.diContext.get(SelectionControllerModule.Controller);
this.itemsController = this.diContext.get(ItemsController);
this.toolbarController = this.diContext.get(ToolbarController);
this.toolbarView = this.diContext.get(ToolbarView);
@@ -180,7 +185,9 @@ export class GridCoreNew extends ColumnsControllerModule.PublicMethods(
SortingControllerModule.PublicMethods(
FilterControllerModule.PublicMethods(
ColumnChooserModule.PublicMethods(
- GridCoreNewBase,
+ SelectionControllerModule.PublicMethods(
+ GridCoreNewBase,
+ ),
),
),
),
diff --git a/packages/devextreme/js/localization/messages/ar.json b/packages/devextreme/js/localization/messages/ar.json
index 039cacfe0db0..bc06a65226a7 100644
--- a/packages/devextreme/js/localization/messages/ar.json
+++ b/packages/devextreme/js/localization/messages/ar.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/bg.json b/packages/devextreme/js/localization/messages/bg.json
index ffe66183eb3f..23b770647304 100644
--- a/packages/devextreme/js/localization/messages/bg.json
+++ b/packages/devextreme/js/localization/messages/bg.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/ca.json b/packages/devextreme/js/localization/messages/ca.json
index 3ad6dc2eb2bc..cccd4195eea1 100644
--- a/packages/devextreme/js/localization/messages/ca.json
+++ b/packages/devextreme/js/localization/messages/ca.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/cs.json b/packages/devextreme/js/localization/messages/cs.json
index 0afcdba63d40..eda09c001363 100644
--- a/packages/devextreme/js/localization/messages/cs.json
+++ b/packages/devextreme/js/localization/messages/cs.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/da.json b/packages/devextreme/js/localization/messages/da.json
index b64093972d77..98eb60d22787 100644
--- a/packages/devextreme/js/localization/messages/da.json
+++ b/packages/devextreme/js/localization/messages/da.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "Der er {0} valgte datointervaller",
"dxCalendar-readOnlyLabel": "Skrivebeskyttet kalender",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Snak",
diff --git a/packages/devextreme/js/localization/messages/de.json b/packages/devextreme/js/localization/messages/de.json
index d79eb59fd1f6..feab06796cba 100644
--- a/packages/devextreme/js/localization/messages/de.json
+++ b/packages/devextreme/js/localization/messages/de.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/el.json b/packages/devextreme/js/localization/messages/el.json
index cedbfab47c4b..3b30bdcbb976 100644
--- a/packages/devextreme/js/localization/messages/el.json
+++ b/packages/devextreme/js/localization/messages/el.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/en.json b/packages/devextreme/js/localization/messages/en.json
index cbaaed3d3027..bd5179956c5e 100644
--- a/packages/devextreme/js/localization/messages/en.json
+++ b/packages/devextreme/js/localization/messages/en.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/es.json b/packages/devextreme/js/localization/messages/es.json
index 7d12b2a5034e..b59c5a02be81 100644
--- a/packages/devextreme/js/localization/messages/es.json
+++ b/packages/devextreme/js/localization/messages/es.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/fa.json b/packages/devextreme/js/localization/messages/fa.json
index 4ff324cf091e..5c6f6c96bcc9 100644
--- a/packages/devextreme/js/localization/messages/fa.json
+++ b/packages/devextreme/js/localization/messages/fa.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/fi.json b/packages/devextreme/js/localization/messages/fi.json
index afc16d4fb591..f540c9ae48cf 100644
--- a/packages/devextreme/js/localization/messages/fi.json
+++ b/packages/devextreme/js/localization/messages/fi.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/fr.json b/packages/devextreme/js/localization/messages/fr.json
index 32a0d26db48f..17c283aebe5b 100644
--- a/packages/devextreme/js/localization/messages/fr.json
+++ b/packages/devextreme/js/localization/messages/fr.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/hu.json b/packages/devextreme/js/localization/messages/hu.json
index 76fcd527f557..5e81c2d81db1 100644
--- a/packages/devextreme/js/localization/messages/hu.json
+++ b/packages/devextreme/js/localization/messages/hu.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/it.json b/packages/devextreme/js/localization/messages/it.json
index cc0787857a50..b07dfe033415 100644
--- a/packages/devextreme/js/localization/messages/it.json
+++ b/packages/devextreme/js/localization/messages/it.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/ja.json b/packages/devextreme/js/localization/messages/ja.json
index a7edb92ab04b..747c34fb4e28 100644
--- a/packages/devextreme/js/localization/messages/ja.json
+++ b/packages/devextreme/js/localization/messages/ja.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/lt.json b/packages/devextreme/js/localization/messages/lt.json
index 039b0c1cd435..b821d6171d88 100644
--- a/packages/devextreme/js/localization/messages/lt.json
+++ b/packages/devextreme/js/localization/messages/lt.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/lv.json b/packages/devextreme/js/localization/messages/lv.json
index 59fe1dcca45a..ec702a5b8d18 100644
--- a/packages/devextreme/js/localization/messages/lv.json
+++ b/packages/devextreme/js/localization/messages/lv.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/nb.json b/packages/devextreme/js/localization/messages/nb.json
index 8ef0f8fbd19d..6e736afc2df4 100644
--- a/packages/devextreme/js/localization/messages/nb.json
+++ b/packages/devextreme/js/localization/messages/nb.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/nl.json b/packages/devextreme/js/localization/messages/nl.json
index 021676f8099d..f6c0d62d7d6d 100644
--- a/packages/devextreme/js/localization/messages/nl.json
+++ b/packages/devextreme/js/localization/messages/nl.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/pl.json b/packages/devextreme/js/localization/messages/pl.json
index 6da54c267144..11d71b7bbb3f 100644
--- a/packages/devextreme/js/localization/messages/pl.json
+++ b/packages/devextreme/js/localization/messages/pl.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/pt.json b/packages/devextreme/js/localization/messages/pt.json
index c81a2dfc14e2..ef3bda25b497 100644
--- a/packages/devextreme/js/localization/messages/pt.json
+++ b/packages/devextreme/js/localization/messages/pt.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "Há {0} intervalos de datas selecionados",
"dxCalendar-readOnlyLabel": "Calendário somente leitura",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/ro.json b/packages/devextreme/js/localization/messages/ro.json
index 27caf28773cd..b2cad2bbdabe 100644
--- a/packages/devextreme/js/localization/messages/ro.json
+++ b/packages/devextreme/js/localization/messages/ro.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/ru.json b/packages/devextreme/js/localization/messages/ru.json
index b6c09107489d..fc00e71181b0 100644
--- a/packages/devextreme/js/localization/messages/ru.json
+++ b/packages/devextreme/js/localization/messages/ru.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/sl.json b/packages/devextreme/js/localization/messages/sl.json
index 0d4e55b25975..d11b0f56989b 100644
--- a/packages/devextreme/js/localization/messages/sl.json
+++ b/packages/devextreme/js/localization/messages/sl.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/sv.json b/packages/devextreme/js/localization/messages/sv.json
index c4accb5360e7..91524c06072d 100644
--- a/packages/devextreme/js/localization/messages/sv.json
+++ b/packages/devextreme/js/localization/messages/sv.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/tr.json b/packages/devextreme/js/localization/messages/tr.json
index 959aefea14b1..d1d017401101 100644
--- a/packages/devextreme/js/localization/messages/tr.json
+++ b/packages/devextreme/js/localization/messages/tr.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/uk.json b/packages/devextreme/js/localization/messages/uk.json
index 484699c38ec7..028231065dce 100644
--- a/packages/devextreme/js/localization/messages/uk.json
+++ b/packages/devextreme/js/localization/messages/uk.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/vi.json b/packages/devextreme/js/localization/messages/vi.json
index a76af1a4c789..b627205680bf 100644
--- a/packages/devextreme/js/localization/messages/vi.json
+++ b/packages/devextreme/js/localization/messages/vi.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/zh-tw.json b/packages/devextreme/js/localization/messages/zh-tw.json
index 4a67b665b093..590f28e05536 100644
--- a/packages/devextreme/js/localization/messages/zh-tw.json
+++ b/packages/devextreme/js/localization/messages/zh-tw.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/localization/messages/zh.json b/packages/devextreme/js/localization/messages/zh.json
index 9d12d9a79f4b..5cb3b1fd6022 100644
--- a/packages/devextreme/js/localization/messages/zh.json
+++ b/packages/devextreme/js/localization/messages/zh.json
@@ -350,6 +350,9 @@
"dxCalendar-selectedDateRangeCount": "There are {0} selected date ranges",
"dxCalendar-readOnlyLabel": "Read-only calendar",
+ "dxCardView-selectAll": "Select All",
+ "dxCardView-clearSelection": "Clear selection",
+
"dxAvatar-defaultImageAlt": "Avatar",
"dxChat-elementAriaLabel": "Chat",
diff --git a/packages/devextreme/js/ui/card_view.d.ts b/packages/devextreme/js/ui/card_view.d.ts
index 2754141cb958..c352fdf8a70a 100644
--- a/packages/devextreme/js/ui/card_view.d.ts
+++ b/packages/devextreme/js/ui/card_view.d.ts
@@ -225,6 +225,16 @@ export interface DataRow {
* @docid
*/
data: TRowData;
+ /**
+ * @public
+ * @docid
+ */
+ index: number;
+ /**
+ * @public
+ * @docid
+ */
+ isSelected: boolean;
}
type InheritedColumnProps =
diff --git a/packages/testcafe-models/cardView/card.ts b/packages/testcafe-models/cardView/card.ts
index e11eb4fce919..15ef47212e09 100644
--- a/packages/testcafe-models/cardView/card.ts
+++ b/packages/testcafe-models/cardView/card.ts
@@ -4,13 +4,29 @@ export const CLASS = {
field: 'dx-cardview-field',
fieldName: 'dx-cardview-field-name',
fieldValue: 'dx-cardview-field-value',
+ toolbarItem: 'dx-toolbar-item',
+ toolbarItemContent: 'dx-toolbar-item-content',
+ selectCheckbox: 'dx-cardview-select-checkbox',
+ selectCard: 'dx-cardview-card-selection',
+ checkbox: 'dx-checkbox',
}
export default class Card {
+ isSelected: Promise;
+
element: Selector;
constructor(selector: Selector) {
this.element = selector;
+ this.isSelected = this.element.hasClass(CLASS.selectCard);
+ }
+
+ getToolbarItemContent(index: number): Selector {
+ return this.element.find(`.${CLASS.toolbarItem}`).nth(index).child(`.${CLASS.toolbarItemContent}`);
+ }
+
+ getSelectCheckbox(): Selector {
+ return this.element.find(`.${CLASS.selectCheckbox} .${CLASS.checkbox}`);
}
getFieldCaptionCell(fieldCaption: String): Selector {
diff --git a/packages/testcafe-models/cardView/index.ts b/packages/testcafe-models/cardView/index.ts
index 4dd5100c5b32..fb8f8b519296 100644
--- a/packages/testcafe-models/cardView/index.ts
+++ b/packages/testcafe-models/cardView/index.ts
@@ -4,6 +4,7 @@ import { CLASS as CLASS_BASE } from '../gridCore';
import Card from './card';
import HeadersElement from './headers/headers';
import HeaderPanel from './headerPanel';
+import Toolbar from './toolbar';
import Popup from "../popup";
import List from "../list";
import {ClientFunction} from "testcafe";
@@ -11,10 +12,13 @@ import {ClientFunction} from "testcafe";
export const CLASS = {
...CLASS_BASE,
cardView: 'dx-cardview',
+ cardViewContent: 'dx-cardview-content',
+ selectCheckBoxesHidden: 'dx-cardview-select-checkboxes-hidden',
headers: 'headers',
headerItem: 'header-item',
headerFilterMenu: 'dx-header-filter-menu',
card: 'card',
+ toolbar: 'dx-toolbar',
}
export default class CardView extends GridCore {
@@ -68,4 +72,30 @@ export default class CardView extends GridCore {
{ dependencies: { getInstance, columnName } },
)();
}
+
+ getSelectedCardKeys(): Promise> {
+ const { getInstance } = this;
+
+ return ClientFunction(
+ () => (getInstance() as any).getSelectedCardKeys(),
+ { dependencies: { getInstance } },
+ )();
+ }
+
+ getToolbar(): Toolbar {
+ return new Toolbar(this.element.child(`.${CLASS.toolbar}`));
+ }
+
+ isCheckBoxesHidden(): Promise {
+ return this.element.find(`.${CLASS.cardViewContent}`).hasClass(`${CLASS.selectCheckBoxesHidden}`);
+ }
+
+ apiPageIndex(pageIndex: number): Promise {
+ const { getInstance } = this;
+
+ return ClientFunction(
+ () => (getInstance() as any).pageIndex(pageIndex),
+ { dependencies: { getInstance, pageIndex } },
+ )();
+ }
}
diff --git a/packages/testcafe-models/cardView/toolbar.ts b/packages/testcafe-models/cardView/toolbar.ts
new file mode 100644
index 000000000000..b795f76b8e36
--- /dev/null
+++ b/packages/testcafe-models/cardView/toolbar.ts
@@ -0,0 +1,27 @@
+const CLASS = {
+ disabledState: 'dx-state-disabled',
+};
+
+export default class Toolbar {
+ element: Selector;
+
+ constructor(selector: Selector) {
+ this.element = selector;
+ }
+
+ getSelectAllButton(): Selector {
+ return this.element.find('[aria-label=\'Select All\']');
+ }
+
+ getClearSelectionButton(): Selector {
+ return this.element.find('[aria-label=\'Clear selection\']');
+ }
+
+ isSelectAllButtonDisabled(): Promise {
+ return this.getSelectAllButton().hasClass(CLASS.disabledState);
+ }
+
+ isClearSelectionButtonDisabled(): Promise {
+ return this.getClearSelectionButton().hasClass(CLASS.disabledState);
+ }
+}