Skip to content

Commit 731ff8c

Browse files
committed
do not invalidate header panel on visible item option changed
1 parent 866069b commit 731ff8c

File tree

4 files changed

+156
-71
lines changed

4 files changed

+156
-71
lines changed

packages/devextreme/js/__internal/grids/grid_core/header_panel/__tests__/m_header_panel.integration.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,62 @@ describe('HeaderPanel', () => {
8585
expect(invalidateSpy).toHaveBeenCalled();
8686
});
8787

88+
it('should not call _invalidate when updating an existing item in rendered toolbar', async () => {
89+
const { instance } = await createDataGrid({
90+
dataSource: [{ id: 1 }],
91+
searchPanel: { visible: true },
92+
});
93+
94+
const headerPanel = getHeaderPanel(instance);
95+
96+
headerPanel.setToolbarItem('customButton', {
97+
text: 'Original',
98+
location: 'after',
99+
name: 'customButton',
100+
});
101+
jest.runAllTimers();
102+
headerPanel.render();
103+
104+
const invalidateSpy = jest.spyOn(headerPanel, '_invalidate');
105+
106+
headerPanel.setToolbarItem('customButton', {
107+
text: 'Updated',
108+
location: 'after',
109+
name: 'customButton',
110+
});
111+
112+
expect(invalidateSpy).not.toHaveBeenCalled();
113+
});
114+
115+
it('should update toolbar item in-place without full re-render', async () => {
116+
const { instance } = await createDataGrid({
117+
dataSource: [{ id: 1 }],
118+
searchPanel: { visible: true },
119+
});
120+
121+
const headerPanel = getHeaderPanel(instance);
122+
123+
headerPanel.setToolbarItem('customButton', {
124+
text: 'Original',
125+
location: 'after',
126+
name: 'customButton',
127+
});
128+
jest.runAllTimers();
129+
headerPanel.render();
130+
131+
const items = headerPanel._toolbar?.option('items') ?? [];
132+
expect(items[0].text).toBe('Original');
133+
134+
headerPanel.setToolbarItem('customButton', {
135+
text: 'Updated',
136+
location: 'after',
137+
name: 'customButton',
138+
});
139+
140+
const updatedItems = headerPanel._toolbar?.option('items') ?? [];
141+
expect(updatedItems[0].text).toBe('Updated');
142+
});
143+
88144
it('should not call _invalidate when setting item before first render', async () => {
89145
const { instance } = await createDataGrid({
90146
dataSource: [{ id: 1 }],

packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class HeaderPanel extends ColumnsView {
2626

2727
private _toolbarOptions?: ToolbarProperties;
2828

29-
private readonly _registeredToolbarItems = new Map<string, ToolbarItem>();
29+
private readonly registeredToolbarItems = new Map<string, ToolbarItem>();
3030

3131
protected _editingController!: EditingController;
3232

@@ -42,16 +42,52 @@ export class HeaderPanel extends ColumnsView {
4242
}
4343

4444
public setToolbarItem(name: string, item: ToolbarItem): void {
45-
this._registeredToolbarItems.set(name, { ...item, name });
45+
const isExisting = this.registeredToolbarItems.has(name);
46+
this.registeredToolbarItems.set(name, { ...item, name });
4647

47-
if (this._$element) {
48+
if (!this._$element) {
49+
return;
50+
}
51+
52+
const itemIndex = isExisting ? this.findToolbarItemIndex(name) : -1;
53+
54+
if (itemIndex >= 0) {
55+
const normalizedItem = this.getNormalizedRegisteredItem(name);
56+
this._toolbar?.option(`items[${itemIndex}]`, normalizedItem);
57+
} else {
4858
this._invalidate();
4959
}
5060
}
5161

62+
private findToolbarItemIndex(name: string): number {
63+
const items: ToolbarItem[] = this._toolbar?.option('items') ?? [];
64+
65+
return items.findIndex((i) => i.name === name);
66+
}
67+
68+
private getNormalizedRegisteredItem(name: string): ToolbarItem | undefined {
69+
const registeredItem = this.registeredToolbarItems.get(name);
70+
71+
const userToolbarOptions = this.option('toolbar');
72+
73+
const userItem = userToolbarOptions?.items?.find(
74+
(ui) => (typeof ui === 'string' ? ui === name : ui?.name === name),
75+
);
76+
77+
if (!userItem) {
78+
return registeredItem;
79+
}
80+
81+
return normalizeToolbarItems(
82+
[registeredItem] as DefaultToolbarItem[],
83+
[userItem],
84+
DEFAULT_TOOLBAR_ITEM_NAMES,
85+
)[0];
86+
}
87+
5288
public removeToolbarItem(name: string): void {
53-
if (this._registeredToolbarItems.has(name)) {
54-
this._registeredToolbarItems.delete(name);
89+
if (this.registeredToolbarItems.has(name)) {
90+
this.registeredToolbarItems.delete(name);
5591

5692
if (this._$element) {
5793
this._invalidate();
@@ -63,11 +99,11 @@ export class HeaderPanel extends ColumnsView {
6399
* @extended: column_chooser, editing, filter_row
64100
*/
65101
protected _getToolbarItems(): ToolbarItem[] {
66-
return Array.from(this._registeredToolbarItems.values());
102+
return Array.from(this.registeredToolbarItems.values());
67103
}
68104

69105
// eslint-disable-next-line class-methods-use-this
70-
private _sortToolbarItems(items: ToolbarItem[]): ToolbarItem[] {
106+
private sortToolbarItems(items: ToolbarItem[]): ToolbarItem[] {
71107
return [...items].sort((a, b) => (a.sortIndex ?? 0) - (b.sortIndex ?? 0));
72108
}
73109

@@ -83,7 +119,7 @@ export class HeaderPanel extends ColumnsView {
83119

84120
private _getToolbarOptions(): ToolbarProperties<DefaultToolbarItem | ToolbarItem> {
85121
const { toolbar: userToolbarOptions } = this.option();
86-
const sortedToolbarItems: ToolbarItem[] = this._sortToolbarItems(this._getToolbarItems());
122+
const sortedToolbarItems: ToolbarItem[] = this.sortToolbarItems(this._getToolbarItems());
87123

88124
const options: { toolbarOptions: ToolbarProperties<DefaultToolbarItem | ToolbarItem> } = {
89125
toolbarOptions: {

packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,6 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo
133133

134134
protected _editingController!: Controllers['editing'];
135135

136-
private _headerPanel!: Views['headerPanel'];
137-
138136
protected _rowsView!: Views['rowsView'];
139137

140138
private _editorFactory!: Controllers['editorFactory'];
@@ -145,19 +143,21 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo
145143

146144
private _columnResizerController!: Controllers['columnsResizer'];
147145

146+
private searchPanel!: Controllers['searchPanel'];
147+
148148
private _needNavigationToCell = false;
149149

150150
// #region Initialization
151151
public init() {
152152
this._dataController = this.getController('data');
153153
this._selectionController = this.getController('selection');
154154
this._editingController = this.getController('editing');
155-
this._headerPanel = this.getView('headerPanel');
156155
this._editorFactory = this.getController('editorFactory');
157156
this._focusController = this.getController('focus');
158157
this._adaptiveColumnsController = this.getController('adaptiveColumns');
159158
this._columnResizerController = this.getController('columnsResizer');
160159
this._rowsView = this.getView('rowsView');
160+
this.searchPanel = this.getController('searchPanel');
161161

162162
super.init();
163163

@@ -1225,9 +1225,9 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo
12251225
return false;
12261226
}
12271227

1228-
private _ctrlFKeyHandler(eventArgs) {
1228+
private _ctrlFKeyHandler(eventArgs): void {
12291229
if (this.option('searchPanel.visible')) {
1230-
const searchTextEditor = this.getController('searchPanel').getSearchTextEditor();
1230+
const searchTextEditor = this.searchPanel.getSearchTextEditor();
12311231
if (searchTextEditor) {
12321232
searchTextEditor.focus();
12331233
eventArgs.originalEvent.preventDefault();

packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts

Lines changed: 51 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const dataController = (
7171
return gridCoreUtils.combineFilters([filter, searchFilter]);
7272
}
7373

74-
private searchByText(text): void {
74+
public searchByText(text: string | undefined): void {
7575
this.option('searchPanel.text', text);
7676
}
7777

@@ -136,16 +136,18 @@ const dataController = (
136136
}
137137
};
138138

139+
type SearchDataControllerExtender = InstanceType<ReturnType<typeof dataController>>;
140+
139141
export class SearchPanelViewController extends modules.ViewController {
140-
private _headerPanel?: HeaderPanel;
142+
private headerPanel?: HeaderPanel;
141143

142-
private _dataController?: DataController;
144+
private dataController?: SearchDataControllerExtender;
143145

144146
public init(): void {
145-
this._headerPanel = this.getView('headerPanel');
146-
this._dataController = this.getController('data');
147+
this.headerPanel = this.getView('headerPanel');
148+
this.dataController = this.getController('data') as SearchDataControllerExtender;
147149

148-
this._syncSearchPanelItem();
150+
this.syncSearchPanelItem();
149151
}
150152

151153
public optionChanged(args: OptionChanged): void {
@@ -156,7 +158,7 @@ export class SearchPanelViewController extends modules.ViewController {
156158
editor.option('value', args.value);
157159
}
158160
} else {
159-
this._syncSearchPanelItem();
161+
this.syncSearchPanelItem();
160162
}
161163

162164
args.handled = true;
@@ -165,79 +167,70 @@ export class SearchPanelViewController extends modules.ViewController {
165167
}
166168
}
167169

168-
private _syncSearchPanelItem(): void {
169-
if (!this._headerPanel) {
170+
private syncSearchPanelItem(): void {
171+
if (!this.headerPanel) {
170172
return;
171173
}
172174

173-
const { searchPanel: searchPanelOptions } = this.option();
175+
const searchPanelOptions = this.option('searchPanel');
174176

175177
if (searchPanelOptions && searchPanelOptions.visible) {
176-
const searchPanelToolbarItem = this._getSearchPanelToolbarItem();
178+
const searchPanelToolbarItem = this.getSearchPanelToolbarItem();
177179

178180
if (searchPanelToolbarItem) {
179-
this._headerPanel.setToolbarItem(SEARCH_PANEL_ITEM_NAME, searchPanelToolbarItem);
181+
this.headerPanel.setToolbarItem(SEARCH_PANEL_ITEM_NAME, searchPanelToolbarItem);
180182
}
181183
} else {
182-
this._headerPanel.removeToolbarItem(SEARCH_PANEL_ITEM_NAME);
184+
this.headerPanel.removeToolbarItem(SEARCH_PANEL_ITEM_NAME);
183185
}
184186
}
185187

186-
private _getSearchPanelToolbarItem(): ToolbarItem | null {
187-
const { searchPanel: searchPanelOptions } = this.option();
188-
189-
if (this._headerPanel && searchPanelOptions && searchPanelOptions.visible) {
190-
return {
191-
template: (data, index, container: dxElementWrapper | Element): void => {
192-
if (this._headerPanel) {
193-
const $search = $('<div>')
194-
.addClass(this._headerPanel.addWidgetPrefix(SEARCH_PANEL_CLASS))
195-
.appendTo(container);
196-
197-
this.getController('editorFactory').createEditor($search, {
198-
width: searchPanelOptions.width,
199-
placeholder: searchPanelOptions.placeholder,
200-
parentType: 'searchPanel',
201-
value: this.option('searchPanel.text'),
202-
updateValueTimeout: FILTERING_TIMEOUT,
203-
setValue: (value) => {
204-
// @ts-expect-error
205-
this._dataController.searchByText(value);
206-
},
207-
editorOptions: {
208-
inputAttr: {
209-
'aria-label': messageLocalization.format(`${this.component.NAME}-ariaSearchInGrid`),
210-
},
211-
},
212-
});
188+
private getSearchPanelToolbarItem(): ToolbarItem {
189+
const searchPanelOptions = this.option('searchPanel');
213190

214-
this._headerPanel.resize();
215-
}
216-
},
217-
name: SEARCH_PANEL_ITEM_NAME,
218-
location: 'after',
219-
locateInMenu: 'never',
220-
sortIndex: 50,
221-
};
222-
}
191+
return {
192+
template: (_data, _index, container: dxElementWrapper | Element): void => {
193+
if (this.headerPanel) {
194+
const $search = $('<div>')
195+
.addClass(this.headerPanel.addWidgetPrefix(SEARCH_PANEL_CLASS))
196+
.appendTo(container);
197+
198+
this.getController('editorFactory').createEditor($search, {
199+
width: searchPanelOptions?.width,
200+
placeholder: searchPanelOptions?.placeholder,
201+
parentType: 'searchPanel',
202+
value: this.option('searchPanel.text'),
203+
updateValueTimeout: FILTERING_TIMEOUT,
204+
setValue: (value: string | undefined): void => {
205+
this.dataController?.searchByText(value);
206+
},
207+
editorOptions: {
208+
inputAttr: {
209+
'aria-label': messageLocalization.format(`${this.component.NAME}-ariaSearchInGrid`),
210+
},
211+
},
212+
});
223213

224-
return null;
214+
this.headerPanel.resize();
215+
}
216+
},
217+
name: SEARCH_PANEL_ITEM_NAME,
218+
location: 'after',
219+
locateInMenu: 'never',
220+
sortIndex: 50,
221+
};
225222
}
226223

227224
public getSearchTextEditor(): TextBox | null {
228-
if (!this._headerPanel) {
229-
return null;
230-
}
231-
232-
const $element = this._headerPanel.element();
225+
const $element = this.headerPanel?.element();
233226

234-
if (!$element) {
227+
if (!this.headerPanel || !$element) {
235228
return null;
236229
}
237230

238-
const headerPanelClass = this._headerPanel.addWidgetPrefix(HEADER_PANEL_CLASS);
231+
const headerPanelClass = this.headerPanel.addWidgetPrefix(HEADER_PANEL_CLASS);
239232
const $searchPanel = $element
240-
.find(`.${this._headerPanel.addWidgetPrefix(SEARCH_PANEL_CLASS)}`)
233+
.find(`.${this.headerPanel.addWidgetPrefix(SEARCH_PANEL_CLASS)}`)
241234
.filter((_, el: HTMLElement) => $(el).closest(`.${headerPanelClass}`).is($element));
242235

243236
if ($searchPanel.length) {

0 commit comments

Comments
 (0)