diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (fluent-blue-light).png new file mode 100644 index 000000000000..8f3f5f76f989 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (generic-light).png new file mode 100644 index 000000000000..63ec3409be65 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (material-blue-light).png new file mode 100644 index 000000000000..bf817684bff8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (fluent-blue-light).png new file mode 100644 index 000000000000..b54354d5a569 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (generic-light).png new file mode 100644 index 000000000000..6a69641f7cbf Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (material-blue-light).png new file mode 100644 index 000000000000..2275798c2681 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (fluent-blue-light).png new file mode 100644 index 000000000000..940ef9ca78e1 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (generic-light).png new file mode 100644 index 000000000000..aa802f84e1a5 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (material-blue-light).png new file mode 100644 index 000000000000..06ae64fee9d4 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (fluent-blue-light).png new file mode 100644 index 000000000000..cce3f986af65 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (generic-light).png new file mode 100644 index 000000000000..9ebde77be913 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (material-blue-light).png new file mode 100644 index 000000000000..d3bc948b61f8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (fluent-blue-light).png new file mode 100644 index 000000000000..cce3f986af65 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (generic-light).png new file mode 100644 index 000000000000..9ebde77be913 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (material-blue-light).png new file mode 100644 index 000000000000..d3bc948b61f8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (fluent-blue-light).png new file mode 100644 index 000000000000..0d8961f88054 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (generic-light).png new file mode 100644 index 000000000000..d5939b39029b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (material-blue-light).png new file mode 100644 index 000000000000..50aacdcd2edc Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (fluent-blue-light).png new file mode 100644 index 000000000000..cce3f986af65 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (generic-light).png new file mode 100644 index 000000000000..9ebde77be913 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (material-blue-light).png new file mode 100644 index 000000000000..d3bc948b61f8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (fluent-blue-light).png new file mode 100644 index 000000000000..cce3f986af65 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (generic-light).png new file mode 100644 index 000000000000..9ebde77be913 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (material-blue-light).png new file mode 100644 index 000000000000..d3bc948b61f8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (fluent-blue-light).png new file mode 100644 index 000000000000..940ef9ca78e1 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (generic-light).png new file mode 100644 index 000000000000..aa802f84e1a5 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (material-blue-light).png new file mode 100644 index 000000000000..06ae64fee9d4 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (fluent-blue-light).png new file mode 100644 index 000000000000..cce3f986af65 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (generic-light).png new file mode 100644 index 000000000000..9ebde77be913 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (material-blue-light).png new file mode 100644 index 000000000000..d3bc948b61f8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (fluent-blue-light).png new file mode 100644 index 000000000000..8dc25df64abc Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (generic-light).png new file mode 100644 index 000000000000..f14dc159f4b9 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (material-blue-light).png new file mode 100644 index 000000000000..9c8b461bcf48 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (fluent-blue-light).png new file mode 100644 index 000000000000..4b71c2ceb701 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (generic-light).png new file mode 100644 index 000000000000..848d8b1818c2 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (material-blue-light).png new file mode 100644 index 000000000000..6ba45703fb65 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (material-blue-light).png differ 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..4fc8708c2b99 --- /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`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-scss/scss/widgets/base/cardView/content_view/content/card/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_index.scss index c425dc3f79f5..cdd89b6143a7 100644 --- a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_index.scss @@ -10,4 +10,9 @@ border: $cardview-card-border-size solid $cardview-card-border-color; border-radius: $cardview-card-border-radius; background-color: $cardview-card-background-color; + overflow: hidden; +} + +.dx-cardview-card-selection { + background-color: $cardview-card-selection-background-color; } diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_variables.scss index 6ed020add8f6..b4528656fde3 100644 --- a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_variables.scss +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_variables.scss @@ -4,3 +4,4 @@ $cardview-card-border-size: null !default; $cardview-card-min-width: null !default; $cardview-card-border-radius: null !default; $cardview-card-background-color: null !default; +$cardview-card-selection-background-color: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_index.scss index 3cc44c0919c0..0a0f670986cc 100644 --- a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_index.scss @@ -11,3 +11,27 @@ } } } + +.dx-cardview-select-checkboxes-hidden .dx-cardview-card:not(.dx-cardview-card-selection) .dx-cardview-select-checkbox { + .dx-checkbox { + display: none; + } + + .dx-toolbar-item-content::before { + content: ''; + width: 20px; + height: 20px; + display: inline-block; + pointer-events: none; + } + + .dx-toolbar-item-content:hover { + &::before { + display: none; + } + + .dx-checkbox { + display: inline-block; + } + } +} diff --git a/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss index 8dc0532a29a2..8cab6255c17e 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss @@ -23,3 +23,5 @@ $cardview-header-filter-icon-selected-color: $base-accent !default; $cardview-card-content-field-value-highlight-color: $base-inverted-text-color !default; $cardview-card-content-field-value-highlight-background: $base-accent !default; + +$cardview-card-selection-background-color: #EBF3FC; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.events.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.events.test.tsx index d91b4b2c49ae..52d831b15266 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.events.test.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.events.test.tsx @@ -3,23 +3,21 @@ /* eslint-disable @typescript-eslint/init-declarations */ import { describe, expect, it } from '@jest/globals'; -import { render } from 'inferno'; +import { createRef, render } from 'inferno'; -import { Card } from './card'; +import { Card, CLASSES } from './card'; -const mockOnDblClick = { +const createMockCallback = () => ({ called: false, - call() { + call(): void { this.called = true; }, -}; +}); -const mockOnClick = { - called: false, - call() { - this.called = true; - }, -}; +const mockSelectCard = createMockCallback(); +const mockOnDblClick = createMockCallback(); +const mockOnClick = createMockCallback(); +const mockOnHold = createMockCallback(); const props = { row: { @@ -70,12 +68,10 @@ const props = { maxWidth: 300, width: 300, minWidth: 300, - onDblClick: mockOnDblClick.call(), - onClick: mockOnClick.call(), -}; - -const CLASSES = { - card: 'dx-cardview-card', + selectCard: mockSelectCard.call.bind(mockSelectCard), + onDblClick: mockOnDblClick.call.bind(mockOnDblClick), + onClick: mockOnClick.call.bind(mockOnClick), + onHold: mockOnHold.call.bind(mockOnHold), }; describe('Events', () => { @@ -84,29 +80,32 @@ describe('Events', () => { beforeEach(() => { container = document.createElement('div'); // @ts-expect-error - render(, container); + render(, container); }); it('should trigger onClick event', () => { - // @ts-expect-error - render(, container); - const cardElement = container.querySelector(`.${CLASSES.card}`); cardElement?.dispatchEvent(new MouseEvent('click')); expect(mockOnClick.called).toBe(true); }); - it('should trigger onDblClick event', () => { - // @ts-expect-error - render(, container); - + it.skip('should trigger onDblClick event', () => { const cardElement = container.querySelector(`.${CLASSES.card}`); + cardElement?.dispatchEvent(new MouseEvent('dblclick')); expect(mockOnDblClick.called).toBe(true); }); + it('should trigger onHold event', () => { + const cardElement = container.querySelector(`.${CLASSES.card}`); + + cardElement?.dispatchEvent(new MouseEvent('dxhold')); + + expect(mockOnHold.called).toBe(true); + }); + it('should trigger onHoverChanged event on mouse enter', () => { const mockHover: { called: boolean; fn: ({ isHovered }: { isHovered: boolean }) => void } = { called: false, @@ -161,3 +160,42 @@ describe('Events', () => { expect(fieldValue?.textContent).toBe('devextreme'); }); }); + +describe('Callbacks', () => { + describe('selectCard', () => { + // @ts-expect-errors + beforeEach(() => { + mockSelectCard.called = false; + }); + + describe('when allowSelectOnClick = true', () => { + it('should rise it', () => { + const container = document.createElement('div'); + const newProps = { ...props, elementRef: createRef(), allowSelectOnClick: true }; + // @ts-expect-error + render(, container); + + const cardElement = container.querySelector(`.${CLASSES.card}`); + + cardElement?.dispatchEvent(new MouseEvent('click')); + + expect(mockSelectCard.called).toBe(true); + }); + }); + + describe('when allowSelectOnClick = false', () => { + it('should not rise it', () => { + const container = document.createElement('div'); + const newProps = { ...props, elementRef: createRef(), allowSelectOnClick: false }; + // @ts-expect-error + render(, container); + + const cardElement = container.querySelector(`.${CLASSES.card}`); + + cardElement?.dispatchEvent(new MouseEvent('click')); + + expect(mockSelectCard.called).toBe(false); + }); + }); + }); +}); 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 a46eda640c6d..34b08c80cc59 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,10 +1,13 @@ -/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ - +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { isCommandKeyPressed } from '@js/common/core/events/utils/index'; +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'; import type { InfernoNode, RefObject } from 'inferno'; import { Component, createRef } from 'inferno'; +import type { SelectCardOptions } from '../../types'; import { Cover } from './cover'; import { Field } from './field'; import type { CardHeaderItem } from './header'; @@ -14,10 +17,11 @@ export const CLASSES = { card: 'dx-cardview-card', cardHover: 'dx-cardview-card-hoverable', content: 'dx-cardview-card-content', + selectCard: 'dx-cardview-card-selection', }; export interface CardClickEvent { - event: MouseEvent; + event?: MouseEvent; row: DataRow; } @@ -33,12 +37,18 @@ export interface CardPreparedEvent { export interface CardProps { row: DataRow; + allowSelectOnClick?: boolean; + cover?: { imageExpr?: (data: DataObject) => string; altExpr?: (data: DataObject) => string; }; + header?: { + captionExpr?: (data: DataObject) => string; + }; + elementRef?: RefObject; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -48,15 +58,21 @@ export interface CardProps { toolbar?: CardHeaderItem[]; + isCheckBoxesRendered?: boolean; + template?: (row: DataRow) => JSX.Element; onClick?: (e: CardClickEvent) => void; + onHold?: (e: CardClickEvent) => void; + onDblClick?: (e: CardClickEvent) => void; onHoverChanged?: (e: CardHoverEvent) => void; onPrepared?: (e: CardPreparedEvent) => void; + + selectCard?: (row: DataRow, options: SelectCardOptions) => void; } export class Card extends Component { @@ -75,16 +91,16 @@ export class Card extends Component { fieldTemplate: FieldTemplate = Field, hoverStateEnabled, cover, + row, } = this.props; - const className = [ - CLASSES.card, - hoverStateEnabled ? CLASSES.cardHover : '', - ].filter(Boolean).join(' '); + const className = combineClasses({ + [CLASSES.card]: true, + [CLASSES.cardHover]: !!hoverStateEnabled, + [CLASSES.selectCard]: !!row.isSelected, + }); - // @ts-expect-error const imageSrc = cover?.imageExpr?.(this.props.row.data); - // @ts-expect-error const alt = cover?.altExpr?.(this.props.row.data); return ( @@ -92,13 +108,15 @@ export class Card extends Component { className={className} tabIndex={0} ref={this.props.elementRef} - onClick={this.handleClick} onDblClick={this.handleDoubleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} > {imageSrc && ( {
{this.props.row.cells.map((cell, index) => ( { componentDidMount(): void { const { onPrepared } = this.props; + if (onPrepared) { onPrepared({ instance: this }); } + + on(this.containerRef.current!, 'dxclick', this.handleClick); + + if (this.props.onHold) { + on(this.containerRef.current!, 'dxhold', this.handleHold); + } + } + + componentWillUnmount(): void { + off(this.containerRef.current!, 'dxclick', this.handleClick); + + if (this.props.onHold) { + off(this.containerRef.current!, 'dxhold', this.handleHold); + } } handleMouseEnter = (): void => { @@ -142,12 +174,29 @@ export class Card extends Component { }; handleClick = (event: MouseEvent): void => { - const { onClick, row } = this.props; + const { + allowSelectOnClick, + onClick, + selectCard, + row, + } = this.props; + onClick?.({ event, row }); + + if (allowSelectOnClick) { + selectCard?.(row, { control: isCommandKeyPressed(event), shift: event.shiftKey }); + } }; handleDoubleClick = (event: MouseEvent): void => { const { onDblClick, row } = this.props; onDblClick?.({ event, row }); }; + + handleHold = (event: MouseEvent): void => { + const { onHold, row } = this.props; + + onHold?.({ event, row }); + event.stopPropagation(); + }; } diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.test.tsx index 7b11faed033a..7cf06f9421d6 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.test.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.test.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-extraneous-dependencies */ + import { describe, expect, it } from '@jest/globals'; import { render } from 'inferno'; @@ -68,4 +68,20 @@ describe('CardHeader', () => { expect(customHeader).not.toBeNull(); expect(customHeader?.textContent).toBe('Custom Header'); }); + + it('should render a selection checkbox', () => { + const container = document.createElement('div'); + render( + , + container, + ); + + const checkboxItem = container.querySelector('.dx-cardview-select-checkbox'); + expect(checkboxItem).not.toBeNull(); + }); }); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.tsx index 8641fc8f36e5..9da452c883d4 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.tsx @@ -1,17 +1,28 @@ -import type { dxToolbarItem } from '@js/ui/toolbar'; +import { isCommandKeyPressed } from '@js/common/core/events/utils/index'; +import type { ValueChangedEvent } from '@js/ui/check_box'; import type { DataRow } from '@ts/grids/new/grid_core/columns_controller/types'; import { Toolbar } from '@ts/grids/new/grid_core/inferno_wrappers/toolbar'; import { Component } from 'inferno'; +import type { SelectCardOptions } from '../../types'; + export const CLASSES = { cardHeader: 'dx-cardview-card-header', + cardSelectCheckBox: 'dx-cardview-select-checkbox', }; +export interface CheckBoxClickEvent { + event?: MouseEvent; + row: DataRow; +} + export interface CardHeaderItem { location: 'before' | 'after'; widget?: string; text?: string; - options?: dxToolbarItem; + cssClass?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options?: any; } export interface CardHeaderProps { @@ -20,9 +31,38 @@ export interface CardHeaderProps { captionExpr?: string; template?: (items: CardHeaderItem[]) => JSX.Element; row?: DataRow; + isCheckBoxesRendered?: boolean; + selectCard?: (row: DataRow, options: SelectCardOptions) => void; } export class CardHeader extends Component { + private getCheckBoxItem(): CardHeaderItem | null { + const { isCheckBoxesRendered, selectCard, row } = this.props; + + if (row && isCheckBoxesRendered) { + return { + location: 'before', + widget: 'dxCheckBox', + cssClass: CLASSES.cardSelectCheckBox, + options: { + value: row.isSelected, + onValueChanged: (e: ValueChangedEvent): void => { + const event = e.event as MouseEvent; + + selectCard?.(row, { + control: isCommandKeyPressed(event), + shift: event.shiftKey, + needToUpdateCheckboxes: true, + }); + event.stopPropagation(); + }, + }, + }; + } + + return null; + } + render(): JSX.Element | null { const { visible = true, @@ -36,11 +76,14 @@ export class CardHeader extends Component { return null; } + const checkBoxItem = this.getCheckBoxItem(); + const captionItem: CardHeaderItem | null = captionExpr && row?.[captionExpr] ? { location: 'before', text: row[captionExpr] } : null; - const finalItems = captionItem ? [captionItem, ...items] : items; + const finalItems = [checkBoxItem, captionItem, ...items] + .filter((item): item is CardHeaderItem => !!item); if (template) { return template(finalItems); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/content.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/content.tsx index 016acae38842..f8e908f24ade 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/content.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/content.tsx @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { combineClasses } from '@ts/core/utils/combine_classes'; import type { DataRow } from '@ts/grids/new/grid_core/columns_controller/types'; import type { RefObject } from 'inferno'; import { Component, createRef } from 'inferno'; @@ -16,6 +17,8 @@ export interface ContentProps { cardsPerRow?: number; + needToHiddenCheckBoxes?: boolean; + cardProps?: { toolbar?: CardHeaderItem[]; minWidth?: number; @@ -26,6 +29,7 @@ export interface ContentProps { export const CLASSES = { content: 'dx-cardview-content', grid: 'dx-cardview-content-grid', + selectCheckBoxesHidden: 'dx-cardview-select-checkboxes-hidden', }; function getInfernoCardKey(card: DataRow): undefined | string | number { @@ -73,10 +77,15 @@ export class Content extends Component { 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 (
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 ac12d5ee4440..b4bec55041ef 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx @@ -4,11 +4,13 @@ import { compileGetter } from '@js/core/utils/data'; import { isDefined } from '@js/core/utils/type'; import { combined, computed, state } from '@ts/core/reactive/index'; import type { OptionsController } from '@ts/grids/new/card_view/options_controller'; +import type { DataRow } from '@ts/grids/new/grid_core/columns_controller/types'; import { ContentView as ContentViewBase } from '../../grid_core/content_view/view'; import type { DataObject } from '../../grid_core/data_controller/types'; import type { ContentViewProps } from './content_view'; import { ContentView as ContentViewComponent } from './content_view'; +import type { CardHoldEvent, SelectCardOptions } from './types'; import { factors } from './utils'; export class ContentView extends ContentViewBase { @@ -43,13 +45,16 @@ export class ContentView extends ContentViewBase { ...this.getBaseProps(), contentProps: combined({ items: this.itemsController.items, - // items: computed((virtualState) => virtualState.virtualItems, [this.virtualState]), + needToHiddenCheckBoxes: this.selectionController.needToHiddenCheckBoxes, fieldTemplate: this.options.template('fieldTemplate'), cardsPerRow: this.cardsPerRow, onRowHeightChange: this.rowHeight.update.bind(this.rowHeight), cardProps: combined({ minWidth: this.cardMinWidth, maxWidth: this.options.oneWay('cardMaxWidth'), + isCheckBoxesRendered: this.selectionController.isCheckBoxesRendered, + allowSelectOnClick: this.selectionController.allowSelectOnClick, + onHold: this.onCardHold.bind(this), onClick: this.options.action('onCardClick'), onDblClick: this.options.action('onCardDblClick'), onHoverChanged: this.options.action('onCardHoverChanged'), @@ -68,6 +73,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), }), }), }); @@ -82,4 +88,16 @@ export class ContentView extends ContentViewBase { // @ts-expect-error return compileGetter(expr); } + + private selectCard(row: DataRow, options: SelectCardOptions) { + if (options.needToUpdateCheckboxes) { + this.selectionController.updateSelectionCheckBoxesVisible(true); + } + + this.selectionController.changeCardSelection(row.index, options); + } + + private onCardHold(e: CardHoldEvent) { + this.selectionController.processLongTap(e.row); + } } diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/types.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/types.ts index bc0cf045db20..ee36cd4e12fe 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/types.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/types.ts @@ -3,7 +3,7 @@ import type { ColumnBase } from '@js/common/grids'; import type { HeaderFilterColumnOptions } from '@ts/grids/new/grid_core/filtering/header_filter/index'; import type { ComponentType } from 'inferno'; -import type { DataObject } from '../data_controller/types'; +import type { DataObject, Key } from '../data_controller/types'; import type { HighlightedTextItem } from '../search/types'; type InheritedColumnProps = @@ -71,9 +71,11 @@ export interface Cell { export interface DataRow { cells: Cell[]; - key: unknown; + key: Key; - data: unknown; + data: DataObject; + + isSelected?: boolean; index: number; } diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx index 08158ba4b6bb..936bddfde638 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx +++ b/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx @@ -7,6 +7,7 @@ import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/co import { View } from '@ts/grids/new/grid_core/core/view'; import { DataController } from '@ts/grids/new/grid_core/data_controller/index'; import { ErrorController } from '@ts/grids/new/grid_core/error_controller/error_controller'; +import { SelectionController } from '@ts/grids/new/grid_core/selection/controller'; import { createRef } from 'inferno'; import { ItemsController } from '../items_controller/items_controller'; @@ -33,6 +34,7 @@ export abstract class ContentView extends View { OptionsController, ErrorController, ColumnsController, + SelectionController, ItemsController, ] as const; @@ -41,6 +43,7 @@ export abstract class ContentView extends View { protected readonly options: OptionsController, protected readonly errorController: ErrorController, protected readonly columnsController: ColumnsController, + protected readonly selectionController: SelectionController, protected readonly itemsController: ItemsController, ) { super(); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts index b3a2682450b0..ae4614b13a36 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 @@ -65,6 +65,8 @@ export class DataController { [this.totalCount, this.pageSize], ); + public readonly isLoaded = state(false); + private readonly normalizedRemoteOptions = computed( (remoteOperations, dataSource) => { const store = dataSource.store(); @@ -88,6 +90,7 @@ export class DataController { effect( (dataSource) => { const changedCallback = (e?): void => { + this.isLoaded.update(true); this.onChanged(dataSource, e); }; const loadingChangedCallback = (): void => { diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap index 4b5f7b23564e..25afd10b1355 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap +++ b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap @@ -80,6 +80,7 @@ exports[`ItemsController createDataRow should process data object to data row us "id": 1, }, "index": 0, + "isSelected": false, "key": 1, } `; @@ -164,6 +165,65 @@ exports[`ItemsController createDataRow should process data object to data row us "id": 1, }, "index": 0, + "isSelected": true, "key": 1, } `; + +exports[`ItemsController setSelectionState should update the select state of the item 1`] = ` +InterruptableComputed { + "callbacks": Set {}, + "depInitialized": [ + true, + true, + true, + true, + ], + "depValues": [ + [ + { + "a": "my a value", + "id": 1, + }, + ], + [], + [ + 1, + ], + { + "caseSensitive": false, + "enabled": true, + "searchStr": "", + }, + ], + "isInitialized": true, + "subscriptions": SubscriptionBag { + "subscriptions": [ + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + ], + }, + "value": [ + { + "cells": [], + "data": { + "a": "my a value", + "id": 1, + }, + "index": 0, + "isSelected": true, + "key": 1, + }, + ], +} +`; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.test.ts index c99b849d34fd..34c6a954dcc5 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.test.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.test.ts @@ -61,8 +61,21 @@ describe('ItemsController', () => { }); const columns = columnsController.columns.unreactive_get(); - const dataRow = itemsController.createDataRow(dataObject, columns, 0); + const dataRow = itemsController.createDataRow(dataObject, columns, 0, [1]); expect(dataRow).toMatchSnapshot(); }); }); + + describe('setSelectionState', () => { + it('should update the select state of the item', () => { + const { itemsController } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, a: 'my a value' }], + }); + + itemsController.setSelectionState([1]); + + expect(itemsController.items).toMatchSnapshot(); + }); + }); }); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts index 821852754286..1f3ff3cc1609 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts @@ -1,13 +1,16 @@ +import { equalByValue } from '@js/core/utils/common'; import formatHelper from '@js/format_helper'; -import { computed } from '@ts/core/reactive/index'; +import { computed, state } from '@ts/core/reactive/index'; import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/columns_controller'; import { DataController } from '@ts/grids/new/grid_core/data_controller/data_controller'; import { SearchController } from '@ts/grids/new/grid_core/search/index'; import type { Column, DataRow } from '../columns_controller/types'; -import type { DataObject } from '../data_controller/types'; +import type { DataObject, Key } from '../data_controller/types'; export class ItemsController { + private readonly selectedCardKeys = state([]); + public static dependencies = [ DataController, ColumnsController, @@ -18,6 +21,7 @@ export class ItemsController { ( dataItems, columns: Column[], + selectedCardKeys, // NOTE: We should trigger computed by search options change // But all work with these options encapsulated in SearchHighlightTextProcessor // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -27,11 +31,13 @@ export class ItemsController { item, columns, itemIndex, + selectedCardKeys, ), ), [ this.dataController.items, this.columnsController.visibleColumns, + this.selectedCardKeys, this.searchController.highlightTextOptions, ], ); @@ -42,10 +48,15 @@ export class ItemsController { private readonly searchController: SearchController, ) {} + public setSelectionState(keys: Key[]): void { + this.selectedCardKeys.update(keys); + } + public createDataRow( data: DataObject, columns: Column[], itemIndex: number, + selectedCardKeys?: Key[], ): DataRow { const itemKey = this.dataController.getDataKey(data); @@ -71,7 +82,15 @@ export class ItemsController { }), key: itemKey, index: itemIndex, + isSelected: !!selectedCardKeys?.includes(itemKey), data, }; } + + public getRowByKey(key: Key): DataRow | undefined { + // eslint-disable-next-line spellcheck/spell-checker + const items = this.items.unreactive_get(); + + return items.find((item) => equalByValue(item.key, key)); + } } diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/options.ts index 5fc9346d722d..75b9bfae7f1e 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/options.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/options.ts @@ -11,6 +11,7 @@ import { filterPanel } from './filtering/index'; import * as pager from './pager/index'; import * as searchPanel from './search/index'; import type { SearchProperties } from './search/types'; +import * as selection from './selection/index'; import * as sortingController from './sorting_controller/index'; import type * as toolbar from './toolbar/index'; import type { GridCoreNew } from './widget'; @@ -29,6 +30,7 @@ export type Options = & headerFilter.Options & contentView.Options & searchPanel.Options + & selection.Options // TODO: Remove this mock search options during search implementation & SearchProperties & toolbar.Options; @@ -42,6 +44,7 @@ export const defaultOptions = { ...headerFilter.defaultOptions, ...contentView.defaultOptions, ...searchPanel.defaultOptions, + ...selection.defaultOptions, searchText: '', } satisfies Options; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/controller.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/controller.test.ts.snap new file mode 100644 index 000000000000..6237a65341e9 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/controller.test.ts.snap @@ -0,0 +1,343 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SelectionController changeCardSelection when the control arg equal to false should update the select state of the item 1`] = ` +InterruptableComputed { + "callbacks": Set {}, + "depInitialized": [ + true, + true, + true, + true, + ], + "depValues": [ + [ + { + "id": 1, + "value": "test", + }, + ], + [], + [ + 1, + ], + { + "caseSensitive": false, + "enabled": true, + "searchStr": "", + }, + ], + "isInitialized": true, + "subscriptions": SubscriptionBag { + "subscriptions": [ + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + ], + }, + "value": [ + { + "cells": [], + "data": { + "id": 1, + "value": "test", + }, + "index": 0, + "isSelected": true, + "key": 1, + }, + ], +} +`; + +exports[`SelectionController changeCardSelection when the control arg equal to true should update the select state of the item 1`] = ` +InterruptableComputed { + "callbacks": Set {}, + "depInitialized": [ + true, + true, + true, + true, + ], + "depValues": [ + [ + { + "id": 1, + "value": "test", + }, + ], + [], + [], + { + "caseSensitive": false, + "enabled": true, + "searchStr": "", + }, + ], + "isInitialized": true, + "subscriptions": SubscriptionBag { + "subscriptions": [ + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + ], + }, + "value": [ + { + "cells": [], + "data": { + "id": 1, + "value": "test", + }, + "index": 0, + "isSelected": false, + "key": 1, + }, + ], +} +`; + +exports[`SelectionController deselectCards should deselect item 1`] = ` +InterruptableComputed { + "callbacks": Set {}, + "depInitialized": [ + true, + true, + true, + true, + ], + "depValues": [ + [ + { + "id": 1, + "value": "test", + }, + ], + [], + [], + { + "caseSensitive": false, + "enabled": true, + "searchStr": "", + }, + ], + "isInitialized": true, + "subscriptions": SubscriptionBag { + "subscriptions": [ + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + ], + }, + "value": [ + { + "cells": [], + "data": { + "id": 1, + "value": "test", + }, + "index": 0, + "isSelected": false, + "key": 1, + }, + ], +} +`; + +exports[`SelectionController deselectCardsByIndexes should deselect item 1`] = ` +InterruptableComputed { + "callbacks": Set {}, + "depInitialized": [ + true, + true, + true, + true, + ], + "depValues": [ + [ + { + "id": 1, + "value": "test", + }, + ], + [], + [], + { + "caseSensitive": false, + "enabled": true, + "searchStr": "", + }, + ], + "isInitialized": true, + "subscriptions": SubscriptionBag { + "subscriptions": [ + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + ], + }, + "value": [ + { + "cells": [], + "data": { + "id": 1, + "value": "test", + }, + "index": 0, + "isSelected": false, + "key": 1, + }, + ], +} +`; + +exports[`SelectionController selectCards should select item 1`] = ` +InterruptableComputed { + "callbacks": Set {}, + "depInitialized": [ + true, + true, + true, + true, + ], + "depValues": [ + [ + { + "id": 1, + "value": "test", + }, + ], + [], + [ + 1, + ], + { + "caseSensitive": false, + "enabled": true, + "searchStr": "", + }, + ], + "isInitialized": true, + "subscriptions": SubscriptionBag { + "subscriptions": [ + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + ], + }, + "value": [ + { + "cells": [], + "data": { + "id": 1, + "value": "test", + }, + "index": 0, + "isSelected": true, + "key": 1, + }, + ], +} +`; + +exports[`SelectionController selectCardsByIndexes should select item 1`] = ` +InterruptableComputed { + "callbacks": Set {}, + "depInitialized": [ + true, + true, + true, + true, + ], + "depValues": [ + [ + { + "id": 1, + "value": "test", + }, + ], + [], + [ + 1, + ], + { + "caseSensitive": false, + "enabled": true, + "searchStr": "", + }, + ], + "isInitialized": true, + "subscriptions": SubscriptionBag { + "subscriptions": [ + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + ], + }, + "value": [ + { + "cells": [], + "data": { + "id": 1, + "value": "test", + }, + "index": 0, + "isSelected": true, + "key": 1, + }, + ], +} +`; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/options.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/options.test.ts.snap new file mode 100644 index 000000000000..450dc1044be7 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/__snapshots__/options.test.ts.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Options selectedCardKeys when given should set the select state of the item 1`] = ` +InterruptableComputed { + "callbacks": Set {}, + "depInitialized": [ + true, + true, + true, + true, + ], + "depValues": [ + [ + { + "id": 1, + "value": "test", + }, + ], + [], + [ + 1, + ], + { + "caseSensitive": false, + "enabled": true, + "searchStr": "", + }, + ], + "isInitialized": true, + "subscriptions": SubscriptionBag { + "subscriptions": [ + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + ], + }, + "value": [ + { + "cells": [], + "data": { + "id": 1, + "value": "test", + }, + "index": 0, + "isSelected": true, + "key": 1, + }, + ], +} +`; + +exports[`Options selection allowSelectAll when it is false and selection mode is 'multiple' selection should not work 1`] = `[]`; + +exports[`Options selection allowSelectAll when it is true and selection mode is 'multiple' selection should not work 1`] = ` +[ + { + "locateInMenu": "auto", + "location": "before", + "name": "selectAllButton", + "options": { + "disabled": false, + "icon": "selectall", + "onClick": [Function], + "text": "Select All", + }, + "widget": "dxButton", + }, + { + "locateInMenu": "auto", + "location": "before", + "name": "clearSelectionButton", + "options": { + "disabled": true, + "icon": "close", + "onClick": [Function], + "text": "Clear selection", + }, + "widget": "dxButton", + }, +] +`; + +exports[`Options selection allowSelectAll when it is true and selection mode isn't 'multiple' selection should not work 1`] = `[]`; + +exports[`Options selection mode when it is 'none' and the selectedCardKeys is specified selection should not apply 1`] = ` +InterruptableComputed { + "callbacks": Set {}, + "depInitialized": [ + true, + true, + true, + true, + ], + "depValues": [ + [ + { + "id": 1, + "value": "test", + }, + ], + [], + [], + { + "caseSensitive": false, + "enabled": true, + "searchStr": "", + }, + ], + "isInitialized": true, + "subscriptions": SubscriptionBag { + "subscriptions": [ + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + ], + }, + "value": [ + { + "cells": [], + "data": { + "id": 1, + "value": "test", + }, + "index": 0, + "isSelected": false, + "key": 1, + }, + ], +} +`; + +exports[`Options selection mode when it is 'none' selection should not work 1`] = ` +InterruptableComputed { + "callbacks": Set {}, + "depInitialized": [ + true, + true, + true, + true, + ], + "depValues": [ + [ + { + "id": 1, + "value": "test", + }, + ], + [], + [], + { + "caseSensitive": false, + "enabled": true, + "searchStr": "", + }, + ], + "isInitialized": true, + "subscriptions": SubscriptionBag { + "subscriptions": [ + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + { + "unsubscribe": [Function], + }, + ], + }, + "value": [ + { + "cells": [], + "data": { + "id": 1, + "value": "test", + }, + "index": 0, + "isSelected": false, + "key": 1, + }, + ], +} +`; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/const.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/const.ts new file mode 100644 index 000000000000..0664d40bec41 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/const.ts @@ -0,0 +1,17 @@ +export enum SelectionMode { + Multiple = 'multiple', + + Single = 'single', + + None = 'none', +} + +export enum ShowCheckBoxesMode { + Always = 'always', + + OnClick = 'onClick', + + OnLongTap = 'onLongTap', + + None = 'none', +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.test.ts new file mode 100644 index 000000000000..fbed51d7649f --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.test.ts @@ -0,0 +1,847 @@ +/* eslint-disable spellcheck/spell-checker */ +import { + describe, expect, it, jest, +} from '@jest/globals'; + +import { ColumnsController } from '../columns_controller/columns_controller'; +import { DataController } from '../data_controller'; +import { FilterController } from '../filtering/filter_controller'; +import { ItemsController } from '../items_controller/items_controller'; +import type { Options } from '../options'; +import { OptionsControllerMock } from '../options_controller/options_controller.mock'; +import { SearchController } from '../search/controller'; +import { SortingController } from '../sorting_controller/sorting_controller'; +import { ToolbarController } from '../toolbar/controller'; +import { SelectionController } from './controller'; + +const setup = (config: Options = {}) => { + const optionsController = new OptionsControllerMock({ + selection: { + mode: 'single', + }, + selectedCardKeys: [], + ...config, + }); + + const filterController = new FilterController(optionsController); + const columnsController = new ColumnsController(optionsController); + const sortingController = new SortingController(optionsController, columnsController); + + const dataController = new DataController(optionsController, sortingController, filterController); + + const searchController = new SearchController(optionsController); + const itemsController = new ItemsController(dataController, columnsController, searchController); + const toolbarController = new ToolbarController(optionsController); + + const selectionController = new SelectionController( + optionsController, + dataController, + itemsController, + toolbarController, + ); + + return { + optionsController, + selectionController, + itemsController, + }; +}; + +describe('SelectionController', () => { + // Public methods + + describe('selectCards', () => { + it('should select item', () => { + const { + selectionController, + itemsController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + }); + + selectionController.selectCards([1]); + expect(itemsController.items).toMatchSnapshot(); + }); + }); + + describe('deselectCards', () => { + it('should deselect item', () => { + const { + selectionController, + itemsController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selectedCardKeys: [1], + }); + + selectionController.deselectCards([1]); + expect(itemsController.items).toMatchSnapshot(); + }); + }); + + describe('selectCardsByIndexes', () => { + it('should select item', () => { + const { + selectionController, + itemsController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + }); + + selectionController.selectCardsByIndexes([0]); + expect(itemsController.items).toMatchSnapshot(); + }); + }); + + describe('deselectCardsByIndexes', () => { + it('should deselect item', () => { + const { + selectionController, + itemsController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selectedCardKeys: [1], + }); + + selectionController.deselectCardsByIndexes([0]); + expect(itemsController.items).toMatchSnapshot(); + }); + }); + + describe('changeCardSelection', () => { + describe('when the control arg equal to false', () => { + it('should update the select state of the item', () => { + const { + selectionController, + itemsController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + }); + + selectionController.changeCardSelection(0, { control: false }); + expect(itemsController.items).toMatchSnapshot(); + }); + }); + + describe('when the control arg equal to true', () => { + it('should update the select state of the item', () => { + const { + selectionController, + itemsController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selectedCardKeys: [1], + }); + + selectionController.changeCardSelection(0, { control: true }); + expect(itemsController.items).toMatchSnapshot(); + }); + }); + + describe('when item is selected and multiple selection enabled', () => { + it('should update the select state of the item', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selectedCardKeys: [1], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + }, + }); + + selectionController.changeCardSelection(0); + expect(selectionController.getSelectedCardKeys()).toEqual([]); + }); + }); + }); + + describe('isCardSelected', () => { + describe('when the selectedCardKeys is specified', () => { + it('should return true', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selectedCardKeys: [1], + }); + + expect(selectionController.isCardSelected(1)).toBe(true); + }); + }); + + describe('when the selectedCardKeys isn\'t specified', () => { + it('should return false', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + }); + + expect(selectionController.isCardSelected(1)).toBe(false); + }); + }); + }); + + describe('getSelectedCardKeys', () => { + it('should return the selected card keys', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selectedCardKeys: [1], + }); + + expect(selectionController.getSelectedCardKeys()).toEqual([1]); + }); + }); + + describe('getSelectedCards', () => { + it('should return the selected card keys', () => { + const { + selectionController, + itemsController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selectedCardKeys: [1], + }); + + expect(selectionController.getSelectedCards()) + .toEqual(itemsController.items.unreactive_get()); + }); + }); + + describe('clearSelection', () => { + it('should clear the selection', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selectedCardKeys: [1], + }); + + selectionController.clearSelection(); + expect(selectionController.getSelectedCardKeys().length).toBe(0); + }); + }); + + describe('updateSelectionCheckBoxesVisible', () => { + describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onClick\'', () => { + it('should show the selection checkboxes', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + }, + }); + + selectionController.updateSelectionCheckBoxesVisible(true); + expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(true); + }); + + it('should hide the selection checkboxes', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + }, + }); + + selectionController.updateSelectionCheckBoxesVisible(false); + expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(false); + }); + }); + }); + + describe('processLongTap', () => { + describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onLongTap\'', () => { + it('should render the selection checkbox', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onLongTap', + }, + }); + + // @ts-expect-error + selectionController.processLongTap({ index: 0 }); + expect(selectionController.isCheckBoxesRendered.unreactive_get()).toBe(true); + }); + }); + + describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onClick\'', () => { + it('should show the selection checkbox', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + }, + }); + + // @ts-expect-error + selectionController.processLongTap({ index: 0 }); + expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(true); + }); + }); + + describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'none\'', () => { + it('should select a first item', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'none', + }, + }); + + // @ts-expect-error + selectionController.processLongTap({ index: 0 }); + expect(selectionController.getSelectedCardKeys()).toEqual([1]); + }); + }); + + describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'none\'', () => { + it('should not select a first item', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + }, + }); + + // @ts-expect-error + selectionController.processLongTap({ index: 0 }); + expect(selectionController.getSelectedCardKeys()).toEqual([]); + }); + }); + }); + + // Public properties + describe('isCheckBoxesRendered', () => { + describe('when the selection mode is equal to \'none\'', () => { + it('should return false', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'none', + }, + }); + + expect(selectionController.isCheckBoxesRendered.unreactive_get()).toBe(false); + }); + }); + + describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'always\'', () => { + it('should return true', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + }, + }); + + expect(selectionController.isCheckBoxesRendered.unreactive_get()).toBe(true); + }); + }); + + describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onClick\'', () => { + it('should return true', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + }, + }); + + expect(selectionController.isCheckBoxesRendered.unreactive_get()).toBe(true); + }); + }); + + describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onLongTap\'', () => { + it('should return false', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onLongTap', + }, + }); + + expect(selectionController.isCheckBoxesRendered.unreactive_get()).toBe(false); + }); + }); + }); + + describe('isCheckBoxesVisible', () => { + describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onClick\'', () => { + it('should return false', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + }, + }); + + expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(false); + }); + }); + + describe('when selecting one card', () => { + it('should return false', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }, { id: 3, value: 'test3' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + }, + }); + + selectionController.selectCards([1]); + expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(false); + }); + }); + + describe('when selecting two cards', () => { + it('should return true', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }, { id: 3, value: 'test3' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + }, + }); + + selectionController.selectCards([1, 2]); + expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(true); + }); + }); + + describe('when deselecting all cards', () => { + it('should return false', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }, { id: 3, value: 'test3' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + }, + selectedCardKeys: [1, 2], + }); + + selectionController.deselectCards([1, 2]); + expect(selectionController.isCheckBoxesVisible.unreactive_get()).toBe(false); + }); + }); + }); + + describe('needToHiddenCheckBoxes', () => { + describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'onClick\'', () => { + it('should return true', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + }, + }); + + expect(selectionController.needToHiddenCheckBoxes.unreactive_get()).toBe(true); + }); + }); + + describe('when the selection mode is equal to \'multiple\' and the showCheckBoxesMode is equal to \'always\'', () => { + it('should return false', () => { + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + }, + }); + + expect(selectionController.needToHiddenCheckBoxes.unreactive_get()).toBe(false); + }); + }); + }); + + // Events + + describe('onSelectionChanging', () => { + describe('when selecting a card', () => { + it('should be called', () => { + const selectionChangingMockFn = jest.fn(); + const cardData = { id: 1, value: 'test' }; + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [cardData], + selection: { + mode: 'multiple', + }, + onSelectionChanging: selectionChangingMockFn, + }); + + selectionController.selectCards([1]); + + expect(selectionChangingMockFn.mock.calls).toHaveLength(1); + expect(selectionChangingMockFn.mock.lastCall).toMatchObject([{ + cancel: false, + currentDeselectedCardKeys: [], + currentSelectedCardKeys: [1], + isDeselectAll: false, + isSelectAll: false, + selectedCardKeys: [1], + selectedCardsData: [cardData], + }]); + }); + }); + + describe('when deselecting a card', () => { + it('should be called', () => { + const selectionChangingMockFn = jest.fn(); + const cardData = { id: 1, value: 'test' }; + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [cardData], + selection: { + mode: 'multiple', + }, + selectedCardKeys: [1], + onSelectionChanging: selectionChangingMockFn, + }); + + selectionController.deselectCards([1]); + + expect(selectionChangingMockFn.mock.calls).toHaveLength(1); + expect(selectionChangingMockFn.mock.lastCall).toMatchObject([{ + cancel: false, + currentDeselectedCardKeys: [1], + currentSelectedCardKeys: [], + isDeselectAll: false, + isSelectAll: false, + selectedCardKeys: [], + selectedCardsData: [], + }]); + }); + }); + + describe('when selecting all cards', () => { + it('should be called', () => { + const selectionChangingMockFn = jest.fn(); + const data = [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }]; + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: data, + selection: { + mode: 'multiple', + allowSelectAll: true, + }, + onSelectionChanging: selectionChangingMockFn, + }); + + selectionController.selectAll(); + + expect(selectionChangingMockFn.mock.calls).toHaveLength(1); + expect(selectionChangingMockFn.mock.lastCall).toMatchObject([{ + cancel: false, + currentDeselectedCardKeys: [], + currentSelectedCardKeys: [1, 2], + isDeselectAll: false, + isSelectAll: false, + selectedCardKeys: [1, 2], + selectedCardsData: data, + }]); + }); + }); + + describe('when deselecting all cards', () => { + it('should be called', () => { + const selectionChangingMockFn = jest.fn(); + const data = [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }]; + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: data, + selection: { + mode: 'multiple', + allowSelectAll: true, + }, + selectedCardKeys: [1, 2], + onSelectionChanging: selectionChangingMockFn, + }); + + selectionController.deselectAll(); + + expect(selectionChangingMockFn.mock.calls).toHaveLength(1); + expect(selectionChangingMockFn.mock.lastCall).toMatchObject([{ + cancel: false, + currentDeselectedCardKeys: [1, 2], + currentSelectedCardKeys: [], + isDeselectAll: false, + isSelectAll: false, + selectedCardKeys: [], + selectedCardsData: [], + }]); + }); + }); + + describe('when a cancel arg is specified as true', () => { + it('should be called', () => { + const selectionChangingMockFn = jest.fn((e: any) => { e.cancel = true; }); + const cardData = { id: 1, value: 'test' }; + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [cardData], + selection: { + mode: 'multiple', + }, + onSelectionChanging: selectionChangingMockFn, + }); + + selectionController.selectCards([1]); + + expect(selectionChangingMockFn.mock.calls).toHaveLength(1); + expect(selectionChangingMockFn.mock.lastCall).toMatchObject([{ + cancel: true, + currentDeselectedCardKeys: [], + currentSelectedCardKeys: [1], + isDeselectAll: false, + isSelectAll: false, + selectedCardKeys: [1], + selectedCardsData: [cardData], + }]); + expect(selectionController.getSelectedCardKeys()).toEqual([]); + }); + }); + + describe('when a cancel arg is specified as Promise', () => { + it('should be called', () => { + const cancel = Promise.resolve(true); + const selectionChangingMockFn = jest.fn((e: any) => { e.cancel = cancel; }); + const cardData = { id: 1, value: 'test' }; + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [cardData], + selection: { + mode: 'multiple', + }, + onSelectionChanging: selectionChangingMockFn, + }); + + selectionController.selectCards([1]); + + expect(selectionChangingMockFn.mock.calls).toHaveLength(1); + expect(selectionChangingMockFn.mock.lastCall).toMatchObject([{ + cancel, + currentDeselectedCardKeys: [], + currentSelectedCardKeys: [1], + isDeselectAll: false, + isSelectAll: false, + selectedCardKeys: [1], + selectedCardsData: [cardData], + }]); + expect(selectionController.getSelectedCardKeys()).toEqual([]); + }); + }); + }); + + describe('onSelectionChanged', () => { + describe('when selecting a card', () => { + it('should be called', () => { + const selectionChangedMockFn = jest.fn(); + const cardData = { id: 1, value: 'test' }; + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [cardData], + selection: { + mode: 'multiple', + }, + onSelectionChanged: selectionChangedMockFn, + }); + + selectionController.selectCards([1]); + + expect(selectionChangedMockFn.mock.calls).toHaveLength(1); + expect(selectionChangedMockFn.mock.lastCall).toMatchObject([{ + currentDeselectedCardKeys: [], + currentSelectedCardKeys: [1], + isDeselectAll: false, + isSelectAll: false, + selectedCardKeys: [1], + selectedCardsData: [cardData], + }]); + }); + }); + + describe('when deselecting a card', () => { + it('should be called', () => { + const selectionChangedMockFn = jest.fn(); + const cardData = { id: 1, value: 'test' }; + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [cardData], + selection: { + mode: 'multiple', + }, + selectedCardKeys: [1], + onSelectionChanged: selectionChangedMockFn, + }); + + selectionController.deselectCards([1]); + + expect(selectionChangedMockFn.mock.calls).toHaveLength(1); + expect(selectionChangedMockFn.mock.lastCall).toMatchObject([{ + currentDeselectedCardKeys: [1], + currentSelectedCardKeys: [], + isDeselectAll: false, + isSelectAll: false, + selectedCardKeys: [], + selectedCardsData: [], + }]); + }); + }); + + describe('when selecting all cards', () => { + it('should be called', () => { + const selectionChangedMockFn = jest.fn(); + const data = [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }]; + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: data, + selection: { + mode: 'multiple', + allowSelectAll: true, + }, + onSelectionChanged: selectionChangedMockFn, + }); + + selectionController.selectAll(); + + expect(selectionChangedMockFn.mock.calls).toHaveLength(1); + expect(selectionChangedMockFn.mock.lastCall).toMatchObject([{ + currentDeselectedCardKeys: [], + currentSelectedCardKeys: [1, 2], + isDeselectAll: false, + isSelectAll: false, + selectedCardKeys: [1, 2], + selectedCardsData: data, + }]); + }); + }); + + describe('when deselecting all cards', () => { + it('should be called', () => { + const selectionChangedMockFn = jest.fn(); + const data = [{ id: 1, value: 'test1' }, { id: 2, value: 'test2' }]; + const { + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: data, + selection: { + mode: 'multiple', + allowSelectAll: true, + }, + selectedCardKeys: [1, 2], + onSelectionChanged: selectionChangedMockFn, + }); + + selectionController.deselectAll(); + + expect(selectionChangedMockFn.mock.calls).toHaveLength(1); + expect(selectionChangedMockFn.mock.lastCall).toMatchObject([{ + currentDeselectedCardKeys: [1, 2], + currentSelectedCardKeys: [], + isDeselectAll: false, + isSelectAll: false, + selectedCardKeys: [], + selectedCardsData: [], + }]); + }); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts new file mode 100644 index 000000000000..184cdc48643f --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts @@ -0,0 +1,397 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable spellcheck/spell-checker */ +import type { DeferredObj } from '@js/core/utils/deferred'; +import messageLocalization from '@js/localization/message'; +import type { SubsGets } from '@ts/core/reactive/index'; +import { computed, effect, state } from '@ts/core/reactive/index'; +import { DataController } from '@ts/grids/new/grid_core/data_controller/index'; +import { ShowCheckBoxesMode } from '@ts/grids/new/grid_core/selection/const'; +import Selection from '@ts/ui/selection/m_selection'; + +import type { DataRow } from '../columns_controller/types'; +import type { Key } from '../data_controller/types'; +import { ItemsController } from '../items_controller/items_controller'; +import { OptionsController } from '../options_controller/options_controller'; +import { ToolbarController } from '../toolbar/controller'; +import { SelectionMode } from './const'; +import type { + SelectedCardKeys, SelectionEventInfo, SelectionOptions, +} from './types'; + +export class SelectionController { + public static dependencies = [ + OptionsController, + DataController, + ItemsController, + ToolbarController, + ] as const; + + private readonly selectedCardKeys = this.options.twoWay('selectedCardKeys'); + + private readonly selectionOption: SubsGets = this.options.oneWay('selection'); + + private readonly selectionHelper: SubsGets; + + private readonly _isCheckBoxesRendered = state(false); + + private readonly onSelectionChanging = this.options.action('onSelectionChanging'); + + private readonly onSelectionChanged = this.options.action('onSelectionChanged'); + + public readonly isCheckBoxesRendered = computed( + (selectionMode, showCheckBoxesMode, _isCheckBoxesRendered) => { + if (selectionMode === SelectionMode.Multiple) { + switch (showCheckBoxesMode) { + case ShowCheckBoxesMode.Always: + case ShowCheckBoxesMode.OnClick: + return true; + case ShowCheckBoxesMode.OnLongTap: + return _isCheckBoxesRendered; + default: + return false; + } + } + + return false; + }, + [ + this.options.oneWay('selection.mode'), + this.options.oneWay('selection.showCheckBoxesMode'), + this._isCheckBoxesRendered, + ], + ); + + public readonly _isCheckBoxesVisible = state(false); + + public readonly isCheckBoxesVisible = computed( + (selectionOption, _isCheckBoxesVisible) => { + const { mode, showCheckBoxesMode } = selectionOption; + + if (mode === SelectionMode.Multiple) { + return showCheckBoxesMode !== ShowCheckBoxesMode.OnClick || _isCheckBoxesVisible; + } + + return false; + }, + [ + this.selectionOption, + this._isCheckBoxesVisible, + ], + ); + + public readonly needToHiddenCheckBoxes = computed( + (isCheckBoxesVisible, selectionOption) => { + const { mode, showCheckBoxesMode } = selectionOption; + + if (mode === SelectionMode.Multiple && showCheckBoxesMode === ShowCheckBoxesMode.OnClick) { + return !isCheckBoxesVisible; + } + + return false; + }, + [ + this.isCheckBoxesVisible, + this.selectionOption, + ], + ); + + public readonly allowSelectOnClick = computed( + (selectionOption) => { + const { mode, showCheckBoxesMode } = selectionOption; + + return mode !== SelectionMode.Multiple || showCheckBoxesMode !== ShowCheckBoxesMode.Always; + }, + [this.selectionOption], + ); + + public readonly needToAddSelectionButtons = computed( + (selectionMode, allowSelectAll) => selectionMode === SelectionMode.Multiple && allowSelectAll, + [ + this.options.oneWay('selection.mode'), + this.options.oneWay('selection.allowSelectAll'), + ], + ); + + constructor( + private readonly options: OptionsController, + private readonly dataController: DataController, + private readonly itemsController: ItemsController, + private readonly toolbarController: ToolbarController, + ) { + this.selectionHelper = computed( + ( + dataSource, + selectionOption, + ) => { + if (selectionOption.mode === SelectionMode.None) { + return undefined; + } + + const selectionConfig = this.getSelectionConfig( + dataSource, + selectionOption, + ); + + return new Selection(selectionConfig); + }, + [ + this.dataController.dataSource, + this.selectionOption, + ], + ); + + effect((selectedCardKeys, selectionOption) => { + if (selectionOption.mode !== SelectionMode.None) { + this.itemsController.setSelectionState(selectedCardKeys); + + if (selectedCardKeys.length > 1) { + this._isCheckBoxesVisible.update(true); + } else if (selectedCardKeys.length === 0) { + this._isCheckBoxesVisible.update(false); + } + } + }, [this.selectedCardKeys, this.selectionOption]); + + effect((isLoaded) => { + if (isLoaded) { + const selectedCardKeys = this.selectedCardKeys.unreactive_get(); + + this.selectCards(selectedCardKeys); + } + }, [this.dataController.isLoaded]); + + effect((selectedCardKeys) => { + this.updateSelectionToolbarButtons(selectedCardKeys); + }, [this.selectedCardKeys, this.dataController.items]); + } + + private getSelectionConfig(dataSource, selectionOption): object { + const selectedCardKeys = this.selectedCardKeys.unreactive_get(); + + return { + selectedKeys: selectedCardKeys, + mode: selectionOption.mode, + maxFilterLengthInRequest: selectionOption.maxFilterLengthInRequest, + ignoreDisabledItems: true, + key() { + return dataSource.key(); + }, + keyOf(item) { + return dataSource.store().keyOf(item); + }, + dataFields() { + return dataSource.select(); + }, + load(options) { + return dataSource.store().load(options); + }, + plainItems() { + return dataSource.items(); + }, + filter() { + // TODO Salimov: Need to take combined filter + return dataSource.filter(); + }, + totalCount: () => dataSource.totalCount(), + onSelectionChanging: this.selectionChanging.bind(this), + onSelectionChanged: this.selectionChanged.bind(this), + }; + } + + private getSelectionEventArgs(e): SelectionEventInfo { + return { + currentSelectedCardKeys: [...e.addedItemKeys], + currentDeselectedCardKeys: [...e.removedItemKeys], + selectedCardKeys: [...e.selectedItemKeys], + selectedCardsData: [...e.selectedItems], + isSelectAll: false, + isDeselectAll: false, + }; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private selectionChanging(e: any): void { + if (e.addedItemKeys.length || e.removedItemKeys.length) { + const onSelectionChanging = this.onSelectionChanging.unreactive_get(); + const eventArgs = { + ...this.getSelectionEventArgs(e), + cancel: false, + }; + + // @ts-expect-error + 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]); + // @ts-expect-error + onSelectionChanged?.(eventArgs); + } + } + + private isOnePageSelectAll(): boolean { + const selectionOption = this.selectionOption.unreactive_get(); + + return selectionOption?.selectAllMode === 'page'; + } + + private isSelectAll(): boolean | undefined { + const selectionHelper = this.selectionHelper.unreactive_get(); + + return selectionHelper?.getSelectAllState(this.isOnePageSelectAll()); + } + + private updateSelectionToolbarButtons( + selectedCardKeys: SelectedCardKeys, + ) { + const isSelectAll = this.isSelectAll(); + const isOnePageSelectAll = this.isOnePageSelectAll(); + + this.toolbarController.addDefaultItem({ + name: 'selectAllButton', + widget: 'dxButton', + options: { + icon: 'selectall', + onClick: () => { + this.selectAll(); + }, + disabled: !!isSelectAll, + text: messageLocalization.format('dxCardView-selectAll'), + }, + location: 'before', + locateInMenu: 'auto', + }, this.needToAddSelectionButtons); + this.toolbarController.addDefaultItem({ + name: 'clearSelectionButton', + widget: 'dxButton', + options: { + icon: 'close', + onClick: () => { + this.deselectAll(); + }, + disabled: isOnePageSelectAll ? isSelectAll === false : selectedCardKeys.length === 0, + text: messageLocalization.format('dxCardView-clearSelection'), + }, + location: 'before', + locateInMenu: 'auto', + }, this.needToAddSelectionButtons); + } + + private getItemKeysByIndexes(indexes: number[]): Key[] { + const items = this.itemsController.items.unreactive_get(); + + return indexes + .map((index) => items[index]?.key) + .filter((key) => key !== undefined); + } + + public changeCardSelection( + cardIndex: number, + options?: { control?: boolean; shift?: boolean }, + ): void { + const selectionHelper = this.selectionHelper?.unreactive_get(); + const isCheckBoxesVisible = this.isCheckBoxesVisible.unreactive_get(); + const keys = options ?? {}; + + if (isCheckBoxesVisible) { + keys.control = isCheckBoxesVisible; + } + + selectionHelper?.changeItemSelection(cardIndex, keys, false); + } + + public selectCards(keys: Key[], preserve = false): DeferredObj | undefined { + const selectionHelper = this.selectionHelper?.unreactive_get(); + + return selectionHelper?.selectedItemKeys(keys, preserve); + } + + public selectCardsByIndexes(indexes: number[]): DeferredObj | undefined { + const keys = this.getItemKeysByIndexes(indexes); + + return this.selectCards(keys); + } + + public deselectCards(keys: Key[]): DeferredObj | undefined { + const selectionHelper = this.selectionHelper?.unreactive_get(); + + return selectionHelper?.selectedItemKeys(keys, true, true); + } + + public deselectCardsByIndexes(indexes: number[]): DeferredObj | undefined { + const keys = this.getItemKeysByIndexes(indexes); + + return this.deselectCards(keys); + } + + public isCardSelected(key: Key): boolean { + const selectedCardKeys = this.selectedCardKeys.unreactive_get(); + + return selectedCardKeys.includes(key); + } + + public selectAll(): DeferredObj | undefined { + const selectionHelper = this.selectionHelper.unreactive_get(); + + return selectionHelper?.selectAll(this.isOnePageSelectAll()); + } + + public deselectAll(): DeferredObj | undefined { + const selectionHelper = this.selectionHelper.unreactive_get(); + + return selectionHelper?.deselectAll(this.isOnePageSelectAll()); + } + + public clearSelection(): DeferredObj | undefined { + const selectionHelper = this.selectionHelper.unreactive_get(); + + return selectionHelper?.clearSelection(); + } + + public getSelectedCards(): DataRow[] { + const selectedCardKey = this.getSelectedCardKeys(); + + return selectedCardKey + .map((key) => this.itemsController.getRowByKey(key)) + .filter((item): item is DataRow => !!item); + } + + public getSelectedCardKeys(): Key[] { + return this.selectedCardKeys.unreactive_get(); + } + + private toggleSelectionCheckBoxes(): void { + const isCheckBoxesRendered = this._isCheckBoxesRendered.unreactive_get(); + + this._isCheckBoxesRendered.update(!isCheckBoxesRendered); + } + + public updateSelectionCheckBoxesVisible(value: boolean): void { + this._isCheckBoxesVisible.update(value); + } + + public processLongTap(row: DataRow): void { + const { mode, showCheckBoxesMode } = this.selectionOption.unreactive_get(); + + if (mode !== SelectionMode.None) { + if (showCheckBoxesMode === ShowCheckBoxesMode.OnLongTap) { + this.toggleSelectionCheckBoxes(); + } else { + if (showCheckBoxesMode === ShowCheckBoxesMode.OnClick) { + this._isCheckBoxesVisible.update(true); + } + if (showCheckBoxesMode !== ShowCheckBoxesMode.Always) { + this.changeCardSelection(row.index, { control: true }); + } + } + } + } +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/index.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/index.ts new file mode 100644 index 000000000000..afd0cbb3be7d --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/index.ts @@ -0,0 +1,3 @@ +export { SelectionController as Controller } from './controller'; +export { defaultOptions, type Options } from './options'; +export { PublicMethods } from './public_methods'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.test.ts new file mode 100644 index 000000000000..2b763c0f86b7 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.test.ts @@ -0,0 +1,154 @@ +/* eslint-disable spellcheck/spell-checker */ +import { describe, expect, it } from '@jest/globals'; + +import { ColumnsController } from '../columns_controller/columns_controller'; +import { DataController } from '../data_controller'; +import { FilterController } from '../filtering/filter_controller'; +import { ItemsController } from '../items_controller/items_controller'; +import type { Options } from '../options'; +import { OptionsControllerMock } from '../options_controller/options_controller.mock'; +import { SearchController } from '../search/controller'; +import { SortingController } from '../sorting_controller/sorting_controller'; +import { ToolbarController } from '../toolbar/controller'; +import { SelectionController } from './controller'; + +const setup = (config: Options = {}) => { + const optionsController = new OptionsControllerMock({ + selection: { + mode: 'single', + }, + ...config, + }); + + const filterController = new FilterController(optionsController); + const columnsController = new ColumnsController(optionsController); + const sortingController = new SortingController(optionsController, columnsController); + + const dataController = new DataController(optionsController, sortingController, filterController); + + const searchController = new SearchController(optionsController); + const itemsController = new ItemsController(dataController, columnsController, searchController); + const toolbarController = new ToolbarController(optionsController); + + const selectionController = new SelectionController( + optionsController, + dataController, + itemsController, + toolbarController, + ); + + return { + selectionController, + itemsController, + toolbarController, + }; +}; + +describe('Options', () => { + describe('selectedCardKeys', () => { + describe('when given', () => { + it('should set the select state of the item', () => { + const { + itemsController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selectedCardKeys: [1], + }); + + expect(itemsController.items).toMatchSnapshot(); + }); + }); + }); + + describe('selection', () => { + describe('mode', () => { + describe('when it is \'none\'', () => { + it('selection should not work', () => { + const { + itemsController, + selectionController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'none', + }, + }); + + selectionController.selectCards([1]); + expect(itemsController.items).toMatchSnapshot(); + }); + }); + describe('when it is \'none\' and the selectedCardKeys is specified', () => { + it('selection should not apply', () => { + const { + itemsController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selectedCardKeys: [1], + selection: { + mode: 'none', + }, + }); + + expect(itemsController.items).toMatchSnapshot(); + }); + }); + }); + + describe('allowSelectAll', () => { + describe('when it is true and selection mode is \'multiple\'', () => { + it('selection should not work', () => { + const { + toolbarController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + allowSelectAll: true, + }, + }); + + expect(toolbarController.items.unreactive_get()).toMatchSnapshot(); + }); + }); + + describe('when it is false and selection mode is \'multiple\'', () => { + it('selection should not work', () => { + const { + toolbarController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'multiple', + allowSelectAll: false, + }, + }); + + expect(toolbarController.items.unreactive_get()).toMatchSnapshot(); + }); + }); + + describe('when it is true and selection mode isn\'t \'multiple\'', () => { + it('selection should not work', () => { + const { + toolbarController, + } = setup({ + keyExpr: 'id', + dataSource: [{ id: 1, value: 'test' }], + selection: { + mode: 'single', + allowSelectAll: true, + }, + }); + + expect(toolbarController.items.unreactive_get()).toMatchSnapshot(); + }); + }); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.ts new file mode 100644 index 000000000000..f8378f15251e --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/options.ts @@ -0,0 +1,23 @@ +import type { + SelectedCardKeys, + SelectionChangedEvent, + SelectionChangingEvent, + SelectionOptions, +} from './types'; + +export interface Options { + selectedCardKeys?: SelectedCardKeys; + selection?: SelectionOptions; + onSelectionChanging?: ((e: SelectionChangingEvent) => void); + onSelectionChanged?: ((e: SelectionChangedEvent) => void); +} + +export const defaultOptions: Options = { + selectedCardKeys: [], + selection: { + mode: 'none', + showCheckBoxesMode: 'always', + allowSelectAll: true, + selectAllMode: 'allPages', + }, +}; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/public_methods.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/public_methods.ts new file mode 100644 index 000000000000..8d1ac090b53f --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/public_methods.ts @@ -0,0 +1,52 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import type { DeferredObj } from '@js/core/utils/deferred'; + +import type { DataRow } from '../columns_controller/types'; +import type { Key } from '../data_controller/types'; +import type { Constructor } from '../types'; +import type { GridCoreNewBase } from '../widget'; + +export function PublicMethods>(GridCore: TBase) { + return class GridCoreWithSelectionController extends GridCore { + public isCardSelected(key: Key): boolean { + return this.selectionController.isCardSelected(key); + } + + public getSelectedCardKeys(): Key[] { + return this.selectionController.getSelectedCardKeys(); + } + + public getSelectedCards(): DataRow[] { + return this.selectionController.getSelectedCards(); + } + + public selectCards(keys: Key[], preserve = false): DeferredObj | undefined { + return this.selectionController.selectCards(keys, preserve); + } + + public deselectCards(keys: Key[]): DeferredObj | undefined { + return this.selectionController.deselectCards(keys); + } + + public selectCardsByIndexes(indexes: number[]): DeferredObj | undefined { + return this.selectionController.selectCardsByIndexes(indexes); + } + + public deselectCardsByIndexes(indexes: number[]): DeferredObj | undefined { + return this.selectionController.deselectCardsByIndexes(indexes); + } + + public selectAll(): DeferredObj | undefined { + return this.selectionController.selectAll(); + } + + public deselectAll(): DeferredObj | undefined { + return this.selectionController.deselectAll(); + } + + public clearSelection(): void { + this.selectionController.clearSelection(); + } + }; +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/types.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/types.ts new file mode 100644 index 000000000000..5b36648c5e79 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/types.ts @@ -0,0 +1,42 @@ +import type { SelectAllMode, SingleMultipleOrNone } from '@js/common'; +import type { EventInfo } from '@js/common/core/events'; +import type { SelectionColumnDisplayMode } from '@js/common/grids'; +import type dxCardView from '@js/ui/card_view'; + +import type { Key } from '../data_controller/types'; + +export type SelectedCardKeys = Key[]; + +export interface SelectionEventInfo { + readonly currentSelectedCardKeys: TKey[]; + + readonly currentDeselectedCardKeys: TKey[]; + + readonly selectedCardKeys: TKey[]; + + readonly selectedCardsData: TCardData[]; + + readonly isSelectAll: boolean; + + readonly isDeselectAll: boolean; +} + +export type SelectionChangingEvent = + EventInfo & SelectionEventInfo & { + cancel: boolean | PromiseLike | PromiseLike; + }; + +export type SelectionChangedEvent = + EventInfo & SelectionEventInfo; + +export type { SelectionColumnDisplayMode as ShowCheckBoxesMode }; + +export interface SelectionOptions { + mode: SingleMultipleOrNone; + + showCheckBoxesMode?: SelectionColumnDisplayMode; + + allowSelectAll?: boolean; + + selectAllMode?: SelectAllMode; +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts index ff415c5fce5a..7d3fa0f48a11 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts @@ -24,6 +24,7 @@ import { MainView } from './main_view'; import { defaultOptions, defaultOptionsRules, type Options } from './options'; import { PagerView } from './pager/view'; import { SearchController } from './search/controller'; +import * as SelectionControllerModule from './selection/index'; import * as SortingControllerModule from './sorting_controller/index'; import type { SortingController } from './sorting_controller/sorting_controller'; import { ToolbarController } from './toolbar/controller'; @@ -45,6 +46,8 @@ export class GridCoreNewBase< protected sortingController!: SortingController; + protected selectionController!: SelectionControllerModule.Controller; + private pagerView!: PagerView; private toolbarController!: ToolbarController; @@ -67,6 +70,7 @@ export class GridCoreNewBase< this.diContext.register(DataControllerModule.CompatibilityDataController); this.diContext.register(ItemsController); this.diContext.register(ColumnsControllerModule.ColumnsController); + this.diContext.register(SelectionControllerModule.Controller); this.diContext.register(ColumnsControllerModule.CompatibilityColumnsController); this.diContext.register(SortingControllerModule.SortingController); this.diContext.register(ToolbarController); @@ -94,6 +98,7 @@ export class GridCoreNewBase< this.dataController = this.diContext.get(DataControllerModule.DataController); this.columnsController = this.diContext.get(ColumnsControllerModule.ColumnsController); this.sortingController = this.diContext.get(SortingControllerModule.SortingController); + this.selectionController = this.diContext.get(SelectionControllerModule.Controller); this.itemsController = this.diContext.get(ItemsController); this.toolbarController = this.diContext.get(ToolbarController); this.toolbarView = this.diContext.get(ToolbarView); @@ -162,7 +167,9 @@ export class GridCoreNew extends ColumnsControllerModule.PublicMethods( DataControllerModule.PublicMethods( SortingControllerModule.PublicMethods( FilterControllerModule.PublicMethods( - GridCoreNewBase, + SelectionControllerModule.PublicMethods( + GridCoreNewBase, + ), ), ), ), diff --git a/packages/devextreme/js/localization/messages/ar.json b/packages/devextreme/js/localization/messages/ar.json index 1b8f770c4a06..870f54c9af89 100644 --- a/packages/devextreme/js/localization/messages/ar.json +++ b/packages/devextreme/js/localization/messages/ar.json @@ -351,6 +351,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 805e4603b231..620bbc0162ea 100644 --- a/packages/devextreme/js/localization/messages/bg.json +++ b/packages/devextreme/js/localization/messages/bg.json @@ -351,6 +351,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 1d0473c18f45..dcfaec2a937f 100644 --- a/packages/devextreme/js/localization/messages/ca.json +++ b/packages/devextreme/js/localization/messages/ca.json @@ -351,6 +351,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 1be21868564f..b31a21b08354 100644 --- a/packages/devextreme/js/localization/messages/cs.json +++ b/packages/devextreme/js/localization/messages/cs.json @@ -351,6 +351,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 b8e0734fddbc..aa7aeb170eea 100644 --- a/packages/devextreme/js/localization/messages/da.json +++ b/packages/devextreme/js/localization/messages/da.json @@ -351,6 +351,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 3bd8df5d3642..6feb67ae7122 100644 --- a/packages/devextreme/js/localization/messages/de.json +++ b/packages/devextreme/js/localization/messages/de.json @@ -351,6 +351,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 0efc0cf8911b..9a1b4bbae1ed 100644 --- a/packages/devextreme/js/localization/messages/el.json +++ b/packages/devextreme/js/localization/messages/el.json @@ -351,6 +351,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 5b3fe8808bcf..41dd3b1098e8 100644 --- a/packages/devextreme/js/localization/messages/en.json +++ b/packages/devextreme/js/localization/messages/en.json @@ -351,6 +351,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 ff2fe9750a2e..2e74c9eff049 100644 --- a/packages/devextreme/js/localization/messages/es.json +++ b/packages/devextreme/js/localization/messages/es.json @@ -351,6 +351,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 40a5dad9a2cb..cc0e477a513c 100644 --- a/packages/devextreme/js/localization/messages/fa.json +++ b/packages/devextreme/js/localization/messages/fa.json @@ -351,6 +351,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 9162df88b756..13364e955684 100644 --- a/packages/devextreme/js/localization/messages/fi.json +++ b/packages/devextreme/js/localization/messages/fi.json @@ -351,6 +351,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 85e32f9f4242..aaa00d91f56c 100644 --- a/packages/devextreme/js/localization/messages/fr.json +++ b/packages/devextreme/js/localization/messages/fr.json @@ -351,6 +351,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 8c4bb9fb456d..da7ca2f1a25b 100644 --- a/packages/devextreme/js/localization/messages/hu.json +++ b/packages/devextreme/js/localization/messages/hu.json @@ -351,6 +351,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 8818c03856ea..d71477c9fb79 100644 --- a/packages/devextreme/js/localization/messages/it.json +++ b/packages/devextreme/js/localization/messages/it.json @@ -351,6 +351,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 413ecc36836b..b53ae4e1978d 100644 --- a/packages/devextreme/js/localization/messages/ja.json +++ b/packages/devextreme/js/localization/messages/ja.json @@ -351,6 +351,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 2b8fd5893edf..c25359d0fe7a 100644 --- a/packages/devextreme/js/localization/messages/lt.json +++ b/packages/devextreme/js/localization/messages/lt.json @@ -351,6 +351,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 aa7e2f62eef8..a4b76c2c8903 100644 --- a/packages/devextreme/js/localization/messages/lv.json +++ b/packages/devextreme/js/localization/messages/lv.json @@ -351,6 +351,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 f863253d9035..7148c0d989b2 100644 --- a/packages/devextreme/js/localization/messages/nb.json +++ b/packages/devextreme/js/localization/messages/nb.json @@ -351,6 +351,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 bc3575d934a0..14f6027245fd 100644 --- a/packages/devextreme/js/localization/messages/nl.json +++ b/packages/devextreme/js/localization/messages/nl.json @@ -351,6 +351,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 0d08f56723ac..8c8757d21101 100644 --- a/packages/devextreme/js/localization/messages/pl.json +++ b/packages/devextreme/js/localization/messages/pl.json @@ -351,6 +351,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 b7bf11b8edfc..28dd201482ac 100644 --- a/packages/devextreme/js/localization/messages/pt.json +++ b/packages/devextreme/js/localization/messages/pt.json @@ -351,6 +351,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 1f6a4037fff2..c61a0ce4d261 100644 --- a/packages/devextreme/js/localization/messages/ro.json +++ b/packages/devextreme/js/localization/messages/ro.json @@ -351,6 +351,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 42d766898a62..900a1b45c546 100644 --- a/packages/devextreme/js/localization/messages/ru.json +++ b/packages/devextreme/js/localization/messages/ru.json @@ -351,6 +351,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 738bd79979b6..d6468b884399 100644 --- a/packages/devextreme/js/localization/messages/sl.json +++ b/packages/devextreme/js/localization/messages/sl.json @@ -351,6 +351,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 931610488846..be637b61841b 100644 --- a/packages/devextreme/js/localization/messages/sv.json +++ b/packages/devextreme/js/localization/messages/sv.json @@ -351,6 +351,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 8773c91a180b..91412065cc59 100644 --- a/packages/devextreme/js/localization/messages/tr.json +++ b/packages/devextreme/js/localization/messages/tr.json @@ -351,6 +351,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 38ebce74f552..66ed94581ee1 100644 --- a/packages/devextreme/js/localization/messages/uk.json +++ b/packages/devextreme/js/localization/messages/uk.json @@ -351,6 +351,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 5cbed5bdc18e..dcfc96c3a20d 100644 --- a/packages/devextreme/js/localization/messages/vi.json +++ b/packages/devextreme/js/localization/messages/vi.json @@ -351,6 +351,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 6f77ed41eba6..2c2650fe92e7 100644 --- a/packages/devextreme/js/localization/messages/zh-tw.json +++ b/packages/devextreme/js/localization/messages/zh-tw.json @@ -351,6 +351,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 dcff92620e93..506948fe1c2d 100644 --- a/packages/devextreme/js/localization/messages/zh.json +++ b/packages/devextreme/js/localization/messages/zh.json @@ -351,6 +351,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",