From d6553677bf29bbfd17cd7a504c7b64ee631e3aeb Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Wed, 1 Apr 2026 14:21:11 +0200 Subject: [PATCH 1/7] GridCore: add methods to HeaderPanel to add/remove toolbar items --- .../grids/data_grid/export/m_export.ts | 5 - .../grids/grid_core/filter/m_filter_row.ts | 9 +- .../grid_core/header_panel/m_header_panel.ts | 62 ++++++-- .../m_keyboard_navigation.ts | 3 +- .../js/__internal/grids/grid_core/m_types.ts | 1 + .../grids/grid_core/search/m_search.ts | 137 +++++++++++------- .../grids/new/grid_core/toolbar/types.ts | 9 +- .../grids/new/grid_core/toolbar/utils.ts | 8 +- 8 files changed, 156 insertions(+), 78 deletions(-) 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/m_header_panel.ts b/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts index 725add2ad56e..c3b3d57e5954 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,7 +7,8 @@ import type { Properties as ToolbarProperties } from '@js/ui/toolbar'; import Toolbar from '@js/ui/toolbar'; import type { EditingController } from '@ts/grids/grid_core/editing/m_editing'; import type { HeaderFilterController } from '@ts/grids/grid_core/header_filter/m_header_filter'; -import { normalizeToolbarItems } from '@ts/grids/new/grid_core/toolbar/utils'; +import type { DefaultToolbarItem, ToolbarItem } from '@ts/grids/new/grid_core/toolbar/types'; +import { isDefaultToolbarItem, normalizeToolbarItems } from '@ts/grids/new/grid_core/toolbar/utils'; import type { ModuleType } from '../m_types'; import { ColumnsView } from '../views/m_columns_view'; @@ -25,22 +26,50 @@ export class HeaderPanel extends ColumnsView { private _toolbarOptions?: ToolbarProperties; + private _registeredToolbarItems: Record = {}; + 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 addToolbarItem(name: string, item: ToolbarItem): void { + this._registeredToolbarItems[name] = item; + + if (this._toolbar) { + this._invalidate(); + } + } + + public removeToolbarItem(name: string): void { + if (this._registeredToolbarItems[name]) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this._registeredToolbarItems[name]; + + if (this._toolbar) { + this._invalidate(); + } + } + } + /** - * @extended: column_chooser, editing, filter_row, search + * @extended: column_chooser, editing, filter_row */ - protected _getToolbarItems(): any[] { - return []; + protected _getToolbarItems(): ToolbarItem[] { + return Object.values(this._registeredToolbarItems); + } + + // 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,19 +82,24 @@ 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 { itemData } = e; + + if (itemData && isDefaultToolbarItem(itemData)) { + const itemRenderedCallback = itemData.onItemRendered; - if (itemRenderedCallback) { - itemRenderedCallback(e); + if (itemRenderedCallback) { + itemRenderedCallback(e); + } } }, }, @@ -73,7 +107,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 +213,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 d860e27ac746..5d9529bf84bf 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 b2311a123c62..6137c9802937 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/m_search.ts b/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts index 95af6a7fc774..dd1a77bad89c 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._updateSearchPanelItem(); + } + + 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._updateSearchPanelItem(); } args.handled = true; @@ -151,68 +165,89 @@ const headerPanel = ( } } - protected _getToolbarItems() { - const items = super._getToolbarItems(); + private _updateSearchPanelItem(): 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.addToolbarItem(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..23d051e6c088 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,14 @@ export type DefaultToolbarItemName = typeof DEFAULT_TOOLBAR_ITEMS[number]; export interface ToolbarItem extends BaseToolbarItem { name?: DefaultToolbarItemName | string; + sortIndex?: number; } -export type DefaultToolbarItem = ToolbarItem & { name: DefaultToolbarItemName }; +export type DefaultToolbarItem = ToolbarItem & { + name: DefaultToolbarItemName, + sortIndex?: number, + onItemRendered?: (e: ItemRenderedEvent) => void, +}; 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..e12be3ac5af6 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, DefaultToolbarItemName, DefaultToolbarItemsCollection, ToolbarItems, +} from './types'; export function isVisible( visibleConfig: boolean | undefined, @@ -75,3 +77,7 @@ export function normalizeToolbarItems( ) => normalizeToolbarItem(item, defaultButtonsMap, defaultItemNames), ); } + +export const isDefaultToolbarItem = ( + toolbarItem: ToolbarItem, +): toolbarItem is DefaultToolbarItem => 'name' in toolbarItem && DEFAULT_TOOLBAR_ITEMS.includes(toolbarItem.name as DefaultToolbarItemName); From 8f3e1b492e6290dee5155e7ea48ed9862f406530 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Wed, 1 Apr 2026 18:00:53 +0200 Subject: [PATCH 2/7] GridCore: fix tests --- .../grid_core/header_panel/m_header_panel.ts | 16 ++++++---------- .../grids/new/grid_core/toolbar/types.ts | 1 + .../gridView.tests.js | 10 ++++++---- .../keyboardNavigation.keyboardKeys.tests.js | 15 ++++++++------- 4 files changed, 21 insertions(+), 21 deletions(-) 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 c3b3d57e5954..8678be6ddcf5 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 @@ -8,7 +8,7 @@ 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 { isDefaultToolbarItem, normalizeToolbarItems } from '@ts/grids/new/grid_core/toolbar/utils'; +import { normalizeToolbarItems } from '@ts/grids/new/grid_core/toolbar/utils'; import type { ModuleType } from '../m_types'; import { ColumnsView } from '../views/m_columns_view'; @@ -44,7 +44,7 @@ export class HeaderPanel extends ColumnsView { public addToolbarItem(name: string, item: ToolbarItem): void { this._registeredToolbarItems[name] = item; - if (this._toolbar) { + if (this._$element) { this._invalidate(); } } @@ -54,7 +54,7 @@ export class HeaderPanel extends ColumnsView { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this._registeredToolbarItems[name]; - if (this._toolbar) { + if (this._$element) { this._invalidate(); } } @@ -92,14 +92,10 @@ export class HeaderPanel extends ColumnsView { visible: userToolbarOptions?.visible, disabled: userToolbarOptions?.disabled, onItemRendered(e) { - const { itemData } = e; + const itemRenderedCallback = e.itemData?.onItemRendered; - if (itemData && isDefaultToolbarItem(itemData)) { - const itemRenderedCallback = itemData.onItemRendered; - - if (itemRenderedCallback) { - itemRenderedCallback(e); - } + if (itemRenderedCallback) { + itemRenderedCallback(e); } }, }, 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 23d051e6c088..885b15688612 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 @@ -7,6 +7,7 @@ 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 & { 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..b9873bc26210 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'); @@ -819,6 +819,8 @@ QUnit.module('Grid view', { const headersTable = gridView.getView('columnHeadersView')._tableElement; const scrollerWidth = gridView.getView('rowsView').getScrollbarWidth(); + debugger; + if(device.ios || device.mac || device.android || (device.deviceType !== 'desktop')) { assert.strictEqual(scrollerWidth, 0); } else { 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'); From 0714870af23860204531ee3bebd736d89bf07bae Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Thu, 2 Apr 2026 11:28:42 +0200 Subject: [PATCH 3/7] GridCore: fix tests --- .../dataGrid.tests.js | 3 -- .../headerPanel.tests.js | 36 +++++++++---------- 2 files changed, 18 insertions(+), 21 deletions(-) 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/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); From 1d4c4905c7005d2ffaa78347024f5927eafa3fe3 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Thu, 2 Apr 2026 11:51:51 +0200 Subject: [PATCH 4/7] GridCore: add tests for new functionality --- .../m_header_panel.integration.test.ts | 353 ++++++++++++++++++ .../grid_core/header_panel/m_header_panel.ts | 11 +- .../__tests__/m_search.integration.test.ts | 78 ++++ .../grids/new/grid_core/toolbar/types.ts | 4 +- .../grids/new/grid_core/toolbar/utils.ts | 6 +- .../gridView.tests.js | 2 - 6 files changed, 438 insertions(+), 16 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/header_panel/__tests__/m_header_panel.integration.test.ts create mode 100644 packages/devextreme/js/__internal/grids/grid_core/search/__tests__/m_search.integration.test.ts 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..60dd62e454f1 --- /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('addToolbarItem', () => { + it('should add 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.addToolbarItem('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 adding with the same name', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + }); + const headerPanel = getHeaderPanel(instance); + + headerPanel.addToolbarItem('myItem', { + text: 'First', + location: 'before', + name: 'myItem', + sortIndex: 10, + }); + + headerPanel.addToolbarItem('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 adding 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.addToolbarItem('customButton', { + text: 'Custom', + location: 'after', + name: 'customButton', + }); + + expect(invalidateSpy).toHaveBeenCalled(); + }); + + it('should not call _invalidate when adding 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.addToolbarItem('test', { + text: 'Test', + name: 'test', + }); + + expect(invalidateSpy).not.toHaveBeenCalled(); + + headerPanel._$element = originalElement; + }); + }); + + describe('removeToolbarItem', () => { + it('should remove a previously added item', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.addToolbarItem('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.addToolbarItem('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.addToolbarItem('c', { + text: 'C', name: 'c', sortIndex: 30, location: 'after', + }); + headerPanel.addToolbarItem('a', { + text: 'A', name: 'a', sortIndex: 10, location: 'after', + }); + headerPanel.addToolbarItem('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.addToolbarItem('withIndex', { + text: 'With', name: 'withIndex', sortIndex: 10, location: 'after', + }); + headerPanel.addToolbarItem('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.addToolbarItem('b', { + text: 'B', name: 'b', sortIndex: 20, location: 'after', + }); + headerPanel.addToolbarItem('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 added via addToolbarItem', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.addToolbarItem('item1', { + text: 'Item 1', name: 'item1', location: 'before', + }); + headerPanel.addToolbarItem('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 addToolbarItem combined', () => { + it('should include items from both extensions and addToolbarItem', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + columnChooser: { enabled: true }, + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.addToolbarItem('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.addToolbarItem('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.addToolbarItem('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 8678be6ddcf5..f4e6264f2792 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 @@ -26,7 +26,7 @@ export class HeaderPanel extends ColumnsView { private _toolbarOptions?: ToolbarProperties; - private _registeredToolbarItems: Record = {}; + private readonly _registeredToolbarItems = new Map(); protected _editingController!: EditingController; @@ -42,7 +42,7 @@ export class HeaderPanel extends ColumnsView { } public addToolbarItem(name: string, item: ToolbarItem): void { - this._registeredToolbarItems[name] = item; + this._registeredToolbarItems.set(name, item); if (this._$element) { this._invalidate(); @@ -50,9 +50,8 @@ export class HeaderPanel extends ColumnsView { } public removeToolbarItem(name: string): void { - if (this._registeredToolbarItems[name]) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this._registeredToolbarItems[name]; + if (this._registeredToolbarItems.has(name)) { + this._registeredToolbarItems.delete(name); if (this._$element) { this._invalidate(); @@ -64,7 +63,7 @@ export class HeaderPanel extends ColumnsView { * @extended: column_chooser, editing, filter_row */ protected _getToolbarItems(): ToolbarItem[] { - return Object.values(this._registeredToolbarItems); + return Array.from(this._registeredToolbarItems.values()); } // eslint-disable-next-line class-methods-use-this 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/new/grid_core/toolbar/types.ts b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/types.ts index 885b15688612..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 @@ -7,13 +7,11 @@ export type DefaultToolbarItemName = typeof DEFAULT_TOOLBAR_ITEMS[number]; export interface ToolbarItem extends BaseToolbarItem { name?: DefaultToolbarItemName | string; sortIndex?: number; - onItemRendered?: (e: ItemRenderedEvent) => void, + onItemRendered?: (e: ItemRenderedEvent) => void; } export type DefaultToolbarItem = ToolbarItem & { name: DefaultToolbarItemName, - sortIndex?: number, - onItemRendered?: (e: ItemRenderedEvent) => void, }; 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 e12be3ac5af6..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 @@ -4,7 +4,7 @@ import type { Item as BaseToolbarItem } from '@js/ui/toolbar'; import { DEFAULT_TOOLBAR_ITEMS } from './const'; import type { - DefaultToolbarItem, DefaultToolbarItemName, DefaultToolbarItemsCollection, ToolbarItems, + DefaultToolbarItem, DefaultToolbarItemsCollection, ToolbarItems, } from './types'; export function isVisible( @@ -77,7 +77,3 @@ export function normalizeToolbarItems( ) => normalizeToolbarItem(item, defaultButtonsMap, defaultItemNames), ); } - -export const isDefaultToolbarItem = ( - toolbarItem: ToolbarItem, -): toolbarItem is DefaultToolbarItem => 'name' in toolbarItem && DEFAULT_TOOLBAR_ITEMS.includes(toolbarItem.name as DefaultToolbarItemName); 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 b9873bc26210..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 @@ -819,8 +819,6 @@ QUnit.module('Grid view', { const headersTable = gridView.getView('columnHeadersView')._tableElement; const scrollerWidth = gridView.getView('rowsView').getScrollbarWidth(); - debugger; - if(device.ios || device.mac || device.android || (device.deviceType !== 'desktop')) { assert.strictEqual(scrollerWidth, 0); } else { From 6a16b597c8a49a6946b6f0b3663d24816a92b812 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Fri, 3 Apr 2026 11:24:41 +0200 Subject: [PATCH 5/7] GridCore: call _updateSearchPanelItem on visibility change only --- .../js/__internal/grids/grid_core/search/m_search.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 dd1a77bad89c..686a114e78ea 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 @@ -155,7 +155,9 @@ export class SearchPanelViewController extends modules.ViewController { if (editor) { editor.option('value', args.value); } - } else { + } + + if (args.fullName === 'searchPanel.visible') { this._updateSearchPanelItem(); } From 866069bf3be1470307d1de7570052816dd18b0d7 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Fri, 3 Apr 2026 11:54:26 +0200 Subject: [PATCH 6/7] GridCore: rename some methods for clarity --- .../m_header_panel.integration.test.ts | 56 +++++++++---------- .../grid_core/header_panel/m_header_panel.ts | 4 +- .../grids/grid_core/search/m_search.ts | 12 ++-- 3 files changed, 35 insertions(+), 37 deletions(-) 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 index 60dd62e454f1..7f6791eae2e6 100644 --- 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 @@ -18,8 +18,8 @@ describe('HeaderPanel', () => { }); afterEach(afterTest); - describe('addToolbarItem', () => { - it('should add a toolbar item and make header panel visible', async () => { + describe('setToolbarItem', () => { + it('should set a toolbar item and make header panel visible', async () => { const { instance } = await createDataGrid({ dataSource: [{ id: 1 }], }); @@ -27,7 +27,7 @@ describe('HeaderPanel', () => { expect(headerPanel.isVisible()).toBe(false); - headerPanel.addToolbarItem('customButton', { + headerPanel.setToolbarItem('customButton', { widget: 'dxButton', options: { text: 'Custom' }, location: 'after', @@ -41,20 +41,20 @@ describe('HeaderPanel', () => { expect($toolbar.length).toBe(1); }); - it('should replace an existing item when adding with the same name', async () => { + 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.addToolbarItem('myItem', { + headerPanel.setToolbarItem('myItem', { text: 'First', location: 'before', name: 'myItem', sortIndex: 10, }); - headerPanel.addToolbarItem('myItem', { + headerPanel.setToolbarItem('myItem', { text: 'Replaced', location: 'after', name: 'myItem', @@ -67,7 +67,7 @@ describe('HeaderPanel', () => { expect(items[0].text).toBe('Replaced'); }); - it('should call _invalidate when adding item after first render', async () => { + it('should call _invalidate when setting item after first render', async () => { const { instance } = await createDataGrid({ dataSource: [{ id: 1 }], searchPanel: { visible: true }, @@ -76,7 +76,7 @@ describe('HeaderPanel', () => { const headerPanel = getHeaderPanel(instance); const invalidateSpy = jest.spyOn(headerPanel, '_invalidate'); - headerPanel.addToolbarItem('customButton', { + headerPanel.setToolbarItem('customButton', { text: 'Custom', location: 'after', name: 'customButton', @@ -85,7 +85,7 @@ describe('HeaderPanel', () => { expect(invalidateSpy).toHaveBeenCalled(); }); - it('should not call _invalidate when adding item before first render', async () => { + it('should not call _invalidate when setting item before first render', async () => { const { instance } = await createDataGrid({ dataSource: [{ id: 1 }], }); @@ -96,7 +96,7 @@ describe('HeaderPanel', () => { const invalidateSpy = jest.spyOn(headerPanel, '_invalidate'); - headerPanel.addToolbarItem('test', { + headerPanel.setToolbarItem('test', { text: 'Test', name: 'test', }); @@ -108,14 +108,14 @@ describe('HeaderPanel', () => { }); describe('removeToolbarItem', () => { - it('should remove a previously added item', async () => { + it('should remove a previously set item', async () => { const { instance } = await createDataGrid({ dataSource: [{ id: 1 }], }); const headerPanel = getHeaderPanel(instance); - headerPanel.addToolbarItem('toRemove', { + headerPanel.setToolbarItem('toRemove', { text: 'Remove me', location: 'after', name: 'toRemove', @@ -150,7 +150,7 @@ describe('HeaderPanel', () => { const headerPanel = getHeaderPanel(instance); - headerPanel.addToolbarItem('temp', { + headerPanel.setToolbarItem('temp', { text: 'Temp', name: 'temp', }); @@ -172,13 +172,13 @@ describe('HeaderPanel', () => { const headerPanel = getHeaderPanel(instance); - headerPanel.addToolbarItem('c', { + headerPanel.setToolbarItem('c', { text: 'C', name: 'c', sortIndex: 30, location: 'after', }); - headerPanel.addToolbarItem('a', { + headerPanel.setToolbarItem('a', { text: 'A', name: 'a', sortIndex: 10, location: 'after', }); - headerPanel.addToolbarItem('b', { + headerPanel.setToolbarItem('b', { text: 'B', name: 'b', sortIndex: 20, location: 'after', }); @@ -196,10 +196,10 @@ describe('HeaderPanel', () => { const headerPanel = getHeaderPanel(instance); - headerPanel.addToolbarItem('withIndex', { + headerPanel.setToolbarItem('withIndex', { text: 'With', name: 'withIndex', sortIndex: 10, location: 'after', }); - headerPanel.addToolbarItem('noIndex', { + headerPanel.setToolbarItem('noIndex', { text: 'No', name: 'noIndex', location: 'before', }); @@ -217,10 +217,10 @@ describe('HeaderPanel', () => { const headerPanel = getHeaderPanel(instance); - headerPanel.addToolbarItem('b', { + headerPanel.setToolbarItem('b', { text: 'B', name: 'b', sortIndex: 20, location: 'after', }); - headerPanel.addToolbarItem('a', { + headerPanel.setToolbarItem('a', { text: 'A', name: 'a', sortIndex: 10, location: 'after', }); @@ -235,17 +235,17 @@ describe('HeaderPanel', () => { }); describe('_getToolbarItems', () => { - it('should return items added via addToolbarItem', async () => { + it('should return items set via setToolbarItem', async () => { const { instance } = await createDataGrid({ dataSource: [{ id: 1 }], }); const headerPanel = getHeaderPanel(instance); - headerPanel.addToolbarItem('item1', { + headerPanel.setToolbarItem('item1', { text: 'Item 1', name: 'item1', location: 'before', }); - headerPanel.addToolbarItem('item2', { + headerPanel.setToolbarItem('item2', { text: 'Item 2', name: 'item2', location: 'after', }); @@ -268,8 +268,8 @@ describe('HeaderPanel', () => { }); }); - describe('items from extensions and addToolbarItem combined', () => { - it('should include items from both extensions and addToolbarItem', async () => { + 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 }, @@ -277,7 +277,7 @@ describe('HeaderPanel', () => { const headerPanel = getHeaderPanel(instance); - headerPanel.addToolbarItem('customItem', { + headerPanel.setToolbarItem('customItem', { text: 'Custom', name: 'customItem', location: 'after', @@ -300,7 +300,7 @@ describe('HeaderPanel', () => { const headerPanel = getHeaderPanel(instance); - headerPanel.addToolbarItem('middleItem', { + headerPanel.setToolbarItem('middleItem', { text: 'Middle', name: 'middleItem', location: 'after', @@ -332,7 +332,7 @@ describe('HeaderPanel', () => { const headerPanel = getHeaderPanel(instance); - headerPanel.addToolbarItem('toRemove', { + headerPanel.setToolbarItem('toRemove', { text: 'Remove', name: 'toRemove', location: 'after', 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 f4e6264f2792..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 @@ -41,8 +41,8 @@ export class HeaderPanel extends ColumnsView { this.createAction('onToolbarPreparing', { excludeValidators: ['disabled', 'readOnly'] }); } - public addToolbarItem(name: string, item: ToolbarItem): void { - this._registeredToolbarItems.set(name, item); + public setToolbarItem(name: string, item: ToolbarItem): void { + this._registeredToolbarItems.set(name, { ...item, name }); if (this._$element) { this._invalidate(); 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 686a114e78ea..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 @@ -145,7 +145,7 @@ export class SearchPanelViewController extends modules.ViewController { this._headerPanel = this.getView('headerPanel'); this._dataController = this.getController('data'); - this._updateSearchPanelItem(); + this._syncSearchPanelItem(); } public optionChanged(args: OptionChanged): void { @@ -155,10 +155,8 @@ export class SearchPanelViewController extends modules.ViewController { if (editor) { editor.option('value', args.value); } - } - - if (args.fullName === 'searchPanel.visible') { - this._updateSearchPanelItem(); + } else { + this._syncSearchPanelItem(); } args.handled = true; @@ -167,7 +165,7 @@ export class SearchPanelViewController extends modules.ViewController { } } - private _updateSearchPanelItem(): void { + private _syncSearchPanelItem(): void { if (!this._headerPanel) { return; } @@ -178,7 +176,7 @@ export class SearchPanelViewController extends modules.ViewController { const searchPanelToolbarItem = this._getSearchPanelToolbarItem(); if (searchPanelToolbarItem) { - this._headerPanel.addToolbarItem(SEARCH_PANEL_ITEM_NAME, searchPanelToolbarItem); + this._headerPanel.setToolbarItem(SEARCH_PANEL_ITEM_NAME, searchPanelToolbarItem); } } else { this._headerPanel.removeToolbarItem(SEARCH_PANEL_ITEM_NAME); From 731ff8ccedfd4c38107dc03ec36b23b20c35e7a8 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Mon, 6 Apr 2026 12:59:48 +0200 Subject: [PATCH 7/7] do not invalidate header panel on visible item option changed --- .../m_header_panel.integration.test.ts | 56 +++++++++ .../grid_core/header_panel/m_header_panel.ts | 52 +++++++-- .../m_keyboard_navigation.ts | 10 +- .../grids/grid_core/search/m_search.ts | 109 ++++++++---------- 4 files changed, 156 insertions(+), 71 deletions(-) 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 index 7f6791eae2e6..6d0a3f28d28f 100644 --- 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 @@ -85,6 +85,62 @@ describe('HeaderPanel', () => { expect(invalidateSpy).toHaveBeenCalled(); }); + it('should not call _invalidate when updating an existing item in rendered toolbar', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + searchPanel: { visible: true }, + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.setToolbarItem('customButton', { + text: 'Original', + location: 'after', + name: 'customButton', + }); + jest.runAllTimers(); + headerPanel.render(); + + const invalidateSpy = jest.spyOn(headerPanel, '_invalidate'); + + headerPanel.setToolbarItem('customButton', { + text: 'Updated', + location: 'after', + name: 'customButton', + }); + + expect(invalidateSpy).not.toHaveBeenCalled(); + }); + + it('should update toolbar item in-place without full re-render', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1 }], + searchPanel: { visible: true }, + }); + + const headerPanel = getHeaderPanel(instance); + + headerPanel.setToolbarItem('customButton', { + text: 'Original', + location: 'after', + name: 'customButton', + }); + jest.runAllTimers(); + headerPanel.render(); + + const items = headerPanel._toolbar?.option('items') ?? []; + expect(items[0].text).toBe('Original'); + + headerPanel.setToolbarItem('customButton', { + text: 'Updated', + location: 'after', + name: 'customButton', + }); + + const updatedItems = headerPanel._toolbar?.option('items') ?? []; + expect(updatedItems[0].text).toBe('Updated'); + }); + it('should not call _invalidate when setting item before first render', async () => { const { instance } = await createDataGrid({ dataSource: [{ id: 1 }], 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 a3e3c75e6a66..7ba9a50b9d0a 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 @@ -26,7 +26,7 @@ export class HeaderPanel extends ColumnsView { private _toolbarOptions?: ToolbarProperties; - private readonly _registeredToolbarItems = new Map(); + private readonly registeredToolbarItems = new Map(); protected _editingController!: EditingController; @@ -42,16 +42,52 @@ export class HeaderPanel extends ColumnsView { } public setToolbarItem(name: string, item: ToolbarItem): void { - this._registeredToolbarItems.set(name, { ...item, name }); + const isExisting = this.registeredToolbarItems.has(name); + this.registeredToolbarItems.set(name, { ...item, name }); - if (this._$element) { + if (!this._$element) { + return; + } + + const itemIndex = isExisting ? this.findToolbarItemIndex(name) : -1; + + if (itemIndex >= 0) { + const normalizedItem = this.getNormalizedRegisteredItem(name); + this._toolbar?.option(`items[${itemIndex}]`, normalizedItem); + } else { this._invalidate(); } } + private findToolbarItemIndex(name: string): number { + const items: ToolbarItem[] = this._toolbar?.option('items') ?? []; + + return items.findIndex((i) => i.name === name); + } + + private getNormalizedRegisteredItem(name: string): ToolbarItem | undefined { + const registeredItem = this.registeredToolbarItems.get(name); + + const userToolbarOptions = this.option('toolbar'); + + const userItem = userToolbarOptions?.items?.find( + (ui) => (typeof ui === 'string' ? ui === name : ui?.name === name), + ); + + if (!userItem) { + return registeredItem; + } + + return normalizeToolbarItems( + [registeredItem] as DefaultToolbarItem[], + [userItem], + DEFAULT_TOOLBAR_ITEM_NAMES, + )[0]; + } + public removeToolbarItem(name: string): void { - if (this._registeredToolbarItems.has(name)) { - this._registeredToolbarItems.delete(name); + if (this.registeredToolbarItems.has(name)) { + this.registeredToolbarItems.delete(name); if (this._$element) { this._invalidate(); @@ -63,11 +99,11 @@ export class HeaderPanel extends ColumnsView { * @extended: column_chooser, editing, filter_row */ protected _getToolbarItems(): ToolbarItem[] { - return Array.from(this._registeredToolbarItems.values()); + return Array.from(this.registeredToolbarItems.values()); } // eslint-disable-next-line class-methods-use-this - private _sortToolbarItems(items: ToolbarItem[]): ToolbarItem[] { + private sortToolbarItems(items: ToolbarItem[]): ToolbarItem[] { return [...items].sort((a, b) => (a.sortIndex ?? 0) - (b.sortIndex ?? 0)); } @@ -83,7 +119,7 @@ export class HeaderPanel extends ColumnsView { private _getToolbarOptions(): ToolbarProperties { const { toolbar: userToolbarOptions } = this.option(); - const sortedToolbarItems: ToolbarItem[] = this._sortToolbarItems(this._getToolbarItems()); + const sortedToolbarItems: ToolbarItem[] = this.sortToolbarItems(this._getToolbarItems()); const options: { toolbarOptions: ToolbarProperties } = { toolbarOptions: { 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 5d9529bf84bf..cdc8ca4c38a3 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 @@ -133,8 +133,6 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo protected _editingController!: Controllers['editing']; - private _headerPanel!: Views['headerPanel']; - protected _rowsView!: Views['rowsView']; private _editorFactory!: Controllers['editorFactory']; @@ -145,6 +143,8 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo private _columnResizerController!: Controllers['columnsResizer']; + private searchPanel!: Controllers['searchPanel']; + private _needNavigationToCell = false; // #region Initialization @@ -152,12 +152,12 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo this._dataController = this.getController('data'); this._selectionController = this.getController('selection'); this._editingController = this.getController('editing'); - this._headerPanel = this.getView('headerPanel'); this._editorFactory = this.getController('editorFactory'); this._focusController = this.getController('focus'); this._adaptiveColumnsController = this.getController('adaptiveColumns'); this._columnResizerController = this.getController('columnsResizer'); this._rowsView = this.getView('rowsView'); + this.searchPanel = this.getController('searchPanel'); super.init(); @@ -1225,9 +1225,9 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo return false; } - private _ctrlFKeyHandler(eventArgs) { + private _ctrlFKeyHandler(eventArgs): void { if (this.option('searchPanel.visible')) { - const searchTextEditor = this.getController('searchPanel').getSearchTextEditor(); + const searchTextEditor = this.searchPanel.getSearchTextEditor(); if (searchTextEditor) { searchTextEditor.focus(); eventArgs.originalEvent.preventDefault(); 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 74256ea6bb66..5e044dfab452 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 @@ -71,7 +71,7 @@ const dataController = ( return gridCoreUtils.combineFilters([filter, searchFilter]); } - private searchByText(text): void { + public searchByText(text: string | undefined): void { this.option('searchPanel.text', text); } @@ -136,16 +136,18 @@ const dataController = ( } }; +type SearchDataControllerExtender = InstanceType>; + export class SearchPanelViewController extends modules.ViewController { - private _headerPanel?: HeaderPanel; + private headerPanel?: HeaderPanel; - private _dataController?: DataController; + private dataController?: SearchDataControllerExtender; public init(): void { - this._headerPanel = this.getView('headerPanel'); - this._dataController = this.getController('data'); + this.headerPanel = this.getView('headerPanel'); + this.dataController = this.getController('data') as SearchDataControllerExtender; - this._syncSearchPanelItem(); + this.syncSearchPanelItem(); } public optionChanged(args: OptionChanged): void { @@ -156,7 +158,7 @@ export class SearchPanelViewController extends modules.ViewController { editor.option('value', args.value); } } else { - this._syncSearchPanelItem(); + this.syncSearchPanelItem(); } args.handled = true; @@ -165,79 +167,70 @@ export class SearchPanelViewController extends modules.ViewController { } } - private _syncSearchPanelItem(): void { - if (!this._headerPanel) { + private syncSearchPanelItem(): void { + if (!this.headerPanel) { return; } - const { searchPanel: searchPanelOptions } = this.option(); + const searchPanelOptions = this.option('searchPanel'); if (searchPanelOptions && searchPanelOptions.visible) { - const searchPanelToolbarItem = this._getSearchPanelToolbarItem(); + const searchPanelToolbarItem = this.getSearchPanelToolbarItem(); if (searchPanelToolbarItem) { - this._headerPanel.setToolbarItem(SEARCH_PANEL_ITEM_NAME, searchPanelToolbarItem); + this.headerPanel.setToolbarItem(SEARCH_PANEL_ITEM_NAME, searchPanelToolbarItem); } } else { - this._headerPanel.removeToolbarItem(SEARCH_PANEL_ITEM_NAME); + 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`), - }, - }, - }); + private getSearchPanelToolbarItem(): ToolbarItem { + const searchPanelOptions = this.option('searchPanel'); - this._headerPanel.resize(); - } - }, - name: SEARCH_PANEL_ITEM_NAME, - location: 'after', - locateInMenu: 'never', - sortIndex: 50, - }; - } + 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: string | undefined): void => { + this.dataController?.searchByText(value); + }, + editorOptions: { + inputAttr: { + 'aria-label': messageLocalization.format(`${this.component.NAME}-ariaSearchInGrid`), + }, + }, + }); - return null; + this.headerPanel.resize(); + } + }, + name: SEARCH_PANEL_ITEM_NAME, + location: 'after', + locateInMenu: 'never', + sortIndex: 50, + }; } public getSearchTextEditor(): TextBox | null { - if (!this._headerPanel) { - return null; - } - - const $element = this._headerPanel.element(); + const $element = this.headerPanel?.element(); - if (!$element) { + if (!this.headerPanel || !$element) { return null; } - const headerPanelClass = this._headerPanel.addWidgetPrefix(HEADER_PANEL_CLASS); + const headerPanelClass = this.headerPanel.addWidgetPrefix(HEADER_PANEL_CLASS); const $searchPanel = $element - .find(`.${this._headerPanel.addWidgetPrefix(SEARCH_PANEL_CLASS)}`) + .find(`.${this.headerPanel.addWidgetPrefix(SEARCH_PANEL_CLASS)}`) .filter((_, el: HTMLElement) => $(el).closest(`.${headerPanelClass}`).is($element)); if ($searchPanel.length) {