diff --git a/atom/browser/web_contents_preferences.cc b/atom/browser/web_contents_preferences.cc
index 6c706199fb9f..b2d8bc50855d 100644
--- a/atom/browser/web_contents_preferences.cc
+++ b/atom/browser/web_contents_preferences.cc
@@ -186,24 +186,23 @@ void WebContentsPreferences::AppendExtraCommandLineSwitches(
command_line->AppendSwitchASCII(::switches::kDisableBlinkFeatures,
disable_blink_features);
- // The initial visibility state.
- NativeWindow* window = NativeWindow::FromWebContents(web_contents);
-
- // Use embedder window for webviews
- if (guest_instance_id && !window) {
+ if (guest_instance_id) {
+ // Webview `document.visibilityState` tracks window visibility so we need
+ // to let it know if the window happens to be hidden right now.
auto manager = WebViewManager::GetWebViewManager(web_contents);
if (manager) {
auto embedder = manager->GetEmbedder(guest_instance_id);
- if (embedder)
- window = NativeWindow::FromWebContents(embedder);
+ if (embedder) {
+ auto window = NativeWindow::FromWebContents(embedder);
+ if (window) {
+ const bool visible = window->IsVisible() && !window->IsMinimized();
+ if (!visible) {
+ command_line->AppendSwitch(switches::kHiddenPage);
+ }
+ }
+ }
}
}
-
- if (window) {
- bool visible = window->IsVisible() && !window->IsMinimized();
- if (!visible) // Default state is visible.
- command_line->AppendSwitch(switches::kHiddenPage);
- }
}
bool WebContentsPreferences::IsPreferenceEnabled(
diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md
index 69407ec2a712..468cdd292575 100644
--- a/docs/api/browser-window.md
+++ b/docs/api/browser-window.md
@@ -97,6 +97,25 @@ child.once('ready-to-show', () => {
})
```
+### Page visibility
+
+The [Page Visibility API][page-visibility-api] works as follows:
+
+* On all platforms, the visibility state tracks whether the window is
+ hidden/minimized or not.
+* Additionally, on macOS, the visibility state also tracks the window
+ occlusion state. If the window is occluded (i.e. fully covered) by another
+ window, the visibility state will be `hidden`. On other platforms, the
+ visibility state will be `hidden` only when the window is minimized or
+ explicitly hidden with `win.hide()`.
+* If a `BrowserWindow` is created with `show: false`, the initial visibility
+ state will be `visible` despite the window actually being hidden.
+* If `backgroundThrottling` is disabled, the visibility state will remain
+ `visible` even if the window is minimized, occluded, or hidden.
+
+It is recommended that you pause expensive operations when the visibility
+state is `hidden` in order to minimize power consumption.
+
### Platform notices
* On macOS modal windows will be displayed as sheets attached to the parent window.
@@ -294,7 +313,8 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
* `minimumFontSize` Integer (optional) - Defaults to `0`.
* `defaultEncoding` String (optional) - Defaults to `ISO-8859-1`.
* `backgroundThrottling` Boolean (optional) - Whether to throttle animations and timers
- when the page becomes background. Defaults to `true`.
+ when the page becomes background. This also affects the
+ [Page Visibility API][#page-visibility]. Defaults to `true`.
* `offscreen` Boolean (optional) - Whether to enable offscreen rendering for the browser
window. Defaults to `false`. See the
[offscreen rendering tutorial](../tutorial/offscreen-rendering.md) for
@@ -1326,6 +1346,7 @@ removed in future Electron releases.
removed in future Electron releases.
[blink-feature-string]: https://cs.chromium.org/chromium/src/third_party/WebKit/Source/platform/RuntimeEnabledFeatures.json5?l=62
+[page-visibility-api]: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
[quick-look]: https://en.wikipedia.org/wiki/Quick_Look
[vibrancy-docs]: https://developer.apple.com/reference/appkit/nsvisualeffectview?language=objc
[window-levels]: https://developer.apple.com/reference/appkit/nswindow/1664726-window_levels
diff --git a/docs/api/webview-tag.md b/docs/api/webview-tag.md
index b13011fc2a72..0fcb0f8ad50b 100644
--- a/docs/api/webview-tag.md
+++ b/docs/api/webview-tag.md
@@ -60,12 +60,11 @@ container when used with traditional and flexbox layouts (since v0.36.11). Pleas
do not overwrite the default `display:flex;` CSS property, unless specifying
`display:inline-flex;` for inline layout.
-`webview` has issues being hidden using the `hidden` attribute or using `display: none;`.
-It can cause unusual rendering behaviour within its child `browserplugin` object
-and the web page is reloaded, when the `webview` is un-hidden, as opposed to just
-becoming visible again. The recommended approach is to hide the `webview` using
-CSS by zeroing the `width` & `height` and allowing the element to shrink to the 0px
-dimensions via `flex`.
+`webview` has issues being hidden using the `hidden` attribute or using
+`display: none;`. It can cause unusual rendering behaviour within its child
+`browserplugin` object and the web page is reloaded when the `webview` is
+un-hidden. The recommended approach is to hide the `webview` using
+`visibility: hidden`.
```html
```
diff --git a/lib/browser/api/browser-window.js b/lib/browser/api/browser-window.js
index c6931c854a06..2226f7f03aa2 100644
--- a/lib/browser/api/browser-window.js
+++ b/lib/browser/api/browser-window.js
@@ -109,7 +109,6 @@ BrowserWindow.prototype._init = function () {
if (isVisible !== newState) {
isVisible = newState
const visibilityState = isVisible ? 'visible' : 'hidden'
- this.webContents.send('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', visibilityState)
this.webContents.emit('-window-visibility-change', visibilityState)
}
}
diff --git a/lib/browser/guest-view-manager.js b/lib/browser/guest-view-manager.js
index 45244b1b4466..0bf1f6db7e81 100644
--- a/lib/browser/guest-view-manager.js
+++ b/lib/browser/guest-view-manager.js
@@ -281,7 +281,7 @@ const watchEmbedder = function (embedder) {
for (const guestInstanceId of Object.keys(guestInstances)) {
const guestInstance = guestInstances[guestInstanceId]
if (guestInstance.embedder === embedder) {
- guestInstance.guest.send('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', visibilityState)
+ guestInstance.guest.send('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', visibilityState)
}
}
}
diff --git a/lib/renderer/window-setup.js b/lib/renderer/window-setup.js
index b1187e96f18f..31767a290d4d 100644
--- a/lib/renderer/window-setup.js
+++ b/lib/renderer/window-setup.js
@@ -175,27 +175,35 @@ module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage, usesNative
}
})
- // The initial visibilityState.
- let cachedVisibilityState = hiddenPage ? 'hidden' : 'visible'
+ if (guestInstanceId != null) {
+ // Webview `document.visibilityState` tracks window visibility (and ignores
+ // the actual element visibility) for backwards compatibility.
+ // See discussion in #9178.
+ //
+ // Note that this results in duplicate visibilitychange events (since
+ // Chromium also fires them) and potentially incorrect visibility change.
+ // We should reconsider this decision for Electron 2.0.
+ let cachedVisibilityState = hiddenPage ? 'hidden' : 'visible'
- // Subscribe to visibilityState changes.
- ipcRenderer.on('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', function (event, visibilityState) {
- if (cachedVisibilityState !== visibilityState) {
- cachedVisibilityState = visibilityState
- document.dispatchEvent(new Event('visibilitychange'))
- }
- })
+ // Subscribe to visibilityState changes.
+ ipcRenderer.on('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', function (event, visibilityState) {
+ if (cachedVisibilityState !== visibilityState) {
+ cachedVisibilityState = visibilityState
+ document.dispatchEvent(new Event('visibilitychange'))
+ }
+ })
- // Make document.hidden and document.visibilityState return the correct value.
- defineProperty(document, 'hidden', {
- get: function () {
- return cachedVisibilityState !== 'visible'
- }
- })
+ // Make document.hidden and document.visibilityState return the correct value.
+ defineProperty(document, 'hidden', {
+ get: function () {
+ return cachedVisibilityState !== 'visible'
+ }
+ })
- defineProperty(document, 'visibilityState', {
- get: function () {
- return cachedVisibilityState
- }
- })
+ defineProperty(document, 'visibilityState', {
+ get: function () {
+ return cachedVisibilityState
+ }
+ })
+ }
}
diff --git a/spec/api-browser-window-spec.js b/spec/api-browser-window-spec.js
index e6e4661f7bbf..83da2d093c63 100644
--- a/spec/api-browser-window-spec.js
+++ b/spec/api-browser-window-spec.js
@@ -1412,6 +1412,164 @@ describe('BrowserWindow module', function () {
})
})
+ describe('document.visibilityState/hidden', function () {
+ beforeEach(function () {
+ w.destroy()
+ })
+
+ function onVisibilityChange (callback) {
+ ipcMain.on('pong', function (event, visibilityState, hidden) {
+ if (event.sender.id === w.webContents.id) {
+ callback(visibilityState, hidden)
+ }
+ })
+ }
+
+ function onNextVisibilityChange (callback) {
+ ipcMain.once('pong', function (event, visibilityState, hidden) {
+ if (event.sender.id === w.webContents.id) {
+ callback(visibilityState, hidden)
+ }
+ })
+ }
+
+ afterEach(function () {
+ ipcMain.removeAllListeners('pong')
+ })
+
+ it('visibilityState is initially visible despite window being hidden', function (done) {
+ w = new BrowserWindow({ show: false, width: 100, height: 100 })
+
+ let readyToShow = false
+ w.once('ready-to-show', function () {
+ readyToShow = true
+ })
+
+ onNextVisibilityChange(function (visibilityState, hidden) {
+ assert.equal(readyToShow, false)
+ assert.equal(visibilityState, 'visible')
+ assert.equal(hidden, false)
+
+ done()
+ })
+
+ w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
+ })
+
+ it('visibilityState changes when window is hidden', function (done) {
+ w = new BrowserWindow({width: 100, height: 100})
+
+ onNextVisibilityChange(function (visibilityState, hidden) {
+ assert.equal(visibilityState, 'visible')
+ assert.equal(hidden, false)
+
+ onNextVisibilityChange(function (visibilityState, hidden) {
+ assert.equal(visibilityState, 'hidden')
+ assert.equal(hidden, true)
+
+ done()
+ })
+
+ w.hide()
+ })
+
+ w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
+ })
+
+ it('visibilityState changes when window is shown', function (done) {
+ w = new BrowserWindow({width: 100, height: 100})
+
+ onNextVisibilityChange(function (visibilityState, hidden) {
+ onVisibilityChange(function (visibilityState, hidden) {
+ if (!hidden) {
+ assert.equal(visibilityState, 'visible')
+ done()
+ }
+ })
+
+ w.hide()
+ w.show()
+ })
+
+ w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
+ })
+
+ it('visibilityState changes when window is shown inactive', function (done) {
+ if (isCI && process.platform === 'win32') return done()
+
+ w = new BrowserWindow({width: 100, height: 100})
+
+ onNextVisibilityChange(function (visibilityState, hidden) {
+ onVisibilityChange(function (visibilityState, hidden) {
+ if (!hidden) {
+ assert.equal(visibilityState, 'visible')
+ done()
+ }
+ })
+
+ w.hide()
+ w.showInactive()
+ })
+
+ w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
+ })
+
+ it('visibilityState changes when window is minimized', function (done) {
+ if (isCI && process.platform === 'linux') return done()
+
+ w = new BrowserWindow({width: 100, height: 100})
+
+ onNextVisibilityChange(function (visibilityState, hidden) {
+ assert.equal(visibilityState, 'visible')
+ assert.equal(hidden, false)
+
+ onNextVisibilityChange(function (visibilityState, hidden) {
+ assert.equal(visibilityState, 'hidden')
+ assert.equal(hidden, true)
+
+ done()
+ })
+
+ w.minimize()
+ })
+
+ w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
+ })
+
+ it('visibilityState remains visible if backgroundThrottling is disabled', function (done) {
+ w = new BrowserWindow({
+ show: false,
+ width: 100,
+ height: 100,
+ webPreferences: {
+ backgroundThrottling: false
+ }
+ })
+
+ onNextVisibilityChange(function (visibilityState, hidden) {
+ assert.equal(visibilityState, 'visible')
+ assert.equal(hidden, false)
+
+ onNextVisibilityChange(function (visibilityState, hidden) {
+ done(new Error(`Unexpected visibility change event. visibilityState: ${visibilityState} hidden: ${hidden}`))
+ })
+ })
+
+ w.once('show', () => {
+ w.once('hide', () => {
+ w.once('show', () => {
+ done()
+ })
+ w.show()
+ })
+ w.hide()
+ })
+ w.show()
+
+ w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
+ })
+ })
+
describe('new-window event', function () {
if (isCI && process.platform === 'darwin') {
return
@@ -2318,9 +2476,7 @@ describe('BrowserWindow module', function () {
typeofArrayPush: 'number',
typeofFunctionApply: 'boolean',
typeofPreloadExecuteJavaScriptProperty: 'number',
- typeofOpenedWindow: 'object',
- documentHidden: true,
- documentVisibilityState: 'hidden'
+ typeofOpenedWindow: 'object'
}
}
diff --git a/spec/chromium-spec.js b/spec/chromium-spec.js
index f33fb284a47b..0e06668cdc9a 100644
--- a/spec/chromium-spec.js
+++ b/spec/chromium-spec.js
@@ -48,35 +48,6 @@ describe('chromium feature', function () {
})
})
- describe('document.hidden', function () {
- var url = 'file://' + fixtures + '/pages/document-hidden.html'
-
- it('is set correctly when window is not shown', function (done) {
- w = new BrowserWindow({
- show: false
- })
- w.webContents.once('ipc-message', function (event, args) {
- assert.deepEqual(args, ['hidden', true])
- done()
- })
- w.loadURL(url)
- })
-
- it('is set correctly when window is inactive', function (done) {
- if (isCI && process.platform === 'win32') return done()
-
- w = new BrowserWindow({
- show: false
- })
- w.webContents.once('ipc-message', function (event, args) {
- assert.deepEqual(args, ['hidden', false])
- done()
- })
- w.showInactive()
- w.loadURL(url)
- })
- })
-
xdescribe('navigator.webkitGetUserMedia', function () {
it('calls its callbacks', function (done) {
navigator.webkitGetUserMedia({
diff --git a/spec/fixtures/api/isolated.html b/spec/fixtures/api/isolated.html
index 25269b35e984..562bf01b7c10 100644
--- a/spec/fixtures/api/isolated.html
+++ b/spec/fixtures/api/isolated.html
@@ -19,9 +19,7 @@
typeofArrayPush: typeof Array.prototype.push,
typeofFunctionApply: typeof Function.prototype.apply,
typeofPreloadExecuteJavaScriptProperty: typeof window.preloadExecuteJavaScriptProperty,
- typeofOpenedWindow: typeof opened,
- documentHidden: document.hidden,
- documentVisibilityState: document.visibilityState
+ typeofOpenedWindow: typeof opened
}, '*')
diff --git a/spec/fixtures/pages/document-hidden.html b/spec/fixtures/pages/document-hidden.html
deleted file mode 100644
index 8b157e05549e..000000000000
--- a/spec/fixtures/pages/document-hidden.html
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/spec/webview-spec.js b/spec/webview-spec.js
index 51600d4711d3..f6aa3d18c5ea 100644
--- a/spec/webview-spec.js
+++ b/spec/webview-spec.js
@@ -6,7 +6,6 @@ const {ipcRenderer, remote} = require('electron')
const {app, session, getGuestWebContents, ipcMain, BrowserWindow, webContents} = remote
const {closeWindow} = require('./window-helpers')
-const isCI = remote.getGlobal('isCi')
const nativeModulesEnabled = remote.getGlobal('nativeModulesEnabled')
describe(' tag', function () {
@@ -485,9 +484,7 @@ describe(' tag', function () {
typeofArrayPush: 'number',
typeofFunctionApply: 'boolean',
typeofPreloadExecuteJavaScriptProperty: 'number',
- typeofOpenedWindow: 'object',
- documentHidden: isCI,
- documentVisibilityState: isCI ? 'hidden' : 'visible'
+ typeofOpenedWindow: 'object'
}
})
done()