docs: update macos-dark-mode fiddle and guide content (#29198)

* update macos dark mode docs for Electron v12

* pr review fixes

* more pr review fixes

* reorg paragraphs for better flow

* Update docs/tutorial/dark-mode.md

Co-authored-by: Erick Zhao <erick@hotmail.ca>

* pr fixes

Co-authored-by: Erick Zhao <erick@hotmail.ca>
This commit is contained in:
Ethan Arrowood 2021-05-18 19:02:12 -06:00 committed by GitHub
parent adb85f341b
commit 5656493676
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 93 additions and 83 deletions

View file

@ -1,11 +1,12 @@
const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron') const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron')
const path = require('path')
function createWindow () { function createWindow () {
const win = new BrowserWindow({ const win = new BrowserWindow({
width: 800, width: 800,
height: 600, height: 600,
webPreferences: { webPreferences: {
nodeIntegration: true preload: path.join(__dirname, 'preload.js')
} }
}) })
@ -25,16 +26,18 @@ function createWindow () {
}) })
} }
app.whenReady().then(createWindow) app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
app.quit() app.quit()
} }
}) })
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})

View file

@ -0,0 +1,6 @@
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('darkMode', {
toggle: () => ipcRenderer.invoke('dark-mode:toggle'),
system: () => ipcRenderer.invoke('dark-mode:system')
})

View file

@ -1,11 +1,9 @@
const { ipcRenderer } = require('electron')
document.getElementById('toggle-dark-mode').addEventListener('click', async () => { document.getElementById('toggle-dark-mode').addEventListener('click', async () => {
const isDarkMode = await ipcRenderer.invoke('dark-mode:toggle') const isDarkMode = await window.darkMode.toggle()
document.getElementById('theme-source').innerHTML = isDarkMode ? 'Dark' : 'Light' document.getElementById('theme-source').innerHTML = isDarkMode ? 'Dark' : 'Light'
}) })
document.getElementById('reset-to-system').addEventListener('click', async () => { document.getElementById('reset-to-system').addEventListener('click', async () => {
await ipcRenderer.invoke('dark-mode:system') await window.darkMode.system()
document.getElementById('theme-source').innerHTML = 'System' document.getElementById('theme-source').innerHTML = 'System'
}) })

View file

@ -47,18 +47,18 @@ of this theming, due to the use of the macOS 10.14 SDK.
## Example ## Example
We'll start with a working application from the This example demonstrates an Electron application that derives its theme colors from the
[Quick Start Guide](quick-start.md) and add functionality gradually. `nativeTheme`. Additionally, it provides theme toggle and reset controls using IPC channels.
First, let's edit our interface so users can toggle between light and dark ```javascript fiddle='docs/fiddles/features/macos-dark-mode'
modes. This basic UI contains buttons to change the `nativeTheme.themeSource`
setting and a text element indicating which `themeSource` value is selected.
By default, Electron follows the system's dark mode preference, so we
will hardcode the theme source as "System".
Add the following lines to the `index.html` file: ```
```html ### How does this work?
Starting with the `index.html` file:
```html title='index.html'
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -80,65 +80,70 @@ Add the following lines to the `index.html` file:
</html> </html>
``` ```
Next, add [event listeners](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) And the `style.css` file:
that listen for `click` events on the toggle buttons. Because the `nativeTheme`
module only exposed in the Main process, you need to set up each listener's
callback to use IPC to send messages to and handle responses from the Main
process:
* when the "Toggle Dark Mode" button is clicked, we send the ```css title='style.css'
`dark-mode:toggle` message (event) to tell the Main process to trigger a theme @media (prefers-color-scheme: dark) {
change, and update the "Current Theme Source" label in the UI based on the body { background: #333; color: white; }
response from the Main process. }
* when the "Reset to System Theme" button is clicked, we send the
`dark-mode:system` message (event) to tell the Main process to use the system
color scheme, and update the "Current Theme Source" label to `System`.
To add listeners and handlers, add the following lines to the `renderer.js` file: @media (prefers-color-scheme: light) {
body { background: #ddd; color: black; }
}
```
```javascript The example renders an HTML page with a couple elements. The `<strong id="theme-source">`
const { ipcRenderer } = require('electron') element shows which theme is currently selected, and the two `<button>` elements are the
controls. The CSS file uses the [`prefers-color-scheme`][prefers-color-scheme] media query
to set the `<body>` element background and text colors.
The `preload.js` script adds a new API to the `window` object called `darkMode`. This API
exposes two IPC channels to the renderer process, `'dark-mode:toggle'` and `'dark-mode:system'`.
It also assigns two methods, `toggle` and `system`, which pass messages from the renderer to the
main process.
```js title='preload.js'
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('darkMode', {
toggle: () => ipcRenderer.invoke('dark-mode:toggle'),
system: () => ipcRenderer.invoke('dark-mode:system')
})
```
Now the renderer process can communicate with the main process securely and perform the necessary
mutations to the `nativeTheme` object.
The `renderer.js` file is responsible for controlling the `<button>` functionality.
```js title='renderer.js'
document.getElementById('toggle-dark-mode').addEventListener('click', async () => { document.getElementById('toggle-dark-mode').addEventListener('click', async () => {
const isDarkMode = await ipcRenderer.invoke('dark-mode:toggle') const isDarkMode = await window.darkMode.toggle()
document.getElementById('theme-source').innerHTML = isDarkMode ? 'Dark' : 'Light' document.getElementById('theme-source').innerHTML = isDarkMode ? 'Dark' : 'Light'
}) })
document.getElementById('reset-to-system').addEventListener('click', async () => { document.getElementById('reset-to-system').addEventListener('click', async () => {
await ipcRenderer.invoke('dark-mode:system') await window.darkMode.system()
document.getElementById('theme-source').innerHTML = 'System' document.getElementById('theme-source').innerHTML = 'System'
}) })
``` ```
If you run your code at this point, you'll see that your buttons don't do Using `addEventListener`, the `renderer.js` file adds `'click'` [event listeners][event-listeners]
anything just yet, and your Main process will output an error like this when to each button element. Each event listener handler makes calls to the respective `window.darkMode`
you click on your buttons: API methods.
`Error occurred in handler for 'dark-mode:toggle': No handler registered for 'dark-mode:toggle'`
This is expected — we haven't actually touched any `nativeTheme` code yet.
Now that we're done wiring the IPC from the Renderer's side, the next step Finally, the `main.js` file represents the main process and contains the actual `nativeTheme` API.
is to update the `main.js` file to handle events from the Renderer process.
Depending on the received event, we update the ```js
[`nativeTheme.themeSource`](../api/native-theme.md#nativethemethemesource)
property to apply the desired theme on the system's native UI elements
(e.g. context menus) and propagate the preferred color scheme to the Renderer
process:
* Upon receiving `dark-mode:toggle`, we check if the dark theme is currently
active using the `nativeTheme.shouldUseDarkColors` property, and set the
`themeSource` to the opposite theme.
* Upon receiving `dark-mode:system`, we reset the `themeSource` to `system`.
```javascript
const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron') const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron')
const path = require('path')
function createWindow () { function createWindow () {
const win = new BrowserWindow({ const win = new BrowserWindow({
width: 800, width: 800,
height: 600, height: 600,
webPreferences: { webPreferences: {
nodeIntegration: true preload: path.join(__dirname, 'preload.js')
} }
}) })
@ -154,44 +159,41 @@ function createWindow () {
}) })
ipcMain.handle('dark-mode:system', () => { ipcMain.handle('dark-mode:system', () => {
nativeTheme.themeSource = 'system' nativeTheme.themeSouce = 'system'
}) })
} }
app.whenReady().then(createWindow) app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
app.quit() app.quit()
} }
}) })
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
``` ```
The final step is to add a bit of styling to enable dark mode for the web parts The `ipcMain.handle` methods are how the main process responds to the click events from the buttons
of the UI by leveraging the [`prefers-color-scheme`][prefer-color-scheme] CSS on the HTML page.
attribute. The value of `prefers-color-scheme` will follow your
`nativeTheme.themeSource` setting.
Create a `styles.css` file and add the following lines: The `'dark-mode:toggle'` IPC channel handler method checks the `shouldUseDarkColors` boolean property,
sets the corresponding `themeSource`, and then returns the current `shouldUseDarkColors` property.
Looking back on the renderer process event listener for this IPC channel, the return value from this
handler is utilized to assign the correct text to the `<strong id='theme-source'>` element.
```css fiddle='docs/fiddles/features/macos-dark-mode' The `'dark-mode:system'` IPC channel handler method assigns the string `'system'` to the `themeSource`
@media (prefers-color-scheme: dark) { and returns nothing. This also corresponds with the relative renderer process event listener as the
body { background: #333; color: white; } method is awaited with no return value expected.
}
@media (prefers-color-scheme: light) { Run the example using Electron Fiddle and then click the "Toggle Dark Mode" button; the app should
body { background: #ddd; color: black; } start alternating between a light and dark background color.
}
```
After launching the Electron application, you can change modes or reset the
theme to system default by clicking corresponding buttons:
![Dark Mode](../images/dark_mode.gif) ![Dark Mode](../images/dark_mode.gif)
@ -199,4 +201,5 @@ theme to system default by clicking corresponding buttons:
[electron-forge]: https://www.electronforge.io/ [electron-forge]: https://www.electronforge.io/
[electron-packager]: https://github.com/electron/electron-packager [electron-packager]: https://github.com/electron/electron-packager
[packager-darwindarkmode-api]: https://electron.github.io/electron-packager/master/interfaces/electronpackager.options.html#darwindarkmodesupport [packager-darwindarkmode-api]: https://electron.github.io/electron-packager/master/interfaces/electronpackager.options.html#darwindarkmodesupport
[prefer-color-scheme]: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme [prefers-color-scheme]: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
[event-listeners]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener