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..a661de8f19c0
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..2e77b00baff1
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..73aecd38b13b
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/functional.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/functional.ts
new file mode 100644
index 000000000000..8863daec461c
--- /dev/null
+++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/functional.ts
@@ -0,0 +1,542 @@
+import CardView from 'devextreme-testcafe-models/cardView';
+import url from '../../../helpers/getPageUrl';
+import { createWidget } from '../../../helpers/createWidget';
+
+// TODO: Write test with remote DataSource after remote grouping will be supported
+// TODO: Write integration test with filtering after filtering will be implemented
+fixture.disablePageReloads`HeaderFilter.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 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,
+}));
+
+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/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/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
index 4fd6fa46af65..207e2d3c6c45 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (fluent-blue-light).png 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
index 4f613c8823f1..4732bb1955de 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (generic-light).png 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
index 4f478508c862..a7a290562508 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (material-blue-light).png 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
index d6af8ac82aba..b467ddad97c7 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (fluent-blue-light).png 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
index 511a9bce6c8d..40677cd1deb1 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (generic-light).png 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
index b0a668aaaa01..36f50ebf8503 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (material-blue-light).png 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
index 8e1c851fe8ec..439f71cfa073 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (fluent-blue-light).png 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
index 38d2fdf29e97..a9582f15e672 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (generic-light).png 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
index 16402b3da9b3..e41e1af32e21 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (material-blue-light).png 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
index a6d10e08af1a..843050edd001 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (fluent-blue-light).png 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
index 21f645dd344f..77d0f2c70249 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (generic-light).png 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
index 9206a1df7499..eb295a312854 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (material-blue-light).png 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
index 705fad70cf7e..3a65bffcc094 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (fluent-blue-light).png 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
index 33113d4348ab..29b69c2441d9 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (generic-light).png 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
index 45e72019ab16..9373893c8f37 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (material-blue-light).png 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
index 8cc6569e1122..938535fdf824 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (fluent-blue-light).png 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
index 83b2bdba4331..59b651a64d21 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (generic-light).png 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
index 355e88d2ad9b..fe3c415f724e 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (material-blue-light).png 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
index 6479bd472336..fb1a0b4f0391 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (fluent-blue-light).png 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
index 4416f69f9bbf..aafec32238f9 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (generic-light).png 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
index fa9ee26a29f5..ef359f90b4ad 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (material-blue-light).png 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
index 399f125d7b89..266674f118b3 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (fluent-blue-light).png 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
index fb93cb267d4e..b3c96e7dc545 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (generic-light).png 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
index cdf598158061..d8c025dc1922 100644
Binary files a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (material-blue-light).png and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (material-blue-light).png differ
diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/_index.scss
index 479e1b081f12..cd81dfb11b1d 100644
--- a/packages/devextreme-scss/scss/widgets/base/cardView/_index.scss
+++ b/packages/devextreme-scss/scss/widgets/base/cardView/_index.scss
@@ -12,3 +12,7 @@
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/header_panel/item/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_index.scss
index 017cf710782e..8d4bc8f3b498 100644
--- 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
@@ -18,3 +18,16 @@
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
index 6e1d5c63136d..9e62897e1fa4 100644
--- 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
@@ -10,3 +10,6 @@ $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/cardView/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss
index af0215b6d533..8dc0532a29a2 100644
--- a/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss
+++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss
@@ -18,5 +18,8 @@ $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;
diff --git a/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss
index 2f6c2ca6b355..32bda5dd989e 100644
--- a/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss
+++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss
@@ -61,3 +61,4 @@ $cardview-card-content-cell-padding-horizontal: $cardview-fluent-paddings-12 !de
$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-themebuilder/tests/data/dependencies.ts b/packages/devextreme-themebuilder/tests/data/dependencies.ts
index 0297c842bab5..58ff9e9158d0 100644
--- a/packages/devextreme-themebuilder/tests/data/dependencies.ts
+++ b/packages/devextreme-themebuilder/tests/data/dependencies.ts
@@ -16,7 +16,7 @@ export const dependencies: FlatStylesDependencies = {
buttongroup: ['validation', 'button'],
dropdownbutton: ['validation', 'button', 'buttongroup', 'popup', 'loadindicator', 'loadpanel', 'scrollview', 'list'],
calendar: ['validation', 'button'],
- cardview: ['button', 'checkbox', 'list', 'loadindicator', 'loadpanel', 'numberbox', 'popup', 'scrollview', 'selectbox', 'sortable', 'textbox', 'toast', 'toolbar', 'validation'],
+ cardview: ['box', 'button', 'calendar', 'checkbox', '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/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/new/card_view/__snapshots__/widget.test.ts.snap b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap
index 7401846f1f62..23187a4262ea 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap
+++ b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap
@@ -93,6 +93,13 @@ exports[`common initial render should be successfull 1`] = `
+
+
+
@@ -161,6 +168,16 @@ exports[`common initial render should be successfull 1`] = `
class="dx-gridcore-error-row"
/>
+
+
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
index 0d3c465aaacc..cf32532bde3f 100644
--- 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
@@ -1,5 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`Item should render headerFilter icons if enabled 1`] = `
+
+
+
+`;
+
exports[`Item should render sort icons 1`] = `
+
`;
@@ -25,6 +42,9 @@ exports[`Item should use column caption as text 1`] = `
tabindex="0"
>
my column caption
+
`;
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
index 5604c582a316..b661f60e5489 100644
--- 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
@@ -23,6 +23,12 @@ export interface HeaderPanelProps {
onSortClick: (column: Column, e: MouseEvent) => void;
+ onFilterClick?: (
+ element: Element,
+ column: Column,
+ onFilterCloseCallback?: () => void,
+ ) => void;
+
itemTemplate?: ComponentType<{ column: Column }>;
itemCssClass?: string;
@@ -64,6 +70,10 @@ export class HeaderPanel extends Component {
onSortClick={(e): void => { this.props.onSortClick(column, e); }}
template={this.props.itemTemplate}
cssClass={this.props.itemCssClass}
+ onFilterClick={(
+ element: Element,
+ callback?: () => void,
+ ) => this.props.onFilterClick?.(element, column, callback)}
/>
))}
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
index c629938c7f5c..b99e6b13e936 100644
--- 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
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { describe, expect, it } from '@jest/globals';
import { render } from 'inferno';
@@ -39,4 +38,15 @@ describe('Item', () => {
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
index 3275d35cc66d..4579ed903000 100644
--- 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
@@ -1,6 +1,7 @@
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 } from 'inferno';
+import { Component, createRef } from 'inferno';
import type { Status } from './column_sortable';
@@ -69,18 +70,41 @@ export interface ItemProps {
template?: ComponentType<{ column: Column }>;
cssClass?: string;
onSortClick?: (e: MouseEvent) => void;
+ onFilterClick?: (
+ element: Element,
+ onFilterCloseCallback?: () => void,
+ ) => 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}
>
{this.props.status && ICONS[this.props.status]}
{Template && }
@@ -95,7 +119,32 @@ export class Item extends Component {
/>
)
}
+ { this.props.column?.allowHeaderFiltering && (
+
+ )}
);
}
+
+ private readonly onFilterClickHandler = (event: Event): void => {
+ event.stopPropagation();
+
+ if (this.containerRef.current) {
+ this.props.onFilterClick?.(this.containerRef.current);
+ }
+ };
+
+ private readonly onFilterKeyPressHandler = (event: KeyboardEvent): void => {
+ event.preventDefault();
+
+ if (this.containerRef.current) {
+ this.props.onFilterClick?.(this.containerRef.current, this.focusItem);
+ }
+ };
+
+ private readonly focusItem = (): void => {
+ this.containerRef?.current?.focus();
+ };
}
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/options.test.ts b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/options.test.ts
index f120f6d16eb2..b44acbcf95e8 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/options.test.ts
+++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/options.test.ts
@@ -2,10 +2,12 @@
import {
describe, expect, it, jest,
} from '@jest/globals';
+import { HeaderFilterController } from '@ts/grids/new/grid_core/filtering/header_filter/index';
import { rerender } from 'inferno';
import { ColumnsController } from '../../grid_core/columns_controller';
import { DataController } from '../../grid_core/data_controller';
+import { FilterController } from '../../grid_core/filtering';
import { Sortable } from '../../grid_core/inferno_wrappers/sortable';
import { SortingController } from '../../grid_core/sorting_controller';
import type { Options } from '../options';
@@ -17,13 +19,20 @@ const setup = (options: Options) => {
rootElement.classList.add('test-container');
const optionsController = new OptionsControllerMock(options);
+ const filterController = new FilterController(optionsController);
const columnsController = new ColumnsController(optionsController);
const sortingController = new SortingController(optionsController, columnsController);
- const dataController = new DataController(optionsController, sortingController);
+ const dataController = new DataController(optionsController, sortingController, filterController);
+ const headerFilterController = new HeaderFilterController(
+ optionsController,
+ dataController,
+ columnsController,
+ );
const headerPanelView = new HeaderPanelView(
sortingController,
columnsController,
optionsController,
+ headerFilterController,
);
headerPanelView.render(rootElement);
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/view.tsx
index 4fe2977bd2a4..b6aab4e32193 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/view.tsx
+++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/view.tsx
@@ -3,6 +3,7 @@ import type { SubsGets } from '@ts/core/reactive/index';
import { combined, computed } from '@ts/core/reactive/index';
import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/columns_controller';
import { View } from '@ts/grids/new/grid_core/core/view';
+import { HeaderFilterController } from '@ts/grids/new/grid_core/filtering/header_filter/index';
import type { Column } from '../../grid_core/columns_controller/types';
import { SortingController } from '../../grid_core/sorting_controller/sorting_controller';
@@ -17,12 +18,14 @@ export class HeaderPanelView extends View {
SortingController,
ColumnsController,
OptionsController,
+ HeaderFilterController,
] as const;
constructor(
private readonly sortingController: SortingController,
private readonly columnsController: ColumnsController,
private readonly options: OptionsController,
+ private readonly headerFilterController: HeaderFilterController,
) {
super();
}
@@ -40,6 +43,7 @@ export class HeaderPanelView extends View {
onSortClick: this.onSortClick.bind(this),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
itemTemplate: this.options.template('headerPanel.itemTemplate') as any,
+ onFilterClick: this.onFilterClick.bind(this),
itemCssClass: this.options.oneWay('headerPanel.itemCssClass'),
visible: this.options.oneWay('headerPanel.visible'),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -48,7 +52,7 @@ export class HeaderPanelView extends View {
}
public onRemove(column: Column): void {
- this.columnsController.columnOption(column, 'visible', !column.visible);
+ this.columnsController.columnOption(column, 'visible', false);
}
public onMove(column: Column, toIndex: number): void {
@@ -71,4 +75,12 @@ export class HeaderPanelView extends View {
throw new Error('Unsupported sorting state');
}
}
+
+ private onFilterClick(
+ element: Element,
+ column: Column,
+ onFilterCloseCallback?: () => void,
+ ): void {
+ this.headerFilterController.openPopup(element, column, onFilterCloseCallback);
+ }
}
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx
index 83adf230b4bd..05dad093833c 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx
+++ b/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx
@@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { combined } from '@ts/core/reactive/index';
import { View } from '@ts/grids/new/grid_core/core/view';
+import { FilterPanelView } from '@ts/grids/new/grid_core/filtering/filter_panel/view';
+import { HeaderFilterPopupView } from '@ts/grids/new/grid_core/filtering/header_filter/index';
import { PagerView } from '@ts/grids/new/grid_core/pager/view';
import { ToolbarView } from '@ts/grids/new/grid_core/toolbar/view';
import type { ComponentType, RefObject } from 'inferno';
@@ -21,12 +23,14 @@ interface MainViewProps {
Content: ComponentType;
Pager: ComponentType;
HeaderPanel: ComponentType;
+ HeaderFilterPopup: ComponentType;
+ FilterPanel: ComponentType;
config: Config;
rootElementRef: RefObject;
}
function MainViewComponent({
- Toolbar, Content, Pager, HeaderPanel, config, rootElementRef,
+ Toolbar, Content, Pager, HeaderPanel, HeaderFilterPopup, FilterPanel, config, rootElementRef,
}: MainViewProps): JSX.Element {
return (<>
@@ -36,7 +40,9 @@ function MainViewComponent({
>
+
+
{/*
Pager, as renovated component, has strange disposing.
@@ -56,7 +62,13 @@ export class MainView extends View
{
protected override component = MainViewComponent;
public static dependencies = [
- ContentView, PagerView, ToolbarView, HeaderPanelView, OptionsController,
+ ContentView,
+ PagerView,
+ ToolbarView,
+ HeaderPanelView,
+ HeaderFilterPopupView,
+ FilterPanelView,
+ OptionsController,
] as const;
constructor(
@@ -64,6 +76,8 @@ export class MainView extends View {
private readonly pager: PagerView,
private readonly toolbar: ToolbarView,
private readonly headerPanel: HeaderPanelView,
+ private readonly headerFilterPopup: HeaderFilterPopupView,
+ private readonly filterPanel: FilterPanelView,
private readonly options: OptionsController,
) {
super();
@@ -77,6 +91,8 @@ export class MainView extends View {
Content: this.content.asInferno(),
Pager: this.pager.asInferno(),
HeaderPanel: this.headerPanel.asInferno(),
+ HeaderFilterPopup: this.headerFilterPopup.asInferno(),
+ FilterPanel: this.filterPanel.asInferno(),
config: combined({
rtlEnabled: this.options.oneWay('rtlEnabled'),
disabled: this.options.oneWay('disabled'),
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/columns_controller.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/columns_controller.test.ts.snap
index 0c85ca710890..e71a7eea318e 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/columns_controller.test.ts.snap
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/columns_controller.test.ts.snap
@@ -4,6 +4,8 @@ exports[`ColumnsController columns should contain processed column configs 1`] =
[
{
"alignment": "left",
+ "allowFiltering": true,
+ "allowHeaderFiltering": false,
"allowReordering": true,
"allowSorting": true,
"calculateCellValue": [Function],
@@ -12,6 +14,17 @@ exports[`ColumnsController columns should contain processed column configs 1`] =
"dataField": "a",
"dataType": "string",
"falseText": "false",
+ "headerFilter": {
+ "allowSelectAll": true,
+ "height": 325,
+ "search": {
+ "editorOptions": {},
+ "enabled": false,
+ "mode": "contains",
+ "timeout": 500,
+ },
+ "width": 252,
+ },
"headerItemTemplate": undefined,
"name": "a",
"trueText": "true",
@@ -20,6 +33,8 @@ exports[`ColumnsController columns should contain processed column configs 1`] =
},
{
"alignment": "left",
+ "allowFiltering": true,
+ "allowHeaderFiltering": false,
"allowReordering": true,
"allowSorting": true,
"calculateCellValue": [Function],
@@ -28,6 +43,17 @@ exports[`ColumnsController columns should contain processed column configs 1`] =
"dataField": "b",
"dataType": "string",
"falseText": "false",
+ "headerFilter": {
+ "allowSelectAll": true,
+ "height": 325,
+ "search": {
+ "editorOptions": {},
+ "enabled": false,
+ "mode": "contains",
+ "timeout": 500,
+ },
+ "width": 252,
+ },
"headerItemTemplate": undefined,
"name": "b",
"trueText": "true",
@@ -36,6 +62,8 @@ exports[`ColumnsController columns should contain processed column configs 1`] =
},
{
"alignment": "left",
+ "allowFiltering": true,
+ "allowHeaderFiltering": false,
"allowReordering": true,
"allowSorting": true,
"calculateCellValue": [Function],
@@ -44,6 +72,17 @@ exports[`ColumnsController columns should contain processed column configs 1`] =
"dataField": "c",
"dataType": "string",
"falseText": "false",
+ "headerFilter": {
+ "allowSelectAll": true,
+ "height": 325,
+ "search": {
+ "editorOptions": {},
+ "enabled": false,
+ "mode": "contains",
+ "timeout": 500,
+ },
+ "width": 252,
+ },
"headerItemTemplate": undefined,
"name": "c",
"trueText": "true",
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/options.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/options.test.ts.snap
index 623ad72290e8..edd6e47bde76 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/options.test.ts.snap
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/options.test.ts.snap
@@ -4,6 +4,8 @@ exports[`Options columns when given as object should be normalized 1`] = `
[
{
"alignment": "left",
+ "allowFiltering": true,
+ "allowHeaderFiltering": false,
"allowReordering": true,
"allowSorting": true,
"calculateCellValue": [Function],
@@ -12,6 +14,17 @@ exports[`Options columns when given as object should be normalized 1`] = `
"dataField": "a",
"dataType": "string",
"falseText": "false",
+ "headerFilter": {
+ "allowSelectAll": true,
+ "height": 325,
+ "search": {
+ "editorOptions": {},
+ "enabled": false,
+ "mode": "contains",
+ "timeout": 500,
+ },
+ "width": 252,
+ },
"headerItemTemplate": undefined,
"name": "a",
"trueText": "true",
@@ -20,6 +33,8 @@ exports[`Options columns when given as object should be normalized 1`] = `
},
{
"alignment": "left",
+ "allowFiltering": true,
+ "allowHeaderFiltering": false,
"allowReordering": true,
"allowSorting": true,
"calculateCellValue": [Function],
@@ -28,6 +43,17 @@ exports[`Options columns when given as object should be normalized 1`] = `
"dataField": "b",
"dataType": "string",
"falseText": "false",
+ "headerFilter": {
+ "allowSelectAll": true,
+ "height": 325,
+ "search": {
+ "editorOptions": {},
+ "enabled": false,
+ "mode": "contains",
+ "timeout": 500,
+ },
+ "width": 252,
+ },
"headerItemTemplate": undefined,
"name": "b",
"trueText": "true",
@@ -36,6 +62,8 @@ exports[`Options columns when given as object should be normalized 1`] = `
},
{
"alignment": "left",
+ "allowFiltering": true,
+ "allowHeaderFiltering": false,
"allowReordering": true,
"allowSorting": true,
"calculateCellValue": [Function],
@@ -44,6 +72,17 @@ exports[`Options columns when given as object should be normalized 1`] = `
"dataField": "c",
"dataType": "string",
"falseText": "false",
+ "headerFilter": {
+ "allowSelectAll": true,
+ "height": 325,
+ "search": {
+ "editorOptions": {},
+ "enabled": false,
+ "mode": "contains",
+ "timeout": 500,
+ },
+ "width": 252,
+ },
"headerItemTemplate": undefined,
"name": "c",
"trueText": "true",
@@ -57,6 +96,8 @@ exports[`Options columns when given as string should be normalized 1`] = `
[
{
"alignment": "left",
+ "allowFiltering": true,
+ "allowHeaderFiltering": false,
"allowReordering": true,
"allowSorting": true,
"calculateCellValue": [Function],
@@ -65,6 +106,17 @@ exports[`Options columns when given as string should be normalized 1`] = `
"dataField": "a",
"dataType": "string",
"falseText": "false",
+ "headerFilter": {
+ "allowSelectAll": true,
+ "height": 325,
+ "search": {
+ "editorOptions": {},
+ "enabled": false,
+ "mode": "contains",
+ "timeout": 500,
+ },
+ "width": 252,
+ },
"headerItemTemplate": undefined,
"name": "a",
"trueText": "true",
@@ -73,6 +125,8 @@ exports[`Options columns when given as string should be normalized 1`] = `
},
{
"alignment": "left",
+ "allowFiltering": true,
+ "allowHeaderFiltering": false,
"allowReordering": true,
"allowSorting": true,
"calculateCellValue": [Function],
@@ -81,6 +135,17 @@ exports[`Options columns when given as string should be normalized 1`] = `
"dataField": "b",
"dataType": "string",
"falseText": "false",
+ "headerFilter": {
+ "allowSelectAll": true,
+ "height": 325,
+ "search": {
+ "editorOptions": {},
+ "enabled": false,
+ "mode": "contains",
+ "timeout": 500,
+ },
+ "width": 252,
+ },
"headerItemTemplate": undefined,
"name": "b",
"trueText": "true",
@@ -89,6 +154,8 @@ exports[`Options columns when given as string should be normalized 1`] = `
},
{
"alignment": "left",
+ "allowFiltering": true,
+ "allowHeaderFiltering": false,
"allowReordering": true,
"allowSorting": true,
"calculateCellValue": [Function],
@@ -97,6 +164,17 @@ exports[`Options columns when given as string should be normalized 1`] = `
"dataField": "c",
"dataType": "string",
"falseText": "false",
+ "headerFilter": {
+ "allowSelectAll": true,
+ "height": 325,
+ "search": {
+ "editorOptions": {},
+ "enabled": false,
+ "mode": "contains",
+ "timeout": 500,
+ },
+ "width": 252,
+ },
"headerItemTemplate": undefined,
"name": "c",
"trueText": "true",
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts
index 11ecbf4616c9..7cb4d3339923 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts
@@ -3,6 +3,7 @@ import { describe, expect, it } from '@jest/globals';
import { SearchController } from '@ts/grids/new/grid_core/search';
import { DataController } from '../data_controller';
+import { FilterController } from '../filtering';
import { ItemsController } from '../items_controller/items_controller';
import type { Options } from '../options';
import { OptionsControllerMock } from '../options_controller/options_controller.mock';
@@ -11,9 +12,10 @@ import { ColumnsController } from './columns_controller';
const setup = (config: Options = {}) => {
const options = new OptionsControllerMock(config);
+ const filterController = new FilterController(options);
const columnsController = new ColumnsController(options);
const sortingController = new SortingController(options, columnsController);
- const dataController = new DataController(options, sortingController);
+ const dataController = new DataController(options, sortingController, filterController);
const searchController = new SearchController(options);
const itemsController = new ItemsController(
dataController,
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts
index 792ce463ca99..5aaca2d5a42e 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts
@@ -3,6 +3,8 @@ import type { Subscribable, SubsGets, SubsGetsUpd } from '@ts/core/reactive/inde
import {
computed, interruptableComputed,
} from '@ts/core/reactive/index';
+import type { HeaderFilterRootOptions } from '@ts/grids/new/grid_core/filtering/header_filter/index';
+import headerFilterUtils from '@ts/grids/new/grid_core/filtering/header_filter/utils';
import { OptionsController } from '../options_controller/options_controller';
import type { ColumnProperties, ColumnSettings, PreNormalizedColumn } from './options';
@@ -14,6 +16,8 @@ import {
export class ColumnsController {
private readonly columnsConfiguration: Subscribable;
+ private readonly headerFilterConfiguration: Subscribable;
+
private readonly columnsSettings: SubsGetsUpd;
public readonly columns: SubsGets;
@@ -30,6 +34,7 @@ export class ColumnsController {
private readonly options: OptionsController,
) {
this.columnsConfiguration = this.options.oneWay('columns');
+ this.headerFilterConfiguration = this.options.oneWay('headerFilter');
this.columnsSettings = interruptableComputed(
(columnsConfiguration) => preNormalizeColumns(columnsConfiguration ?? []),
@@ -39,12 +44,17 @@ export class ColumnsController {
);
this.columns = computed(
- (columnsSettings) => normalizeColumns(
+ (
+ columnsSettings,
+ headerFilterRootOptions,
+ ) => normalizeColumns(
columnsSettings ?? [],
this.options.normalizeTemplate.bind(this.options),
- ),
+ ).map((column) => headerFilterUtils
+ .mergeColumnHeaderFilterOptions(column, headerFilterRootOptions)),
[
this.columnsSettings,
+ this.headerFilterConfiguration,
],
);
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/compatibility.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/compatibility.ts
new file mode 100644
index 000000000000..678def3252cc
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/compatibility.ts
@@ -0,0 +1,19 @@
+/* eslint-disable spellcheck/spell-checker */
+import { ColumnsController } from './columns_controller';
+import type { Column } from './types';
+
+export class CompatibilityColumnsController {
+ public static dependencies = [ColumnsController] as const;
+
+ constructor(
+ private readonly realColumnsController: ColumnsController,
+ ) {}
+
+ public getColumns(): Column[] {
+ return this.realColumnsController.columns.unreactive_get();
+ }
+
+ public getFilteringColumns(): Column[] {
+ return this.realColumnsController.columns.unreactive_get();
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/index.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/index.ts
index 45601c054834..3308e378b506 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/index.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/index.ts
@@ -1,3 +1,4 @@
export { ColumnsController } from './columns_controller';
+export { CompatibilityColumnsController } from './compatibility';
export { defaultOptions, type Options } from './options';
export { PublicMethods } from './public_methods';
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/options.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/options.test.ts
index d7d72a8768ee..a1c0b94c14c0 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/options.test.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/options.test.ts
@@ -3,6 +3,7 @@ import { describe, expect, it } from '@jest/globals';
import { SearchController } from '@ts/grids/new/grid_core/search';
import { DataController } from '../data_controller';
+import { FilterController } from '../filtering';
import { ItemsController } from '../items_controller/items_controller';
import type { Options } from '../options';
import { OptionsControllerMock } from '../options_controller/options_controller.mock';
@@ -13,9 +14,10 @@ const setup = (config: Options) => {
const options = new OptionsControllerMock(config);
const columnsController = new ColumnsController(options);
+ const filterController = new FilterController(options);
const sortingController = new SortingController(options, columnsController);
const searchController = new SearchController(options);
- const dataController = new DataController(options, sortingController);
+ const dataController = new DataController(options, sortingController, filterController);
const itemsController = new ItemsController(
dataController,
columnsController,
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/options.ts
index fe357869502b..72796d7fc786 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/options.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/options.ts
@@ -32,6 +32,8 @@ export const defaultColumnProperties = {
visible: true,
allowReordering: true,
allowSorting: true,
+ allowFiltering: true,
+ allowHeaderFiltering: true,
trueText: messageLocalization.format('dxDataGrid-trueText'),
falseText: messageLocalization.format('dxDataGrid-falseText'),
} satisfies Partial;
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/types.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/types.ts
index bde839e6352f..bc0cf045db20 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/types.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/types.ts
@@ -1,5 +1,6 @@
import type { Format, SortOrder } from '@js/common';
import type { ColumnBase } from '@js/common/grids';
+import type { HeaderFilterColumnOptions } from '@ts/grids/new/grid_core/filtering/header_filter/index';
import type { ComponentType } from 'inferno';
import type { DataObject } from '../data_controller/types';
@@ -11,6 +12,8 @@ type InheritedColumnProps =
| 'visible'
| 'visibleIndex'
| 'allowReordering'
+ | 'allowFiltering'
+ | 'allowHeaderFiltering'
| 'trueText'
| 'falseText'
| 'caption';
@@ -46,6 +49,9 @@ export type Column = Pick, InheritedColumnProps> & {
headerItemTemplate?: ComponentType<{ column: Column }>;
headerItemCssClass?: string;
+
+ // header filter options for specific column.
+ headerFilter?: HeaderFilterColumnOptions;
};
export type VisibleColumn = Column & { visible: true };
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/const.ts b/packages/devextreme/js/__internal/grids/new/grid_core/const.ts
new file mode 100644
index 000000000000..857cafe3e905
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/const.ts
@@ -0,0 +1,3 @@
+export const CLASSES = {
+ excludeFlexBox: 'dx-cardview-exclude-flexbox',
+};
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/compatibility.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/compatibility.ts
new file mode 100644
index 000000000000..24cae989e55a
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/compatibility.ts
@@ -0,0 +1,27 @@
+import createCallback from '@js/core/utils/callbacks';
+import type DataSource from '@js/data/data_source';
+import { effect } from '@ts/core/reactive/index';
+
+import { DataController } from './data_controller';
+
+export class CompatibilityDataController {
+ public dataSourceChanged = createCallback();
+
+ public static dependencies = [DataController] as const;
+
+ constructor(
+ private readonly realDataController: DataController,
+ ) {
+ effect(
+ (dataSource) => {
+ this.dataSourceChanged.fire(dataSource);
+ },
+ [this.realDataController.dataSource],
+ );
+ }
+
+ public dataSource(): DataSource {
+ // eslint-disable-next-line spellcheck/spell-checker
+ return this.realDataController.dataSource.unreactive_get();
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts
index ba507572a7f3..b3a2682450b0 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts
@@ -8,6 +8,7 @@ import {
} from '@ts/core/reactive/index';
import { createPromise } from '@ts/core/utils/promise';
+import { FilterController } from '../filtering/filter_controller';
import { OptionsController } from '../options_controller/options_controller';
import { SortingController } from '../sorting_controller/sorting_controller';
import type { DataObject, Key } from './types';
@@ -77,11 +78,12 @@ export class DataController {
[this.normalizedRemoteOptions],
);
- public static dependencies = [OptionsController, SortingController] as const;
+ public static dependencies = [OptionsController, SortingController, FilterController] as const;
constructor(
private readonly options: OptionsController,
private readonly sortingController: SortingController,
+ private readonly filterController: FilterController,
) {
effect(
(dataSource) => {
@@ -159,7 +161,7 @@ export class DataController {
);
effect(
- (dataSource, pageIndex, pageSize, pagingEnabled, sortParameters) => {
+ (dataSource, pageIndex, pageSize, displayFilter, pagingEnabled, sortParameters) => {
let someParamChanged = false;
if (dataSource.pageIndex() !== pageIndex) {
dataSource.pageIndex(pageIndex);
@@ -174,6 +176,10 @@ export class DataController {
dataSource.requireTotalCount(true);
someParamChanged ||= true;
}
+ if (dataSource.filter() !== displayFilter) {
+ dataSource.filter(displayFilter);
+ someParamChanged ||= true;
+ }
if (dataSource.paginate() !== pagingEnabled) {
dataSource.paginate(pagingEnabled);
someParamChanged ||= true;
@@ -192,6 +198,7 @@ export class DataController {
this.dataSource,
this.pageIndex,
this.pageSize,
+ this.filterController.displayFilter,
this.pagingEnabled,
this.sortingController.sortParameters,
],
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/index.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/index.ts
index 11e3ebad75f5..9f8f8f624380 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/index.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/index.ts
@@ -1,3 +1,4 @@
+export { CompatibilityDataController } from './compatibility';
export { DataController } from './data_controller';
export { defaultOptions, type Options } from './options';
export { PublicMethods } from './public_methods';
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/options.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/options.test.ts
index 812f766debb2..abbc6756a997 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/options.test.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/options.test.ts
@@ -10,6 +10,7 @@ import { logger } from '@ts/core/utils/m_console';
import ArrayStore from '@ts/data/m_array_store';
import { ColumnsController } from '../columns_controller';
+import { FilterController } from '../filtering';
import type { Options } from '../options';
import { OptionsControllerMock } from '../options_controller/options_controller.mock';
import { SortingController } from '../sorting_controller';
@@ -24,9 +25,10 @@ afterAll(() => {
const setup = (options: Options) => {
const optionsController = new OptionsControllerMock(options);
+ const filterController = new FilterController(optionsController);
const columnsController = new ColumnsController(optionsController);
const sortingController = new SortingController(optionsController, columnsController);
- const dataController = new DataController(optionsController, sortingController);
+ const dataController = new DataController(optionsController, sortingController, filterController);
return {
optionsController,
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.test.ts
index fa8abb15b963..deb1e09c45ce 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.test.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.test.ts
@@ -5,6 +5,7 @@ import {
import ArrayStore from '@ts/data/m_array_store';
import { ColumnsController } from '../columns_controller';
+import { FilterController } from '../filtering';
import type { Options } from '../options';
import { OptionsControllerMock } from '../options_controller/options_controller.mock';
import { SortingController } from '../sorting_controller';
@@ -13,9 +14,10 @@ import { PublicMethods } from './public_methods';
const setup = (options: Options) => {
const optionsController = new OptionsControllerMock(options);
+ const filterController = new FilterController(optionsController);
const columnsController = new ColumnsController(optionsController);
const sortingController = new SortingController(optionsController, columnsController);
- const dataController = new DataController(optionsController, sortingController);
+ const dataController = new DataController(optionsController, sortingController, filterController);
// @ts-expect-error
const gridCore = new (PublicMethods(class {
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_controller.ts
new file mode 100644
index 000000000000..dc6ed10d5b05
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_controller.ts
@@ -0,0 +1,24 @@
+import { computed } from '@ts/core/reactive/index';
+
+import { OptionsController } from '../options_controller/options_controller';
+
+export class FilterController {
+ public readonly filter = this.options.twoWay('filterValue');
+
+ public readonly filterEnabled = this.options.twoWay('filterPanel.filterEnabled');
+
+ public static dependencies = [OptionsController] as const;
+
+ public readonly displayFilter = computed(
+ (filter, filterEnabled) => (filterEnabled ? filter : null),
+ [this.filter, this.filterEnabled],
+ );
+
+ constructor(
+ private readonly options: OptionsController,
+ ) { }
+
+ public clearFilter(): void {
+ this.filter.update(null);
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/filter_panel.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/filter_panel.tsx
new file mode 100644
index 000000000000..a17ebb992225
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/filter_panel.tsx
@@ -0,0 +1,45 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import type { FilterPanel } from '@js/common/grids';
+import $ from '@js/core/renderer';
+import type { Properties as FilterBuilderProperties } from '@js/ui/filter_builder';
+import type { Properties as PopupProperties } from '@js/ui/popup';
+import type { FilterBuilderView as OldFilterBuilderView } from '@ts/grids/grid_core/filter/m_filter_builder';
+import type { FilterPanelView as OldFilterPanelView } from '@ts/grids/grid_core/filter/m_filter_panel';
+import { Component, createRef } from 'inferno';
+
+import { CLASSES } from '../../const';
+
+export interface FilterPanelProps {
+ oldFilterPanelView: OldFilterPanelView;
+ oldFilterBuilderView: OldFilterBuilderView;
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ filterValue?: any;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ filterPanel?: FilterPanel;
+ filterBuilder?: FilterBuilderProperties;
+ filterBuilderPopup?: PopupProperties;
+}
+
+export class FilterPanelComponent extends Component {
+ private readonly filterPanelRef = createRef();
+
+ private readonly filterBuilderRef = createRef();
+
+ public render(): JSX.Element {
+ return <>
+
+
+ >;
+ }
+
+ public componentDidMount(): void {
+ this.props.oldFilterPanelView.render($(this.filterPanelRef.current!));
+ this.props.oldFilterBuilderView.render($(this.filterBuilderRef.current!));
+ }
+
+ public componentDidUpdate(): void {
+ this.props.oldFilterPanelView.render($(this.filterPanelRef.current!));
+ this.props.oldFilterBuilderView.render($(this.filterBuilderRef.current!));
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/index.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/index.ts
new file mode 100644
index 000000000000..dd5675aa4863
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/index.ts
@@ -0,0 +1,2 @@
+export { defaultOptions, type Options } from './options';
+export { FilterPanelView } from './view';
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/options.ts
new file mode 100644
index 000000000000..6a38a94ae7fc
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/options.ts
@@ -0,0 +1,49 @@
+import type { FilterPanel } from '@js/common/grids';
+import messageLocalization from '@js/localization/message';
+import type { Properties as FilterBuilderProperties } from '@js/ui/filter_builder';
+import type { Properties as PopupProperties } from '@js/ui/popup';
+
+export interface Options {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ filterPanel?: FilterPanel;
+ filterBuilder?: FilterBuilderProperties;
+ filterBuilderPopup?: PopupProperties;
+}
+
+export const defaultOptions = {
+ filterBuilder: {
+ groupOperationDescriptions: {
+ and: messageLocalization.format('dxFilterBuilder-and'),
+ or: messageLocalization.format('dxFilterBuilder-or'),
+ notAnd: messageLocalization.format('dxFilterBuilder-notAnd'),
+ notOr: messageLocalization.format('dxFilterBuilder-notOr'),
+ },
+ filterOperationDescriptions: {
+ between: messageLocalization.format('dxFilterBuilder-filterOperationBetween'),
+ equal: messageLocalization.format('dxFilterBuilder-filterOperationEquals'),
+ notEqual: messageLocalization.format('dxFilterBuilder-filterOperationNotEquals'),
+ lessThan: messageLocalization.format('dxFilterBuilder-filterOperationLess'),
+ lessThanOrEqual: messageLocalization.format('dxFilterBuilder-filterOperationLessOrEquals'),
+ greaterThan: messageLocalization.format('dxFilterBuilder-filterOperationGreater'),
+ greaterThanOrEqual: messageLocalization.format('dxFilterBuilder-filterOperationGreaterOrEquals'),
+ startsWith: messageLocalization.format('dxFilterBuilder-filterOperationStartsWith'),
+ contains: messageLocalization.format('dxFilterBuilder-filterOperationContains'),
+ notContains: messageLocalization.format('dxFilterBuilder-filterOperationNotContains'),
+ endsWith: messageLocalization.format('dxFilterBuilder-filterOperationEndsWith'),
+ isBlank: messageLocalization.format('dxFilterBuilder-filterOperationIsBlank'),
+ isNotBlank: messageLocalization.format('dxFilterBuilder-filterOperationIsNotBlank'),
+ },
+ },
+
+ filterPanel: {
+ visible: false,
+ filterEnabled: true,
+ texts: {
+ createFilter: messageLocalization.format('dxDataGrid-filterPanelCreateFilter'),
+ clearFilter: messageLocalization.format('dxDataGrid-filterPanelClearFilter'),
+ filterEnabledHint: messageLocalization.format('dxDataGrid-filterPanelFilterEnabledHint'),
+ },
+ },
+
+ filterBuilderPopup: {},
+} satisfies Options;
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/view.tsx
new file mode 100644
index 000000000000..0f2444b69b65
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/view.tsx
@@ -0,0 +1,52 @@
+import type { SubsGets } from '@ts/core/reactive/index';
+import { combined } from '@ts/core/reactive/index';
+import { FilterBuilderView as OldFilterBuilderView } from '@ts/grids/grid_core/filter/m_filter_builder';
+import { FilterPanelView as OldFilterPanelView } from '@ts/grids/grid_core/filter/m_filter_panel';
+
+import { View } from '../../core/view';
+import { OptionsController } from '../../options_controller/options_controller';
+import { WidgetMock } from '../../widget_mock';
+import type { FilterPanelProps } from './filter_panel';
+import { FilterPanelComponent } from './filter_panel';
+
+export class FilterPanelView extends View {
+ protected component = FilterPanelComponent;
+
+ private readonly oldFilterPanelView = new OldFilterPanelView(this.widget);
+
+ private readonly oldFilterBuilderView = new OldFilterBuilderView(this.widget);
+
+ public static dependencies = [OptionsController, WidgetMock] as const;
+
+ constructor(
+ private readonly options: OptionsController,
+ private readonly widget: WidgetMock,
+ ) {
+ super();
+
+ this.oldFilterPanelView.init();
+ this.oldFilterBuilderView.init();
+ }
+
+ protected override getProps(): SubsGets {
+ return combined({
+ oldFilterBuilderView: this.oldFilterBuilderView,
+ oldFilterPanelView: this.oldFilterPanelView,
+
+ filterValue: this.options.oneWay('filterValue'),
+ filterPanel: this.options.oneWay('filterPanel'),
+ filterBuilder: this.options.oneWay('filterBuilder'),
+ filterBuilderPopup: this.options.oneWay('filterBuilderPopup'),
+ });
+ }
+
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+ public optionChanged(args): void {
+ this.oldFilterBuilderView.optionChanged(args);
+ this.oldFilterPanelView.optionChanged(args);
+ }
+
+ public isCompatibilityMode(): boolean {
+ return true;
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/options.integration.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/options.integration.test.ts.snap
new file mode 100644
index 000000000000..a7425c721f1e
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/options.integration.test.ts.snap
@@ -0,0 +1,3716 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Options Column.HeaderFilter dataSource: custom dataSource 1`] = `
+
+`;
+
+exports[`Options Column.HeaderFilter dataSource: custom dataSource with exclude filter 1`] = `
+
+`;
+
+exports[`Options Column.HeaderFilter dataSource: custom dataSource with exclude filter and values 1`] = `
+
+`;
+
+exports[`Options Column.HeaderFilter dataSource: custom dataSource with filter values 1`] = `
+
+`;
+
+exports[`Options Column.HeaderFilter filterType + values: exclude filter 1`] = `
+
+`;
+
+exports[`Options Column.HeaderFilter filterType + values: exclude filter with values 1`] = `
+
+`;
+
+exports[`Options Column.HeaderFilter filterType + values: filter values 1`] = `
+
+`;
+
+exports[`Options HeaderFilter texts: custom translations 1`] = `
+
+`;
+
+exports[`Options HeaderFilter texts: default translation 1`] = `
+
+`;
+
+exports[`Options HeaderFilter visible: false 1`] = `
+
+`;
+
+exports[`Options HeaderFilter visible: true 1`] = `
+
+`;
+
+exports[`Options HeaderFilter visible: undefined 1`] = `
+
+`;
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/view.intergration.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/view.intergration.test.tsx.snap
new file mode 100644
index 000000000000..3eea1c516929
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/view.intergration.test.tsx.snap
@@ -0,0 +1,957 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`HeaderFilter View integration should render popup with list by default 1`] = `
+
+`;
+
+exports[`HeaderFilter View integration should render popup with tree list if dataType is date-like 1`] = `
+
+`;
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/view.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/view.test.tsx.snap
new file mode 100644
index 000000000000..30b53bfca5e0
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/view.test.tsx.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`HeaderFilter HeaderFilterPopupComponent should render 1`] = `
+
+`;
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/controller.test.ts
new file mode 100644
index 000000000000..bbaccdeeb383
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/controller.test.ts
@@ -0,0 +1,227 @@
+/* eslint-disable spellcheck/spell-checker, no-spaced-func */
+import { describe, expect, it } from '@jest/globals';
+import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller';
+import type { Column } from '@ts/grids/new/grid_core/columns_controller/types';
+import { DataController } from '@ts/grids/new/grid_core/data_controller';
+import { FilterController } from '@ts/grids/new/grid_core/filtering';
+import { HeaderFilterController } from '@ts/grids/new/grid_core/filtering/header_filter/controller';
+import type { HeaderFilterColumnOptions } from '@ts/grids/new/grid_core/filtering/header_filter/types';
+import type { Options } from '@ts/grids/new/grid_core/options';
+import { OptionsControllerMock } from '@ts/grids/new/grid_core/options_controller/options_controller.mock';
+
+import { SortingController } from '../../sorting_controller/sorting_controller';
+
+const setup = (config: Options = {}) => {
+ const options = new OptionsControllerMock(config);
+ const filterController = new FilterController(options);
+ const columnsController = new ColumnsController(options);
+ const sortingController = new SortingController(options, columnsController);
+ const dataController = new DataController(options, sortingController, filterController);
+ const headerFilterController = new HeaderFilterController(
+ options,
+ dataController,
+ columnsController,
+ );
+
+ return {
+ headerFilterController,
+ columnsController,
+ };
+};
+
+describe('HeaderFilter', () => {
+ describe('Controller', () => {
+ describe('openPopup', () => {
+ it('should pass element as is to popupState$', () => {
+ const mockElement = {} as Element;
+ const { headerFilterController } = setup();
+ headerFilterController.openPopup(mockElement, {} as Column);
+
+ const result = headerFilterController.popupState$.unreactive_get();
+ expect(result?.element).toBe(mockElement);
+ });
+
+ it.each<{
+ dataType: Column['dataType']; result: 'tree' | 'list';
+ }>([
+ { dataType: 'string', result: 'list' },
+ { dataType: 'number', result: 'list' },
+ { dataType: 'boolean', result: 'list' },
+ { dataType: 'date', result: 'tree' },
+ { dataType: 'datetime', result: 'tree' },
+ ])('options.type in state with columns dataType "$dataType" -> "$result"', ({ dataType, result }) => {
+ const { headerFilterController } = setup();
+ headerFilterController.openPopup({} as Element, { dataType } as Column);
+
+ const state = headerFilterController.popupState$.unreactive_get();
+ expect(state?.options?.type).toBe(result);
+ });
+
+ it('should pass headerFilter options', () => {
+ const expectedFilterType = 'TEST_TYPE';
+ const expectedFilterValues = ['VAL_0', 'VAL_1', 'VAL_2'];
+ const expectedHeaderFilter = {
+ allowSearch: true,
+ testRandomField: 'A',
+ filterType: expectedFilterType as any,
+ values: expectedFilterValues,
+ } as HeaderFilterColumnOptions;
+
+ const { headerFilterController } = setup();
+ headerFilterController.openPopup(
+ {} as Element,
+ { headerFilter: expectedHeaderFilter } as Column,
+ );
+
+ const state = headerFilterController.popupState$.unreactive_get();
+ expect(state?.options?.headerFilter).toStrictEqual(expectedHeaderFilter);
+ expect(state?.options?.filterType).toEqual(expectedFilterType);
+ expect(state?.options?.filterValues).toEqual(expectedFilterValues);
+ });
+
+ it('should apply headerFilter to column options by callback call', () => {
+ const expectedFilterType = 'TEST_TYPE';
+ const expectedFilterValues = ['VAL_0', 'VAL_1', 'VAL_2'];
+ const expectedHeaderFilter = {
+ filterType: expectedFilterType as any,
+ values: expectedFilterValues,
+ } as HeaderFilterColumnOptions;
+
+ const { headerFilterController, columnsController } = setup({
+ headerFilter: { visible: true },
+ columns: [{ name: 'A' }],
+ });
+
+ headerFilterController.openPopup(
+ {} as Element,
+ { name: 'A' } as Column,
+ );
+
+ const state = headerFilterController.popupState$.unreactive_get();
+ state?.options?.apply?.call({
+ filterType: expectedFilterType,
+ filterValues: expectedFilterValues,
+ });
+
+ const updatedColumn = columnsController.columns.unreactive_get()[0];
+
+ expect(updatedColumn?.headerFilter)
+ .toMatchObject(expectedHeaderFilter as Record);
+ });
+
+ it('should save passed headerFilter values during update by callback call', () => {
+ const expectedFilterType = 'TEST_TYPE';
+ const expectedFilterValues = ['VAL_0', 'VAL_1', 'VAL_2'];
+ const expectedSearch = {
+ enabled: true,
+ editorOptions: { testOpt: 'TEST_OPT' },
+ };
+ const expectedHeaderFilter = {
+ search: expectedSearch,
+ filterType: expectedFilterType as any,
+ values: expectedFilterValues,
+ } as HeaderFilterColumnOptions;
+
+ const { headerFilterController, columnsController } = setup({
+ headerFilter: { visible: true },
+ columns: [{ name: 'A' }],
+ });
+
+ headerFilterController.openPopup(
+ {} as Element,
+ { name: 'A', headerFilter: { search: expectedSearch } } as unknown as Column,
+ );
+
+ const state = headerFilterController.popupState$.unreactive_get();
+ state?.options?.apply?.call({
+ filterType: expectedFilterType,
+ filterValues: expectedFilterValues,
+ });
+
+ const updatedColumn = columnsController.columns.unreactive_get()[0];
+
+ expect(updatedColumn?.headerFilter)
+ .toMatchObject(expectedHeaderFilter as Record);
+ });
+
+ it('should clear popupState$ on hide popup callback', () => {
+ const { headerFilterController } = setup({
+ headerFilter: { visible: true },
+ columns: [{ name: 'A' }],
+ });
+
+ headerFilterController.openPopup(
+ {} as Element,
+ { name: 'A' } as Column,
+ );
+
+ const state = headerFilterController.popupState$.unreactive_get();
+ expect(state !== null).toBeTruthy();
+
+ state?.options?.hidePopupCallback?.();
+
+ const stateAfterClose = headerFilterController.popupState$.unreactive_get();
+ expect(stateAfterClose === null).toBeTruthy();
+ });
+ });
+
+ describe('openPopup - get dataSource legacy', () => {
+ it('dataSource options should contain load and postProcess functions', () => {
+ const { headerFilterController } = setup({
+ headerFilter: { visible: true },
+ columns: [{ name: 'A' }],
+ });
+
+ headerFilterController.openPopup(
+ {} as Element,
+ { name: 'A' } as Column,
+ );
+
+ const state = headerFilterController.popupState$.unreactive_get();
+
+ expect(typeof state?.options.dataSource.load).toBe('function');
+ expect(typeof state?.options.dataSource.postProcess).toBe('function');
+ });
+
+ // TODO: Add remoteGrouping cases here
+ // NOTE: Unfortunately, we cannot test perfectly local group functions here
+ // Because these functions are local and too deep in the old grid_core
+ it.each<{
+ caseName: string;
+ column: Column;
+ checkFn: (group: any) => boolean;
+ }>([
+ {
+ caseName: 'default',
+ column: { dataField: 'A' } as Column,
+ checkFn: (group): boolean => typeof group === 'function',
+ },
+ {
+ caseName: 'groupInterval',
+ column: { dataField: 'A', headerFilter: { groupInterval: 2 } } as Column,
+ checkFn: ([group]): boolean => typeof group === 'function',
+ },
+ {
+ caseName: 'sortingMethod',
+ column: { dataField: 'A', sortingMethod: () => {} } as unknown as Column,
+ checkFn: ([{ selector, compare }]): boolean => typeof selector === 'function' && typeof compare === 'function',
+ },
+ ])('$caseName: dataSource options should contains correct group', ({ column, checkFn }) => {
+ const { headerFilterController } = setup({
+ headerFilter: { visible: true },
+ columns: [column as any],
+ });
+
+ headerFilterController.openPopup(
+ {} as Element,
+ column,
+ );
+
+ const state = headerFilterController.popupState$.unreactive_get();
+
+ expect(state?.options.dataSource.group).toBeTruthy();
+ expect(checkFn(state?.options.dataSource.group)).toBeTruthy();
+ });
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/controller.ts
new file mode 100644
index 000000000000..59359fa0d5bb
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/controller.ts
@@ -0,0 +1,92 @@
+/* eslint-disable spellcheck/spell-checker */
+import type { SubsGets } from '@ts/core/reactive/index';
+import { state } from '@ts/core/reactive/index';
+import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/index';
+import type { Column } from '@ts/grids/new/grid_core/columns_controller/types';
+import { DataController } from '@ts/grids/new/grid_core/data_controller/index';
+import {
+ getDataSourceOptions,
+ getFilterType,
+} from '@ts/grids/new/grid_core/filtering/header_filter/legacy_header_filter';
+import { OptionsController } from '@ts/grids/new/grid_core/options_controller/options_controller';
+
+export type PopupState = {
+ element: Element;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ options: Record;
+} | null;
+
+export class HeaderFilterController {
+ public static dependencies = [
+ OptionsController,
+ DataController,
+ ColumnsController,
+ ] as const;
+
+ private readonly popupState = state(null);
+
+ public readonly popupState$: SubsGets = this.popupState;
+
+ constructor(
+ private readonly optionsController: OptionsController,
+ private readonly dataController: DataController,
+ private readonly columnsController: ColumnsController,
+ ) {
+ }
+
+ public openPopup(
+ element: Element,
+ column: Column,
+ onFilterCloseCallback?: () => void,
+ ): void {
+ const rootDataSource = this.dataController.dataSource.unreactive_get();
+ const rootHeaderFilterOptions = this.optionsController.oneWay('headerFilter').unreactive_get();
+
+ const filterDataSourceOptions = getDataSourceOptions(
+ rootDataSource,
+ {
+ ...column,
+ filterType: column.headerFilter?.filterType,
+ filterValues: column.headerFilter?.values,
+ },
+ // NOTE: Only text used from root options
+ {
+ texts: rootHeaderFilterOptions.texts,
+ },
+ );
+
+ const type = getFilterType(column);
+ const colsController = this.columnsController;
+
+ this.popupState.update({
+ element,
+ options: {
+ type,
+ headerFilter: { ...column.headerFilter },
+ dataSource: filterDataSourceOptions,
+ filterType: column.headerFilter?.filterType,
+ // NOTE: Copy array because of mutations in legacy code
+ filterValues: Array.isArray(column.headerFilter?.values)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ ? [...column.headerFilter!.values]
+ : column.headerFilter?.values,
+ apply() {
+ colsController.columnOption(column, 'headerFilter', {
+ ...column.headerFilter,
+ filterType: this.filterType,
+ // NOTE: Copy array because of mutations in legacy code
+ values: Array.isArray(this.filterValues)
+ ? [...this.filterValues]
+ : this.filterValues,
+ });
+
+ onFilterCloseCallback?.();
+ },
+ hidePopupCallback: () => {
+ this.popupState.update(null);
+ onFilterCloseCallback?.();
+ },
+ },
+ });
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/index.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/index.ts
new file mode 100644
index 000000000000..8a6b85751cfe
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/index.ts
@@ -0,0 +1,18 @@
+import headerFilterUtils from './utils';
+
+export {
+ HeaderFilterController,
+} from './controller';
+export type { Options } from './options';
+export { defaultOptions } from './options';
+export type {
+ HeaderFilterColumnOptions,
+ HeaderFilterRootOptions,
+ HeaderFilterSearchColumnOptions,
+ HeaderFilterSearchMode,
+ HeaderFilterSearchRootOptions,
+} from './types';
+export {
+ HeaderFilterPopupView,
+} from './view';
+export { headerFilterUtils };
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts
new file mode 100644
index 000000000000..2c5e0c874285
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts
@@ -0,0 +1,215 @@
+// NOTE: This code moved from old grid_core/header_filter/m_header_filter
+// with minimal possible modifications
+/* eslint-disable
+ @typescript-eslint/explicit-function-return-type,
+ @typescript-eslint/no-unsafe-return,
+ @typescript-eslint/naming-convention,
+ no-plusplus,
+ @typescript-eslint/init-declarations,
+ no-param-reassign,
+ prefer-destructuring,
+ @typescript-eslint/explicit-module-boundary-types,
+ @typescript-eslint/no-shadow,
+ @typescript-eslint/no-explicit-any,
+*/
+import { Deferred } from '@js/core/utils/deferred';
+import { isDefined, isFunction, isObject } from '@js/core/utils/type';
+import filteringUtils from '@js/ui/shared/filtering';
+import { extend } from '@ts/core/utils/m_extend';
+import { normalizeDataSourceOptions as oldNormalizeDataSourceOptions } from '@ts/data/data_source/m_utils';
+import {
+ convertDataFromUTCToLocal,
+ getFormatOptions,
+ isUTCFormat,
+} from '@ts/grids/grid_core/header_filter/m_header_filter';
+import { updateHeaderFilterItemSelectionState } from '@ts/grids/grid_core/header_filter/m_header_filter_core';
+import gridCoreUtils from '@ts/grids/grid_core/m_utils';
+import type { Column } from '@ts/grids/new/grid_core/columns_controller/types';
+
+const getHeaderItemText = (
+ displayValue,
+ column,
+ currentLevel,
+ // NOTE: Only text used from header filter options
+ headerFilterOptions,
+) => {
+ let text = gridCoreUtils
+ .formatValue(displayValue, getFormatOptions(displayValue, column, currentLevel));
+
+ if (!text) {
+ text = headerFilterOptions?.texts?.emptyValue ?? '(Blank)';
+ }
+
+ return text;
+};
+
+const _updateSelectedState = (
+ items,
+ column,
+) => {
+ let i = items.length;
+ const isExclude = column.filterType === 'exclude';
+
+ while (i--) {
+ const item = items[i];
+
+ if ('items' in items[i]) {
+ _updateSelectedState(items[i].items, column);
+ }
+
+ updateHeaderFilterItemSelectionState(
+ item,
+ gridCoreUtils.getIndexByKey(items[i].value, column.filterValues, null) > -1,
+ isExclude,
+ );
+ }
+};
+
+const _normalizeGroupItem = (
+ item,
+ currentLevel,
+ options,
+) => {
+ let value;
+ let displayValue;
+ const { path } = options;
+ const { valueSelector } = options;
+ const { displaySelector } = options;
+ const { column } = options;
+
+ if (valueSelector && displaySelector) {
+ value = valueSelector(item);
+ displayValue = displaySelector(item);
+ } else {
+ value = item.key;
+ displayValue = value;
+ }
+
+ if (!isObject(item)) {
+ item = {};
+ } else {
+ item = extend({}, item);
+ }
+
+ path.push(value);
+
+ if (path.length === 1) {
+ // NOTE: Important! Deconstructing here causes a lot of failed usage scenarios.
+
+ item.value = path[0];
+ } else {
+ item.value = path.join('/');
+ }
+
+ item.text = getHeaderItemText(displayValue, column, currentLevel, options.headerFilterOptions);
+
+ return item;
+};
+
+const _processGroupItems = (
+ groupItems,
+ currentLevel,
+ path,
+ options,
+) => {
+ const { level } = options;
+
+ path = path || [];
+ currentLevel = currentLevel || 0;
+
+ for (let i = 0; i < groupItems.length; i++) {
+ groupItems[i] = _normalizeGroupItem(groupItems[i], currentLevel, {
+ column: options.column,
+ headerFilterOptions: options.headerFilterOptions,
+ path,
+ });
+
+ if ('items' in groupItems[i]) {
+ if (currentLevel === level || !isDefined(groupItems[i].value)) {
+ delete groupItems[i].items;
+ } else {
+ _processGroupItems(groupItems[i].items, currentLevel + 1, path, options);
+ }
+ }
+
+ path.pop();
+ }
+};
+
+export const getDataSourceOptions = (
+ dataSource,
+ column,
+ headerFilterOptions,
+) => {
+ if (!dataSource) {
+ return undefined;
+ }
+
+ // TODO: Support remote grouping
+ const remoteGrouping = false;
+ const group = gridCoreUtils.getHeaderFilterGroupParameters(column, remoteGrouping);
+ const headerFilterDataSource = column.headerFilter?.dataSource;
+ const options: any = {};
+
+ if (isDefined(headerFilterDataSource) && !isFunction(headerFilterDataSource)) {
+ // @ts-expect-error
+ options.dataSource = oldNormalizeDataSourceOptions(headerFilterDataSource);
+ return options.dataSource;
+ }
+
+ const cutoffLevel = Array.isArray(group) ? group.length - 1 : 0;
+ // const filter = this._dataController.getCombinedFilter();
+
+ options.dataSource = {
+ // filter,
+ group,
+ useDefaultSearch: true,
+ load: (options) => {
+ // @ts-expect-error Deferred ctor.
+ const d = new Deferred();
+ // NOTE: this marked as deprecated in original code
+ options.dataField = column.dataField || column.name;
+ dataSource.store().load(options).done((data) => {
+ const convertUTCDates = remoteGrouping
+ && isUTCFormat(column.serializationFormat)
+ && cutoffLevel > 3;
+
+ if (convertUTCDates) {
+ data = convertDataFromUTCToLocal(data, column);
+ }
+
+ _processGroupItems(data, null, null, {
+ level: cutoffLevel,
+ column,
+ headerFilterOptions,
+ });
+
+ d.resolve(data);
+ }).fail(d.reject);
+
+ return d;
+ },
+ };
+
+ if (isFunction(headerFilterDataSource)) {
+ headerFilterDataSource.call(column, options);
+ }
+
+ const origPostProcess = options.dataSource.postProcess;
+ options.dataSource.postProcess = (data) => {
+ let items = data;
+
+ items = origPostProcess?.call(this, items) || items;
+ _updateSelectedState(items, column);
+ return items;
+ };
+
+ return options.dataSource;
+};
+
+export const getFilterType = (
+ column: Column,
+): 'tree' | 'list' => {
+ const groupInterval = filteringUtils.getGroupInterval(column);
+ return groupInterval && groupInterval.length > 1 ? 'tree' : 'list';
+};
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/options.integration.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/options.integration.test.ts
new file mode 100644
index 000000000000..fa7430981502
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/options.integration.test.ts
@@ -0,0 +1,625 @@
+/* eslint-disable spellcheck/spell-checker, no-spaced-func */
+import {
+ afterEach, describe, expect, it,
+} from '@jest/globals';
+import $ from '@js/core/renderer';
+import CardView from '@ts/grids/new/card_view/widget';
+import { HeaderFilterController } from '@ts/grids/new/grid_core/filtering/header_filter/controller';
+import type {
+ HeaderFilterSearchColumnOptions,
+ HeaderFilterSearchMode,
+ HeaderFilterTextOptions, HeaderFilterType,
+} from '@ts/grids/new/grid_core/filtering/header_filter/types';
+import type { Options as GridCoreOptions } from '@ts/grids/new/grid_core/options';
+import { rerender } from 'inferno';
+
+import { defaultOptions } from './options';
+
+const SELECTORS = {
+ cardView: '.dx-cardview',
+ headers: '.dx-cardview-headers',
+ popup: '.dx-popup',
+ popupContent: '.dx-popup-wrapper.dx-header-filter-menu',
+ list: '.dx-list',
+};
+
+const rootQuerySelector = (selector: string) => document.body.querySelector(selector);
+
+const setup = (options: GridCoreOptions = {}): CardView => {
+ const container = document.createElement('div');
+ const { body } = document;
+ body.append(container);
+
+ return new CardView(container, options);
+};
+
+const openHeaderFilterPopup = (cardView: CardView): Element => {
+ const popupContainer = document.createElement('div');
+
+ // @ts-expect-error get protected property
+ const controller = cardView.diContext.get(HeaderFilterController);
+
+ const column = cardView.getVisibleColumns()[0];
+ controller.openPopup(popupContainer, column);
+ rerender();
+
+ return popupContainer;
+};
+
+const getPopup = () => {
+ const popupElement = rootQuerySelector(SELECTORS.popup);
+ const realPopupContentElement = rootQuerySelector(SELECTORS.popupContent);
+ const instance = ($(popupElement ?? undefined) as any).dxPopup('instance');
+
+ return { element: realPopupContentElement, instance };
+};
+
+const getPopupList = (popupContentElement: Element | null) => {
+ const listElement = popupContentElement?.querySelector(SELECTORS.list);
+ const instance = ($(listElement ?? undefined) as any).dxList('instance');
+
+ return { element: listElement, instance };
+};
+
+describe('Options', () => {
+ afterEach(() => {
+ const cardView = rootQuerySelector(SELECTORS.cardView);
+ // @ts-expect-error bad typed renderer
+ $(cardView ?? undefined as any)?.dxCardView('dispose');
+ });
+
+ describe('HeaderFilter', () => {
+ it.each([true, false, undefined])('visible: %s', () => {
+ setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: ['A'],
+ headerFilter: { visible: true },
+ });
+
+ const headerPanel = rootQuerySelector(SELECTORS.headers);
+
+ // NOTE: Check that headerPanel has (or not) filter icon
+ expect(headerPanel).toMatchSnapshot();
+ });
+
+ it.each<{
+ value?: number; result: number | string | undefined;
+ }>([
+ { value: undefined, result: defaultOptions.headerFilter?.width },
+ { value: 100, result: 100 },
+ { value: 1000, result: 1000 },
+ ])('width: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: ['A'],
+ headerFilter: { visible: true, width: value },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { instance: popupInstance } = getPopup();
+
+ expect(popupInstance.option('width')).toBe(result);
+ });
+
+ it.each<{
+ value?: number; result: number | string | undefined;
+ }>([
+ { value: undefined, result: defaultOptions.headerFilter?.height },
+ { value: 100, result: 100 },
+ { value: 1000, result: 1000 },
+ ])('height: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: ['A'],
+ headerFilter: { visible: true, height: value },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { instance: popupInstance } = getPopup();
+
+ expect(popupInstance.option('height')).toBe(result);
+ });
+
+ it.each<{
+ value?: boolean; result: 'all' | 'multiple';
+ }>([
+ { value: undefined, result: 'all' },
+ { value: true, result: 'all' },
+ { value: false, result: 'multiple' },
+ ])('allowSelectAll: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: ['A'],
+ headerFilter: { visible: true, allowSelectAll: value },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+ const { instance } = getPopupList(popupContentElement);
+
+ expect(instance.option('selectionMode')).toBe(result);
+ });
+
+ it.each<{
+ value?: boolean; result: boolean;
+ }>([
+ { value: undefined, result: false },
+ { value: true, result: true },
+ { value: false, result: false },
+ ])('search.enabled: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: ['A'],
+ headerFilter: { visible: true, search: { enabled: value } },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+ const { instance } = getPopupList(popupContentElement);
+
+ expect(instance.option('searchEnabled')).toBe(result);
+ });
+
+ it.each<{
+ value?: number; result: number;
+ }>([
+ { value: undefined, result: 500 },
+ { value: 100, result: 100 },
+ { value: 1000, result: 1000 },
+ ])('search.timeout: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: ['A'],
+ headerFilter: { visible: true, search: { enabled: true, timeout: value } },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+ const { instance } = getPopupList(popupContentElement);
+
+ expect(instance.option('searchTimeout')).toBe(result);
+ });
+
+ it.each<{
+ value?: HeaderFilterSearchMode; result: HeaderFilterSearchMode;
+ }>([
+ { value: undefined, result: 'contains' },
+ { value: 'contains', result: 'contains' },
+ { value: 'equals', result: 'equals' },
+ { value: 'startswith', result: 'startswith' },
+ ])('search.mode: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: ['A'],
+ headerFilter: { visible: true, search: { enabled: true, mode: value } },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+ const { instance } = getPopupList(popupContentElement);
+
+ expect(instance.option('searchMode')).toBe(result);
+ });
+
+ it.each<{
+ value?: Record; result: Record;
+ }>([
+ { result: {} },
+ { value: { disabled: true }, result: { disabled: true } },
+ { value: { height: 999 }, result: { height: 999 } },
+ ])('search.editorOptions: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: ['A'],
+ headerFilter: { visible: true, search: { enabled: true, editorOptions: value } },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+ const { instance } = getPopupList(popupContentElement);
+
+ expect(instance.option('searchEditorOptions')).toMatchObject(result);
+ });
+
+ it.each<{
+ caseName: string; texts?: HeaderFilterTextOptions;
+ }>([
+ { caseName: 'default translation' },
+ {
+ caseName: 'custom translations',
+ texts: { ok: 'TEST_OK', cancel: 'TEST_CANCEL', emptyValue: 'TEST_EMTPY' },
+ },
+ ])('texts: $caseName', ({ texts }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: [{
+ dataField: 'A',
+ // NOTE: WA for check "emptyValue" translation
+ calculateCellValue: (): null => null,
+ }],
+ headerFilter: {
+ visible: true,
+ texts,
+ },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+
+ expect(popupContentElement).toMatchSnapshot();
+ });
+ });
+
+ describe('Column.HeaderFilter', () => {
+ it.each<{
+ value?: number; result: number | string | undefined;
+ }>([
+ { value: undefined, result: -999 },
+ { value: 100, result: 100 },
+ { value: 1000, result: 1000 },
+ ])('width: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: [{
+ dataField: 'A',
+ headerFilter: {
+ width: value,
+ },
+ }],
+ headerFilter: { visible: true, width: -999 },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { instance: popupInstance } = getPopup();
+
+ expect(popupInstance.option('width')).toBe(result);
+ });
+
+ it.each<{
+ value?: number; result: number | string | undefined;
+ }>([
+ { value: undefined, result: -999 },
+ { value: 100, result: 100 },
+ { value: 1000, result: 1000 },
+ ])('height: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: [{
+ dataField: 'A',
+ headerFilter: {
+ height: value,
+ },
+ }],
+ headerFilter: { visible: true, height: -999 },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { instance: popupInstance } = getPopup();
+
+ expect(popupInstance.option('height')).toBe(result);
+ });
+
+ it.each<{
+ value?: boolean; result: 'all' | 'multiple';
+ }>([
+ { value: undefined, result: 'all' },
+ { value: true, result: 'all' },
+ { value: false, result: 'multiple' },
+ ])('allowSelectAll: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: [{
+ dataField: 'A',
+ headerFilter: {
+ allowSelectAll: value,
+ },
+ }],
+ headerFilter: { visible: true, allowSelectAll: !value },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+ const { instance } = getPopupList(popupContentElement);
+
+ expect(instance.option('selectionMode')).toBe(result);
+ });
+
+ it.each<{
+ value?: boolean; result: boolean;
+ }>([
+ { value: undefined, result: true },
+ { value: true, result: true },
+ { value: false, result: false },
+ ])('search.enabled: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: [{
+ dataField: 'A',
+ headerFilter: {
+ search: { enabled: value },
+ },
+ }],
+ headerFilter: { visible: true, search: { enabled: true } },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+ const { instance } = getPopupList(popupContentElement);
+
+ expect(instance.option('searchEnabled')).toBe(result);
+ });
+
+ it.each<{
+ value?: number; result: number;
+ }>([
+ { value: undefined, result: 1 },
+ { value: 100, result: 100 },
+ { value: 1000, result: 1000 },
+ ])('search.timeout: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: [{
+ dataField: 'A',
+ headerFilter: {
+ search: {
+ timeout: value,
+ },
+ },
+ }],
+ headerFilter: { visible: true, search: { enabled: true, timeout: 1 } },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+ const { instance } = getPopupList(popupContentElement);
+
+ expect(instance.option('searchTimeout')).toBe(result);
+ });
+
+ it.each<{
+ value?: HeaderFilterSearchMode; result: HeaderFilterSearchMode;
+ }>([
+ { value: undefined, result: 'contains' },
+ { value: 'contains', result: 'contains' },
+ { value: 'equals', result: 'equals' },
+ { value: 'startswith', result: 'startswith' },
+ ])('search.mode: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: [{
+ dataField: 'A',
+ headerFilter: {
+ search: {
+ mode: value,
+ },
+ },
+ }],
+ headerFilter: { visible: true, search: { enabled: true } },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+ const { instance } = getPopupList(popupContentElement);
+
+ expect(instance.option('searchMode')).toBe(result);
+ });
+
+ it.each<{
+ value?: Record; result: Record;
+ }>([
+ { result: {} },
+ { value: { disabled: true }, result: { disabled: true } },
+ { value: { height: 999 }, result: { height: 999 } },
+ ])('search.editorOptions: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: [{
+ dataField: 'A',
+ headerFilter: {
+ search: {
+ editorOptions: value,
+ },
+ },
+ }],
+ headerFilter: {
+ visible: true,
+ search:
+ { enabled: true, editorOptions: { disabled: false, height: 10 } },
+ },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+ const { instance } = getPopupList(popupContentElement);
+
+ expect(instance.option('searchEditorOptions')).toMatchObject(result);
+ });
+
+ it('search.searchExpr: undefined', () => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: [{
+ dataField: 'A',
+ headerFilter: {
+ search: {
+ searchExpr: undefined,
+ },
+ },
+ }],
+ headerFilter: {
+ visible: true,
+ search: { enabled: true },
+ },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+ const { instance } = getPopupList(popupContentElement);
+
+ const searchExpr = instance.option('searchExpr');
+
+ expect(typeof searchExpr).toBe('function');
+ });
+
+ it.each<{
+ value?: HeaderFilterSearchColumnOptions['searchExpr']; result: any;
+ }>([
+ { value: ['B'], result: ['B'] },
+ { value: ['B', 'C', 'D'], result: ['B', 'C', 'D'] },
+ { value: [() => {}], result: [() => {}] },
+ { value: ['B', () => {}, 'D'], result: ['B', () => {}, 'D'] },
+ ])('search.searchExpr: $value', ({ value, result }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
+ ],
+ columns: [{
+ dataField: 'A',
+ headerFilter: {
+ search: {
+ searchExpr: value,
+ },
+ },
+ }],
+ headerFilter: {
+ visible: true,
+ search: { enabled: true },
+ },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+ const { instance } = getPopupList(popupContentElement);
+
+ const searchExpr = instance.option('searchExpr');
+
+ searchExpr.forEach((expr, idx) => {
+ if (typeof result[idx] === 'function') {
+ // NOTE: We cannot test custom selector fn here.
+ expect(typeof expr).toBe('function');
+ } else {
+ expect(expr).toEqual(result[idx]);
+ }
+ });
+ });
+
+ it.each<{
+ caseName: string;
+ filterType?: HeaderFilterType;
+ filterValues?: string[];
+ }>([
+ { caseName: 'exclude filter', filterType: 'exclude' },
+ { caseName: 'filter values', filterValues: ['B'] },
+ { caseName: 'exclude filter with values', filterType: 'exclude', filterValues: ['B'] },
+ ])('filterType + values: $caseName', ({ filterType, filterValues }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0', B: 'B_0' },
+ { A: 'A_1', B: 'B_1' },
+ { A: 'A_2', B: 'B_2' },
+ { A: 'A_3', B: 'B_3' },
+ { A: 'A_4', B: 'B_4' },
+ ],
+ columns: [{
+ dataField: 'A',
+ headerFilter: { filterType, values: filterValues },
+ }],
+ headerFilter: {
+ visible: true,
+ },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+
+ expect(popupContentElement).toMatchSnapshot();
+ });
+
+ it.each<{
+ caseName: string;
+ dataSource?: ({ text: string; value: string })[];
+ filterType?: HeaderFilterType;
+ filterValues?: string[];
+ }>([
+ {
+ caseName: 'custom dataSource',
+ dataSource: [{ text: 'A', value: 'A' }, { text: 'B', value: 'B' }],
+ filterType: undefined,
+ filterValues: undefined,
+ },
+ {
+ caseName: 'custom dataSource with exclude filter',
+ dataSource: [{ text: 'A', value: 'A' }, { text: 'B', value: 'B' }],
+ filterType: 'exclude',
+ filterValues: undefined,
+ },
+ {
+ caseName: 'custom dataSource with filter values',
+ dataSource: [{ text: 'A', value: 'A' }, { text: 'B', value: 'B' }],
+ filterValues: ['B'],
+ },
+ {
+ caseName: 'custom dataSource with exclude filter and values',
+ dataSource: [{ text: 'A', value: 'A' }, { text: 'B', value: 'B' }],
+ filterType: 'exclude',
+ filterValues: ['B'],
+ },
+ ])('dataSource: $caseName', ({ dataSource, filterType, filterValues }) => {
+ const cardView = setup({
+ dataSource: [
+ { A: 'A_0', B: 'B_0' },
+ { A: 'A_1', B: 'B_1' },
+ { A: 'A_2', B: 'B_2' },
+ { A: 'A_3', B: 'B_3' },
+ { A: 'A_4', B: 'B_4' },
+ ],
+ columns: [{
+ dataField: 'A',
+ headerFilter: { dataSource, filterType, values: filterValues },
+ }],
+ headerFilter: {
+ visible: true,
+ },
+ });
+
+ openHeaderFilterPopup(cardView);
+ const { element: popupContentElement } = getPopup();
+
+ expect(popupContentElement).toMatchSnapshot();
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/options.ts
new file mode 100644
index 000000000000..76ab06aaaf9a
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/options.ts
@@ -0,0 +1,28 @@
+import messageLocalization from '@js/common/core/localization/message';
+
+import type { HeaderFilterRootOptions } from './types';
+
+export interface Options {
+ // general header filter options.
+ headerFilter?: HeaderFilterRootOptions;
+}
+
+export const defaultOptions: Options = {
+ headerFilter: {
+ visible: false,
+ width: 252,
+ height: 325,
+ allowSelectAll: true,
+ search: {
+ enabled: false,
+ timeout: 500,
+ mode: 'contains',
+ editorOptions: {},
+ },
+ texts: {
+ emptyValue: messageLocalization.format('dxDataGrid-headerFilterEmptyValue'),
+ ok: messageLocalization.format('dxDataGrid-headerFilterOK'),
+ cancel: messageLocalization.format('dxDataGrid-headerFilterCancel'),
+ },
+ },
+};
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/types.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/types.ts
new file mode 100644
index 000000000000..c327957f2acb
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/types.ts
@@ -0,0 +1,73 @@
+import type { DataSourceLike } from '@js/data/data_source';
+
+export type HeaderFilterSearchMode = 'contains' | 'startswith' | 'equals';
+export type HeaderFilterType = 'include' | 'exclude';
+
+export interface HeaderFilterTextOptions {
+ // Specifies text for the button that applies the specified filter.
+ ok?: string;
+ // Specifies text for the button that closes the popup menu without applying a filter.
+ cancel?: string;
+ // Specifies a name for the item that represents empty values in the popup menu.
+ emptyValue?: string;
+}
+
+export interface HeaderFilterSearchBaseOptions {
+ // An object defining configuration properties for the TextBox UI component (Search editor)
+ // NOTE: Original DataGrid type not typed too:
+ // https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/columns/headerFilter/search/#editorOptions
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ editorOptions?: any;
+ // Specifies whether search UI is enabled in the header filter.
+ enabled?: boolean;
+ // Specifies a comparison operation used to search header filter values.
+ mode?: HeaderFilterSearchMode;
+ // Specifies a timeout, in milliseconds, during which a user may continue
+ // to modify the search value without starting the search operation.
+ timeout?: number;
+}
+
+export interface HeaderFilterSearchColumnOptions extends HeaderFilterSearchBaseOptions {
+ // Specifies a data object's field name
+ // or an expression whose value is compared to the search string
+ searchExpr?: string | Function | (string | Function)[];
+}
+
+export interface HeaderFilterSearchRootOptions extends HeaderFilterSearchBaseOptions {}
+
+export interface HeaderFilterBaseOptions {
+ // Specifies whether a "Select All" option is available to users
+ allowSelectAll?: boolean;
+ // Specifies the height of the popup menu containing filtering values.
+ height?: number | string;
+ // Specifies the width of the popup menu that contains values for filtering.
+ width?: number | string;
+}
+
+export interface HeaderFilterColumnOptions extends HeaderFilterBaseOptions {
+ // Specifies the header filter's data source.
+ dataSource?: DataSourceLike;
+ // Specifies how the header filter combines values into groups.
+ // Does not apply if you specify a custom header filter data source
+ // TODO: Maybe we should add values for dates to this option
+ // Type described here: https://js.devexpress.com/jQuery/Documentation/ApiReference/Common_Types/grids/#HeaderFilterGroupInterval
+ groupInterval?: number;
+ // Specifies the header filter search options.
+ search?: HeaderFilterSearchColumnOptions;
+ // Whitelist or blacklist meaning of values property.
+ filterType?: HeaderFilterType;
+ // Current column's filter values
+ // NOTE: Original DataGrid type not typed too:
+ // https://js.devexpress.com/jQuery/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/columns/#filterValues
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ values?: any[];
+}
+
+export interface HeaderFilterRootOptions extends HeaderFilterBaseOptions {
+ // Specifies the header filter search options.
+ search?: HeaderFilterSearchRootOptions;
+ // Translations for header filter popup.
+ texts?: HeaderFilterTextOptions;
+ // Specifies whether the header filter can be used to filter data by this column
+ visible?: boolean;
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/utils.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/utils.test.ts
new file mode 100644
index 000000000000..9890d6738e50
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/utils.test.ts
@@ -0,0 +1,154 @@
+import { describe, expect, it } from '@jest/globals';
+import type { DataSourceLike } from '@js/data/data_source';
+import type { Column } from '@ts/grids/new/grid_core/columns_controller/types';
+import type {
+ HeaderFilterColumnOptions,
+ HeaderFilterRootOptions,
+} from '@ts/grids/new/grid_core/filtering/header_filter/types';
+
+import utils from './utils';
+
+describe('HeaderFilter', () => {
+ describe('Utils', () => {
+ describe('mergeColumnHeaderFilterOptions', () => {
+ it('should spread other column options as is', () => {
+ const otherOptions = {
+ optA: 'A',
+ optB: 'B',
+ };
+
+ const mergedOptions = utils.mergeColumnHeaderFilterOptions(
+ otherOptions as unknown as Column,
+ {},
+ );
+
+ expect(mergedOptions).toMatchObject(otherOptions);
+ });
+
+ it.each<{
+ rootVisible: boolean;
+ columnAllowFiltering: boolean;
+ result: boolean;
+ }>([
+ { rootVisible: false, columnAllowFiltering: false, result: false },
+ { rootVisible: false, columnAllowFiltering: true, result: false },
+ { rootVisible: true, columnAllowFiltering: false, result: false },
+ { rootVisible: true, columnAllowFiltering: true, result: false },
+ ])(
+ 'allowHeaderFiltering option (rootVisible: $rootVisible | columnAllowFiltering: $columnAllowFiltering)',
+ ({ rootVisible, columnAllowFiltering, result }) => {
+ const mergedOptions = utils.mergeColumnHeaderFilterOptions(
+ {
+ allowHeaderFiltering: columnAllowFiltering,
+ } as Column,
+ {
+ visible: rootVisible,
+ },
+ );
+
+ expect(mergedOptions.allowHeaderFiltering).toBe(result);
+ },
+ );
+
+ it.each<{
+ caseName: string;
+ root: HeaderFilterRootOptions;
+ col: HeaderFilterColumnOptions;
+ result: HeaderFilterColumnOptions;
+ }>([
+ {
+ caseName: 'not take uniq properties from root',
+ root: { visible: true, texts: {} },
+ col: {},
+ result: { search: {} },
+ },
+ {
+ caseName: 'take uniq properties from column',
+ root: {},
+ col: {
+ dataSource: { test: 'TEST_DS' } as DataSourceLike,
+ filterType: 'exclude',
+ values: ['A', 'B', 'C'],
+ },
+ result: {
+ dataSource: { test: 'TEST_DS' } as DataSourceLike,
+ filterType: 'exclude',
+ values: ['A', 'B', 'C'],
+ search: {},
+ },
+ },
+ {
+ caseName: 'apply root if columns not specified',
+ root: {
+ allowSelectAll: true, width: 150, height: 150,
+ },
+ col: {},
+ result: {
+ allowSelectAll: true, width: 150, height: 150, search: {},
+ },
+ },
+ {
+ caseName: 'override root if columns specified',
+ root: {
+ allowSelectAll: true, width: 150, height: 150,
+ },
+ col: {
+ allowSelectAll: false, width: 200, height: 200,
+ },
+ result: {
+ allowSelectAll: false, width: 200, height: 200, search: {},
+ },
+ },
+ {
+ caseName: 'apply root search if columns not specified',
+ root: {
+ search: {
+ enabled: true, editorOptions: { optA: 'A' }, mode: 'equals', timeout: 999,
+ },
+ },
+ col: {},
+ result: {
+ search: {
+ enabled: true, editorOptions: { optA: 'A' }, mode: 'equals', timeout: 999,
+ },
+ },
+ },
+ {
+ caseName: 'override root search if columns specified',
+ root: {
+ search: {
+ enabled: true, editorOptions: { optA: 'A' }, mode: 'equals', timeout: 999,
+ },
+ },
+ col: {
+ search: {
+ enabled: false, editorOptions: { optA: 'B' }, mode: 'contains', timeout: 100,
+ },
+ },
+ result: {
+ search: {
+ enabled: false, editorOptions: { optA: 'B' }, mode: 'contains', timeout: 100,
+ },
+ },
+ },
+ {
+ caseName: 'take uniq properties from columns search',
+ root: {},
+ col: {
+ search: { searchExpr: '123_TEST' },
+ },
+ result: {
+ search: { searchExpr: '123_TEST' },
+ },
+ },
+ ])('$caseName: should correctly merge options', ({ root, col, result }) => {
+ const mergedOptions = utils.mergeColumnHeaderFilterOptions(
+ { headerFilter: col } as Column,
+ root,
+ );
+
+ expect(mergedOptions.headerFilter).toStrictEqual(result);
+ });
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/utils.ts
new file mode 100644
index 000000000000..5fb35c57eece
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/utils.ts
@@ -0,0 +1,29 @@
+import type { Column } from '@ts/grids/new/grid_core/columns_controller/types';
+
+import type { HeaderFilterRootOptions } from './types';
+
+const mergeColumnHeaderFilterOptions = (
+ column: Column,
+ rootOptions: HeaderFilterRootOptions | undefined,
+): Column => {
+ const { texts, visible, ...restRootOptions } = rootOptions ?? {};
+
+ return {
+ ...column,
+ allowHeaderFiltering: !!rootOptions?.visible
+ && !!column?.allowFiltering
+ && !!column?.allowHeaderFiltering,
+ headerFilter: {
+ ...restRootOptions,
+ ...column?.headerFilter,
+ search: {
+ ...restRootOptions?.search,
+ ...column?.headerFilter?.search,
+ },
+ },
+ };
+};
+
+export default {
+ mergeColumnHeaderFilterOptions,
+};
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.intergration.test.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.intergration.test.tsx
new file mode 100644
index 000000000000..f862ec676931
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.intergration.test.tsx
@@ -0,0 +1,82 @@
+/* eslint-disable spellcheck/spell-checker */
+import {
+ describe,
+ expect,
+ it,
+} from '@jest/globals';
+import CardView from '@ts/grids/new/card_view/widget';
+import { HeaderFilterController } from '@ts/grids/new/grid_core/filtering/header_filter/controller';
+import { rerender } from 'inferno';
+
+const SELECTORS = {
+ popupContent: '.dx-popup-wrapper.dx-header-filter-menu',
+};
+
+const rootQuerySelector = (selector: string) => document.body.querySelector(selector);
+
+describe('HeaderFilter', () => {
+ describe('View integration', () => {
+ it('should render popup with list by default', () => {
+ const container = document.createElement('div');
+ const popupContainer = document.createElement('div');
+ const { body } = document;
+ body.append(container);
+
+ const cardView = new CardView(container, {
+ dataSource: [
+ { A: 'A_0' },
+ { A: 'A_1' },
+ { A: 'A_2' },
+ { A: 'A_3' },
+ { A: 'A_4' },
+ ],
+ columns: ['A'],
+ headerFilter: {
+ visible: true,
+ },
+ });
+
+ // @ts-expect-error getting protected field
+ const controller = cardView.diContext.get(HeaderFilterController);
+
+ const column = cardView.getVisibleColumns()[0];
+ controller.openPopup(popupContainer, column);
+ rerender();
+
+ expect(rootQuerySelector(SELECTORS.popupContent)).toMatchSnapshot();
+ });
+
+ it('should render popup with tree list if dataType is date-like', () => {
+ const container = document.createElement('div');
+ const popupContainer = document.createElement('div');
+ const { body } = document;
+ body.append(container);
+
+ const cardView = new CardView(container, {
+ dataSource: [
+ { A: 'A_0' },
+ { A: 'A_1' },
+ { A: 'A_2' },
+ { A: 'A_3' },
+ { A: 'A_4' },
+ ],
+ columns: [{
+ dataField: 'A',
+ dataType: 'date',
+ }],
+ headerFilter: {
+ visible: true,
+ },
+ });
+
+ // @ts-expect-error getting protected field
+ const controller = cardView.diContext.get(HeaderFilterController);
+
+ const column = cardView.getVisibleColumns()[0];
+ controller.openPopup(popupContainer, column);
+ rerender();
+
+ expect(rootQuerySelector(SELECTORS.popupContent)).toMatchSnapshot();
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.test.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.test.tsx
new file mode 100644
index 000000000000..fd39d36be921
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.test.tsx
@@ -0,0 +1,140 @@
+/* eslint-disable
+ spellcheck/spell-checker,
+ @typescript-eslint/no-non-null-assertion,
+ @typescript-eslint/explicit-member-accessibility,
+ no-new
+*/
+import {
+ beforeEach,
+ describe,
+ expect,
+ it,
+ jest,
+} from '@jest/globals';
+import { state } from '@ts/core/reactive/index';
+import { render, rerender } from 'inferno';
+
+import type { PopupState } from './controller';
+import {
+ HeaderFilterPopupComponent,
+ HeaderFilterPopupView,
+ type OldHeaderFilterPopupInterface,
+} from './view';
+
+const oldHeaderFilterMock = {
+ init: jest.fn(),
+ showHeaderFilterMenu: jest.fn(),
+};
+
+jest.mock('@ts/grids/grid_core/header_filter/m_header_filter_core', () => ({
+ HeaderFilterView: class {
+ init = oldHeaderFilterMock.init;
+
+ showHeaderFilterMenu = oldHeaderFilterMock.showHeaderFilterMenu;
+ },
+}));
+
+describe('HeaderFilter', () => {
+ describe('HeaderFilterPopupComponent', () => {
+ // eslint-disable-next-line @typescript-eslint/init-declarations
+ let oldHeaderFilterPopupMock: OldHeaderFilterPopupInterface;
+
+ beforeEach(() => {
+ oldHeaderFilterPopupMock = {
+ render: jest.fn(),
+ dispose: jest.fn(),
+ };
+ });
+
+ it('should render', () => {
+ const container = document.createElement('div');
+
+ render(
+ ,
+ container,
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
+ it('should call legacy render after mount', () => {
+ const container = document.createElement('div');
+
+ render(
+ ,
+ container,
+ );
+
+ rerender();
+
+ expect(oldHeaderFilterPopupMock!.render).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call legacy render after update', () => {
+ const container = document.createElement('div');
+
+ render(
+ ,
+ container,
+ );
+
+ rerender();
+
+ render(
+ ,
+ container,
+ );
+
+ expect(oldHeaderFilterPopupMock!.render).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('View', () => {
+ beforeEach(() => {
+ oldHeaderFilterMock.init.mockClear();
+ oldHeaderFilterMock.showHeaderFilterMenu.mockClear();
+ });
+
+ it('should init old popup module on creation', () => {
+ new HeaderFilterPopupView(
+ {} as any,
+ { popupState$: state(null) } as any,
+ );
+
+ expect(oldHeaderFilterMock.init).toHaveBeenCalledTimes(1);
+ });
+
+ it('should open popup if popupState$ changed', () => {
+ const expectedElement = { 0: {}, length: 1 } as any;
+ const expectedOptions = { optA: 'A', optB: 'B' };
+ const state$ = state(null);
+
+ new HeaderFilterPopupView(
+ {} as any,
+ { popupState$: state$ } as any,
+ );
+
+ state$.update({ element: {} as any, options: expectedOptions });
+
+ expect(oldHeaderFilterMock.showHeaderFilterMenu)
+ .toHaveBeenCalledTimes(1);
+ expect(oldHeaderFilterMock.showHeaderFilterMenu)
+ .toHaveBeenCalledWith(expectedElement, expectedOptions);
+ });
+
+ it('should do nothing if popupState$ update is empty', () => {
+ const state$ = state(null);
+
+ new HeaderFilterPopupView(
+ {} as any,
+ { popupState$: state$ } as any,
+ );
+
+ state$.update({ element: {} as any, options: {} });
+ state$.update(null);
+
+ expect(oldHeaderFilterMock.showHeaderFilterMenu)
+ .toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.tsx
new file mode 100644
index 000000000000..891549bacd34
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.tsx
@@ -0,0 +1,80 @@
+/* eslint-disable max-classes-per-file */
+import type { dxElementWrapper } from '@js/core/renderer';
+import $ from '@js/core/renderer';
+import type { SubsGets } from '@ts/core/reactive/index';
+import { combined, effect } from '@ts/core/reactive/index';
+import { HeaderFilterView as OldHeaderFilterPopup } from '@ts/grids/grid_core/header_filter/m_header_filter_core';
+import { View } from '@ts/grids/new/grid_core/core/view';
+import { WidgetMock } from '@ts/grids/new/grid_core/widget_mock';
+import { Component, createRef } from 'inferno';
+
+import { CLASSES } from '../../const';
+import { HeaderFilterController } from './controller';
+
+export interface OldHeaderFilterPopupInterface {
+ render: (dxWrapper: dxElementWrapper) => void;
+ dispose: () => void;
+}
+
+export interface HeaderFilterPopupComponentProps {
+ oldHeaderFilterPopup: OldHeaderFilterPopupInterface;
+}
+
+export class HeaderFilterPopupComponent extends Component {
+ private readonly containerRef = createRef();
+
+ public render(): JSX.Element {
+ return (
+
+ );
+ }
+
+ public componentDidMount(): void {
+ this.props.oldHeaderFilterPopup.render($(this.containerRef.current ?? undefined));
+ }
+
+ public componentDidUpdate(): void {
+ this.props.oldHeaderFilterPopup.render($(this.containerRef.current ?? undefined));
+ }
+
+ public componentWillUnmount(): void {
+ this.props.oldHeaderFilterPopup.dispose();
+ }
+}
+
+export class HeaderFilterPopupView extends View<{}> {
+ private readonly oldHeaderFilterPopup: OldHeaderFilterPopup;
+
+ protected component = HeaderFilterPopupComponent;
+
+ public static dependencies = [
+ WidgetMock,
+ HeaderFilterController,
+ ] as const;
+
+ constructor(
+ private readonly widget: WidgetMock,
+ private readonly controller: HeaderFilterController,
+ ) {
+ super();
+ this.oldHeaderFilterPopup = new OldHeaderFilterPopup(this.widget);
+ this.oldHeaderFilterPopup.init();
+
+ effect(
+ (popupState) => {
+ if (!popupState) {
+ return;
+ }
+
+ this.oldHeaderFilterPopup.showHeaderFilterMenu($(popupState.element), popupState.options);
+ },
+ [this.controller.popupState$],
+ );
+ }
+
+ protected getProps(): SubsGets<{}> {
+ return combined({
+ oldHeaderFilterPopup: this.oldHeaderFilterPopup,
+ });
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/index.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/index.ts
new file mode 100644
index 000000000000..107feb262ac4
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/index.ts
@@ -0,0 +1,5 @@
+export { FilterController } from './filter_controller';
+export { FilterPanelView } from './filter_panel/index';
+export * as filterPanel from './filter_panel/index';
+export { defaultOptions, type FilterOptions as Options } from './options';
+export { PublicMethods } from './public_methods';
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/options.ts
new file mode 100644
index 000000000000..a91a7238aeaf
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/options.ts
@@ -0,0 +1,10 @@
+import type { Options as FilterPanelOptions } from './filter_panel/options';
+
+export { defaultOptions } from './filter_panel/options';
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+export interface Options {
+ filterValue?: any;
+}
+
+export type FilterOptions = Options & FilterPanelOptions;
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/public_methods.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/public_methods.ts
new file mode 100644
index 000000000000..5dad8b4033e3
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/public_methods.ts
@@ -0,0 +1,13 @@
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import type { Constructor } from '@ts/grids/new/grid_core/types';
+
+import type { GridCoreNewBase } from '../widget';
+
+export function PublicMethods>(GridCore: T) {
+ return class GridCoreWithFilterController extends GridCore {
+ public clearFilter(): void {
+ this.filterController.clearFilter();
+ }
+ };
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap
index c9a71d863264..4b5f7b23564e 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap
@@ -6,6 +6,8 @@ exports[`ItemsController createDataRow should process data object to data row us
{
"column": {
"alignment": "left",
+ "allowFiltering": true,
+ "allowHeaderFiltering": false,
"allowReordering": true,
"allowSorting": true,
"calculateCellValue": [Function],
@@ -14,6 +16,17 @@ exports[`ItemsController createDataRow should process data object to data row us
"dataField": "a",
"dataType": "string",
"falseText": "false",
+ "headerFilter": {
+ "allowSelectAll": true,
+ "height": 325,
+ "search": {
+ "editorOptions": {},
+ "enabled": false,
+ "mode": "contains",
+ "timeout": 500,
+ },
+ "width": 252,
+ },
"headerItemTemplate": undefined,
"name": "a",
"trueText": "true",
@@ -28,6 +41,8 @@ exports[`ItemsController createDataRow should process data object to data row us
{
"column": {
"alignment": "left",
+ "allowFiltering": true,
+ "allowHeaderFiltering": false,
"allowReordering": true,
"allowSorting": true,
"calculateCellValue": [Function],
@@ -36,6 +51,17 @@ exports[`ItemsController createDataRow should process data object to data row us
"dataField": "b",
"dataType": "string",
"falseText": "false",
+ "headerFilter": {
+ "allowSelectAll": true,
+ "height": 325,
+ "search": {
+ "editorOptions": {},
+ "enabled": false,
+ "mode": "contains",
+ "timeout": 500,
+ },
+ "width": 252,
+ },
"headerItemTemplate": undefined,
"name": "b",
"trueText": "true",
@@ -64,6 +90,8 @@ exports[`ItemsController createDataRow should process data object to data row us
{
"column": {
"alignment": "left",
+ "allowFiltering": true,
+ "allowHeaderFiltering": false,
"allowReordering": true,
"allowSorting": true,
"calculateCellValue": [Function],
@@ -72,6 +100,17 @@ exports[`ItemsController createDataRow should process data object to data row us
"dataField": "a",
"dataType": "string",
"falseText": "false",
+ "headerFilter": {
+ "allowSelectAll": true,
+ "height": 325,
+ "search": {
+ "editorOptions": {},
+ "enabled": false,
+ "mode": "contains",
+ "timeout": 500,
+ },
+ "width": 252,
+ },
"headerItemTemplate": undefined,
"name": "a",
"trueText": "true",
@@ -86,6 +125,8 @@ exports[`ItemsController createDataRow should process data object to data row us
{
"column": {
"alignment": "left",
+ "allowFiltering": true,
+ "allowHeaderFiltering": false,
"allowReordering": true,
"allowSorting": true,
"calculateCellValue": [Function],
@@ -94,6 +135,17 @@ exports[`ItemsController createDataRow should process data object to data row us
"dataField": "b",
"dataType": "string",
"falseText": "false",
+ "headerFilter": {
+ "allowSelectAll": true,
+ "height": 325,
+ "search": {
+ "editorOptions": {},
+ "enabled": false,
+ "mode": "contains",
+ "timeout": 500,
+ },
+ "width": 252,
+ },
"headerItemTemplate": undefined,
"name": "b",
"trueText": "true",
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.test.ts
index 5f215b33e91e..c99b849d34fd 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.test.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.test.ts
@@ -4,6 +4,7 @@ import { SearchController } from '@ts/grids/new/grid_core/search';
import { ColumnsController } from '../columns_controller/columns_controller';
import { DataController } from '../data_controller';
+import { FilterController } from '../filtering';
import type { Options } from '../options';
import { OptionsControllerMock } from '../options_controller/options_controller.mock';
import { SortingController } from '../sorting_controller';
@@ -12,9 +13,10 @@ import { ItemsController } from './items_controller';
const setup = (config: Options = {}) => {
const options = new OptionsControllerMock(config);
const columnsController = new ColumnsController(options);
+ const filterController = new FilterController(options);
const sortingController = new SortingController(options, columnsController);
const searchController = new SearchController(options);
- const dataController = new DataController(options, sortingController);
+ const dataController = new DataController(options, sortingController, filterController);
const itemsController = new ItemsController(
dataController,
columnsController,
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/keyboard_navigation/index.ts b/packages/devextreme/js/__internal/grids/new/grid_core/keyboard_navigation/index.ts
new file mode 100644
index 000000000000..48dbfb0be4d5
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/keyboard_navigation/index.ts
@@ -0,0 +1 @@
+export { MultipleKeyDownHandler } from './multipleKeyDownHandler';
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/keyboard_navigation/multipleKeyDownHandler.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/keyboard_navigation/multipleKeyDownHandler.test.ts
new file mode 100644
index 000000000000..00ec05a54e9f
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/keyboard_navigation/multipleKeyDownHandler.test.ts
@@ -0,0 +1,60 @@
+import {
+ describe, expect, it, jest,
+} from '@jest/globals';
+import { MultipleKeyDownHandler } from '@ts/grids/new/grid_core/keyboard_navigation/multipleKeyDownHandler';
+
+describe('KeyboardNavigation', () => {
+ describe('MultipleKeyDownHandler', () => {
+ it('should call callback on key down if all key pressed', () => {
+ const handler = new MultipleKeyDownHandler(['A', 'B', 'C']);
+ const callback = jest.fn();
+
+ handler.onKeyDownHandler({ key: 'A' } as unknown as KeyboardEvent, callback);
+ handler.onKeyDownHandler({ key: 'B' } as unknown as KeyboardEvent, callback);
+ handler.onKeyDownHandler({ key: 'C' } as unknown as KeyboardEvent, callback);
+
+ expect(callback).toHaveBeenCalledTimes(1);
+ });
+
+ it('should pass event to callback', () => {
+ const handler = new MultipleKeyDownHandler(['A']);
+ const callback = jest.fn();
+ const event = { key: 'A' } as unknown as KeyboardEvent;
+
+ handler.onKeyDownHandler(event, callback);
+
+ expect(callback).toHaveBeenCalledWith(event);
+ });
+
+ it('should compare key names case insensitive', () => {
+ const handler = new MultipleKeyDownHandler(['AbC']);
+ const callback = jest.fn();
+
+ handler.onKeyDownHandler({ key: 'aBc' } as unknown as KeyboardEvent, callback);
+
+ expect(callback).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not call callback if not all key pressed', () => {
+ const handler = new MultipleKeyDownHandler(['A', 'B', 'C']);
+ const callback = jest.fn();
+
+ handler.onKeyDownHandler({ key: 'A' } as unknown as KeyboardEvent, callback);
+ handler.onKeyDownHandler({ key: 'B' } as unknown as KeyboardEvent, callback);
+
+ expect(callback).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not call callback if some key was released', () => {
+ const handler = new MultipleKeyDownHandler(['A', 'B', 'C']);
+ const callback = jest.fn();
+
+ handler.onKeyDownHandler({ key: 'A' } as unknown as KeyboardEvent, callback);
+ handler.onKeyDownHandler({ key: 'B' } as unknown as KeyboardEvent, callback);
+ handler.onKeyUpHandler({ key: 'A' } as unknown as KeyboardEvent);
+ handler.onKeyDownHandler({ key: 'C' } as unknown as KeyboardEvent, callback);
+
+ expect(callback).toHaveBeenCalledTimes(0);
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/keyboard_navigation/multipleKeyDownHandler.ts b/packages/devextreme/js/__internal/grids/new/grid_core/keyboard_navigation/multipleKeyDownHandler.ts
new file mode 100644
index 000000000000..46ba61fba33b
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/keyboard_navigation/multipleKeyDownHandler.ts
@@ -0,0 +1,41 @@
+export class MultipleKeyDownHandler {
+ private readonly trackedKeysSet: Set;
+
+ private readonly pressedKeysSet: Set;
+
+ constructor(
+ trackedKeys: KeyboardEvent['key'][],
+ ) {
+ this.trackedKeysSet = new Set(trackedKeys.map((key) => key.toLowerCase()));
+ this.pressedKeysSet = new Set();
+ }
+
+ public readonly onKeyDownHandler = (
+ event: KeyboardEvent,
+ callback: (event: KeyboardEvent) => void,
+ ): void => {
+ const normalizedKey = event.key.toLowerCase();
+
+ if (!this.trackedKeysSet.has(normalizedKey)) {
+ return;
+ }
+
+ this.pressedKeysSet.add(normalizedKey);
+
+ if (this.trackedKeysSet.size !== this.pressedKeysSet.size) {
+ return;
+ }
+
+ callback(event);
+ };
+
+ public readonly onKeyUpHandler = (event: KeyboardEvent): void => {
+ const normalizedKey = event.key.toLowerCase();
+
+ if (!this.trackedKeysSet.has(normalizedKey)) {
+ return;
+ }
+
+ this.pressedKeysSet.delete(normalizedKey);
+ };
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/options.ts
index f64a8316dbb5..5fc9346d722d 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/options.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/options.ts
@@ -5,6 +5,9 @@ import type { WidgetOptions } from '@js/ui/widget/ui.widget';
import * as columnsController from './columns_controller/index';
import * as contentView from './content_view/index';
import * as dataController from './data_controller/index';
+import * as headerFilter from './filtering/header_filter/index';
+import type * as filterController from './filtering/index';
+import { filterPanel } from './filtering/index';
import * as pager from './pager/index';
import * as searchPanel from './search/index';
import type { SearchProperties } from './search/types';
@@ -21,6 +24,9 @@ export type Options =
& sortingController.Options
& pager.Options
& columnsController.Options
+ & filterController.Options
+ & filterPanel.Options
+ & headerFilter.Options
& contentView.Options
& searchPanel.Options
// TODO: Remove this mock search options during search implementation
@@ -32,6 +38,8 @@ export const defaultOptions = {
...sortingController.defaultOptions,
...columnsController.defaultOptions,
...pager.defaultOptions,
+ ...filterPanel.defaultOptions,
+ ...headerFilter.defaultOptions,
...contentView.defaultOptions,
...searchPanel.defaultOptions,
searchText: '',
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.test.ts
index 5dd03eb8cffe..6b126cc23c4e 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.test.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from '@jest/globals';
import { ColumnsController } from '../columns_controller';
import { DataController } from '../data_controller/data_controller';
+import { FilterController } from '../filtering';
import type { Options } from '../options';
import { OptionsControllerMock } from '../options_controller/options_controller.mock';
import { SortingController } from '../sorting_controller';
@@ -16,9 +17,10 @@ const createPagerView = (options?: Options) => {
},
});
+ const filterController = new FilterController(optionsController);
const columnsController = new ColumnsController(optionsController);
const sortingController = new SortingController(optionsController, columnsController);
- const dataController = new DataController(optionsController, sortingController);
+ const dataController = new DataController(optionsController, sortingController, filterController);
const pager = new PagerView(dataController, optionsController);
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts
index 312cbbe061e7..ff415c5fce5a 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts
@@ -9,9 +9,16 @@ import type { Subscription } from '@ts/core/reactive/index';
import { SearchView } from '@ts/grids/new/grid_core/search/view';
import { render } from 'inferno';
+import { CompatibilityColumnsController } from './columns_controller/compatibility';
import * as ColumnsControllerModule from './columns_controller/index';
import * as DataControllerModule from './data_controller/index';
import { ErrorController } from './error_controller/error_controller';
+import { FilterPanelView } from './filtering/filter_panel/view';
+import {
+ HeaderFilterController,
+ HeaderFilterPopupView,
+} from './filtering/header_filter/index';
+import * as FilterControllerModule from './filtering/index';
import { ItemsController } from './items_controller/items_controller';
import { MainView } from './main_view';
import { defaultOptions, defaultOptionsRules, type Options } from './options';
@@ -21,6 +28,7 @@ import * as SortingControllerModule from './sorting_controller/index';
import type { SortingController } from './sorting_controller/sorting_controller';
import { ToolbarController } from './toolbar/controller';
import { ToolbarView } from './toolbar/view';
+import { WidgetMock } from './widget_mock';
export class GridCoreNewBase<
TProperties extends Options = Options,
@@ -49,20 +57,39 @@ export class GridCoreNewBase<
private searchView!: SearchView;
+ public filterController!: FilterControllerModule.FilterController;
+
+ private filterPanelView!: FilterControllerModule.FilterPanelView;
+
protected _registerDIContext(): void {
this.diContext = new DIContext();
this.diContext.register(DataControllerModule.DataController);
+ this.diContext.register(DataControllerModule.CompatibilityDataController);
this.diContext.register(ItemsController);
this.diContext.register(ColumnsControllerModule.ColumnsController);
+ this.diContext.register(ColumnsControllerModule.CompatibilityColumnsController);
this.diContext.register(SortingControllerModule.SortingController);
this.diContext.register(ToolbarController);
this.diContext.register(ToolbarView);
this.diContext.register(PagerView);
this.diContext.register(SearchController);
this.diContext.register(SearchView);
+ this.diContext.register(FilterControllerModule.FilterController);
+ this.diContext.register(FilterControllerModule.FilterPanelView);
+ this.diContext.register(FilterPanelView);
+ this.diContext.register(HeaderFilterController);
+ this.diContext.register(HeaderFilterPopupView);
this.diContext.register(ErrorController);
}
+ protected _initWidgetMock() {
+ this.diContext.registerInstance(WidgetMock, new WidgetMock(
+ this,
+ this.diContext.get(DataControllerModule.CompatibilityDataController),
+ this.diContext.get(CompatibilityColumnsController),
+ ));
+ }
+
protected _initDIContext(): void {
this.dataController = this.diContext.get(DataControllerModule.DataController);
this.columnsController = this.diContext.get(ColumnsControllerModule.ColumnsController);
@@ -74,12 +101,15 @@ export class GridCoreNewBase<
this.searchController = this.diContext.get(SearchController);
this.searchView = this.diContext.get(SearchView);
this.errorController = this.diContext.get(ErrorController);
+ this.filterController = this.diContext.get(FilterControllerModule.FilterController);
+ this.filterPanelView = this.diContext.get(FilterControllerModule.FilterPanelView);
}
protected _init(): void {
// @ts-expect-error
super._init();
this._registerDIContext();
+ this._initWidgetMock();
this._initDIContext();
}
@@ -105,6 +135,21 @@ export class GridCoreNewBase<
);
}
+ private _optionChanged(args) {
+ [
+ this.filterPanelView,
+ ].forEach((c) => {
+ if (c.isCompatibilityMode()) {
+ c.optionChanged(args);
+ }
+ });
+
+ if (!args.handled) {
+ // @ts-expect-error
+ super._optionChanged(args);
+ }
+ }
+
protected _clean(): void {
this.renderSubscription?.unsubscribe();
render(null, this.$element().get(0));
@@ -116,7 +161,9 @@ export class GridCoreNewBase<
export class GridCoreNew extends ColumnsControllerModule.PublicMethods(
DataControllerModule.PublicMethods(
SortingControllerModule.PublicMethods(
- GridCoreNewBase,
+ FilterControllerModule.PublicMethods(
+ GridCoreNewBase,
+ ),
),
),
) {}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/widget_mock.ts b/packages/devextreme/js/__internal/grids/new/grid_core/widget_mock.ts
new file mode 100644
index 000000000000..e56b339c86a0
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/widget_mock.ts
@@ -0,0 +1,39 @@
+import type { CompatibilityColumnsController } from './columns_controller/compatibility';
+import type { CompatibilityDataController } from './data_controller';
+import type { GridCoreNewBase } from './widget';
+
+export class WidgetMock {
+ public NAME = 'dxDataGrid';
+
+ private readonly _controllers = {
+ data: this.data,
+ columns: this.columns,
+ filterSync: {
+ getCustomFilterOperations(): unknown[] {
+ return [];
+ },
+ },
+ };
+
+ constructor(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private readonly widget: GridCoreNewBase,
+ private readonly data: CompatibilityDataController,
+ private readonly columns: CompatibilityColumnsController,
+ ) {}
+
+ public option(...args: unknown[]): unknown {
+ // @ts-expect-error
+ return this.widget.option(...args);
+ }
+
+ public _createActionByOption(...args: unknown[]): unknown {
+ // @ts-expect-error
+ return this.widget._createActionByOption(...args);
+ }
+
+ public _createComponent(...args: unknown[]): unknown {
+ // @ts-expect-error
+ return this.widget._createComponent(...args);
+ }
+}
diff --git a/packages/devextreme/testing/helpers/memoryLeaksHelper.js b/packages/devextreme/testing/helpers/memoryLeaksHelper.js
index 9e23a3ffdd31..4ae50e15eeda 100644
--- a/packages/devextreme/testing/helpers/memoryLeaksHelper.js
+++ b/packages/devextreme/testing/helpers/memoryLeaksHelper.js
@@ -75,7 +75,10 @@
};
exports.componentCanBeTriviallyInstantiated = function(componentName) {
- return $.inArray(componentName, ['dxDashboardViewer']) === -1;
+ return $.inArray(componentName, [
+ 'dxDashboardViewer',
+ 'dxCardView', // TODO
+ ]) === -1;
};
exports.getComponentOptions = function(componentName) {