fix(desktop): ignore auto-repeat and post-focus Ctrl+W/Ctrl+R
Holding W through a browser's own Ctrl+W keeps sending auto-repeat keyDowns once Hermes inherits focus, and each one closed a tab; a keydown synthesized on window activation could do the same. Close Tab and Reload now swallow auto-repeats and anything within 200ms of the window gaining focus. They are still claimed so the renderer's mod+w keybind can't act on them. Zoom keeps repeating. Fixes #105498 Co-authored-by: kokhlo <konstantin.khlopkov93@gmail.com>
This commit is contained in:
@@ -6624,8 +6624,23 @@ function installDevToolsShortcut(window) {
|
||||
}
|
||||
|
||||
function installPreviewShortcut(window) {
|
||||
let focusedAt = 0
|
||||
|
||||
window.on('focus', () => {
|
||||
focusedAt = Date.now()
|
||||
})
|
||||
|
||||
window.webContents.on('before-input-event', (event, input) => {
|
||||
const action = windowAcceleratorAction(input, IS_MAC)
|
||||
const action = windowAcceleratorAction(input, IS_MAC, Date.now() - focusedAt)
|
||||
|
||||
// A ⌘W/⌘R that auto-repeats or lands right as focus arrives belongs to
|
||||
// the app that just lost focus (#105498). Claim it so the renderer's
|
||||
// keybind doesn't act on it either, and do nothing.
|
||||
if (action === 'swallow') {
|
||||
event.preventDefault()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Always claim ⌘W here (the File>Close item deliberately has no
|
||||
// accelerator, so nothing else does). The renderer decides tab-vs-window
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
|
||||
|
||||
import { test } from 'vitest'
|
||||
|
||||
import { windowAcceleratorAction, type WindowAcceleratorInput } from './window-accelerator'
|
||||
import { FOCUS_GRACE_MS, windowAcceleratorAction, type WindowAcceleratorInput } from './window-accelerator'
|
||||
|
||||
function chord(overrides: Partial<WindowAcceleratorInput> = {}): WindowAcceleratorInput {
|
||||
return {
|
||||
@@ -115,3 +115,24 @@ test('folds letter case for Close Tab and Reload only', () => {
|
||||
assert.equal(windowAcceleratorAction(chord({ key: 'W', control: true }), false), 'close-tab')
|
||||
assert.equal(windowAcceleratorAction(chord({ key: 'R', meta: true }), true), 'reload')
|
||||
})
|
||||
|
||||
test('swallows an auto-repeating Ctrl+W or Ctrl+R instead of acting on it (#105498)', () => {
|
||||
assert.equal(windowAcceleratorAction(chord({ key: 'w', control: true, isAutoRepeat: true }), false), 'swallow')
|
||||
assert.equal(windowAcceleratorAction(chord({ key: 'w', meta: true, isAutoRepeat: true }), true), 'swallow')
|
||||
assert.equal(windowAcceleratorAction(chord({ key: 'r', control: true, isAutoRepeat: true }), false), 'swallow')
|
||||
})
|
||||
|
||||
test('swallows Ctrl+W and Ctrl+R that land inside the post-focus grace window (#105498)', () => {
|
||||
assert.equal(windowAcceleratorAction(chord({ key: 'w', control: true }), false, 0), 'swallow')
|
||||
assert.equal(windowAcceleratorAction(chord({ key: 'r', control: true }), false, FOCUS_GRACE_MS - 1), 'swallow')
|
||||
})
|
||||
|
||||
test('acts on Ctrl+W and Ctrl+R once the post-focus grace window has passed', () => {
|
||||
assert.equal(windowAcceleratorAction(chord({ key: 'w', control: true }), false, FOCUS_GRACE_MS), 'close-tab')
|
||||
assert.equal(windowAcceleratorAction(chord({ key: 'r', meta: true }), true, FOCUS_GRACE_MS + 50), 'reload')
|
||||
})
|
||||
|
||||
test('keeps zoom on auto-repeat and inside the focus grace window (holding Ctrl+= zooms)', () => {
|
||||
assert.equal(windowAcceleratorAction(chord({ key: '=', control: true, isAutoRepeat: true }), false), 'zoom-in')
|
||||
assert.equal(windowAcceleratorAction(chord({ key: '-', control: true }), false, 0), 'zoom-out')
|
||||
})
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
export type WindowAcceleratorAction = 'close-tab' | 'reload' | 'zoom-in' | 'zoom-out' | 'zoom-reset' | 'ignore'
|
||||
export type WindowAcceleratorAction =
|
||||
| 'close-tab'
|
||||
| 'reload'
|
||||
| 'zoom-in'
|
||||
| 'zoom-out'
|
||||
| 'zoom-reset'
|
||||
| 'swallow'
|
||||
| 'ignore'
|
||||
|
||||
/**
|
||||
* How long after the window gains focus a Close Tab / Reload keyDown is
|
||||
* treated as belonging to the app that just lost focus (#105498).
|
||||
*/
|
||||
export const FOCUS_GRACE_MS = 200
|
||||
|
||||
export interface WindowAcceleratorInput {
|
||||
alt?: boolean
|
||||
control?: boolean
|
||||
isAutoRepeat?: boolean
|
||||
key?: string
|
||||
meta?: boolean
|
||||
shift?: boolean
|
||||
@@ -15,8 +29,18 @@ export interface WindowAcceleratorInput {
|
||||
* `before-input-event` fires for keydown and keyup. A keyup that arrives after
|
||||
* Windows transfers focus (Ctrl+W started in another app) is not a chord this
|
||||
* window owns, so only `keyDown` is an accelerator.
|
||||
*
|
||||
* Close Tab and Reload are destructive, so two more keyDowns are claimed but
|
||||
* not acted on ('swallow'): auto-repeats (W held through a browser's own
|
||||
* Ctrl+W keeps repeating once this window is foreground) and anything within
|
||||
* FOCUS_GRACE_MS of focus arriving (a keydown synthesized on activation).
|
||||
* They are still claimed so the renderer's `mod+w` keybind can't act on them.
|
||||
*/
|
||||
export function windowAcceleratorAction(input: WindowAcceleratorInput, isMac: boolean): WindowAcceleratorAction {
|
||||
export function windowAcceleratorAction(
|
||||
input: WindowAcceleratorInput,
|
||||
isMac: boolean,
|
||||
msSinceFocus = Number.POSITIVE_INFINITY
|
||||
): WindowAcceleratorAction {
|
||||
if (input.type !== 'keyDown') {
|
||||
return 'ignore'
|
||||
}
|
||||
@@ -29,12 +53,12 @@ export function windowAcceleratorAction(input: WindowAcceleratorInput, isMac: bo
|
||||
return 'ignore'
|
||||
}
|
||||
|
||||
if (folded === 'w' && !input.shift) {
|
||||
return 'close-tab'
|
||||
}
|
||||
if ((folded === 'w' || folded === 'r') && !input.shift) {
|
||||
if (input.isAutoRepeat || msSinceFocus < FOCUS_GRACE_MS) {
|
||||
return 'swallow'
|
||||
}
|
||||
|
||||
if (folded === 'r' && !input.shift) {
|
||||
return 'reload'
|
||||
return folded === 'w' ? 'close-tab' : 'reload'
|
||||
}
|
||||
|
||||
if (key === '0') {
|
||||
|
||||
Reference in New Issue
Block a user