Skip to content

Commit 2b29ab1

Browse files
author
Tajudeen
committed
Fix menubar dropdown infinite loop and visibility on Windows
- Remove focus() calls from cleanupCustomMenu to prevent infinite recursion - Add re-entrancy guard to prevent recursive cleanup calls - Fix menu visibility and height calculation on Windows - Ensure proper z-index and positioning when menu is appended to document.body - Let Menu widget handle theming via IMenuStyles (no color overrides) - Add validation for empty actions before creating menus - Fix bounding rect fallback for invalid measurements
1 parent 9c0bda4 commit 2b29ab1

File tree

2 files changed

+73
-25
lines changed

2 files changed

+73
-25
lines changed

src/vs/base/browser/ui/menu/menubar.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,28 @@ body > .menubar-menu-items-holder.monaco-menu-container {
118118
isolation: auto;
119119
/* Ensure menu appears above all workbench content including titlebar with backdrop-filter */
120120
will-change: auto;
121+
/* Ensure menu container doesn't interfere with theme - Menu widget handles theming */
122+
background: transparent !important;
123+
}
124+
125+
/* Ensure proper height calculation for menu content on Windows */
126+
/* Note: Menu widget applies colors via IMenuStyles, so we only fix sizing here */
127+
body > .menubar-menu-items-holder.monaco-menu-container .monaco-scrollable-element {
128+
/* Ensure proper height calculation - let content determine height */
129+
height: auto !important;
130+
min-height: fit-content !important;
131+
}
132+
133+
body > .menubar-menu-items-holder.monaco-menu-container .monaco-menu {
134+
/* Ensure proper height calculation */
135+
height: auto !important;
136+
min-height: fit-content !important;
137+
}
138+
139+
body > .menubar-menu-items-holder.monaco-menu-container .actions-container {
140+
/* Ensure proper height calculation */
141+
height: auto !important;
142+
min-height: fit-content !important;
121143
}
122144

123145
.menubar .toolbar-toggle-more {

src/vs/base/browser/ui/menu/menubar.ts

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export class MenuBar extends Disposable {
8787

8888
private numMenusShown: number = 0;
8989
private overflowLayoutScheduled: IDisposable | undefined = undefined;
90+
private isCleaningUp: boolean = false;
9091

9192
private readonly menuDisposables = this._register(new DisposableStore());
9293

@@ -998,64 +999,89 @@ export class MenuBar extends Disposable {
998999
}
9991000

10001001
private cleanupCustomMenu(): void {
1001-
if (this.focusedMenu) {
1002-
// Remove focus from the menus first
1003-
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
1004-
this.overflowMenu.buttonElement.focus();
1005-
} else {
1006-
this.menus[this.focusedMenu.index].buttonElement?.focus();
1007-
}
1008-
1009-
if (this.focusedMenu.holder) {
1010-
// Remove 'open' class from button element (not from document.body on Windows)
1011-
const actualMenuIndex = this.focusedMenu.index >= this.numMenusShown ? MenuBar.OVERFLOW_INDEX : this.focusedMenu.index;
1012-
const customMenu = actualMenuIndex === MenuBar.OVERFLOW_INDEX ? this.overflowMenu : this.menus[actualMenuIndex];
1013-
customMenu.buttonElement?.classList.remove('open');
1002+
// Prevent re-entrancy - if we're already cleaning up, don't do it again
1003+
if (this.isCleaningUp) {
1004+
return;
1005+
}
10141006

1015-
this.focusedMenu.holder.remove();
1016-
}
1007+
this.isCleaningUp = true;
1008+
try {
1009+
const focusedMenu = this.focusedMenu;
1010+
if (focusedMenu) {
1011+
// Defensive check: ensure holder exists before accessing it
1012+
// This prevents errors when cleanupCustomMenu is called in edge cases
1013+
if (focusedMenu.holder) {
1014+
// Remove 'open' class from button element (not from document.body on Windows)
1015+
const actualMenuIndex = focusedMenu.index >= this.numMenusShown ? MenuBar.OVERFLOW_INDEX : focusedMenu.index;
1016+
const customMenu = actualMenuIndex === MenuBar.OVERFLOW_INDEX ? this.overflowMenu : this.menus[actualMenuIndex];
1017+
customMenu.buttonElement?.classList.remove('open');
1018+
1019+
focusedMenu.holder.remove();
1020+
}
10171021

1018-
this.focusedMenu.widget?.dispose();
1022+
focusedMenu.widget?.dispose();
10191023

1020-
this.focusedMenu = { index: this.focusedMenu.index };
1024+
// Only update focusedMenu if it hasn't been cleared by another operation
1025+
if (this.focusedMenu === focusedMenu) {
1026+
this.focusedMenu = { index: focusedMenu.index };
1027+
}
1028+
}
1029+
this.menuDisposables.clear();
1030+
} finally {
1031+
this.isCleaningUp = false;
10211032
}
1022-
this.menuDisposables.clear();
10231033
}
10241034

10251035
private showCustomMenu(menuIndex: number, selectFirst = true): void {
10261036
const actualMenuIndex = menuIndex >= this.numMenusShown ? MenuBar.OVERFLOW_INDEX : menuIndex;
10271037
const customMenu = actualMenuIndex === MenuBar.OVERFLOW_INDEX ? this.overflowMenu : this.menus[actualMenuIndex];
10281038

1029-
if (!customMenu.actions || !customMenu.buttonElement || !customMenu.titleElement) {
1039+
if (!customMenu.actions || customMenu.actions.length === 0 || !customMenu.buttonElement || !customMenu.titleElement) {
10301040
return;
10311041
}
10321042

10331043
const menuHolder = $('div.menubar-menu-items-holder', { 'title': '' });
10341044

10351045
customMenu.buttonElement.classList.add('open');
10361046

1047+
// Ensure the title element is visible and laid out before getting bounding rect
1048+
// This is critical on Windows where elements might not be laid out immediately
10371049
const titleBoundingRect = customMenu.titleElement.getBoundingClientRect();
10381050
const titleBoundingRectZoom = DOM.getDomNodeZoomLevel(customMenu.titleElement);
10391051

1052+
// Validate bounding rect - if invalid, use button element as fallback
1053+
let validBoundingRect = titleBoundingRect;
1054+
if (titleBoundingRect.width === 0 && titleBoundingRect.height === 0) {
1055+
validBoundingRect = customMenu.buttonElement.getBoundingClientRect();
1056+
}
1057+
10401058
if (this.options.compactMode?.horizontal === HorizontalDirection.Right) {
1041-
menuHolder.style.left = `${titleBoundingRect.left + this.container.clientWidth}px`;
1059+
menuHolder.style.left = `${validBoundingRect.left + this.container.clientWidth}px`;
10421060
} else if (this.options.compactMode?.horizontal === HorizontalDirection.Left) {
10431061
const windowWidth = DOM.getWindow(this.container).innerWidth;
1044-
menuHolder.style.right = `${windowWidth - titleBoundingRect.left}px`;
1062+
menuHolder.style.right = `${windowWidth - validBoundingRect.left}px`;
10451063
menuHolder.style.left = 'auto';
10461064
} else {
1047-
menuHolder.style.left = `${titleBoundingRect.left * titleBoundingRectZoom}px`;
1065+
menuHolder.style.left = `${validBoundingRect.left * titleBoundingRectZoom}px`;
10481066
}
10491067

10501068
if (this.options.compactMode?.vertical === VerticalDirection.Above) {
10511069
// TODO@benibenj Do not hardcode the height of the menu holder
1052-
menuHolder.style.top = `${titleBoundingRect.top - this.menus.length * 30 + this.container.clientHeight}px`;
1070+
menuHolder.style.top = `${validBoundingRect.top - this.menus.length * 30 + this.container.clientHeight}px`;
10531071
} else if (this.options.compactMode?.vertical === VerticalDirection.Below) {
1054-
menuHolder.style.top = `${titleBoundingRect.top}px`;
1072+
menuHolder.style.top = `${validBoundingRect.top}px`;
10551073
} else {
1056-
menuHolder.style.top = `${titleBoundingRect.bottom * titleBoundingRectZoom}px`;
1074+
menuHolder.style.top = `${validBoundingRect.bottom * titleBoundingRectZoom}px`;
10571075
}
10581076

1077+
// Ensure menu holder is visible with explicit styles
1078+
menuHolder.style.position = 'fixed';
1079+
menuHolder.style.zIndex = '10000';
1080+
menuHolder.style.visibility = 'visible';
1081+
menuHolder.style.display = 'block';
1082+
menuHolder.style.opacity = '1';
1083+
menuHolder.style.pointerEvents = 'auto';
1084+
10591085
// On Windows, append dropdown to document.body to avoid stacking context issues
10601086
// where the dropdown renders behind the workbench content
10611087
if (isWindows) {

0 commit comments

Comments
 (0)