Repository: electron/electron
Pull request: fix: menu should allow focused `BaseWindow` where possible

#### Description of Change

Closes https://github.com/electron/electron/issues/43398.

Most `windowMethod` functions in MenuItem allow `BaseWindow`s with few exceptions. This PR shifts `instanceof BrowserWindow` checks to the `windowMethod`s that explicitly don't work with `BaseWindow` so that `BaseWindows` can at least be used with some of them.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fixed `MenuItem.click` handler not setting `focusedWindow` parameter when focused window is a `BaseWindow`.


Changed files:
filename: docs/api/menu-item.md
patch: @@ -10,9 +10,9 @@ See [`Menu`](menu.md) for examples.
 
 * `options` Object
   * `click` Function (optional) - Will be called with
-    `click(menuItem, browserWindow, event)` when the menu item is clicked.
+    `click(menuItem, window, event)` when the menu item is clicked.
     * `menuItem` MenuItem
-    * `browserWindow` [BrowserWindow](browser-window.md) | undefined - This will not be defined if no window is open.
+    * `window` [BaseWindow](base-window.md) | undefined - This will not be defined if no window is open.
     * `event` [KeyboardEvent](structures/keyboard-event.md)
   * `role` string (optional) - Can be `undo`, `redo`, `cut`, `copy`, `paste`, `pasteAndMatchStyle`, `delete`, `selectAll`, `reload`, `forceReload`, `toggleDevTools`, `resetZoom`, `zoomIn`, `zoomOut`, `toggleSpellChecker`, `togglefullscreen`, `window`, `minimize`, `close`, `help`, `about`, `services`, `hide`, `hideOthers`, `unhide`, `quit`, `showSubstitutions`, `toggleSmartQuotes`, `toggleSmartDashes`, `toggleTextReplacement`, `startSpeaking`, `stopSpeaking`, `zoom`, `front`, `appMenu`, `fileMenu`, `editMenu`, `viewMenu`, `shareMenu`, `recentDocuments`, `toggleTabBar`, `selectNextTab`, `selectPreviousTab`, `showAllTabs`, `mergeAllWindows`, `clearRecentDocuments`, `moveTabToNewWindow` or `windowMenu` - Define the action of the menu item, when specified the
     `click` property will be ignored. See [roles](#roles).
@@ -146,7 +146,7 @@ A `Function` that is fired when the MenuItem receives a click event.
 It can be called with `menuItem.click(event, focusedWindow, focusedWebContents)`.
 
 * `event` [KeyboardEvent](structures/keyboard-event.md)
-* `focusedWindow` [BrowserWindow](browser-window.md)
+* `focusedWindow` [BaseWindow](browser-window.md)
 * `focusedWebContents` [WebContents](web-contents.md)
 
 #### `menuItem.submenu`

filename: lib/browser/api/menu-item-roles.ts
patch: @@ -1,4 +1,4 @@
-import { app, BrowserWindow, session, webContents, WebContents, MenuItemConstructorOptions } from 'electron/main';
+import { app, BaseWindow, BrowserWindow, session, webContents, WebContents, MenuItemConstructorOptions } from 'electron/main';
 
 const isMac = process.platform === 'darwin';
 const isWindows = process.platform === 'win32';
@@ -13,7 +13,7 @@ interface Role {
   label: string;
   accelerator?: string;
   checked?: boolean;
-  windowMethod?: ((window: BrowserWindow) => void);
+  windowMethod?: ((window: BaseWindow) => void);
   webContentsMethod?: ((webContents: WebContents) => void);
   appMethod?: () => void;
   registerAccelerator?: boolean;
@@ -53,8 +53,10 @@ export const roleList: Record<RoleId, Role> = {
     label: 'Force Reload',
     accelerator: 'Shift+CmdOrCtrl+R',
     nonNativeMacOSRole: true,
-    windowMethod: (window: BrowserWindow) => {
-      window.webContents.reloadIgnoringCache();
+    windowMethod: (window: BaseWindow) => {
+      if (window instanceof BrowserWindow) {
+        window.webContents.reloadIgnoringCache();
+      }
     }
   },
   front: {
@@ -110,7 +112,11 @@ export const roleList: Record<RoleId, Role> = {
     label: 'Reload',
     accelerator: 'CmdOrCtrl+R',
     nonNativeMacOSRole: true,
-    windowMethod: w => w.reload()
+    windowMethod: (w: BaseWindow) => {
+      if (w instanceof BrowserWindow) {
+        w.reload();
+      }
+    }
   },
   resetzoom: {
     label: 'Actual Size',
@@ -164,7 +170,7 @@ export const roleList: Record<RoleId, Role> = {
   togglefullscreen: {
     label: 'Toggle Full Screen',
     accelerator: isMac ? 'Control+Command+F' : 'F11',
-    windowMethod: (window: BrowserWindow) => {
+    windowMethod: (window: BaseWindow) => {
       window.setFullScreen(!window.isFullScreen());
     }
   },
@@ -361,7 +367,7 @@ export function getDefaultSubmenu (role: RoleId) {
   return submenu;
 }
 
-export function execute (role: RoleId, focusedWindow: BrowserWindow, focusedWebContents: WebContents) {
+export function execute (role: RoleId, focusedWindow: BaseWindow, focusedWebContents: WebContents) {
   if (!canExecuteRole(role)) return false;
 
   const { appMethod, webContentsMethod, windowMethod } = roleList[role];

filename: lib/browser/api/menu-item.ts
patch: @@ -1,5 +1,5 @@
 import * as roles from '@electron/internal/browser/api/menu-item-roles';
-import { Menu, BrowserWindow, WebContents, KeyboardEvent } from 'electron/main';
+import { Menu, BaseWindow, WebContents, KeyboardEvent } from 'electron/main';
 
 let nextCommandId = 0;
 
@@ -53,7 +53,7 @@ const MenuItem = function (this: any, options: any) {
   });
 
   const click = options.click;
-  this.click = (event: KeyboardEvent, focusedWindow: BrowserWindow, focusedWebContents: WebContents) => {
+  this.click = (event: KeyboardEvent, focusedWindow: BaseWindow, focusedWebContents: WebContents) => {
     // Manually flip the checked flags when clicked.
     if (!roles.shouldOverrideCheckStatus(this.role) &&
         (this.type === 'checkbox' || this.type === 'radio')) {

filename: lib/browser/api/menu.ts
patch: @@ -1,4 +1,4 @@
-import { BaseWindow, MenuItem, webContents, Menu as MenuType, BrowserWindow, MenuItemConstructorOptions } from 'electron/main';
+import { BaseWindow, MenuItem, webContents, Menu as MenuType, MenuItemConstructorOptions } from 'electron/main';
 import { sortMenuItems } from '@electron/internal/browser/api/menu-utils';
 import { setApplicationMenuWasSet } from '@electron/internal/browser/default-menu';
 
@@ -54,7 +54,7 @@ Menu.prototype._executeCommand = function (event, id) {
   const command = this.commandsMap[id];
   if (!command) return;
   const focusedWindow = BaseWindow.getFocusedWindow();
-  command.click(event, focusedWindow instanceof BrowserWindow ? focusedWindow : undefined, webContents.getFocusedWebContents());
+  command.click(event, focusedWindow, webContents.getFocusedWebContents());
 };
 
 Menu.prototype._menuWillShow = function () {

filename: spec/ts-smoke/electron/main.ts
patch: @@ -717,7 +717,7 @@ const template = <Electron.MenuItemConstructorOptions[]> [
         label: 'Reload',
         accelerator: 'Command+R',
         click: (item, focusedWindow) => {
-          if (focusedWindow) {
+          if (focusedWindow instanceof BrowserWindow) {
             focusedWindow.webContents.reloadIgnoringCache();
           }
         }
@@ -726,7 +726,7 @@ const template = <Electron.MenuItemConstructorOptions[]> [
         label: 'Toggle DevTools',
         accelerator: 'Alt+Command+I',
         click: (item, focusedWindow) => {
-          if (focusedWindow) {
+          if (focusedWindow instanceof BrowserWindow) {
             focusedWindow.webContents.toggleDevTools();
           }
         }
@@ -738,7 +738,7 @@ const template = <Electron.MenuItemConstructorOptions[]> [
         label: 'Actual Size',
         accelerator: 'CmdOrCtrl+0',
         click: (item, focusedWindow) => {
-          if (focusedWindow) {
+          if (focusedWindow instanceof BrowserWindow) {
             focusedWindow.webContents.zoomLevel = 0;
           }
         }
@@ -747,7 +747,7 @@ const template = <Electron.MenuItemConstructorOptions[]> [
         label: 'Zoom In',
         accelerator: 'CmdOrCtrl+Plus',
         click: (item, focusedWindow) => {
-          if (focusedWindow) {
+          if (focusedWindow instanceof BrowserWindow) {
             const { webContents } = focusedWindow;
             webContents.zoomLevel += 0.5;
           }
@@ -757,7 +757,7 @@ const template = <Electron.MenuItemConstructorOptions[]> [
         label: 'Zoom Out',
         accelerator: 'CmdOrCtrl+-',
         click: (item, focusedWindow) => {
-          if (focusedWindow) {
+          if (focusedWindow instanceof BrowserWindow) {
             const { webContents } = focusedWindow;
             webContents.zoomLevel -= 0.5;
           }