Repository: electron/electron
Pull request: fix: default accelerator for role-based menu items

#### Description of Change

Closes https://github.com/electron/electron/issues/48313.

Role-based menu items (copy, paste, cut, etc.) were returning `null` for their accelerator property, even though these roles have default keyboard shortcuts defined in `menu-item-roles.ts`.

To fix this, pass `roles.getDefaultAccelerator(this.role)` as the default value to `overrideReadOnlyProperty('accelerator')` so that role-based items correctly expose their expected accelerators.

Also fixes an incorrect type in `menu-item.md`.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [x] relevant API documentation, tutorials, and examples are updated and follow the [documentation style guide](https://github.com/electron/electron/blob/main/docs/development/style-guide.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 an issue where role-based menu items were incorrectly returning `null` for their accelerator property.

Changed files:
filename: docs/api/menu-item.md
patch: @@ -107,7 +107,7 @@ A `string` (optional) indicating the item's role, if set. Can be `undo`, `redo`,
 
 #### `menuItem.accelerator`
 
-An `Accelerator` (optional) indicating the item's accelerator, if set.
+An `Accelerator | null` indicating the item's accelerator, if set.
 
 #### `menuItem.userAccelerator` _Readonly_ _macOS_
 

filename: lib/browser/api/menu-item-roles.ts
patch: @@ -353,6 +353,7 @@ export function shouldOverrideCheckStatus (role: RoleId) {
 
 export function getDefaultAccelerator (role: RoleId) {
   if (hasRole(role)) return roleList[role].accelerator;
+  return undefined;
 }
 
 export function shouldRegisterAccelerator (role: RoleId) {

filename: lib/browser/api/menu-item.ts
patch: @@ -25,7 +25,7 @@ const MenuItem = function (this: any, options: any) {
 
   this.overrideReadOnlyProperty('type', roles.getDefaultType(this.role));
   this.overrideReadOnlyProperty('role');
-  this.overrideReadOnlyProperty('accelerator');
+  this.overrideReadOnlyProperty('accelerator', roles.getDefaultAccelerator(this.role));
   this.overrideReadOnlyProperty('icon');
   this.overrideReadOnlyProperty('submenu');
 

filename: spec/api-menu-item-spec.ts
patch: @@ -43,6 +43,65 @@ describe('MenuItems', () => {
       expect(item).to.have.property('role').that.is.a('string');
       expect(item).to.have.property('icon');
     });
+
+    it('should have a default accelerator for certain roles', () => {
+      const items: Record<string, Electron.MenuItem['accelerator']> = {
+        undo: 'CommandOrControl+Z',
+        redo: process.platform === 'win32' ? 'Control+Y' : 'Shift+CommandOrControl+Z',
+        cut: 'CommandOrControl+X',
+        copy: 'CommandOrControl+C',
+        paste: 'CommandOrControl+V',
+        pasteAndMatchStyle: process.platform === 'darwin' ? 'Cmd+Option+Shift+V' : 'Shift+CommandOrControl+V',
+        delete: null,
+        selectAll: 'CommandOrControl+A',
+        reload: 'CmdOrCtrl+R',
+        forceReload: 'Shift+CmdOrCtrl+R',
+        toggleDevTools: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
+        resetZoom: 'CommandOrControl+0',
+        zoomIn: 'CommandOrControl+Plus',
+        zoomOut: 'CommandOrControl+-',
+        toggleSpellChecker: null,
+        togglefullscreen: process.platform === 'darwin' ? 'Control+Command+F' : 'F11',
+        window: null,
+        minimize: 'CommandOrControl+M',
+        close: 'CommandOrControl+W',
+        help: null,
+        about: null,
+        services: null,
+        hide: 'Command+H',
+        hideOthers: 'Command+Alt+H',
+        unhide: null,
+        quit: process.platform === 'win32' ? null : 'CommandOrControl+Q',
+        showSubstitutions: null,
+        toggleSmartQuotes: null,
+        toggleSmartDashes: null,
+        toggleTextReplacement: null,
+        startSpeaking: null,
+        stopSpeaking: null,
+        zoom: null,
+        front: null,
+        appMenu: null,
+        fileMenu: null,
+        editMenu: null,
+        viewMenu: null,
+        shareMenu: null,
+        recentDocuments: null,
+        toggleTabBar: null,
+        selectNextTab: null,
+        selectPreviousTab: null,
+        showAllTabs: null,
+        mergeAllWindows: null,
+        clearRecentDocuments: null,
+        moveTabToNewWindow: null,
+        windowMenu: null
+      };
+
+      for (const role in items) {
+        if (!Object.hasOwn(items, role)) continue;
+        const item = new MenuItem({ role: role as any });
+        expect(item.accelerator).to.equal(items[role]);
+      }
+    });
   });
 
   describe('MenuItem.click', () => {
@@ -480,7 +539,7 @@ describe('MenuItems', () => {
 
     it('should display modifiers correctly for simple keys', () => {
       const menu = Menu.buildFromTemplate([
-        { label: 'text', accelerator: 'CmdOrCtrl+A' },
+        { label: 'text', accelerator: 'CommandOrControl+A' },
         { label: 'text', accelerator: 'Shift+A' },
         { label: 'text', accelerator: 'Alt+A' }
       ]);
@@ -492,7 +551,7 @@ describe('MenuItems', () => {
 
     it('should display modifiers correctly for special keys', () => {
       const menu = Menu.buildFromTemplate([
-        { label: 'text', accelerator: 'CmdOrCtrl+Tab' },
+        { label: 'text', accelerator: 'CommandOrControl+Tab' },
         { label: 'text', accelerator: 'Shift+Tab' },
         { label: 'text', accelerator: 'Alt+Tab' }
       ]);