{
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 8cb7684bf73e..6dc3158b5abd 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
@@ -1,16 +1,16 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
-import { isCommandKeyPressed } from '@js/common/core/events/utils/index';
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 { CardClickEvent } from './content/card/card';
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 {
@@ -73,10 +73,13 @@ export class ContentView extends ContentViewBase {
protected override component = ContentViewComponent;
protected override getProps() {
+ const allowSelectOnClick = this.selectionController.allowSelectOnClick();
+
return combined({
...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,
@@ -84,8 +87,10 @@ export class ContentView extends ContentViewBase {
cardProps: combined({
minWidth: this.cardMinWidth,
maxWidth: this.options.oneWay('cardMaxWidth'),
+ isCheckBoxesRendered: this.selectionController.isCheckBoxesRendered,
+ allowSelectOnClick,
+ onHold: this.onCardHold.bind(this),
onClick: this.options.action('onCardClick'),
- onSelectClick: this.onSelectClick.bind(this),
onDblClick: this.options.action('onCardDblClick'),
onHoverChanged: this.options.action('onCardHoverChanged'),
onPrepared: this.options.action('onCardPrepared'),
@@ -110,6 +115,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({
@@ -131,9 +137,15 @@ export class ContentView extends ContentViewBase {
return compileGetter(expr);
}
- private onSelectClick(e: CardClickEvent) {
- this.selectionController.changeCardSelection(e.row.index, {
- control: isCommandKeyPressed(e.event),
- });
+ 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/items_controller/items_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts
index 1f31a58b97ad..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,3 +1,4 @@
+import { equalByValue } from '@js/core/utils/common';
import formatHelper from '@js/format_helper';
import { computed, state } from '@ts/core/reactive';
import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/columns_controller';
@@ -86,4 +87,11 @@ export class ItemsController {
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/selection/__snapshots__/controller.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/controller.test.ts.snap
index 8852d12a036c..f1a669237598 100644
--- 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
@@ -114,6 +114,129 @@ InterruptableComputed {
}
`;
+exports[`SelectionController deselectCards should select item 1`] = `
+InterruptableComputed {
+ "callbacks": Set {},
+ "depInitialized": [
+ true,
+ true,
+ true,
+ ],
+ "depValues": [
+ [
+ {
+ "id": 1,
+ "value": "test",
+ },
+ ],
+ [
+ {
+ "alignment": "left",
+ "allowHiding": true,
+ "allowReordering": true,
+ "calculateCellValue": [Function],
+ "calculateDisplayValue": [Function],
+ "caption": "Id",
+ "dataField": "id",
+ "dataType": "string",
+ "falseText": "false",
+ "headerItemTemplate": undefined,
+ "name": "id",
+ "trueText": "true",
+ "visible": true,
+ "visibleIndex": 0,
+ },
+ {
+ "alignment": "left",
+ "allowHiding": true,
+ "allowReordering": true,
+ "calculateCellValue": [Function],
+ "calculateDisplayValue": [Function],
+ "caption": "Value",
+ "dataField": "value",
+ "dataType": "string",
+ "falseText": "false",
+ "headerItemTemplate": undefined,
+ "name": "value",
+ "trueText": "true",
+ "visible": true,
+ "visibleIndex": 1,
+ },
+ ],
+ [],
+ ],
+ "isInitialized": true,
+ "subscriptions": SubscriptionBag {
+ "subscriptions": [
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ {
+ "unsubscribe": [Function],
+ },
+ ],
+ },
+ "value": [
+ {
+ "cells": [
+ {
+ "column": {
+ "alignment": "left",
+ "allowHiding": true,
+ "allowReordering": true,
+ "calculateCellValue": [Function],
+ "calculateDisplayValue": [Function],
+ "caption": "Id",
+ "dataField": "id",
+ "dataType": "string",
+ "falseText": "false",
+ "headerItemTemplate": undefined,
+ "name": "id",
+ "trueText": "true",
+ "visible": true,
+ "visibleIndex": 0,
+ },
+ "displayValue": 1,
+ "text": "1",
+ "value": 1,
+ },
+ {
+ "column": {
+ "alignment": "left",
+ "allowHiding": true,
+ "allowReordering": true,
+ "calculateCellValue": [Function],
+ "calculateDisplayValue": [Function],
+ "caption": "Value",
+ "dataField": "value",
+ "dataType": "string",
+ "falseText": "false",
+ "headerItemTemplate": undefined,
+ "name": "value",
+ "trueText": "true",
+ "visible": true,
+ "visibleIndex": 1,
+ },
+ "displayValue": "test",
+ "text": "test",
+ "value": "test",
+ },
+ ],
+ "data": {
+ "id": 1,
+ "value": "test",
+ },
+ "index": 0,
+ "isSelected": false,
+ "key": 1,
+ },
+ ],
+}
+`;
+
exports[`SelectionController selectCards should select item 1`] = `
InterruptableComputed {
"callbacks": Set {},
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
index b54f301d83f1..d839ae99eb7d 100644
--- 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
@@ -1,3 +1,4 @@
+/* eslint-disable spellcheck/spell-checker */
import { describe, expect, it } from '@jest/globals';
import { ColumnsController } from '../columns_controller/columns_controller';
@@ -15,6 +16,7 @@ const setup = (config: Options = {}) => {
selection: {
mode: 'single',
},
+ selectedCardKeys: [],
...config,
});
@@ -56,6 +58,22 @@ describe('SelectionController', () => {
});
});
+ describe('deselectCards', () => {
+ it('should select item', () => {
+ const {
+ selectionController,
+ itemsController,
+ } = setup({
+ keyExpr: 'id',
+ dataSource: [{ id: 1, value: 'test' }],
+ selectedCardKeys: [1],
+ });
+
+ selectionController.deselectCards([1]);
+ expect(itemsController.items).toMatchSnapshot();
+ });
+ });
+
describe('changeCardSelection', () => {
describe('when the control arg equal to false', () => {
it('should update the select state of the item', () => {
@@ -87,6 +105,25 @@ describe('SelectionController', () => {
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', () => {
@@ -132,6 +169,22 @@ describe('SelectionController', () => {
});
});
+ 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 {
@@ -146,4 +199,297 @@ describe('SelectionController', () => {
expect(selectionController.getSelectedCardKeys().length).toBe(0);
});
});
+
+ 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);
+ });
+ });
+ });
+
+ 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([]);
+ });
+ });
+ });
});
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
index b42cba01eea7..b7e7292cd423 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts
@@ -4,13 +4,16 @@
import type { DeferredObj } from '@js/core/utils/deferred';
import { isDefined } from '@js/core/utils/type';
import type { SubsGets } from '@ts/core/reactive/index';
-import { computed, effect } 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 { DataObject, Key } from '../data_controller/types';
+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 { SelectionMode } from './const';
import type { SelectionChangedEvent, SelectionOptions } from './types';
export class SelectionController {
@@ -22,6 +25,65 @@ export class SelectionController {
private readonly selectionHelper: SubsGets;
+ private readonly _isCheckBoxesRendered = state(false);
+
+ 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(
+ (isCheckBoxesRendered, _isCheckBoxesVisible) => {
+ if (isCheckBoxesRendered) {
+ const { showCheckBoxesMode } = this.selectionOption.unreactive_get();
+
+ return showCheckBoxesMode !== ShowCheckBoxesMode.OnClick || _isCheckBoxesVisible;
+ }
+
+ return false;
+ },
+ [
+ this.isCheckBoxesRendered,
+ this._isCheckBoxesVisible,
+ ],
+ );
+
+ public readonly needToHiddenCheckBoxes = computed(
+ (isCheckBoxesVisible) => {
+ const { showCheckBoxesMode } = this.selectionOption.unreactive_get();
+ const isCheckBoxesRendered = this.isCheckBoxesRendered.unreactive_get();
+
+ if (isCheckBoxesRendered && showCheckBoxesMode === ShowCheckBoxesMode.OnClick) {
+ return !isCheckBoxesVisible;
+ }
+
+ return false;
+ },
+ [
+ this.isCheckBoxesVisible,
+ ],
+ );
+
constructor(
private readonly options: OptionsController,
private readonly dataController: DataController,
@@ -32,7 +94,7 @@ export class SelectionController {
dataSource,
selectionOption,
) => {
- if (selectionOption.mode === 'none') {
+ if (selectionOption.mode === SelectionMode.None) {
return undefined;
}
@@ -50,8 +112,14 @@ export class SelectionController {
);
effect((selectedCardKeys, selectionOption) => {
- if (selectionOption.mode !== 'none') {
+ 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]);
}
@@ -65,6 +133,7 @@ export class SelectionController {
mode: selectionOption.mode,
maxFilterLengthInRequest: selectionOption.maxFilterLengthInRequest,
ignoreDisabledItems: true,
+ alwaysSelectByShift: false,
key() {
return dataSource.key();
},
@@ -121,10 +190,19 @@ export class SelectionController {
this.selectedCardKeys.update([...e.selectedItemKeys]);
}
- public changeCardSelection(cardIndex: number, options: { control: boolean }): void {
+ 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, options, false);
+ selectionHelper?.changeItemSelection(cardIndex, keys, false);
}
public selectCards(keys: Key[], preserve = false): DeferredObj | undefined {
@@ -133,6 +211,12 @@ export class SelectionController {
return selectionHelper?.selectedItemKeys(keys, preserve);
}
+ public deselectCards(keys: Key[]): DeferredObj | undefined {
+ const selectionHelper = this.selectionHelper?.unreactive_get();
+
+ return selectionHelper?.selectedItemKeys(keys, true, true);
+ }
+
public isCardSelected(key: Key): boolean {
const selectedCardKeys = this.selectedCardKeys.unreactive_get();
@@ -143,7 +227,48 @@ export class SelectionController {
this.selectedCardKeys.update([]);
}
+ 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 allowSelectOnClick(): boolean {
+ const { mode, showCheckBoxesMode } = this.selectionOption.unreactive_get();
+
+ return mode !== SelectionMode.Multiple || showCheckBoxesMode !== ShowCheckBoxesMode.Always;
+ }
+
+ 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/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.ts
index 1d784be2f597..ed1bbf88f434 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.ts
@@ -9,5 +9,6 @@ export const defaultOptions: Options = {
selectedCardKeys: [],
selection: {
mode: 'none',
+ showCheckBoxesMode: 'always',
},
};
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
index 6388d68a7606..ff30889ea5b5 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/selection/types.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/types.ts
@@ -1,4 +1,5 @@
import type { SingleMultipleOrNone } from '@js/common';
+import type { SelectionColumnDisplayMode } from '@js/common/grids';
import type { DataObject, Key } from '../data_controller/types';
@@ -18,6 +19,10 @@ export interface SelectionChangedEvent {
removedItems: DataObject[];
}
+export type { SelectionColumnDisplayMode as ShowCheckBoxesMode };
+
export interface SelectionOptions {
mode: SingleMultipleOrNone;
+
+ showCheckBoxesMode?: SelectionColumnDisplayMode;
}
From fd77e234de3fd722b375bc1bb2edda2e38381712 Mon Sep 17 00:00:00 2001
From: Alyar <>
Date: Wed, 26 Mar 2025 15:55:08 +0400
Subject: [PATCH 3/8] CardView: Implement SelectAll and DeselectAll features
---
.../data_controller/data_controller.ts | 3 +
.../__snapshots__/controller.test.ts.snap | 311 +++++++++----
.../__snapshots__/options.test.ts.snap | 33 ++
.../grid_core/selection/controller.test.ts | 407 ++++++++++++++++--
.../new/grid_core/selection/controller.ts | 174 ++++++--
.../new/grid_core/selection/options.test.ts | 58 +++
.../grids/new/grid_core/selection/options.ts | 11 +-
.../grids/new/grid_core/selection/types.ts | 34 +-
.../grids/new/grid_core/toolbar/defaults.tsx | 2 +
.../js/localization/messages/ar.json | 3 +
.../js/localization/messages/bg.json | 3 +
.../js/localization/messages/ca.json | 3 +
.../js/localization/messages/cs.json | 3 +
.../js/localization/messages/da.json | 3 +
.../js/localization/messages/de.json | 3 +
.../js/localization/messages/el.json | 3 +
.../js/localization/messages/en.json | 3 +
.../js/localization/messages/es.json | 3 +
.../js/localization/messages/fa.json | 3 +
.../js/localization/messages/fi.json | 3 +
.../js/localization/messages/fr.json | 3 +
.../js/localization/messages/hu.json | 3 +
.../js/localization/messages/it.json | 3 +
.../js/localization/messages/ja.json | 3 +
.../js/localization/messages/lt.json | 3 +
.../js/localization/messages/lv.json | 3 +
.../js/localization/messages/nb.json | 3 +
.../js/localization/messages/nl.json | 3 +
.../js/localization/messages/pl.json | 3 +
.../js/localization/messages/pt.json | 3 +
.../js/localization/messages/ro.json | 3 +
.../js/localization/messages/ru.json | 3 +
.../js/localization/messages/sl.json | 3 +
.../js/localization/messages/sv.json | 3 +
.../js/localization/messages/tr.json | 3 +
.../js/localization/messages/uk.json | 3 +
.../js/localization/messages/vi.json | 3 +
.../js/localization/messages/zh-tw.json | 3 +
.../js/localization/messages/zh.json | 3 +
39 files changed, 955 insertions(+), 168 deletions(-)
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/selection/__snapshots__/controller.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/controller.test.ts.snap
index f1a669237598..83f018ce5358 100644
--- 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
@@ -114,13 +114,14 @@ InterruptableComputed {
}
`;
-exports[`SelectionController deselectCards should select item 1`] = `
+exports[`SelectionController deselectCards should deselect item 1`] = `
InterruptableComputed {
"callbacks": Set {},
"depInitialized": [
true,
true,
true,
+ true,
],
"depValues": [
[
@@ -129,41 +130,71 @@ InterruptableComputed {
"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": [
[
{
- "alignment": "left",
- "allowHiding": true,
- "allowReordering": true,
- "calculateCellValue": [Function],
- "calculateDisplayValue": [Function],
- "caption": "Id",
- "dataField": "id",
- "dataType": "string",
- "falseText": "false",
- "headerItemTemplate": undefined,
- "name": "id",
- "trueText": "true",
- "visible": true,
- "visibleIndex": 0,
- },
- {
- "alignment": "left",
- "allowHiding": true,
- "allowReordering": true,
- "calculateCellValue": [Function],
- "calculateDisplayValue": [Function],
- "caption": "Value",
- "dataField": "value",
- "dataType": "string",
- "falseText": "false",
- "headerItemTemplate": undefined,
- "name": "value",
- "trueText": "true",
- "visible": true,
- "visibleIndex": 1,
+ "id": 1,
+ "value": "test",
},
],
[],
+ [
+ 1,
+ ],
+ {
+ "caseSensitive": false,
+ "enabled": true,
+ "searchStr": "",
+ },
],
"isInitialized": true,
"subscriptions": SubscriptionBag {
@@ -177,54 +208,126 @@ InterruptableComputed {
{
"unsubscribe": [Function],
},
+ {
+ "unsubscribe": [Function],
+ },
],
},
"value": [
{
- "cells": [
- {
- "column": {
- "alignment": "left",
- "allowHiding": true,
- "allowReordering": true,
- "calculateCellValue": [Function],
- "calculateDisplayValue": [Function],
- "caption": "Id",
- "dataField": "id",
- "dataType": "string",
- "falseText": "false",
- "headerItemTemplate": undefined,
- "name": "id",
- "trueText": "true",
- "visible": true,
- "visibleIndex": 0,
- },
- "displayValue": 1,
- "text": "1",
- "value": 1,
- },
- {
- "column": {
- "alignment": "left",
- "allowHiding": true,
- "allowReordering": true,
- "calculateCellValue": [Function],
- "calculateDisplayValue": [Function],
- "caption": "Value",
- "dataField": "value",
- "dataType": "string",
- "falseText": "false",
- "headerItemTemplate": undefined,
- "name": "value",
- "trueText": "true",
- "visible": true,
- "visibleIndex": 1,
- },
- "displayValue": "test",
- "text": "test",
- "value": "test",
- },
- ],
+ "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",
@@ -237,6 +340,64 @@ InterruptableComputed {
}
`;
+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 {},
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
index c876530a843b..450dc1044be7 100644
--- 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
@@ -58,6 +58,39 @@ InterruptableComputed {
}
`;
+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 {},
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
index d839ae99eb7d..8c535227e833 100644
--- 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
@@ -1,5 +1,7 @@
/* eslint-disable spellcheck/spell-checker */
-import { describe, expect, it } from '@jest/globals';
+import {
+ describe, expect, it, jest,
+} from '@jest/globals';
import { ColumnsController } from '../columns_controller/columns_controller';
import { DataController } from '../data_controller';
@@ -9,6 +11,7 @@ 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 = {}) => {
@@ -28,11 +31,13 @@ const setup = (config: Options = {}) => {
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 {
@@ -43,6 +48,8 @@ const setup = (config: Options = {}) => {
};
describe('SelectionController', () => {
+ // Public methods
+
describe('selectCards', () => {
it('should select item', () => {
const {
@@ -59,7 +66,7 @@ describe('SelectionController', () => {
});
describe('deselectCards', () => {
- it('should select item', () => {
+ it('should deselect item', () => {
const {
selectionController,
itemsController,
@@ -200,6 +207,121 @@ describe('SelectionController', () => {
});
});
+ 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', () => {
@@ -379,117 +501,316 @@ describe('SelectionController', () => {
});
});
- describe('updateSelectionCheckBoxesVisible', () => {
- describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onClick\'', () => {
- it('should show the selection checkboxes', () => {
+ // 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: [{ id: 1, value: 'test' }],
+ dataSource: [cardData],
selection: {
mode: 'multiple',
- showCheckBoxesMode: 'onClick',
},
+ onSelectionChanging: selectionChangingMockFn,
});
- selectionController.updateSelectionCheckBoxesVisible(true);
- expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(true);
+ 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],
+ }]);
});
+ });
- it('should hide the selection checkboxes', () => {
+ 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: [{ id: 1, value: 'test' }],
+ dataSource: [cardData],
selection: {
mode: 'multiple',
- showCheckBoxesMode: 'onClick',
},
+ selectedCardKeys: [1],
+ onSelectionChanging: selectionChangingMockFn,
});
- selectionController.updateSelectionCheckBoxesVisible(false);
- expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(false);
+ 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('processLongTap', () => {
- describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onLongTap\'', () => {
- it('should render the selection checkbox', () => {
+ 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: [{ id: 1, value: 'test' }],
+ dataSource: data,
selection: {
mode: 'multiple',
- showCheckBoxesMode: 'onLongTap',
+ allowSelectAll: true,
},
+ onSelectionChanging: selectionChangingMockFn,
});
- // @ts-expect-error
- selectionController.processLongTap({ index: 0 });
- expect(selectionController.isCheckBoxesRendered.unreactive_get()).toBe(true);
+ 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 the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onClick\'', () => {
- it('should show the selection checkbox', () => {
+ 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: [{ id: 1, value: 'test' }],
+ dataSource: data,
selection: {
mode: 'multiple',
- showCheckBoxesMode: 'onClick',
+ allowSelectAll: true,
},
+ selectedCardKeys: [1, 2],
+ onSelectionChanging: selectionChangingMockFn,
});
- // @ts-expect-error
- selectionController.processLongTap({ index: 0 });
- expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(true);
+ 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 the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'none\'', () => {
- it('should select a first item', () => {
+ 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: [{ id: 1, value: 'test' }],
+ dataSource: [cardData],
selection: {
mode: 'multiple',
- showCheckBoxesMode: 'none',
},
+ onSelectionChanging: selectionChangingMockFn,
});
- // @ts-expect-error
- selectionController.processLongTap({ index: 0 });
- expect(selectionController.getSelectedCardKeys()).toEqual([1]);
+ 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 the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'none\'', () => {
- it('should not select a first item', () => {
+ 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: [{ id: 1, value: 'test' }],
+ dataSource: [cardData],
selection: {
mode: 'multiple',
- showCheckBoxesMode: 'always',
},
+ onSelectionChanging: selectionChangingMockFn,
});
- // @ts-expect-error
- selectionController.processLongTap({ index: 0 });
+ 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
index b7e7292cd423..867c2ec89b9a 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts
@@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable spellcheck/spell-checker */
import type { DeferredObj } from '@js/core/utils/deferred';
-import { isDefined } from '@js/core/utils/type';
+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';
@@ -13,11 +13,19 @@ 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 { SelectionChangedEvent, SelectionOptions } from './types';
+import type {
+ SelectedCardKeys, SelectionEventInfo, SelectionOptions,
+} from './types';
export class SelectionController {
- public static dependencies = [OptionsController, DataController, ItemsController] as const;
+ public static dependencies = [
+ OptionsController,
+ DataController,
+ ItemsController,
+ ToolbarController,
+ ] as const;
private readonly selectedCardKeys = this.options.twoWay('selectedCardKeys');
@@ -27,6 +35,10 @@ export class SelectionController {
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) {
@@ -88,6 +100,7 @@ export class SelectionController {
private readonly options: OptionsController,
private readonly dataController: DataController,
private readonly itemsController: ItemsController,
+ private readonly toolbarController: ToolbarController,
) {
this.selectionHelper = computed(
(
@@ -122,18 +135,28 @@ export class SelectionController {
}
}
}, [this.selectedCardKeys, this.selectionOption]);
+
+ effect((selectedCardKeys, selectionOption) => {
+ this.updateSelectionToolbarButtons(selectedCardKeys, selectionOption);
+ }, [this.selectedCardKeys, this.selectionOption, this.dataController.items]);
+
+ effect((isLoaded) => {
+ if (isLoaded) {
+ const selectedCardKeys = this.selectedCardKeys.unreactive_get();
+
+ this.selectCards(selectedCardKeys);
+ }
+ }, [this.dataController.isLoaded]);
}
private getSelectionConfig(dataSource, selectionOption): object {
const selectedCardKeys = this.selectedCardKeys.unreactive_get();
- const { itemsController } = this;
return {
selectedKeys: selectedCardKeys,
mode: selectionOption.mode,
maxFilterLengthInRequest: selectionOption.maxFilterLengthInRequest,
ignoreDisabledItems: true,
- alwaysSelectByShift: false,
key() {
return dataSource.key();
},
@@ -144,50 +167,109 @@ export class SelectionController {
return dataSource.select();
},
load(options) {
- return dataSource.load(options);
+ return dataSource.store().load(options);
},
plainItems() {
- return itemsController.items.unreactive_get();
- },
- isItemSelected(item) {
- return item.isSelected;
- },
- isSelectableItem(item) {
- return !!item?.data;
- },
- getItemData(item) {
- return item?.data ?? item;
+ return dataSource.items();
},
filter() {
// TODO Salimov: Need to take combined filter
return dataSource.filter();
},
totalCount: () => dataSource.totalCount(),
- getLoadOptions(loadItemIndex, focusedItemIndex, shiftItemIndex) {
- const { sort, filter } = dataSource.loadOptions();
- let minIndex = Math.min(loadItemIndex, focusedItemIndex);
- let maxIndex = Math.max(loadItemIndex, focusedItemIndex);
-
- if (isDefined(shiftItemIndex)) {
- minIndex = Math.min(shiftItemIndex, minIndex);
- maxIndex = Math.max(shiftItemIndex, maxIndex);
- }
-
- const take = maxIndex - minIndex + 1;
+ onSelectionChanging: this.selectionChanging.bind(this),
+ onSelectionChanged: this.selectionChanged.bind(this),
+ };
+ }
- return {
- skip: minIndex,
- take,
- filter,
- sort,
- };
- },
- onSelectionChanged: this.onSelectionChanged.bind(this),
+ private getSelectionEventArgs(e): SelectionEventInfo {
+ return {
+ currentSelectedCardKeys: [...e.addedItemKeys],
+ currentDeselectedCardKeys: [...e.removedItemKeys],
+ selectedCardKeys: [...e.selectedItemKeys],
+ selectedCardsData: [...e.selectedItems],
+ isSelectAll: false,
+ isDeselectAll: false,
};
}
- private onSelectionChanged(e: SelectionChangedEvent): void {
- this.selectedCardKeys.update([...e.selectedItemKeys]);
+ // 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,
+ selectionOption: SelectionOptions,
+ ) {
+ if (selectionOption.mode === SelectionMode.Multiple && selectionOption.allowSelectAll) {
+ 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.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',
+ });
+ } else {
+ this.toolbarController.removeDefaultItem('selectAllButton');
+ this.toolbarController.removeDefaultItem('clearSelectionButton');
+ }
}
public changeCardSelection(
@@ -223,8 +305,22 @@ export class SelectionController {
return selectedCardKeys.includes(key);
}
- public clearSelection(): void {
- this.selectedCardKeys.update([]);
+ 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[] {
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
index c76e0c7dfc0b..2b763c0f86b7 100644
--- 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
@@ -1,3 +1,4 @@
+/* eslint-disable spellcheck/spell-checker */
import { describe, expect, it } from '@jest/globals';
import { ColumnsController } from '../columns_controller/columns_controller';
@@ -8,6 +9,7 @@ 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 = {}) => {
@@ -26,16 +28,19 @@ const setup = (config: Options = {}) => {
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,
};
};
@@ -92,5 +97,58 @@ describe('Options', () => {
});
});
});
+
+ 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
index ed1bbf88f434..f8378f15251e 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.ts
@@ -1,8 +1,15 @@
-import type { SelectedCardKeys, SelectionOptions } from './types';
+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 = {
@@ -10,5 +17,7 @@ export const defaultOptions: Options = {
selection: {
mode: 'none',
showCheckBoxesMode: 'always',
+ allowSelectAll: true,
+ selectAllMode: 'allPages',
},
};
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
index ff30889ea5b5..bc6f533c538d 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/selection/types.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/types.ts
@@ -1,28 +1,42 @@
-import type { SingleMultipleOrNone } from '@js/common';
+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 { DataObject, Key } from '../data_controller/types';
+import type { Key } from '../data_controller/types';
-export type SelectedCardKeys = any[];
+export type SelectedCardKeys = Key[];
-export interface SelectionChangedEvent {
- selectedItems: DataObject[];
+export interface SelectionEventInfo {
+ readonly currentSelectedCardKeys: TKey[];
- selectedItemKeys: Key[];
+ readonly currentDeselectedCardKeys: TKey[];
- addedItemKeys: Key[];
+ readonly selectedCardKeys: TKey[];
- removedItemKeys: Key[];
+ readonly selectedCardsData: TCardData[];
- addedItems: DataObject[];
+ readonly isSelectAll: boolean;
- removedItems: DataObject[];
+ 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/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",
From cd5c94cad4a8ceb042f9dba778e63441e404e4cb Mon Sep 17 00:00:00 2001
From: Alyar <>
Date: Wed, 26 Mar 2025 20:06:00 +0400
Subject: [PATCH 4/8] Add storybook example
---
.../stories/card_view/CardView.stories.tsx | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/apps/react-storybook/stories/card_view/CardView.stories.tsx b/apps/react-storybook/stories/card_view/CardView.stories.tsx
index fabeedd51b81..2164c9c04ae2 100644
--- a/apps/react-storybook/stories/card_view/CardView.stories.tsx
+++ b/apps/react-storybook/stories/card_view/CardView.stories.tsx
@@ -253,3 +253,17 @@ export const HeaderFilterStory: Story = {
}
}
+export const SelectionStory: Story = {
+ ...DefaultMode,
+ args: {
+ ...DefaultMode.args,
+ keyExpr: 'id',
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ allowSelectAll: true,
+ selectAllMode: 'allPages',
+ }
+ }
+}
+
From 56c4f4104c45a7135fb444d6fe7a1b2564ab16ef Mon Sep 17 00:00:00 2001
From: Alyar <>
Date: Thu, 27 Mar 2025 01:30:35 +0400
Subject: [PATCH 5/8] CardView: Implement selectCardsByIndexes and
deselectCardsByIndexes methods
---
.../__snapshots__/controller.test.ts.snap | 114 ++++++++++++++++++
.../grid_core/selection/controller.test.ts | 31 +++++
.../new/grid_core/selection/controller.ts | 20 +++
3 files changed, 165 insertions(+)
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
index 83f018ce5358..875389acbdd4 100644
--- 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
@@ -170,6 +170,62 @@ InterruptableComputed {
}
`;
+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 {},
@@ -455,3 +511,61 @@ InterruptableComputed {
],
}
`;
+
+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/controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.test.ts
index 8c535227e833..fbed51d7649f 100644
--- 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
@@ -81,6 +81,37 @@ describe('SelectionController', () => {
});
});
+ 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', () => {
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
index 867c2ec89b9a..5e10621efd5d 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts
@@ -272,6 +272,14 @@ export class SelectionController {
}
}
+ 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 },
@@ -293,12 +301,24 @@ export class SelectionController {
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();
From 7b082d00cd43865ebd7e03c01049d7ddd3a19293 Mon Sep 17 00:00:00 2001
From: Alyar <>
Date: Fri, 28 Mar 2025 05:13:35 +0400
Subject: [PATCH 6/8] CardView: Add e2e tests
---
.../tests/cardView/selection/functional.ts | 970 ++++++++++++++++++
.../tests/cardView/selection/visual.ts | 355 +++++++
.../grids/new/card_view/content_view/view.tsx | 4 +-
.../new/grid_core/selection/controller.ts | 39 +-
packages/testcafe-models/cardView/card.ts | 16 +
packages/testcafe-models/cardView/index.ts | 30 +
packages/testcafe-models/cardView/toolbar.ts | 27 +
7 files changed, 1420 insertions(+), 21 deletions(-)
create mode 100644 e2e/testcafe-devextreme/tests/cardView/selection/functional.ts
create mode 100644 e2e/testcafe-devextreme/tests/cardView/selection/visual.ts
create mode 100644 packages/testcafe-models/cardView/toolbar.ts
diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/functional.ts b/e2e/testcafe-devextreme/tests/cardView/selection/functional.ts
new file mode 100644
index 000000000000..49a82523f63e
--- /dev/null
+++ b/e2e/testcafe-devextreme/tests/cardView/selection/functional.ts
@@ -0,0 +1,970 @@
+import CardView from 'devextreme-testcafe-models/cardView';
+import { ClientFunction } from 'testcafe';
+import url from '../../../helpers/getPageUrl';
+import { createWidget } from '../../../helpers/createWidget';
+
+fixture.disablePageReloads`Selection.Functional`
+ .page(url(__dirname, '../../container.html'));
+
+const CARD_VIEW_SELECTOR = '#container';
+
+test('Single mode: select a first card -> select a second card -> deselect a second card', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const firstCard = cardView.getCard(0);
+ const secondCard = cardView.getCard(1);
+
+ // act
+ await t.click(firstCard.element);
+
+ // assert
+ await t.expect(firstCard.isSelected).ok();
+
+ // act
+ await t.click(secondCard.element);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .notOk()
+ .expect(secondCard.isSelected)
+ .ok();
+
+ // act
+ await t.click(secondCard.element, { modifiers: { ctrl: true } });
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .notOk()
+ .expect(secondCard.isSelected)
+ .notOk();
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'single',
+ },
+}));
+
+test('Multiple mode with showCheckBoxesMode=\'always\': select a first card -> select a second card -> deselect a first card -> deselect a second card', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const firstCard = cardView.getCard(0);
+ const firstSelectCheckbox = firstCard.getSelectCheckbox();
+ const secondCard = cardView.getCard(1);
+ const secondSelectCheckbox = secondCard.getSelectCheckbox();
+
+ // act
+ await t.click(firstSelectCheckbox);
+
+ // assert
+ await t.expect(firstCard.isSelected).ok();
+
+ // act
+ await t.click(secondSelectCheckbox);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .ok()
+ .expect(secondCard.isSelected)
+ .ok();
+
+ // act
+ await t.click(firstSelectCheckbox);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .notOk()
+ .expect(secondCard.isSelected)
+ .ok();
+
+ // act
+ await t.click(secondSelectCheckbox);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .notOk()
+ .expect(secondCard.isSelected)
+ .notOk();
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ allowSelectAll: true,
+ },
+}));
+
+test('Multiple mode with showCheckBoxesMode=\'always\': select a several cards with shift -> unselect a several cards with shift', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const firstCard = cardView.getCard(0);
+ const secondCard = cardView.getCard(1);
+ const thirdCard = cardView.getCard(2);
+ const firstSelectCheckbox = firstCard.getSelectCheckbox();
+ const thirdSelectCheckbox = thirdCard.getSelectCheckbox();
+
+ // act
+ await t.click(firstSelectCheckbox);
+
+ // assert
+ await t.expect(firstCard.isSelected).ok();
+
+ // act
+ await t.click(thirdSelectCheckbox, { modifiers: { shift: true } });
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .ok()
+ .expect(secondCard.isSelected)
+ .ok()
+ .expect(thirdCard.isSelected)
+ .ok();
+
+ // act
+ await t.click(firstSelectCheckbox, { modifiers: { shift: true } });
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .ok()
+ .expect(secondCard.isSelected)
+ .notOk()
+ .expect(thirdCard.isSelected)
+ .notOk();
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ allowSelectAll: true,
+ },
+}));
+
+test('Multiple mode with showCheckBoxesMode=\'onClick\': select a first card by clicking a checkbox -> deselect a first card by clicking a checkbox', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const firstCard = cardView.getCard(0);
+ const firstSelectCheckbox = firstCard.getSelectCheckbox();
+ const firstSelectCheckboxItemContent = firstCard.getToolbarItemContent(0);
+
+ // act
+ await t.hover(firstSelectCheckboxItemContent);
+
+ // assert
+ await t.expect(firstSelectCheckbox.visible).ok();
+
+ // act
+ await t.click(firstSelectCheckbox);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .ok()
+ .expect(cardView.isCheckBoxesHidden())
+ .notOk();
+
+ // act
+ await t.click(firstSelectCheckbox);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .notOk()
+ .expect(cardView.isCheckBoxesHidden())
+ .ok();
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ allowSelectAll: true,
+ },
+}));
+
+test('Multiple mode with showCheckBoxesMode=\'onClick\': select a first card by clicking a card -> deselect a first card by clicking a card', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const firstCard = cardView.getCard(0);
+
+ // act
+ await t.click(firstCard.element);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .ok()
+ .expect(cardView.isCheckBoxesHidden())
+ .ok();
+
+ // act
+ await t.click(firstCard.element, { modifiers: { ctrl: true } });
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .notOk()
+ .expect(cardView.isCheckBoxesHidden())
+ .ok();
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ allowSelectAll: true,
+ },
+}));
+
+test('Multiple mode with showCheckBoxesMode=\'onClick\': select a first card -> select a second card (first card selection state is reset) -> select a first card with ctrl', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const firstCard = cardView.getCard(0);
+ const secondCard = cardView.getCard(1);
+
+ // act
+ await t.click(firstCard.element);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .ok()
+ .expect(cardView.isCheckBoxesHidden())
+ .ok();
+
+ // act
+ await t.click(secondCard.element);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .notOk()
+ .expect(secondCard.isSelected)
+ .ok()
+ .expect(cardView.isCheckBoxesHidden())
+ .ok();
+
+ // act
+ await t.click(firstCard.element, { modifiers: { ctrl: true } });
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .ok()
+ .expect(secondCard.isSelected)
+ .ok()
+ .expect(cardView.isCheckBoxesHidden())
+ .notOk();
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ allowSelectAll: true,
+ },
+}));
+
+test('Multiple mode with showCheckBoxesMode=\'onClick\': select a first card by card hold -> deselect a first card by card hold', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const firstCard = cardView.getCard(0);
+
+ // act
+ await ClientFunction((card) => {
+ $(card()).trigger('dxhold');
+ })(firstCard.element);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .ok()
+ .expect(cardView.isCheckBoxesHidden())
+ .notOk();
+
+ // act
+ await ClientFunction((card) => {
+ $(card()).trigger('dxhold');
+ })(firstCard.element);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .notOk()
+ .expect(cardView.isCheckBoxesHidden())
+ .ok();
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ allowSelectAll: true,
+ },
+}));
+
+test('Multiple mode with showCheckBoxesMode=\'onLongTap\': select a several cards', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const firstCard = cardView.getCard(0);
+ const secondCard = cardView.getCard(1);
+
+ // act
+ await ClientFunction((card) => {
+ $(card()).trigger('dxhold');
+ })(firstCard.element);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .notOk()
+ .expect(cardView.isCheckBoxesHidden())
+ .notOk();
+
+ // act
+ await t.click(firstCard.element);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .ok();
+
+ await t.click(secondCard.element);
+
+ // assert
+ await t
+ .expect(secondCard.isSelected)
+ .ok();
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onLongTap',
+ allowSelectAll: true,
+ },
+}));
+
+test('Select all when selectAllMode = \'allPages\'', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const toolbar = cardView.getToolbar();
+ const selectAllButton = toolbar.getSelectAllButton();
+
+ // act
+ await t.click(selectAllButton);
+
+ // assert
+ await t
+ .expect(toolbar.isSelectAllButtonDisabled())
+ .ok()
+ .expect(toolbar.isClearSelectionButtonDisabled())
+ .notOk()
+ .expect(cardView.getSelectedCardKeys())
+ .eql([0, 1, 2, 3, 4]);
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ allowSelectAll: true,
+ selectAllMode: 'allPages',
+ },
+}));
+
+test('Deselect all when selectAllMode = \'allPages\'', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const toolbar = cardView.getToolbar();
+ const clearSelectionButton = toolbar.getClearSelectionButton();
+
+ // assert
+ await t
+ .expect(toolbar.isSelectAllButtonDisabled())
+ .ok()
+ .expect(toolbar.isClearSelectionButtonDisabled())
+ .notOk();
+
+ // act
+ await t.click(clearSelectionButton);
+
+ // assert
+ await t
+ .expect(toolbar.isSelectAllButtonDisabled())
+ .notOk()
+ .expect(toolbar.isClearSelectionButtonDisabled())
+ .ok()
+ .expect(cardView.getSelectedCardKeys())
+ .eql([]);
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selectedCardKeys: [0, 1, 2, 3, 4],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ allowSelectAll: true,
+ selectAllMode: 'allPages',
+ },
+}));
+
+test('Select all when selectAllMode = \'page\'', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const toolbar = cardView.getToolbar();
+ const selectAllButton = toolbar.getSelectAllButton();
+
+ // act
+ await t.click(selectAllButton);
+
+ // assert
+ await t
+ .expect(toolbar.isSelectAllButtonDisabled())
+ .ok()
+ .expect(toolbar.isClearSelectionButtonDisabled())
+ .notOk()
+ .expect(cardView.getSelectedCardKeys())
+ .eql([0, 1, 2]);
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ paging: {
+ pageSize: 3,
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ allowSelectAll: true,
+ selectAllMode: 'page',
+ },
+}));
+
+test('Deselect all when selectAllMode = \'page\'', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const toolbar = cardView.getToolbar();
+ const clearSelectionButton = toolbar.getClearSelectionButton();
+
+ // assert
+ await t
+ .expect(toolbar.isSelectAllButtonDisabled())
+ .ok()
+ .expect(toolbar.isClearSelectionButtonDisabled())
+ .notOk();
+
+ // act
+ await t.click(clearSelectionButton);
+
+ // assert
+ await t
+ .expect(toolbar.isSelectAllButtonDisabled())
+ .notOk()
+ .expect(toolbar.isClearSelectionButtonDisabled())
+ .ok()
+ .expect(cardView.getSelectedCardKeys())
+ .eql([]);
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ paging: {
+ pageSize: 3,
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selectedCardKeys: [0, 1, 2],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ allowSelectAll: true,
+ selectAllMode: 'page',
+ },
+}));
+
+test('The states of the Select All and Clear selection buttons should update correctly after changing the page when selectAllMode = \'allPages\'', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const toolbar = cardView.getToolbar();
+
+ // assert
+ await t
+ .expect(toolbar.isSelectAllButtonDisabled())
+ .ok()
+ .expect(toolbar.isClearSelectionButtonDisabled())
+ .notOk();
+
+ // act
+ await cardView.apiPageIndex(1);
+
+ // assert
+ await t
+ .expect(toolbar.isSelectAllButtonDisabled())
+ .ok()
+ .expect(toolbar.isClearSelectionButtonDisabled())
+ .notOk();
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ paging: {
+ pageSize: 3,
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selectedCardKeys: [0, 1, 2, 3, 4],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ allowSelectAll: true,
+ selectAllMode: 'allPages',
+ },
+}));
+
+test('The states of the Select All and Clear selection buttons should update correctly after changing the page when selectAllMode = \'page\'', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const toolbar = cardView.getToolbar();
+
+ // assert
+ await t
+ .expect(toolbar.isSelectAllButtonDisabled())
+ .ok()
+ .expect(toolbar.isClearSelectionButtonDisabled())
+ .notOk();
+
+ // act
+ await cardView.apiPageIndex(1);
+
+ // assert
+ await t
+ .expect(toolbar.isSelectAllButtonDisabled())
+ .notOk()
+ .expect(toolbar.isClearSelectionButtonDisabled())
+ .ok()
+ .expect(cardView.getSelectedCardKeys())
+ .eql([0, 1, 2]);
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ paging: {
+ pageSize: 3,
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selectedCardKeys: [0, 1, 2],
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ allowSelectAll: true,
+ selectAllMode: 'page',
+ },
+}));
+
+test('Switching the showCheckBoxesMode option from onClick to always at runtime should work correctly', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const firstCard = cardView.getCard(0);
+
+ // assert
+ await t
+ .expect(cardView.isCheckBoxesHidden())
+ .ok();
+
+ // act
+ await cardView.apiOption('selection.showCheckBoxesMode', 'always');
+
+ // assert
+ await t
+ .expect(cardView.isCheckBoxesHidden())
+ .notOk();
+
+ // act
+ await t.click(firstCard.element);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .notOk()
+ .expect(cardView.getSelectedCardKeys())
+ .eql([]);
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ },
+}));
+
+test('Switching the showCheckBoxesMode option from always to onClick at runtime should work correctly', async (t) => {
+ // arrange
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const firstCard = cardView.getCard(0);
+
+ // assert
+ await t
+ .expect(cardView.isCheckBoxesHidden())
+ .notOk();
+
+ // act
+ await cardView.apiOption('selection.showCheckBoxesMode', 'onClick');
+
+ // assert
+ await t
+ .expect(cardView.isCheckBoxesHidden())
+ .ok();
+
+ // act
+ await t.click(firstCard.element);
+
+ // assert
+ await t
+ .expect(firstCard.isSelected)
+ .ok()
+ .expect(cardView.getSelectedCardKeys())
+ .eql([0]);
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ },
+}));
diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/visual.ts b/e2e/testcafe-devextreme/tests/cardView/selection/visual.ts
new file mode 100644
index 000000000000..cd453c1b8aa3
--- /dev/null
+++ b/e2e/testcafe-devextreme/tests/cardView/selection/visual.ts
@@ -0,0 +1,355 @@
+import CardView from 'devextreme-testcafe-models/cardView';
+import { createScreenshotsComparer } from 'devextreme-screenshot-comparer';
+import { ClientFunction } from 'testcafe';
+import url from '../../../helpers/getPageUrl';
+import { createWidget } from '../../../helpers/createWidget';
+import { testScreenshot } from '../../../helpers/themeUtils';
+
+fixture.skip`Selection.Visual`
+ .page(url(__dirname, '../../container.html'));
+
+const CARD_VIEW_SELECTOR = '#container';
+
+test('Single mode', async (t) => {
+ const { takeScreenshot, compareResults } = createScreenshotsComparer(t);
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+
+ await testScreenshot(t, takeScreenshot, 'card-view_single_selection.png', { element: cardView.element });
+
+ await t
+ .expect(compareResults.isValid())
+ .ok(compareResults.errorMessages());
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selectedCardKeys: [0],
+ selection: {
+ mode: 'single',
+ },
+}));
+
+test('Multiple mode with Select All/Deselect All and showCheckBoxesMode = \'none\'', async (t) => {
+ const { takeScreenshot, compareResults } = createScreenshotsComparer(t);
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+
+ await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_none.png', { element: cardView.element });
+
+ await t
+ .expect(compareResults.isValid())
+ .ok(compareResults.errorMessages());
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'none',
+ allowSelectAll: true,
+ },
+}));
+
+test('Multiple mode with Select All/Deselect All and showCheckBoxesMode = \'always\'', async (t) => {
+ const { takeScreenshot, compareResults } = createScreenshotsComparer(t);
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+
+ await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_always.png', { element: cardView.element });
+
+ await t
+ .expect(compareResults.isValid())
+ .ok(compareResults.errorMessages());
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'always',
+ allowSelectAll: true,
+ },
+}));
+
+test('Multiple mode with Select All/Deselect All and showCheckBoxesMode = \'onClick\'', async (t) => {
+ const { takeScreenshot, compareResults } = createScreenshotsComparer(t);
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const firstSelectCheckboxItemContent = cardView
+ .getCard(0)
+ .getToolbarItemContent(0);
+
+ await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1.png', { element: cardView.element });
+
+ await t.hover(firstSelectCheckboxItemContent);
+
+ await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2.png', { element: cardView.element });
+
+ await t.hover(cardView.element);
+
+ await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3.png', { element: cardView.element });
+
+ await t
+ .expect(compareResults.isValid())
+ .ok(compareResults.errorMessages());
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ allowSelectAll: true,
+ },
+}));
+
+test('Multiple mode with a selected card and showCheckBoxesMode = \'onClick\'', async (t) => {
+ const { takeScreenshot, compareResults } = createScreenshotsComparer(t);
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+
+ await testScreenshot(t, takeScreenshot, 'card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick.png', { element: cardView.element });
+
+ await t
+ .expect(compareResults.isValid())
+ .ok(compareResults.errorMessages());
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ selectedCardKeys: [0],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ allowSelectAll: true,
+ },
+}));
+
+test('Multiple mode with selected cards and showCheckBoxesMode = \'onClick\'', async (t) => {
+ const { takeScreenshot, compareResults } = createScreenshotsComparer(t);
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+
+ await testScreenshot(t, takeScreenshot, 'card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick.png', { element: cardView.element });
+
+ await t
+ .expect(compareResults.isValid())
+ .ok(compareResults.errorMessages());
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ selectedCardKeys: [0, 1],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onClick',
+ allowSelectAll: true,
+ },
+}));
+
+test('Multiple mode with Select All/Deselect All and showCheckBoxesMode = \'onLongTap\'', async (t) => {
+ const { takeScreenshot, compareResults } = createScreenshotsComparer(t);
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+ const firstCard = cardView.getCard(0);
+
+ await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1.png', { element: cardView.element });
+
+ await ClientFunction((card) => {
+ $(card()).trigger('dxhold');
+ })(firstCard.element);
+
+ await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2.png', { element: cardView.element });
+
+ await ClientFunction((card) => {
+ $(card()).trigger('dxhold');
+ })(firstCard.element);
+
+ await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3.png', { element: cardView.element });
+
+ await t
+ .expect(compareResults.isValid())
+ .ok(compareResults.errorMessages());
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ showCheckBoxesMode: 'onLongTap',
+ allowSelectAll: true,
+ },
+}));
+
+test('Multiple mode without Select All/Deselect All', async (t) => {
+ const { takeScreenshot, compareResults } = createScreenshotsComparer(t);
+ const cardView = new CardView(CARD_VIEW_SELECTOR);
+
+ await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_without_select-all.png', { element: cardView.element });
+
+ await t
+ .expect(compareResults.isValid())
+ .ok(compareResults.errorMessages());
+}).before(async () => createWidget('dxCardView', {
+ dataSource: [
+ {
+ id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0',
+ },
+ {
+ id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1',
+ },
+ {
+ id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2',
+ },
+ {
+ id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3',
+ },
+ {
+ id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4',
+ },
+ ],
+ cardHeader: {
+ captionExpr: () => 'title',
+ },
+ columns: ['A', 'B', 'C'],
+ keyExpr: 'id',
+ height: 700,
+ selection: {
+ mode: 'multiple',
+ allowSelectAll: false,
+ },
+}));
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 6dc3158b5abd..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
@@ -73,8 +73,6 @@ export class ContentView extends ContentViewBase {
protected override component = ContentViewComponent;
protected override getProps() {
- const allowSelectOnClick = this.selectionController.allowSelectOnClick();
-
return combined({
...this.getBaseProps(),
contentProps: combined({
@@ -88,7 +86,7 @@ export class ContentView extends ContentViewBase {
minWidth: this.cardMinWidth,
maxWidth: this.options.oneWay('cardMaxWidth'),
isCheckBoxesRendered: this.selectionController.isCheckBoxesRendered,
- allowSelectOnClick,
+ allowSelectOnClick: this.selectionController.allowSelectOnClick,
onHold: this.onCardHold.bind(this),
onClick: this.options.action('onCardClick'),
onDblClick: this.options.action('onCardDblClick'),
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
index 5e10621efd5d..c830051f2bb6 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts
@@ -65,27 +65,26 @@ export class SelectionController {
public readonly _isCheckBoxesVisible = state(false);
public readonly isCheckBoxesVisible = computed(
- (isCheckBoxesRendered, _isCheckBoxesVisible) => {
- if (isCheckBoxesRendered) {
- const { showCheckBoxesMode } = this.selectionOption.unreactive_get();
+ (selectionOption, _isCheckBoxesVisible) => {
+ const { mode, showCheckBoxesMode } = selectionOption;
+ if (mode === SelectionMode.Multiple) {
return showCheckBoxesMode !== ShowCheckBoxesMode.OnClick || _isCheckBoxesVisible;
}
return false;
},
[
- this.isCheckBoxesRendered,
+ this.selectionOption,
this._isCheckBoxesVisible,
],
);
public readonly needToHiddenCheckBoxes = computed(
- (isCheckBoxesVisible) => {
- const { showCheckBoxesMode } = this.selectionOption.unreactive_get();
- const isCheckBoxesRendered = this.isCheckBoxesRendered.unreactive_get();
+ (isCheckBoxesVisible, selectionOption) => {
+ const { mode, showCheckBoxesMode } = selectionOption;
- if (isCheckBoxesRendered && showCheckBoxesMode === ShowCheckBoxesMode.OnClick) {
+ if (mode === SelectionMode.Multiple && showCheckBoxesMode === ShowCheckBoxesMode.OnClick) {
return !isCheckBoxesVisible;
}
@@ -93,9 +92,19 @@ export class SelectionController {
},
[
this.isCheckBoxesVisible,
+ this.selectionOption,
],
);
+ public readonly allowSelectOnClick = computed(
+ (selectionOption) => {
+ const { mode, showCheckBoxesMode } = selectionOption;
+
+ return mode !== SelectionMode.Multiple || showCheckBoxesMode !== ShowCheckBoxesMode.Always;
+ },
+ [this.selectionOption],
+ );
+
constructor(
private readonly options: OptionsController,
private readonly dataController: DataController,
@@ -136,10 +145,6 @@ export class SelectionController {
}
}, [this.selectedCardKeys, this.selectionOption]);
- effect((selectedCardKeys, selectionOption) => {
- this.updateSelectionToolbarButtons(selectedCardKeys, selectionOption);
- }, [this.selectedCardKeys, this.selectionOption, this.dataController.items]);
-
effect((isLoaded) => {
if (isLoaded) {
const selectedCardKeys = this.selectedCardKeys.unreactive_get();
@@ -147,6 +152,10 @@ export class SelectionController {
this.selectCards(selectedCardKeys);
}
}, [this.dataController.isLoaded]);
+
+ effect((selectedCardKeys, selectionOption) => {
+ this.updateSelectionToolbarButtons(selectedCardKeys, selectionOption);
+ }, [this.selectedCardKeys, this.selectionOption, this.dataController.items]);
}
private getSelectionConfig(dataSource, selectionOption): object {
@@ -365,12 +374,6 @@ export class SelectionController {
this._isCheckBoxesVisible.update(value);
}
- public allowSelectOnClick(): boolean {
- const { mode, showCheckBoxesMode } = this.selectionOption.unreactive_get();
-
- return mode !== SelectionMode.Multiple || showCheckBoxesMode !== ShowCheckBoxesMode.Always;
- }
-
public processLongTap(row: DataRow): void {
const { mode, showCheckBoxesMode } = this.selectionOption.unreactive_get();
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);
+ }
+}
From 82c976e66ab5330cb9689b2919fb8510102ef8ea Mon Sep 17 00:00:00 2001
From: Alyar <>
Date: Fri, 28 Mar 2025 05:26:01 +0400
Subject: [PATCH 7/8] CardView: Add public methods
---
.../new/grid_core/selection/public_methods.ts | 39 +++++++++++++++++--
1 file changed, 35 insertions(+), 4 deletions(-)
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
index 123a9a1d7776..8d1ac090b53f 100644
--- 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
@@ -1,5 +1,8 @@
/* 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';
@@ -10,12 +13,40 @@ export function PublicMethods>(GridCo
return this.selectionController.isCardSelected(key);
}
- public clearSelection(): void {
- this.selectionController.clearSelection();
- }
-
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();
+ }
};
}
From c933a4147bc98efc1e08ee319f33ddd7e48ce1fc Mon Sep 17 00:00:00 2001
From: Alyar <>
Date: Fri, 28 Mar 2025 05:55:56 +0400
Subject: [PATCH 8/8] Fix comments
---
.../content_view/content/card/card.tsx | 6 +-
.../new/grid_core/selection/controller.ts | 78 ++++++++++---------
2 files changed, 42 insertions(+), 42 deletions(-)
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.tsx
index 5e46e632a409..b24c31f5a5ab 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.tsx
+++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.tsx
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { isCommandKeyPressed } from '@js/common/core/events/utils/index';
-import { off, on } from '@js/events';
+import { off, on } from '@js/events/index';
import { combineClasses } from '@ts/core/utils/combine_classes';
import type { DataRow } from '@ts/grids/new/grid_core/columns_controller/types';
import type { DataObject } from '@ts/grids/new/grid_core/data_controller/types';
@@ -59,8 +59,6 @@ export interface CardProps {
toolbar?: CardHeaderItem[];
- width?: number;
-
isCheckBoxesRendered?: boolean;
template?: (row: DataRow) => JSX.Element;
@@ -120,7 +118,7 @@ export class Card extends Component {
>
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
index c830051f2bb6..77521ef65c84 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts
@@ -105,6 +105,14 @@ export class SelectionController {
[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,
@@ -153,9 +161,9 @@ export class SelectionController {
}
}, [this.dataController.isLoaded]);
- effect((selectedCardKeys, selectionOption) => {
- this.updateSelectionToolbarButtons(selectedCardKeys, selectionOption);
- }, [this.selectedCardKeys, this.selectionOption, this.dataController.items]);
+ effect((selectedCardKeys) => {
+ this.updateSelectionToolbarButtons(selectedCardKeys);
+ }, [this.selectedCardKeys, this.dataController.items]);
}
private getSelectionConfig(dataSource, selectionOption): object {
@@ -241,44 +249,38 @@ export class SelectionController {
private updateSelectionToolbarButtons(
selectedCardKeys: SelectedCardKeys,
- selectionOption: SelectionOptions,
) {
- if (selectionOption.mode === SelectionMode.Multiple && selectionOption.allowSelectAll) {
- 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'),
+ const isSelectAll = this.isSelectAll();
+ const isOnePageSelectAll = this.isOnePageSelectAll();
+
+ this.toolbarController.addDefaultItem({
+ name: 'selectAllButton',
+ widget: 'dxButton',
+ options: {
+ icon: 'selectall',
+ onClick: () => {
+ this.selectAll();
},
- location: 'before',
- locateInMenu: 'auto',
- });
- 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'),
+ 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();
},
- location: 'before',
- locateInMenu: 'auto',
- });
- } else {
- this.toolbarController.removeDefaultItem('selectAllButton');
- this.toolbarController.removeDefaultItem('clearSelectionButton');
- }
+ disabled: isOnePageSelectAll ? isSelectAll === false : selectedCardKeys.length === 0,
+ text: messageLocalization.format('dxCardView-clearSelection'),
+ },
+ location: 'before',
+ locateInMenu: 'auto',
+ }, this.needToAddSelectionButtons);
}
private getItemKeysByIndexes(indexes: number[]): Key[] {