Skip to content

Commit 69f7e6b

Browse files
jayhesselberthJay Hesselberthhadley
authored
Implement light switch (#2338)
Opt-in "light switch" to allow the user to switch between light and dark modes. Fixes #1696. Co-authored-by: Jay Hesselberth <[email protected]> Co-authored-by: Hadley Wickham <[email protected]>
1 parent a70fcc7 commit 69f7e6b

File tree

12 files changed

+319
-50
lines changed

12 files changed

+319
-50
lines changed

Diff for: NEWS.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# pkgdown (development version)
22

3+
* New light switch makes it easy for users to switch between light and dark themes for the website (based on work in bslib by @gadenbuie). For now this behaviour is opt-in with `template.light-switch: true` but in the future we may turn it on automatically. See the customization vignette for details (#1696).
4+
* The search dropdown has been tweaked to look more like the other navbar menu items (#2338).
35
* `vignette("search")` has been removed since BS3 is deprecated and all the BS5 docs are also included in `build_search()` (#2564).
46
* YAML validation has been substantially improved so you should get much clearer errors if you have made a mistake (#1927). Please file an issue if you find a case where the error message is not helpful.
57
* `template_reference()` and `template_article()` now only add backticks to function names if needed (#2561).

Diff for: R/navbar-menu.R

+28-5
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22

33
# Helpers for use within pkgdown itself - these must stay the same as the
44
# yaml structure defined in vignette("customise")
5-
menu_submenu <- function(text, menu) {
5+
menu_submenu <- function(text, menu, icon = NULL, label = NULL, id = NULL) {
66
if (length(menu) == 0) {
77
return()
88
} else {
9-
list(text = text, menu = menu)
9+
purrr::compact(list(
10+
text = text,
11+
icon = icon,
12+
"aria-label" = label,
13+
id = id,
14+
menu = menu
15+
))
1016
}
1117
}
1218
menu_link <- function(text, href, target = NULL) {
@@ -15,6 +21,10 @@ menu_link <- function(text, href, target = NULL) {
1521
menu_links <- function(text, href) {
1622
purrr::map2(text, href, menu_link)
1723
}
24+
menu_theme <- function(text, icon, theme) {
25+
purrr::compact(list(text = text, theme = theme, icon = icon))
26+
}
27+
1828
menu_heading <- function(text, ...) list(text = text, ...)
1929
menu_separator <- function() list(text = "---------")
2030
menu_search <- function() list(search = list())
@@ -36,6 +46,8 @@ menu_type <- function(x, menu_depth = 0L) {
3646
"menu"
3747
} else if (!is.null(x$text) && grepl("^\\s*-{3,}\\s*$", x$text)) {
3848
"separator"
49+
} else if (!is.null(x$theme)) {
50+
"theme"
3951
} else if (!is.null(x$text) && is.null(x$href)) {
4052
"heading"
4153
} else if ((!is.null(x$text) || !is.null(x$icon)) && !is.null(x$href)) {
@@ -64,7 +76,8 @@ navbar_html <- function(x, path_depth = 0L, menu_depth = 0L, side = c("left", "r
6476
heading = navbar_html_heading(x),
6577
link = navbar_html_link(x, menu_depth = menu_depth),
6678
separator = navbar_html_separator(),
67-
search = navbar_html_search(x, path_depth = path_depth)
79+
search = navbar_html_search(x, path_depth = path_depth),
80+
theme = navbar_html_theme(x)
6881
)
6982

7083
class <- c(
@@ -86,7 +99,7 @@ navbar_html_list <- function(x, path_depth = 0L, menu_depth = 0L, side = "left")
8699
}
87100

88101
navbar_html_menu <- function(x, path_depth = 0L, menu_depth = 0L, side = "left") {
89-
id <- paste0("dropdown-", make_slug(x$text))
102+
id <- paste0("dropdown-", x$id %||% make_slug(x$text))
90103

91104
button <- html_tag("button",
92105
type = "button",
@@ -126,6 +139,16 @@ navbar_html_link <- function(x, menu_depth = 0) {
126139
)
127140
}
128141

142+
navbar_html_theme <- function(x) {
143+
html_tag(
144+
"button",
145+
class = "dropdown-item",
146+
"aria-label" = x$`aria-label`,
147+
"data-bs-theme-value" = x$theme,
148+
navbar_html_text(x)
149+
)
150+
}
151+
129152
navbar_html_heading <- function(x) {
130153
html_tag(
131154
"h6",
@@ -198,7 +221,7 @@ navbar_html_text <- function(x) {
198221

199222
icon <- html_tag("span", class = unique(c(iconset, classes)))
200223

201-
if (is.null(x$`aria-label`)) {
224+
if (is.null(x$`aria-label`) && is.null(x$text)) {
202225
cli::cli_inform(
203226
c(
204227
x = "Icon {.str {x$icon}} lacks an {.var aria-label}.",

Diff for: R/navbar.R

+30-8
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,26 @@ data_navbar <- function(pkg = ".", depth = 0L) {
22
pkg <- as_pkgdown(pkg)
33

44
navbar <- config_pluck(pkg, "navbar")
5-
6-
style <- navbar_style(
7-
navbar = navbar,
8-
theme = get_bslib_theme(pkg),
9-
bs_version = pkg$bs_version
10-
)
11-
5+
6+
if (uses_lightswitch(pkg)) {
7+
style <- NULL
8+
} else {
9+
style <- navbar_style(
10+
navbar = navbar,
11+
theme = get_bslib_theme(pkg),
12+
bs_version = pkg$bs_version
13+
)
14+
}
15+
1216
links <- navbar_links(pkg, depth = depth)
1317

1418
c(style, links)
1519
}
1620

21+
uses_lightswitch <- function(pkg) {
22+
config_pluck_bool(pkg, "template.light-switch", default = FALSE)
23+
}
24+
1725
# Default navbar ----------------------------------------------------------
1826

1927
navbar_style <- function(navbar = list(), theme = "_default", bs_version = 3) {
@@ -31,7 +39,7 @@ navbar_style <- function(navbar = list(), theme = "_default", bs_version = 3) {
3139
navbar_structure <- function() {
3240
print_yaml(list(
3341
left = c("intro", "reference", "articles", "tutorials", "news"),
34-
right = c("search", "github")
42+
right = c("search", "github", "lightswitch")
3543
))
3644
}
3745

@@ -125,6 +133,20 @@ navbar_components <- function(pkg = ".") {
125133
menu$search <- menu_search()
126134
}
127135

136+
if (uses_lightswitch(pkg)) {
137+
menu$lightswitch <- menu_submenu(
138+
text = NULL,
139+
icon = "fa-sun",
140+
label = tr_("Light switch"),
141+
id = "lightswitch",
142+
list(
143+
menu_theme(tr_("Light"), icon = "fa-sun", theme = "light"),
144+
menu_theme(tr_("Dark"), icon = "fa-moon", theme = "dark"),
145+
menu_theme(tr_("Auto"), icon = "fa-adjust", theme = "auto")
146+
)
147+
)
148+
}
149+
128150
if (!is.null(pkg$tutorials)) {
129151
menu$tutorials <- menu_submenu(
130152
tr_("Tutorials"),

Diff for: R/theme.R

+46-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
build_bslib <- function(pkg = ".") {
1+
build_bslib <- function(pkg = ".", call = caller_env()) {
22
pkg <- as_pkgdown(pkg)
3-
bs_theme <- bs_theme(pkg)
3+
bs_theme <- bs_theme(pkg, call = call)
44

55
cur_deps <- find_deps(pkg)
66
cur_digest <- purrr::map_chr(cur_deps, file_digest)
@@ -56,7 +56,7 @@ find_deps <- function(pkg) {
5656
}
5757
}
5858

59-
bs_theme <- function(pkg = ".") {
59+
bs_theme <- function(pkg = ".", call = caller_env()) {
6060
pkg <- as_pkgdown(pkg)
6161

6262
bs_theme_args <- pkg$meta$template$bslib %||% list()
@@ -71,26 +71,39 @@ bs_theme <- function(pkg = ".") {
7171
bs_theme <- bslib::bs_remove(bs_theme, "bs3compat")
7272

7373
# Add additional pkgdown rules
74-
rules <- bs_theme_rules(pkg)
74+
rules <- bs_theme_rules(pkg, call = call)
7575
files <- lapply(rules, sass::sass_file)
7676
bs_theme <- bslib::bs_add_rules(bs_theme, files)
7777

78+
# Add dark theme if needed
79+
if (uses_lightswitch(pkg)) {
80+
dark_theme <- config_pluck_string(pkg, "template.theme-dark", default = "arrow-dark")
81+
check_theme(
82+
dark_theme,
83+
error_pkg = pkg,
84+
error_path = "template.theme-dark",
85+
error_call = call
86+
)
87+
path <- highlight_path(dark_theme)
88+
css <- c('[data-bs-theme="dark"] {', read_lines(path), '}')
89+
bs_theme <- bslib::bs_add_rules(bs_theme, css)
90+
}
91+
7892
bs_theme
7993
}
8094

81-
bs_theme_rules <- function(pkg) {
95+
bs_theme_rules <- function(pkg, call = caller_env()) {
8296
paths <- path_pkgdown("BS5", "assets", "pkgdown.scss")
8397

8498
theme <- config_pluck_string(pkg, "template.theme", default = "arrow-light")
85-
theme_path <- path_pkgdown("highlight-styles", paste0(theme, ".scss"))
86-
if (!file_exists(theme_path)) {
87-
cli::cli_abort(c(
88-
"Unknown theme: {.val {theme}}",
89-
i = "Valid themes are: {.val highlight_styles()}"
90-
), call = caller_env())
91-
}
92-
paths <- c(paths, theme_path)
93-
99+
check_theme(
100+
theme,
101+
error_pkg = pkg,
102+
error_path = "template.theme",
103+
error_call = call
104+
)
105+
paths <- c(paths, highlight_path(theme))
106+
94107
package <- config_pluck_string(pkg, "template.package")
95108
if (!is.null(package)) {
96109
package_extra <- path_package_pkgdown("extra.scss", package, pkg$bs_version)
@@ -108,6 +121,25 @@ bs_theme_rules <- function(pkg) {
108121
paths
109122
}
110123

124+
check_theme <- function(theme,
125+
error_pkg,
126+
error_path,
127+
error_call = caller_env()) {
128+
129+
if (theme %in% highlight_styles()) {
130+
return()
131+
}
132+
config_abort(
133+
error_pkg,
134+
"{.field {error_path}} uses theme {.val {theme}}",
135+
call = error_call
136+
)
137+
}
138+
139+
highlight_path <- function(theme) {
140+
path_pkgdown("highlight-styles", paste0(theme, ".scss"))
141+
}
142+
111143
highlight_styles <- function() {
112144
paths <- dir_ls(path_pkgdown("highlight-styles"), glob = "*.scss")
113145
path_ext_remove(path_file(paths))

Diff for: inst/BS5/assets/pkgdown.js

+83
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,87 @@ async function searchFuse(query, callback) {
153153
});
154154
})(window.jQuery || window.$)
155155

156+
/*!
157+
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
158+
* Copyright 2011-2023 The Bootstrap Authors
159+
* Licensed under the Creative Commons Attribution 3.0 Unported License.
160+
* Updates for {pkgdown} by the {bslib} authors, also licensed under CC-BY-3.0.
161+
*/
162+
163+
const getStoredTheme = () => localStorage.getItem('theme')
164+
const setStoredTheme = theme => localStorage.setItem('theme', theme)
165+
166+
const getPreferredTheme = () => {
167+
const storedTheme = getStoredTheme()
168+
if (storedTheme) {
169+
return storedTheme
170+
}
171+
172+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
173+
}
174+
175+
const setTheme = theme => {
176+
if (theme === 'auto') {
177+
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
178+
} else {
179+
document.documentElement.setAttribute('data-bs-theme', theme)
180+
}
181+
}
182+
183+
function bsSetupThemeToggle () {
184+
'use strict'
185+
186+
const showActiveTheme = (theme, focus = false) => {
187+
var activeLabel, activeIcon;
188+
189+
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
190+
const buttonTheme = element.getAttribute('data-bs-theme-value')
191+
const isActive = buttonTheme == theme
192+
193+
element.classList.toggle('active', isActive)
194+
element.setAttribute('aria-pressed', isActive)
195+
196+
if (isActive) {
197+
activeLabel = element.textContent;
198+
activeIcon = element.querySelector('span').classList.value;
199+
}
200+
})
201+
202+
const themeSwitcher = document.querySelector('#dropdown-lightswitch')
203+
if (!themeSwitcher) {
204+
return
205+
}
206+
207+
themeSwitcher.setAttribute('aria-label', activeLabel)
208+
themeSwitcher.querySelector('span').classList.value = activeIcon;
209+
210+
if (focus) {
211+
themeSwitcher.focus()
212+
}
213+
}
214+
215+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
216+
const storedTheme = getStoredTheme()
217+
if (storedTheme !== 'light' && storedTheme !== 'dark') {
218+
setTheme(getPreferredTheme())
219+
}
220+
})
221+
222+
window.addEventListener('DOMContentLoaded', () => {
223+
showActiveTheme(getPreferredTheme())
224+
225+
document
226+
.querySelectorAll('[data-bs-theme-value]')
227+
.forEach(toggle => {
228+
toggle.addEventListener('click', () => {
229+
const theme = toggle.getAttribute('data-bs-theme-value')
230+
setTheme(theme)
231+
setStoredTheme(theme)
232+
showActiveTheme(theme, true)
233+
})
234+
})
235+
})
236+
}
156237

238+
setTheme(getPreferredTheme());
239+
bsSetupThemeToggle();

0 commit comments

Comments
 (0)