Skip to content

Commit f8bdef5

Browse files
authored
feat: enable nativeWindowOpen by default (#28552)
* feat: enable nativeWindowOpen by default * set nativeWindowOpen: false on spec/ main window * update snapshots * fix tests * fix test * fix webview test missing allowpopups * fix other test * update default
1 parent dba4df9 commit f8bdef5

9 files changed

Lines changed: 90 additions & 95 deletions

File tree

docs/api/browser-window.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,9 +349,8 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
349349
context in the dev tools by selecting the 'Electron Isolated Context'
350350
entry in the combo box at the top of the Console tab.
351351
* `nativeWindowOpen` Boolean (optional) - Whether to use native
352-
`window.open()`. Defaults to `false`. Child windows will always have node
353-
integration disabled unless `nodeIntegrationInSubFrames` is true. **Note:** This option is currently
354-
experimental.
352+
`window.open()`. Defaults to `true`. Child windows will always have node
353+
integration disabled unless `nodeIntegrationInSubFrames` is true.
355354
* `webviewTag` Boolean (optional) - Whether to enable the [`<webview>` tag](webview-tag.md).
356355
Defaults to `false`. **Note:** The
357356
`preload` script configured for the `<webview>` will have node integration

docs/api/window-open.md

Lines changed: 45 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@ untrusted content within a renderer. Windows can be created from the renderer in
66
* clicking on links or submitting forms adorned with `target=_blank`
77
* JavaScript calling `window.open()`
88

9-
In non-sandboxed renderers, or when `nativeWindowOpen` is false (the default), this results in the creation of a
10-
[`BrowserWindowProxy`](browser-window-proxy.md), a light wrapper around
11-
`BrowserWindow`.
9+
For same-origin content, the new window is created within the same process,
10+
enabling the parent to access the child window directly. This can be very
11+
useful for app sub-windows that act as preference panels, or similar, as the
12+
parent can render to the sub-window directly, as if it were a `div` in the
13+
parent. This is the same behavior as in the browser.
1214

13-
However, when the `sandbox` (or directly, `nativeWindowOpen`) option is set, a
14-
`Window` instance is created, as you'd expect in the browser. For same-origin
15-
content, the new window is created within the same process, enabling the parent
16-
to access the child window directly. This can be very useful for app sub-windows that act
17-
as preference panels, or similar, as the parent can render to the sub-window
18-
directly, as if it were a `div` in the parent.
15+
When `nativeWindowOpen` is set to false, `window.open` instead results in the
16+
creation of a [`BrowserWindowProxy`](browser-window-proxy.md), a light wrapper
17+
around `BrowserWindow`.
1918

2019
Electron pairs this native Chrome `Window` with a BrowserWindow under the hood.
2120
You can take advantage of all the customization available when creating a
@@ -69,49 +68,18 @@ window.open('https://github.com', '_blank', 'top=500,left=200,frame=false,nodeIn
6968

7069
To customize or cancel the creation of the window, you can optionally set an
7170
override handler with `webContents.setWindowOpenHandler()` from the main
72-
process. Returning `false` cancels the window, while returning an object sets
73-
the `BrowserWindowConstructorOptions` used when creating the window. Note that
74-
this is more powerful than passing options through the feature string, as the
75-
renderer has more limited privileges in deciding security preferences than the
76-
main process.
77-
78-
### `BrowserWindowProxy` example
79-
80-
```javascript
81-
82-
// main.js
83-
const mainWindow = new BrowserWindow()
84-
85-
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
86-
if (url.startsWith('https://github.com/')) {
87-
return { action: 'allow' }
88-
}
89-
return { action: 'deny' }
90-
})
91-
92-
mainWindow.webContents.on('did-create-window', (childWindow) => {
93-
// For example...
94-
childWindow.webContents.on('will-navigate', (e) => {
95-
e.preventDefault()
96-
})
97-
})
98-
```
99-
100-
```javascript
101-
// renderer.js
102-
const windowProxy = window.open('https://github.com/', null, 'minimizable=false')
103-
windowProxy.postMessage('hi', '*')
104-
```
71+
process. Returning `{ action: 'deny' }` cancels the window. Returning `{
72+
action: 'allow', overrideBrowserWindowOptions: { ... } }` will allow opening
73+
the window and setting the `BrowserWindowConstructorOptions` to be used when
74+
creating the window. Note that this is more powerful than passing options
75+
through the feature string, as the renderer has more limited privileges in
76+
deciding security preferences than the main process.
10577

10678
### Native `Window` example
10779

10880
```javascript
10981
// main.js
110-
const mainWindow = new BrowserWindow({
111-
webPreferences: {
112-
nativeWindowOpen: true
113-
}
114-
})
82+
const mainWindow = new BrowserWindow()
11583

11684
// In this example, only windows with the `about:blank` url will be created.
11785
// All other urls will be blocked.
@@ -135,3 +103,33 @@ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
135103
const childWindow = window.open('', 'modal')
136104
childWindow.document.write('<h1>Hello</h1>')
137105
```
106+
107+
### `BrowserWindowProxy` example
108+
109+
```javascript
110+
111+
// main.js
112+
const mainWindow = new BrowserWindow({
113+
webPreferences: { nativeWindowOpen: false }
114+
})
115+
116+
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
117+
if (url.startsWith('https://github.com/')) {
118+
return { action: 'allow' }
119+
}
120+
return { action: 'deny' }
121+
})
122+
123+
mainWindow.webContents.on('did-create-window', (childWindow) => {
124+
// For example...
125+
childWindow.webContents.on('will-navigate', (e) => {
126+
e.preventDefault()
127+
})
128+
})
129+
```
130+
131+
```javascript
132+
// renderer.js
133+
const windowProxy = window.open('https://github.com/', null, 'minimizable=false')
134+
windowProxy.postMessage('hi', '*')
135+
```

docs/breaking-changes.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ ensure your code works with this property enabled. It has been enabled by defau
2828

2929
You will be affected by this change if you use either `webFrame.executeJavaScript` or `webFrame.executeJavaScriptInIsolatedWorld`. You will need to ensure that values returned by either of those methods are supported by the [Context Bridge API](api/context-bridge.md#parameter--error--return-type-support) as these methods use the same value passing semantics.
3030

31+
### Default Changed: `nativeWindowOpen` defaults to `true`
32+
33+
Prior to Electron 14, `window.open` was by default shimmed to use
34+
`BrowserWindowProxy`. This meant that `window.open('about:blank')` did not work
35+
to open synchronously scriptable child windows, among other incompatibilities.
36+
`nativeWindowOpen` is no longer experimental, and is now the default.
37+
38+
See the documentation for [window.open in Electron](api/window-open.md)
39+
for more details.
40+
3141
## Planned Breaking API Changes (13.0)
3242

3343
### API Changed: `session.setPermissionCheckHandler(handler)`

shell/browser/web_contents_preferences.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ WebContentsPreferences::WebContentsPreferences(
141141
SetDefaultBoolIfUndefined(options::kDisableHtmlFullscreenWindowResize, false);
142142
SetDefaultBoolIfUndefined(options::kWebviewTag, false);
143143
SetDefaultBoolIfUndefined(options::kSandbox, false);
144-
SetDefaultBoolIfUndefined(options::kNativeWindowOpen, false);
144+
SetDefaultBoolIfUndefined(options::kNativeWindowOpen, true);
145145
SetDefaultBoolIfUndefined(options::kContextIsolation, true);
146146
SetDefaultBoolIfUndefined(options::kJavaScript, true);
147147
SetDefaultBoolIfUndefined(options::kImages, true);
@@ -474,7 +474,7 @@ void WebContentsPreferences::OverrideWebkitPrefs(
474474
GetPreloadPath(&prefs->preload);
475475

476476
// Check if nativeWindowOpen is enabled.
477-
prefs->native_window_open = IsEnabled(options::kNativeWindowOpen);
477+
prefs->native_window_open = IsEnabled(options::kNativeWindowOpen, true);
478478

479479
// Check if we have node integration specified.
480480
prefs->node_integration = IsEnabled(options::kNodeIntegration);

spec-main/api-browser-window-spec.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3075,19 +3075,7 @@ describe('BrowserWindow module', () => {
30753075
expect(url).to.equal('http://example.com/test');
30763076
expect(frameName).to.equal('');
30773077
expect(disposition).to.equal('foreground-tab');
3078-
expect(options).to.deep.equal({
3079-
show: true,
3080-
width: 800,
3081-
height: 600,
3082-
webPreferences: {
3083-
contextIsolation: true,
3084-
nodeIntegration: false,
3085-
webviewTag: false,
3086-
nodeIntegrationInSubFrames: false,
3087-
openerId: options.webPreferences!.openerId
3088-
},
3089-
webContents: undefined
3090-
});
3078+
expect(options).to.be.an('object').not.null();
30913079
expect(referrer.policy).to.equal('strict-origin-when-cross-origin');
30923080
expect(referrer.url).to.equal('');
30933081
expect(additionalFeatures).to.deep.equal([]);

spec-main/chromium-spec.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,9 @@ describe('chromium features', () => {
744744
}
745745

746746
it('disables node integration when it is disabled on the parent window for chrome devtools URLs', async () => {
747-
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
747+
// NB. webSecurity is disabled because native window.open() is not
748+
// allowed to load devtools:// URLs.
749+
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webSecurity: false } });
748750
w.loadURL('about:blank');
749751
w.webContents.executeJavaScript(`
750752
{ b = window.open('devtools://devtools/bundled/inspector.html', '', 'nodeIntegration=no,show=no'); null }
@@ -755,8 +757,8 @@ describe('chromium features', () => {
755757
});
756758

757759
it('disables JavaScript when it is disabled on the parent window', async () => {
758-
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
759-
w.webContents.loadURL('about:blank');
760+
const w = new BrowserWindow({ show: true, webPreferences: { nodeIntegration: true } });
761+
w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
760762
const windowUrl = require('url').format({
761763
pathname: `${fixturesPath}/pages/window-no-javascript.html`,
762764
protocol: 'file',
@@ -783,7 +785,7 @@ describe('chromium features', () => {
783785
targetURL = `file://${fixturesPath}/pages/base-page.html`;
784786
}
785787
const w = new BrowserWindow({ show: false });
786-
w.loadURL('about:blank');
788+
w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
787789
w.webContents.executeJavaScript(`{ b = window.open(${JSON.stringify(targetURL)}); null }`);
788790
const [, window] = await emittedOnce(app, 'browser-window-created');
789791
await emittedOnce(window.webContents, 'did-finish-load');
@@ -792,7 +794,7 @@ describe('chromium features', () => {
792794

793795
it('defines a window.location setter', async () => {
794796
const w = new BrowserWindow({ show: false });
795-
w.loadURL('about:blank');
797+
w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
796798
w.webContents.executeJavaScript('{ b = window.open("about:blank"); null }');
797799
const [, { webContents }] = await emittedOnce(app, 'browser-window-created');
798800
await emittedOnce(webContents, 'did-finish-load');
@@ -803,7 +805,7 @@ describe('chromium features', () => {
803805

804806
it('defines a window.location.href setter', async () => {
805807
const w = new BrowserWindow({ show: false });
806-
w.loadURL('about:blank');
808+
w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
807809
w.webContents.executeJavaScript('{ b = window.open("about:blank"); null }');
808810
const [, { webContents }] = await emittedOnce(app, 'browser-window-created');
809811
await emittedOnce(webContents, 'did-finish-load');
@@ -1035,7 +1037,7 @@ describe('chromium features', () => {
10351037
// We are testing whether context (3) can access context (2) under various conditions.
10361038

10371039
// This is context (1), the base window for the test.
1038-
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false } });
1040+
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false, nativeWindowOpen: false } });
10391041
await w.loadURL('about:blank');
10401042

10411043
const parentCode = `new Promise((resolve) => {

spec-main/fixtures/snapshots/proxy-window-open.snapshot.txt

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
[
33
"top=5,left=10,resizable=no",
44
{
5-
"sender": "[WebContents]",
6-
"frameId": 1,
7-
"processId": "placeholder-process-id"
5+
"sender": "[WebContents]"
86
},
97
"about:blank",
108
"frame-name",
@@ -20,10 +18,11 @@
2018
"y": 5,
2119
"webPreferences": {
2220
"contextIsolation": true,
21+
"nativeWindowOpen": true,
2322
"nodeIntegration": false,
2423
"webviewTag": false,
2524
"nodeIntegrationInSubFrames": false,
26-
"openerId": "placeholder-opener-id"
25+
"openerId": null
2726
},
2827
"webContents": "[WebContents]"
2928
},
@@ -37,9 +36,7 @@
3736
[
3837
"zoomFactor=2,resizable=0,x=0,y=10",
3938
{
40-
"sender": "[WebContents]",
41-
"frameId": 1,
42-
"processId": "placeholder-process-id"
39+
"sender": "[WebContents]"
4340
},
4441
"about:blank",
4542
"frame-name",
@@ -54,10 +51,11 @@
5451
"webPreferences": {
5552
"zoomFactor": "2",
5653
"contextIsolation": true,
54+
"nativeWindowOpen": true,
5755
"nodeIntegration": false,
5856
"webviewTag": false,
5957
"nodeIntegrationInSubFrames": false,
60-
"openerId": "placeholder-opener-id"
58+
"openerId": null
6159
},
6260
"webContents": "[WebContents]"
6361
},
@@ -71,9 +69,7 @@
7169
[
7270
"backgroundColor=gray,webPreferences=0,x=100,y=100",
7371
{
74-
"sender": "[WebContents]",
75-
"frameId": 1,
76-
"processId": "placeholder-process-id"
72+
"sender": "[WebContents]"
7773
},
7874
"about:blank",
7975
"frame-name",
@@ -85,10 +81,11 @@
8581
"backgroundColor": "gray",
8682
"webPreferences": {
8783
"contextIsolation": true,
84+
"nativeWindowOpen": true,
8885
"nodeIntegration": false,
8986
"webviewTag": false,
9087
"nodeIntegrationInSubFrames": false,
91-
"openerId": "placeholder-opener-id",
88+
"openerId": null,
9289
"backgroundColor": "gray"
9390
},
9491
"x": 100,
@@ -105,9 +102,7 @@
105102
[
106103
"x=50,y=20,title=sup",
107104
{
108-
"sender": "[WebContents]",
109-
"frameId": 1,
110-
"processId": "placeholder-process-id"
105+
"sender": "[WebContents]"
111106
},
112107
"about:blank",
113108
"frame-name",
@@ -121,10 +116,11 @@
121116
"title": "sup",
122117
"webPreferences": {
123118
"contextIsolation": true,
119+
"nativeWindowOpen": true,
124120
"nodeIntegration": false,
125121
"webviewTag": false,
126122
"nodeIntegrationInSubFrames": false,
127-
"openerId": "placeholder-opener-id"
123+
"openerId": null
128124
},
129125
"webContents": "[WebContents]"
130126
},
@@ -138,9 +134,7 @@
138134
[
139135
"show=false,top=1,left=1",
140136
{
141-
"sender": "[WebContents]",
142-
"frameId": 1,
143-
"processId": "placeholder-process-id"
137+
"sender": "[WebContents]"
144138
},
145139
"about:blank",
146140
"frame-name",
@@ -155,10 +149,11 @@
155149
"y": 1,
156150
"webPreferences": {
157151
"contextIsolation": true,
152+
"nativeWindowOpen": true,
158153
"nodeIntegration": false,
159154
"webviewTag": false,
160155
"nodeIntegrationInSubFrames": false,
161-
"openerId": "placeholder-opener-id"
156+
"openerId": null
162157
},
163158
"webContents": "[WebContents]"
164159
},

spec/static/main.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ app.whenReady().then(async function () {
109109
backgroundThrottling: false,
110110
nodeIntegration: true,
111111
webviewTag: true,
112-
contextIsolation: false
112+
contextIsolation: false,
113+
nativeWindowOpen: false
113114
}
114115
});
115116
window.loadFile('static/index.html', {

spec/webview-spec.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,8 @@ describe('<webview> tag', function () {
501501
describe('new-window event', () => {
502502
it('emits when window.open is called', async () => {
503503
loadWebView(webview, {
504-
src: `file://${fixtures}/pages/window-open.html`
504+
src: `file://${fixtures}/pages/window-open.html`,
505+
allowpopups: true
505506
});
506507
const { url, frameName } = await waitForEvent(webview, 'new-window');
507508

@@ -511,7 +512,8 @@ describe('<webview> tag', function () {
511512

512513
it('emits when link with target is called', async () => {
513514
loadWebView(webview, {
514-
src: `file://${fixtures}/pages/target-name.html`
515+
src: `file://${fixtures}/pages/target-name.html`,
516+
allowpopups: true
515517
});
516518
const { url, frameName } = await waitForEvent(webview, 'new-window');
517519

0 commit comments

Comments
 (0)