diff --git a/packages/devextreme/js/__internal/grids/data_grid/export/m_export.ts b/packages/devextreme/js/__internal/grids/data_grid/export/m_export.ts index 77cd7c212fcf..3cf564c68df5 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/export/m_export.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/export/m_export.ts @@ -731,7 +731,6 @@ const headerPanel = (Base: ModuleType) => class ExportHeaderPanelEx if (exportButton) { items.push(exportButton); - this._correctItemsPosition(items); } return items; @@ -840,10 +839,6 @@ const headerPanel = (Base: ModuleType) => class ExportHeaderPanelEx return items; } - private _correctItemsPosition(items) { - items.sort((itemA, itemB) => itemA.sortIndex - itemB.sortIndex); - } - private _isExportButtonVisible() { return this.option('export.enabled'); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/filter/m_filter_row.ts b/packages/devextreme/js/__internal/grids/grid_core/filter/m_filter_row.ts index 01659f857449..639c31a6bb98 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/filter/m_filter_row.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/filter/m_filter_row.ts @@ -13,6 +13,7 @@ import Menu from '@js/ui/menu'; import Overlay from '@js/ui/overlay/ui.overlay'; import { selectView } from '@js/ui/shared/accessibility'; import type { ColumnsController } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; +import type { ToolbarItem } from '@ts/grids/new/grid_core/toolbar/types'; import type MenuInternal from '@ts/ui/menu/menu'; import type { ColumnHeadersView } from '../column_headers/m_column_headers'; @@ -951,16 +952,16 @@ const headerPanel = (Base: ModuleType) => class FilterRowHeaderPane } } - protected _getToolbarItems() { + protected _getToolbarItems(): ToolbarItem[] { const items = super._getToolbarItems(); const filterItem = this._prepareFilterItem(); return filterItem.concat(items); } - private _prepareFilterItem() { + private _prepareFilterItem(): ToolbarItem[] { const that = this; - const filterItem: object[] = []; + const filterItem: ToolbarItem[] = []; if (that._isShowApplyFilterButton()) { const hintText = that.option('filterRow.applyFilterText'); @@ -972,7 +973,7 @@ const headerPanel = (Base: ModuleType) => class FilterRowHeaderPane const onClickHandler = function () { that._applyFilterViewController.applyFilter(); }; - const toolbarItem = { + const toolbarItem: ToolbarItem = { widget: 'dxButton', options: { icon: 'apply-filter', diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_panel/__tests__/m_header_panel.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/header_panel/__tests__/m_header_panel.integration.test.ts new file mode 100644 index 000000000000..7f6791eae2e6 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/header_panel/__tests__/m_header_panel.integration.test.ts @@ -0,0 +1,353 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import $ from '@js/core/renderer'; +import type { ToolbarItem } from '@ts/grids/new/grid_core/toolbar/types'; + +import { + afterTest, + beforeTest, + createDataGrid, +} from '../../__tests__/__mock__/helpers/utils'; + +const getHeaderPanel = (instance): any => instance.getView('headerPanel'); + +describe('HeaderPanel', () => { + beforeEach(() => { + beforeTest(); + }); + afterEach(afterTest); + + describe('setToolbarItem', () => { + it('should set a toolbar item and make header panel visible', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + }); + const headerPanel = getHeaderPanel(instance); + + expect(headerPanel.isVisible()).toBe(false); + + headerPanel.setToolbarItem('customButton', { + widget: 'dxButton', + options: { text: 'Custom' }, + location: 'after', + name: 'customButton', + }); + jest.runAllTimers(); + headerPanel.render(); + + expect(headerPanel.isVisible()).toBe(true); + const $toolbar = $(instance.element()).find('.dx-toolbar'); + expect($toolbar.length).toBe(1); + }); + + it('should replace an existing item when setting with the same name', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + }); + const headerPanel = getHeaderPanel(instance); + + headerPanel.setToolbarItem('myItem', { + text: 'First', + location: 'before', + name: 'myItem', + sortIndex: 10, + }); + + headerPanel.setToolbarItem('myItem', { + text: 'Replaced', + location: 'after', + name: 'myItem', + sortIndex: 10, + }); + + const items = headerPanel._getToolbarItems(); + + expect(items).toHaveLength(1); + expect(items[0].text).toBe('Replaced'); + }); + + it('should call _invalidate when setting item after first render', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + searchPanel: { visible: true }, + }); + + const headerPanel = getHeaderPanel(instance); + const invalidateSpy = jest.spyOn(headerPanel, '_invalidate'); + + headerPanel.setToolbarItem('customButton', { + text: 'Custom', + location: 'after', + name: 'customButton', + }); + + expect(invalidateSpy).toHaveBeenCalled(); + }); + + it('should not call _invalidate when setting item before first render', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + }); + + const headerPanel = getHeaderPanel(instance); + const originalElement = headerPanel._$element; + headerPanel._$element = undefined; + + const invalidateSpy = jest.spyOn(headerPanel, '_invalidate'); + + headerPanel.setToolbarItem('test', { + text: 'Test', + name: 'test', + }); + + expect(invalidateSpy).not.toHaveBeenCalled(); + + headerPanel._$element = originalElement; + }); + }); + + describe('removeToolbarItem', () => { + it('should remove a previously set item', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.setToolbarItem('toRemove', { + text: 'Remove me', + location: 'after', + name: 'toRemove', + }); + + expect(headerPanel._getToolbarItems()).toHaveLength(1); + + headerPanel.removeToolbarItem('toRemove'); + + expect(headerPanel._getToolbarItems()).toHaveLength(0); + }); + + it('should not call _invalidate when removing non-existent item', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + searchPanel: { visible: true }, + }); + + const headerPanel = getHeaderPanel(instance); + const invalidateSpy = jest.spyOn(headerPanel, '_invalidate'); + + headerPanel.removeToolbarItem('nonExistent'); + + expect(invalidateSpy).not.toHaveBeenCalled(); + }); + + it('should call _invalidate when removing existing item after render', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + searchPanel: { visible: true }, + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.setToolbarItem('temp', { + text: 'Temp', + name: 'temp', + }); + jest.runAllTimers(); + + const invalidateSpy = jest.spyOn(headerPanel, '_invalidate'); + + headerPanel.removeToolbarItem('temp'); + + expect(invalidateSpy).toHaveBeenCalled(); + }); + }); + + describe('_sortToolbarItems', () => { + it('should sort items by sortIndex ascending', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.setToolbarItem('c', { + text: 'C', name: 'c', sortIndex: 30, location: 'after', + }); + headerPanel.setToolbarItem('a', { + text: 'A', name: 'a', sortIndex: 10, location: 'after', + }); + headerPanel.setToolbarItem('b', { + text: 'B', name: 'b', sortIndex: 20, location: 'after', + }); + + headerPanel.render(); + const toolbarItems: ToolbarItem[] = headerPanel._toolbarOptions?.items; + + const names = toolbarItems?.map((item) => item.name); + expect(names).toEqual(['a', 'b', 'c']); + }); + + it('should treat missing sortIndex as 0', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.setToolbarItem('withIndex', { + text: 'With', name: 'withIndex', sortIndex: 10, location: 'after', + }); + headerPanel.setToolbarItem('noIndex', { + text: 'No', name: 'noIndex', location: 'before', + }); + + headerPanel.render(); + const toolbarItems: ToolbarItem[] = headerPanel._toolbarOptions?.items; + + const names = toolbarItems?.map((item) => item.name); + expect(names).toEqual(['noIndex', 'withIndex']); + }); + + it('should not mutate the original items array', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.setToolbarItem('b', { + text: 'B', name: 'b', sortIndex: 20, location: 'after', + }); + headerPanel.setToolbarItem('a', { + text: 'A', name: 'a', sortIndex: 10, location: 'after', + }); + + const itemsBefore = headerPanel._getToolbarItems(); + const firstItemNameBefore = itemsBefore[0].name; + + headerPanel.render(); + + const itemsAfter = headerPanel._getToolbarItems(); + expect(itemsAfter[0].name).toBe(firstItemNameBefore); + }); + }); + + describe('_getToolbarItems', () => { + it('should return items set via setToolbarItem', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.setToolbarItem('item1', { + text: 'Item 1', name: 'item1', location: 'before', + }); + headerPanel.setToolbarItem('item2', { + text: 'Item 2', name: 'item2', location: 'after', + }); + + const items: ToolbarItem[] = headerPanel._getToolbarItems(); + + expect(items).toHaveLength(2); + expect(items.map((i) => i.name)).toEqual( + expect.arrayContaining(['item1', 'item2']), + ); + }); + + it('should return empty array when no items registered', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + }); + + const headerPanel = getHeaderPanel(instance); + + expect(headerPanel._getToolbarItems()).toEqual([]); + }); + }); + + describe('items from extensions and setToolbarItem combined', () => { + it('should include items from both extensions and setToolbarItem', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + columnChooser: { enabled: true }, + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.setToolbarItem('customItem', { + text: 'Custom', + name: 'customItem', + location: 'after', + sortIndex: 100, + }); + + const items: ToolbarItem[] = headerPanel._getToolbarItems(); + const names = items.map((i) => i.name); + + expect(names).toContain('columnChooserButton'); + expect(names).toContain('customItem'); + }); + + it('should sort extension items and registered items together by sortIndex', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + columnChooser: { enabled: true }, + searchPanel: { visible: true }, + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.setToolbarItem('middleItem', { + text: 'Middle', + name: 'middleItem', + location: 'after', + sortIndex: 45, + }); + + headerPanel.render(); + + const toolbarItems: ToolbarItem[] = headerPanel._toolbarOptions?.items ?? []; + const names = toolbarItems.map((i) => i.name); + + const columnChooserIdx = names.indexOf('columnChooserButton'); + const middleIdx = names.indexOf('middleItem'); + const searchIdx = names.indexOf('searchPanel'); + + expect(columnChooserIdx).toBeGreaterThanOrEqual(0); + expect(middleIdx).toBeGreaterThanOrEqual(0); + expect(searchIdx).toBeGreaterThanOrEqual(0); + + expect(columnChooserIdx).toBeLessThan(middleIdx); + expect(middleIdx).toBeLessThan(searchIdx); + }); + + it('should remove only the registered item without affecting extension items', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + columnChooser: { enabled: true }, + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.setToolbarItem('toRemove', { + text: 'Remove', + name: 'toRemove', + location: 'after', + }); + + let items: ToolbarItem[] = headerPanel._getToolbarItems(); + expect(items.map((i) => i.name)).toContain('toRemove'); + expect(items.map((i) => i.name)).toContain('columnChooserButton'); + + // act + headerPanel.removeToolbarItem('toRemove'); + + items = headerPanel._getToolbarItems(); + expect(items.map((i) => i.name)).not.toContain('toRemove'); + expect(items.map((i) => i.name)).toContain('columnChooserButton'); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts b/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts index 725add2ad56e..a3e3c75e6a66 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts @@ -7,6 +7,7 @@ import type { Properties as ToolbarProperties } from '@js/ui/toolbar'; import Toolbar from '@js/ui/toolbar'; import type { EditingController } from '@ts/grids/grid_core/editing/m_editing'; import type { HeaderFilterController } from '@ts/grids/grid_core/header_filter/m_header_filter'; +import type { DefaultToolbarItem, ToolbarItem } from '@ts/grids/new/grid_core/toolbar/types'; import { normalizeToolbarItems } from '@ts/grids/new/grid_core/toolbar/utils'; import type { ModuleType } from '../m_types'; @@ -25,22 +26,49 @@ export class HeaderPanel extends ColumnsView { private _toolbarOptions?: ToolbarProperties; + private readonly _registeredToolbarItems = new Map(); + protected _editingController!: EditingController; protected _headerFilterController!: HeaderFilterController; - public init() { + public init(): void { super.init(); + this._editingController = this.getController('editing'); this._headerFilterController = this.getController('headerFilter'); + this.createAction('onToolbarPreparing', { excludeValidators: ['disabled', 'readOnly'] }); } + public setToolbarItem(name: string, item: ToolbarItem): void { + this._registeredToolbarItems.set(name, { ...item, name }); + + if (this._$element) { + this._invalidate(); + } + } + + public removeToolbarItem(name: string): void { + if (this._registeredToolbarItems.has(name)) { + this._registeredToolbarItems.delete(name); + + if (this._$element) { + this._invalidate(); + } + } + } + /** - * @extended: column_chooser, editing, filter_row, search + * @extended: column_chooser, editing, filter_row */ - protected _getToolbarItems(): any[] { - return []; + protected _getToolbarItems(): ToolbarItem[] { + return Array.from(this._registeredToolbarItems.values()); + } + + // eslint-disable-next-line class-methods-use-this + private _sortToolbarItems(items: ToolbarItem[]): ToolbarItem[] { + return [...items].sort((a, b) => (a.sortIndex ?? 0) - (b.sortIndex ?? 0)); } private _getButtonContainer() { @@ -53,16 +81,17 @@ export class HeaderPanel extends ColumnsView { return this.addWidgetPrefix(TOOLBAR_BUTTON_CLASS) + secondClass; } - private _getToolbarOptions() { - const userToolbarOptions: any = this.option('toolbar'); + private _getToolbarOptions(): ToolbarProperties { + const { toolbar: userToolbarOptions } = this.option(); + const sortedToolbarItems: ToolbarItem[] = this._sortToolbarItems(this._getToolbarItems()); - const options = { + const options: { toolbarOptions: ToolbarProperties } = { toolbarOptions: { - items: this._getToolbarItems(), + items: sortedToolbarItems, visible: userToolbarOptions?.visible, disabled: userToolbarOptions?.disabled, onItemRendered(e) { - const itemRenderedCallback = e.itemData.onItemRendered; + const itemRenderedCallback = e.itemData?.onItemRendered; if (itemRenderedCallback) { itemRenderedCallback(e); @@ -73,7 +102,7 @@ export class HeaderPanel extends ColumnsView { const userItems = userToolbarOptions?.items; options.toolbarOptions.items = normalizeToolbarItems( - options.toolbarOptions.items, + sortedToolbarItems as DefaultToolbarItem[], userItems, DEFAULT_TOOLBAR_ITEM_NAMES, ); @@ -179,7 +208,7 @@ export class HeaderPanel extends ColumnsView { } else if (parts.length === 3) { // `toolbar.items[i]` case const normalizedItem = normalizeToolbarItems( - this._getToolbarItems(), + this._getToolbarItems() as DefaultToolbarItem[], [args.value], DEFAULT_TOOLBAR_ITEM_NAMES, )[0]; diff --git a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts index ac4bfc7fb4e0..1e53d88a1543 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts @@ -1227,8 +1227,7 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo private _ctrlFKeyHandler(eventArgs) { if (this.option('searchPanel.visible')) { - // @ts-expect-error - const searchTextEditor = this._headerPanel.getSearchTextEditor(); + const searchTextEditor = this.getController('searchPanel').getSearchTextEditor(); if (searchTextEditor) { searchTextEditor.focus(); eventArgs.originalEvent.preventDefault(); diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts index 605f88240eaa..0beef2d64cd4 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts @@ -199,6 +199,7 @@ export interface Controllers { resizing: import('./views/m_grid_view').ResizingController; selection: import('./selection/m_selection').SelectionController; validating: import('./validating/m_validating').ValidatingController; + searchPanel: import('./search/m_search').SearchPanelViewController; stateStoring: import('./state_storing/m_state_storing_core').StateStoringController; synchronizeScrolling: import('./views/m_grid_view').SynchronizeScrollingController; tablePosition: import('./columns_resizing_reordering/m_columns_resizing_reordering').TablePositionViewController; diff --git a/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/m_search.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/m_search.integration.test.ts new file mode 100644 index 000000000000..ca3e7abc7792 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/m_search.integration.test.ts @@ -0,0 +1,78 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import $ from '@js/core/renderer'; + +import { + afterTest, + beforeTest, + createDataGrid, +} from '../../__tests__/__mock__/helpers/utils'; + +describe('SearchPanel', () => { + beforeEach(() => { + beforeTest(); + }); + afterEach(afterTest); + + describe('searchPanel options change', () => { + it('should not invalidate headerPanel when changing option on invisible searchPanel', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1, a: 'test' }], + searchPanel: { + visible: false, + }, + }); + + const headerPanel = (instance as any).getView('headerPanel'); + const invalidateSpy = jest.spyOn(headerPanel, '_invalidate'); + + instance.option('searchPanel.placeholder', 'Search...'); + jest.runAllTimers(); + + expect(invalidateSpy).not.toHaveBeenCalled(); + }); + + it('should apply searchPanel option set while it was invisible once it becomes visible', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1, a: 'test' }], + searchPanel: { + visible: false, + }, + }); + + instance.option('searchPanel.placeholder', 'updated'); + jest.runAllTimers(); + + const $searchPanel = $(instance.element()).find('.dx-datagrid-search-panel'); + expect($searchPanel.length).toBe(0); + + instance.option('searchPanel.visible', true); + jest.runAllTimers(); + + const $input = $(instance.element()).find('.dx-datagrid-search-panel input'); + expect($input.length).toBe(1); + expect($input.attr('placeholder')).toBe('updated'); + }); + + it('should update visible searchPanel option in runtime', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1, a: 'test' }], + searchPanel: { + visible: true, + placeholder: 'initial', + }, + }); + + let $input = $(instance.element()).find('.dx-datagrid-search-panel input'); + expect($input.length).toBe(1); + expect($input.attr('placeholder')).toBe('initial'); + + instance.option('searchPanel.placeholder', 'updated'); + jest.runAllTimers(); + + $input = $(instance.element()).find('.dx-datagrid-search-panel input'); + expect($input.attr('placeholder')).toBe('updated'); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts b/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts index 95af6a7fc774..74256ea6bb66 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts @@ -4,13 +4,17 @@ import messageLocalization from '@js/common/core/localization/message'; import type { LangParams } from '@js/common/data'; import dataQuery from '@js/common/data/query'; import domAdapter from '@js/core/dom_adapter'; +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { compileGetter, toComparable } from '@js/core/utils/data'; +import type TextBox from '@js/ui/text_box'; import type { Column } from '@ts/grids/grid_core/columns_controller/types'; +import type { ToolbarItem } from '@ts/grids/new/grid_core/toolbar/types'; import type { DataController, Filter } from '../data_controller/m_data_controller'; import type { HeaderPanel } from '../header_panel/m_header_panel'; -import type { ModuleType } from '../m_types'; +import modules from '../m_modules'; +import type { ModuleType, OptionChanged } from '../m_types'; import gridCoreUtils from '../m_utils'; import type { RowsView } from '../views/m_rows_view'; @@ -18,6 +22,7 @@ const SEARCH_PANEL_CLASS = 'search-panel'; const SEARCH_TEXT_CLASS = 'search-text'; const HEADER_PANEL_CLASS = 'header-panel'; const FILTERING_TIMEOUT = 700; +const SEARCH_PANEL_ITEM_NAME = 'searchPanel'; function allowSearch(column: Column): boolean { return !!(column.allowSearch ?? column.allowFiltering); @@ -131,10 +136,19 @@ const dataController = ( } }; -const headerPanel = ( - Base: ModuleType, -) => class SearchHeaderPanelExtender extends Base { - public optionChanged(args) { +export class SearchPanelViewController extends modules.ViewController { + private _headerPanel?: HeaderPanel; + + private _dataController?: DataController; + + public init(): void { + this._headerPanel = this.getView('headerPanel'); + this._dataController = this.getController('data'); + + this._syncSearchPanelItem(); + } + + public optionChanged(args: OptionChanged): void { if (args.name === 'searchPanel') { if (args.fullName === 'searchPanel.text') { const editor = this.getSearchTextEditor(); @@ -142,7 +156,7 @@ const headerPanel = ( editor.option('value', args.value); } } else { - this._invalidate(); + this._syncSearchPanelItem(); } args.handled = true; @@ -151,68 +165,89 @@ const headerPanel = ( } } - protected _getToolbarItems() { - const items = super._getToolbarItems(); + private _syncSearchPanelItem(): void { + if (!this._headerPanel) { + return; + } - return this._prepareSearchItem(items); - } - - private _prepareSearchItem(items) { - const that = this; - const dataController = this._dataController; - const searchPanelOptions = this.option('searchPanel'); + const { searchPanel: searchPanelOptions } = this.option(); if (searchPanelOptions && searchPanelOptions.visible) { - const toolbarItem = { - template(data, index, container) { - const $search = $('
') - .addClass(that.addWidgetPrefix(SEARCH_PANEL_CLASS)) - .appendTo(container); - - that._editorFactoryController.createEditor($search, { - width: searchPanelOptions.width, - placeholder: searchPanelOptions.placeholder, - parentType: 'searchPanel', - value: that.option('searchPanel.text'), - updateValueTimeout: FILTERING_TIMEOUT, - setValue(value) { - // @ts-expect-error - dataController.searchByText(value); - }, - editorOptions: { - inputAttr: { - 'aria-label': messageLocalization.format(`${that.component.NAME}-ariaSearchInGrid`), + const searchPanelToolbarItem = this._getSearchPanelToolbarItem(); + + if (searchPanelToolbarItem) { + this._headerPanel.setToolbarItem(SEARCH_PANEL_ITEM_NAME, searchPanelToolbarItem); + } + } else { + this._headerPanel.removeToolbarItem(SEARCH_PANEL_ITEM_NAME); + } + } + + private _getSearchPanelToolbarItem(): ToolbarItem | null { + const { searchPanel: searchPanelOptions } = this.option(); + + if (this._headerPanel && searchPanelOptions && searchPanelOptions.visible) { + return { + template: (data, index, container: dxElementWrapper | Element): void => { + if (this._headerPanel) { + const $search = $('
') + .addClass(this._headerPanel.addWidgetPrefix(SEARCH_PANEL_CLASS)) + .appendTo(container); + + this.getController('editorFactory').createEditor($search, { + width: searchPanelOptions.width, + placeholder: searchPanelOptions.placeholder, + parentType: 'searchPanel', + value: this.option('searchPanel.text'), + updateValueTimeout: FILTERING_TIMEOUT, + setValue: (value) => { + // @ts-expect-error + this._dataController.searchByText(value); }, - }, - }); + editorOptions: { + inputAttr: { + 'aria-label': messageLocalization.format(`${this.component.NAME}-ariaSearchInGrid`), + }, + }, + }); - that.resize(); + this._headerPanel.resize(); + } }, - name: 'searchPanel', + name: SEARCH_PANEL_ITEM_NAME, location: 'after', locateInMenu: 'never', - sortIndex: 40, + sortIndex: 50, }; - - items.push(toolbarItem); } - return items; + return null; } - private getSearchTextEditor() { - const that = this; - const $element = that.element(); - const $searchPanel = $element.find(`.${that.addWidgetPrefix(SEARCH_PANEL_CLASS)}`).filter(function () { - return $(this).closest(`.${that.addWidgetPrefix(HEADER_PANEL_CLASS)}`).is($element); - }); + public getSearchTextEditor(): TextBox | null { + if (!this._headerPanel) { + return null; + } + + const $element = this._headerPanel.element(); + + if (!$element) { + return null; + } + + const headerPanelClass = this._headerPanel.addWidgetPrefix(HEADER_PANEL_CLASS); + const $searchPanel = $element + .find(`.${this._headerPanel.addWidgetPrefix(SEARCH_PANEL_CLASS)}`) + .filter((_, el: HTMLElement) => $(el).closest(`.${headerPanelClass}`).is($element)); if ($searchPanel.length) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return $searchPanel.dxTextBox('instance'); } + return null; } -}; +} const rowsView = ( Base: ModuleType, @@ -386,12 +421,14 @@ export const searchModule = { }, }; }, + controllers: { + searchPanel: SearchPanelViewController, + }, extenders: { controllers: { data: dataController, }, views: { - headerPanel, rowsView, }, }, diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/types.ts b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/types.ts index 3c58ab81c347..133bdd330106 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/types.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/types.ts @@ -1,4 +1,4 @@ -import type { Item as BaseToolbarItem } from '@js/ui/toolbar'; +import type { Item as BaseToolbarItem, ItemRenderedEvent } from '@js/ui/toolbar'; import type { DEFAULT_TOOLBAR_ITEMS } from './const'; @@ -6,9 +6,13 @@ export type DefaultToolbarItemName = typeof DEFAULT_TOOLBAR_ITEMS[number]; export interface ToolbarItem extends BaseToolbarItem { name?: DefaultToolbarItemName | string; + sortIndex?: number; + onItemRendered?: (e: ItemRenderedEvent) => void; } -export type DefaultToolbarItem = ToolbarItem & { name: DefaultToolbarItemName }; +export type DefaultToolbarItem = ToolbarItem & { + name: DefaultToolbarItemName, +}; export type ToolbarItems = (ToolbarItem | DefaultToolbarItemName)[]; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/utils.ts index c01b2c9ab4ce..6abeba274c50 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/utils.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/utils.ts @@ -3,7 +3,9 @@ import { isDefined, isString } from '@js/core/utils/type'; import type { Item as BaseToolbarItem } from '@js/ui/toolbar'; import { DEFAULT_TOOLBAR_ITEMS } from './const'; -import type { DefaultToolbarItem, DefaultToolbarItemsCollection, ToolbarItems } from './types'; +import type { + DefaultToolbarItem, DefaultToolbarItemsCollection, ToolbarItems, +} from './types'; export function isVisible( visibleConfig: boolean | undefined, diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js index 701c8d3a919e..2dd99f420fdd 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js @@ -1898,9 +1898,6 @@ QUnit.module('Assign options', baseModuleConfig, () => { dataGrid.option('groupPanel', { emptyPanelText: 'test' }); assert.equal(headerPanel._getToolbarOptions.callCount, 5, 'Toolbar items are updated after groupPanel options change'); - - dataGrid.option('searchPanel', { placeholder: 'test' }); - assert.equal(headerPanel._getToolbarOptions.callCount, 6, 'Toolbar items are updated after searchPanel options change'); }); QUnit.test('customizeColumns change', function(assert) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/gridView.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/gridView.tests.js index 65725d04292f..4f39cbcc9428 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/gridView.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/gridView.tests.js @@ -211,13 +211,13 @@ QUnit.module('Grid view', { QUnit.test('Check search panel aria attribute', function(assert) { // arrange const testElement = $('#container'); - const gridView = this.createGridView(this.defaultOptions); - - gridView.render(testElement, $.extend(this.options, { + const gridView = this.createGridView(this.defaultOptions, { searchPanel: { visible: true } - })); + }); + + gridView.render(testElement); // assert assert.equal(testElement.find('.dx-datagrid-search-panel :not(.dx-texteditor-input)').attr('aria-label'), undefined, 'aria-label attribute not presents for non \'input\' elements'); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerPanel.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerPanel.tests.js index f9e6c8e1da6e..64f620a0eee4 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerPanel.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerPanel.tests.js @@ -46,10 +46,10 @@ QUnit.module('Header panel', { const headerPanel = this.headerPanel; const testElement = $('#container'); - this.options.searchPanel = { + this.option('searchPanel', { visible: true, width: 160 - }; + }); // act headerPanel.render(testElement); @@ -74,10 +74,10 @@ QUnit.module('Header panel', { const testElement = $('#container'); let input; - this.options.searchPanel = { + this.option('searchPanel', { visible: true, width: 160 - }; + }); // act headerPanel.render(testElement); @@ -295,9 +295,9 @@ QUnit.module('Header panel', { this.options.groupPanel = { visible: true }; - this.options.searchPanel = { + this.option('searchPanel', { visible: true - }; + }); // act headerPanel.render(testElement); @@ -329,9 +329,9 @@ QUnit.module('Header panel', { const headerPanel = this.headerPanel; const testElement = $('#container'); - this.options.searchPanel = { + this.option('searchPanel', { visible: true - }; + }); // act headerPanel.render(testElement); @@ -348,10 +348,10 @@ QUnit.module('Header panel', { const headerPanel = this.headerPanel; const testElement = $('#container'); - this.options.searchPanel = { + this.option('searchPanel', { visible: true, width: 213 - }; + }); // act headerPanel.render(testElement); @@ -370,9 +370,9 @@ QUnit.module('Header panel', { const headerPanel = this.headerPanel; const testElement = $('#container'); - this.options.searchPanel = { + this.option('searchPanel', { visible: true - }; + }); // act headerPanel.render(testElement); @@ -387,9 +387,9 @@ QUnit.module('Header panel', { const headerPanel = this.headerPanel; const container = $('#container'); - this.options.searchPanel = { + this.option('searchPanel', { visible: true - }; + }); headerPanel.render(container); @@ -397,9 +397,9 @@ QUnit.module('Header panel', { assert.strictEqual($headerPanel.css('display'), 'block', 'header panel visible'); // act - this.options.searchPanel = { + this.option('searchPanel', { visible: false - }; + }); headerPanel.render(); @@ -412,9 +412,9 @@ QUnit.module('Header panel', { const headerPanel = that.headerPanel; const container = $('#container'); - that.options.searchPanel = { + that.option('searchPanel', { visible: true - }; + }); headerPanel.render(container); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/keyboardNavigation.keyboardKeys.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/keyboardNavigation.keyboardKeys.tests.js index 18ea6a590091..4c24a28ca81d 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/keyboardNavigation.keyboardKeys.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/keyboardNavigation.keyboardKeys.tests.js @@ -3498,21 +3498,22 @@ QUnit.module('Keyboard keys', { QUnit.testInActiveWindow(`${keyName} + F`, function(assert) { // arrange - setupModules(this); - - // act - this.options.searchPanel = { - visible: true + this.options = { + searchPanel: { + visible: true + } }; + setupModules(this); this.gridView.render($('#container')); + // act $(this.rowsView.element()).click(); - const isPreventDefaultCalled = this.triggerKeyDown('F', keyConfig).preventDefault; - const $searchPanelElement = $('.dx-datagrid-search-panel'); // assert + const $searchPanelElement = $('.dx-datagrid-search-panel'); + assert.ok($searchPanelElement.hasClass('dx-state-focused'), 'search panel has focus class'); assert.ok($searchPanelElement.find(':focus').hasClass('dx-texteditor-input'), 'search panel\'s editor is focused'); assert.ok(isPreventDefaultCalled, 'preventDefault is called');