Add before-input-event event for webContents (fixes #7586)
Embedding arbitrary web content is problematic when it comes to keyboard shortcuts because: * Web content can steal app shortcuts (see e.g. brave/browser-laptop#4408) * Blocked web content (e.g. a focused <webview> performing expensive computation) will also prevent app shortcuts from firing immediately The new before-input-event event can be used to overcome these issues by always handle certain keyboard events in the main process. Note that this requires electron/brightray#261 to compile.
This commit is contained in:
parent
3290c6b335
commit
a3b65ad481
4 changed files with 170 additions and 0 deletions
|
@ -70,6 +70,7 @@
|
|||
#include "third_party/WebKit/public/web/WebFindOptions.h"
|
||||
#include "third_party/WebKit/public/web/WebInputEvent.h"
|
||||
#include "ui/display/screen.h"
|
||||
#include "ui/events/keycodes/dom/keycode_converter.h"
|
||||
|
||||
#if !defined(OS_MACOSX)
|
||||
#include "ui/aura/window.h"
|
||||
|
@ -488,6 +489,34 @@ void WebContents::HandleKeyboardEvent(
|
|||
}
|
||||
}
|
||||
|
||||
bool WebContents::PreHandleKeyboardEvent(
|
||||
content::WebContents* source,
|
||||
const content::NativeWebKeyboardEvent& event,
|
||||
bool* is_keyboard_shortcut) {
|
||||
const char* type =
|
||||
event.type == blink::WebInputEvent::Type::RawKeyDown ? "keyDown" :
|
||||
event.type == blink::WebInputEvent::Type::KeyUp ? "keyUp" :
|
||||
nullptr;
|
||||
if (!type) {
|
||||
// This should never happen.
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
mate::Dictionary dict = mate::Dictionary::CreateEmpty(isolate());
|
||||
dict.Set("type", type);
|
||||
dict.Set("key", ui::KeycodeConverter::DomKeyToKeyString(event.domKey));
|
||||
|
||||
using Modifiers = blink::WebInputEvent::Modifiers;
|
||||
dict.Set("isAutoRepeat", (event.modifiers & Modifiers::IsAutoRepeat) != 0);
|
||||
dict.Set("shift", (event.modifiers & Modifiers::ShiftKey) != 0);
|
||||
dict.Set("control", (event.modifiers & Modifiers::ControlKey) != 0);
|
||||
dict.Set("alt", (event.modifiers & Modifiers::AltKey) != 0);
|
||||
dict.Set("meta", (event.modifiers & Modifiers::MetaKey) != 0);
|
||||
|
||||
return Emit("before-input-event", dict);
|
||||
}
|
||||
|
||||
void WebContents::EnterFullscreenModeForTab(content::WebContents* source,
|
||||
const GURL& origin) {
|
||||
auto permission_helper =
|
||||
|
|
|
@ -244,6 +244,9 @@ class WebContents : public mate::TrackableObject<WebContents>,
|
|||
void HandleKeyboardEvent(
|
||||
content::WebContents* source,
|
||||
const content::NativeWebKeyboardEvent& event) override;
|
||||
bool PreHandleKeyboardEvent(content::WebContents* source,
|
||||
const content::NativeWebKeyboardEvent& event,
|
||||
bool* is_keyboard_shortcut) override;
|
||||
void EnterFullscreenModeForTab(content::WebContents* source,
|
||||
const GURL& origin) override;
|
||||
void ExitFullscreenModeForTab(content::WebContents* source) override;
|
||||
|
|
|
@ -232,6 +232,26 @@ Emitted when a plugin process has crashed.
|
|||
|
||||
Emitted when `webContents` is destroyed.
|
||||
|
||||
#### Event: 'before-input-event'
|
||||
|
||||
Returns:
|
||||
|
||||
* `event` Event
|
||||
* `input` Object - Input properties
|
||||
* `type` String - Either `keyUp` or `keyDown`
|
||||
* `key` String - Equivalent to [KeyboardEvent.key](keyboardevent)
|
||||
* `isAutoRepeat` Boolean - Equivalent to [KeyboardEvent.repeat](keyboardevent)
|
||||
* `shift` Boolean - Equivalent to [KeyboardEvent.shiftKey](keyboardevent)
|
||||
* `control` Boolean - Equivalent to [KeyboardEvent.controlKey](keyboardevent)
|
||||
* `alt` Boolean - Equivalent to [KeyboardEvent.altKey](keyboardevent)
|
||||
* `meta` Boolean - Equivalent to [KeyboardEvent.metaKey](keyboardevent)
|
||||
|
||||
Emitted before dispatching the `keydown` and `keyup` events in the page.
|
||||
Calling `event.preventDefault` will prevent the page `keydown`/`keyup` events
|
||||
from being dispatched.
|
||||
|
||||
[keyboardevent]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
|
||||
|
||||
#### Event: 'devtools-opened'
|
||||
|
||||
Emitted when DevTools is opened.
|
||||
|
|
|
@ -98,6 +98,124 @@ describe('webContents module', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('will-navigate event', function () {
|
||||
it('can be prevented', (done) => {
|
||||
const targetURL = 'file://' + path.join(__dirname, 'fixtures', 'pages', 'location-change.html')
|
||||
w.loadURL(targetURL)
|
||||
w.webContents.once('did-finish-load', () => {
|
||||
|
||||
w.webContents.on('will-navigate', (event, url) => {
|
||||
assert.ok(url, targetURL)
|
||||
})
|
||||
|
||||
setTimeout(done, 5000)
|
||||
|
||||
w.webContents.on('did-navigate', (event, url) => {
|
||||
assert.ok(url, targetURL)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('before-input-event event', () => {
|
||||
it('can prevent document keyboard events', (done) => {
|
||||
w.loadURL('file://' + path.join(__dirname, 'fixtures', 'pages', 'key-events.html'))
|
||||
w.webContents.once('did-finish-load', () => {
|
||||
w.webContents.once('before-input-event', (event, input) => {
|
||||
assert.equal(input.key, 'a')
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
ipcMain.once('keydown', (event, key) => {
|
||||
assert.equal(key, 'b')
|
||||
done()
|
||||
})
|
||||
|
||||
w.webContents.sendInputEvent({type: 'keyDown', keyCode: 'a'})
|
||||
w.webContents.sendInputEvent({type: 'keyDown', keyCode: 'b'})
|
||||
})
|
||||
})
|
||||
|
||||
it('has the correct properties', (done) => {
|
||||
w.loadURL('file://' + path.join(__dirname, 'fixtures', 'pages', 'base-page.html'))
|
||||
w.webContents.once('did-finish-load', () => {
|
||||
const testBeforeInput = (opts) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
w.webContents.once('before-input-event', (event, input) => {
|
||||
assert.equal(input.type, opts.type)
|
||||
assert.equal(input.key, opts.key)
|
||||
assert.equal(input.isAutoRepeat, opts.isAutoRepeat)
|
||||
assert.equal(input.shift, opts.shift)
|
||||
assert.equal(input.control, opts.control)
|
||||
assert.equal(input.alt, opts.alt)
|
||||
assert.equal(input.meta, opts.meta)
|
||||
resolve()
|
||||
})
|
||||
|
||||
const modifiers = []
|
||||
if (opts.shift) modifiers.push('shift')
|
||||
if (opts.control) modifiers.push('control')
|
||||
if (opts.alt) modifiers.push('alt')
|
||||
if (opts.meta) modifiers.push('meta')
|
||||
if (opts.isAutoRepeat) modifiers.push('isAutoRepeat')
|
||||
|
||||
w.webContents.sendInputEvent({
|
||||
type: opts.type,
|
||||
keyCode: opts.keyCode,
|
||||
modifiers: modifiers
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
return testBeforeInput({
|
||||
type: 'keyDown',
|
||||
key: 'A',
|
||||
keyCode: 'a',
|
||||
shift: true,
|
||||
control: true,
|
||||
alt: true,
|
||||
meta: true,
|
||||
isAutoRepeat: true
|
||||
})
|
||||
}).then(() => {
|
||||
return testBeforeInput({
|
||||
type: 'keyUp',
|
||||
key: '.',
|
||||
keyCode: '.',
|
||||
shift: false,
|
||||
control: true,
|
||||
alt: true,
|
||||
meta: false,
|
||||
isAutoRepeat: false
|
||||
})
|
||||
}).then(() => {
|
||||
return testBeforeInput({
|
||||
type: 'keyUp',
|
||||
key: '!',
|
||||
keyCode: '1',
|
||||
shift: true,
|
||||
control: false,
|
||||
alt: false,
|
||||
meta: true,
|
||||
isAutoRepeat: false
|
||||
})
|
||||
}).then(() => {
|
||||
return testBeforeInput({
|
||||
type: 'keyUp',
|
||||
key: 'Tab',
|
||||
keyCode: 'Tab',
|
||||
shift: false,
|
||||
control: true,
|
||||
alt: false,
|
||||
meta: false,
|
||||
isAutoRepeat: true
|
||||
})
|
||||
}).then(done).catch(done)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendInputEvent(event)', function () {
|
||||
beforeEach(function (done) {
|
||||
w.loadURL('file://' + path.join(__dirname, 'fixtures', 'pages', 'key-events.html'))
|
||||
|
|
Loading…
Reference in a new issue