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()