diff --git a/.github/workflows/testcafe_tests.yml b/.github/workflows/testcafe_tests.yml index 52b65c312b04..dbdecf0e4bcb 100644 --- a/.github/workflows/testcafe_tests.yml +++ b/.github/workflows/testcafe_tests.yml @@ -119,6 +119,9 @@ jobs: { componentFolder: "dataGrid/sticky/fixed", name: "dataGrid / sticky (1/3)", indices: "1/3" }, { componentFolder: "dataGrid/sticky/fixed", name: "dataGrid / sticky (2/3)", indices: "2/3" }, { componentFolder: "dataGrid/sticky/fixed", name: "dataGrid / sticky (3/3)", indices: "3/3" }, + { componentFolder: "cardView", name: "cardView" }, + { componentFolder: "cardView", name: "cardView - material", theme: 'material.blue.light' }, + { componentFolder: "cardView", name: "cardView - fluent", theme: 'fluent.blue.light' }, { componentFolder: "pivotGrid", name: "pivotGrid", concurrency: 1 }, { componentFolder: "pivotGrid", name: "pivotGrid - material", theme: 'material.blue.light', concurrency: 1 }, { componentFolder: "pivotGrid", name: "pivotGrid - fluent", theme: 'fluent.blue.light', concurrency: 1 }, diff --git a/apps/demos/configs/Angular/config.js b/apps/demos/configs/Angular/config.js index b104f15225e8..efa4356980aa 100644 --- a/apps/demos/configs/Angular/config.js +++ b/apps/demos/configs/Angular/config.js @@ -12,6 +12,7 @@ const componentNames = [ 'button-group', 'button', 'calendar', + 'card-view', 'chart', 'chat', 'check-box', diff --git a/apps/react-storybook/stories/card_view/Card.stories.tsx b/apps/react-storybook/stories/card_view/Card.stories.tsx new file mode 100644 index 000000000000..843e22211ae0 --- /dev/null +++ b/apps/react-storybook/stories/card_view/Card.stories.tsx @@ -0,0 +1,74 @@ +import React, { useState, useRef, useEffect } from "react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { Card as InfernoCard } from "devextreme/esm/__internal/grids/new/card_view/content_view/content/card/card"; +import { wrapInfernoWithReact } from "../utils"; + +interface Props { + row: { + key: unknown; + cells: { + value: unknown; + column: { + alignment: "left" | "right" | "center"; + caption: string; + }; + }[]; + }; +} + +const Card = wrapInfernoWithReact(InfernoCard); + +const meta: Meta = { + title: "Grids/CardView/Card", + component: Card, +}; + +export default meta; + +type Story = StoryObj; + +export const DefaultMode: Story = { + args: { + row: { + cells: [ + { + value: 1, + column: { + alignment: 'left', + caption: 'asd', + }, + }, + { + value: 1, + column: { + alignment: 'left', + caption: 'asd', + }, + }, + { + value: 1, + column: { + alignment: 'left', + caption: 'asd', + }, + }, + { + value: 1, + column: { + alignment: 'left', + caption: 'asd', + }, + }, + { + value: 1, + column: { + alignment: 'left', + caption: 'asd', + }, + }, + ], + key: 1, + }, + }, +}; diff --git a/apps/react-storybook/stories/card_view/CardView.stories.tsx b/apps/react-storybook/stories/card_view/CardView.stories.tsx new file mode 100644 index 000000000000..8686138e12e6 --- /dev/null +++ b/apps/react-storybook/stories/card_view/CardView.stories.tsx @@ -0,0 +1,333 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import dxCardView from "devextreme/ui/card_view"; +import { wrapDxWithReact } from "../utils"; +import { store } from "./data"; +import { generatedData } from "./generatedData"; + +const CardView = wrapDxWithReact(dxCardView); + +const dataSources = { + empty: [], + local: generatedData, + remote: store, +} + +const columns = { + remote: [ + { + dataField: "OrderNumber", + alignment: 'right', + dataType: "number", + }, + { + dataField: "OrderDate", + visible: false, + }, + "StoreCity", + "StoreState", + "Employee", + { + dataField: "SaleAmount", + dataType: "number", + }, + ], + local: [ + 'firstName', + 'lastName', + 'gender', + 'birthDate' + ], + sortedRemote: [ + { + dataField: "OrderNumber", + alignment: 'right', + dataType: "number", + sortOrder: 'asc', + sortIndex: 1, + }, + { + dataField: "OrderDate", + visible: false, + }, + { + dataField: "StoreCity", + sortOrder: 'desc', + sortIndex: 0, + }, + "StoreState", + "Employee", + "SaleAmount", + ], + localHeaderFilter: [ + { + dataField: 'firstName', + headerFilter: { + allowSelectAll: false, + search: { + enabled: true, + }, + values: ['Anet', 'Annabela'], + }, + }, + { + dataField: 'lastName', + headerFilter: { + filterType: 'exclude', + values: ['Abbey'], + } + }, + { + dataField: 'gender', + allowHeaderFiltering: false, + }, + { + dataField: 'birthDate', + dataType: 'date', + calculateCellValue: (data) => { + return new Date(data.birthDate); + }, + calculateDisplayValue: (data) => { + return new Date(data.birthDate).toDateString(); + } + }, + ], + remoteHeaderFilter: [ + { + dataField: "OrderNumber", + alignment: 'right', + dataType: "number", + }, + { + dataField: "OrderDate", + dataType: 'date', + calculateCellValue: (data) => { + return new Date(data.OrderDate); + }, + calculateDisplayValue: (data) => { + return new Date(data.OrderDate).toDateString(); + } + }, + "StoreCity", + "StoreState", + "Employee", + { + dataField: "SaleAmount", + dataType: "number", + headerFilter: { + groupInterval: 1000, + } + }, +], +} + +const meta: Meta = { + title: "Grids/CardView", + component: CardView, + argTypes: { + dataSource: { + options: Object.keys(dataSources), + mapping: dataSources, + control: { type: 'radio' }, + }, + remoteOperations: { + control: 'radio', + options: [false, true, 'auto'], + }, + width: { + control: 'text', + }, + height: { + control: 'text', + }, + keyExpr: { + control: 'text', + }, + cardsPerRow: { + options: ['auto', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + control: { type: 'select' }, + }, + paging: { + pageSize: 12, + }, + // cardMinWidth: 250, + // cardMaxWidth: 350, + // filterPanel: { visible: true }, + columns: { + options: Object.keys(columns), + mapping: columns, + control: { type: 'radio' }, + }, + headerFilter: { + control: 'object', + }, + searchPanel: { + control: 'object', + }, + } +}; + +export default meta; + +type Story = StoryObj; + +export const DefaultMode: Story = { + args: { + dataSource: 'local', + width: "100%", + // TODO: Fix height limit + // height: '500px', + keyExpr: "OrderNumber", + cardsPerRow: "auto", + paging: { + pageSize: 12, + }, + cardMinWidth: 250, + cardMaxWidth: 350, + columns: 'local', + filterPanel: { visible: true }, + }, +}; + +export const RawControls: Story = { + ...DefaultMode, + argTypes: { + ...meta.argTypes, + dataSource: { + control: 'object', + mapping: null, + }, + columns: { + control: 'object', + mapping: null, + }, + }, + args: { + ...DefaultMode.args, + dataSource: dataSources.local.slice(0, 10), + columns: columns.local, + } +}; + +export const FixatedCardsPerRow: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + cardsPerRow: 3 + }, +}; + +export const EmptyCardView: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + dataSource: 'empty', + }, +}; + +export const CardViewWithCover : Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + cardCover: { + imageExpr: (data) => `https://js.devexpress.com/jQuery/Demos/WidgetsGallery/JSDemos/${data.picture}`, + altExpr: 'FirstName', + // ratio: '2 / 1', + }, + }, +}; + +export const SortedCardView: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + dataSource: 'remote', + columns: 'sortedRemote', + }, +}; + +export const SearchCardView: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + dataSource: 'local', + columns: 'local', + searchPanel: { + highlightCaseSensitive: false, + highlightSearchText: true, + text: '', + visible: true, + placeholder: 'Search...', + searchVisibleColumnsOnly: false, + width: 160, + } + } +} + +export const HeaderFilterStory: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + headerFilter: { + visible: true, + width: 252, + height: 325, + allowSelectAll: true, + search: { + enabled: false, + timeout: 500, + mode: 'contains', + editorOptions: {}, + }, + texts: { + emptyValue: 'empty', + ok: 'ok', + cancel: 'cancel', + }, + } + } +} + +export const SelectionStory: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + keyExpr: 'id', + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + allowSelectAll: true, + selectAllMode: 'allPages', + } + } +} + +export const ContextMenuStory: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + onContextMenuPreparing: (e) => { + e.items = e.items ?? []; + + if(e.target === 'toolbar') { + e.items.push({ + text: 'show column chooser', + onItemClick: () => e.component.showColumnChooser() + }); + } + else if(e.target === 'headerPanel' && e.column) { + e.items.push({ + text: `hide ${e.column.caption}`, + disabled: !e.column.visible, + icon: 'eyeclose', + onItemClick: () => e.component.columnOption(e.columnIndex, 'visible', false) + }); + } + else if(e.target === 'content' && e.card) { + e.items.push({ + text: 'do something with card' + }); + } + } + } +} diff --git a/apps/react-storybook/stories/card_view/data.ts b/apps/react-storybook/stories/card_view/data.ts new file mode 100644 index 000000000000..9e2c22a22a44 --- /dev/null +++ b/apps/react-storybook/stories/card_view/data.ts @@ -0,0 +1,40 @@ +import CustomStore from 'devextreme/data/custom_store'; + +export const items = new Array(1000).fill(null).map(() => ( + {column1: 1, column2: 2} +)); + + +function isNotEmpty(value: string | undefined | null) { + return value !== undefined && value !== null && value !== ''; +} + +export const store = new CustomStore({ + key: 'OrderNumber', + async load(loadOptions) { + const paramNames = [ + 'skip', 'take', 'requireTotalCount', 'requireGroupCount', + 'sort', 'filter', 'totalSummary', 'group', 'groupSummary', + ]; + + const queryString = paramNames + .filter((paramName) => isNotEmpty(loadOptions[paramName])) + .map((paramName) => `${paramName}=${JSON.stringify(loadOptions[paramName])}`) + .join('&'); + + try { + const response = await fetch(`https://js.devexpress.com/Demos/WidgetsGalleryDataService/api/orders?${queryString}`); + + const result = await response.json(); + + return { + data: result.data, + totalCount: result.totalCount, + summary: result.summary, + groupCount: result.groupCount, + }; + } catch (err) { + throw new Error('Data Loading Error'); + } + }, +}); \ No newline at end of file diff --git a/apps/react-storybook/stories/card_view/generatedData.ts b/apps/react-storybook/stories/card_view/generatedData.ts new file mode 100644 index 000000000000..4433715542a5 --- /dev/null +++ b/apps/react-storybook/stories/card_view/generatedData.ts @@ -0,0 +1,799 @@ +export const generatedData = [{ + "id": 1, + "firstName": "Curry", + "lastName": "Moynham", + "email": "cmoynham0@google.com.au", + "gender": "Male", + "birthDate": "1983/11/07", + "picture": "images/employees/01.png", +}, { + "id": 2, + "firstName": "Anya", + "lastName": "Le Claire", + "email": "aleclaire1@tinyurl.com", + "gender": "Female", + "birthDate": "1988/03/27", +}, { + "id": 3, + "firstName": "Raven", + "lastName": "Slayton", + "email": "rslayton2@scientificamerican.com", + "gender": "Female", + "birthDate": "1981/04/29", + "picture": "images/employees/03.png", +}, { + "id": 4, + "firstName": "Mireille", + "lastName": "Casini", + "email": "mcasini3@ted.com", + "gender": "Female", + "birthDate": "1993/11/29", + "picture": "images/employees/04.png", +}, { + "id": 5, + "firstName": "Gloriane", + "lastName": "Workes", + "email": "gworkes4@uol.com.br", + "gender": "Female", + "birthDate": "1980/11/28", +}, { + "id": 6, + "firstName": "Elvis", + "lastName": "Matthew", + "email": "ematthew5@qq.com", + "gender": "Male", + "birthDate": "1981/07/24", + "picture": "images/employees/06.png", +}, { + "id": 7, + "firstName": "Gerti", + "lastName": "Greneham", + "email": "ggreneham6@ucoz.ru", + "gender": "Female", + "birthDate": "1994/12/18", + "picture": "images/employees/07.png", +}, { + "id": 8, + "firstName": "Ken", + "lastName": "Collinge", + "email": "kcollinge7@friendfeed.com", + "gender": "Male", + "birthDate": "1982/11/30", + "picture": "images/employees/08.png", +}, { + "id": 9, + "firstName": "Georgina", + "lastName": "Warder", + "email": "gwarder8@salon.com", + "gender": "Female", + "birthDate": "1999/01/14", + "picture": "images/employees/09.png", +}, { + "id": 10, + "firstName": "Mose", + "lastName": "Vertey", + "email": "mvertey9@vinaora.com", + "gender": "Male", + "birthDate": "1996/09/11", + "picture": "images/employees/02.png", +}, { + "id": 11, + "firstName": "Karlik", + "lastName": "Windows", + "email": "kwindowsa@wunderground.com", + "gender": "Male", + "birthDate": "1980/03/17", + "picture": "images/employees/02.png", +}, { + "id": 12, + "firstName": "Konstanze", + "lastName": "Navan", + "email": "knavanb@hatena.ne.jp", + "gender": "Female", + "birthDate": "1980/04/23", + "picture": "images/employees/02.png", +}, { + "id": 13, + "firstName": "Anet", + "lastName": "Saberton", + "email": "asabertonc@quantcast.com", + "gender": "Female", + "birthDate": "1992/05/05", + "picture": "images/employees/02.png", +}, { + "id": 14, + "firstName": "Ruthy", + "lastName": "Casserly", + "email": "rcasserlyd@ezinearticles.com", + "gender": "Female", + "birthDate": "1982/05/25", + "picture": "images/employees/02.png", +}, { + "id": 15, + "firstName": "Hakim", + "lastName": "McCrainor", + "email": "hmccrainore@prnewswire.com", + "gender": "Polygender", + "birthDate": "1989/02/26", + "picture": "images/employees/02.png", +}, { + "id": 16, + "firstName": "Jennica", + "lastName": "Kinsell", + "email": "jkinsellf@prnewswire.com", + "gender": "Female", + "birthDate": "1989/10/07", + "picture": "images/employees/02.png", +}, { + "id": 17, + "firstName": "Reed", + "lastName": "Abramovitch", + "email": "rabramovitchg@psu.edu", + "gender": "Male", + "birthDate": "1997/10/05", + "picture": "images/employees/02.png", +}, { + "id": 18, + "firstName": "Arlena", + "lastName": "Heinlein", + "email": "aheinleinh@pen.io", + "gender": "Agender", + "birthDate": "1987/05/08", + "picture": "images/employees/02.png", +}, { + "id": 19, + "firstName": "Opaline", + "lastName": "Climar", + "email": "oclimari@histats.com", + "gender": "Female", + "birthDate": "1986/04/10", + "picture": "images/employees/02.png", +}, { + "id": 20, + "firstName": "Serge", + "lastName": "Mullan", + "email": "smullanj@senate.gov", + "gender": "Male", + "birthDate": "1985/10/26", + "picture": "images/employees/02.png", +}, { + "id": 21, + "firstName": "Lucy", + "lastName": "Congrave", + "email": "lcongravek@globo.com", + "gender": "Female", + "birthDate": "1983/04/12", + "picture": "images/employees/02.png", +}, { + "id": 22, + "firstName": "Harland", + "lastName": "Gerardot", + "email": "hgerardotl@comcast.net", + "gender": "Male", + "birthDate": "1992/01/04", + "picture": "images/employees/02.png", +}, { + "id": 23, + "firstName": "Maisey", + "lastName": "Boken", + "email": "mbokenm@economist.com", + "gender": "Female", + "birthDate": "1985/07/03", + "picture": "images/employees/02.png", +}, { + "id": 24, + "firstName": "Devlin", + "lastName": "Sayle", + "email": "dsaylen@squarespace.com", + "gender": "Male", + "birthDate": "1980/06/24", + "picture": "images/employees/02.png", +}, { + "id": 25, + "firstName": "Cleve", + "lastName": "Schuler", + "email": "cschulero@reference.com", + "gender": "Male", + "birthDate": "1995/05/02", + "picture": "images/employees/02.png", +}, { + "id": 26, + "firstName": "Jorgan", + "lastName": "Navan", + "email": "jnavanp@buzzfeed.com", + "gender": "Male", + "birthDate": "1985/03/16", + "picture": "images/employees/02.png", +}, { + "id": 27, + "firstName": "Worth", + "lastName": "Hellens", + "email": "whellensq@wikispaces.com", + "gender": "Male", + "birthDate": "1986/01/20", + "picture": "images/employees/02.png", +}, { + "id": 28, + "firstName": "Annnora", + "lastName": "Garahan", + "email": "agarahanr@sina.com.cn", + "gender": "Female", + "birthDate": "1986/06/09", + "picture": "images/employees/02.png", +}, { + "id": 29, + "firstName": "Thaxter", + "lastName": "Pembridge", + "email": "tpembridges@about.com", + "gender": "Male", + "birthDate": "1991/03/06", + "picture": "images/employees/02.png", +}, { + "id": 30, + "firstName": "Annabela", + "lastName": "Hannaway", + "email": "ahannawayt@dion.ne.jp", + "gender": "Non-binary", + "birthDate": "1996/01/15", + "picture": "images/employees/02.png", +}, { + "id": 31, + "firstName": "Nelson", + "lastName": "Geerdts", + "email": "ngeerdtsu@abc.net.au", + "gender": "Male", + "birthDate": "1999/03/12", + "picture": "images/employees/02.png", +}, { + "id": 32, + "firstName": "Viviyan", + "lastName": "Collet", + "email": "vcolletv@ox.ac.uk", + "gender": "Female", + "birthDate": "1984/09/20", + "picture": "images/employees/02.png", +}, { + "id": 33, + "firstName": "Pammy", + "lastName": "Sneyd", + "email": "psneydw@nyu.edu", + "gender": "Female", + "birthDate": "1998/04/30", + "picture": "images/employees/02.png", +}, { + "id": 34, + "firstName": "Sofia", + "lastName": "Yekel", + "email": "syekelx@netscape.com", + "gender": "Agender", + "birthDate": "1990/03/27", + "picture": "images/employees/02.png", +}, { + "id": 35, + "firstName": "Mose", + "lastName": "Crowcombe", + "email": "mcrowcombey@yahoo.com", + "gender": "Male", + "birthDate": "1993/11/14", + "picture": "images/employees/02.png", +}, { + "id": 36, + "firstName": "Marcello", + "lastName": "Squibb", + "email": "msquibbz@themeforest.net", + "gender": "Male", + "birthDate": "1997/12/06", + "picture": "images/employees/02.png", +}, { + "id": 37, + "firstName": "Tiebold", + "lastName": "Tippings", + "email": "ttippings10@shop-pro.jp", + "gender": "Male", + "birthDate": "1987/06/28", + "picture": "images/employees/02.png", +}, { + "id": 38, + "firstName": "Rosemarie", + "lastName": "Saiens", + "email": "rsaiens11@163.com", + "gender": "Female", + "birthDate": "1980/08/30", + "picture": "images/employees/02.png", +}, { + "id": 39, + "firstName": "Dylan", + "lastName": "Lugton", + "email": "dlugton12@elpais.com", + "gender": "Male", + "birthDate": "1997/01/18", + "picture": "images/employees/02.png", +}, { + "id": 40, + "firstName": "Maxim", + "lastName": "Laphorn", + "email": "mlaphorn13@vinaora.com", + "gender": "Male", + "birthDate": "1990/10/14", + "picture": "images/employees/02.png", +}, { + "id": 41, + "firstName": "Charity", + "lastName": "Lorking", + "email": "clorking14@surveymonkey.com", + "gender": "Female", + "birthDate": "1998/07/07", + "picture": "images/employees/02.png", +}, { + "id": 42, + "firstName": "Stevie", + "lastName": "Wagenen", + "email": "swagenen15@archive.org", + "gender": "Male", + "birthDate": "1989/12/13", + "picture": "images/employees/02.png", +}, { + "id": 43, + "firstName": "Julita", + "lastName": "Hopfner", + "email": "jhopfner16@google.ca", + "gender": "Female", + "birthDate": "1998/10/11", + "picture": "images/employees/02.png", +}, { + "id": 44, + "firstName": "Ethel", + "lastName": "Murdy", + "email": "emurdy17@typepad.com", + "gender": "Female", + "birthDate": "1987/05/19", + "picture": "images/employees/02.png", +}, { + "id": 45, + "firstName": "Freeland", + "lastName": "Brimham", + "email": "fbrimham18@army.mil", + "gender": "Male", + "birthDate": "1994/01/18", + "picture": "images/employees/02.png", +}, { + "id": 46, + "firstName": "Fons", + "lastName": "Mangeon", + "email": "fmangeon19@bravesites.com", + "gender": "Male", + "birthDate": "1998/04/22", + "picture": "images/employees/02.png", +}, { + "id": 47, + "firstName": "Gregoor", + "lastName": "Disney", + "email": "gdisney1a@disqus.com", + "gender": "Male", + "birthDate": "1991/06/04", + "picture": "images/employees/02.png", +}, { + "id": 48, + "firstName": "Thorny", + "lastName": "Descroix", + "email": "tdescroix1b@wunderground.com", + "gender": "Male", + "birthDate": "1981/03/15", + "picture": "images/employees/02.png", +}, { + "id": 49, + "firstName": "Olwen", + "lastName": "Clewett", + "email": "oclewett1c@merriam-webster.com", + "gender": "Female", + "birthDate": "1986/09/28", + "picture": "images/employees/02.png", +}, { + "id": 50, + "firstName": "Kendal", + "lastName": "Scrauniage", + "email": "kscrauniage1d@cmu.edu", + "gender": "Male", + "birthDate": "1982/09/25", + "picture": "images/employees/02.png", +}, { + "id": 51, + "firstName": "Mendie", + "lastName": "Bonallack", + "email": "mbonallack1e@amazon.com", + "gender": "Male", + "birthDate": "1985/05/07", + "picture": "images/employees/02.png", +}, { + "id": 52, + "firstName": "Nellie", + "lastName": "Brave", + "email": "nbrave1f@freewebs.com", + "gender": "Bigender", + "birthDate": "1997/06/10", + "picture": "images/employees/02.png", +}, { + "id": 53, + "firstName": "Shir", + "lastName": "Lipson", + "email": "slipson1g@nydailynews.com", + "gender": "Female", + "birthDate": "1994/11/29", + "picture": "images/employees/02.png", +}, { + "id": 54, + "firstName": "Brittney", + "lastName": "Barley", + "email": "bbarley1h@jigsy.com", + "gender": "Female", + "birthDate": "1999/04/17", + "picture": "images/employees/02.png", +}, { + "id": 55, + "firstName": "Pedro", + "lastName": "Spurrett", + "email": "pspurrett1i@mysql.com", + "gender": "Genderfluid", + "birthDate": "1981/08/31", + "picture": "images/employees/02.png", +}, { + "id": 56, + "firstName": "Merrielle", + "lastName": "Davana", + "email": "mdavana1j@booking.com", + "gender": "Female", + "birthDate": "1985/07/25", + "picture": "images/employees/02.png", +}, { + "id": 57, + "firstName": "Tildie", + "lastName": "Sacaze", + "email": "tsacaze1k@livejournal.com", + "gender": "Female", + "birthDate": "1998/08/26", + "picture": "images/employees/02.png", +}, { + "id": 58, + "firstName": "Hurley", + "lastName": "Loomis", + "email": "hloomis1l@phoca.cz", + "gender": "Male", + "birthDate": "1980/06/23", + "picture": "images/employees/02.png", +}, { + "id": 59, + "firstName": "Rowland", + "lastName": "Keeling", + "email": "rkeeling1m@umn.edu", + "gender": "Male", + "birthDate": "1989/05/13", + "picture": "images/employees/02.png", +}, { + "id": 60, + "firstName": "Bonita", + "lastName": "Harwin", + "email": "bharwin1n@adobe.com", + "gender": "Female", + "birthDate": "1988/09/03", + "picture": "images/employees/02.png", +}, { + "id": 61, + "firstName": "Elva", + "lastName": "Doswell", + "email": "edoswell1o@amazonaws.com", + "gender": "Female", + "birthDate": "1985/08/25", + "picture": "images/employees/02.png", +}, { + "id": 62, + "firstName": "Zeb", + "lastName": "McQuode", + "email": "zmcquode1p@berkeley.edu", + "gender": "Male", + "birthDate": "1998/03/24", + "picture": "images/employees/02.png", +}, { + "id": 63, + "firstName": "Pearce", + "lastName": "Yannikov", + "email": "pyannikov1q@weibo.com", + "gender": "Male", + "birthDate": "1988/09/19", + "picture": "images/employees/02.png", +}, { + "id": 64, + "firstName": "Keeley", + "lastName": "Starford", + "email": "kstarford1r@huffingtonpost.com", + "gender": "Female", + "birthDate": "1998/11/29", + "picture": "images/employees/02.png", +}, { + "id": 65, + "firstName": "Kurt", + "lastName": "Surby", + "email": "ksurby1s@hostgator.com", + "gender": "Male", + "birthDate": "1981/09/19", + "picture": "images/employees/02.png", +}, { + "id": 66, + "firstName": "Dwight", + "lastName": "Sickling", + "email": "dsickling1t@constantcontact.com", + "gender": "Male", + "birthDate": "1988/10/24", + "picture": "images/employees/02.png", +}, { + "id": 67, + "firstName": "Vin", + "lastName": "Tawse", + "email": "vtawse1u@cornell.edu", + "gender": "Female", + "birthDate": "1995/10/13", + "picture": "images/employees/02.png", +}, { + "id": 68, + "firstName": "Lyda", + "lastName": "Edgcombe", + "email": "ledgcombe1v@yelp.com", + "gender": "Female", + "birthDate": "1983/07/23", + "picture": "images/employees/02.png", +}, { + "id": 69, + "firstName": "Oliviero", + "lastName": "Fewell", + "email": "ofewell1w@creativecommons.org", + "gender": "Male", + "birthDate": "1987/10/25", + "picture": "images/employees/02.png", +}, { + "id": 70, + "firstName": "Erick", + "lastName": "Hatchett", + "email": "ehatchett1x@wufoo.com", + "gender": "Male", + "birthDate": "1994/12/04", + "picture": "images/employees/02.png", +}, { + "id": 71, + "firstName": "Magdalena", + "lastName": "Jex", + "email": "mjex1y@friendfeed.com", + "gender": "Female", + "birthDate": "1989/09/03", + "picture": "images/employees/02.png", +}, { + "id": 72, + "firstName": "Carver", + "lastName": "Kisting", + "email": "ckisting1z@netvibes.com", + "gender": "Male", + "birthDate": "1996/06/13", + "picture": "images/employees/02.png", +}, { + "id": 73, + "firstName": "Dore", + "lastName": "Carff", + "email": "dcarff20@about.com", + "gender": "Male", + "birthDate": "1997/09/16", + "picture": "images/employees/02.png", +}, { + "id": 74, + "firstName": "Antonius", + "lastName": "John", + "email": "ajohn21@amazon.co.uk", + "gender": "Male", + "birthDate": "1981/01/23", + "picture": "images/employees/02.png", +}, { + "id": 75, + "firstName": "Bone", + "lastName": "Lourenco", + "email": "blourenco22@sogou.com", + "gender": "Male", + "birthDate": "1995/06/10", + "picture": "images/employees/02.png", +}, { + "id": 76, + "firstName": "Wakefield", + "lastName": "Tandey", + "email": "wtandey23@elpais.com", + "gender": "Male", + "birthDate": "1996/02/08", + "picture": "images/employees/02.png", +}, { + "id": 77, + "firstName": "Juditha", + "lastName": "Gumm", + "email": "jgumm24@goo.ne.jp", + "gender": "Female", + "birthDate": "1991/06/21", + "picture": "images/employees/02.png", +}, { + "id": 78, + "firstName": "Wilone", + "lastName": "Lafaye", + "email": "wlafaye25@pcworld.com", + "gender": "Female", + "birthDate": "1985/02/17", + "picture": "images/employees/02.png", +}, { + "id": 79, + "firstName": "Cece", + "lastName": "Gifkins", + "email": "cgifkins26@slate.com", + "gender": "Male", + "birthDate": "1997/11/10", + "picture": "images/employees/02.png", +}, { + "id": 80, + "firstName": "Darin", + "lastName": "Lyddy", + "email": "dlyddy27@infoseek.co.jp", + "gender": "Male", + "birthDate": "1984/07/07", + "picture": "images/employees/02.png", +}, { + "id": 81, + "firstName": "Trevar", + "lastName": "Knowller", + "email": "tknowller28@sciencedirect.com", + "gender": "Male", + "birthDate": "1992/12/14", + "picture": "images/employees/02.png", +}, { + "id": 82, + "firstName": "Milli", + "lastName": "Coppeard", + "email": "mcoppeard29@ted.com", + "gender": "Female", + "birthDate": "1992/03/29", + "picture": "images/employees/02.png", +}, { + "id": 83, + "firstName": "Cayla", + "lastName": "Davidwitz", + "email": "cdavidwitz2a@yandex.ru", + "gender": "Female", + "birthDate": "1982/02/10", + "picture": "images/employees/02.png", +}, { + "id": 84, + "firstName": "Laura", + "lastName": "Pherps", + "email": "lpherps2b@tripadvisor.com", + "gender": "Female", + "birthDate": "1980/01/12", + "picture": "images/employees/02.png", +}, { + "id": 85, + "firstName": "Lianne", + "lastName": "Jennaway", + "email": "ljennaway2c@parallels.com", + "gender": "Female", + "birthDate": "1993/10/12", + "picture": "images/employees/02.png", +}, { + "id": 86, + "firstName": "Rollin", + "lastName": "Besnardeau", + "email": "rbesnardeau2d@php.net", + "gender": "Agender", + "birthDate": "1999/05/24", + "picture": "images/employees/02.png", +}, { + "id": 87, + "firstName": "Augustine", + "lastName": "Keaton", + "email": "akeaton2e@surveymonkey.com", + "gender": "Female", + "birthDate": "1985/08/16", + "picture": "images/employees/02.png", +}, { + "id": 88, + "firstName": "Wynn", + "lastName": "Couthard", + "email": "wcouthard2f@webmd.com", + "gender": "Male", + "birthDate": "1990/08/06", + "picture": "images/employees/02.png", +}, { + "id": 89, + "firstName": "Stirling", + "lastName": "Cleevely", + "email": "scleevely2g@java.com", + "gender": "Agender", + "birthDate": "1999/04/28", + "picture": "images/employees/02.png", +}, { + "id": 90, + "firstName": "Tracee", + "lastName": "Maitland", + "email": "tmaitland2h@ning.com", + "gender": "Female", + "birthDate": "1999/12/21", + "picture": "images/employees/02.png", +}, { + "id": 91, + "firstName": "Joscelin", + "lastName": "Dregan", + "email": "jdregan2i@ucla.edu", + "gender": "Female", + "birthDate": "1999/01/30", + "picture": "images/employees/02.png", +}, { + "id": 92, + "firstName": "Rolfe", + "lastName": "Tilt", + "email": "rtilt2j@drupal.org", + "gender": "Male", + "birthDate": "1996/02/03", + "picture": "images/employees/02.png", +}, { + "id": 93, + "firstName": "Bordy", + "lastName": "Beyne", + "email": "bbeyne2k@zdnet.com", + "gender": "Male", + "birthDate": "1992/07/18", + "picture": "images/employees/02.png", +}, { + "id": 94, + "firstName": "Grier", + "lastName": "Spaducci", + "email": "gspaducci2l@bravesites.com", + "gender": "Female", + "birthDate": "1995/11/06", + "picture": "images/employees/02.png", +}, { + "id": 95, + "firstName": "Licha", + "lastName": "Witcherley", + "email": "lwitcherley2m@latimes.com", + "gender": "Female", + "birthDate": "1987/01/09", + "picture": "images/employees/02.png", +}, { + "id": 96, + "firstName": "Cori", + "lastName": "Gwatkins", + "email": "cgwatkins2n@marriott.com", + "gender": "Male", + "birthDate": "1984/08/26", + "picture": "images/employees/02.png", +}, { + "id": 97, + "firstName": "Annecorinne", + "lastName": "Abbey", + "email": "aabbey2o@miitbeian.gov.cn", + "gender": "Agender", + "birthDate": "1999/05/03", + "picture": "images/employees/02.png", +}, { + "id": 98, + "firstName": "Denice", + "lastName": "Isaksen", + "email": "disaksen2p@indiatimes.com", + "gender": "Female", + "birthDate": "1994/05/10", + "picture": "images/employees/02.png", +}, { + "id": 99, + "firstName": "Sabrina", + "lastName": "Oulett", + "email": "soulett2q@ifeng.com", + "gender": "Female", + "birthDate": "1985/01/14", + "picture": "images/employees/02.png", +}, { + "id": 100, + "firstName": "Jarrod", + "lastName": "Hellewell", + "email": "jhellewell2r@springer.com", + "gender": "Male", + "birthDate": "1985/08/20", + "picture": "images/employees/02.png", +}]; diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/a11y.functional.ts b/e2e/testcafe-devextreme/tests/cardView/columnChooser/a11y.functional.ts new file mode 100644 index 000000000000..b8a7ed48ca1a --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/columnChooser/a11y.functional.ts @@ -0,0 +1,22 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture.disablePageReloads`CardView - ColumnChooser.A11y.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('column chooser popup should have aria-label attribute', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const columnChooser = cardView.getColumnChooser(); + + await cardView.apiShowColumnChooser(); + + await t.expect(columnChooser.content.getAttribute('aria-label')).ok(); +}).before(async () => createWidget('dxCardView', { + columnChooser: { + enabled: true, + }, + columns: ['Column 1'], +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/api.functional.ts b/e2e/testcafe-devextreme/tests/cardView/columnChooser/api.functional.ts new file mode 100644 index 000000000000..5f1a73eeb508 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/columnChooser/api.functional.ts @@ -0,0 +1,44 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture.disablePageReloads`CardView - ColumnChooser.Functional` + .page(url(__dirname, '../../container.html')); + +// TODO: refator this when cardView is merged to 25.1 . This should be in ColumnChooser POM +const isOpened = (columnChooser) => columnChooser.element.exists; + +test('public method showColumnChooser', async (t) => { + const cardView = new CardView('#container'); + const columnChooser = cardView.getColumnChooser(); + + await t.expect(isOpened(columnChooser)).notOk(); + + await cardView.apiShowColumnChooser(); + await t.expect(isOpened(columnChooser)).ok(); +}).before(async () => { + await createWidget('dxCardView', { + columns: ['Column 1'], + columnChooser: { + enabled: true, + }, + }); +}); + +test('public method hideColumnChooser', async (t) => { + const cardView = new CardView('#container'); + const columnChooser = cardView.getColumnChooser(); + + await t.click(cardView.getColumnChooserButton()); + await t.expect(isOpened(columnChooser)).ok(); + + await cardView.apiHideColumnChooser(); + await t.expect(isOpened(columnChooser)).notOk(); +}).before(async () => { + await createWidget('dxCardView', { + columns: ['Column 1'], + columnChooser: { + enabled: true, + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (fluent-blue-light).png new file mode 100644 index 000000000000..50f1a51a6783 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (generic-light).png new file mode 100644 index 000000000000..c11f6eb3b6f4 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (material-blue-light).png new file mode 100644 index 000000000000..e02c71ffb903 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/visual.ts b/e2e/testcafe-devextreme/tests/cardView/columnChooser/visual.ts new file mode 100644 index 000000000000..f41bca9eea68 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/columnChooser/visual.ts @@ -0,0 +1,43 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import url from '../../../helpers/getPageUrl'; +import { testScreenshot } from '../../../helpers/themeUtils'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture`CardView - ColumnChooser.Visual` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('column chooser in select mode', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + const columnChooser = cardView.getColumnChooser(); + + await cardView.apiShowColumnChooser(); + + await testScreenshot(t, takeScreenshot, 'card-view_column-chooser.png', { element: columnChooser.content }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + columnChooser: { + enabled: true, + mode: 'select', + height: 400, + width: 400, + search: { + enabled: true, + }, + selection: { + allowSelectAll: true, + }, + }, + columns: [ + { dataField: 'Column 1', visible: false }, + { dataField: 'Column 2', allowHiding: false }, + { dataField: 'Column 3', showInColumnChooser: false }, + { dataField: 'Column 4' }, + ], +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/cover.ts b/e2e/testcafe-devextreme/tests/cardView/cover.ts new file mode 100644 index 000000000000..49183b3d7956 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/cover.ts @@ -0,0 +1,60 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../helpers/getPageUrl'; +import { createWidget } from '../../helpers/createWidget'; +import { testScreenshot } from '../../helpers/themeUtils'; + +fixture.disablePageReloads`CardView - HeaderPanel` + .page(url(__dirname, '../container.html')); + +test('default render', async (t) => { + const cardView = new CardView('#container'); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await testScreenshot(t, takeScreenshot, 'cover-default-render.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + width: 1000, + height: 600, + columns: ['Customer', 'Order Date'], + cardCover: { + imageExpr: (data) => data.Picture && `https://js.devexpress.com/jQuery/Demos/WidgetsGallery/JSDemos/${data.Picture}`, + altExpr: 'FirstName', + }, + dataSource: [{ + ID: 1, + FirstName: 'John', + LastName: 'Heart', + Prefix: 'Mr.', + Position: 'CEO', + Picture: 'images/employees/01.png', + BirthDate: '1964/03/16', + HireDate: '1995/01/15', + Notes: 'John has been in the Audio/Video industry since 1990. He has led DevAv as its CEO since 2003. When not working hard as the CEO, John loves to golf and bowl. He once bowled a perfect game of 300.', + Address: '351 S Hill St.', + }, { + ID: 2, + FirstName: 'Olivia', + LastName: 'Peyton', + Prefix: 'Mrs.', + Position: 'Sales Assistant', + BirthDate: '1981/06/03', + HireDate: '2012/05/14', + Notes: 'Olivia loves to sell. She has been selling DevAV products since 2012. Olivia was homecoming queen in high school. She is expecting her first child in 6 months. Good Luck Olivia.', + Address: '807 W Paseo Del Mar', + }, { + ID: 3, + FirstName: 'Robert', + LastName: 'Reagan', + Prefix: 'Mr.', + Position: 'CMO', + Picture: 'images/employees/03.png', + BirthDate: '1974/09/07', + HireDate: '2002/11/08', + Notes: 'Robert was recently voted the CMO of the year by CMO Magazine. He is a proud member of the DevAV Management Team. Robert is a championship BBQ chef, so when you get the chance ask him for his secret recipe.', + Address: '4 Westmoreland Pl.', + }], +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (fluent-blue-light).png new file mode 100644 index 000000000000..10645effe4df Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (generic-light).png new file mode 100644 index 000000000000..2d3272c4bea1 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (material-blue-light).png new file mode 100644 index 000000000000..af632315f77c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (fluent-blue-light).png new file mode 100644 index 000000000000..a57856ecb50c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (generic-light).png new file mode 100644 index 000000000000..339e7dc4c847 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (material-blue-light).png new file mode 100644 index 000000000000..2d6ec4c88f26 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/headers.png b/e2e/testcafe-devextreme/tests/cardView/etalons/headers.png new file mode 100644 index 000000000000..f867d818c52a Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/headers.png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.filterBuilder.functional.ts b/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.filterBuilder.functional.ts new file mode 100644 index 000000000000..78e03d467938 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.filterBuilder.functional.ts @@ -0,0 +1,55 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - FilterBuilder API` + .page(url(__dirname, '../../container.html')); + +test('filterBuilder.height API', async (t) => { + const cardView = new CardView('#container'); + const filterBuilderPopup = await cardView.getFilterPanel().openFilterBuilderPopup(t); + + await t + .expect(filterBuilderPopup.getFilterBuilder().element.clientHeight) + .eql(500); + + await cardView.apiOption('filterBuilder.height', 700); + + await t + .expect(filterBuilderPopup.getFilterBuilder().element.clientHeight) + .eql(700); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterBuilder: { + height: 500, + }, + }, + }); +}); + +test('filterBuilder.hint API', async (t) => { + const cardView = new CardView('#container'); + const filterBuilderPopup = await cardView.getFilterPanel().openFilterBuilderPopup(t); + + await t + .expect(filterBuilderPopup.getFilterBuilder().element.getAttribute('title')) + .eql('Test'); + + await cardView.apiOption('filterBuilder.hint', 'Test2'); + + await t + .expect(filterBuilderPopup.getFilterBuilder().element.getAttribute('title')) + .eql('Test2'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterBuilder: { + hint: 'Test', + }, + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.filterBuilderPopup.functional.ts b/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.filterBuilderPopup.functional.ts new file mode 100644 index 000000000000..ef7fffb7cb4d --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.filterBuilderPopup.functional.ts @@ -0,0 +1,55 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - FilterBuilderPopup API` + .page(url(__dirname, '../../container.html')); + +test('filterBuilderPopup.height API', async (t) => { + const cardView = new CardView('#container'); + const filterBuilderPopup = await cardView.getFilterPanel().openFilterBuilderPopup(t); + + await t + .expect(filterBuilderPopup.asPopup().content.offsetHeight) + .eql(500); + + await cardView.apiOption('filterBuilderPopup.height', 700); + + await t + .expect(filterBuilderPopup.asPopup().content.offsetHeight) + .eql(700); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterBuilderPopup: { + height: 500, + }, + }, + }); +}); + +test('filterBuilderPopup.title API', async (t) => { + const cardView = new CardView('#container'); + const filterBuilderPopup = await cardView.getFilterPanel().openFilterBuilderPopup(t); + + await t + .expect(filterBuilderPopup.asPopup().getToolbar().innerText) + .eql('Test'); + + await cardView.apiOption('filterBuilderPopup.title', 'Test2'); + + await t + .expect(filterBuilderPopup.asPopup().getToolbar().innerText) + .eql('Test2'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterBuilderPopup: { + title: 'Test', + }, + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.functional.ts b/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.functional.ts new file mode 100644 index 000000000000..946a11305ff7 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.functional.ts @@ -0,0 +1,194 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - FilterPanel API` + .page(url(__dirname, '../../container.html')); + +test('filterPanel.customizeText API', async (t) => { + const cardView = new CardView('#container'); + + await t + .expect(cardView.getFilterPanel().getFilterText().element.innerText) + .eql('Men'); + + await cardView.apiOption('filterPanel.customizeText', (e) => { + if (e.text === '[Title] Equals \'Mr.\'') { + return 'Not women'; + } + if (e.text === '[Title] Equals \'Mrs.\'') { + return 'Not men'; + } + return e.text; + }); + + await t + .expect(cardView.getFilterPanel().getFilterText().element.innerText) + .eql('Not women'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + filterPanel: { + ...baseConfig.filterPanel, + customizeText(e) { + if (e.text === '[Title] Equals \'Mr.\'') { + return 'Men'; + } + if (e.text === '[Title] Equals \'Mrs.\'') { + return 'Women'; + } + return e.text; + }, + }, + }, + }); +}); + +test('filterEnabled API', async (t) => { + const cardView = new CardView('#container'); + await t + .expect(cardView.getFilterPanel().getFilterEnabledCheckbox().isChecked) + .notOk() + .expect(cardView.getCards().count) + .eql(4); + + await cardView.apiOption('filterPanel.filterEnabled', true); + + await t + .expect(cardView.getFilterPanel().getFilterEnabledCheckbox().isChecked) + .ok() + .expect(cardView.getCards().count) + .eql(3); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + filterPanel: { + ...baseConfig.filterPanel, + filterEnabled: false, + }, + }, + }); +}); + +test('filterPanel.texts API', async (t) => { + const cardView = new CardView('#container'); + const filterPanel = cardView.getFilterPanel(); + await t + .expect(filterPanel.getFilterEnabledCheckbox().element.getAttribute('title')) + .eql('Custom Filter Enabled Hint') + .expect(filterPanel.getClearFilterButton().element.innerText) + .eql('Custom Clear Filter'); + + await cardView.apiOption('filterPanel.texts.clearFilter', 'Custom Clear Filter2'); + await cardView.apiOption('filterPanel.texts.filterEnabledHint', 'Custom Filter Enabled Hint2'); + + await t + .expect(filterPanel.getFilterEnabledCheckbox().element.getAttribute('title')) + .eql('Custom Filter Enabled Hint2') + .expect(filterPanel.getClearFilterButton().element.innerText) + .eql('Custom Clear Filter2'); + + await t + .click(filterPanel.getClearFilterButton().element) + .expect(filterPanel.getFilterText().element.innerText) + .eql('Custom Create Filter'); + + await cardView.apiOption('filterPanel.texts.createFilter', 'Custom Create Filter2'); + + await t + .expect(filterPanel.getFilterText().element.innerText) + .eql('Custom Create Filter2'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + filterPanel: { + ...baseConfig.filterPanel, + texts: { + clearFilter: 'Custom Clear Filter', + createFilter: 'Custom Create Filter', + filterEnabledHint: 'Custom Filter Enabled Hint', + }, + }, + }, + }); +}); + +test('filterPanel.visible API', async (t) => { + const cardView = new CardView('#container'); + + await t + .expect(cardView.getFilterPanel().element.exists) + .notOk(); + + await cardView.apiOption('filterPanel.visible', true); + + await t + .expect(cardView.getFilterPanel().element.exists) + .ok(); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + filterPanel: { + ...baseConfig.filterPanel, + visible: false, + }, + }, + }); +}); + +test('filterValue API', async (t) => { + const cardView = new CardView('#container'); + const filterText = cardView.getFilterPanel().getFilterText(); + + await t + .expect(filterText.element.innerText) + .eql('[Title] Equals \'Mr.\''); + + await cardView.apiOption('filterValue', ['title', '=', 'Mrs.']); + + await t + .expect(filterText.element.innerText) + .eql('[Title] Equals \'Mrs.\''); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + }, + }); +}); + +test('clearFilter API', async (t) => { + const cardView = new CardView('#container'); + const filterText = cardView.getFilterPanel().getFilterText(); + + await t + .expect(filterText.element.innerText) + .eql('[Title] Equals \'Mr.\'') + .expect(cardView.getCards().count) + .eql(3); + + await cardView.apiClearFilter(); + + await t + .expect(filterText.element.innerText) + .eql('Create Filter') + .expect(cardView.getCards().count) + .eql(4); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/behavior.functional.ts b/e2e/testcafe-devextreme/tests/cardView/filterPanel/behavior.functional.ts new file mode 100644 index 000000000000..edf47ccad66d --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/filterPanel/behavior.functional.ts @@ -0,0 +1,235 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import Button from 'devextreme-testcafe-models/button'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - FilterPanel Behavior` + .page(url(__dirname, '../../container.html')); + +test('filterEnabled checkbox switches the filter by click', async (t) => { + const cardView = new CardView('#container'); + const filterEnabledCheckbox = cardView.getFilterPanel().getFilterEnabledCheckbox(); + await t + .expect(filterEnabledCheckbox.isChecked) + .notOk() + .expect(cardView.getCards().count) + .eql(4); + + await t + .click(filterEnabledCheckbox.element) + .expect(filterEnabledCheckbox.isChecked) + .ok() + .expect(cardView.getCards().count) + .eql(3); + + await t + .click(filterEnabledCheckbox.element) + .expect(filterEnabledCheckbox.isChecked) + .notOk() + .expect(cardView.getCards().count) + .eql(4); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + filterPanel: { + ...baseConfig.filterPanel, + filterEnabled: false, + }, + }, + }); +}); + +test('filterEnabled checkbox switches the filter by keyboard', async (t) => { + const cardView = new CardView('#container'); + const startButton = new Button('#otherContainer'); + const filterEnabledCheckbox = cardView.getFilterPanel().getFilterEnabledCheckbox(); + + await t + .expect(filterEnabledCheckbox.isChecked) + .notOk() + .expect(cardView.getCards().count) + .eql(4); + + await t + .click(startButton.element) + .pressKey('shift+tab shift+tab shift+tab shift+tab') + .pressKey('space') + .expect(filterEnabledCheckbox.isChecked) + .ok() + .expect(cardView.getCards().count) + .eql(3); + + await t + .click(startButton.element) // TODO: remove this when checkbox focus loosing is fixed + .pressKey('shift+tab shift+tab shift+tab shift+tab') + .pressKey('space') + .expect(filterEnabledCheckbox.isChecked) + .notOk() + .expect(cardView.getCards().count) + .eql(4); +}).before(async () => { + await createWidget('dxButton', { + text: 'Click Here First', + }, '#otherContainer'); + + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + filterPanel: { + ...baseConfig.filterPanel, + filterEnabled: false, + }, + }, + }); +}); + +test('FilterIcon opens popup by click', async (t) => { + const cardView = new CardView('#container'); + const popup = cardView.getFilterPanel().getFilterBuilderPopup(); + + await t + .expect(popup.element.exists) + .notOk() + .click(cardView.getFilterPanel().getIconFilter().element) + .expect(popup.element.exists) + .ok(); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + }); +}); + +test('FilterIcon opens popup by keyboard', async (t) => { + const cardView = new CardView('#container'); + const startButton = new Button('#otherContainer'); + const popup = cardView.getFilterPanel().getFilterBuilderPopup(); + + await t + .expect(popup.element.exists) + .notOk() + .click(startButton.element) + .pressKey('shift+tab shift+tab') + .pressKey('enter') + .expect(popup.element.exists) + .ok(); +}).before(async () => { + await createWidget('dxButton', { + text: 'Click Here First', + }, '#otherContainer'); + + await createWidget('dxCardView', { + ...baseConfig, + }); +}); + +test('FilterText opens popup by click', async (t) => { + const cardView = new CardView('#container'); + const popup = cardView.getFilterPanel().getFilterBuilderPopup(); + + await t + .expect(popup.element.exists) + .notOk() + .click(cardView.getFilterPanel().getFilterText().element) + .expect(popup.element.exists) + .ok(); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + }); +}); + +test('FilterText opens popup by click by keyboard', async (t) => { + const cardView = new CardView('#container'); + const startButton = new Button('#otherContainer'); + const popup = cardView.getFilterPanel().getFilterBuilderPopup(); + + await t + .expect(popup.element.exists) + .notOk() + .click(startButton.element) + .pressKey('shift+tab') + .pressKey('enter') + .expect(popup.element.exists) + .ok(); +}).before(async () => { + await createWidget('dxButton', { + text: 'Click Here First', + }, '#otherContainer'); + + await createWidget('dxCardView', { + ...baseConfig, + }); +}); + +test('ClearFilter button clears filter by click', async (t) => { + const cardView = new CardView('#container'); + + await t + .expect(cardView.option('filterValue')) + .eql(['title', '=', 'Mr.']); + + await t + .click(cardView.getFilterPanel().getClearFilterButton().element) + .expect(cardView.option('filterValue')) + .eql(null); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + filterValue: ['title', '=', 'Mr.'], + }); +}); + +test('ClearFilter button clears filter by keyboard', async (t) => { + const cardView = new CardView('#container'); + const startButton = new Button('#otherContainer'); + + await t + .expect(cardView.option('filterValue')) + .eql(['title', '=', 'Mr.']); + + await t + .click(startButton.element) + .pressKey('shift+tab') + .pressKey('enter') + .expect(cardView.option('filterValue')) + .eql(null); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + filterValue: ['title', '=', 'Mr.'], + }); + + await createWidget('dxButton', { + text: 'Click Here First', + }, '#otherContainer'); +}); + +test('Focus returns to FilterIcon after FilterPopup is closed', async (t) => { + const cardView = new CardView('#container'); + const startButton = new Button('#otherContainer'); + const filterIcon = cardView.getFilterPanel().getIconFilter(); + + await t + .click(startButton.element) + .pressKey('shift+tab shift+tab') + .expect(filterIcon.element.focused) + .ok() + .pressKey('enter') + .expect(filterIcon.element.focused) + .notOk() + .pressKey('esc') + .expect(filterIcon.element.focused) + .ok(); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + }); + + await createWidget('dxButton', { + text: 'Click Here First', + }, '#otherContainer'); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/behavior.themes.ts b/e2e/testcafe-devextreme/tests/cardView/filterPanel/behavior.themes.ts new file mode 100644 index 000000000000..e92c62e92a14 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/filterPanel/behavior.themes.ts @@ -0,0 +1,30 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; +import { testScreenshot } from '../../../helpers/themeUtils'; + +fixture.disablePageReloads`CardView - FilterPanel Appearance` + .page(url(__dirname, '../../container.html')); + +test('FilterPanel and FilterBuilderPopup screenshots', async (t) => { + const cardView = new CardView('#container'); + const popup = cardView.getFilterPanel().getFilterBuilderPopup(); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await testScreenshot(t, takeScreenshot, 'cardView_FilterPanel.png', { element: cardView.getFilterPanel().element }); + + await t.click(cardView.getFilterPanel().getIconFilter().element); + + await testScreenshot(t, takeScreenshot, 'cardView_FilterBuilderPopup.png', { element: popup.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + filterValue: ['title', '=', 'Mr.'], + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (fluent-blue-light).png new file mode 100644 index 000000000000..eddaa5c43efb Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (generic-light).png new file mode 100644 index 000000000000..34ae29a9fa4e Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (material-blue-light).png new file mode 100644 index 000000000000..f97ced95bd9e Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (fluent-blue-light).png new file mode 100644 index 000000000000..6f5e86366ec8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (generic-light).png new file mode 100644 index 000000000000..b0343b4560a9 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (material-blue-light).png new file mode 100644 index 000000000000..e0b9976b3b6b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/helpers/baseConfig.ts b/e2e/testcafe-devextreme/tests/cardView/filterPanel/helpers/baseConfig.ts new file mode 100644 index 000000000000..8e9cb7ecf2a0 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/filterPanel/helpers/baseConfig.ts @@ -0,0 +1,22 @@ +import { data } from '../../helpers/simpleArrayData'; + +export const baseConfig = { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + filterPanel: { + visible: true, + }, +}; diff --git a/e2e/testcafe-devextreme/tests/cardView/headerFilter/a11y.functional.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/a11y.functional.ts new file mode 100644 index 000000000000..b1a60beae0f2 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/a11y.functional.ts @@ -0,0 +1,66 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture.disablePageReloads`HeaderFilter.A11y.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('should open popup by enter if filter icon in the focused state', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const firstHeaderItem = cardView + .getHeaderPanel() + .getHeaderItem(); + await t.click(firstHeaderItem.element) + .pressKey('alt+down'); + + // NOTE: We check list here, because this list rendered inside popup + const list = cardView.getHeaderFilterList(); + + await t.expect(list.element.exists).ok(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0' }, + { A: 'A_1' }, + { A: 'A_2' }, + ], + columns: [{ dataField: 'A', caption: 'LONG_COLUMN_A_CAPTION' }], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should return focus on the same icon after the popup closing', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const firstHeaderItem = cardView + .getHeaderPanel() + .getHeaderItem(); + await t.click(firstHeaderItem.element) + .pressKey('alt+down'); + + // NOTE: We check list here, because this list rendered inside popup + const list = cardView.getHeaderFilterList(); + await t.expect(list.element.exists).ok(); + + await t + .pressKey('tab') + .pressKey('tab') + .pressKey('enter'); + + await t.expect(firstHeaderItem.element.focused).ok(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0' }, + { A: 'A_1' }, + { A: 'A_2' }, + ], + columns: [{ dataField: 'A', caption: 'LONG_COLUMN_A_CAPTION' }], + headerFilter: { + visible: true, + }, + height: 600, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/headerFilter/common.functional.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/common.functional.ts new file mode 100644 index 000000000000..dbc24bfef29e --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/common.functional.ts @@ -0,0 +1,58 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import { createWidget } from '../../../helpers/createWidget'; +import url from '../../../helpers/getPageUrl'; + +fixture.disablePageReloads`HeaderFilter.Common.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('should support custom translations', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + const doneBtn = popup.getButton(0); + const closeBtn = popup.getButton(1); + const firstItem = list.getItem(0); + + await t.expect(doneBtn.text) + .eql('TEST_OK') + .expect(closeBtn.text) + .eql('TEST_CANCEL') + .expect(firstItem.text) + .eql('TEST_EMPTY'); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + dataField: 'A', + calculateCellValue: () => undefined, + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + texts: { + ok: 'TEST_OK', + cancel: 'TEST_CANCEL', + emptyValue: 'TEST_EMPTY', + }, + }, + height: 600, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/headerFilter/local.functional.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/local.functional.ts new file mode 100644 index 000000000000..086194d25da2 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/local.functional.ts @@ -0,0 +1,543 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +// TODO: Write integration test with filtering after filtering will be implemented +fixture.disablePageReloads`HeaderFilter.LocalDataSource.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('list should contain all column values', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(5); + + for (let idx = 0; idx < 5; idx += 1) { + await t.expect(list.getItem(idx).text).eql(`A_${idx}`); + } + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('list should contain all column values from all pages', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(5); + + for (let idx = 0; idx < 5; idx += 1) { + await t.expect(list.getItem(idx).text).eql(`A_${idx}`); + } + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + }, + paging: { + pageSize: 1, + pageIndex: 0, + }, + height: 600, +})); + +test('list should contain all values from computed column', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(5); + + for (let idx = 0; idx < 3; idx += 1) { + await t.expect(list.getItem(idx).text).eql(`A_${idx}_B_${idx}`); + } + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + caption: 'Computed', + calculateCellValue: (data) => `${data.A}_${data.B}`, + }, + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should support custom dataSource', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(3); + + for (let idx = 0; idx < 3; idx += 1) { + await t.expect(list.getItem(idx).text).eql(`CUSTOM_${idx}`); + } + + await t.click(cardView.element); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + dataField: 'A', + headerFilter: { + dataSource: [ + { text: 'CUSTOM_0', value: 0 }, + { text: 'CUSTOM_1', value: 1 }, + { text: 'CUSTOM_2', value: 2 }, + ], + }, + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + }, + height: 600, + }); +}); + +test('should update column options with filterType and values (regular selection)', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const okBtn = popup.getButton(0); + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + + await t.click(firstItem.element) + .click(secondItem.element) + .click(okBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.headerFilter.filterType).eql(undefined) + .expect(columnOptions.headerFilter.values).eql(['A_0', 'A_1']); + + await t.click(cardView.element); +}).before(async () => { + await createWidget('dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + }, + height: 600, + }); +}); + +test('should update column options with filterType and values (selectAll case #0)', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const okBtn = popup.getButton(0); + const selectAllCheckbox = list.selectAll.element; + + await t.click(selectAllCheckbox) + .click(okBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.headerFilter.filterType).eql('exclude') + .expect(columnOptions.headerFilter.values).eql(null); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should update column options with filterType and values (selectAll case #1)', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const okBtn = popup.getButton(0); + const selectAllCheckbox = list.selectAll.element; + const firstItem = list.getItem(2); + const secondItem = list.getItem(3); + + await t.click(selectAllCheckbox) + .click(firstItem.element) + .click(secondItem.element) + .click(okBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.headerFilter.filterType).eql('exclude') + .expect(columnOptions.headerFilter.values).eql(['A_2', 'A_3']); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should apply filter from options (type: "include" by default)', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + const thirdItem = list.getItem(2); + + await t + .expect(firstItem.checkBox.isChecked).ok() + .expect(secondItem.checkBox.isChecked).ok() + .expect(thirdItem.checkBox.isChecked) + .notOk(); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + dataField: 'A', + headerFilter: { + values: ['A_0', 'A_1'], + }, + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should apply filter from options (type: "include")', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + const thirdItem = list.getItem(2); + + await t + .expect(firstItem.checkBox.isChecked).ok() + .expect(secondItem.checkBox.isChecked).ok() + .expect(thirdItem.checkBox.isChecked) + .notOk(); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + dataField: 'A', + headerFilter: { + filterType: 'include', + values: ['A_0', 'A_1'], + }, + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should apply filter from options (type: "exclude")', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await t.expect(cardView.getHeaderFilterPopup().element.visible).notOk(); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + const thirdItem = list.getItem(2); + + await t + .expect(firstItem.checkBox.isChecked).ok() + .expect(secondItem.checkBox.isChecked).ok() + .expect(thirdItem.checkBox.isChecked) + .notOk(); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + dataField: 'A', + headerFilter: { + filterType: 'exclude', + values: ['A_2', 'A_3', 'A_4'], + }, + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should process groupInterval option', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const expectedTexts = [ + '0 - 5', + '5 - 10', + ]; + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(expectedTexts.length); + + for (let idx = 0; idx < expectedTexts.length; idx += 1) { + await t.expect(list.getItem(idx).text).eql(expectedTexts[idx]); + } + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { id: 0, A: 'A_0' }, + { id: 1, A: 'A_1' }, + { id: 2, A: 'A_2' }, + { id: 3, A: 'A_3' }, + { id: 4, A: 'A_4' }, + { id: 5, A: 'A_4' }, + { id: 6, A: 'A_4' }, + { id: 7, A: 'A_4' }, + { id: 8, A: 'A_4' }, + { id: 9, A: 'A_4' }, + ], + columns: [ + { + dataField: 'id', + dataType: 'number', + headerFilter: { + groupInterval: 5, + }, + }, + 'A', + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should not update column options if popup cancel btn clicked', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const cancelBtn = popup.getButton(1); + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + + await t + .click(firstItem.element) + .click(secondItem.element) + .click(cancelBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.headerFilter.filterType).eql(undefined) + .expect(columnOptions.headerFilter.values).eql(['A_4']); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + dataField: 'A', + headerFilter: { + values: ['A_4'], + }, + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + }, + height: 600, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/headerFilter/remote.functional.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/remote.functional.ts new file mode 100644 index 000000000000..8596b9f79604 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/remote.functional.ts @@ -0,0 +1,542 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import { ClientFunction } from 'testcafe'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { remoteApiIdGroupMock, remoteApiMock, remoteData } from '../helpers/remoteApiMock'; + +fixture`HeaderFilter.RemoteDataSource.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +const setRemoteOperations = (remoteOperations) => ClientFunction(() => { + (window as any).testRemoteOperations = remoteOperations; +}, { dependencies: { remoteOperations } })(); + +const clearRemoteOperations = () => ClientFunction(() => { + delete (window as any).testRemoteOperations; +})(); + +[ + { remoteOperations: 'auto' }, + { remoteOperations: true }, + { remoteOperations: false }, +].forEach(({ remoteOperations }) => { + test(`remote operations: ${remoteOperations} -> list should contain loaded items`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(remoteData.length); + + for (let idx = 0; idx < remoteData.length; idx += 1) { + await t.expect(list.getItem(idx).text).eql(remoteData[idx].A); + } + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: ['A', 'B', 'C'], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should support custom dataSource`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(3); + + for (let idx = 0; idx < 3; idx += 1) { + await t.expect(list.getItem(idx).text).eql(`CUSTOM_${idx}`); + } + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'A', + headerFilter: { + dataSource: [ + { text: 'CUSTOM_0', value: 0 }, + { text: 'CUSTOM_1', value: 1 }, + { text: 'CUSTOM_2', value: 2 }, + ], + }, + }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should update column options with filterType and values (regular selection)`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const okBtn = popup.getButton(0); + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + + await t.click(firstItem.element) + .click(secondItem.element) + .click(okBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.headerFilter.filterType).eql(undefined) + .expect(columnOptions.headerFilter.values).eql(['A_0', 'A_1']); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: ['A', 'B', 'C'], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should update column options with filterType and values (selectAll case #0)`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const okBtn = popup.getButton(0); + const selectAllCheckbox = list.selectAll.element; + + await t.click(selectAllCheckbox) + .click(okBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.headerFilter.filterType).eql('exclude') + .expect(columnOptions.headerFilter.values).eql(null); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: ['A', 'B', 'C'], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should update column options with filterType and values (selectAll case #1)`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const okBtn = popup.getButton(0); + const selectAllCheckbox = list.selectAll.element; + const firstItem = list.getItem(2); + const secondItem = list.getItem(3); + + await t.click(selectAllCheckbox) + .click(firstItem.element) + .click(secondItem.element) + .click(okBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.headerFilter.filterType).eql('exclude') + .expect(columnOptions.headerFilter.values).eql(['A_2', 'A_3']); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: ['A', 'B', 'C'], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should apply filter from options (type: "include" by default)`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + const thirdItem = list.getItem(2); + + await t + .expect(firstItem.checkBox.isChecked).ok() + .expect(secondItem.checkBox.isChecked).ok() + .expect(thirdItem.checkBox.isChecked) + .notOk(); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'A', + headerFilter: { + values: ['A_0', 'A_1'], + }, + }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should apply filter from options (type: "include")`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + const thirdItem = list.getItem(2); + + await t + .expect(firstItem.checkBox.isChecked).ok() + .expect(secondItem.checkBox.isChecked).ok() + .expect(thirdItem.checkBox.isChecked) + .notOk(); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'A', + headerFilter: { + filterType: 'include', + values: ['A_0', 'A_1'], + }, + }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should apply filter from options (type: "exclude")`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await t.expect(cardView.getHeaderFilterPopup().element.visible).notOk(); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + const thirdItem = list.getItem(2); + + await t + .expect(firstItem.checkBox.isChecked).ok() + .expect(secondItem.checkBox.isChecked).ok() + .expect(thirdItem.checkBox.isChecked) + .notOk(); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'A', + headerFilter: { + filterType: 'exclude', + values: ['A_2', 'A_3', 'A_4'], + }, + }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should process groupInterval option`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const expectedTexts = [ + '0 - 5', + '5 - 10', + ]; + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(expectedTexts.length); + + for (let idx = 0; idx < expectedTexts.length; idx += 1) { + await t.expect(list.getItem(idx).text).eql(expectedTexts[idx]); + } + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiIdGroupMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'id', + dataType: 'number', + headerFilter: { + groupInterval: 5, + }, + }, + 'A', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiIdGroupMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should not update column options if popup cancel btn clicked`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const cancelBtn = popup.getButton(1); + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + + await t + .click(firstItem.element) + .click(secondItem.element) + .click(cancelBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.headerFilter.filterType).eql(undefined) + .expect(columnOptions.headerFilter.values).eql(['A_4']); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'A', + headerFilter: { + values: ['A_4'], + }, + }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/headerFilter/visual.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/visual.ts new file mode 100644 index 000000000000..9cba523769ad --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/visual.ts @@ -0,0 +1,112 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { testScreenshot } from '../../../helpers/themeUtils'; + +// TODO: Unskip this fixture after markup will be stabilized +fixture.skip`HeaderFilter.Visual` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('popup with list', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + await testScreenshot(t, takeScreenshot, 'card-view_header-filter_popup-with-list.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('popup with search', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + await testScreenshot(t, takeScreenshot, 'card-view_header-filter_popup-with-search.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + search: { + enabled: true, + }, + }, + height: 600, +})); + +test('popup with tree', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + await testScreenshot(t, takeScreenshot, 'card-view_header-filter_popup-with-tree.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: '2024-01-01', B: 'B_0', C: 'C_0' }, + { A: '2024-01-01', B: 'B_1', C: 'C_1' }, + { A: '2024-01-01', B: 'B_2', C: 'C_2' }, + { A: '2025-01-01', B: 'B_3', C: 'C_3' }, + { A: '2025-01-01', B: 'B_4', C: 'C_4' }, + { A: '2026-01-01', B: 'B_5', C: 'C_5' }, + ], + columns: [ + { + dataField: 'A', + dataType: 'date', + // TODO calculateCellValue issue: Remove after task will be complete + calculateCellValue: ({ A }) => new Date(A), + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + }, + height: 600, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/headerPanel.ts b/e2e/testcafe-devextreme/tests/cardView/headerPanel.ts new file mode 100644 index 000000000000..993b0cca707d --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerPanel.ts @@ -0,0 +1,23 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../helpers/getPageUrl'; +import { createWidget } from '../../helpers/createWidget'; +import { testScreenshot } from '../../helpers/themeUtils'; + +fixture.disablePageReloads`CardView - HeaderPanel` + .page(url(__dirname, '../container.html')); + +test('default render', async (t) => { + const cardView = new CardView('#container'); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await testScreenshot(t, takeScreenshot, 'header-panel.png', { element: cardView.getHeaderPanel().element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + width: 400, + height: 600, + columns: ['Customer', 'Order Date'], +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/helpers/remoteApiMock.ts b/e2e/testcafe-devextreme/tests/cardView/helpers/remoteApiMock.ts new file mode 100644 index 000000000000..52d25ae55513 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/helpers/remoteApiMock.ts @@ -0,0 +1,53 @@ +import { RequestMock } from 'testcafe'; + +export const remoteData = new Array(10) + .fill(null) + .map((_, idx) => ({ + id: idx, A: `A_${idx}`, B: `B_${idx}`, C: `C_${idx}`, + })); + +export const remoteDataGroupedByA = new Array(10) + .fill(null) + .map((_, idx) => ({ + key: `A_${idx}`, + items: null, + })); + +export const remoteApiMock = RequestMock() + .onRequestTo(/\/api\/data\?.*group=/) + .respond( + { + data: remoteDataGroupedByA, + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data/) + .respond( + { + data: remoteData, + }, + 200, + { 'access-control-allow-origin': '*' }, + ); + +export const remoteApiIdGroupMock = RequestMock() + .onRequestTo(/\/api\/data\?.*group=/) + .respond( + { + data: [ + { key: 0, items: null }, + { key: 5, items: null }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data/) + .respond( + { + data: remoteData, + }, + 200, + { 'access-control-allow-origin': '*' }, + ); diff --git a/e2e/testcafe-devextreme/tests/cardView/helpers/simpleArrayData.ts b/e2e/testcafe-devextreme/tests/cardView/helpers/simpleArrayData.ts new file mode 100644 index 000000000000..fc0fb6ff140f --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/helpers/simpleArrayData.ts @@ -0,0 +1,26 @@ +export const data = [ + { + id: 1, + title: 'Mr.', + name: 'John', + lastName: 'Heart', + }, + { + id: 2, + title: 'Mrs.', + name: 'Olivia', + lastName: 'Peyton', + }, + { + id: 3, + title: 'Mr.', + name: 'Robert', + lastName: 'Reagan', + }, + { + id: 4, + title: 'Mr.', + name: 'Greta', + lastName: 'Sims', + }, +]; diff --git a/e2e/testcafe-devextreme/tests/cardView/pager.ts b/e2e/testcafe-devextreme/tests/cardView/pager.ts new file mode 100644 index 000000000000..cd539b915b87 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/pager.ts @@ -0,0 +1,50 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../helpers/getPageUrl'; +import { createWidget } from '../../helpers/createWidget'; + +async function createCardViewWithPager(): Promise { + const dataSource = Array.from({ length: 20 }, (_, i) => ({ text: i.toString(), value: i })); + return createWidget('dxCardView', { + dataSource, + paging: { + pageSize: 2, + pageIndex: 5, + }, + pager: { + showPageSizeSelector: true, + allowedPageSizes: [2, 3, 4], + showInfo: true, + showNavigationButtons: true, + }, + }); +} +fixture.disablePageReloads`Pager` + .page(url(__dirname, '../container.html')); + +test('Page index interaction', async (t) => { + const cardView = new CardView('#container'); + const pager = cardView.getPager(); + await t + .expect(pager.getPageSize(0).selected) + .ok('page size 2 selected') + .expect(pager.getNavPage('6').selected) + .ok('page 6 selected') + .expect(pager.getInfoText().textContent) + .eql('Page 6 of 10 (20 items)') + .expect(cardView.getCard(1).getFieldValueCell('Text').innerText) + .eql('11'); + + // set page index 7 + await t + .click(pager.getNavPage('7').element) + .expect(cardView.getCard(1).getFieldValueCell('Text').innerText) + .eql('13') + .expect(pager.getInfoText().textContent) + .eql('Page 7 of 10 (20 items)'); + + // navigate to prev page (6) + await t + .click(pager.getPrevNavButton().element) + .expect(pager.getInfoText().textContent) + .eql('Page 6 of 10 (20 items)'); +}).before(async () => createCardViewWithPager()); diff --git a/e2e/testcafe-devextreme/tests/cardView/search/a11y.functional.ts b/e2e/testcafe-devextreme/tests/cardView/search/a11y.functional.ts new file mode 100644 index 000000000000..bbf433e67bcd --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/search/a11y.functional.ts @@ -0,0 +1,18 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - Search.A11y.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('Search field should have aria-label attribute', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const searchBox = cardView.getSearchBox(); + + await t + .expect(searchBox.getInput().getAttribute('aria-label')) + .eql('Search in the card view'); +}).before(async () => createWidget('dxCardView', baseConfig)); diff --git a/e2e/testcafe-devextreme/tests/cardView/search/api.functional.ts b/e2e/testcafe-devextreme/tests/cardView/search/api.functional.ts new file mode 100644 index 000000000000..702b766621b5 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/search/api.functional.ts @@ -0,0 +1,208 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - SearchPanel API` + .page(url(__dirname, '../../container.html')); + +test('searchPanel.visible API', async (t) => { + const cardView = new CardView('#container'); + const searchBox = cardView.getSearchBox(); + + await t + .expect(searchBox.element.exists) + .ok(); + + await cardView.apiOption('searchPanel.visible', false); + + await t + .expect(searchBox.element.exists) + .notOk(); + + await cardView.apiOption('searchPanel.visible', true); + + await t + .expect(searchBox.element.exists) + .ok(); +}).before(async () => { + await createWidget('dxCardView', baseConfig); +}); + +test('searchPanel.width API', async (t) => { + const cardView = new CardView('#container'); + const searchBox = cardView.getSearchBox(); + + await t + .expect(searchBox.element.getBoundingClientRectProperty('width')) + .eql(300); + + await cardView.apiOption('searchPanel.width', 200); + + await t + .expect(searchBox.element.getBoundingClientRectProperty('width')) + .eql(200); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + searchPanel: { + ...baseConfig.searchPanel, + width: 300, + }, + }); +}); + +test('searchPanel.placeholder API', async (t) => { + const cardView = new CardView('#container'); + const searchBox = cardView.getSearchBox(); + + await t + .expect(searchBox.getInput().getAttribute('placeholder')) + .eql('Test placeholder'); + + await cardView.apiOption('searchPanel.placeholder', 'Test placeholder 2'); + + await t + .expect(searchBox.getInput().getAttribute('placeholder')) + .eql('Test placeholder 2'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + searchPanel: { + ...baseConfig.searchPanel, + placeholder: 'Test placeholder', + }, + }); +}); + +// TODO: unskip when update text property state issue is resolved +test.skip('searchPanel.text API', async (t) => { + const cardView = new CardView('#container'); + const searchBox = cardView.getSearchBox(); + + await t + .expect(searchBox.getInput().getAttribute('value')) + .eql('rt'); + + await cardView.apiOption('searchPanel.text', ''); + + await t + .expect(searchBox.getInput().getAttribute('value')) + .eql(''); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + searchPanel: { + ...baseConfig.searchPanel, + text: 'rt', + }, + }); +}); + +test('searchPanel.searchVisibleColumnsOnly API', async (t) => { + const cardView = new CardView('#container'); + const searchInput = cardView.getSearchBox().getInput(); + + await t + .expect(cardView.getCards().count) + .eql(4) + .typeText(searchInput, '2') + .expect(cardView.getCards().count) + .eql(1); + + await cardView.apiOption('searchPanel.searchVisibleColumnsOnly', true); + + await t + .expect(cardView.getCards().count) + .eql(0); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + columns: [ + { + dataField: 'id', + visible: false, + }, + { + dataField: 'title', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }); +}); + +test('searchPanel.highlightSearchText API', async (t) => { + const cardView = new CardView('#container'); + const searchInput = cardView.getSearchBox().getInput(); + + await t + .expect(cardView.getCards().count) + .eql(4) + .typeText(searchInput, 'rt') + .expect(cardView.getCards().count) + .eql(2) + .expect(cardView.getCard(0).getHighlightedTexts().count) + .eql(1) + .expect(cardView.getCard(0).getHighlightedTexts().nth(0).innerText) + .eql('rt') + .expect(cardView.getCard(0).getFieldValueCell('Last Name').innerText) + .eql('Heart'); + + await cardView.apiOption('searchPanel.highlightSearchText', false); + + await t + .expect(cardView.getCards().count) + .eql(2) + .expect(cardView.getCard(0).getHighlightedTexts().count) + .eql(0); +}).before(async () => { + await createWidget('dxCardView', baseConfig); +}); + +test('searchPanel.highlightCaseSensitive API', async (t) => { + const cardView = new CardView('#container'); + const searchInput = cardView.getSearchBox().getInput(); + + await t + .expect(cardView.getCards().count) + .eql(4) + .typeText(searchInput, 'rt') + .expect(cardView.getCards().count) + .eql(2) + .expect(cardView.getCard(0).getHighlightedTexts().count) + .eql(1) + .expect(cardView.getCard(0).getHighlightedTexts().nth(0).innerText) + .eql('rt') + .expect(cardView.getCard(0).getFieldValueCell('Last Name').innerText) + .eql('Heart'); + + await t + .typeText(searchInput, 'RT', { replace: true }) + .expect(cardView.getCards().count) + .eql(2) + .expect(cardView.getCard(0).getHighlightedTexts().count) + .eql(0); + + await cardView.apiOption('searchPanel.highlightCaseSensitive', false); + + await t + .expect(cardView.getCard(0).getHighlightedTexts().count) + .eql(1) + .expect(cardView.getCard(0).getHighlightedTexts().nth(0).innerText) + .eql('rt') + .expect(cardView.getCard(0).getFieldValueCell('Last Name').innerText) + .eql('Heart'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + searchPanel: { + ...baseConfig.searchPanel, + highlightCaseSensitive: true, + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/search/behavior.functional.ts b/e2e/testcafe-devextreme/tests/cardView/search/behavior.functional.ts new file mode 100644 index 000000000000..97588dd65b5d --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/search/behavior.functional.ts @@ -0,0 +1,61 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - SearchPanel API` + .page(url(__dirname, '../../container.html')); + +test('Search panel should filter cards', async (t) => { + const cardView = new CardView('#container'); + const searchInput = cardView.getSearchBox().getInput(); + + await t + .expect(cardView.getCards().count) + .eql(4) + .typeText(searchInput, 'rt') + .expect(cardView.getCards().count) + .eql(2) + .pressKey('ctrl+a backspace') + .expect(cardView.getCards().count) + .eql(4); +}).before(async () => { + await createWidget('dxCardView', baseConfig); +}); + +test('Search panel should should take into account calculateFilterExpression', async (t) => { + const cardView = new CardView('#container'); + const searchInput = cardView.getSearchBox().getInput(); + + await t + .expect(cardView.getCards().count) + .eql(4) + .typeText(searchInput, '1') + .expect(cardView.getCards().count) + .eql(2) + .expect(cardView.getCard(0).getFieldValueCell('Last Name').innerText) + .eql('Reagan') + .expect(cardView.getCard(1).getFieldValueCell('Last Name').innerText) + .eql('Sims'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + columns: [ + { + dataField: 'id', + calculateFilterExpression() { + return [this.dataField, '>', '2']; + }, + }, + { + dataField: 'title', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (fluent-blue-light).png new file mode 100644 index 000000000000..cea0fec88916 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (generic-light).png new file mode 100644 index 000000000000..bd79b3b6be20 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (material-blue-light).png new file mode 100644 index 000000000000..a4072e937113 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/search/helpers/baseConfig.ts b/e2e/testcafe-devextreme/tests/cardView/search/helpers/baseConfig.ts new file mode 100644 index 000000000000..b811ad1f6244 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/search/helpers/baseConfig.ts @@ -0,0 +1,22 @@ +import { data } from '../../helpers/simpleArrayData'; + +export const baseConfig = { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + searchPanel: { + visible: true, + }, +}; diff --git a/e2e/testcafe-devextreme/tests/cardView/search/visual.ts b/e2e/testcafe-devextreme/tests/cardView/search/visual.ts new file mode 100644 index 000000000000..1e62e873f6c8 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/search/visual.ts @@ -0,0 +1,64 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { testScreenshot } from '../../../helpers/themeUtils'; + +fixture`Search.Visual` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +const COLUMNS = ['id', 'firstName', 'lastName', 'email', 'gender']; + +const DATA = [{ + id: 1, + firstName: 'Darin', + lastName: 'Heritege', + email: 'dheritege0@jugem.jp', + gender: 'Male', +}, { + id: 2, + firstName: 'Aeriel', + lastName: 'Giggs', + email: 'agiggs1@hubpages.com', + gender: 'Female', +}, { + id: 3, + firstName: 'Theo', + lastName: 'Aleksidze', + email: 'taleksidze2@patch.com', + gender: 'Female', +}, { + id: 4, + firstName: 'Dalli', + lastName: 'Ashwood', + email: 'dashwood3@buzzfeed.com', + gender: 'Male', +}, { + id: 5, + firstName: 'Paule', + lastName: 'Pidgeley', + email: 'ppidgeley4@upenn.edu', + gender: 'Female', +}]; + +test('highlighted search text', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await testScreenshot(t, takeScreenshot, 'card-view_search_text-highlighting.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: DATA, + columns: COLUMNS, + searchPanel: { + visible: true, + text: 'rt', + }, + height: 600, +})); 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..c2de48e2ad11 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..3e872b3dcb93 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..4766698815b2 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..685334c8c0b6 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..51e8ad995556 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..3ea7d60187ce 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..2c6289137fc3 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..16494157406c 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..e36cde62b6b3 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..5b6b9063e8d0 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..c8b3e3209241 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..41ffba342b2a 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..5b6b9063e8d0 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..c8b3e3209241 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..41ffba342b2a 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..c3048df9ff19 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..45d51d3a3dc5 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..bd15fb5d723e 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..5b6b9063e8d0 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..c8b3e3209241 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..41ffba342b2a 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..5b6b9063e8d0 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..c8b3e3209241 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..41ffba342b2a 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..2c6289137fc3 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..16494157406c 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..e36cde62b6b3 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..5b6b9063e8d0 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..c8b3e3209241 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..41ffba342b2a 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..77393f962003 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..f3b99a7aa82e 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..9f4bb4f4b03e 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..25bfa3bf416f 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..7a95651075cb 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..d28f9097bd8c 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/e2e/testcafe-devextreme/tests/cardView/sorting/api.themes.ts b/e2e/testcafe-devextreme/tests/cardView/sorting/api.themes.ts new file mode 100644 index 000000000000..b701101632b7 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/sorting/api.themes.ts @@ -0,0 +1,192 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { data } from '../helpers/simpleArrayData'; +import { testScreenshot } from '../../../helpers/themeUtils'; + +fixture.disablePageReloads`CardView - Sorting Behavior` + .page(url(__dirname, '../../container.html')); + +test('Sort index API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, 'cardview_sort_index_api.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + sortIndex: 1, + }, + { + dataField: 'name', + sortOrder: 'asc', + sortIndex: 0, + }, + { + dataField: 'lastName', + }, + ], + }); +}); + +test('ShowSortIndexes API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, 'cardview_show_sort_indexes_api.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + showSortIndexes: false, + }, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + sortIndex: 1, + }, + { + dataField: 'name', + sortOrder: 'asc', + sortIndex: 0, + }, + { + dataField: 'lastName', + }, + ], + }); +}); + +test('AllowSorting API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await t + .click(cardView.getHeaders().getHeaderItemByText('Title').element); + + await testScreenshot(t, takeScreenshot, 'cardview_allow_sorting_api.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + showSortIndexes: false, + }, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + sortIndex: 1, + allowSorting: false, + }, + { + dataField: 'name', + sortOrder: 'asc', + sortIndex: 0, + }, + { + dataField: 'lastName', + }, + ], + }); +}); + +[ + function (rowData) { + return rowData.id % 3; + }, + 'name', +].forEach((calculateSortValue) => { + test('CalculateSortValue API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, `cardview_calculate_sort_value_is_${calculateSortValue === 'name' ? 'filed' : 'function'}_api.png`, { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + showSortIndexes: false, + }, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'asc', + calculateSortValue, + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }); + }); +}); + +test('SortingMethod API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, 'cardview_sorting_method_api.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + showSortIndexes: false, + }, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'asc', + sortingMethod(value1, value2) { + if (value1 === 'Mr.' && value2 !== 'Mr.') return 1; + if (value1 !== 'Mr.' && value2 === 'Mr.') return -1; + return value1.localeCompare(value2); + }, + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/bahavior.themes.ts b/e2e/testcafe-devextreme/tests/cardView/sorting/bahavior.themes.ts new file mode 100644 index 000000000000..1f2b1f28cfbc --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/sorting/bahavior.themes.ts @@ -0,0 +1,67 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { data } from '../helpers/simpleArrayData'; +import { testScreenshot } from '../../../helpers/themeUtils'; + +fixture.disablePageReloads`CardView - Sorting Behavior - Themes` + .page(url(__dirname, '../../container.html')); + +test('Default render', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, 'cardview_headers_default_render.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }); +}); +test('Default multiple sorting render', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + await testScreenshot(t, takeScreenshot, 'cardview_headers_with_multiple_sorting_render.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + }, + { + dataField: 'name', + sortOrder: 'asc', + }, + { + dataField: 'lastName', + }, + ], + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/behavior.functional.ts b/e2e/testcafe-devextreme/tests/cardView/sorting/behavior.functional.ts new file mode 100644 index 000000000000..a3c09a607fb2 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/sorting/behavior.functional.ts @@ -0,0 +1,253 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { data } from '../helpers/simpleArrayData'; + +fixture.disablePageReloads`CardView - Sorting Behavior - Functional` + .page(url(__dirname, '../../container.html')); + +([ + ['none', false, false, false, [undefined, undefined]], + ['none', true, false, false, [undefined, undefined]], + ['none', false, true, false, [undefined, undefined]], + ['none', false, false, true, [undefined, undefined]], + + ['single', false, false, false, ['desc', undefined]], + ['single', true, false, false, ['desc', undefined]], + ['single', false, true, false, [undefined, undefined]], + ['single', false, false, true, [undefined, undefined]], + + ['multiple', false, false, false, ['desc', 0]], + ['multiple', true, false, false, ['desc', 0]], + ['multiple', false, true, false, [undefined, undefined]], + ['multiple', false, false, true, [undefined, undefined]], +] as [ + string, + boolean, + boolean, + boolean, + [ + string | undefined, + number | undefined, + ], +][] +).forEach(([ + mode, + shift, + ctrl, + meta, + [ + titleSortOrder, + titleSortIndex, + ]]) => { + test(`Change sorting of sorted item in ${mode} mode with shift=${shift}, ctrl=${ctrl}, meta=${meta}`, async (t) => { + const cardView = new CardView('#container'); + const titleHeaderItem = cardView.getHeaders().getHeaderItemByText('Title'); + + await t + .click(titleHeaderItem.element); + + await t + .click(titleHeaderItem.element, { + modifiers: { + shift, + ctrl, + meta, + }, + }) + .expect(cardView.apiColumnOption('title', 'sortOrder')) + .eql(titleSortOrder) + .expect(cardView.apiColumnOption('title', 'sortIndex')) + .eql(titleSortIndex); + }).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + mode, + }, + columns: [ + { + dataField: 'title', + }, + { + dataField: 'name', + }, + ], + }); + }); +}); + +([ + ['none', false, false, false, [undefined, undefined], [undefined, undefined]], + ['none', true, false, false, [undefined, undefined], [undefined, undefined]], + ['none', false, true, false, [undefined, undefined], [undefined, undefined]], + ['none', false, false, true, [undefined, undefined], [undefined, undefined]], + + ['single', false, false, false, [undefined, undefined], ['asc', undefined]], + ['single', true, false, false, [undefined, undefined], ['asc', undefined]], + ['single', false, true, false, ['asc', undefined], [undefined, undefined]], + ['single', false, false, true, ['asc', undefined], [undefined, undefined]], + + ['multiple', false, false, false, [undefined, undefined], ['asc', 0]], + ['multiple', true, false, false, ['asc', 0], ['asc', 1]], + ['multiple', false, true, false, ['asc', 0], [undefined, undefined]], + ['multiple', false, false, true, ['asc', 0], [undefined, undefined]], +] as [ + string, + boolean, + boolean, + boolean, + [ + string | undefined, + number | undefined, + ], + [ + string | undefined, + number | undefined, + ], +][] +).forEach(([ + mode, + shift, + ctrl, + meta, + [ + titleSortOrder, + titleSortIndex, + ], + [ + nameSortOrder, + nameSortIndex, + ], +]) => { + test(`Change sorting of neighbour non sorted item in ${mode} mode with shift=${shift}, ctrl=${ctrl}, meta=${meta}`, async (t) => { + const cardView = new CardView('#container'); + const titleHeaderItem = cardView.getHeaders().getHeaderItemByText('Title'); + const nameHeaderItem = cardView.getHeaders().getHeaderItemByText('Name'); + + await t + .click(titleHeaderItem.element); + + await t + .click(nameHeaderItem.element, { + modifiers: { + shift, + ctrl, + meta, + }, + }) + .expect(cardView.apiColumnOption('title', 'sortOrder')) + .eql(titleSortOrder) + .expect(cardView.apiColumnOption('title', 'sortIndex')) + .eql(titleSortIndex) + .expect(cardView.apiColumnOption('name', 'sortOrder')) + .eql(nameSortOrder) + .expect(cardView.apiColumnOption('name', 'sortIndex')) + .eql(nameSortIndex); + }).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + mode, + }, + columns: [ + { + dataField: 'title', + }, + { + dataField: 'name', + }, + ], + }); + }); +}); + +([ + ['none', false, false, false, [undefined, undefined], [undefined, undefined]], + ['none', true, false, false, [undefined, undefined], [undefined, undefined]], + ['none', false, true, false, [undefined, undefined], [undefined, undefined]], + ['none', false, false, true, [undefined, undefined], [undefined, undefined]], + + ['single', false, false, false, [undefined, undefined], ['desc', undefined]], + ['single', true, false, false, [undefined, undefined], ['desc', undefined]], + ['single', false, true, false, [undefined, undefined], [undefined, undefined]], + ['single', false, false, true, [undefined, undefined], [undefined, undefined]], + + ['multiple', false, false, false, [undefined, undefined], ['desc', 0]], + ['multiple', true, false, false, ['asc', 0], ['desc', 1]], + ['multiple', false, true, false, ['asc', 0], [undefined, undefined]], + ['multiple', false, false, true, ['asc', 0], [undefined, undefined]], +] as [ + string, + boolean, + boolean, + boolean, + [ + string | undefined, + number | undefined, + ], + [ + string | undefined, + number | undefined, + ], +][] +).forEach(([ + mode, + shift, + ctrl, + meta, + [ + titleSortOrder, + titleSortIndex, + ], + [ + nameSortOrder, + nameSortIndex, + ], +]) => { + test(`Change sorting of neighbour sorted item in ${mode} mode with shift=${shift}, ctrl=${ctrl}, meta=${meta}`, async (t) => { + const cardView = new CardView('#container'); + const titleHeaderItem = cardView.getHeaders().getHeaderItemByText('Title'); + const nameHeaderItem = cardView.getHeaders().getHeaderItemByText('Name'); + + await t + .click(titleHeaderItem.element) + .click(nameHeaderItem.element, { + modifiers: { + shift: true, + }, + }); + + await t + .click(nameHeaderItem.element, { + modifiers: { + shift, + ctrl, + meta, + }, + }) + .expect(cardView.apiColumnOption('title', 'sortOrder')) + .eql(titleSortOrder) + .expect(cardView.apiColumnOption('title', 'sortIndex')) + .eql(titleSortIndex) + .expect(cardView.apiColumnOption('name', 'sortOrder')) + .eql(nameSortOrder) + .expect(cardView.apiColumnOption('name', 'sortIndex')) + .eql(nameSortIndex); + }).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + mode, + }, + columns: [ + { + dataField: 'title', + }, + { + dataField: 'name', + }, + ], + }); + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (fluent-blue-light).png new file mode 100644 index 000000000000..7ee7a0b138c8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (generic-light).png new file mode 100644 index 000000000000..048ee4e72e6e Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (material-blue-light).png new file mode 100644 index 000000000000..00c816b8cfb8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (fluent-blue-light).png new file mode 100644 index 000000000000..44cd9d0dcf89 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (generic-light).png new file mode 100644 index 000000000000..688afa32fa9a Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (material-blue-light).png new file mode 100644 index 000000000000..52eb41ebd1cf Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (fluent-blue-light).png new file mode 100644 index 000000000000..936702e0ea7a Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (generic-light).png new file mode 100644 index 000000000000..41dce37db3cf Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (material-blue-light).png new file mode 100644 index 000000000000..9be9a395ff7b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (fluent-blue-light).png new file mode 100644 index 000000000000..49d6622196b5 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (generic-light).png new file mode 100644 index 000000000000..6475da837712 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (material-blue-light).png new file mode 100644 index 000000000000..89ca18375040 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (fluent-blue-light).png new file mode 100644 index 000000000000..78700b6d187b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (generic-light).png new file mode 100644 index 000000000000..1d48347c649c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (material-blue-light).png new file mode 100644 index 000000000000..6ba22f1981a8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (fluent-blue-light).png new file mode 100644 index 000000000000..456966335b06 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (generic-light).png new file mode 100644 index 000000000000..38b8b3fb8714 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (material-blue-light).png new file mode 100644 index 000000000000..64e6d8f73c3d Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (fluent-blue-light).png new file mode 100644 index 000000000000..5fef3fd6c008 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (generic-light).png new file mode 100644 index 000000000000..530b9615a833 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (material-blue-light).png new file mode 100644 index 000000000000..4086ee38d293 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (fluent-blue-light).png new file mode 100644 index 000000000000..ddb8d2508f26 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (generic-light).png new file mode 100644 index 000000000000..bf66fbd0b7a1 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (material-blue-light).png new file mode 100644 index 000000000000..121a15913772 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (material-blue-light).png differ diff --git a/packages/devextreme-angular/src/server/render.ts b/packages/devextreme-angular/src/server/render.ts index 04773ba08834..3027dd54b1af 100644 --- a/packages/devextreme-angular/src/server/render.ts +++ b/packages/devextreme-angular/src/server/render.ts @@ -37,6 +37,12 @@ export class DxServerModule { container.innerHTML = childString; }, + renderIntoContainer: ( + jsx, + container, + ) => { + container.innerHTML = renderToString(jsx); + }, }); } } diff --git a/packages/devextreme-angular/tests/src/server/component-names.ts b/packages/devextreme-angular/tests/src/server/component-names.ts index d5914606f2ba..baed4ebe863c 100644 --- a/packages/devextreme-angular/tests/src/server/component-names.ts +++ b/packages/devextreme-angular/tests/src/server/component-names.ts @@ -7,6 +7,7 @@ export const componentNames = [ 'bullet', 'button', 'calendar', + 'card-view', 'chart', 'chat', 'check-box', diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/_index.scss new file mode 100644 index 000000000000..cd81dfb11b1d --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/_index.scss @@ -0,0 +1,18 @@ +@use './content_view'; +@use './header_panel'; +@use './variables' as *; + +// adduse + +.dx-cardview { + display: flex; + flex-direction: column; + gap: $cardview-gap; + padding: $cardview-padding; + background-color: $cardview-background-color; + border-radius: $cardview-border-radius; +} + +.dx-cardview-exclude-flexbox { + position: absolute; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/_variables.scss new file mode 100644 index 000000000000..893760ad1f6e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/_variables.scss @@ -0,0 +1,4 @@ +$cardview-gap: null !default; +$cardview-padding: null !default; +$cardview-background-color: null !default; +$cardview-border-radius: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/_index.scss new file mode 100644 index 000000000000..a7575792f397 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/_index.scss @@ -0,0 +1,7 @@ +@use '../variables' as *; +@use './content'; + +.dx-cardview-contentview { + overflow: hidden; + flex-grow: 1; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_index.scss new file mode 100644 index 000000000000..b220687c03b8 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_index.scss @@ -0,0 +1,10 @@ +@use './variables' as *; +@use './card'; + +.dx-cardview-content { + display: grid; + justify-items: center; + column-gap: $cardview-content-column-gap; + row-gap: $cardview-content-row-gap; + grid-template-columns: repeat(var(--dx-cardview-cardsperrow), 1fr); +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_variables.scss new file mode 100644 index 000000000000..19754837130c --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_variables.scss @@ -0,0 +1,2 @@ +$cardview-content-column-gap: null !default; +$cardview-content-row-gap: null !default; 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 new file mode 100644 index 000000000000..cdd89b6143a7 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_index.scss @@ -0,0 +1,18 @@ +@use './header'; +@use './content'; +@use './cover'; +@use './variables' as *; + +.dx-cardview-card { + width: 100%; + min-width: var(--dx-cardview-card-min-width, $cardview-card-min-width); + max-width: var(--dx-cardview-card-max-width); + 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 new file mode 100644 index 000000000000..b4528656fde3 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_variables.scss @@ -0,0 +1,7 @@ +$cardview-card-border-color: null !default; + +$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/content/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_index.scss new file mode 100644 index 000000000000..7fae4600b2f3 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_index.scss @@ -0,0 +1,55 @@ +@use './variables' as *; +@use '../variables' as *; + +.dx-cardview-card-content { + padding: ($cardview-card-content-padding-vertical - $cardview-card-content-field-gap) $cardview-card-content-padding-horizontal; + display: table; + border-spacing: $cardview-card-content-field-gap; + width: 100%; +} + +.dx-cardview-field { + display: table-row; +} + +.dx-cardview-field-name, +.dx-cardview-field-value { + display: table-cell; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: $cardview-card-content-cell-padding-vertical $cardview-card-content-cell-padding-horizontal; +} + +.dx-cardview-field-name { + font-weight: 600; +} + +.dx-cardview-field-value { + &--text-align-right { + text-align: right; + } + + &--text-align-left { + text-align: left; + } + + &--text-align-center { + text-align: center; + } + + &--white-space-normal { + white-space: normal; + } + + &--white-space-nowrap { + white-space: nowrap; + } + + &__text-part { + &--highlighted { + color: $cardview-card-content-field-value-highlight-color; + background: $cardview-card-content-field-value-highlight-background; + } + } +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_variables.scss new file mode 100644 index 000000000000..5d44ab4b2455 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_variables.scss @@ -0,0 +1,8 @@ +$cardview-card-content-padding-vertical: null !default; +$cardview-card-content-padding-horizontal: null !default; +$cardview-card-content-field-gap: null !default; +$cardview-card-content-cell-padding-vertical: null !default; +$cardview-card-content-cell-padding-horizontal: null !default; + +$cardview-card-content-field-value-highlight-color: null !default; +$cardview-card-content-field-value-highlight-background: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_index.scss new file mode 100644 index 000000000000..75fdb043e97b --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_index.scss @@ -0,0 +1,28 @@ +@use './variables' as *; +@use '../variables' as *; + +.dx-card-cover { + overflow: hidden; + display: flex; + justify-content: center; + border-top: $cardview-card-border-size solid $cardview-card-border-color; + border-bottom: $cardview-card-border-size solid $cardview-card-border-color; + max-height: var(--dx-cardview-card-cover-max-height); + aspect-ratio: var(--dx-cardview-card-cover-ratio); + + &-noimage { + background-color: $cardview-card-cover-noimage-background-color; + align-items: center; + + .dx-icon-imagethumbnail { + font-size: $cardview-card-cover-noimage-icon-size; + color: $cardview-card-cover-noimage-icon-color; + } + } +} + +.dx-card-cover-image { + object-fit: contain; + width: 100%; +} + diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_variables.scss new file mode 100644 index 000000000000..b3982b5e08e3 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_variables.scss @@ -0,0 +1,3 @@ +$cardview-card-cover-noimage-background-color: null !default; +$cardview-card-cover-noimage-icon-color: null !default; +$cardview-card-cover-noimage-icon-size: 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 new file mode 100644 index 000000000000..0a0f670986cc --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_index.scss @@ -0,0 +1,37 @@ +@use './variables' as *; + +.dx-cardview-card-header { + .dx-toolbar { + padding: 0 12px; + border-radius: $cardview-card-header-border-radius; + + + .dx-toolbar-label { + font-size: $cardview-card-header-text-size; + } + } +} + +.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/base/cardView/content_view/content/card/header/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_variables.scss new file mode 100644 index 000000000000..814471d72bd7 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_variables.scss @@ -0,0 +1,2 @@ +$cardview-card-header-text-size: null !default; +$cardview-card-header-border-radius: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_index.scss new file mode 100644 index 000000000000..7842a1b5e2e2 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_index.scss @@ -0,0 +1,12 @@ + +@use './item'; +@use './variables' as *; + +.dx-cardview-headerpanel-content { + display: flex; + gap: $cardview-headerpanel-content-gap; +} + +.dx-cardview-header-item-sorting { + display: flex; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_variables.scss new file mode 100644 index 000000000000..6ab96b987436 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_variables.scss @@ -0,0 +1 @@ +$cardview-headerpanel-content-gap: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_index.scss new file mode 100644 index 000000000000..8d4bc8f3b498 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_index.scss @@ -0,0 +1,33 @@ +@use '../../../icons' as *; +@use './variables' as *; + +.dx-cardview-header-item { + display: flex; + align-items: center; + padding: ($cardview-header-item-padding-vertical - $cardview-header-item-border-width) $cardview-header-item-padding-horizontal; + min-width: fit-content; + user-select: none; + gap: $cardview-header-item-content-gap; + border: solid $cardview-header-item-border-width $cardview-header-item-border-color; + border-radius: $cardview-header-item-border-radius; + background-color: $cardview-header-item-background-color; + cursor: pointer; + + &:hover { + background-color: $cardview-header-item-hovered-background-color; + border: solid $cardview-header-item-border-width $cardview-header-item-hovered-border-color; + } +} + +.dx-cardview { + .dx-header-filter-icon { + @include dx-icon(filter); + + color: $cardview-header-filter-icon-empty-color; + font-size: $cardview-header-filter-icon-size; + + &--selected { + color: $cardview-header-filter-icon-selected-color; + } + } +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_variables.scss new file mode 100644 index 000000000000..9e62897e1fa4 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_variables.scss @@ -0,0 +1,15 @@ +$cardview-header-item-background-color: null !default; +$cardview-header-item-border-color: null !default; + +$cardview-header-item-hovered-background-color: null !default; +$cardview-header-item-hovered-border-color: null !default; + +$cardview-header-item-border-width: null !default; +$cardview-header-item-border-radius: null !default; +$cardview-header-item-padding-horizontal: 8px !default; +$cardview-header-item-padding-vertical: 6px !default; + +$cardview-header-item-content-gap: 4px !default; +$cardview-header-filter-icon-size: null !default; +$cardview-header-filter-icon-empty-color: null !default; +$cardview-header-filter-icon-selected-color: null !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/_index.scss index e9d6526025ba..ff95df79b8ec 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/_index.scss @@ -61,3 +61,4 @@ @use "./sortable"; @use "./deferRendering"; @use "./map"; +@use "./cardView"; diff --git a/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss new file mode 100644 index 000000000000..5682e96cf31e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss @@ -0,0 +1,31 @@ +@use '../colors' as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/cover/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; + +// adduse +$cardview-background-color: $base-typography-bg !default; + +$cardview-header-item-background-color: #F0F0F0 !default; +$cardview-header-item-border-color: #E0E0E0 !default; +$cardview-header-item-hovered-background-color: #EBEBEB !default; +$cardview-header-item-hovered-border-color: #BDBDBD !default; + +$cardview-card-border-color: $base-border-color !default; +$cardview-card-background-color: $base-bg !default; + +$cardview-header-filter-icon-empty-color: $base-text-color !default; +$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-cover-noimage-background-color: #E0E0E0 !default; +$cardview-card-cover-noimage-icon-color: #B3B3B3 !default; + +$cardview-card-selection-background-color: #EBF3FC; diff --git a/packages/devextreme-scss/scss/widgets/fluent/cardView/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_index.scss new file mode 100644 index 000000000000..29f2262bbe3e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_index.scss @@ -0,0 +1,5 @@ +@use 'colors'; +@use 'sizes'; +@use '../../base/cardView/index' as *; + +// adduse diff --git a/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss new file mode 100644 index 000000000000..59391bb36c82 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss @@ -0,0 +1,70 @@ +@use "../sizes" as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/cover/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; + +// adduse + +$cardview-fluent-sizes-48: null; +$cardview-fluent-paddings-24: null; +$cardview-fluent-paddings-12: null; +$cardview-fluent-paddings-6: null; +$cardview-fluent-text-size-16: null; +$cardview-fluent-border-radius-8: null; +$cardview-fluent-gaps-16: null; +$cardview-fluent-gaps-24: null; + +@if $size == "default" { + $cardview-fluent-sizes-48: 48px; + $cardview-fluent-paddings-24: 24px; + $cardview-fluent-paddings-12: 12px; + $cardview-fluent-paddings-6: 6px; + $cardview-fluent-text-size-16: 16px; + $cardview-fluent-border-radius-8: 8px; + $cardview-fluent-gaps-16: 16px; + $cardview-fluent-gaps-24: 24px; +} +@else if $size == "compact" { + $cardview-fluent-sizes-48: 32px; + $cardview-fluent-paddings-24: 16px; + $cardview-fluent-paddings-12: 8px; + $cardview-fluent-paddings-6: 4px; + $cardview-fluent-text-size-16: 14px; + $cardview-fluent-border-radius-8: 6px; + $cardview-fluent-gaps-16: 12px; + $cardview-fluent-gaps-24: 16px; +} + +$cardview-border-radius: 16px !default; +$cardview-gap: $cardview-fluent-gaps-24 !default; +$cardview-padding: $cardview-fluent-paddings-24 !default; + +$cardview-headerpanel-content-gap: 8px !default; + +$cardview-header-item-border-width: 1px !default; +$cardview-header-item-border-radius: 6px !default; + +$cardview-content-column-gap: $cardview-fluent-gaps-16 !default; +$cardview-content-row-gap: $cardview-fluent-gaps-16 !default; + +$cardview-card-border-size: 1px !default; +$cardview-card-min-width: 250px !default; +$cardview-card-border-radius: $cardview-fluent-border-radius-8 !default; + +$cardview-card-cover-noimage-icon-size: $cardview-fluent-sizes-48 !default; + +$cardview-card-content-padding-vertical: $cardview-fluent-paddings-12 !default; +$cardview-card-content-padding-horizontal: $cardview-fluent-paddings-12 !default; +$cardview-card-content-field-gap: 5px !default; +$cardview-card-content-cell-padding-vertical: $cardview-fluent-paddings-6 !default; +$cardview-card-content-cell-padding-horizontal: $cardview-fluent-paddings-12 !default; + +$cardview-card-header-text-size: $cardview-fluent-text-size-16 !default; +$cardview-card-header-border-radius: 8px !default; + +$cardview-header-filter-icon-size: 20px !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/_index.scss b/packages/devextreme-scss/scss/widgets/generic/_index.scss index aac2ff330556..ff95df79b8ec 100644 --- a/packages/devextreme-scss/scss/widgets/generic/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/_index.scss @@ -61,4 +61,4 @@ @use "./sortable"; @use "./deferRendering"; @use "./map"; - +@use "./cardView"; diff --git a/packages/devextreme-scss/scss/widgets/generic/cardView/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/cardView/_colors.scss new file mode 100644 index 000000000000..3a0c2a1ba902 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/generic/cardView/_colors.scss @@ -0,0 +1,23 @@ +@use '../colors' as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; + +// adduse + +$cardview-background-color: $typography-bg !default; + +$cardview-header-item-background-color: #F0F0F0 !default; +$cardview-header-item-border-color: #E0E0E0 !default; +$cardview-header-item-hovered-background-color: #EBEBEB !default; +$cardview-header-item-hovered-border-color: #BDBDBD !default; + +$cardview-card-border-color: $base-border-color !default; +$cardview-card-background-color: $base-bg !default; + +$cardview-card-content-field-value-highlight-color: $base-inverted-text-color !default; +$cardview-card-content-field-value-highlight-background: $base-accent !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/cardView/_index.scss b/packages/devextreme-scss/scss/widgets/generic/cardView/_index.scss new file mode 100644 index 000000000000..29f2262bbe3e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/generic/cardView/_index.scss @@ -0,0 +1,5 @@ +@use 'colors'; +@use 'sizes'; +@use '../../base/cardView/index' as *; + +// adduse diff --git a/packages/devextreme-scss/scss/widgets/generic/cardView/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/cardView/_sizes.scss new file mode 100644 index 000000000000..b22cd0209b8e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/generic/cardView/_sizes.scss @@ -0,0 +1,62 @@ +@use "../sizes" as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; + +$cardview-fluent-paddings-24: null; +$cardview-fluent-paddings-12: null; +$cardview-fluent-paddings-6: null; +$cardview-fluent-text-size-16: null; +$cardview-fluent-border-radius-8: null; +$cardview-fluent-gaps-16: null; +$cardview-fluent-gaps-24: null; + +@if $size == "default" { + $cardview-fluent-paddings-24: 24px; + $cardview-fluent-paddings-12: 12px; + $cardview-fluent-paddings-6: 6px; + $cardview-fluent-text-size-16: 16px; + $cardview-fluent-border-radius-8: 8px; + $cardview-fluent-gaps-16: 16px; + $cardview-fluent-gaps-24: 24px; +} +@else if $size == "compact" { + $cardview-fluent-paddings-24: 16px; + $cardview-fluent-paddings-12: 8px; + $cardview-fluent-paddings-6: 4px; + $cardview-fluent-text-size-16: 14px; + $cardview-fluent-border-radius-8: 6px; + $cardview-fluent-gaps-16: 12px; + $cardview-fluent-gaps-24: 16px; +} + +$cardview-border-radius: 16px !default; +$cardview-gap: $cardview-fluent-gaps-24 !default; +$cardview-padding: $cardview-fluent-paddings-24 !default; + +$cardview-headerpanel-content-gap: 8px !default; + +$cardview-header-item-border-width: 1px !default; +$cardview-header-item-border-radius: 6px !default; + +$cardview-content-column-gap: $cardview-fluent-gaps-16 !default; +$cardview-content-row-gap: $cardview-fluent-gaps-16 !default; + +$cardview-card-border-size: 1px !default; +$cardview-card-min-width: 250px !default; +$cardview-card-border-radius: $cardview-fluent-border-radius-8 !default; + +$cardview-card-content-padding-vertical: $cardview-fluent-paddings-12 !default; +$cardview-card-content-padding-horizontal: $cardview-fluent-paddings-12 !default; +$cardview-card-content-field-gap: 5px !default; +$cardview-card-content-cell-padding-vertical: $cardview-fluent-paddings-6 !default; +$cardview-card-content-cell-padding-horizontal: $cardview-fluent-paddings-12 !default; + +$cardview-card-header-text-size: $cardview-fluent-text-size-16 !default; +$cardview-card-header-border-radius: 8px !default; + +// adduse diff --git a/packages/devextreme-scss/scss/widgets/material/_index.scss b/packages/devextreme-scss/scss/widgets/material/_index.scss index e9d6526025ba..ff95df79b8ec 100644 --- a/packages/devextreme-scss/scss/widgets/material/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/_index.scss @@ -61,3 +61,4 @@ @use "./sortable"; @use "./deferRendering"; @use "./map"; +@use "./cardView"; diff --git a/packages/devextreme-scss/scss/widgets/material/cardView/_colors.scss b/packages/devextreme-scss/scss/widgets/material/cardView/_colors.scss new file mode 100644 index 000000000000..3a0c2a1ba902 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/material/cardView/_colors.scss @@ -0,0 +1,23 @@ +@use '../colors' as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; + +// adduse + +$cardview-background-color: $typography-bg !default; + +$cardview-header-item-background-color: #F0F0F0 !default; +$cardview-header-item-border-color: #E0E0E0 !default; +$cardview-header-item-hovered-background-color: #EBEBEB !default; +$cardview-header-item-hovered-border-color: #BDBDBD !default; + +$cardview-card-border-color: $base-border-color !default; +$cardview-card-background-color: $base-bg !default; + +$cardview-card-content-field-value-highlight-color: $base-inverted-text-color !default; +$cardview-card-content-field-value-highlight-background: $base-accent !default; diff --git a/packages/devextreme-scss/scss/widgets/material/cardView/_index.scss b/packages/devextreme-scss/scss/widgets/material/cardView/_index.scss new file mode 100644 index 000000000000..29f2262bbe3e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/material/cardView/_index.scss @@ -0,0 +1,5 @@ +@use 'colors'; +@use 'sizes'; +@use '../../base/cardView/index' as *; + +// adduse diff --git a/packages/devextreme-scss/scss/widgets/material/cardView/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/cardView/_sizes.scss new file mode 100644 index 000000000000..0524c5d6ca62 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/material/cardView/_sizes.scss @@ -0,0 +1,62 @@ +@use "../sizes" as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; + +// adduse + +$cardview-fluent-paddings-24: null; +$cardview-fluent-paddings-12: null; +$cardview-fluent-paddings-6: null; +$cardview-fluent-text-size-16: null; +$cardview-fluent-border-radius-8: null; +$cardview-fluent-gaps-16: null; +$cardview-fluent-gaps-24: null; + +@if $size == "default" { + $cardview-fluent-paddings-24: 24px; + $cardview-fluent-paddings-12: 12px; + $cardview-fluent-paddings-6: 6px; + $cardview-fluent-text-size-16: 16px; + $cardview-fluent-border-radius-8: 8px; + $cardview-fluent-gaps-16: 16px; + $cardview-fluent-gaps-24: 24px; +} +@else if $size == "compact" { + $cardview-fluent-paddings-24: 16px; + $cardview-fluent-paddings-12: 8px; + $cardview-fluent-paddings-6: 4px; + $cardview-fluent-text-size-16: 14px; + $cardview-fluent-border-radius-8: 6px; + $cardview-fluent-gaps-16: 12px; + $cardview-fluent-gaps-24: 16px; +} + +$cardview-border-radius: 16px !default; +$cardview-gap: $cardview-fluent-gaps-24 !default; +$cardview-padding: $cardview-fluent-paddings-24 !default; + +$cardview-headerpanel-content-gap: 8px !default; + +$cardview-header-item-border-width: 1px !default; +$cardview-header-item-border-radius: 6px !default; + +$cardview-content-column-gap: $cardview-fluent-gaps-16 !default; +$cardview-content-row-gap: $cardview-fluent-gaps-16 !default; + +$cardview-card-border-size: 1px !default; +$cardview-card-min-width: 250px !default; +$cardview-card-border-radius: $cardview-fluent-border-radius-8 !default; + +$cardview-card-content-padding-vertical: $cardview-fluent-paddings-12 !default; +$cardview-card-content-padding-horizontal: $cardview-fluent-paddings-12 !default; +$cardview-card-content-field-gap: 5px !default; +$cardview-card-content-cell-padding-vertical: $cardview-fluent-paddings-6 !default; +$cardview-card-content-cell-padding-horizontal: $cardview-fluent-paddings-12 !default; + +$cardview-card-header-text-size: $cardview-fluent-text-size-16 !default; +$cardview-card-header-border-radius: 8px !default; diff --git a/packages/devextreme-themebuilder/tests/data/dependencies.ts b/packages/devextreme-themebuilder/tests/data/dependencies.ts index 27ffe61a73de..b2d36dd7ea7b 100644 --- a/packages/devextreme-themebuilder/tests/data/dependencies.ts +++ b/packages/devextreme-themebuilder/tests/data/dependencies.ts @@ -16,6 +16,7 @@ export const dependencies: FlatStylesDependencies = { buttongroup: ['validation', 'button'], dropdownbutton: ['validation', 'button', 'buttongroup', 'popup', 'loadindicator', 'loadpanel', 'scrollview', 'list'], calendar: ['validation', 'button'], + cardview: ['box', 'button', 'calendar', 'checkbox', 'contextmenu', 'datebox', 'filterbuilder', 'list', 'loadindicator', 'loadpanel', 'numberbox', 'popup', 'scrollview', 'selectbox', 'sortable', 'textbox', 'toast', 'toolbar', 'treeview', 'validation'], chat: ['button', 'loadindicator', 'loadpanel', 'scrollview', 'textbox', 'validation'], checkbox: ['validation'], numberbox: ['validation', 'button', 'loadindicator'], diff --git a/packages/devextreme/js/__internal/core/di/index.test.ts b/packages/devextreme/js/__internal/core/di/index.test.ts new file mode 100644 index 000000000000..4e5ece50a074 --- /dev/null +++ b/packages/devextreme/js/__internal/core/di/index.test.ts @@ -0,0 +1,186 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ +/* eslint-disable prefer-const */ +/* eslint-disable @typescript-eslint/init-declarations */ +/* eslint-disable max-classes-per-file */ +/* eslint-disable class-methods-use-this */ +import { describe, expect, it } from '@jest/globals'; + +import { DIContext } from './index'; + +describe('basic', () => { + describe('register', () => { + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + + it('should return registered class', () => { + const ctx = new DIContext(); + ctx.register(MyClass); + + expect(ctx.get(MyClass)).toBeInstanceOf(MyClass); + expect(ctx.get(MyClass).getNumber()).toBe(1); + }); + + it('should return registered class with tryGet', () => { + const ctx = new DIContext(); + ctx.register(MyClass); + + expect(ctx.tryGet(MyClass)).toBeInstanceOf(MyClass); + expect(ctx.tryGet(MyClass)?.getNumber()).toBe(1); + }); + + it('should return same instance each time', () => { + const ctx = new DIContext(); + ctx.register(MyClass); + + expect(ctx.get(MyClass)).toBe(ctx.get(MyClass)); + }); + }); + + describe('registerInstance', () => { + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + + const ctx = new DIContext(); + const instance = new MyClass(); + ctx.registerInstance(MyClass, instance); + + it('should work', () => { + expect(ctx.get(MyClass)).toBe(instance); + }); + }); + + describe('non registered items', () => { + const ctx = new DIContext(); + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + it('should throw', () => { + expect(() => ctx.get(MyClass)).toThrow(); + }); + it('should not throw if tryGet', () => { + expect(ctx.tryGet(MyClass)).toBe(null); + }); + }); +}); + +describe('dependencies', () => { + class MyUtilityClass { + static dependencies = [] as const; + + getNumber(): number { + return 2; + } + } + + class MyClass { + static dependencies = [MyUtilityClass] as const; + + constructor(private readonly utility: MyUtilityClass) {} + + getSuperNumber(): number { + return this.utility.getNumber() * 2; + } + } + + const ctx = new DIContext(); + ctx.register(MyUtilityClass); + ctx.register(MyClass); + + it('should return registered class', () => { + expect(ctx.get(MyClass)).toBeInstanceOf(MyClass); + expect(ctx.get(MyUtilityClass)).toBeInstanceOf(MyUtilityClass); + }); + + it('dependecies should work', () => { + expect(ctx.get(MyClass).getSuperNumber()).toBe(4); + }); +}); + +describe('mocks', () => { + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + + class MyClassMock implements MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 2; + } + } + + const ctx = new DIContext(); + ctx.register(MyClass, MyClassMock); + + it('should return mock class when they are registered', () => { + expect(ctx.get(MyClass)).toBeInstanceOf(MyClassMock); + expect(ctx.get(MyClass).getNumber()).toBe(2); + }); +}); + +it('should work regardless of registration order', () => { + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + + class MyDependentClass { + static dependencies = [MyClass] as const; + + constructor(private readonly myClass: MyClass) {} + + getSuperNumber(): number { + return this.myClass.getNumber() * 2; + } + } + + const ctx = new DIContext(); + ctx.register(MyDependentClass); + ctx.register(MyClass); + expect(ctx.get(MyDependentClass).getSuperNumber()).toBe(2); +}); + +describe('dependency cycle', () => { + class MyClass1 { + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-use-before-define + static dependencies = [MyClass2] as const; + + constructor(private readonly myClass2: MyClass2) {} + } + class MyClass2 { + static dependencies = [MyClass1] as const; + + constructor(private readonly myClass1: MyClass1) {} + } + + const ctx = new DIContext(); + ctx.register(MyClass1); + ctx.register(MyClass2); + + it('should throw', () => { + expect(() => ctx.get(MyClass1)).toThrow(); + expect(() => ctx.get(MyClass2)).toThrow(); + }); +}); diff --git a/packages/devextreme/js/__internal/core/di/index.ts b/packages/devextreme/js/__internal/core/di/index.ts new file mode 100644 index 000000000000..20f8700496f1 --- /dev/null +++ b/packages/devextreme/js/__internal/core/di/index.ts @@ -0,0 +1,88 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +interface AbstractType extends Function { + prototype: T; +} + +type Constructor = new(...deps: TDeps) => T; + +interface DIItem extends Constructor { + dependencies: readonly [...{ [P in keyof TDeps]: AbstractType }]; +} + +export class DIContext { + private readonly instances: Map = new Map(); + + private readonly fabrics: Map = new Map(); + + private readonly antiRecursionSet = new Set(); + + public register( + id: AbstractType, + fabric: DIItem, + ): void; + public register( + idAndFabric: DIItem, + ): void; + public register( + id: DIItem, + fabric?: DIItem, + ): void { + // eslint-disable-next-line no-param-reassign + fabric ??= id; + this.fabrics.set(id, fabric); + } + + public registerInstance( + id: AbstractType, + instance: T, + ): void { + this.instances.set(id, instance); + } + + public get( + id: AbstractType, + ): T { + const instance = this.tryGet(id); + + if (instance) { + return instance; + } + + throw new Error(`DI item is not registered: ${id}`); + } + + public tryGet( + id: AbstractType, + ): T | null { + if (this.instances.get(id)) { + return this.instances.get(id) as T; + } + + const fabric = this.fabrics.get(id); + if (fabric) { + const res: T = this.create(fabric as any); + this.instances.set(id, res); + this.instances.set(fabric, res); + return res; + } + + return null; + } + + private create(fabric: DIItem): T { + if (this.antiRecursionSet.has(fabric)) { + throw new Error('dependency cycle in DI'); + } + + this.antiRecursionSet.add(fabric); + + const args = fabric.dependencies.map((dependency) => this.get(dependency)); + + this.antiRecursionSet.delete(fabric); + + // eslint-disable-next-line new-cap + return new fabric(...args as any); + } +} diff --git a/packages/devextreme/js/__internal/core/m_inferno_renderer.ts b/packages/devextreme/js/__internal/core/m_inferno_renderer.ts index 2a1e550457a0..06ee5e06927b 100644 --- a/packages/devextreme/js/__internal/core/m_inferno_renderer.ts +++ b/packages/devextreme/js/__internal/core/m_inferno_renderer.ts @@ -1,9 +1,9 @@ +/* eslint-disable spellcheck/spell-checker */ import domAdapter from '@js/core/dom_adapter'; import { cleanDataRecursive } from '@js/core/element_data'; import injector from '@js/core/utils/dependency_injector'; import { hydrate, InfernoEffectHost } from '@ts/core/r1/runtime/inferno/index'; import { render } from 'inferno'; -// eslint-disable-next-line import/no-extraneous-dependencies import { createElement } from 'inferno-create-element'; const remove = (element) => { @@ -59,6 +59,14 @@ const infernoRenderer = injector({ render(createElement(component, props), container); } }, + + renderIntoContainer: (jsx, container, replace) => { + if (!replace) { + hydrate(jsx, container); + } else { + render(jsx, container); + } + }, }); export { infernoRenderer }; diff --git a/packages/devextreme/js/__internal/core/r1/runtime/inferno/create_context.ts b/packages/devextreme/js/__internal/core/r1/runtime/inferno/create_context.ts index 94cda0c2ce25..ddf7e068703d 100644 --- a/packages/devextreme/js/__internal/core/r1/runtime/inferno/create_context.ts +++ b/packages/devextreme/js/__internal/core/r1/runtime/inferno/create_context.ts @@ -1,13 +1,13 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable func-names */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable no-plusplus */ -/* eslint-disable @typescript-eslint/no-explicit-any */ + import { Component } from 'inferno'; let contextId = 0; -export const createContext = function(defaultValue: T): { id: number; - Provider: any; - defaultValue: unknown; } { +export const createContext = function(defaultValue: T) { const id = contextId++; return { diff --git a/packages/devextreme/js/__internal/core/reactive/core.ts b/packages/devextreme/js/__internal/core/reactive/core.ts new file mode 100644 index 000000000000..e0874385e623 --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/core.ts @@ -0,0 +1,88 @@ +/* eslint-disable spellcheck/spell-checker */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable max-classes-per-file */ + +import { type Subscription, SubscriptionBag } from './subscription'; +import type { + Callback, Gettable, Subscribable, Updatable, +} from './types'; + +export class Observable implements Subscribable, Updatable, Gettable { + private readonly callbacks: Set> = new Set(); + + constructor(private value: T) {} + + update(value: T): void { + if (this.value === value) { + return; + } + this.value = value; + + this.callbacks.forEach((c) => { + c(value); + }); + } + + updateFunc(func: (oldValue: T) => T): void { + this.update(func(this.value)); + } + + subscribe(callback: Callback): Subscription { + this.callbacks.add(callback); + callback(this.value); + + return { + unsubscribe: () => this.callbacks.delete(callback), + }; + } + + unreactive_get(): T { + return this.value; + } + + dispose(): void { + this.callbacks.clear(); + } +} + +export class InterruptableComputed< + TArgs extends readonly any[], TValue, +> extends Observable { + private readonly depValues: [...TArgs]; + + private readonly depInitialized: boolean[]; + + private isInitialized = false; + + private readonly subscriptions = new SubscriptionBag(); + + constructor( + compute: (...args: TArgs) => TValue, + deps: { [I in keyof TArgs]: Subscribable }, + ) { + super(undefined as any); + + this.depValues = deps.map(() => undefined) as any; + this.depInitialized = deps.map(() => false); + + deps.forEach((dep, i) => { + this.subscriptions.add(dep.subscribe((v) => { + this.depValues[i] = v; + + if (!this.isInitialized) { + this.depInitialized[i] = true; + this.isInitialized = this.depInitialized.every((e) => e); + } + + if (this.isInitialized) { + this.update(compute(...this.depValues)); + } + })); + }); + } + + dispose(): void { + super.dispose(); + this.subscriptions.unsubscribe(); + } +} diff --git a/packages/devextreme/js/__internal/core/reactive/index.ts b/packages/devextreme/js/__internal/core/reactive/index.ts new file mode 100644 index 000000000000..e2e8474530df --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/index.ts @@ -0,0 +1,3 @@ +export * from './subscription'; +export * from './types'; +export * from './utilities'; diff --git a/packages/devextreme/js/__internal/core/reactive/subscription.ts b/packages/devextreme/js/__internal/core/reactive/subscription.ts new file mode 100644 index 000000000000..d3bc303311df --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/subscription.ts @@ -0,0 +1,17 @@ +export interface Subscription { + unsubscribe: () => void; +} + +export class SubscriptionBag implements Subscription { + private readonly subscriptions: Subscription[] = []; + + add(subscription: Subscription): void { + this.subscriptions.push(subscription); + } + + unsubscribe(): void { + this.subscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + } +} diff --git a/packages/devextreme/js/__internal/core/reactive/types.ts b/packages/devextreme/js/__internal/core/reactive/types.ts new file mode 100644 index 000000000000..176361809ac4 --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/types.ts @@ -0,0 +1,28 @@ +/* eslint-disable spellcheck/spell-checker */ +import type { Subscription } from './subscription'; + +export interface Subscribable { + subscribe: (callback: Callback) => Subscription; +} + +export type MaybeSubscribable = T | Subscribable; + +export type MapMaybeSubscribable = { [K in keyof T]: MaybeSubscribable }; + +export function isSubscribable(value: unknown): value is Subscribable { + return typeof value === 'object' && !!value && 'subscribe' in value; +} + +export type Callback = (value: T) => void; + +export interface Updatable { + update: (value: T) => void; + updateFunc: (func: (oldValue: T) => T) => void; +} + +export interface Gettable { + unreactive_get: () => T; +} + +export type SubsGets = Subscribable & Gettable; +export type SubsGetsUpd = Subscribable & Gettable & Updatable; diff --git a/packages/devextreme/js/__internal/core/reactive/utilities.test.ts b/packages/devextreme/js/__internal/core/reactive/utilities.test.ts new file mode 100644 index 000000000000..c2faab3d23c4 --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/utilities.test.ts @@ -0,0 +1,217 @@ +/* eslint-disable spellcheck/spell-checker */ +import { + beforeEach, describe, expect, it, jest, +} from '@jest/globals'; + +import { + computed, interruptableComputed, state, toSubscribable, +} from './utilities'; + +describe('state', () => { + let myState = state('some value'); + + beforeEach(() => { + myState = state('some value'); + }); + + describe('unreactive_get', () => { + it('should return value', () => { + expect(myState.unreactive_get()).toBe('some value'); + }); + + it('should return current value if it was updated', () => { + myState.update('new value'); + expect(myState.unreactive_get()).toBe('new value'); + }); + }); + + describe('subscribe', () => { + it('should call callback on initial set', () => { + const callback = jest.fn(); + myState.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value'); + }); + + it('should call callback on update', () => { + const callback = jest.fn(); + myState.subscribe(callback); + + myState.update('new value'); + + expect(callback).toBeCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'some value'); + expect(callback).toHaveBeenNthCalledWith(2, 'new value'); + }); + + it('should not trigger update if value is not changed', () => { + const callback = jest.fn(); + myState.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + + myState.update('some value'); + + expect(callback).toBeCalledTimes(1); + }); + }); + + describe('dispose', () => { + it('should prevent all updates', () => { + const callback = jest.fn(); + myState.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value'); + + // @ts-expect-error + myState.dispose(); + myState.update('new value'); + + expect(callback).toBeCalledTimes(1); + }); + }); +}); + +describe('computed', () => { + let myState1 = state('some value'); + let myState2 = state('other value'); + let myComputed = computed( + (v1, v2) => `${v1} ${v2}`, + [myState1, myState2], + ); + + beforeEach(() => { + myState1 = state('some value'); + myState2 = state('other value'); + myComputed = computed( + (v1, v2) => `${v1} ${v2}`, + [myState1, myState2], + ); + }); + + describe('unreactive_get', () => { + it('should calculate initial value', () => { + expect(myComputed.unreactive_get()).toBe('some value other value'); + }); + + it('should return current value if it dependency is updated', () => { + myState1.update('new value'); + expect(myComputed.unreactive_get()).toBe('new value other value'); + }); + }); + + describe('subscribe', () => { + it('should call callback on initial set', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value other value'); + }); + + it('should call callback on update of dependency', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + myState1.update('new value'); + + expect(callback).toBeCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'some value other value'); + expect(callback).toHaveBeenNthCalledWith(2, 'new value other value'); + }); + }); +}); + +describe('interruptableComputed', () => { + let myState1 = state('some value'); + let myState2 = state('other value'); + let myComputed = interruptableComputed( + (v1, v2) => `${v1} ${v2}`, + [myState1, myState2], + ); + + beforeEach(() => { + myState1 = state('some value'); + myState2 = state('other value'); + myComputed = interruptableComputed( + (v1, v2) => `${v1} ${v2}`, + [myState1, myState2], + ); + }); + + describe('unreactive_get', () => { + it('should calculate initial value', () => { + expect(myComputed.unreactive_get()).toBe('some value other value'); + }); + + it('should return current value if it was updated', () => { + myComputed.update('new value'); + expect(myComputed.unreactive_get()).toBe('new value'); + }); + + it('should return current value if it dependency is updated', () => { + myState1.update('new value'); + expect(myComputed.unreactive_get()).toBe('new value other value'); + }); + }); + + describe('subscribe', () => { + it('should call callback on initial set', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value other value'); + }); + + it('should call callback on update', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + myComputed.update('new value'); + + expect(callback).toBeCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'some value other value'); + expect(callback).toHaveBeenNthCalledWith(2, 'new value'); + }); + + it('should call callback on update of dependency', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + myState1.update('new value'); + + expect(callback).toBeCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'some value other value'); + expect(callback).toHaveBeenNthCalledWith(2, 'new value other value'); + }); + + it('should not trigger update if value is not changed', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + + myComputed.update('some value other value'); + + expect(callback).toBeCalledTimes(1); + }); + }); +}); + +describe('toSubscribable', () => { + it('should wrap value if it is not subscribable', () => { + const callback = jest.fn(); + toSubscribable('some value').subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value'); + }); + + it('should return value as is if subscribable', () => { + const myState = state(1); + expect(toSubscribable(myState)).toBe(myState); + }); +}); diff --git a/packages/devextreme/js/__internal/core/reactive/utilities.ts b/packages/devextreme/js/__internal/core/reactive/utilities.ts new file mode 100644 index 000000000000..79e6cee8ec0a --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/utilities.ts @@ -0,0 +1,248 @@ +/* eslint-disable @typescript-eslint/no-invalid-void-type */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable spellcheck/spell-checker */ +import { InterruptableComputed, Observable } from './core'; +import { type Subscription, SubscriptionBag } from './subscription'; +import type { + Gettable, MapMaybeSubscribable, MaybeSubscribable, Subscribable, SubsGets, SubsGetsUpd, Updatable, +} from './types'; +import { isSubscribable } from './types'; + +/** + * Creates new reactive state atom. + * @example + * ``` + * const myState = state(0); + * myState.update(1); + * ``` + * @param value initial value of state + */ +export function state(value: T): Subscribable & Updatable & Gettable { + return new Observable(value); +} + +/** + * Creates computed atom based on other atoms. + * @example + * ``` + * const myState = state(0); + * const myComputed = computed( + * (value) => value + 1, + * [myState] + * ); + * ``` + * @param compute computation func + * @param deps dependency atoms + */ +export function computed( + compute: (t1: T1) => TValue, + deps: [Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2) => TValue, + deps: [Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2, t3: T3,) => TValue, + deps: [Subscribable, Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2, t3: T3, t4: T4) => TValue, + deps: [Subscribable, Subscribable, Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => TValue, + deps: [Subscribable, Subscribable, Subscribable, Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => TValue, + // eslint-disable-next-line @stylistic/max-len + deps: [Subscribable, Subscribable, Subscribable, Subscribable, Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (...args: TArgs) => TValue, + deps: { [I in keyof TArgs]: Subscribable }, +): SubsGets; +export function computed( + compute: (...args: TArgs) => TValue, + deps: { [I in keyof TArgs]: Subscribable }, +): SubsGets { + return new InterruptableComputed(compute, deps); +} + +/** + * Computed, with ability to override value using `.update(...)` method. + * @see {@link computed} + */ +export function interruptableComputed( + compute: (t1: T1) => TValue, + deps: [Subscribable] +): SubsGetsUpd; +export function interruptableComputed( + compute: (t1: T1, t2: T2) => TValue, + deps: [Subscribable, Subscribable] +): SubsGetsUpd; +export function interruptableComputed( + compute: (t1: T1, t2: T2, t3: T3,) => TValue, + deps: [Subscribable, Subscribable, Subscribable] +): SubsGetsUpd; +export function interruptableComputed( + compute: (t1: T1, t2: T2, t3: T3, t4: T4) => TValue, + deps: [Subscribable, Subscribable, Subscribable, Subscribable] +): SubsGetsUpd; +export function interruptableComputed( + compute: (...args: TArgs) => TValue, + deps: { [I in keyof TArgs]: Subscribable }, +): SubsGetsUpd { + return new InterruptableComputed(compute, deps); +} + +/** + * Allows to subscribe function with some side effects to changes of dependency atoms. + * @param callback function which is executed each time any dependency is updated + * @param deps dependencies + */ +export function effect( + callback: (t1: T1) => ((() => void) | void), + deps: [Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2) => ((() => void) | void), + deps: [Subscribable, Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3,) => ((() => void) | void), + deps: [Subscribable, Subscribable, Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3, t4: T4) => ((() => void) | void), + deps: [Subscribable, Subscribable, Subscribable, Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => ((() => void) | void), + deps: [Subscribable, Subscribable, Subscribable, Subscribable, Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => ((() => void) | void), + deps: [ + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + ] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7) => ((() => void) | void), + deps: [ + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + ] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8) + => ((() => void) | void), + deps: [ + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + ] +): Subscription; +export function effect( + callback: (...args: TArgs) => ((() => void) | void), + deps: { [I in keyof TArgs]: Subscribable }, +): Subscription { + const depValues: [...TArgs] = deps.map(() => undefined) as any; + const depInitialized = deps.map(() => false); + let isInitialized = false; + + const subscription = new SubscriptionBag(); + + deps.forEach((dep, i) => { + subscription.add(dep.subscribe((v) => { + depValues[i] = v; + + if (!isInitialized) { + depInitialized[i] = true; + isInitialized = depInitialized.every((e) => e); + } + + if (isInitialized) { + callback(...depValues); + } + })); + }); + + return subscription; +} + +export function toSubscribable(v: MaybeSubscribable): Subscribable { + if (isSubscribable(v)) { + return v; + } + + return new Observable(v); +} + +/** + * Condition atom, basing whether `cond` is true or false, + * returns value of `ifTrue` or `ifFalse` param. + * @param cond + * @param ifTrue + * @param ifFalse + */ +export function iif( + cond: MaybeSubscribable, + ifTrue: MaybeSubscribable, + ifFalse: MaybeSubscribable, +): Subscribable { + const obs = state(undefined as any); + // eslint-disable-next-line @typescript-eslint/init-declarations + let subscription: Subscription | undefined; + + // eslint-disable-next-line @typescript-eslint/no-shadow + toSubscribable(cond).subscribe((cond) => { + subscription?.unsubscribe(); + const newSource = cond ? ifTrue : ifFalse; + subscription = toSubscribable(newSource).subscribe(obs.update.bind(obs)); + }); + + return obs; +} + +/** + * Combines object of Subscribables to Subscribable of object. + * @example + * ``` + * const myValueA = state(0); + * const myValueB = state(1); + * const obj = combine({ + * myValueA, myValueB + * }); + * + * obj.unreactive_get(); // {myValueA: 0, myValueB: 1} + * @returns + */ +export function combined( + obj: MapMaybeSubscribable, +): SubsGets { + const entries = Object.entries(obj) as any as [string, Subscribable][]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return computed( + (...args) => Object.fromEntries( + args.map((v, i) => [entries[i][0], v]), + ), + entries.map(([, v]) => toSubscribable(v)), + ) as any; +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/const.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/const.ts new file mode 100644 index 000000000000..9d180a29dcf8 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/const.ts @@ -0,0 +1,26 @@ +import messageLocalization from '@js/common/core/localization/message'; +import type { ColumnChooser } from '@js/common/grids'; + +export const defaultOptions = { + columnChooser: { + enabled: false, + search: { + enabled: false, + timeout: 500, + editorOptions: {}, + }, + selection: { + allowSelectAll: false, + selectByClick: false, + recursive: false, + }, + position: undefined, + sortOrder: undefined, + mode: 'dragAndDrop', + width: 250, + height: 260, + title: messageLocalization.format('dxDataGrid-columnChooserTitle'), + emptyPanelText: messageLocalization.format('dxDataGrid-columnChooserEmptyText'), + container: undefined, + } as ColumnChooser, +}; diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index 6db8f21ffb23..4cb0ef5d6f09 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -19,6 +19,7 @@ import type { HeaderPanel } from '../header_panel/m_header_panel'; import modules from '../m_modules'; import type { ModuleType } from '../m_types'; import { ColumnsView } from '../views/m_columns_view'; +import { defaultOptions } from './const'; const COLUMN_CHOOSER_CLASS = 'column-chooser'; const COLUMN_CHOOSER_BUTTON_CLASS = 'column-chooser-button'; @@ -600,29 +601,7 @@ const columnHeadersView = (Base: ModuleType) => class ColumnC export const columnChooserModule = { defaultOptions() { - return { - columnChooser: { - enabled: false, - search: { - enabled: false, - timeout: 500, - editorOptions: {}, - }, - selection: { - allowSelectAll: false, - selectByClick: false, - recursive: false, - }, - position: undefined, - mode: 'dragAndDrop', - width: 250, - height: 260, - title: messageLocalization.format('dxDataGrid-columnChooserTitle'), - emptyPanelText: messageLocalization.format('dxDataGrid-columnChooserEmptyText'), - // TODO private option - container: undefined, - }, - }; + return defaultOptions; }, controllers: { columnChooser: ColumnChooserController, diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts index 9701c24a428c..bebdc62db134 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts @@ -60,7 +60,7 @@ function ungroupUTCDates(items, dateParts?, dates?) { return dates; } -function convertDataFromUTCToLocal(data, column) { +export function convertDataFromUTCToLocal(data, column) { const dates = ungroupUTCDates(data); // @ts-expect-error const query = dataQuery(dates); @@ -72,11 +72,11 @@ function convertDataFromUTCToLocal(data, column) { return storeHelper.queryByOptions(query, { group }).toArray(); } -function isUTCFormat(format) { +export function isUTCFormat(format) { return format?.slice(-1) === 'Z' || format?.slice(-3) === '\'Z\''; } -const getFormatOptions = function (value, column, currentLevel) { +export const getFormatOptions = function (value, column, currentLevel) { const groupInterval = filterUtils.getGroupInterval(column); const result: any = gridCoreUtils.getFormatOptionsByColumn(column, 'headerFilter'); diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts index 4eaa86385eb8..17a9e96d867c 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts @@ -213,6 +213,7 @@ export class HeaderFilterView extends Modules.View { const $element = that.element(); const headerFilterOptions = this._normalizeHeaderFilterOptions(options); + const { hidePopupCallback } = options; const { height, width } = headerFilterOptions; const dxPopupOptions = { @@ -247,6 +248,7 @@ export class HeaderFilterView extends Modules.View { text: headerFilterOptions.texts.cancel, onClick() { that.hideHeaderFilterMenu(); + hidePopupCallback?.(); }, }, }, diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts b/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts index c4682b9c663d..57b5dd0307ec 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts @@ -2,12 +2,12 @@ import messageLocalization from '@js/common/core/localization/message'; import $ from '@js/core/renderer'; import { getPathParts } from '@js/core/utils/data'; -import { extend } from '@js/core/utils/extend'; -import { isDefined, isString } from '@js/core/utils/type'; +import { isDefined } from '@js/core/utils/type'; import type { Properties as ToolbarProperties } from '@js/ui/toolbar'; import Toolbar from '@js/ui/toolbar'; import type { EditingController } from '@ts/grids/grid_core/editing/m_editing'; import type { HeaderFilterController } from '@ts/grids/grid_core/header_filter/m_header_filter'; +import { normalizeToolbarItems } from '@ts/grids/new/grid_core/toolbar/utils'; import type { ModuleType } from '../m_types'; import { ColumnsView } from '../views/m_columns_view'; @@ -72,7 +72,11 @@ export class HeaderPanel extends ColumnsView { }; const userItems = userToolbarOptions?.items; - options.toolbarOptions.items = this._normalizeToolbarItems(options.toolbarOptions.items, userItems); + options.toolbarOptions.items = normalizeToolbarItems( + options.toolbarOptions.items, + userItems, + DEFAULT_TOOLBAR_ITEM_NAMES, + ); this.executeAction('onToolbarPreparing', options); @@ -84,51 +88,6 @@ export class HeaderPanel extends ColumnsView { return options.toolbarOptions; } - private _normalizeToolbarItems(defaultItems, userItems) { - defaultItems.forEach((button) => { - if (!DEFAULT_TOOLBAR_ITEM_NAMES.includes(button.name)) { - throw new Error(`Default toolbar item '${button.name}' is not added to DEFAULT_TOOLBAR_ITEM_NAMES`); - } - }); - - const defaultProps = { - location: 'after', - }; - - const isArray = Array.isArray(userItems); - - if (!isDefined(userItems)) { - return defaultItems; - } - - if (!isArray) { - userItems = [userItems]; - } - - const defaultButtonsByNames = {}; - defaultItems.forEach((button) => { - defaultButtonsByNames[button.name] = button; - }); - - const normalizedItems = userItems.map((button) => { - if (isString(button)) { - button = { name: button }; - } - - if (isDefined(button.name)) { - if (isDefined(defaultButtonsByNames[button.name])) { - button = extend(true, {}, defaultButtonsByNames[button.name], button); - } else if (DEFAULT_TOOLBAR_ITEM_NAMES.includes(button.name)) { - button = { ...button, visible: false }; - } - } - - return extend(true, {}, defaultProps, button); - }); - - return isArray ? normalizedItems : normalizedItems[0]; - } - protected _renderCore() { if (!this._toolbar) { const $headerPanel = this.element(); @@ -217,7 +176,11 @@ export class HeaderPanel extends ColumnsView { this._invalidate(); } else if (parts.length === 3) { // `toolbar.items[i]` case - const normalizedItem = this._normalizeToolbarItems(this._getToolbarItems(), args.value); + const normalizedItem = normalizeToolbarItems( + this._getToolbarItems(), + [args.value], + DEFAULT_TOOLBAR_ITEM_NAMES, + )[0]; this._toolbar?.option(optionName, normalizedItem); } else if (parts.length >= 4) { // `toolbar.items[i].prop` case diff --git a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap new file mode 100644 index 000000000000..3f9c8e343b33 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap @@ -0,0 +1,144 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`common initial render should be successfull 1`] = ` +
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ + No data + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+ +
+ +
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/caption.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/caption.test.tsx.snap new file mode 100644 index 000000000000..ff2eb4967616 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/caption.test.tsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Content View Caption should render template with title 1`] = ` +
+
+ TEST_TITLE +
+
+`; + +exports[`Content View Caption should render title 1`] = ` +
+ + TEST_TITLE + : + +
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/card.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/card.test.tsx.snap new file mode 100644 index 000000000000..2346af4bb7c3 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/card.test.tsx.snap @@ -0,0 +1,151 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rendering should be rendered correctly 1`] = ` +
+
+
+ +
+ Card Cover +
+
+
+ + Field + : + + + devextreme + +
+
+
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/cover.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/cover.test.tsx.snap new file mode 100644 index 000000000000..519e26a53d55 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/cover.test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Cover when there is no image should render image thumbnail 1`] = ` +
+
+
+
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/value_text.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/value_text.test.tsx.snap new file mode 100644 index 000000000000..c16424aeef68 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/value_text.test.tsx.snap @@ -0,0 +1,52 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Content View ValueText should add title attribute 1`] = ` +
+ + TEST + +
+`; + +exports[`Content View ValueText should render highlighted text if passed 1`] = ` +
+ + + USUAL_PART + + + MATCH_PART + + + USUAL_PART + + +
+`; + +exports[`Content View ValueText should render plain text if highlighted text is null 1`] = ` +
+ + TEST_TEXT + +
+`; + +exports[`Content View ValueText should set root classes 1`] = ` +
+ + TEST + +
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/caption.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/caption.test.tsx new file mode 100644 index 000000000000..db963239ab33 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/caption.test.tsx @@ -0,0 +1,28 @@ +import { + describe, expect, it, +} from '@jest/globals'; +import { render } from 'inferno'; + +import { Caption } from './caption'; + +describe('Content View', () => { + describe('Caption', () => { + it('should render title', () => { + const container = document.createElement('div'); + + render(, container); + + expect(container).toMatchSnapshot(); + }); + + it('should render template with title', () => { + const container = document.createElement('div'); + + render( (
{title}
)} + />, container); + + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/caption.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/caption.tsx new file mode 100644 index 000000000000..46d595a12b06 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/caption.tsx @@ -0,0 +1,12 @@ +export interface CaptionProps { + title: string | undefined; + template?: (title: string) => JSX.Element; + wordWrapEnabled?: boolean; +} + +export const Caption = ( + { title, template }: CaptionProps, +): JSX.Element => (template && title + ? template(title) + : ({title}:) +); 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 new file mode 100644 index 000000000000..52d831b15266 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.events.test.tsx @@ -0,0 +1,201 @@ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ + +/* eslint-disable @typescript-eslint/init-declarations */ + +import { describe, expect, it } from '@jest/globals'; +import { createRef, render } from 'inferno'; + +import { Card, CLASSES } from './card'; + +const createMockCallback = () => ({ + called: false, + call(): void { + this.called = true; + }, +}); + +const mockSelectCard = createMockCallback(); +const mockOnDblClick = createMockCallback(); +const mockOnClick = createMockCallback(); +const mockOnHold = createMockCallback(); + +const props = { + row: { + cells: [ + { + column: { + dataField: 'Name', + name: 'Field', + }, + value: 'devextreme', + text: 'devextreme', + }, + ], + key: 0, + }, + toolbar: [ + { + location: 'before', + widget: 'dxCheckBox', + }, + { + location: 'before', + text: 'Card Header', + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'edit', + stylingMode: 'text', + }, + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'trash', + stylingMode: 'text', + }, + }, + ], + cover: { + src: 'https://www.devexpress.com/support/demos/i/demo-thumbs/aspnetcore-grid.png', + alt: 'Card Cover', + className: 'card-cover', + }, + hoverStateEnabled: true, + maxWidth: 300, + width: 300, + minWidth: 300, + selectCard: mockSelectCard.call.bind(mockSelectCard), + onDblClick: mockOnDblClick.call.bind(mockOnDblClick), + onClick: mockOnClick.call.bind(mockOnClick), + onHold: mockOnHold.call.bind(mockOnHold), +}; + +describe('Events', () => { + let container: HTMLDivElement; + // @ts-expect-error + beforeEach(() => { + container = document.createElement('div'); + // @ts-expect-error + render(, container); + }); + + it('should trigger onClick event', () => { + const cardElement = container.querySelector(`.${CLASSES.card}`); + cardElement?.dispatchEvent(new MouseEvent('click')); + + expect(mockOnClick.called).toBe(true); + }); + + 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, + fn: ({ isHovered }: { isHovered: boolean }) => { + mockHover.called = true; + expect(isHovered).toBe(true); + }, + }; + + const newProps = { ...props, hoverStateEnabled: true, onHoverChanged: mockHover.fn }; + // @ts-expect-error + render(, container); + + const cardElement = container.querySelector(`.${CLASSES.card}`); + cardElement?.dispatchEvent(new MouseEvent('mouseenter')); + + expect(mockHover.called).toBe(true); + }); + + it('should trigger onHoverChanged event on mouse leave', () => { + const mockHover: { called: boolean; fn: ({ isHovered }: { isHovered: boolean }) => void } = { + called: false, + fn: ({ isHovered }: { isHovered: boolean }) => { + mockHover.called = true; + expect(isHovered).toBe(false); + }, + }; + + const newProps = { ...props, hoverStateEnabled: true, onHoverChanged: mockHover.fn }; + // @ts-expect-error + render(, container); + + const cardElement = container.querySelector(`.${CLASSES.card}`); + cardElement?.dispatchEvent(new MouseEvent('mouseleave')); + + expect(mockHover.called).toBe(true); + }); + + it('should handle hoverStateEnabled prop correctly', () => { + const cardElement = container.querySelector('.dx-cardview-card'); + cardElement?.dispatchEvent(new MouseEvent('mouseenter')); + + const classList = cardElement?.getAttribute('class') || ''; + expect(classList).toContain('dx-cardview-card-hover'); + }); + + it('should render field template correctly', () => { + const fieldName = container.querySelector('.dx-cardview-field-name'); + const fieldValue = container.querySelector('.dx-cardview-field-value'); + + expect(fieldName?.textContent).toBe('Field:'); + 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.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.test.tsx new file mode 100644 index 000000000000..3b060f83f28d --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.test.tsx @@ -0,0 +1,121 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { describe, expect, it } from '@jest/globals'; +import { compileGetter } from '@js/common/data'; +import { render } from 'inferno'; + +import { Card } from './card'; + +const props = { + row: { + cells: [ + { + column: { + dataField: 'Name', + name: 'Field', + }, + value: 'devextreme', + text: 'devextreme', + }, + ], + key: 0, + data: { + Field: 'Name', + img: 'https://www.devexpress.com/support/demos/i/demo-thumbs/aspnetcore-grid.png', + alt: 'Card Cover', + }, + }, + toolbar: [ + { + location: 'before', + widget: 'dxCheckBox', + }, + { + location: 'before', + text: 'Card Header', + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'edit', + stylingMode: 'text', + }, + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'trash', + stylingMode: 'text', + }, + }, + ], + cover: { + imageExpr: compileGetter('img'), + altExpr: compileGetter('alt'), + }, +}; + +describe('Rendering', () => { + it('should be rendered correctly', () => { + const container = document.createElement('div'); + // @ts-expect-error + render(, container); + + expect(container).toMatchSnapshot(); + }); + + it('should render content correctly', () => { + const container = document.createElement('div'); + // @ts-expect-error + render(, container); + + const fieldValue = container.querySelector('.dx-cardview-field-value'); + expect(fieldValue?.textContent).toEqual('devextreme'); + }); +}); + +describe('Card Header', () => { + it('should render the card header components correctly', () => { + const container = document.createElement('div'); + // @ts-expect-error + render(, container); + + const cardHeaderText = container.querySelector('.dx-toolbar-label .dx-toolbar-item-content > div'); + expect(cardHeaderText?.textContent).toBe('Card Header'); + + const checkbox = container.querySelectorAll('.dx-checkbox'); + expect(checkbox).toHaveLength(1); + + const editButton = container.querySelectorAll('.dx-icon-edit'); + expect(editButton).toHaveLength(1); + + const trashButton = container.querySelectorAll('.dx-icon-trash'); + expect(trashButton).toHaveLength(1); + }); +}); + +describe('Image', () => { + it('should render the image correctly', () => { + const container = document.createElement('div'); + // @ts-expect-error + render(, container); + + const image = container.querySelector('img'); + expect(image).not.toBeNull(); + }); +}); + +describe('Field Template', () => { + it('should render field template correctly', () => { + const container = document.createElement('div'); + // @ts-expect-error + render(, container); + + const fieldName = container.querySelector('.dx-cardview-field-name'); + const fieldValue = container.querySelector('.dx-cardview-field-value'); + + expect(fieldName?.textContent).toBe('Field:'); + expect(fieldValue?.textContent).toBe('devextreme'); + }); +}); 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 new file mode 100644 index 000000000000..99258197212c --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.tsx @@ -0,0 +1,207 @@ +/* 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'; +import { CardHeader } from './header'; + +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; + row: DataRow; +} + +export interface CardHoverEvent { + isHovered: boolean; + row: DataRow; +} + +export interface CardPreparedEvent { + instance: Card; +} + +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 + fieldTemplate?: any; + + hoverStateEnabled?: boolean; + + 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; + + onContextMenu?: (e: MouseEvent, card?: DataRow, cardIndex?: number) => void; + + selectCard?: (row: DataRow, options: SelectCardOptions) => void; +} + +export class Card extends Component { + private containerRef = createRef(); + + private fieldRefs: RefObject[] = []; + + render(): InfernoNode { + if (this.props.elementRef) { + this.containerRef = this.props.elementRef; + } + + this.fieldRefs = new Array(this.props.row.cells.length).fill(undefined).map(() => createRef()); + + const { + fieldTemplate: FieldTemplate = Field, + hoverStateEnabled, + cover, + row, + } = this.props; + + const className = combineClasses({ + [CLASSES.card]: true, + [CLASSES.cardHover]: !!hoverStateEnabled, + [CLASSES.selectCard]: !!row.isSelected, + }); + + const hasCover = cover?.imageExpr; + + const imageSrc = cover?.imageExpr?.(this.props.row.data); + const alt = cover?.altExpr?.(this.props.row.data); + + return ( +
+ + {hasCover && ( + + ) } +
+ {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 => { + const { onHoverChanged, row } = this.props; + + onHoverChanged?.({ isHovered: true, row }); + }; + + handleMouseLeave = (): void => { + const { onHoverChanged, row } = this.props; + + onHoverChanged?.({ isHovered: false, row }); + }; + + handleClick = (event: MouseEvent): void => { + 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/cover.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/cover.test.tsx new file mode 100644 index 000000000000..8936e11f1425 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/cover.test.tsx @@ -0,0 +1,33 @@ +import { describe, expect, it } from '@jest/globals'; +import { render } from 'inferno'; + +import { Cover } from './cover'; + +describe('Cover', () => { + it('should render the image correctly', () => { + const container = document.createElement('div'); + const props = { + imageSrc: 'https://www.devexpress.com/support/demos/i/demo-thumbs/aspnetcore-grid.png', + alt: 'Card Cover', + className: 'cover-image', + }; + + render(, container); + + const image = container.querySelector('img'); + expect(image).not.toBeNull(); + expect(image?.src).toBe(props.imageSrc); + expect(image?.alt).toBe(props.alt); + expect(image?.className).toContain(props.className); + }); + + describe('when there is no image', () => { + it('should render image thumbnail', () => { + const container = document.createElement('div'); + const props = {}; + + render(, container); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/cover.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/cover.tsx new file mode 100644 index 000000000000..eeba93408a65 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/cover.tsx @@ -0,0 +1,52 @@ +import messageLocalization from '@js/localization/message'; +import { combineClasses } from '@ts/core/utils/combine_classes'; +import { Component } from 'inferno'; + +export const CLASSES = { + cover: 'dx-card-cover', + image: 'dx-card-cover-image', + noImage: 'dx-card-cover-noimage', + noImageIcon: 'dx-icon dx-icon-imagethumbnail', +}; + +export interface CoverProps { + imageSrc?: string; + alt?: string; + template?: (src: string | undefined, alt: string | undefined, className: string) => JSX.Element; +} + +export class Cover extends Component { + render(): JSX.Element { + const { + imageSrc, alt, template, + } = this.props; + const src = imageSrc; + + if (template) { + return template(src, alt, CLASSES.image); + } + + const containerClasses = combineClasses({ + [CLASSES.cover]: true, + [CLASSES.noImage]: !src, + }); + + return ( +
+ {src && ( + {alt} + )} + {!src && ( +
+ )} +
+ ); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/field.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/field.tsx new file mode 100644 index 000000000000..fe8d14126372 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/field.tsx @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +import type { HighlightedTextItem } from '@ts/grids/new/grid_core/search/types'; +import type { RefObject } from 'inferno'; +import { Component, createRef } from 'inferno'; + +import { Caption } from './caption'; +import { ValueText } from './value_text'; + +export const CLASSES = { + field: 'dx-cardview-field', + fieldValue: 'dx-cardview-field-value', + overflowHint: 'dx-cardview-overflow-hint', +}; + +export interface FieldProps { + title: string | undefined; + text: string; + highlightedText: HighlightedTextItem[] | null; + alignment: 'right' | 'center' | 'left'; + wordWrapEnabled?: boolean; + cellHintEnabled?: boolean; + elementRef?: RefObject; + captionTemplate?: (title: string) => JSX.Element; + valueTemplate?: (value: unknown) => JSX.Element; + + onClick?: (e: MouseEvent) => void; + onDblClick?: (e: MouseEvent) => void; + onHoverChanged?: (hovered: boolean) => void; + onPrepared?: (element: HTMLElement) => void; +} + +export class Field extends Component { + private readonly containerRef: RefObject; + + constructor(props: FieldProps) { + super(props); + this.containerRef = this.props.elementRef || createRef(); + } + + componentDidMount(): void { + this.props.onPrepared?.(this.containerRef.current!); + } + + render(): JSX.Element { + return ( +
this.props.onHoverChanged?.(true)} + onMouseLeave={(): void => this.props.onHoverChanged?.(false)} + onClick={this.props.onClick} + onDblClick={this.props.onDblClick} + > + + +
+ ); + } +} 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 new file mode 100644 index 000000000000..7cf06f9421d6 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.test.tsx @@ -0,0 +1,87 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ + +import { describe, expect, it } from '@jest/globals'; +import { render } from 'inferno'; + +import { CardHeader, CLASSES } from './header'; + +describe('CardHeader', () => { + it('should render with default properties', () => { + const container = document.createElement('div'); + render(, container); + + // Verify the rendered element + const header = container.querySelector(`.${CLASSES.cardHeader}`); + expect(header).not.toBeNull(); + + // Verify the item text + const headerItem = container.querySelector('.dx-toolbar-item'); + expect(headerItem).not.toBeNull(); + expect(headerItem?.textContent).toBe('Test Header'); + }); + + it('should not render when visible is false', () => { + const container = document.createElement('div'); + render(, container); + + // Verify the header is not rendered + const header = container.querySelector(CLASSES.cardHeader); + expect(header).toBeNull(); + }); + + it('should render with caption from captionExpr', () => { + const container = document.createElement('div'); + render( + , + container, + ); + + // Verify the caption text + const captionItem = container.querySelector('.dx-toolbar-item'); + expect(captionItem).not.toBeNull(); + expect(captionItem?.textContent).toBe('Card Title'); + }); + + it('should render with a custom template', () => { + const container = document.createElement('div'); + const CustomTemplate = (items: any[]) => ( +
{items[0].text}
+ ); + + render( + , + container, + ); + + // Verify the custom template + const customHeader = container.querySelector('.custom-header'); + 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 new file mode 100644 index 000000000000..9da452c883d4 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.tsx @@ -0,0 +1,98 @@ +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; + cssClass?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options?: any; +} + +export interface CardHeaderProps { + items?: CardHeaderItem[]; + visible?: boolean; + 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, + items = [], + captionExpr, + template, + row, + } = this.props; + + if (!visible) { + return null; + } + + const checkBoxItem = this.getCheckBoxItem(); + + const captionItem: CardHeaderItem | null = captionExpr && row?.[captionExpr] + ? { location: 'before', text: row[captionExpr] } + : null; + + const finalItems = [checkBoxItem, captionItem, ...items] + .filter((item): item is CardHeaderItem => !!item); + + if (template) { + return template(finalItems); + } + + return ( +
+ +
+ ); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/value_text.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/value_text.test.tsx new file mode 100644 index 000000000000..3cb7a57f7129 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/value_text.test.tsx @@ -0,0 +1,71 @@ +import { + describe, expect, it, +} from '@jest/globals'; +import { render } from 'inferno'; + +import { ValueText } from './value_text'; + +describe('Content View', () => { + describe('ValueText', () => { + it('should set root classes', () => { + const container = document.createElement('div'); + + render( + , + container, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should add title attribute', () => { + const container = document.createElement('div'); + + render( + , + container, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render plain text if highlighted text is null', () => { + const container = document.createElement('div'); + + render( + , + container, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render highlighted text if passed', () => { + const container = document.createElement('div'); + + render( + , + container, + ); + + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/value_text.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/value_text.tsx new file mode 100644 index 000000000000..5e31e30ce4d4 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/value_text.tsx @@ -0,0 +1,49 @@ +import type { HighlightedTextItem } from '@ts/grids/new/grid_core/search/types'; + +export interface ValueTextProps { + text: string; + highlightedText: HighlightedTextItem[] | null; + valueTemplate?: (text: string) => JSX.Element; + wordWrapEnabled?: boolean; + alignment: 'right' | 'center' | 'left'; + cellHintEnabled?: boolean; +} + +const ROOT_CLASS = 'dx-cardview-field-value'; +const CLASS = { + root: ROOT_CLASS, + textPartHighlighted: `${ROOT_CLASS}__text-part--highlighted`, +}; + +export const ValueText = ({ + text, + highlightedText, + valueTemplate, + wordWrapEnabled, + alignment, + cellHintEnabled, +}: ValueTextProps): JSX.Element => { + if (valueTemplate && text) { + return valueTemplate(text); + } + + const classNames = [ + CLASS.root, + `${CLASS.root}--text-align-${alignment}`, + `${CLASS.root}--white-space-${wordWrapEnabled ? 'normal' : 'nowrap'}`, + ].join(' '); + + return ( + + { highlightedText + ? highlightedText.map(({ type, text: textPart }) => ( + {textPart} + )) + : text + } + + ); +}; 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 new file mode 100644 index 000000000000..2f573918c9ac --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/content.tsx @@ -0,0 +1,129 @@ +/* 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'; + +import { Card } from './card/card'; +import type { CardHeaderItem } from './card/header'; + +export interface ContentProps { + items: DataRow[]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fieldTemplate?: any; + + onRowHeightChange?: (value: number) => void; + + showContextMenu?: (e: MouseEvent, card?: DataRow, cardIndex?: number) => void; + + cardsPerRow?: number; + + needToHiddenCheckBoxes?: boolean; + + cardProps?: { + toolbar?: CardHeaderItem[]; + minWidth?: number; + maxWidth?: number; + }; +} + +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 { + if (typeof card.key === 'string' || typeof card.key === 'number') { + return card.key; + } + + return undefined; +} + +export class Content extends Component { + private readonly containerRef = createRef(); + + private cardRefs: RefObject[] = []; + + private getCssVariables(): Record { + const variables = {}; + + if (this.props.cardsPerRow) { + variables['--dx-cardview-cardsperrow'] = this.props.cardsPerRow; + } + + if (this.props.cardProps?.minWidth) { + variables['--dx-cardview-card-min-width'] = `${this.props.cardProps?.minWidth}px`; + } + + if (this.props.cardProps?.maxWidth) { + variables['--dx-cardview-card-max-width'] = `${this.props.cardProps?.maxWidth}px`; + } + + // @ts-expect-error + if (this.props.cardProps?.cover?.maxHeight) { + // @ts-expect-error + variables['--dx-cardview-card-cover-max-height'] = `${this.props.cardProps?.cover?.maxHeight}px`; + } + + // @ts-expect-error + if (this.props.cardProps?.cover?.ratio) { + // @ts-expect-error + variables['--dx-cardview-card-cover-ratio'] = `${this.props.cardProps?.cover?.ratio}`; + } + + return variables; + } + + render(): JSX.Element { + this.cardRefs = new Array(this.props.items.length).fill(undefined).map(() => createRef()); + const className = combineClasses({ + [CLASSES.content]: true, + [CLASSES.grid]: true, + [CLASSES.selectCheckBoxesHidden]: !!this.props.needToHiddenCheckBoxes, + }); + return ( +
+ {this.props.items.map((item, index) => ( + { + this.props.showContextMenu?.(e, item, index); + }} + /> + ))} +
+ ); + } + + updateSizesInfo(): void { + const firstCard = this.cardRefs[0]; + if (!firstCard) { + return; + } + const cardHeight = firstCard.current!.offsetHeight; + const gapHeight = parseFloat(getComputedStyle(this.containerRef.current!).rowGap); + const rowHeight = cardHeight + gapHeight; + this.props.onRowHeightChange?.(rowHeight); + } + + componentDidMount(): void { + this.updateSizesInfo(); + } + + componentDidUpdate(): void { + this.updateSizesInfo(); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content_view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content_view.tsx new file mode 100644 index 000000000000..ad6fc036c914 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content_view.tsx @@ -0,0 +1,21 @@ +import type { ContentViewProps as ContentViewBaseProps } from '@ts/grids/new/grid_core/content_view/content_view'; +import { ContentView as ContentViewBase } from '@ts/grids/new/grid_core/content_view/content_view'; +import type { InfernoNode } from 'inferno'; +import { Component } from 'inferno'; + +import type { ContentProps } from './content/content'; +import { Content } from './content/content'; + +export interface ContentViewProps extends ContentViewBaseProps { + contentProps: ContentProps; +} + +export class ContentView extends Component { + render(): InfernoNode { + return ( + + + + ); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/index.ts b/packages/devextreme/js/__internal/grids/new/card_view/content_view/index.ts new file mode 100644 index 000000000000..89edfeb2d9f6 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/index.ts @@ -0,0 +1,3 @@ +export * from './options'; +export * from './public_methods'; +export { ContentView as View } from './view'; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/options.ts b/packages/devextreme/js/__internal/grids/new/card_view/content_view/options.ts new file mode 100644 index 000000000000..2d2efcd32242 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/options.ts @@ -0,0 +1,22 @@ +import * as Base from '../../grid_core/content_view/options'; +import type { DataObject } from '../../grid_core/data_controller/types'; + +export interface Options extends Base.Options { + cardsPerRow?: number | 'auto'; + cardMinWidth?: number; + cardMaxWidth?: number; + cardCover?: { + imageExpr?: string | ((data: DataObject) => string); + altExpr?: string | ((data: DataObject) => string); + maxHeight?: number; + ratio?: string; + }; +} + +export const defaultOptions = { + cardsPerRow: 3, + cardCover: { + ratio: '1 / 1', + }, + ...Base.defaultOptions, +} satisfies Options; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/public_methods.ts b/packages/devextreme/js/__internal/grids/new/card_view/content_view/public_methods.ts new file mode 100644 index 000000000000..316555f210b8 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/public_methods.ts @@ -0,0 +1,40 @@ +/* eslint-disable spellcheck/spell-checker */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import type { DxElement } from '@js/core/element'; +import { getPublicElement } from '@js/core/element'; +import $ from '@js/core/renderer'; +import type { DataRow } from '@ts/grids/new/grid_core/columns_controller/types'; +import type { Constructor } from '@ts/grids/new/grid_core/types'; + +import * as Base from '../../grid_core/content_view/public_methods'; +import type { Key } from '../../grid_core/data_controller/types'; +import { ItemsController } from '../../grid_core/items_controller/items_controller'; +import type { CardViewBase } from '../widget'; +import * as cardModule from './content/card/card'; + +export function PublicMethods>(GridCore: T) { + return class CardViewWithContentView extends Base.PublicMethods(GridCore) { + public getCardElement(cardIndex: number): DxElement { + const card = $(this.element()).find(cardModule.CLASSES.card).eq(cardIndex); + + return getPublicElement(card); + } + + public getVisibleCards(): DataRow[] { + const itemsController = this.diContext.get(ItemsController); + return itemsController.items.unreactive_get(); + } + + public getCardIndexByKey(key: Key): number { + const itemsController = this.diContext.get(ItemsController); + const cards = itemsController.items.unreactive_get(); + + return cards.findIndex((card) => card.key === key); + } + + public getKeyByCardIndex(cardIndex: number): Key { + return this.getVisibleCards()[cardIndex]?.key; + } + }; +} 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/utils.test.ts b/packages/devextreme/js/__internal/grids/new/card_view/content_view/utils.test.ts new file mode 100644 index 000000000000..367c1d215476 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/utils.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from '@jest/globals'; + +import { factors } from './utils'; + +describe('factors', () => { + it('should return all factors of given number', () => { + expect(factors(1)).toEqual([1]); + expect(factors(2)).toEqual([1, 2]); + expect(factors(7)).toEqual([1, 7]); + expect(factors(6)).toEqual([1, 2, 3, 6]); + expect(factors(8)).toEqual([1, 2, 4, 8]); + expect(factors(12)).toEqual([1, 2, 3, 4, 6, 12]); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/utils.ts b/packages/devextreme/js/__internal/grids/new/card_view/content_view/utils.ts new file mode 100644 index 000000000000..1a23a1034c6a --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/utils.ts @@ -0,0 +1,9 @@ +export function factors(n: number): number[] { + const res: number[] = []; + for (let i = 1; i <= n; i += 1) { + if (n % i === 0) { + res.push(i); + } + } + return res; +} 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 new file mode 100644 index 000000000000..8b6292754ae0 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx @@ -0,0 +1,108 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +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 { + // @ts-expect-error + protected options: OptionsController; + + private readonly cardMinWidth = this.options.oneWay('cardMinWidth'); + + private readonly rowHeight = state(0); + + private readonly cardsPerRow = computed( + (width, cardMinWidth, pageSize, cardsPerRowProp) => { + if (cardsPerRowProp !== 'auto') { + return cardsPerRowProp; + } + + const result = factors(pageSize).reverse().find((cardsPerRow) => { + const cardWidth = (width - 6 * (cardsPerRow - 1)) / cardsPerRow; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return cardMinWidth! <= cardWidth; + }); + + return result ?? 1; + }, + [this.width, this.cardMinWidth, this.dataController.pageSize, this.options.oneWay('cardsPerRow')], + ); + + protected override component = ContentViewComponent; + + protected override getProps() { + return combined({ + ...this.getBaseProps(), + contentProps: combined({ + items: this.itemsController.items, + needToHiddenCheckBoxes: this.selectionController.needToHiddenCheckBoxes, + fieldTemplate: this.options.template('fieldTemplate'), + cardsPerRow: this.cardsPerRow, + onRowHeightChange: this.rowHeight.update.bind(this.rowHeight), + showContextMenu: this.showContextMenu.bind(this), + 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'), + onPrepared: this.options.action('onCardPrepared'), + cover: combined({ + imageExpr: computed( + (imageExpr) => this.processExpr(imageExpr), + [this.options.oneWay('cardCover.imageExpr')], + ), + altExpr: computed( + (altExpr) => this.processExpr(altExpr), + [this.options.oneWay('cardCover.altExpr')], + ), + maxHeight: this.options.oneWay('cardCover.maxHeight'), + ratio: this.options.oneWay('cardCover.ratio'), + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + toolbar: this.options.oneWay('cardHeader.items') as any, + selectCard: this.selectCard.bind(this), + }), + }), + }); + } + + private processExpr( + expr: T | ((data: DataObject) => T) | undefined, + ): ((data: DataObject) => T) | undefined { + if (!isDefined(expr)) { + return undefined; + } + // @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); + } + + private showContextMenu(e: MouseEvent, card?: DataRow, cardIndex?: number): void { + this.contextMenuController.show(e, 'content', { card, cardIndex }); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.mock.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.mock.ts new file mode 100644 index 000000000000..f35a967157d4 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.mock.ts @@ -0,0 +1,14 @@ +import type { Item as ContextMenuItem } from '@js/ui/context_menu'; + +import type { ContextInfo, ContextMenuTarget } from '.'; +import { ContextMenuController } from '.'; + +export class ContextMenuControllerMock extends ContextMenuController { + public override getItems( + view: ContextMenuTarget, + targetElement: Element, + contextInfo: ContextInfo = {}, + ): ContextMenuItem[] | undefined { + return super.getItems(view, targetElement, contextInfo); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.test.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.test.ts new file mode 100644 index 000000000000..be88221bf82c --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.test.ts @@ -0,0 +1,41 @@ +import { + describe, expect, it, jest, +} from '@jest/globals'; +import dxContextMenu from '@js/ui/context_menu'; + +import { ColumnsController } from '../../grid_core/columns_controller'; +import type { Options } from '../options'; +import { OptionsControllerMock } from '../options_controller.mock'; +import { ContextMenuControllerMock } from './controller.mock'; + +const setup = (options: Options) => { + const optionsController = new OptionsControllerMock(options); + const columnsController = new ColumnsController(optionsController); + const controller = new ContextMenuControllerMock(columnsController, optionsController); + + const container = document.createElement('div'); + // eslint-disable-next-line new-cap + const contextMenu = new dxContextMenu(container, { + onPositioning: controller.onPositioning, + }); + + // @ts-expect-error + controller.contextMenuRef = { current: contextMenu }; + + return { controller, contextMenu }; +}; + +describe('ContextMenu', () => { + describe('Controller', () => { + it('onContextMenuPreparing is called on getItems()', () => { + const onContextMenuPreparing = jest.fn(); + const { controller } = setup({ + onContextMenuPreparing, + }); + + controller.getItems('content', document.createElement('div')); + + expect(onContextMenuPreparing).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.ts new file mode 100644 index 000000000000..9d0502c4880f --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.ts @@ -0,0 +1,96 @@ +/* eslint-disable spellcheck/spell-checker */ +import type { Item as ContextMenuItem, ItemClickEvent } from '@js/ui/context_menu'; + +import { ColumnsController } from '../../grid_core/columns_controller/index'; +import type { Column, DataRow } from '../../grid_core/columns_controller/types'; +import { BaseContextMenuController } from '../../grid_core/context_menu/controller'; +import { OptionsController } from '../options_controller'; +import type { ContextMenuPreparingEvent, ContextMenuTarget } from '.'; + +export interface ContextInfo { + column?: Column; + columnIndex?: number; + card?: DataRow; + cardIndex?: number; +} + +export class ContextMenuController + extends BaseContextMenuController { + public static dependencies = [ColumnsController, OptionsController] as const; + + constructor( + private readonly columnsController: ColumnsController, + private readonly options: OptionsController, + ) { + super(); + } + + public override show( + event: MouseEvent, + view: ContextMenuTarget, + contextInfo: ContextInfo = {}, + ): void { + super.show(event, view, contextInfo); + } + + protected override getItems( + view: ContextMenuTarget, + targetElement: Element, + contextInfo: ContextInfo = {}, + ): ContextMenuItem[] | undefined { + const items: ContextMenuItem[] = []; + + if (view === 'headerPanel' && contextInfo.column) { + items.push(...this.getSortingItems(contextInfo.column)); + } + + // @ts-expect-error + const event: ContextMenuPreparingEvent = { + items: items.length > 0 ? items : undefined, + target: view, + targetElement: targetElement as HTMLElement, + columnIndex: undefined, + card: undefined, + cardIndex: undefined, + column: undefined, + + ...contextInfo, + }; + + const callback = this.options.action('onContextMenuPreparing').unreactive_get(); + + callback(event); + + return event.items; + } + + private getSortingItems(column: Column): ContextMenuItem[] { + const onItemClick = (e: ItemClickEvent): void => { + this.columnsController.columnOption(column, 'sortOrder', e.itemData?.value); + }; + + return [ + { + text: this.options.oneWay('sorting.ascendingText').unreactive_get(), + value: 'asc', + disabled: column.sortOrder === 'asc', + icon: 'sortuptext', + onItemClick, + }, + { + text: this.options.oneWay('sorting.descendingText').unreactive_get(), + value: 'desc', + disabled: column.sortOrder === 'desc', + icon: 'sortdowntext', + onItemClick, + }, + { + text: this.options.oneWay('sorting.clearText').unreactive_get(), + value: undefined, + disabled: !column.sortOrder, + icon: 'none', + onItemClick, + }, + ] as ContextMenuItem[]; + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/index.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/index.ts new file mode 100644 index 000000000000..5026a5a5d22c --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/index.ts @@ -0,0 +1,3 @@ +export * from './controller'; +export type { ContextMenuPreparingEvent, ContextMenuTarget, Options } from './options'; +export * from './view'; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/options.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/options.ts new file mode 100644 index 000000000000..10e54bc3d3eb --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/options.ts @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { EventInfo } from '@js/common/core/events'; +import type { DxElement } from '@js/core/element'; + +import type { Column, DataRow } from '../../grid_core/columns_controller/types'; + +export type ContextMenuTarget = 'toolbar' | 'headerPanel' | 'content'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export type ContextMenuPreparingEvent += EventInfo & { + items?: any[]; + + readonly target: ContextMenuTarget; + + readonly targetElement: DxElement; + + readonly columnIndex?: number; + + readonly column?: Column; + + readonly cardIndex?: number; + + readonly card?: TCardData; +}; + +export interface Options { + onContextMenuPreparing?: (args: ContextMenuPreparingEvent) => void; +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/view.integration.test.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/view.integration.test.ts new file mode 100644 index 000000000000..90ca60250a51 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/view.integration.test.ts @@ -0,0 +1,285 @@ +/* eslint-disable spellcheck/spell-checker */ +import { + afterEach, describe, expect, it, +} from '@jest/globals'; +import type { SortOrder } from '@js/common'; +import $ from '@js/core/renderer'; +import type ContextMenu from '@js/ui/context_menu'; +import type { PositioningEvent } from '@js/ui/context_menu'; +import type { Options as CardViewOptions } from '@ts/grids/new/card_view/options'; +import CardView from '@ts/grids/new/card_view/widget'; +import { rerender } from 'inferno'; + +import type { ContextMenuPreparingEvent } from '.'; + +const setup = (options: CardViewOptions = {}): CardView => { + const container = document.createElement('div'); + const { body } = document; + body.append(container); + + const cardView = new CardView(container, options); + + rerender(); + + return cardView; +}; + +const SELECTORS = { + cardView: '.dx-widget.dx-cardview', + contextMenu: '.dx-widget.dx-context-menu', + contextMenuContent: '.dx-context-menu.dx-overlay-content', + toolbar: '.dx-widget.dx-toolbar', + headerPanel: '.dx-cardview-headerpanel-content', + contentView: '.dx-cardview-content', + headerItem: '.dx-cardview-header-item', + card: '.dx-cardview-card', + menuItem: '.dx-menu-item', +}; + +const WIDGET_CONTAINER_CLASS = 'dx-cardview-container'; + +const rootQuerySelector = (selector: string) => document.body.querySelector(selector); + +const getContextMenuInstance = (): ContextMenu => { + const contextMenuElement = rootQuerySelector(SELECTORS.contextMenu); + + if (!contextMenuElement) { + throw new Error('ContextMenu element not found'); + } + + // @ts-expect-error + const contextMenu = $(contextMenuElement).dxContextMenu('instance') as ContextMenu; + + return contextMenu; +}; + +const getContextMenuElement = (): Element => { + const contextMenuElement = rootQuerySelector(SELECTORS.contextMenuContent); + + if (!contextMenuElement) { + throw new Error('ContextMenu content element not present in the DOM'); + } + + return contextMenuElement; +}; + +const openContextMenu = (cardView: CardView, selector: string): ContextMenuPreparingEvent => { + let itemsPreparingEvent: ContextMenuPreparingEvent | null = null; + + cardView.on('contextMenuPreparing', (e) => { + itemsPreparingEvent = e; + }); + + const eventElement = rootQuerySelector(selector); + + const contextMenuEvent = new MouseEvent('contextmenu', { + bubbles: true, + cancelable: true, + view: window, + }); + + eventElement?.dispatchEvent(contextMenuEvent); + + rerender(); + + if (itemsPreparingEvent === null) { + throw new Error('contextMenuPreparing event was not fired'); + } + + return itemsPreparingEvent; +}; + +describe('ContextMenu', () => { + describe('View', () => { + afterEach(() => { + const cardView = rootQuerySelector(SELECTORS.cardView); + // @ts-expect-error bad typed renderer + $(cardView ?? undefined as any)?.dxCardView('dispose'); + }); + + it('contextMenu.onPositioning event is correct', () => { + const cardView = setup({ columns: ['Column 1'] }); + const contextMenu = getContextMenuInstance(); + + expect(contextMenu.option('target')).toBe(undefined); + expect(contextMenu.option('showEvent')).toBe(undefined); + + let invokesCount = 0; + // eslint-disable-next-line @typescript-eslint/init-declarations + let positioningEvent: PositioningEvent | undefined; + + contextMenu.on('positioning', (e: PositioningEvent) => { + invokesCount += 1; + positioningEvent = e; + }); + + openContextMenu(cardView, SELECTORS.headerItem); + + expect(invokesCount).toEqual(1); + expect(positioningEvent?.event).toBeUndefined(); + }); + + it('contextMenu has class', () => { + const cardView = setup({ columns: ['Column 1'] }); + openContextMenu(cardView, SELECTORS.headerItem); + + const contextMenuElement = getContextMenuElement(); + + expect(contextMenuElement.classList).toContain(WIDGET_CONTAINER_CLASS); + }); + + it.each<{ + targetView: 'toolbar' | 'headerPanel' | 'content'; selector: string; + }>([ + { targetView: 'toolbar', selector: SELECTORS.toolbar }, + { targetView: 'headerPanel', selector: SELECTORS.headerPanel }, + { targetView: 'content', selector: SELECTORS.contentView }, + ])('onContextMenuPreparing fired on $targetView', ({ targetView, selector }) => { + const cardView = setup({ + columns: ['Column 1'], + toolbar: { items: [{ text: 'a' }] }, + }); + const event = openContextMenu(cardView, selector); + + expect(event.card).toBeUndefined(); + expect(event.cardIndex).toBeUndefined(); + expect(event.column).toBeUndefined(); + expect(event.columnIndex).toBeUndefined(); + + expect(event.component).toBe(cardView); + expect(event.element).toBe(cardView.element()); + expect(event.target).toEqual(targetView); + expect(event.targetElement).toBe(rootQuerySelector(selector)); + + expect(event.items).toBeUndefined(); + }); + + it('onContextMenuPreparing fired on headerItem', () => { + const cardView = setup({ columns: ['Column 1'] }); + const event = openContextMenu(cardView, SELECTORS.headerItem); + + expect(event.card).toBeUndefined(); + expect(event.cardIndex).toBeUndefined(); + expect(event.column).toBeDefined(); + expect(event.column?.name).toEqual('Column 1'); + expect(event.columnIndex).toEqual(0); + + expect(event.component).toBe(cardView); + expect(event.element).toBe(cardView.element()); + expect(event.target).toEqual('headerPanel'); + expect(event.targetElement).toBe(rootQuerySelector(SELECTORS.headerItem)); + + expect(event.items).toHaveLength(3); + expect(event.items).toMatchObject([ + { value: 'asc', icon: 'sortuptext' }, + { value: 'desc', icon: 'sortdowntext' }, + { value: undefined, icon: 'none' }, + ]); + }); + + it('onContextMenuPreparing fired on contentView card', () => { + const cardView = setup({ + columns: [{ dataField: 'test' }], + keyExpr: 'id', + dataSource: [{ id: 10, test: 'some value 1' }, { id: 11, test: 'some value 2' }], + }); + const event = openContextMenu(cardView, SELECTORS.card); + + expect(event.card).toMatchObject({ + key: 10, + index: 0, + data: { id: 10, test: 'some value 1' }, + cells: [{ value: 'some value 1' }], + }); + expect(event.cardIndex).toEqual(0); + + expect(event.column).toBeUndefined(); + expect(event.columnIndex).toBeUndefined(); + + expect(event.component).toBe(cardView); + expect(event.element).toBe(cardView.element()); + expect(event.target).toEqual('content'); + expect(event.targetElement).toBe(rootQuerySelector(SELECTORS.card)); + + expect(event.items).toBeUndefined(); + }); + + it('columns have customized context menu items sorting text', () => { + const cardView = setup({ + columns: [{ dataField: 'column1' }], + sorting: { + ascendingText: 'custom text 1', + descendingText: 'custom text 2', + clearText: 'custom text 3', + }, + }); + const event = openContextMenu(cardView, SELECTORS.headerItem); + + expect(event.items).toHaveLength(3); + expect(event.items).toMatchObject([ + { text: 'custom text 1' }, + { text: 'custom text 2' }, + { text: 'custom text 3' }, + ]); + }); + + it.each<{ sortOrder?: SortOrder }>([ + { sortOrder: 'asc' }, + { sortOrder: 'desc' }, + { sortOrder: undefined }, + ])('context menu items disabled state when column sortOrder: $sortOrder', ({ sortOrder }) => { + const cardView = setup({ columns: [{ dataField: 'column1', sortOrder }] }); + const event = openContextMenu(cardView, SELECTORS.headerItem); + + expect(event.items).toHaveLength(3); + + event.items?.forEach((item) => { + expect(item.disabled).toBe(sortOrder === item.value); + }); + }); + + it('items set in onContextMenuPreparing are displayed', () => { + const cardView = setup({ }); + + let itemClickFired = false; + + cardView.on('contextMenuPreparing', (e) => { + e.items = [{ + text: 'custom item', + onItemClick: () => { + itemClickFired = true; + }, + }]; + }); + + openContextMenu(cardView, SELECTORS.contentView); + + const contextMenu = getContextMenuInstance(); + const contextMenuElement = getContextMenuElement(); + const contextMenuItems = contextMenu.option('items'); + + expect(contextMenuItems).toHaveLength(1); + expect(contextMenuItems).toMatchObject([ + { text: 'custom item' }, + ]); + + const firstItemElement = contextMenuElement.querySelector(SELECTORS.menuItem) as HTMLElement; + firstItemElement.click(); + + expect(itemClickFired).toBeTruthy(); + }); + + it('onContextMenuPreparing event.column is correct when first column is invisible', () => { + const cardView = setup({ + columns: [ + { dataField: 'column1', visible: false }, + { dataField: 'column2' }, + ], + }); + const event = openContextMenu(cardView, `${SELECTORS.headerItem}:nth-of-type(2)`); + + expect(event.columnIndex).toEqual(1); + expect(event.column?.name).toEqual('column2'); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/view.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/view.ts new file mode 100644 index 000000000000..87e1f0b49f04 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/view.ts @@ -0,0 +1,12 @@ +import { BaseContextMenuView } from '../../grid_core/context_menu/view'; +import { ContextMenuController } from './controller'; + +export class ContextMenuView extends BaseContextMenuView { + public static dependencies = [ContextMenuController] as const; + + constructor( + protected readonly controller: ContextMenuController, + ) { + super(controller); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/di.test_utils.ts b/packages/devextreme/js/__internal/grids/new/card_view/di.test_utils.ts new file mode 100644 index 000000000000..7c23e989bbe7 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/di.test_utils.ts @@ -0,0 +1,20 @@ +/* eslint-disable spellcheck/spell-checker */ +import { DIContext } from '@ts/core/di'; +import { OptionsController as GridCoreOptionsController } from '@ts/grids/new/grid_core/options_controller/options_controller'; + +import { register } from './di'; +import type { Options } from './options'; +import { OptionsController } from './options_controller'; +import { OptionsControllerMock } from './options_controller.mock'; + +export function getContext(config: Options): DIContext { + const diContext = new DIContext(); + register(diContext); + + const options = new OptionsControllerMock(config); + diContext.registerInstance(OptionsController, options); + diContext.registerInstance(OptionsControllerMock, options); + diContext.registerInstance(GridCoreOptionsController, options); + + return diContext; +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/di.ts b/packages/devextreme/js/__internal/grids/new/card_view/di.ts new file mode 100644 index 000000000000..6429e45b1919 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/di.ts @@ -0,0 +1,20 @@ +/* eslint-disable spellcheck/spell-checker */ +import type { DIContext } from '@ts/core/di'; + +import { BaseContextMenuController } from '../grid_core/context_menu/controller'; +import { register as gridCoreDIRegister } from '../grid_core/di'; +import * as ContentViewModule from './content_view/index'; +import { ContextMenuController } from './context_menu/controller'; +import { ContextMenuView } from './context_menu/view'; +import { HeaderPanelView } from './header_panel/view'; + +export function register(diContext: DIContext): void { + gridCoreDIRegister(diContext); + + diContext.register(ContentViewModule.View); + diContext.register(HeaderPanelView); + diContext.register(ContextMenuView); + + diContext.register(ContextMenuController); + diContext.register(BaseContextMenuController, ContextMenuController); +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/item.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/item.test.tsx.snap new file mode 100644 index 000000000000..cf32532bde3f --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/item.test.tsx.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Item should render headerFilter icons if enabled 1`] = ` +
+
+ Column 1 + +
+
+`; + +exports[`Item should render sort icons 1`] = ` +
+
+ Column 1 +
+
+
+ +
+
+`; + +exports[`Item should use column caption as text 1`] = ` +
+
+ my column caption + +
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/options.test.ts.snap b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/options.test.ts.snap new file mode 100644 index 000000000000..efd060095c9e --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/options.test.ts.snap @@ -0,0 +1,156 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColumnProperties headerItemCssClass should override content of headerPanel item 1`] = ` +
+
+
+
+
+
+
+
+ Column 1 +
+
+
+ +
+
+ +
+
+`; + +exports[`Options headerPanel itemCssClass should add css class to headerPanel item 1`] = ` +
+
+
+
+
+
+ Column 1 +
+
+
+ +
+
+`; + +exports[`Options headerPanel visible when it is false should hide headerPanel 1`] = ` +
+ +
+`; + +exports[`Options headerPanel visible when it is true should show headerPanel 1`] = ` +
+
+
+
+
+
+
+
+ Column 1 +
+
+
+ +
+
+ +
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/column_sortable.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/column_sortable.tsx new file mode 100644 index 000000000000..0ba68ff7ae20 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/column_sortable.tsx @@ -0,0 +1,143 @@ +import $ from '@js/core/renderer'; +import type * as SortableTypes from '@js/ui/sortable_types'; +import type { ComponentType, InfernoNode } from 'inferno'; +import { Component, render } from 'inferno'; + +import type { Column } from '../../grid_core/columns_controller/types'; +import type { Props as SortableProps } from '../../grid_core/inferno_wrappers/sortable'; +import { Sortable } from '../../grid_core/inferno_wrappers/sortable'; + +export type Status = 'forbid' | 'show' | 'moving' | 'none'; + +const ALLOWED_DRAGGING_DISTANCE = 64; + +export interface Props extends Omit { + source: string; + + visibleColumns: Column[]; + + allowColumnReordering: boolean; + + onMove: (column: Column, toIndex: number, source: string) => void; + + dragTemplate?: ComponentType<{ column: Column; status?: Status }>; +} + +interface State { + status: Status; +} + +export class ColumnSortable extends Component { + status: Status = 'moving'; + + dragItemProps?: { + container: HTMLElement; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props: any; + }; + + private readonly onDragStart = (e: SortableTypes.DragStartEvent): void => { + const column = this.props.visibleColumns[e.fromIndex]; + + if (!column.allowReordering) { + e.cancel = true; + return; + } + + e.itemData = { + column, + source: this.props.source, + }; + }; + + private readonly onDragMove = (e: SortableTypes.DragMoveEvent): void => { + const containerCoords = $(e.element).get(0).getBoundingClientRect(); + const dragCoords = { + // @ts-expect-error + x: e.event.clientX, + // @ts-expect-error + y: e.event.clientY, + }; + + const yDistance = Math.min( + Math.abs(dragCoords.y - containerCoords.y), + Math.abs(dragCoords.y - containerCoords.y + containerCoords.height), + ); + + this.status = yDistance <= ALLOWED_DRAGGING_DISTANCE + ? 'moving' + : 'forbid'; + + this.renderDragTemplate(); + }; + + private readonly onDragChange = (e: SortableTypes.DragChangeEvent): void => { + if (this.status === 'forbid') { + e.cancel = true; + } + }; + + private readonly onMove = (e: SortableTypes.AddEvent | SortableTypes.ReorderEvent): void => { + this.props.onMove( + e.itemData.column, + e.toIndex, + e.itemData.source, + ); + }; + + // TODO: move all none-native approaches to sortable wrapper + private readonly renderDragTemplate = (): void => { + if (!this.dragItemProps) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const DragTemplate = this.props.dragTemplate!; + render( + , + this.dragItemProps.container, + ); + }; + + render(): InfernoNode { + if (!this.props.allowColumnReordering) { + return this.props.children; + } + + const { + source, + visibleColumns, + dragTemplate, + dropFeedbackMode, + ...restProps + } = this.props; + + const sortableDragTemplate = dragTemplate ? (e, container): void => { + this.dragItemProps = { + props: e, + // @ts-expect-error + container: $(container).get(0), + }; + this.renderDragTemplate(); + } : undefined; + + return ( + + {this.props.children} + + ); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx new file mode 100644 index 000000000000..6745b046506c --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx @@ -0,0 +1,93 @@ +import type { Column } from '@ts/grids/new/grid_core/columns_controller/types'; +import { Scrollable } from '@ts/grids/new/grid_core/inferno_wrappers/scrollable'; +import type { ComponentType } from 'inferno'; +import { Component } from 'inferno'; + +import { ColumnSortable } from './column_sortable'; +import { CLASSES as itemClasses, Item } from './item'; +import type { DraggingOptions } from './options'; + +export const CLASSES = { + headers: 'dx-cardview-headers', + content: 'dx-cardview-headerpanel-content', +}; + +export interface HeaderPanelProps { + columns: Column[]; + + onMove: (column: Column, toIndex: number) => void; + + allowColumnReordering: boolean; + + showSortIndexes: boolean; + + onSortClick: (column: Column, e: MouseEvent) => void; + + onFilterClick?: ( + element: Element, + column: Column, + onFilterCloseCallback?: () => void, + ) => void; + + itemTemplate?: ComponentType<{ column: Column }>; + + itemCssClass?: string; + + visible: boolean; + + draggingOptions?: DraggingOptions; + + showContextMenu: (e: MouseEvent, column?: Column, columnIndex?: number) => void; +} + +export class HeaderPanel extends Component { + public render(): JSX.Element { + if (!this.props.visible) { + return <>; + } + + return ( +
+ this.props.onMove?.(column, index)} + filter={`.${itemClasses.item}`} + dragTemplate={Item} + > + +
+ {this.props.columns.map((column, index) => ( + { this.props.onSortClick(column, e); }} + template={this.props.itemTemplate} + cssClass={this.props.itemCssClass} + onFilterClick={( + element: Element, + callback?: () => void, + ) => this.props.onFilterClick?.(element, column, callback)} + onContextMenu={(e) => { + this.props.showContextMenu(e, column, index); + }} + /> + ))} +
+
+
+
+ ); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/index.ts b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/index.ts new file mode 100644 index 000000000000..66aec0658fc2 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/index.ts @@ -0,0 +1,2 @@ +export * from './options'; +export { HeaderPanelView as View } from './view'; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.test.tsx new file mode 100644 index 000000000000..b99e6b13e936 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.test.tsx @@ -0,0 +1,52 @@ +import { describe, expect, it } from '@jest/globals'; +import { render } from 'inferno'; + +import { normalizeColumn } from '../../grid_core/columns_controller/columns_controller.mock'; +import type { ItemProps } from './item'; +import { Item } from './item'; + +const setup = (props: ItemProps) => { + const rootElement = document.createElement('div'); + render( + , + rootElement, + ); + + return rootElement; +}; + +describe('Item', () => { + it('should use column caption as text', () => { + const el = setup({ + column: normalizeColumn({ + dataField: 'my column data field', + caption: 'my column caption', + }), + }); + + expect(el).toMatchSnapshot(); + }); + + it('should render sort icons', () => { + const el = setup({ + column: normalizeColumn({ + dataField: 'column1', + sortIndex: 0, + sortOrder: 'asc', + }), + }); + + expect(el).toMatchSnapshot(); + }); + + it('should render headerFilter icons if enabled', () => { + const el = setup({ + column: normalizeColumn({ + dataField: 'column1', + allowHeaderFiltering: true, + }), + }); + + expect(el).toMatchSnapshot(); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx new file mode 100644 index 000000000000..d3fe17c14273 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx @@ -0,0 +1,152 @@ +import type { Column } from '@ts/grids/new/grid_core/columns_controller/types'; +import { MultipleKeyDownHandler } from '@ts/grids/new/grid_core/keyboard_navigation/index'; +import type { ComponentType } from 'inferno'; +import { Component, createRef } from 'inferno'; + +import type { Status } from './column_sortable'; + +export const CLASSES = { + item: 'dx-cardview-header-item', + button: 'dx-cardview-header-item-button', + sorting: { + container: 'dx-cardview-header-item-sorting', + order: 'dx-cardview-header-item-sorting-order', + }, + headerFilter: { + iconEmpty: 'dx-header-filter-icon', + iconFilled: 'dx-header-filter-icon--selected', + }, + icon: 'dx-icon', +}; + +// TODO: extract icons to separate component +const ICONS = { + // TODO: move to dx-icon once they are updated + forbid: ( + + + + ), + // TODO: move to dx-icon once they are updated + moving: ( + + + + ), + sortUp:
, + sortDown:
, +}; + +interface SortIconProps { + sortOrder: 'asc' | 'desc'; + sortIndex: number; + showSortIndex: boolean; +} + +function SortIcon(props: SortIconProps): JSX.Element { + return ( +
+ {props.sortOrder === 'asc' && ICONS.sortUp} + {props.sortOrder === 'desc' && ICONS.sortDown} + { + props.showSortIndex && ( +
+ {props.sortIndex} +
+ ) + } +
+ ); +} + +export interface ItemProps { + column: Column; + status?: Status; + showSortIndexes?: boolean; + template?: ComponentType<{ column: Column }>; + cssClass?: string; + onSortClick?: (e: MouseEvent) => void; + onFilterClick?: ( + element: Element, + onFilterCloseCallback?: () => void, + ) => void; + onContextMenu?: (e: MouseEvent) => void; +} + +export class Item extends Component { + private readonly containerRef = createRef(); + + private readonly keyboardHandler = new MultipleKeyDownHandler(['alt', 'arrowdown']); + + public render(): JSX.Element { + const Template = this.props.column.headerItemTemplate ?? this.props.template; + const cssClass = `${CLASSES.item} ${this.props.column.headerItemCssClass ?? ''} ${this.props.cssClass ?? ''}`; + + const { headerFilter } = this.props.column; + + const hasHeaderFilterValue = headerFilter?.filterType === 'exclude' + || !!headerFilter?.values?.length; + const headerFilterIconClass = [ + CLASSES.headerFilter.iconEmpty, + hasHeaderFilterValue ? CLASSES.headerFilter.iconFilled : '', + ].join(' '); + + return ( +
this.keyboardHandler.onKeyDownHandler( + event, + this.onFilterKeyPressHandler, + )} + onKeyUp={this.keyboardHandler.onKeyUpHandler} + onContextMenu={this.props.onContextMenu} + > + {this.props.status && ICONS[this.props.status]} + {Template &&